1+ /*******************************************************************************
2+ * Skriptname: Klingelsignal & Sturm Protection
3+ * Beschreibung: Verarbeitet das Klingelsignal (HmIP), steuert den Türsummer
4+ * bei gewährtem Zutritt und gibt eine Alexa-Sprachmeldung aus.
5+ * Inklusive "Sturm Protection" (Cooldown) gegen Dauer-Klingeln.
6+ * Autor: Sanweb
7+ * * -----------------------------------------------------------------------------
8+ * Versionshistorie:
9+ * v1.0.0 | 04.03.2026 | Initiale Version
10+ *******************************************************************************/
11+
12+ // ==============================================================================
13+ // KONFIGURATION: DATENPUNKTE BITTE ANPASSEN
14+ // ==============================================================================
15+ ( async ( ) => {
16+
17+ // Der von dir genannte Datenpunkt des HmIP Klingelsensors
18+ // (Hinweis: '1.STATE' repräsentiert Kanal 1 und den Datenpunkt STATE)
19+ const DP_KLINGEL = 'hm-rpc.0.0026E0C998D1F2.1.STATE' ;
20+
21+ // Datenpunkt der Zutrittskontrolle (Boolean erwartet: true = gewährt, false = verweigert)
22+ // Falls du eine Werteliste nutzt, muss die Abfrage weiter unten angepasst werden.
23+ const DP_ZUTRITTSKONTROLLE = '0_userdata.0.Status.Haustuer.Zutrittskontrolle_Haustuer' ;
24+
25+ // Datenpunkt zum Speichern des letzten Klingelzeitpunkts (Typ: Number/Zahl)
26+ const DP_LAST_RING = '0_userdata.0.Status.Klingel_LastRingTime' ;
27+
28+ // Datenpunkt für den HmIP-PCBS Haustürsummer (meist Kanal 3 oder 4, .STATE)
29+ const DP_TUERSUMMER = 'hm-rpc.0.00045A49A87CFF.3.STATE' ;
30+
31+ // Datenpunkt für die Alexa Sprachausgabe (Gruppe Überall)
32+ const DP_ALEXA_SPEAK = 'alexa2.0.Echo-Devices.e4a56dc3ec9a497c9209c05d2c29a09d.Commands.speak' ;
33+
34+ // ==============================================================================
35+ // KONFIGURATION: VARIABLEN UND BEFEHLE
36+ // ==============================================================================
37+ const ALEXA_MESSAGE = 'Ding Dong, es hat an der Haustür geklingelt!' ;
38+
39+ // Konfiguration der Zeiten
40+ const ALEXA_START_ZEIT = '11:00' ; // Ab wann darf Alexa sprechen? (Format HH:MM)
41+ const ALEXA_ENDE_ZEIT = '22:00' ; // Bis wann darf Alexa sprechen? (Format HH:MM)
42+ const TUERSUMMER_DAUER_MS = 2000 ; // Wie lange zieht der Türsummer an? (in Millisekunden)
43+ const STURM_PROTECTION_MS = 33000 ; // Wie lange ist die Klingel nach Betätigung gesperrt? (in Millisekunden)
44+
45+ const DEBUG = false ; // Auf true setzen für Entwicklung / detailliertes Logging
46+
47+ // Interne Variablen für Timer und den Klingelschutz (Sturm Protection)
48+ let lastRingTime = 0 ; // Speichert den Zeitstempel des letzten Klingelns (verhindert Race Conditions)
49+ let timerTuersummer = null ;
50+
51+ // ==============================================================================
52+ // HILFSFUNKTIONEN
53+ // ==============================================================================
54+
55+ // Debug-Logging
56+ function logDebug ( msg ) {
57+ if ( DEBUG ) log ( msg , 'debug' ) ;
58+ }
59+
60+ // Funktion zum Prüfen ob die aktuelle Zeit im konfigurierten Fenster liegt
61+ function isTimeInRange ( startTime , endTime ) {
62+ const now = new Date ( ) ;
63+ const startParts = startTime . split ( ':' ) ;
64+ const endParts = endTime . split ( ':' ) ;
65+
66+ const startMins = parseInt ( startParts [ 0 ] , 10 ) * 60 + parseInt ( startParts [ 1 ] , 10 ) ;
67+ const endMins = parseInt ( endParts [ 0 ] , 10 ) * 60 + parseInt ( endParts [ 1 ] , 10 ) ;
68+ const currentMins = now . getHours ( ) * 60 + now . getMinutes ( ) ;
69+
70+ return currentMins >= startMins && currentMins < endMins ;
71+ }
72+
73+ // Prüft synchron und sicher, ob ein Datenpunkt im ioBroker existiert
74+ function stateExists ( id ) {
75+ return typeof getObject ( id ) !== 'undefined' ;
76+ }
77+
78+ // Sicheres Setzen von States mit Prüfung und Logging sowie Callback-Fehlerbehandlung
79+ function safeSetState ( id , val ) {
80+ if ( stateExists ( id ) ) {
81+ log ( `Setze Datenpunkt "${ id } " auf Wert: ${ val } ` , 'debug' ) ;
82+ // Hinweis zum ack-Flag:
83+ // Andere States hier nutzen ack=false (Steuerbefehle an Geräte).
84+ // DP_LAST_RING nutzt im Hauptprogramm ack=true (reiner Status, keine Steuerung).
85+ setState ( id , val , false , ( err ) => {
86+ if ( err ) log ( `Fehler beim Setzen von ${ id } : ${ err } ` , 'error' ) ;
87+ } ) ;
88+ } else {
89+ log ( `Fehler: Datenpunkt "${ id } " existiert nicht. Aktion abgebrochen.` , 'warn' ) ;
90+ }
91+ }
92+
93+ // ==============================================================================
94+ // INITIALISIERUNG
95+ // ==============================================================================
96+
97+ // Plausibilitätsprüfung der konfigurierten Zeiten
98+ if ( ! / ^ ( [ 0 1 ] \d | 2 [ 0 - 3 ] ) : ( [ 0 - 5 ] \d ) $ / . test ( ALEXA_START_ZEIT ) || ! / ^ ( [ 0 1 ] \d | 2 [ 0 - 3 ] ) : ( [ 0 - 5 ] \d ) $ / . test ( ALEXA_ENDE_ZEIT ) ) {
99+ log ( 'WARNUNG: ALEXA_START_ZEIT oder ALEXA_ENDE_ZEIT haben ein ungültiges Format! Erwartet wird HH:MM' , 'warn' ) ;
100+ } else {
101+ // Zusätzlich prüfen, ob Startzeit vor Endzeit liegt
102+ const startParts = ALEXA_START_ZEIT . split ( ':' ) ;
103+ const endParts = ALEXA_ENDE_ZEIT . split ( ':' ) ;
104+ const startMins = parseInt ( startParts [ 0 ] , 10 ) * 60 + parseInt ( startParts [ 1 ] , 10 ) ;
105+ const endMins = parseInt ( endParts [ 0 ] , 10 ) * 60 + parseInt ( endParts [ 1 ] , 10 ) ;
106+ if ( startMins >= endMins ) {
107+ log ( 'WARNUNG: ALEXA_START_ZEIT liegt nach oder auf ALEXA_ENDE_ZEIT!' , 'warn' ) ;
108+ }
109+ }
110+
111+ // Wir prüfen nur, ob der Zutrittskontroll-Datenpunkt existiert.
112+ // Manuelles Anlegen im ioBroker-Objektbaum wird empfohlen, um asynchrone Fehler durch createState zu vermeiden.
113+ if ( ! stateExists ( DP_ZUTRITTSKONTROLLE ) ) {
114+ log ( `WICHTIG: Der Datenpunkt ${ DP_ZUTRITTSKONTROLLE } existiert nicht. Bitte lege ihn manuell als Logikwert (boolean) an!` , 'error' ) ;
115+ }
116+
117+ // Letzten Klingel-Zeitpunkt laden, falls der Datenpunkt existiert
118+ if ( stateExists ( DP_LAST_RING ) ) {
119+ const savedTime = getState ( DP_LAST_RING ) . val ;
120+ if ( typeof savedTime === 'number' && ! isNaN ( savedTime ) ) {
121+ lastRingTime = savedTime ;
122+ logDebug ( `Letzter Klingelzeitpunkt aus Datenpunkt geladen: ${ lastRingTime } ` ) ;
123+ } else {
124+ log ( 'WARNUNG: Gespeicherter Klingelzeitpunkt ist ungültig, verwende 0' , 'warn' ) ;
125+ lastRingTime = 0 ;
126+ }
127+ } else {
128+ log ( `HINWEIS: Der Datenpunkt ${ DP_LAST_RING } existiert nicht. Lege ihn als Zahl (number) an, um die Sturm-Protection über Skript-Neustarts hinweg zu erhalten.` , 'info' ) ;
129+ }
130+
131+ // ==============================================================================
132+ // HAUPTPROGRAMM (TRIGGER)
133+ // ==============================================================================
134+
135+ // Trigger auf Aktualisierung des Klingelsensors (wenn Wert 'true' ist)
136+ on ( { id : DP_KLINGEL , val : true , ack : true } , function ( obj ) {
137+
138+ const now = Date . now ( ) ;
139+
140+ // 1. KLINGELSCHUTZ PRÜFEN (Timestamp-basierter Cooldown)
141+ if ( ( now - lastRingTime ) < STURM_PROTECTION_MS ) {
142+ const remaining = Math . round ( ( STURM_PROTECTION_MS - ( now - lastRingTime ) ) / 1000 ) ;
143+ log ( `Klingel betätigt, aber Sturm Protection ist noch aktiv (${ remaining } s verbleibend). Aktion ignoriert.` , 'info' ) ;
144+ return ;
145+ }
146+
147+ // Klingelschutz aktivieren (Zeitstempel setzen)
148+ lastRingTime = now ;
149+ if ( stateExists ( DP_LAST_RING ) ) {
150+ // Hinweis zum ack-Flag: ack=true, da wir hier nur einen Status im System protokollieren und nichts steuern
151+ setState ( DP_LAST_RING , lastRingTime , true ) ;
152+ }
153+ log ( 'Klingelsignal erkannt. Ablauf startet, Sturm Protection aktiviert.' , 'info' ) ;
154+
155+ // Aktuellen Status der Zutrittskontrolle abrufen (Explizite Boolean-Prüfung)
156+ let zutrittGewaehrt = false ;
157+ if ( stateExists ( DP_ZUTRITTSKONTROLLE ) ) {
158+ zutrittGewaehrt = ( getState ( DP_ZUTRITTSKONTROLLE ) . val === true ) ;
159+ }
160+
161+ if ( ! zutrittGewaehrt ) {
162+ // ----------------------------------------------------------------------
163+ // BEDINGUNG: Zutritt verweigert (Normale Klingelfunktion)
164+ // ----------------------------------------------------------------------
165+ log ( 'Zutritt verweigert. Führe normalen Klingelablauf aus.' , 'info' ) ;
166+
167+ // Uhrzeit abfragen (mit neuer HH:MM Prüfung)
168+ if ( isTimeInRange ( ALEXA_START_ZEIT , ALEXA_ENDE_ZEIT ) ) {
169+ // Alexa Sprachausgabe
170+ safeSetState ( DP_ALEXA_SPEAK , ALEXA_MESSAGE ) ;
171+ } else {
172+ log ( `Uhrzeit außerhalb ${ ALEXA_START_ZEIT } -${ ALEXA_ENDE_ZEIT } Uhr: Alexa bleibt stumm.` , 'info' ) ;
173+ }
174+
175+ } else {
176+ // ----------------------------------------------------------------------
177+ // BEDINGUNG: Zutritt gewährt (Haustürsummer auslösen)
178+ // ----------------------------------------------------------------------
179+ log ( 'Zutritt gewährt. Betätige Türsummer.' , 'info' ) ;
180+
181+ // Haustürsummer sofort Ein
182+ safeSetState ( DP_TUERSUMMER , true ) ;
183+
184+ // Haustürsummer nach definierter Zeit Aus
185+ if ( timerTuersummer ) clearTimeout ( timerTuersummer ) ;
186+ logDebug ( 'Timer für Türsummer gestartet' ) ;
187+ timerTuersummer = setTimeout ( function ( ) {
188+ safeSetState ( DP_TUERSUMMER , false ) ;
189+ } , TUERSUMMER_DAUER_MS ) ;
190+
191+ // Zutrittskontrolle sofort wieder auf "verweigert" zurücksetzen
192+ safeSetState ( DP_ZUTRITTSKONTROLLE , false ) ;
193+ }
194+ } ) ;
195+
196+ // ==============================================================================
197+ // SKRIPT STOPP BEHANDLUNG
198+ // ==============================================================================
199+ onStop ( function ( ) {
200+ log ( 'Skript wird gestoppt. Bereinige Timer und setze Türsummer zurück...' , 'info' ) ;
201+ if ( timerTuersummer ) {
202+ clearTimeout ( timerTuersummer ) ;
203+ timerTuersummer = null ;
204+ // Sicherheitshalber den Summer ausschalten, falls er gerade an war
205+ if ( stateExists ( DP_TUERSUMMER ) ) {
206+ setState ( DP_TUERSUMMER , false ) ;
207+ log ( 'Türsummer wegen Skript-Stopp sicherheitshalber ausgeschaltet.' , 'info' ) ;
208+ }
209+ }
210+ } ) ;
211+
212+ } ) ( ) ;
0 commit comments