// Beschreibung des Scriptes im Forum: // Dies ist ein Alarmanlagenscript mit erweiterter Funktion. Der Ansatz unterscheidet sich von dem anderer Loesungen. // Vorgehensweise ist zunaechst die Raumsituation zu beschreiben. Als naechstes werden dann ueber alle Raeume Sichten gelegt, die als drittes zu Scenarios zusammengefuegt werden // Das macht die Alarmanlage sehr flexibel und doch sind nur wenige Einstellungen im taeglichen Betrieb motwendig (bei richtiger Einstellung) // Autor Looxer01 06.05.2025 Version 1.0 (initiale Version) - Detaillierte Beschreibung im forum: https://forum.iobroker.net/topic/80967/vorlage-alarmanlage-mit-erweiterten-funktionen //----------------------------------------------------------------------------------------------------- //Einstellungen -- Einstellungen mit "BITTE ANPASSEN"-Vermerk muessen durchgearbeitet werden, damit das Script funktional ist //----------------------------------------------------------------------------------------------------- //PFADE---OPTIONAL------------------------------------------------------------------------------------------------------------------------------ // Beschreibung der Pfade // brauchen i.d. Regel nicht angepasst zu werden - koennen aber auf userdata umgestellt werden const path = "javascript.0.Alarm."; //const path = "0_userdata.0.Alarm."; // alternativ zum obigen javascript pfad // Basis-Pfad fuer die Controls und status (objektliste) const controlsPath = path + 'Controls.'; // Empfehlung - nicht aendern const statusPath = path + 'Status.'; // Empfehlung - nicht aendern //DEBUGGING----OPTIONAL----------------------------------------------------------------------------------------------------------------------------- const debugLevel = 1 ; // Empfehlung = 1 const SystemLog = false; // schreib das Sytemprotokoll in ein externes log (sollte normalerweise deaktviert sein nur bei Problemen verwenden) const PathSystemLog = "/opt/iobroker/log/Alarmscript.csv"; // Pfad und Dateiname des externen Logs //const PathSMLog = "/iobroker/log/Alarmscript.csv"; // Pfad des externen logs fuer Windows/ iobroker ist der angenommene iobroker home-pfad //EXTERNES PROKOLL SCHREIBEN IN CSV DATEI ----OPTIONAL----------------------------------------------------------------------------------------- const ProkollExtern = false const PathProkollExtern = "/opt/iobroker/log/AlarmMeldungen.csv"; // Pfad und Dateiname des externen Logs // const PathProkollExtern = "/iobroker/log/AlarmMeldungen.csv"; // Pfad fuer Windows/ iobroker ist der angenommene iobroker home-pfad //BESCHREIIUNG DER RAEUMLICHEN SITUATION-----BITTE ANPASSEN--------------------------------------------------------------------------------------------------------------------------- // hier ist eine Anpassung an die raeumlichen Gegebenheiten notwendig // nicht benoetigte Zeilen mit Geraetetypen (Rauch, Wasser etc) koennen geloescht weden // Achung die Eingaben sind alle case-Sensitiv // hier eine komplette Liste der implementierten Felder // Fuer Erweiterungen muessen Tabellen raumDefinition, FilterDefinition, ScenarioDefinition und Alarmtypes ensprechend angepasst werden /* Musterraum: (Dieser Name ist frei vergebbar) { Type: "innen", Position: "unten", (Typen koennen frei definiert werden) Gueltige GeraeteTypen sind: Bewegung: [], Fenster: [], Tueren: [], Rollladen: [], Glasbruch: [], Wasser: [], }, (hier werden Datenpunkte zugeordnet) */ const raumDefinition = { Wohnzimmer: { Type: "innen", Position: "oben", Fenster: [], Tueren: ['hm-rpc.1.00000000000000.1.STATE'], // Terrassentuere }, Kueche: { Type: "innen", Position: "oben", Fenster: ['hm-rpc.1.00000000000000.1.STATE','hm-rpc.1.00000000000000.1.STATE'], // Fenstersensor Kueche links und rechts }, Elternbad: { Type: "innen", Position: "oben", Fenster: ['hm-rpc.1.00000000000000.1.STATE'], // Fenstersensor Elternbad }, Schlafzimmer: { Type: "innen", Position: "oben", Fenster: ['hm-rpc.1.00000000000000.1.STATE'], // Fenstersensor Schlafzimmer }, GaesteWC: { Type: "innen", Position: "oben", Fenster: ['hm-rpc.1.00000000000000.1.STATE'], }, Flur_Oben: { Type: "innen", Position: "oben", Bewegung: [], Fenster: ['hm-rpc.1.00000000000000.1.STATE'], // Fenstersensor Flur oben Tueren: ['hm-rpc.1.00000000000000.1.LOCK_STATE'] // Keymatic }, Flur_Unten: { Type: "innen", Position: "unten", Bewegung: [], }, KiZi1: { Type: "innen", Position: "unten", Fenster: ['hm-rpc.1.00000000000000.1.STATE','hm-rpc.1.00000000000000.1.STATE','hm-rpc.1.00000000000000.1.STATE'], // Fenstersensor KiZi Nord links rechts UG }, KiZi2: { Type: "innen", Position: "unten", Fenster: ['hm-rpc.1.00000000000000.1.STATE','hm-rpc.1.00000000000000.1.STATE'], // Fenstersensor KiZi2 links rechts UG }, Kinderbad: { Type: "innen", Position: "unten", Fenster: ['hm-rpc.1.00000000000000.1.STATE'], // Fenstersensor Kinderbad Bewegung: ['hm-rpc.1.00000000000000.1.MOTION'], }, Hobbyraum: { Type: "innen", Position: "unten", Bewegung: ['hm-rpc.1.00000000000000.1.MOTION'], Tueren: ['hm-rpc.1.00000000000000.1.STATE'], }, Schwimmbad: { Type: "innen", Position: "unten", Bewegung: ['hm-rpc.1.00000000000000.1.MOTION'], Tueren: ['hm-rpc.1.00000000000000.1.STATE'], }, Vorkeller: { Type: "innen", Position: "unten", Bewegung: [], }, Keller: { Type: "innen", Position: "unten", Wasser: ['hm-rpc.1.00000000000000.1.ALARMSTATE'], Rauch: ['hm-rpc.1.00000000000000.1.SMOKE_DETECTOR_ALARM_STATUS'], // Status 0 = Idle / Status 1 = PRIMARY_ALARM }, Garage: { Type: "aussen", Position: "oben", Bewegung: [], Tueren: ['hm-rpc.1.00000000000000.1.MOTION'], // bei false ist geoeffnet }, Garagenhaus: { Type: "aussen", Position: "oben", Bewegung: ['hm-rpc.1.00000000000000.1.MOTION', 'hm-rpc.1.00000000000000.1.MOTION'], // Melder Garagenhaus }, Loggia: { Type: "aussen", Position: "unten", Bewegung: [], Rollladen: ['hm-rpc.0.HEQ0402562.1.LEVEL'], // Rolladen Loggia }, Aussen_Norden: { Type: "aussen", Position: "aussen", Bewegung: ['hm-rpc.0.MEQ1000000.1.MOTION'], // Melder Aussen Nordseite } }; //DFINITION DER FILTER DIE AUF DIE RAEUMLICHE SITUATION GELEGT WERDEN KOENNEN----FILTER WERDEN SCENARIOS ZUGEORDNET------BITTE ANPASSEN---------------------------------------------------------------------------------------------------------------------- // Eigene Scenarien koennen erstellt werden. RaumTypeen RaumPositionen, GeraeteTypen und raeume wirken wie filter auf die Raumdefinitionen // die Inhalte der Filter sind frei waehlbar : raumPosition: z. B. oben, unten oder ErsterStock,ZweiterStock,DritterStock etc. // Bei Eingabe von [] werden alle selektiert also z.B. raumTyp: [] = alle innen und aussen liegenden Raeume // raumType,raumPosition,gerateTypen,raeume sind schliessen alle entsprechenden Datenpunkte ein. Anschliessend koennen einzelne Datenpunkte aber wieder ausgeschlossen werden // moegliche keys sind raumTyp,raumPosition,geraeteTypen,raeume,Datenpunkte // ein vorangestelltes minus (-) bedeutet den Ausschluss dieses Keys. z.B. raeume '-Wohnzimmer' = Der Raum Wohnzimmer wird aus dem zugeordneten Scenario ausgeschlossen - oder Datenpukte '-hm-rpc.1.00000000000000.1.ALARMSTATE' // ein vorgestelltes doppelminus (--) bewirkt, dass das jeweilige Element (z.B. Raum Wohnzimmer ueber alle Scenarien ausgeschlossen wird) let FilterDefinition = { Schlafen: { raumTyp: ['innen'], // Raumtypen (z. B. innen, aussen) als Array oder keine Angabe = alle raumPosition: [], // Positionen (z. B. oben, unten oder ErsterStock,ZweiterStock,DritterStock) als Array oder keine Angabe = alle geraetetypen: [], // Geraetegruppen (z. B. Bewegung, Fenster, etc.) oder keine Angabe = alle raeume: [], // Einzelraeume bei keiner Angabe werden alle Raeume verwendet Datenpunkte: ['-hm-rpc.1.00000000000000.1.STATE','-hm-rpc.1.00000000000000.1.STATE'] // Schlafzimmerfenster und Elternbadfenster ausgeschlossen (Lueftung) }, Abwesend: { raumTyp: ['innen'], raumPosition: [], geraetetypen: [], raeume: [], Datenpunkte: [] }, HochRisiko: { raumTyp: [], raumPosition: [], geraetetypen: [], raeume: ['Kinderbad','Schwimmbad'], // nur diese Raeume sollen ausgewaehlt werden, da diese am hoechsten Einbruchsgefaehrdet sind Datenpunkte: [] }, Garage: { raumTyp: ['aussen'], raumPosition: [], geraetetypen: [], raeume: ['Garage','Garagenhaus'], Datenpunkte: [] }, Gaeste: { raumTyp: ['innen'], raumPosition: [], geraetetypen: [], raeume: ['--KiZi1','--KiZi2','--Kinderbad'], // bei doppelminus werden diese raeume global entfernt, auch wenn andere Scenarios/Filter diese einschliessen Datenpunkte: [] }, Haustiere: { raumTyp: [], raumPosition: [], geraetetypen: [], raeume: ['-Aussen_Norden'], Datenpunkte: ['-hm-rpc.1.00000000000000.1.MOTION'], // Melder Hobbyraum }, Urlaub: { raumTyp: ['innen'], raumPosition: [], geraetetypen: [], raeume: [], Datenpunkte: [] }, Wasser: { geraetetypen: ['Wasser'], }, Rauch: { geraetetypen: ['Rauch'], }, }; //DEFINTION DER SCENARIEN----BITTE ANPASSEN----------------------------------------------------------------------------------------------------------------------------- // Zuordnung von Filter zu Scenarien sowie Alerts zu Scenarios (Sirenen etc) // Scenario-Struktur muss in den keys Scenario01 - Scenario99 enthalten - andere Definitionen werden ignoriert // Scenarien koennen in den Datenpunkten z.B. ueber VIS aktiviert oder deaktiviert werden // Aktivierung mehrerer Scenarien ist moeglich // Die Namen der Scenarios koennen frei vergeben werden. // Filter sind in FilterDefinition definiert. Es koennen mehrere Filter eingetragen werden // Bei einem Alarmevent wird das passende Scenario welches die Alarme steuert (Alert)gesucht. Wenn ein Datenpunkt in mehreren Scenarios vorkommt, wird das Scenario mit der hoechsten Prio selektiert // Alerts steuern im Falle eines Alarms die AlertAusgabeDevices an und/oder die Gruppenschaltungen. Mehrere AusgabeDevices und/oder Gruppenschaltungen koennen eingetragen werden const scenarioDefinition = { Scenario01: { name: "Schlafen", Prio: 6, Filter: ['Schlafen'], Alert: ['FlashSchlafzimmerEIN',"FlashStandard1EIN"] }, Scenario02: { name: "Abwesend", Prio: 5, Filter: ["Abwesend"], Alert: ["SireneASIR2EIN","Licht_OG"] }, Scenario03: { name: "Garage", Prio: 10, Filter: ["Garage"], Alert: ['FlashSchlafzimmerEIN'] }, Scenario04: { name: "Urlaub", Prio: 5, Filter: ['Urlaub'], Alert: ["SireneASIR1EIN","SireneASIR2EIN"] }, Scenario05: { name: "Haustiere", Prio: 7, Filter: ['Haustiere'], Alert: [] }, Scenario06: { name: "Gaeste", Prio: 4, Filter: ["Gaeste"], Alert: ["FlashSchlafzimmerEIN"] }, Scenario07: { name: "frei", Prio: 8, Filter: [], Alert: [] }, Scenario08: { name: "Stoerung", Prio: 9, Filter: ['Stoerung'], Alert: [] }, // Filter ist bei Stoerung immer = Stoerung Scenario09: { name: "Gefaehrdet", Prio: 3, Filter: ['HochRisiko'], Alert: ["SireneASIR2EIN","Licht_Hoch_Risiko_Aussen"] }, Scenario10: { name: "Wasser", Prio: 2, Filter: ['Wasser'], Alert: ["SireneASIR2EIN"] }, Scenario11: { name: "Rauch", Prio: 1, Filter: ['Rauch'], Alert: ["SireneASIR1EIN","SireneASIR2EIN"] }, }; //DEFINTION DER AUSGABEGERAETE----BITTE ANPASSEN----------------------------------------------------------------------------------------------------------------------------- // verfuegbare devices sind: "HMIP-ASiR", "Datenpunkt_Standard" und "Datenpunkt_Flash"- abweichende Geraete muessen durch coding hinzugefuegt werden. Zentrale Routine ist: triggerAusgabe // es koennen beliebig viele Ausgabe-Keys eingestellt werden. In diesem Beispiel ist ein Ausgabe-Key = "SireneASIR1EIN". Das ist ein frei vergebarer Name // Funktionen: // HMIP-ASIR - zustand true schaltet das device ein Zustand false schaltet das device aus. // Datenpunkt_Standard - schaltet einen beliebigen Datenpunkt fuer die angegebene Dauer und stellt mit dem Rueckstellwert zurueck // Datenpunkt_Flash - kann z.B. ein Blinken einer Standardlampe bewirken. Frequenz und Haeufigkeit lassen sich eisntellen // AlertAusgabeDevices koennen mit der STILL-Funktion ausgeschaltet werden // Bitte beachten "Zustand" bestimmt einfach den Eintrag der fuer den Key relevant ist, entweder zum einschalten (zustand = true) oder ausschalten (zustand = false) Also SireneEin muss Zustand true haben const AlertAusgabeDevices = { SireneASIR1EIN:{ Device: "HMIP-ASIR", // Device darf nicht veraendert werden. Die Logik der nachfolgenden Felder wird anhand des Device abgearbeitet - hart verdrahtet Zustand: true, // true = ein - false = aus // Dieser Zustand wird nicht gesetzt, bedeutet Geraet ein-oder Ausschalten. Der zu setzende Wert wird weiter unten bestimmt DP: "hm-rpc.1.00000000000000.", AkustikSelectionDP: "3.ACOUSTIC_ALARM_SELECTION", valueTon: 2, Duration_UnitDP: "3.DURATION_UNIT", valueUnit: 0, // 0 = sekunde Duration_ValueDP: "3.DURATION_VALUE", valueDuration: 100, OpticalSelectionDP: "3.OPTICAL_ALARM_SELECTION" , valueOpt: 1, Startdelay: 1000, }, SireneASIR1AUS:{ Device: "HMIP-ASIR", // Device darf nicht veraendert werden. Die Logik der nachfolgenden Felder wird anhand des Device abgearbeitet - hart verdrahtet Zustand: false,// true = ein - false = aus // Dieser Zustand wird nicht gesetzt, bedeutet Geraet ein-oder Ausschalten. Der zu setzende Wert wird weiter unten bestimmt DP: "hm-rpc.1.00000000000000.", AkustikSelectionDP: "3.ACOUSTIC_ALARM_SELECTION", valueTon: 0, Duration_UnitDP: "3.DURATION_UNIT", valueUnit: 0, // 0 = sekunde Duration_ValueDP: "3.DURATION_VALUE", valueDuration: 0, OpticalSelectionDP: "3.OPTICAL_ALARM_SELECTION" , valueOpt: 0, Startdelay: 1000, }, SireneASIR2EIN:{ Device: "HMIP-ASIR", // Device darf nicht veraendert werden. Die Logik der nachfolgenden Felder wird anhand des Device abgearbeitet - hart verdrahtet Zustand: true,// true = ein - false = aus // Dieser Zustand wird nicht gesetzt, bedeutet Geraet ein-oder Ausschalten. Der zu setzende Wert wird weiter unten bestimmt DP: "hm-rpc.1.00000000000000.", AkustikSelectionDP: "3.ACOUSTIC_ALARM_SELECTION", valueTon: 2, Duration_UnitDP: "3.DURATION_UNIT", valueUnit: 0, // 0 = sekunde Duration_ValueDP: "3.DURATION_VALUE", valueDuration: 100, OpticalSelectionDP: "3.OPTICAL_ALARM_SELECTION" , valueOpt: 1, Startdelay: 1000 }, SireneASIR2AUS:{ Device: "HMIP-ASIR", // Device darf nicht veraendert werden. Die Logik der nachfolgenden Felder wird anhand des Device abgearbeitet - hart verdrahtet Zustand: false,// true = ein - false = aus // Dieser Zustand wird nicht gesetzt, bedeutet Geraet ein-oder Ausschalten. Der zu setzende Wert wird weiter unten bestimmt DP: "hm-rpc.1.00000000000000.", AkustikSelectionDP: "3.ACOUSTIC_ALARM_SELECTION", valueTon: 0, Duration_UnitDP: "3.DURATION_UNIT", valueUnit: 0, // 0 = sekunde Duration_ValueDP: "3.DURATION_VALUE", valueDuration: 0, OpticalSelectionDP: "3.OPTICAL_ALARM_SELECTION" , valueOpt: 0, Startdelay: 1000 }, Standard1Ein:{ Device: "Datenpunkt_Standard", // Device darf nicht veraendert werden. Die Logik der nachfolgenden Felder wird anhand des Device abgearbeitet - hart verdrahtet Zustand: true, // true = ein - false = aus // Dieser Zustand wird nicht gesetzt, bedeutet Geraet ein-oder Ausschalten. Der zu setzende Wert wird weiter unten bestimmt DP: "hm-rpc.1.00000000000000.3.STATE", /*Licht WZ Terrassentuere Ausgang:2 STATE*/ Aktivierungswert: true, // true/false 1/100 was auch immer gesetzt werden soll Rueckstellwert: false, // Rueckstellwert wird gesetzt wenn die Dauer abgelaufen ist. Keine Funktion wenn Dauer = null ist Dauer: 100000, // in ms - null ohne Auschaltung Startdelay: 0, // in ms }, Standard1Aus:{ Device: "Datenpunkt_Standard", // Device darf nicht veraendert werden. Die Logik der nachfolgenden Felder wird anhand des Device abgearbeitet - hart verdrahtet Zustand: false, // true = ein - false = aus // Dieser Zustand wird nicht gesetzt, bedeutet Geraet ein-oder Ausschalten. Der zu setzende Wert wird weiter unten bestimmt DP: "hm-rpc.1.00000000000000.3.STATE", /*Licht WZ Terrassentuere Ausgang:2 STATE*/ Aktivierungswert: false, // true/false 1/100 was auch immer gesetzt werden soll }, FlashStandard1EIN:{ Device: "Datenpunkt_Flash", // Device darf nicht veraendert werden. Die Logik der nachfolgenden Felder wird anhand des Device abgearbeitet - hart verdrahtet Zustand: true, // true = ein - false = aus // Dieser Zustand wird nicht gesetzt, bedeutet Geraet ein-oder Ausschalten. Der zu setzende Wert wird weiter unten bestimmt DP: "hm-rpc.1.00000000000000.3.STATE", /*Licht WZ Terrassentuere Ausgang:2 STATE*/ Aktivierungswert: true, // true/false 1/100 was auch immer gesetzt werden soll Wiederholung: true, AnzahlWiederholungen: 4, Frequenz: 1000, // achtung das sind millisekunden }, FlashStandard1AUS:{ Device: "Datenpunkt_Flash", // Device darf nicht veraendert werden. Die Logik der nachfolgenden Felder wird anhand des Device abgearbeitet - hart verdrahtet Zustand: false, // true = ein - false = aus // Dieser Zustand wird nicht gesetzt, bedeutet Geraet ein-oder Ausschalten. Der zu setzende Wert wird weiter unten bestimmt DP: "hm-rpc.1.00000000000000.3.STATE", /*Licht WZ Terrassentuere Ausgang:2 STATE*/ Aktivierungswert: false, // true/false 1/100 was auch immer gesetzt werden soll Wiederholung: false, AnzahlWiederholungen: 4, Frequenz: 1000, // achtung das sind millisekunden }, FlashSchlafzimmerEIN:{ Device: "Datenpunkt_Flash", // Device darf nicht veraendert werden. Die Logik der nachfolgenden Felder wird anhand des Device abgearbeitet - hart verdrahtet Zustand: true, // true = ein - false = aus // Dieser Zustand wird nicht gesetzt, bedeutet Geraet ein-oder Ausschalten. Der zu setzende Wert wird weiter unten bestimmt DP: "hm-rpc.1.00000000000000.3.STATE", /*Licht WZ Terrassentuere Ausgang:2 STATE*/ Aktivierungswert: true, // true/false 1/100 was auch immer gesetzt werden soll Wiederholung: true, AnzahlWiederholungen: 4, Frequenz: 1000, // achtung das sind millisekunden }, FlashSchlafzimmerAUS:{ Device: "Datenpunkt_Flash", // Device darf nicht veraendert werden. Die Logik der nachfolgenden Felder wird anhand des Device abgearbeitet - hart verdrahtet Zustand: false, // true = ein - false = aus // Dieser Zustand wird nicht gesetzt, bedeutet Geraet ein-oder Ausschalten. Der zu setzende Wert wird weiter unten bestimmt DP: "hm-rpc.1.00000000000000.3.STATE", /*Licht WZ Terrassentuere Ausgang:2 STATE*/ Aktivierungswert: false, // true/false oder 1/0 100/0 zulaessig Wiederholung: false, AnzahlWiederholungen: 4, Frequenz: 1000, // achtung das sind millisekunden }, } //DEFINTION DER SCHALTGRUPPEN----BITTE ANPASSEN----------------------------------------------------------------------------------------------------------------------------- // Schaltgruppen koennen frei definiert werden und dann Scenarios zugeordnet werden (Feld: Alert). z.B. Licht einschalten bei Alarm // dauer null = keine Auschaltzeit / delay und dauer = Zeitangaben in Sekunden / 50 ms = 0.05 sekunden 10 ms = 0.01 sekudnen // 200 ms = 0.2 ms 250 ms = 0.25 sekunden const Schaltgruppen = { Licht_OG: [ { Datenpunkt: 'hm-rpc.1.00000000000000.3.STATE', Wert: true, Delay: 0.1, Dauer: null }, // WZ Licht Terrasse Verzoegerung in sekunden // Dauer in sekunden // null = endlos { Datenpunkt: 'hm-rpc.1.00000000000000.4.LEVEL', Wert: 100, Delay: 0.1, Dauer: 600 } // WZ Esszimmertisch Dimmer// Feld Dauer: danach wird auf Ausgangswert zurueckgesetzt ], Licht_Hoch_Risiko_Aussen: [ { Datenpunkt: 'hm-rpc.1.00000000000000.1.STATE', Wert: true, Delay: 0.05, Dauer: 600 }, // Licht Kinderbad { Datenpunkt: 'hm-rpc.1.00000000000000.1.STATE', Wert: true, Delay: 0.1, Dauer: 600 }, // Licht aussen Nordseite { Datenpunkt: 'hm-rpc.1.00000000000000.1.STATE'/*Licht Hobbyraum Status:1 STATE*/, Wert: true, Delay: 150, Dauer: 600 }, { Datenpunkt: 'hm-rpc.1.00000000000000.10.STATE'/*Kelleraktor 5 Maschinenraum PoolHalle:10 STATE*/, Wert: true, Delay: 0.2, Dauer: 600 }, { Datenpunkt: 'hm-rpc.1.00000000000000.18.STATE'/*Keller Aktor 1 Aktor Licht Steinterasse:18 STATE*/, Wert: true, Delay: 0.25, Dauer: 600 }, ] }; //NACHRICHTEN VERSENDEN DEFINTION--------------------------------------------------------------------------------------------------------------------------------- // Messaging fuer Alarmsituationen // fuer alle Spalten mit true werden die Nachrichten ueber den zugeordneten Dienst versendet, vorausgesetzt der Messenge Adapter ist in iobroker installiert/konfiguriert const services = ['email', 'whatsApp', 'Signal', 'Telegram', 'Pushover', 'Pushsafer', 'Sprache',]; const MessengerScope = { 'Rauch': [true, true, false, false, false, false, false, ], 'Einbruch': [true, false, false, false, false, false, false, ], 'Wasser': [true, true, false, false, false, false, false, ], 'Sabotage': [false, false, false, false, false, false, false, ], 'Stoerung': [false, false, false, false, false, false, false, ], 'Alarm_Scharf': [true, false, false, false, false, false, false, ], 'Alarm_Unscharf': [true, false, false, false, false, false, false, ], 'ScenarioAktiviert': [true, false, false, false, false, false, false, ], 'ScenarioDeaktiviert': [true, false, false, false, false, false, false, ], 'Alarm_Still_Geschaltet': [false, false, false, false, false, false, false, ], 'Fallback': [false, false, false, false, false, false, false, ], // Fallback nicht loeschen. wird genutzt, wenn keine andere Definition gemacht worden ist } const MessengerInstanz = [0, 0, 0, 0, 0, 0, 0, ]; // Instanz des Messengers const TextTypeKurz = [false, true, true, true, true, true, true, ]; // bei true wird der Kurztext gesendet - sonst der Langtext // email-Einstellungen let emailAddresse = "Vorname-Nachname@web.de" // Empfaengeraddresse const Headline = "ioBroker Alarmmeldung"; // Ueberschrift Messages fuer email und Pushsafer // telegram Einstellungen const TelegramUser = ""; // Sprachnachricht ueber "sayit" - Sprache wird nur ausgegeben im angegebenen zeitrange wenn zeitvon = zeitbis dann ist die Gueltigkeit = immer bei zeitvon >zeitbis wird der naechste tag fuer zeitbis angenommen const zeitvon = "00:00" const zeitbis = "00:00" //ALARMAKTIVIERUNGSEINSTELLUNG-----OPTIONAL--------------------------------------------------------------------------------------------------------- const AKTIVIERUNGSDELAY_SECONDS = 5; // Wartezeit von Scharfstellung bis der Alarm imn System aktiv ist in Sekunden //AKTIONEN NACH SCHARFSCHALTUNG/UNSCHARFSCHALTUNG----OPTIONAL---------------------------------------------------------------------------------------------------------- const AktionenNachScharfSchaltung = [] // hier koennen die keys aus der Tabelle der Schaltgruppen eingetragen werden zB. ["Licht_OG","Licht_UG"] also mehrere bei Bedarf const AktionenNachUnscharfSchaltung = [] // hier koennen die keys aus der Tabelle der Schaltgruppen eingetragen werden zB. ["Licht_OG","Licht_UG"] also mehrere bei Bedarf // ANWESENHEIT---OPTIONAL---------------------------------------------------------------------------------------------------------------------- // Anwesenheitstracking. Falls eine Reaktion des Alarmscripts bei An/Abwesenheit gewuenscht ist // Anmerkung: Script Anwesenheitstracking funktioniert in Zusammenhang mit dem Script. Es koennen aber auch andere Tools genutzt werden const anwesenheit = 'javascript.0.AnwesenheitsTracking.JemandDa' // bitte anpassen falls Anwesenheitstracking genutzt wird // das hier angegebene scenario wird bei Abwesenheit aktiviert und bei Anwesenheit deaktiviert // es koennen mehrere Scenarios angegeben werden, ein fuehrendes minus dreht den wert um - aus true wird false (Ausschluss) const AbwesenheitScenarioAktivierung = ["Scenario02Aktiv"]; //GAESTE Management - OPTIONAL---------------------------------------------------------------------------------------------------------------------- // Die Funktion GAESTE Management wird nur aktiviert, wenn es den Datenpunkt zum Gaeste-Flag gibt const GaesteFlag = "javascript.0.Steuerungsflags.Gaeste" // das hier angegebene scenario wird bei Anwesenheit von Gaesten (Uebernachtung) aktiviert und deaktiviert const GaesteScenarioAktivierung = ["Scenario06Aktiv"]; // DATENPUNKTE DES SCRIPTES----NICHT AENDERN--------------------------------------------------------------------------------------------------------- // Dies sind die Haupt-Statusanzeigen und Steuerungen des Alarmssytems const DP_Alarmaktivierung = controlsPath + 'AlarmAktivierung' // Empfehlung: nicht aendern - Steuerung - Scharf/Unscharf const DP_Still = controlsPath + 'Still'; // Empfehlung: nicht aendern - Steuerung - wird der Datenpunkt auf true gesetzt werden laufende Sirenen etc ausgeschaltet const DP_Einbruchsmeldung = statusPath + 'Einbruchsalarm'; // Empfehlung: nicht aendern - Anzeige und Quittierung const DP_Wassermeldung = statusPath + 'Wasseralarm'; // Empfehlung: nicht aendern - Anzeige und Quittierung const DP_GeoeffnetMeldung = statusPath + 'Geoeffnet'; // Empfehlung: nicht aendern - Anzeige const DP_Rauchmeldung = statusPath + 'Rauchalarm'; // Empfehlung: nicht aendern - Anzeige und Quittierung const DP_Sabotagemeldung = statusPath + 'Sabotagealarm'; // Empfehlung: nicht aendern - Anzeige und Quittierung const DP_Stoerungsmeldung = statusPath + 'Stoerungsalarm'; // Empfehlung: nicht aendern - Anzeige const DP_Raumuebersicht = statusPath + 'Raumuebersicht'; // Empfehlung: nicht aendern - Anzeige // JSON DATENPUNKTE FUER DIE PROTOKOLLIERUNG----NICHT AENDERN----------------------------------------------------------------------------------------- // JSON fuer das Logging der alarmmeldungen. Kann in VIS eingebunden werden const id_JSON_Alarmmeldung_Aktuell = statusPath + "JSON_Alarmmeldung_Aktuell" // Empfehlung: nicht aendern const id_JSON_Alarmmeldung_Historie = statusPath + "JSON_Alarmmeldung_Historie" // Empfehlung: nicht aendern const id_Button_Refresh_Historie = controlsPath + "Button_Refresh_Historie" // Empfehlung: nicht aendern const id_History_VerbleibendeTage = controlsPath + "HistoryVerbleibendeTage" // Empfehlung: nicht aendern // HTML fuer das Logging der alarmmeldungen. Kann in VIS eingebunden werden const id_HTML_Alarmmeldung_Aktuell = statusPath + "HTML_Alarmmeldung_Aktuell" // Empfehlung: nicht aendern const id_HTML_Alarmmeldung_Historie = statusPath + "HTML_Alarmmeldung_Historie" // Empfehlung: nicht aendern //Wenn aus den JSON Datenpunkte HTML DATENPUNKTE GENERIERT WERDEN SOLL DANN TRUE------OPTONAL----------------------------------------------------------- // sinnvoll wenn inventwo nicht fuer die visualisierung genutzt werden kann const UpdateHTML_Datenpunkte = true // bei true werden die Datenpunkte automatisch angelegt und bei false wieder geloescht //REFESH DER JSON HISTORY - SCHEDULE----OPTONAL---------------------------------------------------------------------------------------------------- //Schedule Zeit fuer refresh der Historie const ScheduleAktiv = true; // Bei "false" wird der schedule nicht durchlaufen. Manuelles Loeschen kann ueber den Datenpunkt id_Button_Refresh_Historie (Button) moeglich const scheduleTimeClearSMTexte = "2 0 1 * *"; // am 1. tag des monats um 00:02 morgens sollen alle Alarmmeldungen des Monats geloescht werden id_History_VerbleibendeTage und aktive bleiben erhalten // const scheduleTimeClearSMTexte = "58 23 * * 0"; // alternative Sonntags um 23:58 Uhr sollen alle Alarmmeldungen der Woche im datenpunkt der Protokoll-Texte geloescht werden //SERVICEMELDUNGSSCRIPT VERKNUEPFUNG----OPTIONAL--------------------------------------------------------------------------------------------------- // Wer das Servicemeldungsscript nutzt kann damit verbinden und Sabotage oder Unreach als Alarm melden const SM_CountSabotage = 'javascript.0.ServicemeldungenVol2.Anzahl_SABOTAGE' // bitte Pfad ggf anpassen falls das Servicemeldungsscript genutzt wird const SM_CountUnreach = 'javascript.0.ServicemeldungenVol2.Anzahl_UNREACH' // bitte Pfad ggf anpassen falls das Servicemeldungsscript genutzt wird const SM_Meldungen_JSON = 'javascript.0.ServicemeldungenVol2.JSONAktuelleSM' // bitte Pfad ggf anpassen falls das Servicemeldungsscript genutzt wird // DEFINITION WIE EIN ALARM ERMITTELT WIRD--ANPASSEN FALS ERFORDERLICH------------------------------------------------------------------------------------------------ // die folgende Tabelle definiert die Ausloesungswerte fuer alarm: einfacher fall : true oder 1. Bei vorliegen von true wird ausgeloest // bei zutreffen nur eines einzigen wertes wird der Zustand des jeweiligen Datenpunktes prinzipiell als alarm verstanden // zulaessig: == > < >= <= != range exception // Exceptions sind datenpunkte deren einordnung als Alarmausloeswert nicht einfach abzuleiten ist. Hier koennen feste Werte je Datenpunkt vergeben werden // Beim Beschleunigungssensor der oft Garagentore ueberwacht wird die Ausloesung mit false signalisiert // Exceptions werden immer zuerst geprueft - eine Ueberschneidung mit den vorangegangen Werte (z.B. true) kann es also nicht geben const AlarmAusloesungswerte = [ { op: '==', wert: true }, // nur true zulaessig - bitte beibehalten { op: '>', wert: 0 }, // groesser als 0 - bitte beibehalten { range: { min: 1, max: 2 } }, // Beispiel range {exception: {dpAusnahme:'hm-rpc.1.00000000000000.1.MOTION', wert: false}}, // je Datenpunkt mit ausnahmen, z.B. fuer Garagentorsensor ist false = geoeffnet - Beispiel {exception: {dpAusnahme:"hm-rpc.1.00000000000000.1.LOCK_STATE", wert: 2}}, // hmip-dld unlocked = 2 ]; //DEFINTION DER STATUSBOARDS ELV SH SB8---NUR BEI VERWENDUNG DES STATUSBOARDS----OPTIONAL-------------------------------------------------------------------------------------------------------------------------- // im folgenden wird das Kontrollgeraet beschrieben und dessen Verknuepfung zu den Alarmzustaenden---> NUR FUER Geraete des typs ELV-SH-SB8 // der DP und die Zuordnung der Datenpunkte zu den LEDs koennen geaendert werden // Es koennen Meldungen ausgeschaltet werden - Alarm an aus - sowie Scenarios ein aus geschaltet werden const ControlDevices = [ { DeviceID: "StatusBoard1", Device: "ELV-SH-SB8", DP: "hm-rpc.1.00000000000000x.", LED1: DP_Alarmaktivierung, DP_2ndPart1 : "10.STATE", // Meldungsanzeige und Aktivierung/Deaktivierung erfolgt durch die entsprechenden Datenpunkte LED2: DP_Einbruchsmeldung, DP_2ndPart2 : "14.STATE", // manuellAenderbar true laesst es zu den Alarm ueber das Board auszuschalten // dann werden auch alle AkustikGeraete Sirenen ausgeschaltet LED3: DP_GeoeffnetMeldung, DP_2ndPart3 : "18.STATE", // DP und DP_2ndPart entspricht dem genauen Datenpunkt jeder LED LED4: DP_Sabotagemeldung, DP_2ndPart4 : "22.STATE", LED5: DP_Stoerungsmeldung, DP_2ndPart5 : "26.STATE", LED6: DP_Rauchmeldung, DP_2ndPart6 : "30.STATE", LED7: DP_Wassermeldung, DP_2ndPart7 : "34.STATE", // ausgeschaltet werden aber nicht die Schaltgruppen LED8: DP_Still, DP_2ndPart8 : "38.STATE", // laufende Alarmgeraete (Sirenen etc) werden ausgeschaltet }, { DeviceID: "StatusBoard2", Device: "ELV-SH-SB8", DP: "hm-rpc.1.00000000000000x.", LED1: "Scenario01Aktiv", DP_2ndPart1 : "10.STATE", // Szenarioschaltung fuer StatusBoard2 LED2: "Scenario02Aktiv", DP_2ndPart2 : "14.STATE", LED3: "Scenario03Aktiv", DP_2ndPart3 : "18.STATE", LED4: "Scenario03Aktiv", DP_2ndPart4 : "22.STATE", LED5: "Scenario05Aktiv", DP_2ndPart5 : "26.STATE", LED6: "Scenario06Aktiv", DP_2ndPart6 : "30.STATE", LED7: "Scenario07Aktiv", DP_2ndPart7 : "34.STATE", // Szenarioschaltung fuer StatusBoard2 LED8: "Scenario08Aktiv", DP_2ndPart8 : "38.STATE", } ]; //----------------------------------------------------------------------------------------------------- // Experten Einstellungen - ALLES OPTIONAL //----------------------------------------------------------------------------------------------------- const AnzahlMaximalerScenarien = 12 // entsprechende Anzahl von Datenpunkten wird angelegt // Texte const NichtRelevantText = "......" const MessageAlarmDeactivated = "Alarmanlage Unscharf" const MessageAlarmActivated = "Alarmanlage Scharf" const MessageScenarioAenderungAktiv = "Scenario wurde aktiviert" const MessageScenarioAenderungDeAktiv = "Scenario wurde deaktiviert" const MessageSTILLAktiviert = "Alarmausloesung mit STILL unterbrochen" const ServiceMeldungSabotageAutoConfirm = true; // Wenn die Sabotage aufgehoben wird, dann wird die der SabotageDatenpunkt automatisch zurueckgesetzt bei true const ServiceMMeldungStoerungAutoConfirm = true; // Wenn die Stoerung aufgehoben wird, dann wird die der StoerungsDatenpunkt automatisch zurueckgesetzt bei true // die folgende Tabelle enthaelt die Alarmtypes, // braucht in der Regel nicht eingestellt zu werden // alwaysOn heisst, dass ein Alarm ausgeloest wird auch wenn die Anlage unscharf geschaltet ist // Beim AlarmType Geoeffnet wird der Datenpunkt DP_GeoeffnetMeldung bei oeffnung einer der DeviceTypes auf true gesetzt const AlarmTypes = [ { alarmtype: 'Rauch', Datenpunkt: DP_Rauchmeldung, alwaysOn: true, Prio: 1, deviceTypes: ["Rauch"] }, { alarmtype: 'Einbruch', Datenpunkt: DP_Einbruchsmeldung, alwaysOn: false, Prio: 2, deviceTypes: ["Fenster","Tueren","Bewegung","Glasbruch","Sabotage"] }, { alarmtype: 'Wasser', Datenpunkt: DP_Wassermeldung, alwaysOn: true, Prio: 3, deviceTypes: ["Wasser"] }, { alarmtype: 'Stoerung', Datenpunkt: DP_Stoerungsmeldung, alwaysOn: false, Prio: 4, deviceTypes: ["Stoerung"] }, { alarmtype: 'Geoeffnet',Datenpunkt: DP_GeoeffnetMeldung, alwaysOn: false, Prio: 5, deviceTypes: ["Fenster","Tueren","Rollladen"] }, // Rollladen werden nicht als Einbrauch interpretiert -Falls gewuenscht, dann Einbruch zuordnen]; ] // Struktur nicht aendern // GeraeteID ID ist der unique identifier, Alarmfeld ist der Wert des Feldes, dass den Alarm ausloest const StrukturDefinition = [ { Adapter: 'hm-rpc', GeraeteID: 2, AlarmFeld: 4, nativeType: 3, common_name: 3 }, // die Ziffer ist die Positinierung des Feldes 'hm-rpc.1.00000000000000.0.UPDATE_PENDING_ALARM' 0=Adapter - 2 = ID 4= Feld Alarm / native Type = die ersten Strings bis zur dritten STelle fuer getObject { Adapter: 'hmip', GeraeteID: 3, AlarmFeld: 6, nativeType: 'hmip.xinstancex.devices.xidx.info.modelType', common_name: 'hmip.xinstancex.devices.xidx.info.label' }, // Positionierung wie bei Rm-rpc - bei hmip wird native type aber ueber den DP ausgelesen und nicht getObject "xidx" wird dann mit der geraeteiD ersetzt ] // Umlaut Umwandlung und entfernung PUnkte - kann aber auch erweitert werden const replacements = { '.': ' ', 'ä': 'ae', 'ü': 'ue', 'ö': 'oe', 'ß': 'ss', 'WZ':'Wohnzimmer'}; // Umwandlung fuer Namen der Geraete (common.name) // im Folgenden werden die Bedingungen fuer die Protokollierung der History Alarmmeldungen in den JSON Tabellen beschrieben. (Die Aktuellen Meldungen werden immer erzeugt) // Dabei werden Einbruch, Wasser Rauch immer in der JSON_Aktuell protokolliert // Alle anderen werden bei true im Feld Protokoll_Schreiben in der Histore protokolliert bzw nicht protokolliert bei false // null heisst, dass diese Situation nur bei Scharfem Alarm in die Historie geschrieben wird. Bei Prokoll_Schreiben = false aber bei scharfem Alarm nicht const JSON_Protokollierung = [ { Typ: "Einbruch", Aktiv: true, Protokoll_Schreiben: true, NurWennAlarmScharf: null }, // wird generell nur protollierrt bei Alarm scharf { Typ: "Wasser", Aktiv: true, Protokoll_Schreiben: true, NurWennAlarmScharf: null }, // wird generell nur protollierrt bei Alarm scharf { Typ: "Rauch", Aktiv: true, Protokoll_Schreiben: true, NurWennAlarmScharf: null }, // wird generell nur protollierrt bei Alarm scharf { Typ: "Sabotage", Aktiv: true, Protokoll_Schreiben: true, NurWennAlarmScharf: null }, // wird generell nur protollierrt bei Alarm scharf { Typ: "Stoerung", Aktiv: true, Protokoll_Schreiben: true, NurWennAlarmScharf: null }, // wird generell nur protollierrt bei Alarm scharf { Typ: "AlarmAktivierung", Aktiv: true, Protokoll_Schreiben: true, NurWennAlarmScharf: false }, { Typ: "ScenarioAenderung", Aktiv: true, Protokoll_Schreiben: true, NurWennAlarmScharf: false }, { Typ: "STILL", Aktiv: true, Protokoll_Schreiben: true, NurWennAlarmScharf: false }, { Typ: "Einbruch", Aktiv: false, Protokoll_Schreiben: true, NurWennAlarmScharf: false }, { Typ: "Wasser", Aktiv: false, Protokoll_Schreiben: true, NurWennAlarmScharf: false }, { Typ: "Rauch", Aktiv: false, Protokoll_Schreiben: true, NurWennAlarmScharf: false }, { Typ: "Sabotage", Aktiv: false, Protokoll_Schreiben: true, NurWennAlarmScharf: false }, { Typ: "Stoerung", Aktiv: false, Protokoll_Schreiben: true, NurWennAlarmScharf: false }, { Typ: "AlarmAktivierung", Aktiv: false, Protokoll_Schreiben: true, NurWennAlarmScharf: false }, { Typ: "ScenarioAenderung", Aktiv: false, Protokoll_Schreiben: true, NurWennAlarmScharf: false }, { Typ: "STILL", Aktiv: false, Protokoll_Schreiben: false, NurWennAlarmScharf: false }, ]; //DEFINITION VON SPALTENBREITEN UND STYLES FUER DIE NUTZUNG VON HTML DATENPUNKTEN FUER DIE DARSTELLUNG DES PROTOKOLLS----OPTIONAL---------------- // // Hauptkonfigurationsobjekt fuer die Tabellendarstellung const HTML_TableWidthsAndStyles = { // Definition der Spaltenbreiten in CSS-Einheiten columnWidths: { datum: '95px', // Breite fuer Datum/Uhrzeit-Spalte alarmtype: '70px', // Breite fuer Alarmtyp-Spalte raum: '90px', // Breite fuer Raum-Spalte deviceType: '80px', // Breite fuer Geraetetyp-Spalte scenario: '50px', // Breite fuer Szenario-Spalte deviceName: '150px', // Breite fuer Geraetebezeichnung deviceId: '50px', // Breite fuer Geraete-ID message: '210px', // Breite fuer Nachrichtenspalte quittiert: '95px' // Breite fuer Quittierungsspalte }, // Visuelle Stileinstellungen fuer die Tabelle styles: { // Basistypografie fontSize: '10px', // Allgemeine Schriftgroesse // Kopfzeilenstil headerColor: '#333333', // Hintergrundfarbe der Kopfzeile headerTextColor: 'white', // Textfarbe der Kopfzeile // Zeilenstile (Zebra-Pattern) evenRowColor: '#4e5049', // Hintergrundfarbe gerade Zeilen oddRowColor: '#333333', // Hintergrundfarbe ungerade Zeilen evenRowTextColor: 'white', // Textfarbe gerade Zeilen oddRowTextColor: 'white', // Textfarbe ungerade Zeilen // Rahmeneinstellungen borderColor: '#000000', // Farbe der Tabellenraender borderWidth: '1px', // Staerke der Rahmenlinien useBorders: true, // Soll Rahmen anzeigen? (true/false) // Zellenlayout cellPadding: '5px' // Innenabstand der Tabellenzellen }, // Texte fuer die Tabellenkopfzeilen headerTexts: { datum: 'Datum/Uhrzeit', // ueberschrift Datumsspalte alarmtype: 'Alarmtyp', // ueberschrift Alarmtyp raum: 'Raum', // ueberschrift Raum deviceType: 'Geraetetyp', // ueberschrift Geraetetyp scenario: 'Szenario', // ueberschrift Szenario deviceName: 'Geraetebez.', // ueberschrift Geraetebezeichnung (abgekuerzt) deviceId: 'Geraete-ID', // ueberschrift Geraete-ID message: 'Nachricht', // ueberschrift Nachrichteninhalt quittiert: 'Quittiert seit' // ueberschrift Quittierungsstatus }, // Konfiguration der Spaltensichtbarkeit - ein oder ausblenden columns: { datum: { visible: true }, alarmtype: { visible: true }, raum: { visible: true }, deviceType: { visible: true }, scenario: { visible: true }, deviceName: { visible: true }, deviceId: { visible: true }, message: { visible: true }, quittiert: { visible: true } } }; // Konfiguration der HTML fuer die Raumuebersicht const HTML_Raumzuordnungen_WidthAndStyles = { columnWidths: { raum: '90px', scenario: '150px', deviceType: '100px', }, styles: { fontSize: '10px', headerColor: '#333333', headerTextColor: 'white', evenRowColor: '#4e5049', oddRowColor: '#333333', evenRowTextColor: 'white', oddRowTextColor: 'white', borderColor: '#000000', borderWidth: '1px', useBorders: true, cellPadding: '5px' }, headerTexts: { raum: 'Raum', scenario: 'Szenarien', deviceType: 'Geraetetyp' }, columns: {raum: { visible: true },scenario: { visible: true }, deviceType: { visible: true } } }; //----------------------------------------------------------------------------------------------------- // Ende Einstellungen //----------------------------------------------------------------------------------------------------- // Globale Routinen LOG(`Routine wird ausgefuehrt`, "Ablauf", "Start", 2); let MessageSendCollector = [], GeneratedSubscriptions = [] , AktuelleMeldungenJSON = [], HistorischeMeldungenJSON = []; let AlarmAktivierungsStatus = false; // Aufbau AlarmScenarios - Das sind die Datenpunkte die in der iobroker objektstuktur angelegt werden // Scenario01Aktiv = false // .... // Scenario10Aktiv = false --etc. const AlarmScenarios = [ ...Array.from({ length: AnzahlMaximalerScenarien }, (_, i) => { const ScenarioNum = String(i + 1).padStart(2, '0'); // Fuehrende Null hinzufuegen return { Control: `Scenario${ScenarioNum}Aktiv`, Datenpunkt: path + `Controls.Scenario${ScenarioNum}Aktiv` }; }), ]; // Aufbau ScenarioStatus // Scenario01: status: false // Scenario02: status: false - etc. const ScenarioStatus = Object.fromEntries( Array.from({ length: AnzahlMaximalerScenarien }, (_, i) => { const ScenarioNum = String(i + 1).padStart(2, '0'); // Fuehrende Null hinzufuegen return [`Scenario${ScenarioNum}`, { status: false }]; }) ); let lastGeoeffnetUpdate = 0; // timer fuer Fenster, Tueren, Rolladen let lastLEDUpdates = {}; // Speichert die letzten Update-Zeiten fuer jede LED im Statusboard falls verwendet let activationTimeout = null; // Globale Variable zur Speicherung des Timeouts fuer Alarmaktivierung - sorgt dafuer, dass eine laufende Alarmaktivierung abgebrochen werden kann // Globales Objekt, das die laufenden Intervalle speichert wird fuer die Funktion Execute_Still benoetigt const blinkingIntervals = {}; // nur fuer Standard_Flash Geraete CheckForNaNinFilterDefinition(FilterDefinition) // Ueberpruefung ob falsche Eingaben in den FilterDefinitions gemacht wurden (ausserhalb der Hochkomma) evt abbruch let ScenarioToDPsTable = {}; // Enthaelt die Datenpunkte die subscribed sind und die Scenarios aus denen sie stammen // innerhalb von Create States werden updateScenario(), CreateSubscriptions() , createAktivierungsSubscription() und ListSystemSubscriptions() aufgerufen CreateStates() if(!existsState(DP_Alarmaktivierung)) { AlarmAktivierungsStatus = false }else{ AlarmAktivierungsStatus = getState(DP_Alarmaktivierung).val } createSubscriptionsfuerControlDevices() // erstellt die subscriptions fuer evt StatusBoards syncControlDevicesInitial() //----------------------------------------------------------------------------------------------------- // Schedule zum Loeschen des Datenpunktwertes der Histore einmal pro Monat oder Woche je nach schedule-Einstellung // die aktiven messages bleien erhaltebn - // Anzahl der zu verbleibenden Tage des Protokolls kann in der VIS eingestellt werden - Datenpunkt: id_History_VerbleibendeTage //----------------------------------------------------------------------------------------------------- if(ScheduleAktiv ) { schedule(scheduleTimeClearSMTexte, function() { LOG(`Schedule zum refresh der Historie wurde ausgeloest - Historie is nun geloescht`, "Schedule", "ScheduleAktiv", 0); Refresh_History() }); } //----------------------------------------------------------------------------------------------------- // Function subscribeToAllScenarioDPs Die Funktion abonniert alle Datenpunkte aus Tabelle Raumdefinition in Verbindung mit den Scenarios // die aktivierten Scenarion aus Tabelle scenarioDefinition sind ausgangspunkt // ScenarioToDPsTable = {Standard_Schlafen: [datenpunkt1, datenpunkt2 etc]} //----------------------------------------------------------------------------------------------------- function subscribeToAllScenarioDPs() { LOG(`Routine wird ausgefuehrt`, "Ablauf", "subscribeToAllScenarioDPs", 2); unsubscribeAll(); ScenarioToDPsTable = {}; // Globale Ausschlusslisten fuer alle Kategorien const globalExclusions = { raumTyp: new Set(), raumPosition: new Set(), geraetetypen: new Set(), raeume: new Set(), datenpunkte: new Set() }; // Phase 1: Globale Ausschluesse sammeln for (const scenarioKey in scenarioDefinition) { if (!ScenarioStatus[scenarioKey]?.status) continue; const scenario = scenarioDefinition[scenarioKey]; const Filter = scenario.Filter || []; for (const Filterbedingungen of Filter) { const filter = FilterDefinition[Filterbedingungen]; if (!filter) continue; // Alle Filterkategorien durchgehen for (const category in globalExclusions) { if (filter[category]) { filter[category].forEach(item => { if (item.startsWith('--')) { globalExclusions[category].add(item.slice(2)); } }); } } } } // Phase 2: Normale Verarbeitung mit Beruecksichtigung der globalen Ausschluesse for (const scenarioKey in scenarioDefinition) { if (!ScenarioStatus[scenarioKey]?.status) continue; const scenario = scenarioDefinition[scenarioKey]; const Filter = scenario.Filter || []; if (!ScenarioToDPsTable[scenarioKey]) ScenarioToDPsTable[scenarioKey] = []; for (const Filterbedingungen of Filter) { const filter = FilterDefinition[Filterbedingungen]; if (!filter) continue; const getFilterInfo = (arr, globalExcludeSet) => { return { include: (arr || []).filter(e => !e.startsWith('-')), exclude: (arr || []).filter(e => e.startsWith('-') && !e.startsWith('--')).map(e => e.slice(1)), globalExclude: globalExcludeSet }; }; const filters = { raumTyp: getFilterInfo(filter.raumTyp, globalExclusions.raumTyp), raumPosition: getFilterInfo(filter.raumPosition, globalExclusions.raumPosition), geraetetypen: getFilterInfo(filter.geraetetypen, globalExclusions.geraetetypen), raeume: getFilterInfo(filter.raeume, globalExclusions.raeume), datenpunkte: getFilterInfo(filter.Datenpunkte, globalExclusions.datenpunkte) }; // Raeume filtern mit allen Ausschluessen const relevanteRaeume = Object.keys(raumDefinition).filter(raumName => { const raum = raumDefinition[raumName]; // Globale Raumausschluesse if (filters.raeume.globalExclude.has(raumName)) return false; // Normale Filter if (filters.raeume.exclude.includes(raumName)) return false; if (filters.raeume.include.length > 0 && !filters.raeume.include.includes(raumName)) return false; // RaumTyp Filter if (filters.raumTyp.globalExclude.has(raum.Type)) return false; if (filters.raumTyp.include.length > 0 && !filters.raumTyp.include.includes(raum.Type)) return false; if (filters.raumTyp.exclude.includes(raum.Type)) return false; // Position Filter if (filters.raumPosition.globalExclude.has(raum.Position)) return false; if (filters.raumPosition.include.length > 0 && !filters.raumPosition.include.includes(raum.Position)) return false; if (filters.raumPosition.exclude.includes(raum.Position)) return false; return true; }); // Datenpunkte sammeln - NEUE LOGIK const datenpunkteInSzenario = new Set(); // 1. Nur Datenpunkte aus relevanten Raeumen hinzufuegen, wenn keine expliziten Datenpunkte angegeben sind if (filters.datenpunkte.include.length === 0) { for (const raumName of relevanteRaeume) { const raum = raumDefinition[raumName]; for (const typ in raum) { if (typ === "Type" || typ === "Position") continue; // Geraetetyp Filter if (filters.geraetetypen.globalExclude.has(typ)) continue; if (filters.geraetetypen.include.length > 0 && !filters.geraetetypen.include.includes(typ)) continue; if (filters.geraetetypen.exclude.includes(typ)) continue; const dpList = raum[typ]; if (!Array.isArray(dpList)) continue; for (const dp of dpList) { if (!filters.datenpunkte.globalExclude.has(dp)) { datenpunkteInSzenario.add(dp); } } } } } // 2. Explizit angegebene Datenpunkte hinzufuegen (ohne Minus) filters.datenpunkte.include.forEach(dp => { if (!filters.datenpunkte.globalExclude.has(dp)) { datenpunkteInSzenario.add(dp); } }); // 3. Explizit ausgeschlossene Datenpunkte entfernen (mit einfachem Minus) filters.datenpunkte.exclude.forEach(dp => { datenpunkteInSzenario.delete(dp); }); // Ergebnis zum Szenario hinzufuegen ScenarioToDPsTable[scenarioKey] = Array.from(datenpunkteInSzenario); } } // Phase 3: Alle Datenpunkte aus raumDefinition fuer "NichtAktiv" sammeln ScenarioToDPsTable["NichtAktiv"] = []; const allDpsFromRooms = new Set(); // Alle Datenpunkte aus raumDefinition sammeln for (const raumName in raumDefinition) { const raum = raumDefinition[raumName]; for (const typ in raum) { if (typ === "Type" || typ === "Position") continue; const dpList = raum[typ]; if (Array.isArray(dpList)) { dpList.forEach(dp => allDpsFromRooms.add(dp)); } } } // Alle bereits in aktiven Szenarien enthaltenen Datenpunkte sammeln const activeScenarioDps = new Set(); for (const scenario in ScenarioToDPsTable) { if (scenario !== "NichtAktiv") { ScenarioToDPsTable[scenario].forEach(dp => activeScenarioDps.add(dp)); } } // Nur die Datenpunkte hinzufuegen, die nicht in aktiven Szenarien sind allDpsFromRooms.forEach(dp => { if (!activeScenarioDps.has(dp)) { ScenarioToDPsTable["NichtAktiv"].push(dp); } }); // Subscriptions setzen GeneratedSubscriptions = []; for (const scenario in ScenarioToDPsTable) { for (const dpId of ScenarioToDPsTable[scenario]) { if (!GeneratedSubscriptions.includes(dpId)) { on({ id: dpId, change: "ne" }, (obj) => { LOG(`Aenderung erkannt fuer ${dpId}: Neuer Wert = ${obj.state.val}`, "Ablauf", "subscribeToAllScenarioDPs", 2); if (scenario !== "NichtAktiv") { DetermineAlarm(dpId, obj.state.val); } else { LOG(`Statusaenderung bei inaktivem Datenpunkt: ${dpId} = ${obj.state.val}`, "Info", "subscribeToAllScenarioDPs", 3); } }); GeneratedSubscriptions.push(dpId); } } } // Statistik LOG("Subscriptions wurden generiert fuer folgende Datenpunkte:", "Ablauf", "subscribeToAllScenarioDPs", 3); for (const scenario in ScenarioToDPsTable) { const scenarioName = scenarioDefinition[scenario]?.name || scenario; LOG(`\nSzenario ${scenario} (${scenarioName}):`, "Ablauf", "subscribeToAllScenarioDPs", 3); ScenarioToDPsTable[scenario].forEach(dpid => { const commonName = ExtractMetaData(dpid, "COMMONNAME") || 'Kein Common-Name'; LOG(`- ${dpid} (${commonName})`, "Ablauf", "subscribeToAllScenarioDPs", 3); }); } const totalDPs = Object.values(ScenarioToDPsTable).flat().length; const uniqueDPs = new Set(Object.values(ScenarioToDPsTable).flat()).size; LOG(`\nGesamt: ${totalDPs} gefilterte Datenpunkte (${uniqueDPs} eindeutige Datenpunkte werden abonniert)`, "Ablauf", "subscribeToAllScenarioDPs", 1); let scenarioNames = Object.keys(ScenarioToDPsTable); LOG(`Anzahl Szenarien: ${scenarioNames.length}`, "Ergebnis", "subscribeToAllScenarioDPs", 1); scenarioNames.forEach(scenario => { let dps = ScenarioToDPsTable[scenario]; LOG(`- ${scenario}: ${dps.length} Datenpunkte`, "Ergebnis", "subscribeToAllScenarioDPs", 1); }); setState(DP_Raumuebersicht,renderScenarioRoomDeviceHTML()) } //----------------------------------------------------------------------------------------------------- // Funktion fuer die Subscription der Datenpunkte Alarmaktivierung, Einbruch, Wassermeldung etc. //----------------------------------------------------------------------------------------------------- function createAktivierungsSubscription() { LOG(`Routine wird ausgefuehrt`, "Ablauf", "createAktivierungsSubscription", 2); for (const {Control, Datenpunkt } of AlarmScenarios) { if (Control.startsWith('Scenario') && Control.endsWith('Aktiv')) { on({ id: Datenpunkt, change: 'any' }, function (obj) { if(obj.state.val != obj.oldState.val) { LOG(`Datenpunkt fuer ${Datenpunkt} auf ${obj.state.val} gesetzt`, "Ablauf", "createAktivierungsSubscription", 1); ScenarioAenderung(Control, obj.state.val); // Aufruf der Funktion ScenarioAenderung, wenn sich einer der ScenarioxxAktiv Datenpunkte aendert setLEDState(Control,obj.state); let jsonString = getState(id_JSON_Alarmmeldung_Historie).val || '[]'; // Lade den aktuellen JSON-String des Historie-Datenpunkts HistorischeMeldungenJSON = JSON.parse(jsonString); const ScenarioNummer = Control.substring(0, 10); // Ergibt z.B. "Scenario01" const ScenarioName = scenarioDefinition[ScenarioNummer]?.name || "Unbekanntes Szenario"; let messagekurz, messagelang if(obj.state.val) { messagekurz = MessageScenarioAenderungAktiv messagelang = func_get_datum() + " " + MessageScenarioAenderungAktiv }else{ messagekurz = MessageScenarioAenderungDeAktiv+ " " + ScenarioNummer messagelang = func_get_datum() + " " + MessageScenarioAenderungDeAktiv } if(obj.state.val && pruefeProtokollierung("ScenarioAenderung", true)) { HistorischeMeldungenJSON = CreateJsonAlarmmeldung(HistorischeMeldungenJSON, func_get_datum(), messagekurz, func_get_datum(),"","","",ScenarioNummer,ScenarioName) } if(!obj.state.val && pruefeProtokollierung("ScenarioAenderung", false)) { HistorischeMeldungenJSON = CreateJsonAlarmmeldung(HistorischeMeldungenJSON, func_get_datum(), messagekurz, func_get_datum(),"","","",ScenarioNummer,ScenarioName) } jsonString = JSON.stringify(HistorischeMeldungenJSON); // Stringify das Json um fuer die speicherung vorzubereiten setState(id_JSON_Alarmmeldung_Historie, jsonString); // Aktuelle Alarmmeldung wird in JSON-Datenpunkt als String gespeichert generateHtmlTable(HistorischeMeldungenJSON,"Hist") writeProkollExtern( messagekurz, func_get_datum() ,"", "","", ScenarioNummer, ScenarioName) messagekurz = messagekurz + " " + ScenarioNummer messagelang = messagelang + " " + ScenarioNummer + " " + ScenarioName if( obj.state.val) { addMessageToCollector("ScenarioAktiviert", messagekurz, messagelang); // zur Abarbeitung der Tabelle MessengerScope }else{ addMessageToCollector("ScenarioDeaktiviert", messagekurz, messagelang); // zur Abarbeitung der Tabelle MessengerScope } sendMessage(); } }); } } on({ id: DP_Alarmaktivierung, change: 'any' }, function (obj) { if(obj.state.val != obj.oldState.val) { setLEDState(DP_Alarmaktivierung, obj.state.val); LOG(`Datenpunkt fuer ${DP_Alarmaktivierung} auf ${obj.state.val} gesetzt`, "Ablauf", "createAktivierungsSubscription", 1); if (activationTimeout) { // Vorhandenen Timeout abbrechen, falls vorhanden clearTimeout(activationTimeout); activationTimeout = null; LOG(`Bestehender Aktivierungs-Timeout wurde abgebrochen`, "Ablauf", "createAktivierungsSubscription", 2); } if (obj.state.val) { // Nur bei Aktivierung (value = true) einen neuen Timeout setzen activationTimeout = setTimeout(() => { // Pruefen, ob der Alarm immer noch aktiviert werden soll const currentState = getState(DP_Alarmaktivierung).val; if (currentState) { Alarmaktivierung(true); } else { LOG(`Alarmaktivierung wurde waehrend des Delays abgebrochen`, "Ablauf", "createAktivierungsSubscription", 2); } activationTimeout = null; // Timeout-Handle zuruecksetzen }, AKTIVIERUNGSDELAY_SECONDS * 1000); } else { Alarmaktivierung(false); // Sofortige Deaktivierung } } }); on({ id: DP_Einbruchsmeldung, change: 'any' }, function (obj) { if(obj.state.val != obj.oldState.val) { LOG(`Datenpunkt fuer ${DP_Einbruchsmeldung} auf ${obj.state.val} gesetzt`, "Ablauf", "createAktivierungsSubscription", 2); setLEDState(DP_Einbruchsmeldung,obj.state.val); if(!obj.state.val){ let jsonString = getState(id_JSON_Alarmmeldung_Aktuell).val || '[]'; // Lade den aktuellen JSON-String des Historie-Datenpunkts AktuelleMeldungenJSON = JSON.parse(jsonString); AktuelleMeldungenJSON = RemoveAlarmtypeFromJSON(AktuelleMeldungenJSON, "Einbruch") jsonString = JSON.stringify(AktuelleMeldungenJSON); setState(id_JSON_Alarmmeldung_Aktuell, jsonString); // Aktuelle Alarmmeldung wird in JSON-Datenpunkt als String gespeichert generateHtmlTable(AktuelleMeldungenJSON,"Akt") if(pruefeProtokollierung("Einbruch", false)) { JSON_Alarmmeldungen_History_Aktualisierung(AktuelleMeldungenJSON,"Einbruch"); // Stateupdate ist Teil der aufgerufenen Funktion } } } }); on({ id: DP_Wassermeldung, change: 'any' }, function (obj) { if(obj.state.val != obj.oldState.val) { LOG(`Datenpunkt fuer ${DP_Wassermeldung} auf ${obj.state.val} gesetzt`, "Ablauf", "createAktivierungsSubscription", 2); setLEDState(DP_Wassermeldung,obj.state.val); if(!obj.state.val){ let jsonString = getState(id_JSON_Alarmmeldung_Aktuell).val || '[]'; // Lade den aktuellen JSON-String des Historie-Datenpunkts AktuelleMeldungenJSON = JSON.parse(jsonString); AktuelleMeldungenJSON = RemoveAlarmtypeFromJSON(AktuelleMeldungenJSON, "Wasser") jsonString = JSON.stringify(AktuelleMeldungenJSON); setState(id_JSON_Alarmmeldung_Aktuell, jsonString); // Aktuelle Alarmmeldung wird in JSON-Datenpunkt als String gespeichert generateHtmlTable(AktuelleMeldungenJSON,"Akt") if(pruefeProtokollierung("Wasser", false)) { JSON_Alarmmeldungen_History_Aktualisierung(AktuelleMeldungenJSON,"Wasser"); // Stateupdate ist Teil der aufgerufenen Funktion } } } }); on({ id: DP_Rauchmeldung, change: 'any' }, function (obj) { if(obj.state.val != obj.oldState.val) { LOG(`Datenpunkt fuer ${DP_Rauchmeldung} auf ${obj.state.val} gesetzt`, "Ablauf", "createAktivierungsSubscription", 2); setLEDState(DP_Rauchmeldung,obj.state.val); if(!obj.state.val){ let jsonString = getState(id_JSON_Alarmmeldung_Aktuell).val || '[]'; // Lade den aktuellen JSON-String des Historie-Datenpunkts AktuelleMeldungenJSON = JSON.parse(jsonString); AktuelleMeldungenJSON = RemoveAlarmtypeFromJSON(AktuelleMeldungenJSON, "Rauch") jsonString = JSON.stringify(AktuelleMeldungenJSON); setState(id_JSON_Alarmmeldung_Aktuell, jsonString); // Aktuelle Alarmmeldung wird in JSON-Datenpunkt als String gespeichert generateHtmlTable(AktuelleMeldungenJSON,"Akt") if(pruefeProtokollierung("Rauch", false)) { JSON_Alarmmeldungen_History_Aktualisierung(AktuelleMeldungenJSON,"Rauch"); // Stateupdate ist Teil der aufgerufenen Funktion } } } }); on({ id: DP_GeoeffnetMeldung, change: 'any' }, function (obj) { if(obj.state.val != obj.oldState.val) { LOG(`Datenpunkt fuer ${DP_GeoeffnetMeldung} auf ${obj.state.val} gesetzt`, "Ablauf", "createAktivierungsSubscription", 2); setLEDState(DP_GeoeffnetMeldung, obj.state.val); } }); on({ id: DP_Sabotagemeldung, change: 'any' }, function (obj) { if(obj.state.val != obj.oldState.val) { LOG(`Datenpunkt fuer ${DP_Sabotagemeldung} auf ${obj.state.val} gesetzt`, "Ablauf", "createAktivierungsSubscription", 2); setLEDState(DP_Sabotagemeldung,obj.state.val); if(!obj.state.val){ if(pruefeProtokollierung("Sabotage", false)) { let jsonString = getState(id_JSON_Alarmmeldung_Aktuell).val || '[]'; // Lade den aktuellen JSON-String des Historie-Datenpunkts AktuelleMeldungenJSON = JSON.parse(jsonString); AktuelleMeldungenJSON = RemoveAlarmtypeFromJSON(AktuelleMeldungenJSON, "Sabotage") jsonString = JSON.stringify(AktuelleMeldungenJSON); setState(id_JSON_Alarmmeldung_Aktuell, jsonString); // Aktuelle Alarmmeldung wird in JSON-Datenpunkt als String gespeichert generateHtmlTable(AktuelleMeldungenJSON,"Akt") JSON_Alarmmeldungen_History_Aktualisierung(AktuelleMeldungenJSON,"Sabotage"); // Stateupdate ist Teil der aufgerufenen Funktion } } } }); on({ id: DP_Stoerungsmeldung, change: 'any' }, function (obj) { if(obj.state.val != obj.oldState.val) { LOG(`Datenpunkt fuer ${DP_Stoerungsmeldung} auf ${obj.state.val} gesetzt`, "Ablauf", "createAktivierungsSubscription", 2); setLEDState(DP_Stoerungsmeldung,obj.state.val); if(!obj.state.val){ if(pruefeProtokollierung("Stoerung", false)) { let jsonString = getState(id_JSON_Alarmmeldung_Aktuell).val || '[]'; // Lade den aktuellen JSON-String des Historie-Datenpunkts AktuelleMeldungenJSON = JSON.parse(jsonString); AktuelleMeldungenJSON = RemoveAlarmtypeFromJSON(AktuelleMeldungenJSON, "Stoerung") jsonString = JSON.stringify(AktuelleMeldungenJSON); setState(id_JSON_Alarmmeldung_Aktuell, jsonString); // Aktuelle Alarmmeldung wird in JSON-Datenpunkt als String gespeichert generateHtmlTable(AktuelleMeldungenJSON,"Akt") JSON_Alarmmeldungen_History_Aktualisierung(AktuelleMeldungenJSON,"Stoerung"); // Stateupdate ist Teil der aufgerufenen Funktion } } } }); on({ id: DP_Still, change: 'any' }, function (obj) { if(obj.state.val != obj.oldState.val) { LOG(`Datenpunkt fuer ${DP_Still} auf ${obj.state.val} gesetzt`, "Ablauf", "createAktivierungsSubscription", 2); setLEDState(DP_Still, obj.state.val); // Immer nach 2 Sekunden zuruecksetzen if(obj.state.val === true) { setTimeout(function() { setState(DP_Still, false); LOG(`Datenpunkt fuer ${DP_Still} automatisch zurueckgesetzt`, "Ablauf", "createAktivierungsSubscription", 2); }, 2000); } // Nur ausfuehren wenn Bedingungen erfuellt sind: status true und eine der 3 meldungen true if(obj.state.val === true && (getState(DP_Einbruchsmeldung).val || getState(DP_Rauchmeldung).val || getState(DP_Wassermeldung).val)) { Execute_STILL(); if(pruefeProtokollierung("STILL", true)) { let jsonString = getState(id_JSON_Alarmmeldung_Historie).val || '[]'; HistorischeMeldungenJSON = JSON.parse(jsonString); HistorischeMeldungenJSON = CreateJsonAlarmmeldung(HistorischeMeldungenJSON, func_get_datum(), MessageSTILLAktiviert, func_get_datum() ); jsonString = JSON.stringify(HistorischeMeldungenJSON); setState(id_JSON_Alarmmeldung_Historie, jsonString); generateHtmlTable(HistorischeMeldungenJSON,"Hist") } writeProkollExtern( MessageSTILLAktiviert, func_get_datum() ) const messagekurz = MessageSTILLAktiviert; const messagelang = func_get_datum() + " " + MessageSTILLAktiviert; addMessageToCollector("Alarm_Still_Geschaltet", messagekurz, messagelang); sendMessage(); } } }); // Subscription fuer Sabotage - Datenpunkt aus dem Servicemeldungsscript if (existsState(SM_CountSabotage)) { on({ id: SM_CountSabotage, change: "ne" }, (obj) => { LOG(`Aenderung erkannt fuer ${SM_CountSabotage}: Neuer Wert = ${obj.state.val}`, "Ablauf", "createAktivierungsSubscription", 2); const Sabotage_DP = Zustandsermittlung("Sabotage") if(Sabotage_DP) { DetermineAlarm(Sabotage_DP, true,true, false); // 1.true = status 2. = Sabotage 3. Stoerung } }); } else { LOG(`Datenpunkt ${SM_CountSabotage} existiert nicht und wird nicht abonniert`, "Ablauf", "createAktivierungsSubscription", 3); } // Subscription fuer Unreach - Datenpunkt aus dem Servicemeldungsscript if (existsState(SM_CountUnreach)) { on({ id: SM_CountUnreach, change: "ne" }, (obj) => { LOG(`Aenderung erkannt fuer ${SM_CountUnreach}: Neuer Wert = ${obj.state.val}`, "Ablauf", "createAktivierungsSubscription", 2); const Stoerung_DP = Zustandsermittlung("Stoerung"); if(Stoerung_DP) { DetermineAlarm(Stoerung_DP, true, false, true); // 1.true = status 2. = Sabotage 3. Stoerung } }); } else { LOG(`Datenpunkt ${SM_CountUnreach} existiert nicht und wird nicht abonniert`, "Ablauf", "createAktivierungsSubscription", 3); } // Subscription fuer An/Abwesenheit - Datenpunkt optional if(existsState(anwesenheit)) { on({ id: anwesenheit, change: 'any' }, function (obj) { if(obj.state.val != obj.oldState.val) { if (!obj.state.val && existsState(anwesenheit)) { AutoScenarioActivation(AbwesenheitScenarioAktivierung,true) LOG(`Anwesenheit wurde festgestellt und folgendes Scenario deaktiviert: ${AbwesenheitScenarioAktivierung.join(', ')}`, "Ablauf", "createAktivierungsSubscription", 2); } if (obj.state.val && existsState(anwesenheit)) { AutoScenarioActivation(AbwesenheitScenarioAktivierung,false) LOG(`Anwesenheit wurde festgestellt und folgendes Scenario deaktiviert: ${AbwesenheitScenarioAktivierung.join(', ')}`, "Ablauf", "createAktivierungsSubscription", 2); } } }); } // Subscription fuer Gaeste Scenario Aktivierung - Datenpunkt optional if(existsState(GaesteFlag)) { on({ id: GaesteFlag, change: 'any' }, function (obj) { if(obj.state.val != obj.oldState.val) { if (obj.state.val) { AutoScenarioActivation(GaesteScenarioAktivierung,true) LOG(`Gaeste-Flag wurde gesetzt - Scenario fuer Gaeste wurde aktiviert: ${GaesteScenarioAktivierung.join(', ')}`, "Ablauf", "createAktivierungsSubscription", 2); } if (!obj.state.val) { AutoScenarioActivation(GaesteScenarioAktivierung,false) LOG(`Gaeste-Flag wurde zurueckgesetzt - Scenario fuer Gaeste wurde deaktiviert: ${GaesteScenarioAktivierung.join(', ')}`, "Ablauf", "createAktivierungsSubscription", 2); } } }); } // Subscription fuer Loeschen der History if(existsState(id_Button_Refresh_Historie)) { on({ id: id_Button_Refresh_Historie, change: 'any' }, function (obj) { if (obj.state.val) { Refresh_History() } }); } } //----------------------------------------------------------------------------------------------------- // Function createSubscriptionsfuerControlDevices fuer die StatusBoards (ELV-SB-SH8) //----------------------------------------------------------------------------------------------------- function createSubscriptionsfuerControlDevices() { LOG(`Routine wird ausgefuehrt`, "Ablauf", "createSubscriptionsfuerControlDevices", 2); ControlDevices.forEach(function(device) { const dp = device.DP; Object.keys(device).forEach(function(ledKey) { if (ledKey.startsWith("LED")) { const ledIndex = parseInt(ledKey.replace("LED", "")); const stateIndex = device[`DP_2ndPart${ledIndex}`]; if (!stateIndex) return; const dataPoint = dp + stateIndex; if(existsState(dataPoint)) { on({ id: dataPoint, change: 'any' }, function (obj) { if (obj.state.val != obj.oldState.val) { LOG(`subscription aufgerufen fuer '${dataPoint}' `, "Alauf", "createSubscriptionsfuerControlDevices", 3); setStatefromBoard(dataPoint, obj.state.val, device.DeviceID); } }); } } }); }); } //----------------------------------------------------------------------------------------------------- // Function Zustandsermittlung Initialsierung Status geoeffnet Sabotage und Stoerung //----------------------------------------------------------------------------------------------------- function Zustandsermittlung(Scope) { LOG(`Starte Zustandsermittlung mit Scope: ${Scope}`, "Ablauf", "Zustandsermittlung", 2); const LocalstartTimePerformance = Date.now(); let result = false; let now = Date.now(); if (Scope === "init" || Scope === "Sabotage") { if (existsState(SM_CountSabotage)) { result = Servicemeldungen("Sabotage"); LOG(`Sabotage Ergebnis aus Routine Servicemeldungen ist ${result} `,"Ablauf",2); if(Scope === "Sabotage") return result } } if (Scope === "init" || Scope === "Stoerung") { if (existsState(SM_CountUnreach)) { result = Servicemeldungen("Stoerung"); LOG(`Stoerung Ergebnis aus Routine Servicemeldungen ist ${result} `,"Ablauf",2); if(Scope === "Stoerung") return result } } // jetzt noch die Ueberpruefung fuer Zustand geoeffnet now = Date.now(); result = false; if (now - lastGeoeffnetUpdate < 200) { LOG("Update zu schnell - ueberspringe", "Ablauf", "Zustandsermittlung", 3); return getState(DP_GeoeffnetMeldung).val; } if (Scope === "init") { //- logik ueberpruefe alle dpids des alarmtypes geoeffnet und checke zusstand je dpid // Bei init alle Geoeffnet-DeviceTypes pruefen const geoeffnetType = AlarmTypes.find(a => a.alarmtype === "Geoeffnet"); // DeviceTypes, die dem AlarmType "geoeffnet" in Tabelle AlarmTypes zugeordnet sind for (const deviceType of geoeffnetType.deviceTypes) { const currentCheck = DetermineGeoeffnetZustandJeDeviceType(deviceType); result = result || currentCheck; // Wenn ein DeviceType true liefert, wird result true LOG(`Init-Zustandspruefung fuer ${deviceType} ergab ${currentCheck} (Gesamtresult: ${result})`, "Ablauf", "Zustandsermittlung", 3); if (result) break; // Wenn result true ist, koennen wir vorzeitig abbrechen } } else { // Pruefen ob Scope zu Geoeffnet gehoert const geoeffnetType = AlarmTypes.find(a => a.alarmtype === "Geoeffnet"); if (geoeffnetType.deviceTypes.includes(Scope)) { result = DetermineGeoeffnetZustandJeDeviceType(Scope); LOG(`${Scope} Zustandspruefung ergab: ${result}`, "Ablauf", "Zustandsermittlung", 3); } else { LOG(`${Scope} gehoert nicht zu Zustand Geoeffnet - keine Pruefung durchgefuehrt`, "Ablauf", "Zustandsermittlung", 3); return false; } } setState(DP_GeoeffnetMeldung, result); lastGeoeffnetUpdate = now; LOG(`Geoeffnet-Status aktualisiert: ${result}`, "Ablauf", "Zustandsermittlung", 2); LOG(`Zeitverbrauch fuer Routine Zustandsermittlung: ${Date.now() - LocalstartTimePerformance} ms`, "Ablauf", "Zustandsermittlung", 1); return result; } // ------------------------------------------------------------------------------------------------- // Function Servicemeldungen Liest die Sabotage/Unreach Messages from Servicmeldungsscript und analysiert diese //----------------------------------------------------------------------------------------------------- function Servicemeldungen(scope) { LOG(`Starte Servicemeldungen-Auswertung fuer Scope: ${scope}`, "INFO", "Servicemeldungen", 2); try { const jsonString = getState(SM_Meldungen_JSON).val || '[]'; const meldungen = JSON.parse(jsonString); LOG(`Verarbeite ${meldungen.length} Meldungen`, "Ablauf", "Servicemeldungen", 3); const targetType = scope === "Sabotage" ? "SABOTAGE_ALARM" : "UNREACH_ALARM"; const meldung = meldungen.find(entry => entry.meldungsart === targetType && entry.GeraeteId ); if (meldung) { LOG(`${scope} erkannt: ${JSON.stringify(meldung)}`, "Ablauf", "Servicemeldungen", 2); const dpid = findDatenpunktByGeraeteID(meldung.GeraeteId); // Suche nach Datenpunkt in raumDefinition if (dpid) { LOG(`${scope}-Datenpunkt gefunden: ${dpid}`, "Ablauf", "Servicemeldungen", 2); setState(scope === "Sabotage" ? DP_Sabotagemeldung : DP_Stoerungsmeldung, true); return dpid; } else { // Die Sabotagemeldung findet sich nicht in Tablle RaumDefinition - also gibt es keine relevanten Servicemeldungen if(targetType === "SABOTAGE_ALARM" && ServiceMeldungSabotageAutoConfirm) { setState(DP_Sabotagemeldung,false) } if(targetType === "UNREACH_ALARM" && ServiceMMeldungStoerungAutoConfirm) { setState(DP_Stoerungsmeldung,false) } LOG(`kein passender Datenpunkt fuer GeraeteID ${meldung.GeraeteId} gefunden`, "WARN", "Servicemeldungen", 2); return null; } } else { LOG(`keine relevanten Servicemeldungen sind derzeit aktiv`, "Ablauf", "Servicemeldungen", 3); if(targetType === "SABOTAGE_ALARM" && ServiceMeldungSabotageAutoConfirm) { setState(DP_Sabotagemeldung,false) } if(targetType === "UNREACH_ALARM" && ServiceMMeldungStoerungAutoConfirm) { setState(DP_Stoerungsmeldung,false) } return null; } } catch (e) { LOG(`Fehler in Servicemeldungen: ${e.message}`, "ERROR", "Servicemeldungen", 0,"error"); return false; } } //----------------------------------------------------------------------------------------------------- // Function findDatenpunktByGeraeteID Sucht in der Raumdefinition nach dem vollstaendigen Datenpunkt anhand der GeraeteID // wird fuer Servicemeldungen Sabotage und Unreach benoetigt //----------------------------------------------------------------------------------------------------- function findDatenpunktByGeraeteID(geraeteID) { for (const raum of Object.values(raumDefinition)) { // 1. Durchsuche alle Raeume (Object.values ist schneller als for-in bei grossen Objekten) for (const datenpunkte of Object.values(raum).filter(Array.isArray)) { // 2. Filtere nur Array-Eigenschaften (Fenster, Tueren etc.) und ignoriere Metadaten for (const dp of datenpunkte) { // 3. Durchsuche alle Datenpunkte dieser Kategorie if (dp.includes(geraeteID)) { // 4. Case-sensitive Pruefung ob die GeraeteID im Datenpunkt enthalten ist return dp; // Fruehes Return bei Fund } } } } return null; // 5. Rueckgabe null wenn nichts gefunden wurde } //----------------------------------------------------------------------------------------------------- // Function DetermineGeoeffnetZustandJeDeviceType Hilfsfunktion fuer Zustandsermittlung Initialsierung Status fuer Fenster / Rollladen etc. Allgemeine Device-Pruefung //----------------------------------------------------------------------------------------------------- function DetermineGeoeffnetZustandJeDeviceType(deviceType) { let result = false; const relevantDps = new Set(); // 1. Alle relevanten Datenpunkte aus ScenarioToDPsTable extrahieren for (const [scenarioName, dpList] of Object.entries(ScenarioToDPsTable)) { // Nur aktive Szenarien beruecksichtigen // if (ScenarioStatus[scenarioName]?.status === true) { dpList.forEach(dp => relevantDps.add(dp)); // } } const filteredDps = new Set(); // 2. Vergleiche mit raumDefinition und filtere relevante Datenpunkte for (const raumName in raumDefinition) { const devices = raumDefinition[raumName]?.[deviceType]; if (!devices?.length) continue; devices.forEach(device => { if (relevantDps.has(device)) { filteredDps.add(device); } }); } // 3. Den Status der relevanten Datenpunkte pruefen for (const device of filteredDps) { const status = getState(device).val; LOG(`${deviceType} ${device}: ${status}`, "Ablauf", "DetermineGeoeffnetZustandJeDeviceType", 3); if (AlarmStateAuswertung(device, status)) { LOG(`Geoeffnetzustand erkannt: ${deviceType} ${device}`, "Ablauf", "DetermineGeoeffnetZustandJeDeviceType", 2); result = true; break; // Frueher abbrechen, wenn ein Zustand erkannt wird } } return result; } //----------------------------------------------------------------------------------------------------- // Function DetermineAlarm Routine zur Notwendigkeit der alarmausloesung - Liegt ein Alarm vor ? //----------------------------------------------------------------------------------------------------- function DetermineAlarm(dpid, status, Sabotage = false, Stoerung = false) { LOG(`Routine wird ausgefuehrt fuer Datenpunkt ${dpid} mit Status ${status}`, "Ablauf", "DetermineAlarm", 2); const LocalstartTimePerformance = Date.now(); // 1. Initiale Werte let DeviceType, AlarmState, AlarmType, Scenario_Ausloeser // 2. Geraetetyp und Alarmstatus ermitteln if (Stoerung) { dpid = Zustandsermittlung("Stoerung"); DeviceType = getDeviceTypeForDataPoint(dpid); Scenario_Ausloeser = Object.values(scenarioDefinition).find(s => s.Filter.includes("Stoerung"))?.name || null } else if (Sabotage) { dpid = Zustandsermittlung("Sabotage"); DeviceType = getDeviceTypeForDataPoint(dpid); AlarmState = AlarmStateAuswertung(dpid, status); Scenario_Ausloeser = DetermineScenario(dpid); } else { DeviceType = getDeviceTypeForDataPoint(dpid); AlarmState = AlarmStateAuswertung(dpid, status); Scenario_Ausloeser = DetermineScenario(dpid); // Geoeffnet-Status nur fuer relevante Geraetetypen pruefen const geoeffnetType = AlarmTypes.find(a => a.alarmtype === "Geoeffnet"); Scenario_Ausloeser = DetermineScenario(dpid); if (geoeffnetType?.deviceTypes.includes(DeviceType)) { const currentState = AlarmState ? true : checkAnyGeoeffnetDevice(); setState(DP_GeoeffnetMeldung, currentState); LOG(`${DeviceType} Zustandspruefung ergab: ${currentState}`, "Ablauf", "DetermineAlarm", 3); } } // 3. Hilfsfunktion fuer Geoeffnet-Status-Check function checkAnyGeoeffnetDevice() { const geoeffnetType = AlarmTypes.find(a => a.alarmtype === "Geoeffnet"); return geoeffnetType?.deviceTypes.some(deviceType => DetermineGeoeffnetZustandJeDeviceType(deviceType) ) ?? false; } // 4. Fruehe Abbruchbedingungen pruefen if (!AlarmState && !Stoerung) { // Stoerungen haben keinen AlarmState LOG(`Kein Alarm fuer ${dpid} - Status ist ${AlarmState}`, "Ablauf", "DetermineAlarm", 2); return; } if (!Scenario_Ausloeser || !ScenarioStatus[Scenario_Ausloeser]?.status) { LOG(`Kein aktives Szenario fuer ${dpid} (DeviceType: ${DeviceType})`, "Ablauf", "DetermineAlarm", 2); return; } // 5. Alarmverarbeitung const alarmType = Stoerung ? "Stoerung" : DeriveAlarmType(DeviceType); LOG(`Alarmtyp ermittelt: ${alarmType}`, "Ablauf", "DetermineAlarm", 2); const DeviceAlarm = AlarmTypes.find(alarm => alarm.alarmtype === alarmType); if (!DeviceAlarm || !existsState(DeviceAlarm.Datenpunkt)) { LOG(`Ungueltiger Alarmtyp/Pfad: ${alarmType} / ${DeviceAlarm?.Datenpunkt}`, "Fehler", "DetermineAlarm", 0, "error"); return; } // 6. Alarmausloesung if (AlarmAktivierungsStatus || DeviceAlarm.alwaysOn) { setState(DeviceAlarm.Datenpunkt, true); Execute_Alarm(Scenario_Ausloeser, dpid, DeviceType, alarmType, Sabotage); LOG(`Ausfuehrungszeit: ${Date.now() - LocalstartTimePerformance}ms`, "Ergebnis", "DetermineAlarm", 1); } else { LOG(`Alarm deaktiviert fuer ${dpid} (kein AlwaysOn-Geraet)`, "Ablauf", "DetermineAlarm", 2); } } //----------------------------------------------------------------------------------------------------- // Function DetermineScenario Routine ermittelt das Scenario mit der hoechsten prioritaet //----------------------------------------------------------------------------------------------------- function DetermineScenario(dpid) { LOG(`Bestimme Szenario fuer Datenpunkt ${dpid}`, "Ablauf", "DetermineScenario", 2); const matchingScenarios = []; // Finde alle Szenarien, die diesen Datenpunkt enthalten for (const scenario in ScenarioToDPsTable) { if (ScenarioToDPsTable[scenario].includes(dpid)) { matchingScenarios.push(scenario); } } // Liste aller Szenarien in denen der DP vorkommt LOG(`Datenpunkt ${dpid} ist in folgenden Szenarien enthalten: ${matchingScenarios.join(', ')}`,"Ablauf", "DetermineScenario", 2); if (matchingScenarios.length === 0) { // kann eigentlich nicht vorkommen LOG(`Kein passendes Szenario fuer Datenpunkt ${dpid} gefunden`, "Ablauf", "DetermineScenario", 2); return null; } matchingScenarios.sort((a, b) => { // Sortiere nach Prioritaet und dann nach Szenario-Nummer const prioA = scenarioDefinition[a]?.Prio || 10; const prioB = scenarioDefinition[b]?.Prio || 10; if (prioA !== prioB) { return prioA - prioB; // Niedrigere Prio (hoehere Prioritaet) zuerst } else { // Bei gleicher Prioritaet nach Szenario-Nummer sortieren const numA = parseInt(a.replace('Scenario', '')); const numB = parseInt(b.replace('Scenario', '')); return numA - numB; } }); // Sortierte Liste der Szenarien nach Priorisierung LOG(`Priorisierte Szenarienreihenfolge fuer ${dpid}: ${matchingScenarios.join(' -> ')}`, "Ablauf", "DetermineScenario", 2); const selectedScenario = matchingScenarios[0]; LOG(`Ausgewaehltes Szenario fuer ${dpid}: ${selectedScenario}`, "Ablauf", "DetermineScenario", 2); return selectedScenario; } //----------------------------------------------------------------------------------------------------- // Function AlarmStateAuswertung Routine Feststellung ob der State des dpid ein alarmstate ist // es wird ein true zurueckgegeben sobald auch nur ein einziger wert zutrifft //----------------------------------------------------------------------------------------------------- function AlarmStateAuswertung(dpid, status) { const ausnahme = AlarmAusloesungswerte.find(regel => // Zuerst nach Ausnahmen in AlarmAusloesungswerte suchen regel.exception && regel.exception.dpAusnahme === dpid ); if (ausnahme) { LOG(`Ausnahme-Regel gefunden fuer ${dpid}: ${JSON.stringify(ausnahme.exception)}`, "Ablauf", "AlarmStateAuswertung", 3); return status === ausnahme.exception.wert; } // Falls keine Ausnahme gefunden wurde, normale Auswertung durchfuehren return AlarmAusloesungswerte.some(regel => { if (regel.exception) return false; // Ueberspringe Regeln mit exception (da diese schon oben behandelt wurden) switch (true) { case regel.op === '==': return status === regel.wert; case regel.op === '!=': return status !== regel.wert; case regel.op === '>': return status > regel.wert; case regel.op === '>=': return status >= regel.wert; case regel.op === '<': return status < regel.wert; case regel.op === '<=': return status <= regel.wert; case regel.range !== undefined: return status >= regel.range.min && status <= regel.range.max; default: return false; } }); } //----------------------------------------------------------------------------------------------------- // Function DeriveAlarmType Routine zur Findung des Alarmtypes // Devicetypes Fenster, Melder etc. Alarmtypen sind Einbruch, Wasser, Rauch, Sabotage, Stoerung // diese routine muss evt erweitert werden fuer Sabotage und unreach //----------------------------------------------------------------------------------------------------- function DeriveAlarmType(DeviceType) { const sortedAlarmTypes = [...AlarmTypes].sort((a, b) => a.Prio - b.Prio); // 1. AlarmTypes nach Prioritaet sortieren (niedrigste Prio-Zahl zuerst) const foundEntry = sortedAlarmTypes.find(entry => // 2. Nach dem ersten passenden Eintrag suchen entry.deviceTypes.includes(DeviceType) ); if (foundEntry) { // 3. Gefundenen Alarmtyp zurueckgeben oder Default-Wert return foundEntry.alarmtype; } LOG(`Unbekannter DeviceType "${DeviceType}" – Standardwert "Einbruch" verwendet`, "Warn", "DeriveAlarmType", 2,"warn"); // sollte eigentlich nicht vorkommen return "Einbruch"; // 4. Fallback fuer unbekannte DeviceTypes } //----------------------------------------------------------------------------------------------------- // Function Execute_Alarm Routine zur Abarbeitung von alarmen - Hauptroutine //----------------------------------------------------------------------------------------------------- function Execute_Alarm(Scenario, dpid,deviceType,alarmType, Sabotage) { LOG(`nachdem ein Alarm erkannt wurde, wird jetzt der Alarm vorbereitet`, "Ablauf", "Execute_Alarm", 2); // Sicherstellen, dass das ausgewaehlte Szenario in der Szenario-Definition existiert const scenarioDetails = scenarioDefinition[Scenario]; if (!scenarioDetails) { LOG(`Das Szenario ${Scenario} wurde nicht in der scenarioDefinition gefunden`, "Fehler", "Execute_Alarm", 0); return; } const AlarmType = alarmType const AlertType = scenarioDetails.Alert; const Raum = GetRoomsForDataPoint(dpid) let commonName = ExtractMetaData(dpid, "COMMONNAME"); commonName = ReplaceString(commonName); let messagekurz, messagelang; const GeraeteID = ExtractMetaData(dpid, "GERAETEID"); if (!Sabotage) { // keine Sabotage messagekurz = `${AlarmType} ${deviceType} ${commonName}`; messagelang = `${func_get_datum()} ${AlarmType} in Raum ${Raum.join(', ')} ${deviceType} ${commonName} Selektiertes Scenario: ${Scenario}`; }else{ // Sabotage messagekurz = `Sabotage ${deviceType} ${commonName}`; messagelang = `${func_get_datum()} Sabotage/Einbruch in Raum ${Raum.join(', ')} ${deviceType} ${commonName} Selektiertes Scenario: ${Scenario}`; } LOG(`Lange Message ist ${messagelang}. Die Message wird jetzt verarbeitet.`, "Ablauf", "Execute_Alarm", 2); LOG(`Kurze Message ist ${messagekurz}. Die Message wird jetzt verarbeitet.`, "Ablauf", "Execute_Alarm", 2); addMessageToCollector(AlarmType, messagekurz, messagelang); // zur Abarbeitung der Tabelle MessengerScope sendMessage(); Execute_Alerts(Scenario); // zur Abbarbeitung der Tabelle AlertAusgabeDevices Execute_GruppenSchaltungen(Scenario); // zur Abbarbeitung der Tabelle Gruppenschaltungen let jsonString = getState(id_JSON_Alarmmeldung_Aktuell).val || '[]'; // Lade den aktuellen JSON-String des Historie-Datenpunkts AktuelleMeldungenJSON = JSON.parse(jsonString); /* existingJson = [], datum_seit, MessageKurz Status, Alarmtype, raum, deviceType, Scenario,common_name, GeraeteId, id) */ AktuelleMeldungenJSON = CreateJsonAlarmmeldung(AktuelleMeldungenJSON, func_get_datum(), messagekurz, "aktiv", AlarmType, Raum , deviceType, Scenario, commonName, GeraeteID, dpid ) jsonString = JSON.stringify(AktuelleMeldungenJSON); setState(id_JSON_Alarmmeldung_Aktuell, jsonString); // Aktuelle Alarmmeldung wird in JSON-Datenpunkt als String gespeichert generateHtmlTable(AktuelleMeldungenJSON,"Akt") if(pruefeProtokollierung(AlarmType, true)) { jsonString = getState(id_JSON_Alarmmeldung_Historie).val || '[]'; // Lade den aktuellen JSON-String des Historie-Datenpunkts HistorischeMeldungenJSON = JSON.parse(jsonString); /* existingJson = [], datum_seit, MessageKurz Alarmtype, raum, deviceType, Scenario,common_name, GeraeteId, id) */ HistorischeMeldungenJSON = CreateJsonAlarmmeldung(HistorischeMeldungenJSON, func_get_datum(), messagekurz, "aktiv", AlarmType, Raum , deviceType, Scenario, commonName, GeraeteID, dpid ) jsonString = JSON.stringify(HistorischeMeldungenJSON); // Stringify das Json um fuer die speicherung vorzubereiten setState(id_JSON_Alarmmeldung_Historie, jsonString); // Aktuelle Alarmmeldung wird in JSON-Datenpunkt als String gespeichert generateHtmlTable(HistorischeMeldungenJSON,"Hist") } writeProkollExtern( messagekurz, "aktiv",AlarmType, Raum.join(', ') , deviceType, Scenario, commonName, GeraeteID, dpid) } //----------------------------------------------------------------------------------------------------- // Funktion setLEDState zum Setzen des LED-Zustands - Hier wird nach setzen der Datenpunkte der LED Status nachgezogen //----------------------------------------------------------------------------------------------------- function setLEDState(DP_Change, value) { LOG(`LED-State wird gesetzt: DP_Change=${DP_Change}, Wert=${value}`, "DEBUG", "setLEDState", 2); ControlDevices.forEach(device => { let updated = false; for (let i = 1; i <= 8; i++) { const ledKey = `LED${i}`; const dpKey = `DP_2ndPart${i}`; const ledDataPoint = device[ledKey]; const dpSuffix = device[dpKey]; if (!ledDataPoint || !dpSuffix) continue; if (DP_Change === ledDataPoint) { const fullDP = device.DP + dpSuffix; const updateKey = `${device.DeviceID}_${fullDP}`; // Kombiniere DeviceID und fullDP fuer eindeutigen Key const now = Date.now(); // Pruefe, ob fuer diese LED in den letzten 200ms bereits ein Update stattfand if (lastLEDUpdates[updateKey] && (now - lastLEDUpdates[updateKey] < 500)) { LOG(`Update fuer ${fullDP} (Device: ${device.DeviceID}) unterdrueckt - zuletzt aktualisiert vor ${now - lastLEDUpdates[updateKey]}ms`, "DEBUG", "setLEDState", 3); updated = true; // Markiere als aktualisiert, um keine Fehlermeldung zu generieren continue; } if (existsState(fullDP)) { setState(fullDP, value); lastLEDUpdates[updateKey] = now; // Speichere den Zeitpunkt des Updates updated = true; } else { LOG(`Ziel-Datenpunkt ${fullDP} existiert nicht`, "Warn", "setLEDState", 3); } } } if (!updated) { LOG(`Kein entsprechender LED-Datenpunkt fuer ${DP_Change} in Geraet ${device.DeviceID} gefunden.`, "Fehler", "setLEDState", 3); } }); } //----------------------------------------------------------------------------------------------------- // Funktion setStatefromBoard zum Setzen des Zustands basierend auf dem ausgeloesten Datenpunkt // Die Funktion setzt die Datenpunkte der LED und der zugeordneten Datenpunkte // Wenn aber manuellAenderbar = false ist, dann wird die LED zurueckgesetzt durch den Timer wird eine endlosloop verhindert //----------------------------------------------------------------------------------------------------- function setStatefromBoard(triggerDataPoint, value, deviceID) { LOG(`Routine aufgerufen mit: triggerDataPoint=${triggerDataPoint}, value=${value}, deviceID=${deviceID}`, "Ablauf", "setStatefromBoard", 2); const device = ControlDevices.find(dev => dev.DeviceID === deviceID); if (!device) { LOG(`Kein Geraet mit DeviceID '${deviceID}' gefunden`, "ERROR", "setStatefromBoard", 0); return; } for (let i = 1; i <= 8; i++) { const dpPart = device[`DP_2ndPart${i}`]; if (!dpPart) { LOG(`DP_2ndPart${i} nicht definiert fuer ${deviceID}`, "WARN", "setStatefromBoard", 1); continue; } const fullTriggerDP = device.DP + dpPart; if (triggerDataPoint !== fullTriggerDP) continue; const ledKey = `LED${i}`; const ledDataPoint = device[ledKey]; if (!ledDataPoint) { LOG(`Kein LED Datenpunkt unter '${ledKey}' gefunden`, "ERROR", "setStatefromBoard", 0); return; } const dataPoint = ledDataPoint.includes("Scenario") && ledDataPoint.endsWith("Aktiv") ? path + "Controls." + ledDataPoint : ledDataPoint; setState(dataPoint, value); return; // Passende LED gefunden und verarbeitet → fertig } LOG(`Kein passender DP_2ndPart fuer ${triggerDataPoint} in Geraet ${deviceID} gefunden`, "WARN", "setStatefromBoard", 1); } //----------------------------------------------------------------------------------------------------- // Funktion syncControlDevicesInitial zum initialen synchen der states //----------------------------------------------------------------------------------------------------- function syncControlDevicesInitial() { LOG('Starte initiale Synchronisation der ControlDevices', 'Ablauf', 'syncControlDevicesInitial', 2); // Temporaere Variable um Endlosschleifen zu verhindern const isSyncing = {}; // Hauptfunktion fuer die Synchronisation eines einzelnen Geraets function syncDevice(device) { const deviceKey = device.DeviceID; if (isSyncing[deviceKey]) { LOG(`Synchronisation fuer ${deviceKey} bereits im Gange - ueberspringe`, 'DEBUG', 'syncControlDevicesInitial', 3); return; } isSyncing[deviceKey] = true; LOG(`Starte Synchronisation fuer ${deviceKey}`, 'Ablauf', 'syncControlDevicesInitial', 2); try { // Durch alle LEDs des Geraets iterieren for (let i = 1; i <= 8; i++) { const ledKey = `LED${i}`; const dpKey = `DP_2ndPart${i}`; const stateDP = device[ledKey]; const controlDP = device.DP + device[dpKey]; if (!stateDP || !controlDP) continue; // Existenz beider Datenpunkte pruefen if (!existsState(stateDP) || !existsState(controlDP)) { LOG(`Datenpunkt ${stateDP} oder ${controlDP} existiert nicht`, 'WARN', 'syncControlDevicesInitial', 3); continue; } // Wert vom State-Datenpunkt lesen const stateValue = getState(stateDP).val; // Wert auf Control-Device setzen (mit Debounce-Check) const updateKey = `${deviceKey}_${controlDP}`; const now = Date.now(); if (lastLEDUpdates[updateKey] && (now - lastLEDUpdates[updateKey] < 500)) { LOG(`Debounce: Ueberspringe Update fuer ${controlDP} (zuletzt vor ${now - lastLEDUpdates[updateKey]}ms)`, 'DEBUG', 'syncControlDevicesInitial', 3); continue; } setState(controlDP, stateValue); lastLEDUpdates[updateKey] = now; LOG(`Synchronisiert ${stateDP} (${stateValue}) → ${controlDP}`, 'Ablauf', 'syncControlDevicesInitial', 3); } } catch (e) { LOG(`Fehler bei Synchronisation fuer ${deviceKey}: ${e.message}`, 'ERROR', 'syncControlDevicesInitial', 1,"error"); } finally { isSyncing[deviceKey] = false; } } // Alle ControlDevices synchronisieren ControlDevices.forEach(device => { // Nur Geraete mit ELV-SH-SB8 synchronisieren (Beispiel-Filter) if (device.Device === 'ELV-SH-SB8') { syncDevice(device); } }); // Wichtige States initial synchronisieren const importantStates = [ DP_Alarmaktivierung, DP_Still, DP_Einbruchsmeldung, DP_GeoeffnetMeldung, DP_Wassermeldung, DP_Rauchmeldung, DP_Sabotagemeldung, DP_Stoerungsmeldung ]; importantStates.forEach(stateDP => { if (existsState(stateDP)) { const value = getState(stateDP).val; setLEDState(stateDP, value); LOG(`Initialer State ${stateDP} mit Wert ${value} gesetzt`, 'DEBUG', 'syncControlDevicesInitial', 3); } }); LOG('Initiale Synchronisation abgeschlossen', 'Ablauf', 'syncControlDevicesInitial', 2); } //----------------------------------------------------------------------------------------------------- // Function Execute_GruppenSchaltungen Routine zur Schaltung aller Gruppenmitglieder der Tabelle Schaltgruppen - aufruf aus Execute_Alarm // Es werden alle Mitglieder des Arrays "Alert" aus Tabelle ScenarioDefinition durchlaufen und durch ein Mapping auf Tabelle "Schaltgruppenb" werden die Datenpunkte gesetzt // Uebergabewert "key" kann scenario sein oder der direkte key der schaltgruppe //----------------------------------------------------------------------------------------------------- function Execute_GruppenSchaltungen(key) { LOG(`Starte GruppenSchaltung fuer Key: ${key}`, "Info", "Execute_GruppenSchaltungen", 2); // 1. Input-Validierung if (!key) { LOG('Fehler: Kein Key angegeben', 'Error', 'Execute_GruppenSchaltungen', 0); return; } // 2. Ermittle zu verarbeitende Alerts const alertsToProcess = /^Scenario\d{2}$/.test(key) ? (scenarioDefinition[key]?.Alert || []) : [key]; // 3. Verarbeite alle Alerts alertsToProcess.forEach(alert => { if (!Schaltgruppen[alert]) { LOG(`Warnung: Schaltgruppen-Key '${alert}' nicht definiert`, 'Warnung', 'Execute_GruppenSchaltungen', 1); return; } Schaltgruppen[alert].forEach(entry => { // Validierung if (!entry?.Datenpunkt) { LOG(`Fehler: Kein Datenpunkt in Gruppe '${alert}'`, 'Error', 'Execute_GruppenSchaltungen', 0); return; } if (!existsState(entry.Datenpunkt)) { LOG(`Fehler: Datenpunkt '${entry.Datenpunkt}' existiert nicht`, 'Error', 'Execute_GruppenSchaltungen', 0); return; } // Zeitumrechnung Sekunden → Millisekunden (fuer beide Werte) const delayMs = (entry.Delay || 0) * 1000; const dauerMs = (entry.Dauer || 0) * 1000; // Schaltlogik const originalValue = getState(entry.Datenpunkt).val; setTimeout(() => { // Hauptschaltung setState(entry.Datenpunkt, entry.Wert); LOG(`Schalte ${entry.Datenpunkt} auf ${entry.Wert} (nach ${entry.Delay}s)`, "Info", "Execute_GruppenSchaltungen", 2); // Rueckstellung (falls Dauer > 0) if (entry.Dauer > 0) { setTimeout(() => { setState(entry.Datenpunkt, originalValue); LOG(`Ruecksetzung ${entry.Datenpunkt} auf ${originalValue} (nach ${entry.Dauer}s)`, "Info", "Execute_GruppenSchaltungen", 2); }, dauerMs); } }, delayMs); }); }); } //----------------------------------------------------------------------------------------------------- // Function Execute_Alerts Routine zur Schaltung der Sonderfunktionen (HMIP_ASIR und An/Aus-Schaltungen - aufruf aus Execute_Alarm // Es werden alle Mitglieder des Arrays "Alert" aus Tabelle ScenarioDefinition durchlaufen und durch ein Mapping auf Tabelle "AlertAusgabeDevices" werden die Datenpunkte gesetzt //----------------------------------------------------------------------------------------------------- function Execute_Alerts(SelectedScenario) { LOG(`Routine wird ausgefuehrt`, "Ablauf", "Execute_Alerts", 2); LOG(`Selected scenario: ${SelectedScenario}`, "Ablauf", "Execute_Alerts", 2); const AssignedAlerts = scenarioDefinition[SelectedScenario]?.Alert || []; LOG(`Zugeordnete Alerts: ${JSON.stringify(AssignedAlerts)}`, "Ablauf", "Execute_Alerts", 2); AssignedAlerts.forEach(alertKey => { // Hier ist alertKey der String-Key const deviceConfig = AlertAusgabeDevices[alertKey]; if (!deviceConfig) { LOG(`Alarmgeraet ${alertKey} nicht gefunden`, "WARN", "Execute_Alerts", 2); return; } LOG(`Verarbeite Alert: ${alertKey}`, "Ablauf", "Execute_Alerts", 2); setTimeout(() => { // Hier den Key (String) uebergeben, nicht das Objekt triggerAusgabe(alertKey, true); }, deviceConfig.Startdelay || 0); }); } //----------------------------------------------------------------------------------------------------- // Function triggerAusgabe Routine zur Schaltung der Sonderfunktionen (tabelle control devices) //----------------------------------------------------------------------------------------------------- function triggerAusgabe(ausgabegeraetKey, zustand) { LOG(`Routine wird ausgefuehrt`, "Ablauf", "triggerAusgabe", 2); // Durchsuche alle Eintraege in AlertAusgabeDevices nach passendem Geraet und Zustand let device = null; for (const key in AlertAusgabeDevices) { const entry = AlertAusgabeDevices[key]; if (key === ausgabegeraetKey && entry.Zustand === zustand) { device = entry; break; } } if (!device) { LOG(`Kein passender Eintrag fuer Geraet ${ausgabegeraetKey} mit Zustand ${zustand} gefunden`, "WARN", "triggerAusgabe", 0); return; } //---------------HMIP-ASIR Sirenen------------------------------------- if (device.Device === "HMIP-ASIR") { // Pruefe ob alle benoetigten Datenpunkte existieren const dpsExist = [ device.DP + device.AkustikSelectionDP, device.DP + device.Duration_UnitDP, device.DP + device.Duration_ValueDP, device.DP + device.OpticalSelectionDP ].every(dp => existsState(dp)); if (!dpsExist) { LOG("Nicht alle benoetigten Datenpunkte fuer HMIP-ASIR existieren", "WARN", "triggerAusgabe", 0); return; } // Setze alle Werte mit Verzoegerung (wenn Startdelay definiert ist) const setStateDelayed = (dp, value, delay) => { setTimeout(() => { setState(dp, value); LOG(`HMIP-ASIR ${dp} auf ${value} gesetzt`, "Ablauf", "triggerAusgabe", 3); }, delay || 0); }; setStateDelayed(device.DP + device.AkustikSelectionDP, device.valueTon, device.Startdelay); setStateDelayed(device.DP + device.Duration_UnitDP, device.valueUnit, device.Startdelay); setStateDelayed(device.DP + device.Duration_ValueDP, device.valueDuration, device.Startdelay); setStateDelayed(device.DP + device.OpticalSelectionDP, device.valueOpt, device.Startdelay); LOG(`HMIP-ASIR ${device.DP} Konfiguration abgeschlossen`, "Ablauf", "triggerAusgabe", 2); } //---------------Datenpunkt_Standard------------------------------------ if (device.Device === "Datenpunkt_Standard") { if (!existsState(device.DP)) { LOG(`Datenpunkt ${device.DP} existiert nicht`, "WARN", "triggerAusgabe", 0); return; } // Nur fuer Zustand=true (Einschalten) die erweiterten Optionen verarbeiten if (device.Zustand === true) { const originalValue = getState(device.DP).val; // Mit Startdelay verarbeiten falls vorhanden const executeWithDelay = () => { // Hauptschaltung setState(device.DP, device.Aktivierungswert); LOG(`Datenpunkt_Standard ${device.DP} auf ${device.Aktivierungswert} gesetzt (Startdelay: ${device.Startdelay || 0}ms)`, "Ablauf", "triggerAusgabe", 2); // Rueckstellung nach Dauer falls definiert if (device.Dauer > 0 && device.Rueckstellwert !== undefined) { setTimeout(() => { setState(device.DP, device.Rueckstellwert); LOG(`Datenpunkt_Standard ${device.DP} zurueckgesetzt auf ${device.Rueckstellwert} (nach ${device.Dauer}ms)`, "Ablauf", "triggerAusgabe", 2); }, device.Dauer); } }; if (device.Startdelay > 0) { setTimeout(executeWithDelay, device.Startdelay); } else { executeWithDelay(); } } // Einfache Ausschaltung fuer Zustand=false else { setState(device.DP, device.Aktivierungswert); LOG(`Datenpunkt_Standard ${device.DP} auf ${device.Aktivierungswert} gesetzt`, "Ablauf", "triggerAusgabe", 2); } return; } //---------------Datenpunkt_Flash--------------------------------------- if (device.Device === "Datenpunkt_Flash") { if (device.Wiederholung) { if (zustand) { if (!blinkingIntervals[device.DP]) { let counter = 0; let isOn = true; const totalSwitches = device.AnzahlWiederholungen * 2; blinkingIntervals[device.DP] = setInterval(() => { const activationValue = typeof device.Aktivierungswert === 'boolean' ? device.Aktivierungswert : device.Aktivierungswert === 1; setState(device.DP, isOn ? activationValue : !activationValue); LOG(`Datenpunkt_Flash: ${device.DP} ${isOn ? 'ein' : 'aus'} (${counter + 1}/${totalSwitches})`, "Ablauf", "triggerAusgabe", 3); if (++counter >= totalSwitches) { clearInterval(blinkingIntervals[device.DP]); delete blinkingIntervals[device.DP]; LOG("Datenpunkt_Flash: Alarmwiederholungen abgeschlossen", "Ablauf", "triggerAusgabe", 2); } isOn = !isOn; }, device.Frequenz); } } else if (blinkingIntervals[device.DP]) { clearInterval(blinkingIntervals[device.DP]); delete blinkingIntervals[device.DP]; setState(device.DP, false); LOG("Datenpunkt_Flash: Blinken beendet", "Ablauf", "triggerAusgabe", 2); } } else { // Einfaches Setzen ohne Blinken setState(device.DP, device.Aktivierungswert); LOG(`Datenpunkt_Flash ${device.DP} auf ${device.Aktivierungswert} gesetzt`, "Ablauf", "triggerAusgabe", 2); } } } //----------------------------------------------------------------------------------------------------- // Function Execute_STILL Routine schaltet alle Geraete aus Tabelle AlertAusgabeDevices aus //----------------------------------------------------------------------------------------------------- function Execute_STILL() { LOG(`Routine wird ausgefuehrt`, "Ablauf", "Execute_STILL", 2); let delayCounter = 0; const baseDelay = 50; // Basisverzoegerung in ms for (const deviceKey in AlertAusgabeDevices) { const deviceConfig = AlertAusgabeDevices[deviceKey]; if (deviceConfig.Zustand === false) { const currentDelay = delayCounter * baseDelay; // Timeout fuer verzoegertes Absetzen der Befehle setTimeout(() => { LOG(`Deaktiviere Geraet: ${deviceKey} (Typ: ${deviceConfig.Device})`, "Ablauf", "Execute_STILL", 2); switch(deviceConfig.Device) { case "HMIP-ASIR": if (existsState(deviceConfig.DP + deviceConfig.AkustikSelectionDP)) { setState(deviceConfig.DP + deviceConfig.AkustikSelectionDP, deviceConfig.valueTon); setState(deviceConfig.DP + deviceConfig.Duration_ValueDP, deviceConfig.valueDuration); setState(deviceConfig.DP + deviceConfig.OpticalSelectionDP, deviceConfig.valueOpt); } break; case "Datenpunkt_Flash": if (blinkingIntervals[deviceConfig.DP]) { clearInterval(blinkingIntervals[deviceConfig.DP]); delete blinkingIntervals[deviceConfig.DP]; } // Fall-through intentional case "Datenpunkt_Standard": if (existsState(deviceConfig.DP)) { setState(deviceConfig.DP, deviceConfig.Aktivierungswert); } break; } }, currentDelay + (deviceConfig.Startdelay || 0)); delayCounter++; } } setTimeout(() => { LOG("STILL-Routine vollstaendig abgeschlossen", "Ablauf", "Execute_STILL", 2); }, delayCounter * baseDelay + 1000); } //----------------------------------------------------------------------------------------------------- // Function getDeviceTypeForDatapoint bestimmt ob der datenpunkt zu wasser oder rauch oder Sabotage // Theoretisch sind mehrere devicetype zuordnungen moeglich. Sollte in der Praxis nicht vorkommen. Trotzdem wird das durch die Priorisierung abgefangen //----------------------------------------------------------------------------------------------------- function getDeviceTypeForDataPoint(datenpunkt) { const foundTypes = new Set(); // Alle DeviceTypes finden, in denen der Datenpunkt auftaucht for (const raum in raumDefinition) { const definition = raumDefinition[raum]; for (const deviceType in definition) { if (['Type', 'Position'].includes(deviceType)) continue; const datenpunkte = definition[deviceType]; if (Array.isArray(datenpunkte) && datenpunkte.includes(datenpunkt)) { foundTypes.add(deviceType); } } } if (foundTypes.size === 0) { LOG(`Kein gueltiger Devicetype in Tabelle raumDefinition gefunden fuer Datenpunkt: ${datenpunkt}`, "Fehler", "getDeviceTypeForDataPoint", 0); return null; } // Jetzt die Priorisierung aus AlarmTypes anwenden let bestMatch = null; let bestPrio = Infinity; for (const alarm of AlarmTypes) { // in alarmtypes weird das Feld Prio abgefragt for (const devType of alarm.deviceTypes) { if (foundTypes.has(devType) && alarm.Prio < bestPrio) { bestMatch = devType; bestPrio = alarm.Prio; } } } if (!bestMatch) { LOG(`Kein priorisierter Devicetype gefunden fuer Datenpunkt: ${datenpunkt}`, "Warnung", "getDeviceTypeForDataPoint", 0); // Optional: irgendeinen der gefundenen Typen zurueckgeben return Array.from(foundTypes)[0]; } LOG(`Priorisierter Devicetype gefunden fuer Datenpunkt: ${datenpunkt} DeviceType ist:${bestMatch} `, "Warnung", "getDeviceTypeForDataPoint", 2); return bestMatch; } //----------------------------------------------------------------------------------------------------- // Function GetRoomsForDataPoint stellt alle betroffenen Raeume eines datenpunktes in ein Array //----------------------------------------------------------------------------------------------------- function GetRoomsForDataPoint(dpid) { const Raum = []; for (const raumName in raumDefinition) { // Durch alle Raeume iterieren const raum = raumDefinition[raumName]; for (const DeviceType in raum) { // Durch alle DeviceTypes im Raum iterieren (Fenster, Tueren, Bewegung, etc.) if (DeviceType === 'Type' || DeviceType === 'Position') continue; // Ueberspringe spezielle Felder wie Type und Position if (Array.isArray(raum[DeviceType]) && raum[DeviceType].includes(dpid)) { // Pruefe ob der Datenpunkt in dieser Kategorie vorhanden ist Raum.push(raumName); } } } return Raum; } //----------------------------------------------------------------------------------------------------- // Function updateScenario Status aus den Datenpunkten lesen und in die Scenario-Tabelle setzen //----------------------------------------------------------------------------------------------------- function updateScenario() { LOG(`Routine wird ausgefuehrt`, "Ablauf", "updateScenario", 2); AlarmScenarios.forEach(control => { if (control.Control.startsWith('Scenario') && control.Control.endsWith('Aktiv')) { const status = getState(control.Datenpunkt).val; // Status vom Datenpunkt abrufen const match = control.Control.match(/Scenario(\d+)Aktiv/); // Extrahiere die Szenario-Nummer if (match) { const scenarioKey = `Scenario${match[1]}`; // ScenarioXXAktiv if (!ScenarioStatus.hasOwnProperty(scenarioKey)) { LOG(`Warnung: ${scenarioKey} existiert nicht in ScenarioStatus`, "Warnung", "updateScenario", 2); } else { ScenarioStatus[scenarioKey].status = status; // Status setzen // LOG(`Status von ${control.Control} wurde auf ${status} gesetzt`, "Ablauf", "updateScenario", 3); } } else { LOG(`Fehler: Konnte keine Szenarionummer in ${control.Control} finden`, "Fehler", "updateScenario", 2); } } }); } //----------------------------------------------------------------------------------------------------- // Function ScenarioAenderung Es wurde ein Scenario aktiviert oder deaktiviert //----------------------------------------------------------------------------------------------------- function ScenarioAenderung(Scenario, value) { LOG(`Routine wird ausgefuehrt`, "Ablauf", "ScenarioAenderung", 2); updateScenario(); subscribeToAllScenarioDPs() ListSystemSubscriptions() LOG(`Scenario geaendert: ${Scenario} - Neuer Wert = ${value}`, "Ablauf", "ScenarioAenderung", 2); } //----------------------------------------------------------------------------------------------------- // Function Alarmaktivierung ALarm aktiviert oder deaktiviert //----------------------------------------------------------------------------------------------------- function Alarmaktivierung(value) { LOG(`Routine wird ausgefuehrt`, "Ablauf", "Alarmaktivierung", 2); AlarmAktivierungsStatus = getState(DP_Alarmaktivierung).val; LOG(`Alarm Aktivierung geaendert: Neuer Wert = ${value}`, "Ablauf", "Alarmaktivierung", 2); let jsonStringA = getState(id_JSON_Alarmmeldung_Aktuell).val || '[]'; // Lade den aktuellen JSON-String des Historie-Datenpunkts AktuelleMeldungenJSON = JSON.parse(jsonStringA); let jsonStringH = getState(id_JSON_Alarmmeldung_Historie).val || '[]'; // Lade den aktuellen JSON-String des Historie-Datenpunkts HistorischeMeldungenJSON = JSON.parse(jsonStringH); if (!value) { // Alarm wird deaktiviert AktuelleMeldungenJSON = CreateJsonAlarmmeldung(AktuelleMeldungenJSON ,func_get_datum(),MessageAlarmDeactivated); // hinzufuegen AktuelleMeldungenJSON = BereinigeAktuelleMeldungen(AktuelleMeldungenJSON) jsonStringA = JSON.stringify(AktuelleMeldungenJSON); setState(id_JSON_Alarmmeldung_Aktuell, jsonStringA); // Alarmanlage deaktiviert wird in JSON-Datenpunkt als String gespeichert generateHtmlTable(AktuelleMeldungenJSON,"Akt") if(pruefeProtokollierung("AlarmAktivierung", false)) { HistorischeMeldungenJSON = CreateJsonAlarmmeldung(HistorischeMeldungenJSON, func_get_datum(), MessageAlarmDeactivated, func_get_datum()) jsonStringH = JSON.stringify(HistorischeMeldungenJSON); // Stringify das Json um fuer die speicherung vorzubereiten setState(id_JSON_Alarmmeldung_Historie, jsonStringH); // Aktuelle Alarmmeldung wird in JSON-Datenpunkt als String gespeichert generateHtmlTable(HistorischeMeldungenJSON,"Hist") } writeProkollExtern( MessageAlarmDeactivated, func_get_datum(),) const messagekurz = MessageAlarmDeactivated; const messagelang = `${func_get_datum()} ${MessageAlarmDeactivated}`; addMessageToCollector("Alarm_Unscharf", messagekurz, messagelang); // zur Abarbeitung der Tabelle MessengerScope sendMessage(); if (AktionenNachUnscharfSchaltung.length > 0) { LOG(`Starte ${AktionenNachUnscharfSchaltung.length} Schaltgruppe(n)`, 'Info', 'Alarmaktivierung', 2); AktionenNachUnscharfSchaltung.forEach((key, index) => { LOG(`Starte Schaltgruppe ${index + 1}: ${key}`, 'Debug', 'Alarmaktivierung', 3); Execute_GruppenSchaltungen(key); }); } else { LOG('Keine Schaltgruppen in AktionenNachUnscharfSchaltung definiert', 'Info', 'Alarmaktivierung', 3); } } else { // Alarm wird aktiviert const OeffnungLiegtVor = getState(DP_GeoeffnetMeldung).val; let messagekurz = MessageAlarmActivated; let messagelang = `${func_get_datum()} ${MessageAlarmActivated}`; if(OeffnungLiegtVor) { messagekurz = messagekurz + " bei Status geoeffnet" messagelang = messagelang + " bei Status geoeffnet" } AktuelleMeldungenJSON = CreateJsonAlarmmeldung(AktuelleMeldungenJSON ,func_get_datum(),messagekurz ); // hinzufuegen AktuelleMeldungenJSON = BereinigeAktuelleMeldungen(AktuelleMeldungenJSON) let jsonStringA = JSON.stringify(AktuelleMeldungenJSON); setState(id_JSON_Alarmmeldung_Aktuell, jsonStringA); // Alarmanlage aktiviert wird in JSON-Datenpunkt als String gespeichert generateHtmlTable(AktuelleMeldungenJSON,"Akt") if(pruefeProtokollierung("AlarmAktivierung", true)) { HistorischeMeldungenJSON = CreateJsonAlarmmeldung(HistorischeMeldungenJSON, func_get_datum(), messagekurz, func_get_datum()) jsonStringH = JSON.stringify(HistorischeMeldungenJSON); // Stringify das Json um fuer die speicherung vorzubereiten setState(id_JSON_Alarmmeldung_Historie, jsonStringH); // Aktuelle Alarmmeldung wird in JSON-Datenpunkt als String gespeichert generateHtmlTable(HistorischeMeldungenJSON,"Hist") } writeProkollExtern( messagekurz, func_get_datum(),) addMessageToCollector("Alarm_Scharf", messagekurz, messagelang); // zur Abarbeitung der Tabelle MessengerScope sendMessage(); // Ausfuehrung aller Schaltgruppen aus dem AktionenNachScharfSchaltung-Array if (AktionenNachScharfSchaltung.length > 0) { LOG(`Starte ${AktionenNachScharfSchaltung.length} Schaltgruppe(n)`, 'Info', 'Alarmaktivierung', 2); AktionenNachScharfSchaltung.forEach((key, index) => { LOG(`Starte Schaltgruppe ${index + 1}: ${key}`, 'Debug', 'Alarmaktivierung', 3); Execute_GruppenSchaltungen(key); }); } else { LOG('Keine Schaltgruppen in AktionenNachScharfSchaltung definiert', 'Info', 'Alarmaktivierung', 3); } } } //----------------------------------------------------------------------------------------------------- // JSON_Alarmmeldungen_History_Aktualisierung Die Historische Jason wird upgedated entsprechend der Meldungsupdates //----------------------------------------------------------------------------------------------------- function JSON_Alarmmeldungen_History_Aktualisierung(AktuelleMeldungenJSON,Meldungstyp) { LOG(`Routine wird ausgefuehrt mit folgendem Meldungstyp ${Meldungstyp}`, "Ablauf", "JSON_Alarmmeldungen_History_Aktualisierung", 2); // Aktuelle und historische Meldungen laden let jsonAktuellString = getState(id_JSON_Alarmmeldung_Aktuell).val || '[]'; AktuelleMeldungenJSON = JSON.parse(jsonAktuellString); let jsonStringHistorie = getState(id_JSON_Alarmmeldung_Historie).val || '[]'; let HistorischeMeldungenJSON = JSON.parse(jsonStringHistorie); let aktiveMeldungen = HistorischeMeldungenJSON.filter(m => m.Quittiert_seit === "aktiv"); const datumJetzt = func_get_datum(); let meldungenGeaendert = false; // Bestimme die zu verarbeitenden Alarmtypen const zuVerarbeitendeTypen = (Meldungstyp === "AllAlarmtypes") ? [...new Set(HistorischeMeldungenJSON.map(m => m.Alarmtype))] // Eindeutige Alarmtypen aus Historie : [Meldungstyp]; zuVerarbeitendeTypen.forEach(typ => { if (!typ || typ === NichtRelevantText) return; // Nicht relevante Eintraege ueberspringen // Pruefe ob dieser Alarmtyp aktuell aktiv ist const istAktuell = AktuelleMeldungenJSON.some(e => e.Alarmtype === typ); // Nur nicht-aktive Alarmtypen quittieren if (!istAktuell) { // Finde alle aktiven Meldungen dieses Alarmtyps const zuQuittierendeMeldungen = aktiveMeldungen.filter(m => m.Alarmtype === typ); if (zuQuittierendeMeldungen.length > 0) { zuQuittierendeMeldungen.forEach(meldung => { meldung.Quittiert_seit = datumJetzt; meldungenGeaendert = true; LOG(`Alarmmeldung quittiert: Alarmtype = '${meldung.Alarmtype}', Quittiert_seit = '${datumJetzt}'`, "Ablauf", "JSON_Alarmmeldungen_History_Aktualisierung", 2); }); } else { LOG(`Keine aktiven Meldungen vom Typ '${typ}' zum Quittieren gefunden.`, "Ablauf", "JSON_Alarmmeldungen_History_Aktualisierung", 3); } } else { LOG(`Alarmtyp '${typ}' ist aktiv – keine Quittierung.`, "Ablauf", "JSON_Alarmmeldungen_History_Aktualisierung", 3); } }); if (meldungenGeaendert) { const updatedString = JSON.stringify(HistorischeMeldungenJSON); setState(id_JSON_Alarmmeldung_Historie, updatedString); generateHtmlTable(HistorischeMeldungenJSON,"Hist") LOG(`Quittierungen fuer '${Meldungstyp}' wurden erfolgreich vorgenommen.`, "Ablauf", "JSON_Alarmmeldungen_History_Aktualisierung", 2); } else { LOG(`Keine quittierbaren Meldungen fuer '${Meldungstyp}' gefunden.`, "Ablauf", "JSON_Alarmmeldungen_History_Aktualisierung", 3); } } //----------------------------------------------------------------------------------------------------- // Funktion BereinigeAktuelleMeldungen Die Aktuelle JSON wird analysiert. Es duerfen keine doppelten Meldungen je Alarmtype vorkommen //----------------------------------------------------------------------------------------------------- function BereinigeAktuelleMeldungen(AktuelleMeldungenJSON) { LOG(`Starte Bereinigung der aktuellen Meldungen`, "Ablauf", "BereinigeAktuelleMeldungen", 2); if (!Array.isArray(AktuelleMeldungenJSON)) { LOG(`Ungueltige Eingabe - erwartet Array, erhalten: ${typeof AktuelleMeldungenJSON}`, "Fehler", "BereinigeAktuelleMeldungen", 1); return AktuelleMeldungenJSON; } // Gruppiere Meldungen nach Alarmtype const gruppierteMeldungen = AktuelleMeldungenJSON.reduce((acc, meldung) => { const alarmtype = meldung.Alarmtype || NichtRelevantText; if (!acc[alarmtype]) { acc[alarmtype] = []; } acc[alarmtype].push(meldung); return acc; }, {}); // Behalte nur die neueste Meldung pro Alarmtype const bereinigteMeldungen = Object.entries(gruppierteMeldungen).map(([alarmtype, meldungen]) => { if (meldungen.length === 1) { return meldungen[0]; // Nur eine Meldung - nichts zu tun } LOG(`Mehrfacheintraege gefunden fuer Alarmtype '${alarmtype}' (${meldungen.length} Eintraege)`, "Ablauf", "BereinigeAktuelleMeldungen", 2); // Finde die neueste Meldung const neuesteMeldung = meldungen.reduce((neueste, aktuelle) => { const neuesteDatum = parseDatum(neueste.datum_seit); const aktuelleDatum = parseDatum(aktuelle.datum_seit); return aktuelleDatum > neuesteDatum ? aktuelle : neueste; }); LOG(`Behalte nur Eintrag vom ${neuesteMeldung.datum_seit} fuer Alarmtype '${alarmtype}'`, "Ablauf", "BereinigeAktuelleMeldungen", 2); return neuesteMeldung; }); // Protokolliere Anzahl der entfernten Eintraege const entfernteEintraege = AktuelleMeldungenJSON.length - bereinigteMeldungen.length; if (entfernteEintraege > 0) { LOG(`${entfernteEintraege} doppelte Eintraege wurden entfernt`, "Ablauf", "BereinigeAktuelleMeldungen", 1); } return bereinigteMeldungen; } //----------------------------------------------------------------------------------------------------- // Funktion RemoveAlarmtypeFromJSON Die Aktuelle JSON wird analysiert. der uebergebene Alarmtype wird aus AktuelleMeldungenJSON geloescht //----------------------------------------------------------------------------------------------------- function RemoveAlarmtypeFromJSON(AktuelleMeldungenJSON, Alarmtype) { LOG(`Starte Entfernung von Alarmtype '${Alarmtype}' aus JSON`, "Ablauf", "RemoveAlarmtypeFromJSON", 2); // Validierung der Eingabeparameter if (!Array.isArray(AktuelleMeldungenJSON)) { LOG(`Ungueltige Eingabe - erwartet Array, erhalten: ${typeof AktuelleMeldungenJSON}`, "Fehler", "RemoveAlarmtypeFromJSON", 1); return AktuelleMeldungenJSON; } if (!Alarmtype || Alarmtype === NichtRelevantText) { LOG(`Ungueltiger Alarmtype '${Alarmtype}' - Abbruch`, "Fehler", "RemoveAlarmtypeFromJSON", 1); return AktuelleMeldungenJSON; } // Filtere alle Eintraege die NICHT dem gesuchten Alarmtype entsprechen const gefilterteMeldungen = AktuelleMeldungenJSON.filter(meldung => { const currentAlarmtype = meldung.Alarmtype || NichtRelevantText; return currentAlarmtype !== Alarmtype; }); // Protokolliere das Ergebnis const entfernteAnzahl = AktuelleMeldungenJSON.length - gefilterteMeldungen.length; if (entfernteAnzahl > 0) { LOG(`${entfernteAnzahl} Eintraege fuer Alarmtype '${Alarmtype}' wurden entfernt`, "Ablauf", "RemoveAlarmtypeFromJSON", 1); } else { LOG(`Keine Eintraege fuer Alarmtype '${Alarmtype}' gefunden`, "Ablauf", "RemoveAlarmtypeFromJSON", 2); } return gefilterteMeldungen; } //----------------------------------------------------------------------------------------------------- // Funktion parseDatum Umwandeln JSON Datumsformat DD.MM.25 HH:MM:SS Uhr in Datum und Zeit //----------------------------------------------------------------------------------------------------- function parseDatum(datumString) { try { const [datePart, timePart] = datumString.split(' '); const [day, month, year] = datePart.split('.'); const [hours, minutes, seconds] = timePart.split(':'); // 20 als Prefix fuer 20xx Jahre (da Format "25" fuer 2025) return new Date(`20${year}-${month}-${day}T${hours}:${minutes}:${seconds}`); } catch (e) { LOG(`Fehler beim Parsen des Datums '${datumString}': ${e}`, "Fehler", "BereinigeAktuelleMeldungen", 1); return new Date(0); // Gibt minimales Datum zurueck, falls Parsing fehlschlaegt } } //----------------------------------------------------------------------------------------------------- // addMessageToCollector Messages werden unter beruecksichtigung der folgenden Objekte in den MessageCollector genommen // MessengerScope, Services, AlarmTypes, TextTypeKurz //----------------------------------------------------------------------------------------------------- function addMessageToCollector(messageType, MessageKurz, MessageLang) { // Pruefe, ob der messageType in MessageSendCollector existiert, falls nicht initialisieren if (!MessageSendCollector[messageType]) { MessageSendCollector[messageType] = {}; // Initialisiere fuer den messageType ein leeres Objekt } // Hole die Konfiguration des Messengers fuer den jeweiligen messageType let messengerConfig = MessengerScope[messageType] || MessengerScope['Sonstige'] || Array(services.length).fill(false); // Verarbeite die Konfiguration fuer jeden Service messengerConfig.forEach((isActive, index) => { if (isActive) { const service = services[index]; const instance = MessengerInstanz[index]; // Pruefe, ob eine gueltige Instanz vorhanden ist if (instance !== null && !isNaN(instance) && instance >= 0) { // Initialisiere MessageSendCollector[messageType][service], falls nicht vorhanden if (!MessageSendCollector[messageType][service]) { MessageSendCollector[messageType][service] = []; // Initialisiere das Array fuer diesen Service } const messageToAdd = TextTypeKurz[index] ? MessageKurz : MessageLang; const existingMessages = MessageSendCollector[messageType][service]; // **Doppelpruefung:** Gibt es bereits eine Nachricht mit demselben Text und Instanz? const isDuplicate = existingMessages.some(msg => msg.message.trim() === messageToAdd.trim() && msg.instance === instance); if (!isDuplicate) { MessageSendCollector[messageType][service].push({ message: messageToAdd + '\n', instance: instance }); LOG(`Nachricht hinzugefuegt fuer ${messageType} - Service: ${service} - Instanz: ${instance}`, "Ablauf", "addMessageToCollector", 3); } else { LOG(`Doppelte Nachricht erkannt und verworfen fuer ${messageType} - Service: ${service} - Instanz: ${instance}`, "DEBUG", "addMessageToCollector", 3); } } else { LOG(`Ungueltige Instanz fuer Service: ${service} (Instanz: ${instance})`, "WARN", "addMessageToCollector", 0); } } }); } //----------------------------------------------------------------------------------------------------- // Aktiviert oder deaktiviert Szenarien basierend auf dem uebergebenen Array und Boolean // fuer Scenarioaktivierung bei Gaesten und Abesenheiten // @param {string[]} scenariosArray - Array von Szenario-Namen (koennen mit Minus beginnen) // @param {boolean} activate - true fuer Aktivierung, false fuer Deaktivierung //----------------------------------------------------------------------------------------------------- function AutoScenarioActivation(scenariosArray, activate) { scenariosArray.forEach(scenario => { // Pruefen, ob das Szenario mit einem Minus beginnt (umgekehrte Logik) const isInverted = scenario.startsWith('-'); const cleanScenarioName = isInverted ? scenario.substring(1) : scenario; const fullPath = controlsPath + cleanScenarioName; // Logik bestimmen let shouldActivate; if (isInverted) { // Umgekehrte Logik: wenn activate true ist, setzen wir false und umgekehrt shouldActivate = !activate; } else { // Normale Logik: direkt den activate-Parameter uebernehmen shouldActivate = activate; } // Zustand setzen setState(fullPath, shouldActivate); // Optional: Logging fuer Debug-Zwecke console.log(`Setze ${fullPath} auf ${shouldActivate} (${isInverted ? 'invertiert' : 'normal'})`); }); } //----------------------------------------------------------------------------------------------------- // sendMessage Hier werden die Nachrichten fuer den jeweiligen Service aufbereitet //----------------------------------------------------------------------------------------------------- function sendMessage(messageType = null) { LOG(`Routine sendMessage wird ausgefuehrt, meldungsart: ${messageType}`, "Ablauf", "sendMessage", 2); const messageTypesToProcess = messageType ? [messageType] : Object.keys(MessageSendCollector); // Bestimme die MessageTypes, die verarbeitet werden sollen messageTypesToProcess.forEach((type) => { const messagesByService = MessageSendCollector[type]; if (messagesByService) { Object.keys(messagesByService).forEach((service) => { const serviceMessages = messagesByService[service]; let messagesToRemove = []; serviceMessages.forEach((item) => { const { message, instance } = item; sendToService(service, message, instance); messagesToRemove.push(item); }); messagesToRemove.forEach((item) => { // Entferne alle verarbeiteten Nachrichten const serviceMessagesIndex = serviceMessages.indexOf(item); if (serviceMessagesIndex > -1) { serviceMessages.splice(serviceMessagesIndex, 1); } }); }); } }); } //----------------------------------------------------------------------------------------------------- // sendToService - hier wird der Versand vorgenommen // Reihenfolge: Email, WhatsApp, Signal, Telegram, Pushover, Pushsafer // MessengerInstanz = [0, 0, 0, 0, 0, 0] Instanzen der Messenger-Dienste in //----------------------------------------------------------------------------------------------------- function sendToService(service, message, instance) { LOG(`Message wird versendet mit: ${service} : ${instance} : ${message}`, "Ablauf", "sendToService", 2); switch (service) { case "email": sendTo(`email.${instance}`, "send", { text: message, to: emailAddresse, subject: Headline }); break; case "whatsApp": sendTo(`whatsapp-cmb.${instance}`, "send", { text: message }); break; case "Signal": sendTo(`signal-cmb.${instance}`, "send", { text: message }); break; case "Telegram": sendTo(`telegram.${instance}`, "send", { text: message, user: TelegramUser // Telegram User ID, um den Nachrichteneempfaenger festzulegen }); break; case "Pushover": sendTo(`pushover.${instance}`, "send", { message: message, sound: "" }); break; case "Pushsafer": sendTo(`pushsafer.${instance}`, "send", { message: message, title: Headline }); break; case "Sprache": const sayitadapter = "sayit."+instance+".tts.text"; if(!existsState(sayitadapter) ){ LOG(`Sayit-Adapter nicht konfiguriert: ${sayitadapter}`, "WARN", "sendToService", 2); break; } if(IsTimeInRange(zeitvon,zeitbis)){ setState(sayitadapter,message) } break; default: LOG(`Unbekannter Service: ${service}`, "WARN", "sendToService", "warn"); } } //----------------------------------------------------------------------------------------------------- // Funktion unsubscribeAll unsubscribed und loescht alle subscriptions //----------------------------------------------------------------------------------------------------- function unsubscribeAll() { LOG(`Loesche alle Subscriptions`, "Ablauf", "unsubscribeAll", 2); let count = 0 GeneratedSubscriptions.forEach(subscription => { count = count + 1; unsubscribe(subscription); }); LOG(`Zur Neuerstellung der Subscriptions wurden fuer ${count} zunaechst geloescht`, "Ablauf", "unsubscribeAll", 3); GeneratedSubscriptions.length = 0; // Array leeren } //----------------------------------------------------------------------------------------------------- // Funktion IsTimeInRange - Ermittlung ob die aktuelle Zeit in der vorgegebenen von bis Zeit liegt //----------------------------------------------------------------------------------------------------- function IsTimeInRange(zeitvon, zeitbis) { if (!/^\d{2}:\d{2}$/.test(zeitvon) || !/^\d{2}:\d{2}$/.test(zeitbis)) { // Ueberpruefen, ob die Eingaben im gueltigen Format sind LOG(`Es wurde ein Fehler bei der Konfiguration von zeitvon/zeitbis festgestellt (Sprach message)`, "WARN", "IsTimeInRange", 0); return false; } let isInRange = false; const getCurrentTime = () => { // Funktion, um die aktuelle Zeit im Format HH:MM zu bekommen const now = new Date(); const hours = String(now.getHours()).padStart(2, '0'); const minutes = String(now.getMinutes()).padStart(2, '0'); return `${hours}:${minutes}`; } const timeToMinutes = (zeit) => { // Zeit in Minuten umwandeln fuer den Vergleich const [hours, minutes] = zeit.split(':').map(Number); return hours * 60 + minutes; } const zeitvonMinuten = timeToMinutes(zeitvon); // Umwandlung der Zeiten in Minuten const zeitbisMinuten = timeToMinutes(zeitbis); const currentTime = getCurrentTime(); const currentMinutes = timeToMinutes(currentTime); if (zeitvon === zeitbis) { // Wenn zeitvon und zeitbis identisch sind, gilt "immer" isInRange = true; } // Wenn zeitvon kleiner als zeitbis, dann sind beide im gleichen Tag if (zeitvonMinuten < zeitbisMinuten) { if (currentMinutes >= zeitvonMinuten && currentMinutes <= zeitbisMinuten) { isInRange = true; } } else { if (currentMinutes >= zeitvonMinuten || currentMinutes <= zeitbisMinuten) { // Wenn zeitvon groesser als zeitbis, dann ist zeitbis am naechsten Tag isInRange = true; } } LOG(`Rueckgabewert zeitvon ist ${zeitvon} und zeitbis ist ${zeitbis}`, "Ergebnis", "IsTimeInRange", 3); return isInRange; } //----------------------------------------------------------------------------------------------------- // Funktion CreateJsonAlarmmeldung erzeugen einer JSON Tabelle //----------------------------------------------------------------------------------------------------- function CreateJsonAlarmmeldung(existingJson = [], datum_seit,Nachricht, Quittiert_seit, Alarmtype=null, raum=null, deviceType=null,Scenario=null, common_name=null, GeraeteId=null, id=null) { let parsedJson; if (Array.isArray(existingJson)) { // ueberpruefen, ob existingJson bereits ein Array ist parsedJson = existingJson; // Nur das Array uebernehmen } else { parsedJson = []; // Falls es kein Array ist, setze es auf ein leeres Array } const neuerEintrag = { datum_seit: datum_seit || func_get_datum(), // Fallback auf aktuelles Datum Alarmtype: Alarmtype || NichtRelevantText, Raum: raum || NichtRelevantText, deviceType: deviceType || NichtRelevantText, Scenario: Scenario || NichtRelevantText, common_name: common_name || NichtRelevantText, GeraeteId: GeraeteId || NichtRelevantText, Nachricht: Nachricht || 'Keine Meldung', // Alarmmeldung mit Geraeteinfos Quittiert_seit: Quittiert_seit || "aktiv", // status ob eine message bereits aufgeboben ist. wenn aufgehoben wird ein Datum eingestellt DataPoint: id || NichtRelevantText, }; parsedJson.unshift(neuerEintrag); return parsedJson; // Gib das aktualisierte Array zurueck } //----------------------------------------------------------------------------------------------------- // Funktion Refresh_History Loeschen der History JSON //----------------------------------------------------------------------------------------------------- function Refresh_History() { LOG(`Refesh History aufgerufen`, "Ablauf", "unsubscribeAll", 2); const verbleibendeTage = getState(id_History_VerbleibendeTage).val || 7; const heute = new Date(); let jsonString = getState(id_JSON_Alarmmeldung_Historie).val || '[]'; let HistorischeMeldungenJSON = JSON.parse(jsonString); const AnzahlHistMeldungenVorher = HistorischeMeldungenJSON.length HistorischeMeldungenJSON = HistorischeMeldungenJSON.filter(meldung => { if (meldung.Quittiert_seit === "aktiv") { return true; } try { const meldungsDatum = parseDatum(meldung.datum_seit); // Explizite Verwendung von getTime() fuer numerische Berechnung const differenzInTagen = Math.floor((heute.getTime() - meldungsDatum.getTime()) / (1000 * 60 * 60 * 24)); return differenzInTagen < verbleibendeTage; } catch (e) { LOG(`Fehler bei der Verarbeitung: ${JSON.stringify(meldung)}. Fehler: ${e}`, "Fehler", "Refresh_History", 1); return false; } }); setState(id_JSON_Alarmmeldung_Historie, JSON.stringify(HistorischeMeldungenJSON)); generateHtmlTable(HistorischeMeldungenJSON,"Hist") LOG(`Historische Meldungen aktualisiert. Anzahl Meldungen vorher:${AnzahlHistMeldungenVorher} Verbleibende Meldungen: ${HistorischeMeldungenJSON.length}`, "Info", "Refresh_History", 1); } //----------------------------------------------------------------------------------------------------- // Funktion schreibt einen Logeintrag in das Filesystem und auch in das interne Log-System //----------------------------------------------------------------------------------------------------- function writeProkollExtern(Nachricht, quittiert, Alarmtype="", raum="", deviceType="", Scenario="",common_name="",GeraeteId="", dpid="") { /*writeProkollExtern( Nachricht, quittiert,Alarmtype, raum, deviceType, Scenario, common_name, GeraeteId, id) Nachricht und quittiert sind mussfelder */ if (!ProkollExtern) return; const fs = require('fs'); // enable write fuer externes log const logdate = formatDate(new Date(), "TT.MM.JJJJ"); const logtime = formatDate(new Date(), "SS:mm:ss"); const logEntry = `${logdate} ;${logtime} ;${Alarmtype} ;${raum} ; ${deviceType} ;${Scenario} ;${common_name};${GeraeteId};${Nachricht};${quittiert};${dpid}\n`; const headerLine = "Datum;Uhrzeit;Alarmtype;Raum;GeraeteTyp;Szenario;Geraetebezeichnung;GeraeteID;Nachricht;quittiert;Datenpunkt\n"; fs.readFile(PathProkollExtern, 'utf8', function(err, data) { if (!err) { fs.appendFileSync(PathProkollExtern, logEntry, 'utf8'); } else { LOG(`Logfile nicht gefunden - wird angelegt`, "Ablauf", "writelog", 0); fs.writeFileSync(PathProkollExtern, headerLine + logEntry, 'utf8'); } }); } //----------------------------------------------------------------------------------------------------- // Funktion ExtractMetaData Verarbeitung des Datenpunkts zur Extraktion der Metadaten: GeraeteId, meldungsart //----------------------------------------------------------------------------------------------------- function ExtractMetaData(datenpunkt, ExtractType) { // LOG(`Routine ExtractMetaData wird ausgefuehrt datenpunkt fuer Metadaten ${datenpunkt} ExtractType ${ExtractType} `, "Ablauf", "ExtractMetaData", 2); ExtractType = ExtractType.toUpperCase(); const teile = datenpunkt.split('.'); // Splitte den Datenpunkt in Teile const adapter = teile[0]; // Der erste Teil ist immer der Adapter const instance = teile[1]; // Der zweite Teil ist immer die Instance const struktur = StrukturDefinition.find(s => s.Adapter === adapter); // Strukturzeile aus Tabelle StrukturDefinition fuer den Adapter aus dem uebergebenen DP const GeraeteID = teile[struktur.GeraeteID]; // Extrahiere Felder basierend auf der Strukturdefinition if (!struktur) { LOG(`Die Struktur fuer MetaDaten Extract ist nicht korrekt eingestellt. Der uebergebenen datenpunkt war:$(datenpunkt)- Abbruch`, "Fehler", "ExtractMetaData", 0); return null; } if(ExtractType === "ADAPTER") { return adapter; } if(ExtractType === "GERAETEID") { return GeraeteID; } if(ExtractType === "MELDUNGSART") { const meldungsart = teile[struktur.AlarmFeld]; // Extrahiere Felder basierend auf der Strukturdefinition return meldungsart; } if(ExtractType === "NATIVETYPE" && typeof struktur.nativeType === 'number') { const MetaDP = datenpunkt.split('.').slice(0, struktur.nativeType).join('.'); // ergibt den Datenpunkt fuer getObject z.B. hm-rpc.1.00085D89B14067 const nativeType = getObject(MetaDP).native.TYPE; return nativeType; } if(ExtractType === "NATIVETYPE" && typeof struktur.nativeType === 'string') { // bei string liegt der native-type im datenpunkt let DPNativeTypeDP = struktur.nativeType; DPNativeTypeDP = DPNativeTypeDP.replace('xidx', GeraeteID); DPNativeTypeDP = DPNativeTypeDP.replace('xinstancex', instance); let nativeType = getState(DPNativeTypeDP).val return nativeType; } if(ExtractType === "COMMONNAME" && typeof struktur.common_name === 'number') { const MetaDP = datenpunkt.split('.').slice(0, struktur.common_name).join('.'); // ergibt den Datenpunkt fuer getObject z.B. hm-rpc.1.00085D89B14067 const common_name = getObject(MetaDP).common.name; return common_name; } if(ExtractType === "COMMONNAME" && typeof struktur.common_name === 'string') { // bei string liegt der common_name im datenpunkt let DPcommmon_name = struktur.common_name; DPcommmon_name = DPcommmon_name.replace('xidx', GeraeteID).replace('xinstancex', instance); let common_name = getState(DPcommmon_name).val return common_name; } LOG(`Der Datenpunkt/ExtractType, die erforderlich zur Ermittlung von Metadaten sind nicht bekannt Der Datenpunkt lautet ${datenpunkt} ExtractType ${ExtractType} - Abbruch`, "Fehler", "ExtractMetaData", 0); return null } //----------------------------------------------------------------------------------------------------- // Funktion CheckForNaNinFilterDefinition checked ob in der filter Tabelle eintragungen ausserhalb der hochkomma vorgenommen wurden (z.B. Minus ausserhalb der Hochkomma ist ein NaN) //----------------------------------------------------------------------------------------------------- function CheckForNaNinFilterDefinition(filterDef) { for (const filterKey in filterDef) { const filter = filterDef[filterKey]; for (const key of ['raumTyp', 'raumPosition', 'geraetetypen', 'raeume', 'Datenpunkte']) { if (Array.isArray(filter[key])) { for (const val of filter[key]) { if (typeof val !== 'string' || val === 'NaN') { LOG(`[Fehler] In "${filterKey}.${key}" wurde ein ungueltiger Wert erkannt: ${val} Beispiel: du hast ein Minus asserhalb der Hochkomma im Filter eingegeben`, "error", "CheckForNaNinFilterDefinition", 0,"error"); } } } } } } //----------------------------------------------------------------------------------------------------- // func_get_datum aktuelles Datum formatiert //----------------------------------------------------------------------------------------------------- function func_get_datum(id) { let datum; if (id && getState(id)) { datum = formatDate(getState(id).lc, "TT.MM.JJ SS:mm:ss"); } else { datum = formatDate(new Date(), "TT.MM.JJ SS:mm:ss"); // Aktuelles Datum } let datumDate = new Date(datum); // korrekte verwendung des datumsformates let cutoffDate = new Date('1971-01-01T01:00:00'); return datumDate < cutoffDate ? '' : `${datum} Uhr`; } //----------------------------------------------------------------------------------------------------- // pruefeProtokollierung // ueberprueft ob ein JSON Protokoll geschrieben werden soll //----------------------------------------------------------------------------------------------------- function pruefeProtokollierung(typ, aktiv) { // Suche den passenden Eintrag in der Tabelle const eintrag = JSON_Protokollierung.find( item => item.Typ === typ && item.Aktiv === aktiv ); if (!eintrag) { return true; } // im zweifel wird das Protokoll geschrieben const alarmScharf = getState(DP_Alarmaktivierung).val; // Lies den Alarmstatus aus // Ueberpruefe die Bedingungen const protokollierenGrundsaetzlich = eintrag.Protokoll_Schreiben; const nurBeiAlarm = eintrag.NurWennAlarmScharf !== null ? eintrag.NurWennAlarmScharf : true; // Wenn NurWennAlarmScharf true ist, muss der Alarm auch scharf sein const alarmBedingungErfuellt = !nurBeiAlarm || alarmScharf; // Alle drei Bedingungen muessen erfuellt sein return protokollierenGrundsaetzlich && alarmBedingungErfuellt; } //----------------------------------------------------------------------------------------------------- // ReplaceString // ersetzen entsprechend tabelle replacements //----------------------------------------------------------------------------------------------------- function ReplaceString(string) { for (const [key, value] of Object.entries(replacements)) { // Escape den Punkt (.) fuer den regulaeren Ausdruck const escapedKey = key.replace('.', '\\.'); string = string.replace(new RegExp(escapedKey, 'g'), value); } return string; } //----------------------------------------------------------------------------------------------------- // getRaumUebersichtVerdichtet // erzeugt eine Raumkarte der aktivierten Geraete //----------------------------------------------------------------------------------------------------- function getRaumUebersichtVerdichtet() { const overview = {}; for (const scenario in ScenarioToDPsTable) { const dps = ScenarioToDPsTable[scenario]; for (const dp of dps) { for (const raum in raumDefinition) { const def = raumDefinition[raum]; for (const geraetetyp in def) { if (geraetetyp === "Type" || geraetetyp === "Position") continue; const dpList = def[geraetetyp]; if (Array.isArray(dpList) && dpList.includes(dp)) { // Initialisierung if (!overview[raum]) { overview[raum] = { szenarien: new Set(), geraete: {}, gezaehlteDPs: {} // zur Vermeidung von Doppelzaehlungen }; } // Szenario hinzufuegen overview[raum].szenarien.add(scenario); // Doppelzaehlung pro Geraetetyp verhindern if (!overview[raum].gezaehlteDPs[geraetetyp]) { overview[raum].gezaehlteDPs[geraetetyp] = new Set(); } if (!overview[raum].gezaehlteDPs[geraetetyp].has(dp)) { overview[raum].gezaehlteDPs[geraetetyp].add(dp); if (!overview[raum].geraete[geraetetyp]) { overview[raum].geraete[geraetetyp] = 0; } overview[raum].geraete[geraetetyp]++; } } } } } } // Ergebnis formatieren const result = {}; for (const raum in overview) { result[raum] = { szenarien: Array.from(overview[raum].szenarien), geraete: overview[raum].geraete }; } return result; } //----------------------------------------------------------------------------------------------------- // ReplaceString // ersetzen entsprechend tabelle replacements //----------------------------------------------------------------------------------------------------- function renderScenarioRoomDeviceHTML() { const map = getRaumUebersichtVerdichtet(); const styles = HTML_Raumzuordnungen_WidthAndStyles.styles; const headers = HTML_Raumzuordnungen_WidthAndStyles.headerTexts; const widths = HTML_Raumzuordnungen_WidthAndStyles.columnWidths; const visibleCols = HTML_Raumzuordnungen_WidthAndStyles.columns; let html = ``; html += ``; if (visibleCols.raum?.visible) html += ``; if (visibleCols.scenario?.visible) html += ``; if (visibleCols.deviceType?.visible) html += ``; html += ``; const sortedRaeume = Object.keys(map).sort(); for (const raum of sortedRaeume) { html += ``; if (visibleCols.raum?.visible) { html += ``; } if (visibleCols.scenario?.visible) { const szenarien = map[raum].szenarien.map(s => { if (s === "NichtAktiv") return "nicht aktiv"; return `${s} (${scenarioDefinition[s]?.name || "?"})`; }).join(', '); html += ``; } if (visibleCols.deviceType?.visible) { const geraete = Object.entries(map[raum].geraete) .map(([typ, count]) => `${typ}: ${count}`) .join('
'); html += ``; } html += ``; } if (sortedRaeume.length === 0) { html += ``; } html += `
${headers.raum}${headers.scenario}${headers.deviceType}
${raum}${szenarien}${geraete}
Keine Zuordnungen gefunden.
`; return html; } //----------------------------------------------------------------------------------------------------- // generateHtmlTable // Generieren HtML Table //----------------------------------------------------------------------------------------------------- function generateHtmlTable(data, HistOderAkt) { LOG(`Routine wird ausgefuehrt fuer HTML-Datenset ${HistOderAkt}`, "Ablauf", "generateHtmlTable", 2); if (!UpdateHTML_Datenpunkte) return; const tableId = "alarm-table-" + Math.random().toString(36).substr(2, 9); const w = HTML_TableWidthsAndStyles.columnWidths; const s = HTML_TableWidthsAndStyles.styles; const h = HTML_TableWidthsAndStyles.headerTexts; const c = HTML_TableWidthsAndStyles.columns; // Tabellenstruktur let table = ` `; // Tabellenkopf Object.keys(c).forEach(colKey => { if (c[colKey].visible) { table += ` `; } }); table += ``; // Tabelleninhalt data.forEach((item, index) => { const rowColor = index % 2 === 0 ? s.evenRowColor : s.oddRowColor; const textColor = index % 2 === 0 ? s.evenRowTextColor : s.oddRowTextColor; table += ``; Object.keys(c).forEach(colKey => { if (!c[colKey].visible) return; const value = (() => { switch(colKey) { case 'datum': return item.datum_seit || NichtRelevantText; case 'alarmtype': return item.Alarmtype || NichtRelevantText; case 'raum': return item.Raum || NichtRelevantText; case 'deviceType': return item.deviceType || NichtRelevantText; case 'scenario': return item.Scenario || NichtRelevantText; case 'deviceName': return item.common_name || NichtRelevantText; case 'deviceId': return item.GeraeteId || NichtRelevantText; case 'message': return item.Nachricht || 'Keine Meldung'; case 'quittiert': return item.Quittiert_seit || 'aktiv'; default: return NichtRelevantText; } })(); table += ``; }); table += ``; }); table += `
${h[colKey]}
${value}
`; const targetDP = HistOderAkt === "Akt" ? id_HTML_Alarmmeldung_Aktuell : id_HTML_Alarmmeldung_Historie; setState(targetDP, table); } //----------------------------------------------------------------------------------------------------- // Funktion schreibt die System-Log-Eintraege in ein externes CSV - File //----------------------------------------------------------------------------------------------------- function LOG(Message,Kategorie,Routine, Level, type) { if(type !== "warn" && type !== "error") {type = "info"} if (!SystemLog && debugLevel >= Level) { log(Message+" Routine:"+Routine,type); return;} // Wenn SystemLog false ist und der Debug-Level hoeher oder gleich dem uebergebenen Level, schreibe normalen Logeintrag if (Level === 0) { log(Message+" Routine:"+Routine,type);} // bei level 0 soll auf jeden fall auch in das normale log geschrieben werden if (SystemLog && debugLevel >= Level) { // Wenn SystemLog true ist und der Debug-Level hoeher oder gleich dem uebergebenen Level const fs = require('fs'); const now = new Date(); const logdate = `${now.getDate().toString().padStart(2, '0')}.${(now.getMonth() + 1).toString().padStart(2, '0')}.${now.getFullYear()}`; const logtime = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}:${now.getSeconds().toString().padStart(2, '0')}:${now.getMilliseconds().toString().padStart(3, '0')}`; const logEntry = `${logdate};${logtime};${Level};${Kategorie};${Routine};${Message}\n`; const headerLine = "Datum;Uhrzeit;Debug Level;Kategorie;Routine;Log-Message\n"; const logFilePath = PathSystemLog || './defaultLog.csv'; // falls PathSystemLog nicht definiert ist, standardmaessigen Pfad verwenden try { if (!fs.existsSync(logFilePath)) { log(`Routine:LOG - Logfile nicht gefunden - wird angelegt`,"info") fs.writeFileSync(logFilePath, headerLine + logEntry, 'utf8'); // Datei erstellen und Header hinzufuegen } else { fs.appendFileSync(logFilePath, logEntry, 'utf8'); // Eintrag zum Logfile hinzufuegen } } catch (err) { log(`Routine:LOG - Fehler beim Schreiben in das Logfile: ${err}`, "error"); // Fehler beim Schreiben } } } //----------------------------------------------------------------------------------------------------- // Funktion ListSystemSubscriptions da es einen fehler in iobroker gibt mit async werden in iobroker die anzahl der subscriptions nicht richtig ausgegeben //----------------------------------------------------------------------------------------------------- function ListSystemSubscriptions() { const subscriptions = getSubscriptions(); // Hole alle Subscriptions let selectedsubscriptions = 0; // Zaehler fuer ausgewaehlte Subscriptions let subscriptionCount = 0; // Zaehler fuer die Gesamtzahl der Subscriptions for (var dp in subscriptions) { // Iteriere ueber die Subscriptions for (var i = 0; i < subscriptions[dp].length; i++) { if (subscriptions[dp][i].name && subscriptions[dp][i].name.includes("script.js.common.Anwesenheit.Alarm")) { // Pruefe, ob der Name der Subscription den Skriptnamen "ALARM" enthaelt selectedsubscriptions++; // Erhoehe den Zaehler fuer ausgewaehlte Subscriptions } subscriptionCount++; // Zaehle jede Subscription } } LOG(`Anzahl der Subscriptions fuer das Skript "ALARM": ${selectedsubscriptions} / Gesamt: ${subscriptionCount}`, "Ergebnis", "ListSystemSubscriptions", 0); } //----------------------------------------------------------------------------------------------------- // Funktion Create States/ //----------------------------------------------------------------------------------------------------- async function CreateStates() { LOG(`Routine wird ausgefuehrt`, "Ablauf", "CreateStates", 2); try { // Erstelle die Promises fuer die asynchronen createStateAsync-Aufrufe const promises = []; for (const { alarmtype, Datenpunkt } of AlarmTypes) { // AlarmTypes Datenpunkte erstellen promises.push(createStateAsync(Datenpunkt, false, { read: true, write: true, type: 'boolean', name: `${alarmtype} Alarm`, desc: `Status des ${alarmtype} Alarms` })); } for (const { Control, Datenpunkt } of AlarmScenarios) { // Scenarios Datenpunkte erstellen promises.push(createStateAsync(Datenpunkt, false, { read: true, write: true, type: 'boolean', name: `${Control} Steuerung`, desc: `Steuerung fuer ${Control}` })); } promises.push(createStateAsync(DP_Alarmaktivierung, false, { // Alarmaktivierung datenpunkt read: true, write: true, type: 'boolean', name: 'Alarm Aktivierung / Deaktivierung', desc: 'true = Alarm ist aktiviert / false = Alarm ist deaktiviert' })); promises.push(createStateAsync(DP_Still, false, { // STILL datenpunkt read: true, write: true, type: 'boolean', name: 'Still = Alarmsirenen aus ', desc: 'laufende Alarmgeraete werden ausgeschaltet - neuer Alarm ist immer still' })); promises.push(createStateAsync(id_JSON_Alarmmeldung_Aktuell, "", { // Zusaetzliche Datenpunkte fuer Alarmmeldungen und Historie read: true, write: true, type: 'string', name: 'Aktuelle Alarmmeldungen als JSON', desc: 'Aktuelle Alarmmeldungen JSON' })); promises.push(createStateAsync(id_JSON_Alarmmeldung_Historie, "", { read: true, write: true, type: 'string', name: 'Alarmmeldungen History', desc: 'History Alarmmeldungen' })); promises.push(createStateAsync(id_Button_Refresh_Historie, false, { read: true, write: true, type: 'boolean', name: 'Refresh Button History', desc: 'Loescht die Historie Alarmmeldungen wenn true' })); promises.push(createStateAsync(id_History_VerbleibendeTage, 7, { read: true, write: true, type: 'number', name: 'Anzahl der zu blebienden Alarmmeldungen beim Loeschen', desc: 'Behaelt die angegebene Anzahl von Tagen in der History' })); if(UpdateHTML_Datenpunkte) { promises.push(createStateAsync(id_HTML_Alarmmeldung_Aktuell, "", { read: true, write: true, type: 'string', name: 'Protokoll Alarmmeldunge HTML-Format', desc: 'Protokoll Alarmmeldunge HTML-Format aus JSON generiert' })); promises.push(createStateAsync(id_HTML_Alarmmeldung_Historie, "", { read: true, write: true, type: 'string', name: 'Protokoll Alarmmeldunge HTML-Format', desc: 'Protokoll Alarmmeldunge HTML-Format aus JSON generiert' })); } promises.push(createStateAsync(DP_Raumuebersicht, "", { read: true, write: true, type: 'string', name: 'Raumuebersicht im HTML Format', desc: 'ebersicht aller Raeume in HTML Format' })); if(!UpdateHTML_Datenpunkte) { // nur wenn Text-Datenpunkte auch gewuenscht sind if (existsState(id_HTML_Alarmmeldung_Aktuell)) { deleteState(id_HTML_Alarmmeldung_Aktuell); } if (existsState(id_HTML_Alarmmeldung_Historie)) { deleteState(id_HTML_Alarmmeldung_Historie); } } await Promise.all(promises); // Warten, bis alle createStateAsync-Aufrufe abgeschlossen sind LOG(`Alle Datenpunkte sind in der Objektliste vorhanden/erstellt!`, "Ablauf", "CreateStates", 2); await updateScenario(); // Nun updateScenario auf LOG(`updateScenario abgeschlossen.`, "Ablauf", "CreateStates", 2); await subscribeToAllScenarioDPs(); // Nach updateScenario createSubscriptions auf LOG(` abgeschlossen.`, "Ablauf", "CreateStates", 2); await createAktivierungsSubscription(); // Optional: Fuehre createAktivierungsSubscription danach aus LOG(`createAktivierungsSubscription abgeschlossen.`, "Ablauf", "CreateStates", 2); await ListSystemSubscriptions(); LOG(`CountSubscriptions abgeschlossen.`, "Ablauf", "CreateStates", 2); await Zustandsermittlung("init") // ermittelt den initialen Zustand von Fenster, Rollladen, Tueren, Sabotage und Stoerungen LOG(`Zustandsermittlung im Initiallauf abgeschlossen.`, "Ablauf", "CreateStates", 2); } catch (error) { LOG(`Kategorie:WARN; Routine:CreateStates; Fehler: ${error}`, 0, "warn"); } }