Skip to content

Commit 0bec5a6

Browse files
committed
Update / Modification from the production live system: 2026-05-24 15:00
1 parent bae60d6 commit 0bec5a6

1 file changed

Lines changed: 226 additions & 130 deletions

File tree

Lines changed: 226 additions & 130 deletions
Original file line numberDiff line numberDiff line change
@@ -1,147 +1,243 @@
11
/**
2-
* @file Briefkasten Benachrichtigungs- und Ansagesystem
3-
* @version 1.1.0
4-
* @author Sanweb
5-
* @description Überwacht den Briefkasten, sendet eine Telegram-Nachricht und
6-
* informiert die Bewohner beim Betreten der Wohnung/Haus über Alexa.
7-
* * Changelog:
8-
* - 1.1.0: Gotify durch Telegram ersetzt, IIFE Kapselung hinzugefügt
9-
* - 1.0.0: Initial release
2+
* @file Briefkasten Komplett-System (Ollama + Telegram + Alexa)
3+
* @version 2.0.0
4+
* @description Kombiniertes Script: Erkennt Post, generiert KI-Text via Ollama,
5+
* sendet Telegram-Benachrichtigung und gibt Alexa-Ansage beim
6+
* Betreten der Wohnung aus.
107
*/
118

12-
(function() {
9+
(function () {
1310
"use strict";
1411

15-
// ==========================================
16-
// 1. KONFIGURATION
17-
// ==========================================
18-
const CONFIG = {
19-
sensors: {
20-
mailbox: 'hm-rpc.0.001E1D899E942E.1.STATE', // Sensor Briefkasten
21-
door: 'hm-rpc.0.0023DA49A3AFF8.1.STATE' // Sensor Haustür / Bewegungsmelder
22-
},
23-
states: {
24-
mailArrived: '0_userdata.0.briefkasten.post_war_da',
25-
inhabitantsInformed: '0_userdata.0.briefkasten.bewohner_informiert',
26-
notificationText: '0_userdata.0.Ollama.Notification.Briefkasten'
27-
},
28-
alexa: {
29-
speak: 'alexa2.0.Echo-Devices.G0911M10020400F5.Commands.speak',
30-
volume: 'alexa2.0.Echo-Devices.G0911M10020400F5.Commands.speak-volume',
31-
announceVolume: 30, // Gewünschte Lautstärke für die Ansage
32-
restoreDelayMs: 10000 // Zeit in ms, bevor alte Lautstärke wiederhergestellt wird
33-
},
34-
telegram: {
35-
instance: 'telegram.0'
36-
},
37-
debounceMs: 60000 // Entprellzeit für den Briefkasten (1 Minute)
38-
};
39-
40-
// ==========================================
41-
// 2. INTERNE VARIABLEN
42-
// ==========================================
43-
let timeoutMailbox = null;
44-
let timeoutAlexaVolume = null;
45-
46-
// ==========================================
47-
// 3. HAUPT-LOGIK
48-
// ==========================================
49-
50-
/**
51-
* Sendet eine Nachricht über Telegram
52-
* @param {string} text - Der zu sendende Text
53-
*/
54-
function sendTelegramNotification(text) {
55-
sendTo(CONFIG.telegram.instance, 'send', {
56-
text: text
57-
});
58-
log(`Telegram Nachricht versendet: ${text}`, 'info');
59-
}
60-
61-
/**
62-
* Initialisiert alle Trigger und Zeitpläne
63-
*/
64-
function init() {
65-
log('Briefkasten-Script gestartet.', 'info');
66-
67-
// --------------------------------------------------------
68-
// TRIGGER 1: Briefkasten wird geöffnet
69-
// --------------------------------------------------------
70-
on({ id: CONFIG.sensors.mailbox, change: 'ne' }, function (obj) {
71-
// Homematic liefert bei STATE oft 1/0 oder true/false. Wir prüfen auf 1 oder true.
72-
if (obj.state.val === 1 || obj.state.val === true) {
73-
74-
// Entprellung: Verhindert mehrfaches Auslösen innerhalb kurzer Zeit
75-
if (!timeoutMailbox) {
76-
log('Briefkasten wurde geöffnet. Post ist da!', 'info');
77-
78-
// Status aktualisieren (true = Update/Acknowledge)
79-
setState(CONFIG.states.mailArrived, true, true);
80-
setState(CONFIG.states.inhabitantsInformed, false, true);
81-
82-
// Text abrufen und senden
83-
const msgText = getState(CONFIG.states.notificationText).val;
84-
sendTelegramNotification(msgText);
85-
86-
// Timeout setzen, damit für die konfigurierte Zeit keine weitere Meldung rausgeht
87-
timeoutMailbox = setTimeout(() => {
88-
timeoutMailbox = null;
89-
}, CONFIG.debounceMs);
12+
// @ts-ignore
13+
const axios = require('axios');
14+
15+
// ==========================================
16+
// 1. KONFIGURATION
17+
// ==========================================
18+
const CONFIG = {
19+
sensors: {
20+
mailbox: 'hm-rpc.0.001E1D899E942E.1.STATE',
21+
door: 'hm-rpc.0.0023DA49A3AFF8.1.STATE'
22+
},
23+
states: {
24+
mailArrived: '0_userdata.0.briefkasten.post_war_da',
25+
inhabitantsInformed: '0_userdata.0.briefkasten.bewohner_informiert',
26+
notificationText: '0_userdata.0.Ollama.Notification.Briefkasten'
27+
},
28+
alexa: {
29+
speak: 'alexa2.0.Echo-Devices.G0911M10020400F5.Commands.speak',
30+
volume: 'alexa2.0.Echo-Devices.G0911M10020400F5.Commands.speak-volume',
31+
announceVolume: 30,
32+
restoreDelayMs: 10000
33+
},
34+
telegram: {
35+
instance: 'telegram.0'
36+
},
37+
ollama: {
38+
baseUrl: 'http://192.168.50.6:11434',
39+
modelPrimary: 'gemma4:31b-cloud',
40+
modelFallback: ['qwen3-coder-next:cloud', 'gpt-oss:20b-cloud'],
41+
options: {
42+
temperature: 0.85,
43+
top_p: 0.92,
44+
top_k: 40,
45+
num_predict: 80,
46+
repeat_penalty: 1.15
9047
}
48+
},
49+
mailboxDebounceMs: 60000 // Entprellzeit Briefkasten (1 Minute)
50+
};
51+
52+
// ==========================================
53+
// 2. INTERNE VARIABLEN
54+
// ==========================================
55+
let timeoutMailbox = null;
56+
let timeoutAlexaVolume = null;
57+
58+
// ==========================================
59+
// 3. OLLAMA-LOGIK
60+
// ==========================================
61+
62+
function getTimeOfDayContext() {
63+
const hour = new Date().getHours();
64+
if (hour >= 5 && hour < 12) return "Es ist Vormittag.";
65+
if (hour >= 12 && hour < 18) return "Es ist Nachmittag.";
66+
if (hour >= 18 && hour < 22) return "Es ist Abend.";
67+
return "Es ist mitten in der Nacht.";
68+
}
69+
70+
function buildPrompt() {
71+
const persona = 'ein extrem höflicher, britischer Butler namens James';
72+
const timeCtx = getTimeOfDayContext();
73+
return `### SYSTEM
74+
Du bist ${persona}.
75+
Deine einzige Aufgabe: Formuliere eine einzige, knappe Smart-Home-Benachrichtigung.
76+
77+
STRIKTE REGELN — halte sie ohne Ausnahme ein:
78+
- Erwähne NUR den Briefkasten. Erfinde nichts dazu.
79+
- Schreibe im Perfekt (Vergangenheit).
80+
- Maximal 1 Satz, maximal 12 Wörter.
81+
- Sprich den Nutzer passend an (z. B. "Hallo", "Guten Tag" o. Ä.).
82+
- Gib NUR den reinen Benachrichtigungstext aus — keine Anführungszeichen, keine Einleitungen, kein Kommentar.
83+
- Antworte ausschließlich auf Deutsch.
84+
85+
### AUFGABE
86+
${timeCtx}
87+
Gerät: Briefkasten im Außenbereich
88+
Ereignis: Post eingeworfen
89+
90+
### BENACHRICHTIGUNG`;
91+
}
92+
93+
async function generateWithFallback(prompt) {
94+
const modelQueue = [CONFIG.ollama.modelPrimary, ...CONFIG.ollama.modelFallback];
95+
const url = `${CONFIG.ollama.baseUrl}/api/generate`;
96+
97+
for (const model of modelQueue) {
98+
try {
99+
log(`Ollama: Versuche Modell "${model}"...`);
100+
const response = await axios.post(url, {
101+
model: model,
102+
prompt: prompt,
103+
stream: false,
104+
options: CONFIG.ollama.options
105+
}, {
106+
headers: { 'Content-Type': 'application/json' },
107+
timeout: 60000
108+
});
109+
110+
if (response.status === 200 && response.data?.response) {
111+
log(`Ollama: Erfolg mit Modell "${model}".`);
112+
return response.data.response;
113+
}
114+
} catch (e) {
115+
if (e.response?.status === 404) {
116+
log(`Ollama: Modell "${model}" nicht gefunden. Versuche nächstes...`, 'warn');
117+
} else if (e.code === 'ECONNREFUSED' || e.code === 'ECONNABORTED') {
118+
log(`Ollama: Server nicht erreichbar (${CONFIG.ollama.baseUrl}): ${e.message}`, 'error');
119+
return null; // Server weg, kein weiterer Versuch
120+
} else {
121+
log(`Ollama: Fehler mit "${model}" (HTTP ${e.response?.status || 'N/A'}): ${e.message}`, 'warn');
122+
}
123+
}
124+
}
125+
log('Ollama: Alle Modelle fehlgeschlagen.', 'error');
126+
return null;
127+
}
128+
129+
/**
130+
* Generiert den Benachrichtigungstext via Ollama und gibt ihn bereinigt zurück.
131+
* Gibt einen Fallback-Text zurück, falls Ollama nicht erreichbar ist.
132+
*/
133+
async function generateNotificationText() {
134+
const rawText = await generateWithFallback(buildPrompt());
135+
136+
if (!rawText) {
137+
return 'Es wurde Post in den Briefkasten eingeworfen.'; // Fallback
91138
}
92-
});
93-
94-
// --------------------------------------------------------
95-
// TRIGGER 2: Bewohner kommt nach Hause / Tür öffnet
96-
// --------------------------------------------------------
97-
on({ id: CONFIG.sensors.door, change: 'ne' }, function (obj) {
98-
if (obj.state.val === 1 || obj.state.val === true) {
99-
100-
// Aktuelle Statuswerte abrufen
101-
const mailArrived = getState(CONFIG.states.mailArrived).val;
139+
140+
return rawText
141+
.trim()
142+
.replace(/^["'»«]|["'""»«]$/g, '')
143+
.replace(/\n.*/s, '')
144+
.replace(/^(Benachrichtigung:|Antwort:|Output:)\s*/i, '')
145+
.trim();
146+
}
147+
148+
// ==========================================
149+
// 4. BENACHRICHTIGUNGS-LOGIK
150+
// ==========================================
151+
152+
function sendTelegramNotification(text) {
153+
sendTo(CONFIG.telegram.instance, 'send', { text });
154+
log(`Telegram: Nachricht versendet: "${text}"`, 'info');
155+
}
156+
157+
function announceViaAlexa(text) {
158+
const previousVolume = getState(CONFIG.alexa.volume).val;
159+
setState(CONFIG.alexa.volume, CONFIG.alexa.announceVolume, false);
160+
setState(CONFIG.alexa.speak, text, false);
161+
162+
if (timeoutAlexaVolume) clearTimeout(timeoutAlexaVolume);
163+
timeoutAlexaVolume = setTimeout(() => {
164+
setState(CONFIG.alexa.volume, previousVolume, false);
165+
log(`Alexa: Lautstärke wieder auf ${previousVolume} gesetzt.`, 'info');
166+
}, CONFIG.alexa.restoreDelayMs);
167+
}
168+
169+
// ==========================================
170+
// 5. HAUPT-TRIGGER
171+
// ==========================================
172+
173+
function init() {
174+
log('Briefkasten-Script v2.0 gestartet.', 'info');
175+
176+
// --------------------------------------------------------
177+
// TRIGGER 1: Briefkasten wird geöffnet
178+
// Ablauf: Ollama-Text generieren → Telegram senden → Status setzen
179+
// --------------------------------------------------------
180+
on({ id: CONFIG.sensors.mailbox, change: 'ne' }, async function (obj) {
181+
if (obj.state.val !== 1 && obj.state.val !== true) return;
182+
if (timeoutMailbox) return; // Entprellung aktiv
183+
184+
log('Briefkasten geöffnet — starte Textgenerierung...', 'info');
185+
186+
// Debounce sofort aktivieren
187+
timeoutMailbox = setTimeout(() => { timeoutMailbox = null; }, CONFIG.mailboxDebounceMs);
188+
189+
// Status zurücksetzen: Bewohner müssen neu informiert werden
190+
setState(CONFIG.states.inhabitantsInformed, false, true);
191+
setState(CONFIG.states.mailArrived, false, true); // Erst am Ende auf true
192+
193+
// *** KERNFIX: Text ZUERST generieren, dann alles andere ***
194+
const msgText = await generateNotificationText();
195+
196+
// Generierten Text persistieren (für Alexa-Ansage später)
197+
setState(CONFIG.states.notificationText, msgText, true);
198+
199+
// Telegram-Benachrichtigung mit frischem Text
200+
sendTelegramNotification(msgText);
201+
202+
// Jetzt erst mailArrived = true setzen (Alexa-Trigger wartet darauf)
203+
setState(CONFIG.states.mailArrived, true, true);
204+
205+
log(`Briefkasten-Benachrichtigung abgeschlossen: "${msgText}"`, 'info');
206+
});
207+
208+
// --------------------------------------------------------
209+
// TRIGGER 2: Bewohner kommt nach Hause / Tür öffnet
210+
// Alexa liest den bereits gespeicherten Text vor
211+
// --------------------------------------------------------
212+
on({ id: CONFIG.sensors.door, change: 'ne' }, function (obj) {
213+
if (obj.state.val !== 1 && obj.state.val !== true) return;
214+
215+
const mailArrived = getState(CONFIG.states.mailArrived).val;
102216
const inhabitantsInformed = getState(CONFIG.states.inhabitantsInformed).val;
103217

104-
// Prüfen, ob Post da ist und Bewohner noch NICHT informiert wurden
105218
if (mailArrived === true && inhabitantsInformed === false) {
106-
log('Bewohner erkannt. Starte Alexa-Ansage zur Post.', 'info');
107-
108-
const msgText = getState(CONFIG.states.notificationText).val;
109-
const previousVolume = getState(CONFIG.alexa.volume).val;
219+
log('Bewohner erkannt — starte Alexa-Ansage.', 'info');
110220

111-
// Lautstärke setzen (false = Steuern/Command)
112-
setState(CONFIG.alexa.volume, CONFIG.alexa.announceVolume, false);
113-
114-
// Ansage ausgeben
115-
setState(CONFIG.alexa.speak, msgText, false);
221+
const msgText = getState(CONFIG.states.notificationText).val;
222+
announceViaAlexa(msgText);
116223

117-
// Status aktualisieren
118224
setState(CONFIG.states.inhabitantsInformed, true, true);
119225
setState(CONFIG.states.mailArrived, false, true);
120-
121-
// Alte Lautstärke nach X Sekunden wiederherstellen
122-
if (timeoutAlexaVolume) clearTimeout(timeoutAlexaVolume);
123-
timeoutAlexaVolume = setTimeout(() => {
124-
setState(CONFIG.alexa.volume, previousVolume, false);
125-
log(`Alexa Lautstärke wieder auf ${previousVolume} gesetzt.`, 'info');
126-
}, CONFIG.alexa.restoreDelayMs);
127226
}
128-
}
129-
});
130-
131-
// --------------------------------------------------------
132-
// ZEITPLAN: Reset um 00:05 Uhr jeden Tag
133-
// --------------------------------------------------------
134-
schedule('5 0 * * *', function () {
135-
log('Täglicher Reset: Post-Status wird zurückgesetzt.', 'info');
136-
setState(CONFIG.states.mailArrived, false, true);
137-
// Hinweis: bewohner_informiert wird hier bewusst nicht zurückgesetzt,
138-
// da es beim Einwurf neuer Post ohnehin auf false geht.
139-
});
140-
}
141-
142-
// ==========================================
143-
// 4. SCRIPT START
144-
// ==========================================
145-
init();
227+
});
228+
229+
// --------------------------------------------------------
230+
// ZEITPLAN: Täglicher Reset um 00:05 Uhr
231+
// --------------------------------------------------------
232+
schedule('5 0 * * *', function () {
233+
log('Täglicher Reset: Post-Status wird zurückgesetzt.', 'info');
234+
setState(CONFIG.states.mailArrived, false, true);
235+
});
236+
}
237+
238+
// ==========================================
239+
// 6. START
240+
// ==========================================
241+
init();
146242

147243
})();

0 commit comments

Comments
 (0)