Skip to content

Commit 25707dd

Browse files
committed
Update / Modification from the production live system: 2026-05-20 13:30
1 parent fe4d969 commit 25707dd

1 file changed

Lines changed: 149 additions & 95 deletions

File tree

iobroker/10_System/Benachrichtigungen/gemini_benachrichtigungen.js

Lines changed: 149 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -1,177 +1,231 @@
11
/*
2-
* NEXT-LEVEL ioBroker Gemini Integration
3-
* * Features:
4-
* - Dynamische Benachrichtigungen mit Google Gemini 2.5 Flash
2+
* NEXT-LEVEL ioBroker Ollama Integration
3+
*
4+
* Features:
5+
* - Dynamische Benachrichtigungen mit lokalem Ollama-Modell
56
* - Spamschutz (Debouncing) für flatternde Sensoren
67
* - Eigene "Personas" (Charaktere) pro Gerät
78
* - Tageszeit-Kontext für natürlichere Antworten
8-
* - Natives API System-Prompting für strikte Regel-Einhaltung
9+
* - Optimiertes Prompting für lokale LLMs (Ollama-kompatibel)
10+
* - Automatisches Modell-Fallback
911
*/
1012

1113
(function() {
1214
"use strict";
13-
14-
// Das 'axios' Modul einmalig laden.
15-
// Muss als zusätzliches NPM-Modul in der Javascript-Adapter Konfiguration eingetragen werden!
15+
16+
// @ts-ignore
1617
const axios = require('axios');
1718

1819
// +++++ KONFIGURATION +++++
1920

20-
const GEMINI_API_KEY = getState("0_userdata.0.API-Keys.GEMINI_API_KEY").val;
21-
const GEMINI_MODEL = 'gemini-2.5-flash';
21+
// Ollama Endpunkt & Modelle
22+
const OLLAMA_BASE_URL = 'http://192.168.50.6:11434';
23+
const OLLAMA_MODEL_PRIMARY = 'gemma4:31b-cloud';
24+
const OLLAMA_MODEL_FALLBACK = ['qwen3-coder-next:cloud', 'gpt-oss:20b-cloud'];
25+
26+
// Generierungs-Parameter (für lokale Modelle wichtiger als bei Cloud-APIs)
27+
const GENERATION_OPTIONS = {
28+
temperature: 0.85, // Kreativität: 0.0 = deterministisch, 1.0 = sehr kreativ
29+
top_p: 0.92,
30+
top_k: 40,
31+
num_predict: 80, // Max. Token-Ausgabe; kurz halten für kompakte Benachrichtigungen
32+
repeat_penalty: 1.15 // Verhindert Wiederholungen im Text
33+
};
2234

2335
// Globale Standard-Persona, falls beim Gerät keine angegeben ist
2436
const DEFAULT_PERSONA = 'eine freundliche und leicht humorvolle Smart-Home KI';
2537

2638
const DEVICES = [
2739
{
28-
triggerId: '0_userdata.0.briefkasten.post_war_da',
29-
targetId: '0_userdata.0.Gemini.Notification.Briefkasten',
40+
triggerId: '0_userdata.0.briefkasten.post_war_da',
41+
targetId: '0_userdata.0.Ollama.Notification.Briefkasten',
3042
deviceName: 'Briefkasten im Außenbereich',
3143
stateMapping: { true: 'Post eingeworfen', false: 'Post geleert' },
32-
triggerOn: [true, false],
33-
tense: 'Vergangenheit',
34-
// NEXT LEVEL FEATURES:
35-
persona: 'ein extrem höflicher, britischer Butler namens James', // KI spielt eine Rolle
36-
debounceTime: 5000 // Wartet 5 Sekunden nach dem letzten Trigger, um "Flattern" zu ignorieren
44+
triggerOn: [true, false],
45+
tense: 'Vergangenheit',
46+
persona: 'ein extrem höflicher, britischer Butler namens James',
47+
debounceTime: 5000
3748
},
3849
// Beispiel für ein weiteres Gerät:
3950
// {
40-
// triggerId: 'alias.0.Wohnzimmer.Fenster.ACTUAL',
41-
// targetId: '0_userdata.0.Gemini.Notification.Fenster_Wohnzimmer',
51+
// triggerId: 'alias.0.Wohnzimmer.Fenster.ACTUAL',
52+
// targetId: '0_userdata.0.Ollama.Notification.Fenster_Wohnzimmer',
4253
// deviceName: 'Wohnzimmerfenster',
4354
// stateMapping: { true: 'geöffnet', false: 'geschlossen' },
44-
// triggerOn: [true],
45-
// tense: 'Gegenwart',
46-
// persona: 'eine leicht besorgte Mutter, die Angst vor Zugluft hat',
55+
// triggerOn: [true],
56+
// tense: 'Gegenwart',
57+
// persona: 'eine leicht besorgte Mutter, die Angst vor Zugluft hat',
4758
// debounceTime: 2000
4859
// }
4960
];
5061

5162
// +++++ INTERNE LOGIK +++++
5263

53-
const GEMINI_API_URL = `https://generativelanguage.googleapis.com/v1beta/models/${GEMINI_MODEL}:generateContent?key=${GEMINI_API_KEY}`;
54-
const debounceTimers = {}; // Speichert die Timer für den Spamschutz
64+
const OLLAMA_GENERATE_URL = `${OLLAMA_BASE_URL}/api/generate`;
65+
const debounceTimers = {};
5566

56-
// Hilfsfunktion: Tageszeit ermitteln für mehr Kontext
5767
function getTimeOfDayContext() {
5868
const hour = new Date().getHours();
59-
if (hour >= 5 && hour < 12) return "Es ist Vormittag.";
69+
if (hour >= 5 && hour < 12) return "Es ist Vormittag.";
6070
if (hour >= 12 && hour < 18) return "Es ist Nachmittag.";
6171
if (hour >= 18 && hour < 22) return "Es ist Abend.";
6272
return "Es ist mitten in der Nacht.";
6373
}
6474

65-
async function getGeminiResponse(deviceConfig, stateName) {
66-
if (GEMINI_API_KEY === 'DEIN_API_SCHLUESSEL' || GEMINI_API_KEY.length < 10) {
67-
log('Bitte tragen Sie einen gültigen Gemini API-Schlüssel ein.', 'error');
68-
return;
69-
}
70-
75+
/**
76+
* Baut den optimierten Prompt für lokale Ollama-Modelle.
77+
* Lokale LLMs reagieren besser auf explizite, direkte Anweisungen
78+
* als auf abstrakte Rollenspiel-Konstrukte.
79+
*/
80+
function buildPrompt(deviceConfig, stateName) {
7181
const tenseMap = {
72-
'vergangenheit': 'Verwende zwingend die Vergangenheitsform (Perfekt).',
73-
'zukunft': 'Verwende zwingend die Zukunftsform (Futur I).',
74-
'gegenwart': 'Verwende zwingend die Gegenwartsform (Präsens).'
82+
'vergangenheit': 'Schreibe im Perfekt (Vergangenheit).',
83+
'zukunft': 'Schreibe im Futur I (Zukunft).',
84+
'gegenwart': 'Schreibe im Präsens (Gegenwart).'
7585
};
7686
const tenseInstruction = tenseMap[(deviceConfig.tense || 'gegenwart').toLowerCase()] || tenseMap['gegenwart'];
77-
const persona = deviceConfig.persona || DEFAULT_PERSONA;
78-
const timeContext = getTimeOfDayContext();
79-
80-
// NEXT LEVEL: Wir nutzen 'systemInstruction', um der KI unumstößliche Kern-Regeln zu geben
81-
const systemInstruction = `Du bist ${persona}.
82-
Deine Aufgabe ist es, den Nutzer über ein Smart-Home-Ereignis zu informieren.
83-
84-
STRENGE REGELN:
85-
1. Erwähne AUSSCHLIESSLICH das vorgegebene Gerät. Erfinde niemals andere Geräte!
86-
2. ${tenseInstruction}
87-
3. Formuliere exakt einen einzigen, kompakten Satz (maximal 12 Worte).
88-
4. Sprich den Nutzer direkt an (z.B. "Sir", "Du", "Euer Majestät" - passend zu deiner Persona).
89-
5. Liefere nur den reinen Benachrichtigungstext, ohne Anführungszeichen oder Einleitungen.
90-
6. Berücksichtige die aktuelle Tageszeit in deiner Formulierung, falls es sich natürlich anfühlt.`;
91-
92-
// Der eigentliche User-Prompt enthält nur noch die reinen, nackten Fakten
93-
const userPrompt = `FAKTEN:
94-
- Aktuelle Tageszeit: ${timeContext}
95-
- Betroffenes Gerät: ${deviceConfig.deviceName}
96-
- Neues Ereignis/Zustand: ${stateName}`;
97-
98-
const payload = {
99-
systemInstruction: { parts: [{ text: systemInstruction }] },
100-
contents: [{ parts: [{ text: userPrompt }] }]
101-
};
87+
const persona = deviceConfig.persona || DEFAULT_PERSONA;
88+
const timeCtx = getTimeOfDayContext();
89+
90+
// Für lokale Modelle: Klarer Aufbau mit System-Block, dann direkter Aufgabe.
91+
// Viele Ollama-Modelle unterstützen keinen separaten system-Parameter im /api/generate
92+
// Endpunkt, daher wird alles in einen einzigen strukturierten Prompt gebettet.
93+
return `### SYSTEM
94+
Du bist ${persona}.
95+
Deine einzige Aufgabe: Formuliere eine einzige, knappe Smart-Home-Benachrichtigung.
96+
97+
STRIKTE REGELN — halte sie ohne Ausnahme ein:
98+
- Erwähne NUR das angegebene Gerät. Erfinde nichts dazu.
99+
- ${tenseInstruction}
100+
- Maximal 1 Satz, maximal 12 Wörter.
101+
- Sprich den Nutzer passend zur Persona an (z. B. "Sir", "Du" o. Ä.).
102+
- Gib NUR den reinen Benachrichtigungstext aus — keine Anführungszeichen, keine Einleitungen, kein Kommentar.
103+
- Antworte ausschließlich auf Deutsch.
104+
105+
### AUFGABE
106+
${timeCtx}
107+
Gerät: ${deviceConfig.deviceName}
108+
Ereignis: ${stateName}
109+
110+
### BENACHRICHTIGUNG`;
111+
}
102112

103-
const MAX_RETRIES = 3;
104-
let delay = 2000;
113+
/**
114+
* Versucht die Generierung mit dem primären Modell,
115+
* fällt bei Fehler automatisch auf die Fallback-Modelle zurück.
116+
*/
117+
async function generateWithFallback(prompt) {
118+
const modelQueue = [OLLAMA_MODEL_PRIMARY, ...OLLAMA_MODEL_FALLBACK];
105119

106-
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
120+
for (const model of modelQueue) {
107121
try {
108-
log(`Sende Anfrage für "${deviceConfig.targetId}" an Gemini API...`);
109-
const response = await axios.post(GEMINI_API_URL, payload, {
110-
headers: { 'Content-Type': 'application/json' }
122+
log(`Versuche Modell: ${model}`);
123+
const payload = {
124+
model: model,
125+
prompt: prompt,
126+
stream: false, // Wichtig: false für eine einzelne vollständige Antwort
127+
options: GENERATION_OPTIONS
128+
};
129+
130+
const response = await axios.post(OLLAMA_GENERATE_URL, payload, {
131+
headers: { 'Content-Type': 'application/json' },
132+
timeout: 60000 // 60 Sekunden Timeout (lokale Modelle können langsam laden)
111133
});
112134

113-
if (response.status === 200) {
114-
const generatedText = response.data.candidates?.[0]?.content?.parts?.[0]?.text;
115-
if (generatedText) {
116-
const cleanText = generatedText.trim().replace(/^["']|["']$/g, ''); // Entfernt evtl. generierte Anführungszeichen
117-
log(`Antwort für ${deviceConfig.targetId}: ${cleanText}`);
118-
setState(deviceConfig.targetId, cleanText, true);
119-
return;
120-
}
135+
if (response.status === 200 && response.data?.response) {
136+
log(`Erfolg mit Modell: ${model}`);
137+
return { text: response.data.response, model };
121138
}
139+
122140
} catch (e) {
123-
if ((e.response?.status === 503 || e.response?.status === 429) && attempt < MAX_RETRIES) {
124-
await new Promise(resolve => setTimeout(resolve, delay));
125-
delay *= 2;
141+
const status = e.response?.status;
142+
const errMsg = e.message;
143+
144+
if (status === 404) {
145+
log(`Modell "${model}" nicht gefunden auf Ollama-Server. Versuche nächstes...`, 'warn');
146+
} else if (e.code === 'ECONNREFUSED' || e.code === 'ECONNABORTED') {
147+
log(`Ollama-Server nicht erreichbar (${OLLAMA_BASE_URL}): ${errMsg}`, 'error');
148+
return null; // Kein Fallback nötig, Server ist weg
126149
} else {
127-
log(`API Fehler bei ${deviceConfig.targetId}: ${e.message}`, 'error');
128-
setState(deviceConfig.targetId, `API Fehler aufgetreten.`, true);
129-
return;
150+
log(`Fehler mit Modell "${model}" (HTTP ${status || 'N/A'}): ${errMsg}`, 'warn');
130151
}
131152
}
132153
}
154+
155+
log('Alle Modelle fehlgeschlagen. Keine Benachrichtigung generiert.', 'error');
156+
return null;
157+
}
158+
159+
async function getOllamaResponse(deviceConfig, stateName) {
160+
const prompt = buildPrompt(deviceConfig, stateName);
161+
log(`Sende Anfrage für "${deviceConfig.deviceName}" an Ollama...`);
162+
163+
const result = await generateWithFallback(prompt);
164+
165+
if (!result) {
166+
setState(deviceConfig.targetId, 'Fehler: Ollama nicht erreichbar.', true);
167+
return;
168+
}
169+
170+
// Bereinigung: Anführungszeichen, Zeilenumbrüche und Modell-Artefakte entfernen
171+
let cleanText = result.text
172+
.trim()
173+
.replace(/^["'»«]|["'""»«]$/g, '') // Anführungszeichen am Rand
174+
.replace(/\n.*/s, '') // Alles nach dem ersten Zeilenumbruch
175+
.replace(/^(Benachrichtigung:|Antwort:|Output:)\s*/i, '') // Modell-Präfixe
176+
.trim();
177+
178+
log(`[${result.model}] Antwort für "${deviceConfig.deviceName}": ${cleanText}`);
179+
setState(deviceConfig.targetId, cleanText, true);
133180
}
134181

135182
async function main() {
136-
// NEXT LEVEL: Saubere ioBroker Datenpunkt-Erstellung mit createStateAsync (kompatibel mit allen JS-Adapter Versionen)
183+
// Zieldatenpunkte anlegen
137184
for (const device of DEVICES) {
138185
await createStateAsync(device.targetId, {
139-
name: `Gemini Info: ${device.deviceName}`,
140-
desc: `KI generierter Text für ${device.deviceName}`,
141-
type: 'string',
142-
role: 'text',
143-
read: true,
186+
name: `Ollama Info: ${device.deviceName}`,
187+
desc: `KI generierter Text für ${device.deviceName}`,
188+
type: 'string',
189+
role: 'text',
190+
read: true,
144191
write: false,
145-
def: ''
192+
def: ''
146193
});
147194
}
148195

196+
// Verbindung zum Ollama-Server beim Start prüfen
197+
try {
198+
await axios.get(`${OLLAMA_BASE_URL}/api/tags`, { timeout: 5000 });
199+
log(`Ollama-Server erreichbar unter ${OLLAMA_BASE_URL}. Skript bereit.`);
200+
} catch (e) {
201+
log(`WARNUNG: Ollama-Server unter ${OLLAMA_BASE_URL} nicht erreichbar. Bitte Server prüfen!`, 'warn');
202+
}
203+
149204
const allTriggerIds = DEVICES.map(device => device.triggerId);
150-
log('Gemini Skript gestartet. Alle Zieldatenpunkte sind bereit.');
205+
log('Ollama-Skript gestartet. Alle Zieldatenpunkte sind bereit.');
151206

152207
on({ id: allTriggerIds, change: 'ne' }, async (obj) => {
153208
const device = DEVICES.find(d => d.triggerId === obj.id);
154209
if (!device) return;
155210

156211
const stateVal = obj.state.val;
157212
const triggerOn = device.triggerOn || [true];
158-
213+
159214
if (!triggerOn.includes(stateVal)) return;
160215

161216
const stateName = device.stateMapping[stateVal.toString()] || (stateVal ? 'aktiviert' : 'deaktiviert');
162-
log(`Trigger erkannt: ${device.deviceName} -> "${stateName}". Starte Spamschutz-Timer...`);
217+
log(`Trigger erkannt: ${device.deviceName} "${stateName}". Starte Debounce-Timer...`);
163218

164-
// NEXT LEVEL: Debouncing (Spamschutz)
165-
// Wenn der Trigger innerhalb der Wartezeit erneut auslöst, wird der alte Timer abgebrochen
219+
// Debouncing: Bei erneutem Trigger innerhalb der Wartezeit alten Timer verwerfen
166220
if (debounceTimers[device.triggerId]) {
167221
clearTimeout(debounceTimers[device.triggerId]);
168222
}
169223

170-
const debounceMs = device.debounceTime || 1000; // Standard: 1 Sekunde
171-
224+
const debounceMs = device.debounceTime || 1000;
225+
172226
debounceTimers[device.triggerId] = setTimeout(() => {
173-
log(`Spamschutz abgelaufen. Generiere Text für ${device.deviceName}...`);
174-
getGeminiResponse(device, stateName);
227+
log(`Debounce abgelaufen. Generiere Text für ${device.deviceName}...`);
228+
getOllamaResponse(device, stateName);
175229
delete debounceTimers[device.triggerId];
176230
}, debounceMs);
177231
});

0 commit comments

Comments
 (0)