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 ( / ^ ( B e n a c h r i c h t i g u n g : | A n t w o r t : | O u t p u t : ) \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