1+ /***************************************************************************************
2+ * Script: Zutrittskontrolle Haustür
3+ * Description: Überwacht Homematic HmIP-WKP Keypad zur temporären Freigabe der Zutrittskontrolle.
4+ * Setzt den Status für 2 Minuten auf TRUE, danach automatisch wieder auf FALSE.
5+ * Author: Sanweb
6+ * Version: 1.6.2
7+ * Date: 2026-03-06
8+ ***************************************************************************************/
9+ ( async ( ) => {
10+
11+ // =========================================================================
12+ // KONFIGURATION
13+ // Bitte passe die folgenden Werte an deine ioBroker-Umgebung an.
14+ // =========================================================================
15+ const CONFIG = {
16+ // ---------------------------------------------------------------------
17+ // HAUPT-EINSTELLUNGEN
18+ // ---------------------------------------------------------------------
19+
20+ // ZIEL-DATENPUNKT (Boolean: true/false)
21+ // Dieser Datenpunkt wird auf "true" gesetzt, wenn eine Taste gedrückt wird.
22+ // Nach Ablauf der Zeit (TIMEOUT_MINUTES) wird er wieder auf "false" gesetzt.
23+ // Tipp: Erstelle diesen Datenpunkt idealerweise manuell unter '0_userdata.0...'.
24+ TARGET_ID : '0_userdata.0.Haustuer.Zutrittskontrolle_Haustuer' ,
25+
26+ // AUSLÖSER / QUELL-DATENPUNKTE (Array von Strings)
27+ // Liste aller Datenpunkte, die den Ziel-Datenpunkt aktivieren sollen (z.B. Taster, Fernbedienungen).
28+ // Das Script reagiert, wenn einer dieser Datenpunkte den Wert "true" annimmt.
29+ SOURCE_IDS : [
30+ 'hm-rpc.0.002E9F29993DEB.1.PRESS_LOCK' ,
31+ 'hm-rpc.0.002E9F29993DEB.2.PRESS_UNLOCK' ,
32+ 'hm-rpc.0.002E9F29993DEB.3.PRESS_LOCK' ,
33+ 'hm-rpc.0.002E9F29993DEB.4.PRESS_UNLOCK'
34+ ] ,
35+
36+ // ZEITSTEUERUNG
37+ // Wie viele Minuten soll die Zutrittskontrolle auf "true" (freigegeben) bleiben?
38+ TIMEOUT_MINUTES : 2 ,
39+
40+ // ---------------------------------------------------------------------
41+ // EXPERTEN-EINSTELLUNGEN (Normalerweise keine Änderung nötig)
42+ // ---------------------------------------------------------------------
43+
44+ // SCRIPT-STOP-VERZÖGERUNG (in Millisekunden)
45+ // Zeitfenster, in dem das Script beim Beenden/Neustarten aufräumen darf (Timer löschen).
46+ STOP_TIMEOUT_MS : 2000 ,
47+
48+ // SPAM-SCHUTZ / DEBOUNCE (in Millisekunden)
49+ // Verhindert, dass mehrmaliges extrem schnelles Drücken Fehler verursacht.
50+ // Nach dieser Zeit wird das Script spätestens wieder für neue Tastendrücke freigegeben.
51+ LOCK_TIMEOUT_MS : 5000 ,
52+
53+ // SYSTEM-ÜBERWACHUNG (Cron-Syntax)
54+ // Wann soll das Script täglich prüfen, ob der Ziel-Datenpunkt noch existiert?
55+ // Standard: '0 0 * * *' (Täglich um Mitternacht)
56+ DAILY_CHECK_CRON : '0 0 * * *' ,
57+
58+ // ---------------------------------------------------------------------
59+ // BENACHRICHTIGUNGEN & LOGGING
60+ // ---------------------------------------------------------------------
61+
62+ // FEHLER-MELDUNGEN PER MESSENGER
63+ // Trage hier deine Messenger-Instanz ein (z.B. 'telegram.0', 'pushover.0', 'email.0').
64+ // Das Script meldet sich dann aktiv, wenn z.B. der Ziel-Datenpunkt versehentlich gelöscht wurde.
65+ // Leer lassen (''), um Benachrichtigungen komplett zu deaktivieren.
66+ NOTIFICATION_INSTANCE : '' ,
67+
68+ // DEBUG-MODUS (true / false)
69+ // Wenn "true", schreibt das Script detaillierte Infos ins ioBroker-Log (gut zur Einrichtung).
70+ // Wenn "false", werden nur echte Fehler ins Log geschrieben (empfohlen für den Dauerbetrieb).
71+ DEBUG_MODE : true
72+ } ;
73+
74+ const TIMEOUT_MS = CONFIG . TIMEOUT_MINUTES * 60 * 1000 ;
75+ let timeoutTimer = null ;
76+ let isProcessing = false ; // Lock-Variable zur Vermeidung von Mehrfachausführungen
77+ let targetExists = false ; // Caching für die Existenz des Ziel-Datenpunktes
78+
79+ // Metriken für Monitoring
80+ const METRICS = {
81+ triggers : 0 ,
82+ timerStarts : 0 ,
83+ timerCancelled : 0 , // Durch neue Trigger abgebrochene Timer
84+ errors : 0 ,
85+ lastTrigger : null
86+ } ;
87+
88+ // State-Change-History für Trigger
89+ const HISTORY = {
90+ maxEntries : 10 ,
91+ entries : [ ]
92+ } ;
93+
94+ // Hilfsfunktion zur Pflege der Historie
95+ function addHistory ( entry ) {
96+ HISTORY . entries . unshift ( {
97+ timestamp : new Date ( ) . toISOString ( ) ,
98+ ...entry
99+ } ) ;
100+ if ( HISTORY . entries . length > HISTORY . maxEntries ) {
101+ HISTORY . entries . pop ( ) ;
102+ }
103+ }
104+
105+ // Hilfsfunktion für Benachrichtigungen bei kritischen Fehlern
106+ function notifyAdmin ( message ) {
107+ if ( CONFIG . NOTIFICATION_INSTANCE ) {
108+ try {
109+ sendTo ( CONFIG . NOTIFICATION_INSTANCE , 'send' , {
110+ text : `🚨 [Zutrittskontrolle] ${ message } `
111+ } ) ;
112+ logDebug ( `Benachrichtigung über ${ CONFIG . NOTIFICATION_INSTANCE } gesendet.` ) ;
113+ } catch ( e ) {
114+ log ( `[Zutrittskontrolle] Fehler beim Senden der Benachrichtigung: ${ e . message } ` , 'error' ) ;
115+ }
116+ }
117+ }
118+
119+ // Hilfsfunktion für optionales Debug-Logging
120+ function logDebug ( message ) {
121+ if ( CONFIG . DEBUG_MODE ) {
122+ log ( '[Zutrittskontrolle] ' + message , 'info' ) ;
123+ }
124+ }
125+
126+ // Sicheres und asynchrones Setzen des Ziel-Datenpunktes mit Typen-Prüfung
127+ async function setTargetState ( value ) {
128+ if ( typeof value !== 'boolean' ) {
129+ METRICS . errors ++ ;
130+ log ( '[Zutrittskontrolle] Fehler: Nur boolean-Werte erlaubt' , 'error' ) ;
131+ return ;
132+ }
133+
134+ if ( ! targetExists ) {
135+ METRICS . errors ++ ;
136+ log ( '[Zutrittskontrolle] Fehler: Ziel-Datenpunkt existiert nicht!' , 'warn' ) ;
137+ return ;
138+ }
139+
140+ try {
141+ // Nutzung der nativen ioBroker Async-Funktion anstelle eines manuellen Promise-Wrappers
142+ await setStateAsync ( CONFIG . TARGET_ID , value , true ) ;
143+ logDebug ( 'Ziel-Datenpunkt auf ' + value + ' gesetzt.' ) ;
144+ } catch ( e ) {
145+ METRICS . errors ++ ;
146+ log ( '[Zutrittskontrolle] Fehler beim Setzen des Datenpunktes: ' + e . message , 'error' ) ;
147+ }
148+ }
149+
150+ // Zentrale Funktion für das Starten/Zurücksetzen des Timers
151+ function startTimer ( duration ) {
152+ if ( duration <= 0 ) {
153+ logDebug ( 'Ungültige oder abgelaufene Timer-Dauer, setze direkt zurück.' ) ;
154+ setTargetState ( false ) ;
155+ return ;
156+ }
157+
158+ if ( timeoutTimer ) {
159+ clearTimeout ( timeoutTimer ) ;
160+ timeoutTimer = null ;
161+ METRICS . timerCancelled ++ ;
162+ logDebug ( 'Vorheriger Timer gelöscht (Vermeidung von Race Conditions).' ) ;
163+ }
164+
165+ METRICS . timerStarts ++ ;
166+ logDebug ( 'Starte Timer für ' + Math . round ( duration / 1000 ) + ' Sekunden.' ) ;
167+ timeoutTimer = setTimeout ( async function ( ) {
168+ try {
169+ logDebug ( 'Timer abgelaufen. Setze Zutrittskontrolle zurück.' ) ;
170+ await setTargetState ( false ) ;
171+ } catch ( e ) {
172+ METRICS . errors ++ ;
173+ log ( '[Zutrittskontrolle] Fehler im Timer-Callback: ' + e . message , 'error' ) ;
174+ } finally {
175+ timeoutTimer = null ;
176+ }
177+ } , duration ) ;
178+ }
179+
180+ // Script-Neustart abfangen: Prüfen ob ein Timer reaktiviert werden muss
181+ async function checkInitialState ( ) {
182+ const currentState = await getStateAsync ( CONFIG . TARGET_ID ) ;
183+
184+ if ( ! currentState ) {
185+ logDebug ( 'Kein aktueller State gefunden für den Ziel-Datenpunkt.' ) ;
186+ return ;
187+ }
188+
189+ // Falls der Datenpunkt aktuell TRUE ist, prüfen wir, wie lange er das schon ist
190+ if ( currentState . val === true ) {
191+ const lastChange = currentState . lc || currentState . ts ;
192+
193+ if ( ! lastChange ) {
194+ logDebug ( 'Kein Zeitstempel verfügbar - setze zurück auf FALSE' ) ;
195+ await setTargetState ( false ) ;
196+ return ;
197+ }
198+
199+ const passedTime = Date . now ( ) - lastChange ;
200+ const remainingTime = TIMEOUT_MS - passedTime ;
201+
202+ if ( remainingTime > 0 ) {
203+ logDebug ( 'Script-Neustart erkannt: Datenpunkt war bereits TRUE. Reaktivierung für restliche ' + Math . round ( remainingTime / 1000 ) + 's.' ) ;
204+ startTimer ( remainingTime ) ;
205+ } else {
206+ logDebug ( 'Script-Neustart erkannt: Datenpunkt war TRUE, aber die 2 Minuten sind abgelaufen. Setze zurück auf FALSE.' ) ;
207+ await setTargetState ( false ) ;
208+ }
209+ }
210+ }
211+
212+ // Konfigurations-Validierung beim Script-Start
213+ function validateConfig ( ) {
214+ if ( ! CONFIG . TARGET_ID || ! CONFIG . SOURCE_IDS || CONFIG . SOURCE_IDS . length === 0 ) {
215+ throw new Error ( 'Ungültige Konfiguration: TARGET_ID oder SOURCE_IDS fehlen.' ) ;
216+ }
217+ if ( CONFIG . TIMEOUT_MINUTES <= 0 ) {
218+ log ( '[Zutrittskontrolle] Warnung: TIMEOUT_MINUTES sollte größer als 0 sein.' , 'warn' ) ;
219+ }
220+ }
221+
222+ // Prüft asynchron, ob die Quell-Datenpunkte existieren
223+ async function validateSourceIds ( ) {
224+ for ( const id of CONFIG . SOURCE_IDS ) {
225+ try {
226+ const exists = await existsStateAsync ( id ) ;
227+ if ( ! exists ) {
228+ log ( '[Zutrittskontrolle] Warnung: Quell-Datenpunkt ' + id + ' existiert nicht!' , 'warn' ) ;
229+ }
230+ } catch ( e ) {
231+ log ( '[Zutrittskontrolle] Fehler bei der Prüfung von ' + id + ': ' + e . message , 'error' ) ;
232+ }
233+ }
234+ }
235+
236+ // Initiale Überprüfung beim Script-Start (mit asynchronem Check des Datenpunktes)
237+ async function init ( ) {
238+ try {
239+ validateConfig ( ) ;
240+ await validateSourceIds ( ) ;
241+
242+ targetExists = await existsStateAsync ( CONFIG . TARGET_ID ) ;
243+ if ( ! targetExists ) {
244+ log ( '[Zutrittskontrolle] Warnung: Ziel-Datenpunkt fehlt. Bitte anlegen.' , 'warn' ) ;
245+ } else {
246+ await checkInitialState ( ) ;
247+ }
248+ } catch ( e ) {
249+ log ( '[Zutrittskontrolle] Fehler bei der Initialisierung: ' + e . message , 'error' ) ;
250+ }
251+ }
252+
253+ // Initialisierung starten
254+ init ( ) ;
255+
256+ // Tägliche Überprüfung der Existenz des Ziel-Datenpunkts und Loggen der Metriken
257+ schedule ( CONFIG . DAILY_CHECK_CRON , async ( ) => {
258+ try {
259+ targetExists = await existsStateAsync ( CONFIG . TARGET_ID ) ;
260+ if ( ! targetExists ) {
261+ METRICS . errors ++ ;
262+ const errorMsg = 'Ziel-Datenpunkt wurde gelöscht oder ist nicht mehr erreichbar!' ;
263+ log ( `[Zutrittskontrolle] Fehler: ${ errorMsg } ` , 'error' ) ;
264+ notifyAdmin ( errorMsg ) ;
265+ } else {
266+ logDebug ( `Tägliche Prüfung OK. Metriken: Triggers: ${ METRICS . triggers } , Timer Starts: ${ METRICS . timerStarts } , Abbrüche: ${ METRICS . timerCancelled } , Fehler: ${ METRICS . errors } ` ) ;
267+
268+ // Metriken nach Report zurücksetzen (lastTrigger bleibt für Historie erhalten)
269+ METRICS . triggers = 0 ;
270+ METRICS . timerStarts = 0 ;
271+ METRICS . timerCancelled = 0 ;
272+ METRICS . errors = 0 ;
273+ }
274+ } catch ( e ) {
275+ METRICS . errors ++ ;
276+ log ( '[Zutrittskontrolle] Fehler bei der täglichen Prüfung: ' + e . message , 'error' ) ;
277+ }
278+ } ) ;
279+
280+ // Trigger für die konfigurierten Datenpunkte
281+ on ( {
282+ id : CONFIG . SOURCE_IDS ,
283+ change : 'ne'
284+ } , async function ( obj ) {
285+ if ( isProcessing ) {
286+ logDebug ( 'Trigger ignoriert - bereits in Verarbeitung' ) ;
287+ return ;
288+ }
289+
290+ isProcessing = true ;
291+ METRICS . triggers ++ ;
292+ METRICS . lastTrigger = new Date ( ) . toISOString ( ) ;
293+
294+ // Zusätzliche Absicherung: Lock nach Zeit X zwingend aufheben, falls ein Fehler unbemerkt bleibt
295+ const processingTimeout = setTimeout ( ( ) => {
296+ if ( isProcessing ) {
297+ isProcessing = false ;
298+ METRICS . errors ++ ;
299+ logDebug ( 'Processing lock automatisch zurückgesetzt (Timeout-Fallback).' ) ;
300+ }
301+ } , CONFIG . LOCK_TIMEOUT_MS ) ;
302+
303+ try {
304+ // Prüfen ob das Event überhaupt ausgelöst wurde und ob der neue Wert TRUE ist
305+ if ( obj . state && obj . state . val === true ) {
306+ logDebug ( 'Trigger ausgelöst durch Taste: ' + obj . id ) ;
307+ addHistory ( { triggerId : obj . id , action : 'Zutritt gewährt' } ) ;
308+ await setTargetState ( true ) ;
309+ startTimer ( TIMEOUT_MS ) ;
310+ }
311+ } catch ( e ) {
312+ METRICS . errors ++ ;
313+ log ( '[Zutrittskontrolle] Fehler in der Trigger-Verarbeitung: ' + e . message , 'error' ) ;
314+ } finally {
315+ clearTimeout ( processingTimeout ) ;
316+ isProcessing = false ;
317+ }
318+ } ) ;
319+
320+ // Bereinigung bei Script-Stop
321+ onStop ( function ( ) {
322+ if ( timeoutTimer ) {
323+ clearTimeout ( timeoutTimer ) ;
324+ timeoutTimer = null ;
325+ logDebug ( 'Script gestoppt - Timer bereinigt.' ) ;
326+ }
327+ } , CONFIG . STOP_TIMEOUT_MS ) ;
328+
329+ } ) ( ) ;
0 commit comments