|
1 | | -/** |
2 | | - * @file Intelligente & träge Anwesenheitserkennung im Bett (ioBroker) |
3 | | - * @version 3.0 FINAL |
4 | | - * @author Ihr Name |
5 | | - * @license MIT |
6 | | - * @description |
7 | | - * Dieses Skript realisiert eine träge und intelligente Nachtschaltung. Es ist die |
8 | | - * stabile und erweiterte ioBroker-Umsetzung der Logik aus dem Homematic-Forum. |
9 | | - * |
10 | | - * FUNKTIONSWEISE: |
11 | | - * 1. Träge Anwesenheitserkennung: Ein kurzes Aufstehen (z.B. für ein Glas Wasser) wird |
12 | | - * ignoriert. Der Status wird erst nach einer saisonal angepassten Verzögerung auf |
13 | | - * "frei" gesetzt. |
14 | | - * 2. Aktivierung bei zwei Personen: Die Nachtschaltung wird SOFORT aktiv, wenn beide |
15 | | - * Personen bei Nacht im Bett sind. |
16 | | - * 3. Aktivierung bei einer Person: Geht nur eine Person bei Nacht ins Bett, startet ein |
17 | | - * 30-Minuten-Timer. Die Nachtschaltung wird erst nach Ablauf aktiv. Kommt die |
18 | | - * zweite Person hinzu, wird der Timer abgebrochen und die Schaltung sofort aktiviert. |
19 | | - * 4. Master-Reset durch Abwesenheit: Wenn niemand im Haus anwesend ist, werden alle |
20 | | - * Timer sofort gestoppt und die Nachtschaltung deaktiviert, um Fehlzustände zu verhindern. |
21 | | - * |
22 | | - * @changelog |
23 | | - * V3.0 (Final): |
24 | | - * - Kommentare umfassend überarbeitet und ergänzt für maximale Verständlichkeit. |
25 | | - * - Code-Struktur durch Hilfsfunktionen für Timer und Zustandsänderungen optimiert. |
26 | | - * - Redundante Code-Teile entfernt und Logik für Klarheit neu geordnet. |
27 | | - * - Visuelle Gliederung des Skripts für bessere Lesbarkeit verbessert. |
28 | | - */ |
29 | | - |
30 | | -// ====================================================================================== |
31 | | -// │ KONFIGURATION │ |
32 | | -// └────────────────────────────────────────────────────────────────────────────────────┘ |
33 | | - |
34 | | -// --- 1. DATENPUNKTE DER SENSOREN --- |
35 | | -// IDs der STATE-Datenpunkte Ihrer Bett-Sensoren (z.B. Homematic Tür-/Fensterkontakte). |
36 | | -// Annahme: false = belegt (Kontakt geschlossen), true = frei (Kontakt offen). |
37 | | -const SENSOR_LINKS_DP = 'hm-rpc.0.001E1D899E94B0.1.STATE'; |
38 | | -const SENSOR_RECHTS_DP = 'hm-rpc.0.001E1D899E922D.1.STATE'; |
39 | | - |
40 | | -// --- 2. ZIEL-DATENPUNKTE IN IO-BROKER --- |
41 | | -// IDs der Zieldatenpunkte, die das Skript steuert. |
42 | | -// Werden bei Bedarf automatisch unter '0_userdata.0' angelegt. |
43 | | -const STATUS_LINKS_VAR = '0_userdata.0.System.Nachtschaltung.BettLinks'; |
44 | | -const STATUS_RECHTS_VAR = '0_userdata.0.System.Nachtschaltung.BettRechts'; |
45 | | -const AKTIV_VAR = '0_userdata.0.System.Nachtschaltung.Aktiv'; |
46 | | - |
47 | | -// --- 3. STEUERNDE DATENPUNKTE --- |
48 | | -// ID des Datenpunkts, der Tag (true) oder Nacht (false) signalisiert. |
49 | | -const TAG_NACHT_VAR = '0_userdata.0.System.Astro.Tag'; |
50 | | -// ID des Datenpunkts für die globale Anwesenheit im Haus. |
51 | | -const ANWESENHEIT_VAR = '0_userdata.0.Anwesenheit.Status'; // true = anwesend, false = abwesend |
52 | | - |
53 | | -// --- 4. VERZÖGERUNGSZEITEN IN MINUTEN --- |
54 | | -const DELAY_MINUTEN_SOMMER = 3; // Verzögerung für das Verlassen des Bettes (Juni - Sep) |
55 | | -const DELAY_MINUTEN_WINTER = 4; // Verzögerung für das Verlassen des Bettes (Okt - Mai) |
56 | | -const DELAY_MINUTEN_EINZEL = 30; // Verzögerung, bis die Schaltung für nur eine Person aktiv wird. |
57 | | - |
58 | | -// --- 5. DEBUG-MODUS --- |
59 | | -// Bei 'true' werden detaillierte Log-Ausgaben erzeugt. |
60 | | -const DEBUG = true; |
61 | | - |
62 | | - |
63 | | -// ====================================================================================== |
64 | | -// │ GLOBALE VARIABLEN & ZUSTÄNDE │ |
65 | | -// └────────────────────────────────────────────────────────────────────────────────────┘ |
66 | | - |
67 | | -const LOG_PREFIX = '[NACHTSCHALTUNG] '; // Prefix für alle Log-Ausgaben. |
68 | | - |
69 | | -let timerLinks = null; // Hält die Timer-ID für die linke Bettseite. |
70 | | -let timerRechts = null; // Hält die Timer-ID für die rechte Bettseite. |
71 | | -let einzelTimer = null; // Hält die Timer-ID für die Einzelbelegungs-Logik. |
72 | | - |
73 | | - |
74 | | -// ====================================================================================== |
75 | | -// │ HILFSFUNKTIONEN │ |
76 | | -// └────────────────────────────────────────────────────────────────────────────────────┘ |
77 | | - |
78 | | -/** |
79 | | - * Setzt einen Datenpunkt nur dann, wenn sich der Wert geändert hat, und loggt die Aktion. |
80 | | - * @param {string} id - Die ID des Datenpunkts. |
81 | | - * @param {boolean | number | string} value - Der zu setzende Wert. |
82 | | - */ |
83 | | -async function setStateChangedAndLog(id, value) { |
84 | | - if ((await getStateAsync(id)).val !== value) { |
85 | | - if (DEBUG) log(`${LOG_PREFIX}AKTION: Setze '${id}' auf '${value}'.`); |
86 | | - await setStateAsync(id, value, true); |
87 | | - } |
88 | | -} |
89 | | - |
90 | | -/** |
91 | | - * Stoppt und löscht alle laufenden Timer im Skript. |
92 | | - * Nützlich als zentraler Reset-Mechanismus. |
93 | | - */ |
94 | | -function clearAllTimers() { |
95 | | - if (timerLinks) { |
96 | | - clearTimeout(timerLinks); |
97 | | - timerLinks = null; |
98 | | - if (DEBUG) log(`${LOG_PREFIX}TIMER-STOP: Abwesenheits-Timer (Links) abgebrochen.`); |
99 | | - } |
100 | | - if (timerRechts) { |
101 | | - clearTimeout(timerRechts); |
102 | | - timerRechts = null; |
103 | | - if (DEBUG) log(`${LOG_PREFIX}TIMER-STOP: Abwesenheits-Timer (Rechts) abgebrochen.`); |
104 | | - } |
105 | | - if (einzelTimer) { |
106 | | - clearTimeout(einzelTimer); |
107 | | - einzelTimer = null; |
108 | | - if (DEBUG) log(`${LOG_PREFIX}TIMER-STOP: Einzelbelegungs-Timer abgebrochen.`); |
109 | | - } |
110 | | -} |
111 | | - |
112 | | - |
113 | | -// ====================================================================================== |
114 | | -// │ SKRIPT-INITIALISIERUNG │ |
115 | | -// └────────────────────────────────────────────────────────────────────────────────────┘ |
116 | | - |
117 | | -/** |
118 | | - * Stellt sicher, dass alle benötigten Datenpunkte existieren. |
119 | | - * Fehlende Datenpunkte werden mit Standardwerten angelegt. |
120 | | - */ |
121 | | -async function initializeDataPoints() { |
122 | | - const statesToCreate = { |
123 | | - [STATUS_LINKS_VAR]: { name: 'Status Bett Links (belegt/frei)', type: 'boolean', def: false, role: 'state.switch' }, |
124 | | - [STATUS_RECHTS_VAR]: { name: 'Status Bett Rechts (belegt/frei)', type: 'boolean', def: false, role: 'state.switch' }, |
125 | | - [AKTIV_VAR]: { name: 'Nachtschaltung aktiv', type: 'boolean', def: false, role: 'switch' } |
126 | | - }; |
127 | | - |
128 | | - for (const [id, config] of Object.entries(statesToCreate)) { |
129 | | - if (!(await existsStateAsync(id))) { |
130 | | - log(`${LOG_PREFIX}Datenpunkt '${id}' wird erstellt.`, 'info'); |
131 | | - await createStateAsync(id, { |
132 | | - type: config.type, name: config.name, def: config.def, |
133 | | - read: true, write: true, role: config.role |
134 | | - }); |
135 | | - } |
136 | | - } |
137 | | - |
138 | | - // Überprüfung der externen Steuerungs-Datenpunkte |
139 | | - const externalDps = [TAG_NACHT_VAR, ANWESENHEIT_VAR]; |
140 | | - for (const dp of externalDps) { |
141 | | - if (!(await existsStateAsync(dp))) { |
142 | | - log(`${LOG_PREFIX}WARNUNG: Der Steuer-Datenpunkt '${dp}' existiert nicht. Das Skript funktioniert möglicherweise nicht korrekt.`, 'warn'); |
143 | | - } |
144 | | - } |
145 | | -} |
146 | | - |
147 | | - |
148 | | -// ====================================================================================== |
149 | | -// │ KERNLOGIK │ |
150 | | -// └────────────────────────────────────────────────────────────────────────────────────┘ |
151 | | - |
152 | | -/** |
153 | | - * Verarbeitet die Status-Änderung eines einzelnen Bett-Sensors. |
154 | | - * @param {'links' | 'rechts'} side - Die Bettseite, die aktualisiert wird. |
155 | | - * @param {boolean} sensorValue - Der vom Sensor gemeldete Wert (true=offen, false=geschlossen). |
156 | | - */ |
157 | | -async function processSensor(side, sensorValue) { |
158 | | - const statusVar = side === 'links' ? STATUS_LINKS_VAR : STATUS_RECHTS_VAR; |
159 | | - let timerRef = side === 'links' ? timerLinks : timerRechts; |
160 | | - const isBelegt = !sensorValue; // Sensor 'offen' (true) bedeutet, Bett ist 'nicht belegt' (false) |
161 | | - |
162 | | - if (isBelegt) { |
163 | | - // Person ist im Bett oder zurückgekehrt. |
164 | | - if (timerRef) { |
165 | | - clearTimeout(timerRef); |
166 | | - if (side === 'links') timerLinks = null; else timerRechts = null; |
167 | | - if (DEBUG) log(`${LOG_PREFIX}TIMER-STOP (${side.toUpperCase()}): Person ist zurück. Abwesenheits-Timer gestoppt.`); |
168 | | - } |
169 | | - await setStateChangedAndLog(statusVar, true); |
170 | | - } else { |
171 | | - // Person hat das Bett verlassen. |
172 | | - if ((await getStateAsync(statusVar)).val) { |
173 | | - const currentMonth = new Date().getMonth() + 1; // 1 (Jan) - 12 (Dez) |
174 | | - const delayMinuten = (currentMonth >= 6 && currentMonth <= 9) ? DELAY_MINUTEN_SOMMER : DELAY_MINUTEN_WINTER; |
175 | | - const delayMilliseconds = delayMinuten * 60 * 1000; |
176 | | - |
177 | | - if (DEBUG) log(`${LOG_PREFIX}TIMER-START (${side.toUpperCase()}): Person hat Bett verlassen. Starte ${delayMinuten}min Abwesenheits-Timer.`); |
178 | | - |
179 | | - const newTimer = setTimeout(async () => { |
180 | | - if (DEBUG) log(`${LOG_PREFIX}TIMER-ENDE (${side.toUpperCase()}): Verzögerung abgelaufen. Setze Status auf 'frei'.`); |
181 | | - await setStateChangedAndLog(statusVar, false); |
182 | | - if (side === 'links') timerLinks = null; else timerRechts = null; |
183 | | - }, delayMilliseconds); |
184 | | - |
185 | | - if (side === 'links') timerLinks = newTimer; else timerRechts = newTimer; |
186 | | - } |
187 | | - } |
188 | | -} |
189 | | - |
190 | | -/** |
191 | | - * Die zentrale Funktion, die den finalen Status der Nachtschaltung berechnet und setzt. |
192 | | - * Wird bei jeder relevanten Zustandsänderung aufgerufen. |
193 | | - */ |
194 | | -async function updateNightMode() { |
195 | | - // 1. Alle relevanten Zustände einlesen |
196 | | - const isBelegtLinks = (await getStateAsync(STATUS_LINKS_VAR)).val; |
197 | | - const isBelegtRechts = (await getStateAsync(STATUS_RECHTS_VAR)).val; |
198 | | - const isNacht = !(await getStateAsync(TAG_NACHT_VAR)).val; |
199 | | - const isAnwesend = (await getStateAsync(ANWESENHEIT_VAR)).val; |
200 | | - const isAktivCurrent = (await getStateAsync(AKTIV_VAR)).val; |
201 | | - |
202 | | - if (DEBUG) log(`${LOG_PREFIX}ANALYSE: Links=${isBelegtLinks}, Rechts=${isBelegtRechts}, Nacht=${isNacht}, Anwesend=${isAnwesend}, AktuellAktiv=${isAktivCurrent}`); |
203 | | - |
204 | | - // --- ENTSCHEIDUNGS-LOGIK --- |
205 | | - |
206 | | - // MASTER-REGEL: Bei Tag oder wenn niemand anwesend ist, wird die Nachtschaltung immer beendet. |
207 | | - if (!isNacht || !isAnwesend) { |
208 | | - clearAllTimers(); |
209 | | - await setStateChangedAndLog(AKTIV_VAR, false); |
210 | | - return; // Verarbeitung hier beenden |
211 | | - } |
212 | | - |
213 | | - // REGELN FÜR DIE NACHT BEI ANWESENHEIT: |
214 | | - const beideBelegt = isBelegtLinks && isBelegtRechts; |
215 | | - const einerBelegt = isBelegtLinks || isBelegtRechts; |
216 | | - |
217 | | - if (beideBelegt) { |
218 | | - // Fall 1: Beide Personen im Bett -> Sofort aktivieren. |
219 | | - if (DEBUG) log(`${LOG_PREFIX}LOGIK: Beide Personen im Bett. Aktiviere Nachtschaltung sofort.`); |
220 | | - clearAllTimers(); |
221 | | - await setStateChangedAndLog(AKTIV_VAR, true); |
222 | | - } else if (einerBelegt) { |
223 | | - // Fall 2: Genau eine Person im Bett. |
224 | | - if (!isAktivCurrent && !einzelTimer) { |
225 | | - // Wenn Schaltung noch aus ist und kein Timer läuft, starte den 30-Minuten-Timer. |
226 | | - const delayMilliseconds = DELAY_MINUTEN_EINZEL * 60 * 1000; |
227 | | - if (DEBUG) log(`${LOG_PREFIX}LOGIK: Eine Person im Bett (von inaktiv). Starte ${DELAY_MINUTEN_EINZEL}min Timer.`); |
228 | | - einzelTimer = setTimeout(async () => { |
229 | | - if (DEBUG) log(`${LOG_PREFIX}TIMER-ENDE (EINZEL): ${DELAY_MINUTEN_EINZEL}min abgelaufen. Aktiviere Nachtschaltung.`); |
230 | | - einzelTimer = null; |
231 | | - await setStateChangedAndLog(AKTIV_VAR, true); |
232 | | - }, delayMilliseconds); |
233 | | - } |
234 | | - // Falls bereits aktiv (weil zweite Person aufgestanden ist), bleibt der Zustand erhalten. |
235 | | - } else { |
236 | | - // Fall 3: Niemand im Bett -> Immer deaktivieren. |
237 | | - if (DEBUG) log(`${LOG_PREFIX}LOGIK: Niemand mehr im Bett. Deaktiviere Nachtschaltung.`); |
238 | | - clearAllTimers(); |
239 | | - await setStateChangedAndLog(AKTIV_VAR, false); |
240 | | - } |
241 | | -} |
242 | | - |
243 | | - |
244 | | -// ====================================================================================== |
245 | | -// │ TRIGGER-DEFINITIONEN │ |
246 | | -// └────────────────────────────────────────────────────────────────────────────────────┘ |
247 | | - |
248 | | -// Trigger für die einzelnen Bett-Sensoren |
249 | | -on({ id: SENSOR_LINKS_DP, change: 'ne' }, async (obj) => { |
250 | | - if (DEBUG) log(`${LOG_PREFIX}TRIGGER: Sensor Links meldet: ${obj.state.val}`); |
251 | | - await processSensor('links', obj.state.val); |
252 | | -}); |
253 | | - |
254 | | -on({ id: SENSOR_RECHTS_DP, change: 'ne' }, async (obj) => { |
255 | | - if (DEBUG) log(`${LOG_PREFIX}TRIGGER: Sensor Rechts meldet: ${obj.state.val}`); |
256 | | - await processSensor('rechts', obj.state.val); |
257 | | -}); |
258 | | - |
259 | | -// Ein kombinierter Trigger für die finale Logik, um unnötige Aufrufe zu vermeiden. |
260 | | -const finalLogicTrigger = [STATUS_LINKS_VAR, STATUS_RECHTS_VAR, TAG_NACHT_VAR, ANWESENHEIT_VAR]; |
261 | | -on({ id: finalLogicTrigger, change: 'ne' }, async (obj) => { |
262 | | - if (DEBUG) log(`${LOG_PREFIX}TRIGGER: Eine für die Logik relevante Variable hat sich geändert -> ${obj.id}`); |
263 | | - await updateNightMode(); |
264 | | -}); |
265 | | - |
266 | | - |
267 | | -// ====================================================================================== |
268 | | -// │ SKRIPT-START & STOP │ |
269 | | -// └────────────────────────────────────────────────────────────────────────────────────┘ |
270 | | - |
271 | | -onStop(function () { |
272 | | - clearAllTimers(); |
273 | | - log(`${LOG_PREFIX}Skript gestoppt.`, 'info'); |
274 | | -}); |
275 | | - |
276 | | -// Hauptfunktion, die beim Skriptstart ausgeführt wird. |
277 | | -(async function main() { |
278 | | - await initializeDataPoints(); |
279 | | - log(`${LOG_PREFIX}Skript gestartet.`, 'info'); |
280 | | - // Beim Start einmal die Logik ausführen, um den korrekten Zustand sicherzustellen. |
281 | | - await updateNightMode(); |
282 | | -})(); |
283 | | - |
0 commit comments