Skip to content

Commit 2b082a3

Browse files
committed
Update / Modification from the production live system: 2026-03-06 01:00
1 parent 535bb70 commit 2b082a3

2 files changed

Lines changed: 329 additions & 14 deletions

File tree

Lines changed: 329 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,329 @@
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+
})();

iobroker/Zugangskonrolle.js

Lines changed: 0 additions & 14 deletions
This file was deleted.

0 commit comments

Comments
 (0)