NEWS
[Vorlage] Anwesenheitssimulation mit dauerhaftem Lernen
-
Hallo zusammen,
ich möchte euch heute ein Projekt vorstellen, das das Thema Anwesenheitssimulation einmal anders angeht. Anstatt starrer Pläne oder durchschaubarer Zufallsschaltungen nutzt dieses Skript einen Snapshot-Ansatz, der sich vollautomatisch eurem echten Leben anpasst.
Das Konzept: Ein System, das mitdenkt
Das Skript erstellt alle 15 Minuten einen "Snapshot" eurer Lichter (oder anderer Geräte) – aber nur, wenn ihr wirklich zu Hause seid. Im Abwesenheitsmodus (Urlaub) spielt es diese Aufzeichnungen zeitversetzt wieder ab.
Die Highlights:
- Absolut wartungsfrei: Einmal eingerichtet, müsst ihr nie wieder eingreifen. Keine Sommer-/Winterzeit-Umstellung, kein Nachjustieren bei neuen Gewohnheiten.
- Ganzjährig nutzbar: Da das Skript kontinuierlich lernt und alte Snapshots nach 5 Tagen automatisch überschreibt (Ringspeicher), "weiß" es im Dezember von selbst, dass das Licht früher brennen muss als im Juli.
- Natürliches Verhalten: Es spiegelt euer echtes Leben. Werden die Tage kürzer oder ändert sich euer Rhythmus, lernt die Simulation das einfach mit.
- Intelligente Variation: Aus den Snapshots der letzten Tage wird per Zufall gewählt und mit einem "Jitter" (zufällige Verzögerung) geschaltet. Das wirkt für Außenstehende absolut menschlich.
Neu in V2:
- Enum-/Aufzählungs-Support: Du kannst Lampen jetzt optional über eine ioBroker-Funktion (z.B. enum.functions.anwesenheitssimulation) verwalten. Das Skript kombiniert die Einträge aus der Liste LIGHTS und dem Enum automatisch. Du kannst sogar ganze Kanäle in das Enum ziehen – das Skript sucht sich den richtigen Datenpunkt (.STATE oder .ON) selbst.
- Helligkeits-Tracking: Das Skript sucht automatisch nach passenden Helligkeits-Datenpunkten im selben Kanal (z.B. .LEVEL, .ACTUAL, .BRIGHTNESS) und speichert den Dimmwert ab.
- Auto-Migration: Falls du das Skript startest und noch alte "Foto"-Daten in der Datenbank hast (die nur An/Aus kannten), stürzt das Skript nicht ab, sondern verarbeitet diese nahtlos weiter, bis sie nach 5 Tagen aus dem Gedächtnis rotieren.
Einrichtung & Installation
- Datenpunkte
0_userdata.0.Anwesenheit(Urlaubs-Trigger) und0_userdata.0.Anwesenheitssimulation_Snapshots_DB(JSON-Speicher) anlegen. - Eure Geräte-IDs im
devices-Array eintragen. Fertig.
Das Skript (TypeScript):
// TITEL: Smart Presence Simulation (KI-Ghost-Mode) - v2 // ===================================================================== // === 1. KONFIGURATION === const DP_PRESENCE: string = '0_userdata.0.Anwesenheit'; // NEU: Optional Lampen über ioBroker "Aufzählungen" (Enums) laden. // Leer lassen (''), falls nicht gewünscht. const ENUM_FUNKTION: string = 'enum.functions.anwesenheitssimulation'; // Hier können Lampen weiterhin manuell eingetragen werden (Kombination mit Enum ist möglich) const LIGHTS: string[] = [ 'alias.0.Licht.Flur.STATE', // <-- Trage hier alle Lampen ein, die 'alias.0.Licht.Wohnzimmer.STATE', // von der Straße aus sichtbar sind. 'alias.0.Licht.Kueche.STATE', 'alias.0.Licht.Bad.STATE' ]; // === 2. EXPERTEN-EINSTELLUNGEN === const MAX_SNAPSHOTS: number = 5; const MAX_SAMPLING_DELAY: number = 300000; const MAX_PLAYBACK_DELAY: number = 840000; const DP_STORAGE: string = '0_userdata.0.Simulation.Snapshot_DB'; // ============================================================ // === TYPEN & INTERFACES === // ============================================================ interface LightStateData { on: boolean; bri?: number; // Optionaler Helligkeitswert } interface LightSnapshot { // Unterstützt sowohl das neue Format (LightStateData) als auch das alte (boolean) für reibungslose Migration [baseId: string]: LightStateData | boolean; } interface SnapshotDatabase { [slotIndex: number]: LightSnapshot[]; } interface ParsedLight { baseId: string; powerId: string; briId: string | null; } // ============================================================ // === HILFSFUNKTIONEN === // ============================================================ function getSlotIndex(): number { const now = new Date(); return Math.floor((now.getHours() * 60 + now.getMinutes()) / 15); } function getSafeBoolean(oid: string, fallback: boolean = false): boolean { if (oid && existsState(oid)) { const val = getState(oid).val; return (val === true || val === 'true' || val === 1); } return fallback; } // NEU: Sammelt dynamisch alle Lampen aus dem Array und dem Enum function getActiveLights(): ParsedLight[] { let rawIds: string[] = [...LIGHTS]; if (ENUM_FUNKTION && existsObject(ENUM_FUNKTION)) { const enumObj = getObject(ENUM_FUNKTION); if (enumObj && enumObj.common && enumObj.common.members) { rawIds = rawIds.concat(enumObj.common.members); } } // Duplikate entfernen rawIds = [...new Set(rawIds)]; const result: ParsedLight[] = []; for (const id of rawIds) { if (!existsObject(id)) continue; let powerId = id; const obj = getObject(id); // Falls der Nutzer einen ganzen Kanal ins Enum gezogen hat, suchen wir den STATE if (obj && obj.type === 'channel') { if (existsState(`${id}.STATE`)) powerId = `${id}.STATE`; else if (existsState(`${id}.ON`)) powerId = `${id}.ON`; } // Helligkeits-Datenpunkt dynamisch im selben Kanal suchen const parts = powerId.split('.'); parts.pop(); const channel = parts.join('.'); let briId: string | null = null; const briCandidates = ['LEVEL', 'ACTUAL', 'BRIGHTNESS', 'DIMMER', 'SET']; for (const candidate of briCandidates) { if (existsState(`${channel}.${candidate}`)) { briId = `${channel}.${candidate}`; break; } } result.push({ baseId: powerId, powerId, briId }); } return result; } // ========================================== // 1. LERN-MODUS (Datenbank füttern) // ========================================== function runLearning(db: SnapshotDatabase, currentSlot: number): void { let snapshot: LightSnapshot = {}; const activeLights = getActiveLights(); // Aktuellen Zustand inkl. Helligkeit aller Lampen einfrieren for (const light of activeLights) { const isOn = getSafeBoolean(light.powerId, false); const stateData: LightStateData = { on: isOn }; if (isOn && light.briId && existsState(light.briId)) { const briVal = getState(light.briId).val; if (typeof briVal === 'number') stateData.bri = briVal; } snapshot[light.baseId] = stateData; } if (!db[currentSlot]) db[currentSlot] = []; db[currentSlot].push(snapshot); if (db[currentSlot].length > MAX_SNAPSHOTS) { db[currentSlot].shift(); } setState(DP_STORAGE, JSON.stringify(db), true); log(`[Simulation LERNEN] Block ${currentSlot} gespeichert. (${db[currentSlot].length}/${MAX_SNAPSHOTS} Snapshots)`, 'debug'); } // ========================================== // 2. GHOST-MODUS (Berechnen & Abspielen) // ========================================== function runSimulation(db: SnapshotDatabase, currentSlot: number): void { const activeLights = getActiveLights(); if (!db[currentSlot] || db[currentSlot].length === 0) { log(`[Simulation GHOST] Keine Daten für Block ${currentSlot}. Schalte Lichter aus.`, 'debug'); for (const light of activeLights) { if (getSafeBoolean(light.powerId, false) === true) setState(light.powerId, false, false); } return; } const slotSnapshots: LightSnapshot[] = db[currentSlot]; const uniqueCount = new Set(slotSnapshots.map(s => JSON.stringify(s))).size; const chaosFactor = uniqueCount / slotSnapshots.length; const dynamicMaxDelay = Math.floor(chaosFactor * MAX_PLAYBACK_DELAY); const randomDelayMs = Math.floor(Math.random() * dynamicMaxDelay); log(`[Simulation GHOST] Block ${currentSlot} | Chaos-Faktor: ${Math.round(chaosFactor*100)}% | Geplanter Delay: ${Math.round(randomDelayMs/1000)}s`, 'info'); // --- ABSPIELEN MIT BERECHNETER VERZÖGERUNG --- setTimeout(() => { const randomIndex = Math.floor(Math.random() * slotSnapshots.length); const chosenSnapshot = slotSnapshots[randomIndex]; for (const light of activeLights) { const targetRaw = chosenSnapshot[light.baseId]; if (targetRaw === undefined) continue; // Migration: Kompatibilität zu alten Boolean-Werten aus V1 let targetOn = false; let targetBri: number | undefined = undefined; if (typeof targetRaw === 'boolean') { targetOn = targetRaw; } else { targetOn = targetRaw.on; targetBri = targetRaw.bri; } const currentOn = getSafeBoolean(light.powerId, false); if (targetOn) { // Wenn Lampe an sein soll: Zuerst (optional) dimmen, dann einschalten if (targetBri !== undefined && light.briId) { const currentBri = getState(light.briId).val; if (currentBri !== targetBri) setState(light.briId, targetBri, false); } if (!currentOn) setState(light.powerId, true, false); } else { // Wenn Lampe aus sein soll if (currentOn) setState(light.powerId, false, false); } } }, randomDelayMs); } // === INIT & CRONJOB === createState(DP_STORAGE, JSON.stringify({}), { type: 'string', name: 'Snapshot Datenbank (JSON)', role: 'json' }, () => { schedule("*/15 * * * *", () => { const isPresent = getSafeBoolean(DP_PRESENCE, true); const currentSlot = getSlotIndex(); let db: SnapshotDatabase = {}; try { const raw = getState(DP_STORAGE).val as string; if (raw) db = JSON.parse(raw) as SnapshotDatabase; } catch (e) { log('[Simulation] Fehler beim Lesen der Datenbank, starte mit leerer DB neu.', 'warn'); } if (isPresent) { const samplingDelayMs = Math.floor(Math.random() * MAX_SAMPLING_DELAY); log(`[Simulation LERNEN] Block ${currentSlot} ausgelöst. Mache das Foto in ${Math.round(samplingDelayMs/1000)}s...`, 'debug'); setTimeout(() => { runLearning(db, currentSlot); }, samplingDelayMs); } else { runSimulation(db, currentSlot); } }); log('[Simulation] KI-Snapshot-Engine gestartet. (V2: Enums & Helligkeit integriert)'); });Fazit
Für mich war wichtig, dass ich mich vor dem Urlaub nicht um die Simulation kümmern muss. Durch den lernenden Charakter ist das System das ganze Jahr über "scharf" und bildet immer den aktuellen Lebensstil ab.
Ich freue mich auf euer Feedback!
Edit 18.03.2026: Ich habe hier in Post 1 auch die V2 eingefügt und die Anleitung entsprechend ergänzt. Der Datenpunkt "0_userdata.0.Anwesenheitssimulation_Snapshots" wurde in "0_userdata.0.Simulation.Snapshot_DB" umbenannt.
-
Finde den Ansatz sehr cool und praktisch.
Werde es evtl mal testen.Im Moment basiert meine Simulation auf dem Sonnenuntergang mit einen willkürlichem Offset der plus oder minus sein kann und dann dem schalten der relevanten Lampen in einer willkürlichen Reihenfolge mit willkürlichem offset.
Das bildet natürlich keine Gewohnheiten ab. Das einzig wirklich Variable ist bei mir aber aicu nur das Klo 🚽 🪠🤣.Ich fände es praktisch, wenn man die Lampen über eine Funktion der DPs vergeben kann.
So mache ich es. -
Hier ist ein kleines Update:
- Enum-/Aufzählungs-Support: Du kannst Lampen jetzt optional über eine ioBroker-Funktion (z.B.
enum.functions.anwesenheitssimulation) verwalten. Das Skript kombiniert die Einträge aus der ListeLIGHTSund dem Enum automatisch. Du kannst sogar ganze Kanäle in das Enum ziehen – das Skript sucht sich den richtigen Datenpunkt (.STATEoder.ON) selbst. - Helligkeits-Tracking: Das Skript sucht automatisch nach passenden Helligkeits-Datenpunkten im selben Kanal (z.B.
.LEVEL,.ACTUAL,.BRIGHTNESS) und speichert den Dimmwert ab. - Auto-Migration: Falls du das Skript startest und noch alte "Foto"-Daten in der Datenbank hast (die nur An/Aus kannten), stürzt das Skript nicht ab, sondern verarbeitet diese nahtlos weiter, bis sie nach 5 Tagen aus dem Gedächtnis rotieren.
// ===================================================================== // TITEL: Smart Presence Simulation (KI-Ghost-Mode) - v2 // ===================================================================== // === 1. KONFIGURATION === const DP_PRESENCE: string = '0_userdata.0.Anwesenheit'; // NEU: Optional Lampen über ioBroker "Aufzählungen" (Enums) laden. // Leer lassen (''), falls nicht gewünscht. const ENUM_FUNKTION: string = 'enum.functions.anwesenheitssimulation'; // Hier können Lampen weiterhin manuell eingetragen werden (Kombination mit Enum ist möglich) const LIGHTS: string[] = [ 'alias.0.Licht.Flur.STATE', // <-- Trage hier alle Lampen ein, die 'alias.0.Licht.Wohnzimmer.STATE', // von der Straße aus sichtbar sind. 'alias.0.Licht.Kueche.STATE', 'alias.0.Licht.Bad.STATE' ]; // === 2. EXPERTEN-EINSTELLUNGEN === const MAX_SNAPSHOTS: number = 5; const MAX_SAMPLING_DELAY: number = 300000; const MAX_PLAYBACK_DELAY: number = 840000; const DP_STORAGE: string = '0_userdata.0.Simulation.Snapshot_DB'; // ============================================================ // === TYPEN & INTERFACES === // ============================================================ interface LightStateData { on: boolean; bri?: number; // Optionaler Helligkeitswert } interface LightSnapshot { // Unterstützt sowohl das neue Format (LightStateData) als auch das alte (boolean) für reibungslose Migration [baseId: string]: LightStateData | boolean; } interface SnapshotDatabase { [slotIndex: number]: LightSnapshot[]; } interface ParsedLight { baseId: string; powerId: string; briId: string | null; } // ============================================================ // === HILFSFUNKTIONEN === // ============================================================ function getSlotIndex(): number { const now = new Date(); return Math.floor((now.getHours() * 60 + now.getMinutes()) / 15); } function getSafeBoolean(oid: string, fallback: boolean = false): boolean { if (oid && existsState(oid)) { const val = getState(oid).val; return (val === true || val === 'true' || val === 1); } return fallback; } // NEU: Sammelt dynamisch alle Lampen aus dem Array und dem Enum function getActiveLights(): ParsedLight[] { let rawIds: string[] = [...LIGHTS]; if (ENUM_FUNKTION && existsObject(ENUM_FUNKTION)) { const enumObj = getObject(ENUM_FUNKTION); if (enumObj && enumObj.common && enumObj.common.members) { rawIds = rawIds.concat(enumObj.common.members); } } // Duplikate entfernen rawIds = [...new Set(rawIds)]; const result: ParsedLight[] = []; for (const id of rawIds) { if (!existsObject(id)) continue; let powerId = id; const obj = getObject(id); // Falls der Nutzer einen ganzen Kanal ins Enum gezogen hat, suchen wir den STATE if (obj && obj.type === 'channel') { if (existsState(`${id}.STATE`)) powerId = `${id}.STATE`; else if (existsState(`${id}.ON`)) powerId = `${id}.ON`; } // Helligkeits-Datenpunkt dynamisch im selben Kanal suchen const parts = powerId.split('.'); parts.pop(); const channel = parts.join('.'); let briId: string | null = null; const briCandidates = ['LEVEL', 'ACTUAL', 'BRIGHTNESS', 'DIMMER', 'SET']; for (const candidate of briCandidates) { if (existsState(`${channel}.${candidate}`)) { briId = `${channel}.${candidate}`; break; } } result.push({ baseId: powerId, powerId, briId }); } return result; } // ========================================== // 1. LERN-MODUS (Datenbank füttern) // ========================================== function runLearning(db: SnapshotDatabase, currentSlot: number): void { let snapshot: LightSnapshot = {}; const activeLights = getActiveLights(); // Aktuellen Zustand inkl. Helligkeit aller Lampen einfrieren for (const light of activeLights) { const isOn = getSafeBoolean(light.powerId, false); const stateData: LightStateData = { on: isOn }; if (isOn && light.briId && existsState(light.briId)) { const briVal = getState(light.briId).val; if (typeof briVal === 'number') stateData.bri = briVal; } snapshot[light.baseId] = stateData; } if (!db[currentSlot]) db[currentSlot] = []; db[currentSlot].push(snapshot); if (db[currentSlot].length > MAX_SNAPSHOTS) { db[currentSlot].shift(); } setState(DP_STORAGE, JSON.stringify(db), true); log(`[Simulation LERNEN] Block ${currentSlot} gespeichert. (${db[currentSlot].length}/${MAX_SNAPSHOTS} Snapshots)`, 'debug'); } // ========================================== // 2. GHOST-MODUS (Berechnen & Abspielen) // ========================================== function runSimulation(db: SnapshotDatabase, currentSlot: number): void { const activeLights = getActiveLights(); if (!db[currentSlot] || db[currentSlot].length === 0) { log(`[Simulation GHOST] Keine Daten für Block ${currentSlot}. Schalte Lichter aus.`, 'debug'); for (const light of activeLights) { if (getSafeBoolean(light.powerId, false) === true) setState(light.powerId, false, false); } return; } const slotSnapshots: LightSnapshot[] = db[currentSlot]; const uniqueCount = new Set(slotSnapshots.map(s => JSON.stringify(s))).size; const chaosFactor = uniqueCount / slotSnapshots.length; const dynamicMaxDelay = Math.floor(chaosFactor * MAX_PLAYBACK_DELAY); const randomDelayMs = Math.floor(Math.random() * dynamicMaxDelay); log(`[Simulation GHOST] Block ${currentSlot} | Chaos-Faktor: ${Math.round(chaosFactor*100)}% | Geplanter Delay: ${Math.round(randomDelayMs/1000)}s`, 'info'); // --- ABSPIELEN MIT BERECHNETER VERZÖGERUNG --- setTimeout(() => { const randomIndex = Math.floor(Math.random() * slotSnapshots.length); const chosenSnapshot = slotSnapshots[randomIndex]; for (const light of activeLights) { const targetRaw = chosenSnapshot[light.baseId]; if (targetRaw === undefined) continue; // Migration: Kompatibilität zu alten Boolean-Werten aus V1 let targetOn = false; let targetBri: number | undefined = undefined; if (typeof targetRaw === 'boolean') { targetOn = targetRaw; } else { targetOn = targetRaw.on; targetBri = targetRaw.bri; } const currentOn = getSafeBoolean(light.powerId, false); if (targetOn) { // Wenn Lampe an sein soll: Zuerst (optional) dimmen, dann einschalten if (targetBri !== undefined && light.briId) { const currentBri = getState(light.briId).val; if (currentBri !== targetBri) setState(light.briId, targetBri, false); } if (!currentOn) setState(light.powerId, true, false); } else { // Wenn Lampe aus sein soll if (currentOn) setState(light.powerId, false, false); } } }, randomDelayMs); } // === INIT & CRONJOB === createState(DP_STORAGE, JSON.stringify({}), { type: 'string', name: 'Snapshot Datenbank (JSON)', role: 'json' }, () => { schedule("*/15 * * * *", () => { const isPresent = getSafeBoolean(DP_PRESENCE, true); const currentSlot = getSlotIndex(); let db: SnapshotDatabase = {}; try { const raw = getState(DP_STORAGE).val as string; if (raw) db = JSON.parse(raw) as SnapshotDatabase; } catch (e) { log('[Simulation] Fehler beim Lesen der Datenbank, starte mit leerer DB neu.', 'warn'); } if (isPresent) { const samplingDelayMs = Math.floor(Math.random() * MAX_SAMPLING_DELAY); log(`[Simulation LERNEN] Block ${currentSlot} ausgelöst. Mache das Foto in ${Math.round(samplingDelayMs/1000)}s...`, 'debug'); setTimeout(() => { runLearning(db, currentSlot); }, samplingDelayMs); } else { runSimulation(db, currentSlot); } }); log('[Simulation] KI-Snapshot-Engine gestartet. (V2: Enums & Helligkeit integriert)'); }); - Enum-/Aufzählungs-Support: Du kannst Lampen jetzt optional über eine ioBroker-Funktion (z.B.
-
Hallo,
eine tolle Idee, die gleich mal testen werde.Eine kurze Frage, sind
"0_userdata.0.Anwesenheitssimulation_Snapshots" und "0_userdata.0.Simulation.Snapshot_DB" nicht die gleichen Datenpunkte?
-
Hallo zusammen,
ich möchte euch heute ein Projekt vorstellen, das das Thema Anwesenheitssimulation einmal anders angeht. Anstatt starrer Pläne oder durchschaubarer Zufallsschaltungen nutzt dieses Skript einen Snapshot-Ansatz, der sich vollautomatisch eurem echten Leben anpasst.
Das Konzept: Ein System, das mitdenkt
Das Skript erstellt alle 15 Minuten einen "Snapshot" eurer Lichter (oder anderer Geräte) – aber nur, wenn ihr wirklich zu Hause seid. Im Abwesenheitsmodus (Urlaub) spielt es diese Aufzeichnungen zeitversetzt wieder ab.
Die Highlights:
- Absolut wartungsfrei: Einmal eingerichtet, müsst ihr nie wieder eingreifen. Keine Sommer-/Winterzeit-Umstellung, kein Nachjustieren bei neuen Gewohnheiten.
- Ganzjährig nutzbar: Da das Skript kontinuierlich lernt und alte Snapshots nach 5 Tagen automatisch überschreibt (Ringspeicher), "weiß" es im Dezember von selbst, dass das Licht früher brennen muss als im Juli.
- Natürliches Verhalten: Es spiegelt euer echtes Leben. Werden die Tage kürzer oder ändert sich euer Rhythmus, lernt die Simulation das einfach mit.
- Intelligente Variation: Aus den Snapshots der letzten Tage wird per Zufall gewählt und mit einem "Jitter" (zufällige Verzögerung) geschaltet. Das wirkt für Außenstehende absolut menschlich.
Neu in V2:
- Enum-/Aufzählungs-Support: Du kannst Lampen jetzt optional über eine ioBroker-Funktion (z.B. enum.functions.anwesenheitssimulation) verwalten. Das Skript kombiniert die Einträge aus der Liste LIGHTS und dem Enum automatisch. Du kannst sogar ganze Kanäle in das Enum ziehen – das Skript sucht sich den richtigen Datenpunkt (.STATE oder .ON) selbst.
- Helligkeits-Tracking: Das Skript sucht automatisch nach passenden Helligkeits-Datenpunkten im selben Kanal (z.B. .LEVEL, .ACTUAL, .BRIGHTNESS) und speichert den Dimmwert ab.
- Auto-Migration: Falls du das Skript startest und noch alte "Foto"-Daten in der Datenbank hast (die nur An/Aus kannten), stürzt das Skript nicht ab, sondern verarbeitet diese nahtlos weiter, bis sie nach 5 Tagen aus dem Gedächtnis rotieren.
Einrichtung & Installation
- Datenpunkte
0_userdata.0.Anwesenheit(Urlaubs-Trigger) und0_userdata.0.Anwesenheitssimulation_Snapshots_DB(JSON-Speicher) anlegen. - Eure Geräte-IDs im
devices-Array eintragen. Fertig.
Das Skript (TypeScript):
// TITEL: Smart Presence Simulation (KI-Ghost-Mode) - v2 // ===================================================================== // === 1. KONFIGURATION === const DP_PRESENCE: string = '0_userdata.0.Anwesenheit'; // NEU: Optional Lampen über ioBroker "Aufzählungen" (Enums) laden. // Leer lassen (''), falls nicht gewünscht. const ENUM_FUNKTION: string = 'enum.functions.anwesenheitssimulation'; // Hier können Lampen weiterhin manuell eingetragen werden (Kombination mit Enum ist möglich) const LIGHTS: string[] = [ 'alias.0.Licht.Flur.STATE', // <-- Trage hier alle Lampen ein, die 'alias.0.Licht.Wohnzimmer.STATE', // von der Straße aus sichtbar sind. 'alias.0.Licht.Kueche.STATE', 'alias.0.Licht.Bad.STATE' ]; // === 2. EXPERTEN-EINSTELLUNGEN === const MAX_SNAPSHOTS: number = 5; const MAX_SAMPLING_DELAY: number = 300000; const MAX_PLAYBACK_DELAY: number = 840000; const DP_STORAGE: string = '0_userdata.0.Simulation.Snapshot_DB'; // ============================================================ // === TYPEN & INTERFACES === // ============================================================ interface LightStateData { on: boolean; bri?: number; // Optionaler Helligkeitswert } interface LightSnapshot { // Unterstützt sowohl das neue Format (LightStateData) als auch das alte (boolean) für reibungslose Migration [baseId: string]: LightStateData | boolean; } interface SnapshotDatabase { [slotIndex: number]: LightSnapshot[]; } interface ParsedLight { baseId: string; powerId: string; briId: string | null; } // ============================================================ // === HILFSFUNKTIONEN === // ============================================================ function getSlotIndex(): number { const now = new Date(); return Math.floor((now.getHours() * 60 + now.getMinutes()) / 15); } function getSafeBoolean(oid: string, fallback: boolean = false): boolean { if (oid && existsState(oid)) { const val = getState(oid).val; return (val === true || val === 'true' || val === 1); } return fallback; } // NEU: Sammelt dynamisch alle Lampen aus dem Array und dem Enum function getActiveLights(): ParsedLight[] { let rawIds: string[] = [...LIGHTS]; if (ENUM_FUNKTION && existsObject(ENUM_FUNKTION)) { const enumObj = getObject(ENUM_FUNKTION); if (enumObj && enumObj.common && enumObj.common.members) { rawIds = rawIds.concat(enumObj.common.members); } } // Duplikate entfernen rawIds = [...new Set(rawIds)]; const result: ParsedLight[] = []; for (const id of rawIds) { if (!existsObject(id)) continue; let powerId = id; const obj = getObject(id); // Falls der Nutzer einen ganzen Kanal ins Enum gezogen hat, suchen wir den STATE if (obj && obj.type === 'channel') { if (existsState(`${id}.STATE`)) powerId = `${id}.STATE`; else if (existsState(`${id}.ON`)) powerId = `${id}.ON`; } // Helligkeits-Datenpunkt dynamisch im selben Kanal suchen const parts = powerId.split('.'); parts.pop(); const channel = parts.join('.'); let briId: string | null = null; const briCandidates = ['LEVEL', 'ACTUAL', 'BRIGHTNESS', 'DIMMER', 'SET']; for (const candidate of briCandidates) { if (existsState(`${channel}.${candidate}`)) { briId = `${channel}.${candidate}`; break; } } result.push({ baseId: powerId, powerId, briId }); } return result; } // ========================================== // 1. LERN-MODUS (Datenbank füttern) // ========================================== function runLearning(db: SnapshotDatabase, currentSlot: number): void { let snapshot: LightSnapshot = {}; const activeLights = getActiveLights(); // Aktuellen Zustand inkl. Helligkeit aller Lampen einfrieren for (const light of activeLights) { const isOn = getSafeBoolean(light.powerId, false); const stateData: LightStateData = { on: isOn }; if (isOn && light.briId && existsState(light.briId)) { const briVal = getState(light.briId).val; if (typeof briVal === 'number') stateData.bri = briVal; } snapshot[light.baseId] = stateData; } if (!db[currentSlot]) db[currentSlot] = []; db[currentSlot].push(snapshot); if (db[currentSlot].length > MAX_SNAPSHOTS) { db[currentSlot].shift(); } setState(DP_STORAGE, JSON.stringify(db), true); log(`[Simulation LERNEN] Block ${currentSlot} gespeichert. (${db[currentSlot].length}/${MAX_SNAPSHOTS} Snapshots)`, 'debug'); } // ========================================== // 2. GHOST-MODUS (Berechnen & Abspielen) // ========================================== function runSimulation(db: SnapshotDatabase, currentSlot: number): void { const activeLights = getActiveLights(); if (!db[currentSlot] || db[currentSlot].length === 0) { log(`[Simulation GHOST] Keine Daten für Block ${currentSlot}. Schalte Lichter aus.`, 'debug'); for (const light of activeLights) { if (getSafeBoolean(light.powerId, false) === true) setState(light.powerId, false, false); } return; } const slotSnapshots: LightSnapshot[] = db[currentSlot]; const uniqueCount = new Set(slotSnapshots.map(s => JSON.stringify(s))).size; const chaosFactor = uniqueCount / slotSnapshots.length; const dynamicMaxDelay = Math.floor(chaosFactor * MAX_PLAYBACK_DELAY); const randomDelayMs = Math.floor(Math.random() * dynamicMaxDelay); log(`[Simulation GHOST] Block ${currentSlot} | Chaos-Faktor: ${Math.round(chaosFactor*100)}% | Geplanter Delay: ${Math.round(randomDelayMs/1000)}s`, 'info'); // --- ABSPIELEN MIT BERECHNETER VERZÖGERUNG --- setTimeout(() => { const randomIndex = Math.floor(Math.random() * slotSnapshots.length); const chosenSnapshot = slotSnapshots[randomIndex]; for (const light of activeLights) { const targetRaw = chosenSnapshot[light.baseId]; if (targetRaw === undefined) continue; // Migration: Kompatibilität zu alten Boolean-Werten aus V1 let targetOn = false; let targetBri: number | undefined = undefined; if (typeof targetRaw === 'boolean') { targetOn = targetRaw; } else { targetOn = targetRaw.on; targetBri = targetRaw.bri; } const currentOn = getSafeBoolean(light.powerId, false); if (targetOn) { // Wenn Lampe an sein soll: Zuerst (optional) dimmen, dann einschalten if (targetBri !== undefined && light.briId) { const currentBri = getState(light.briId).val; if (currentBri !== targetBri) setState(light.briId, targetBri, false); } if (!currentOn) setState(light.powerId, true, false); } else { // Wenn Lampe aus sein soll if (currentOn) setState(light.powerId, false, false); } } }, randomDelayMs); } // === INIT & CRONJOB === createState(DP_STORAGE, JSON.stringify({}), { type: 'string', name: 'Snapshot Datenbank (JSON)', role: 'json' }, () => { schedule("*/15 * * * *", () => { const isPresent = getSafeBoolean(DP_PRESENCE, true); const currentSlot = getSlotIndex(); let db: SnapshotDatabase = {}; try { const raw = getState(DP_STORAGE).val as string; if (raw) db = JSON.parse(raw) as SnapshotDatabase; } catch (e) { log('[Simulation] Fehler beim Lesen der Datenbank, starte mit leerer DB neu.', 'warn'); } if (isPresent) { const samplingDelayMs = Math.floor(Math.random() * MAX_SAMPLING_DELAY); log(`[Simulation LERNEN] Block ${currentSlot} ausgelöst. Mache das Foto in ${Math.round(samplingDelayMs/1000)}s...`, 'debug'); setTimeout(() => { runLearning(db, currentSlot); }, samplingDelayMs); } else { runSimulation(db, currentSlot); } }); log('[Simulation] KI-Snapshot-Engine gestartet. (V2: Enums & Helligkeit integriert)'); });Fazit
Für mich war wichtig, dass ich mich vor dem Urlaub nicht um die Simulation kümmern muss. Durch den lernenden Charakter ist das System das ganze Jahr über "scharf" und bildet immer den aktuellen Lebensstil ab.
Ich freue mich auf euer Feedback!
Edit 18.03.2026: Ich habe hier in Post 1 auch die V2 eingefügt und die Anleitung entsprechend ergänzt. Der Datenpunkt "0_userdata.0.Anwesenheitssimulation_Snapshots" wurde in "0_userdata.0.Simulation.Snapshot_DB" umbenannt.
@mrMuppet sagte in [Vorlage] Anwesenheitssimulation mit dauerhaftem Lernen:
Ich freue mich auf euer Feedback!
Hallo
Ich war so frei und hab dein Script in die Sammlung aufgenommen. :)
-
Hallo,
eine tolle Idee, die gleich mal testen werde.Eine kurze Frage, sind
"0_userdata.0.Anwesenheitssimulation_Snapshots" und "0_userdata.0.Simulation.Snapshot_DB" nicht die gleichen Datenpunkte?
@Chrille1507 sagte in [Vorlage] Anwesenheitssimulation mit dauerhaftem Lernen:
Eine kurze Frage, sind
"0_userdata.0.Anwesenheitssimulation_Snapshots" und "0_userdata.0.Simulation.Snapshot_DB" nicht die gleichen Datenpunkte?
Stimmt. Das sollte der gleiche Datenpunkt sein.
-
Ich habe in Post 1 auch die V2 eingefügt und die Anleitung entsprechend ergänzt. Der Datenpunkt "0_userdata.0.Anwesenheitssimulation_Snapshots" wurde in "0_userdata.0.Simulation.Snapshot_DB" umbenannt.
Wer das Skript schon nutzt und updaten möchte: Kopiert euch einfach den neuen Code aus dem ersten Beitrag und löscht den alten Datenpunkt einmal händisch aus euren Objekten. Danach läuft alles wie gewohnt!