Skip to content
  • Home
  • Aktuell
  • Tags
  • 0 Ungelesen 0
  • Kategorien
  • Unreplied
  • Beliebt
  • GitHub
  • Docu
  • Hilfe
Skins
  • Light
  • Brite
  • Cerulean
  • Cosmo
  • Flatly
  • Journal
  • Litera
  • Lumen
  • Lux
  • Materia
  • Minty
  • Morph
  • Pulse
  • Sandstone
  • Simplex
  • Sketchy
  • Spacelab
  • United
  • Yeti
  • Zephyr
  • Dark
  • Cyborg
  • Darkly
  • Quartz
  • Slate
  • Solar
  • Superhero
  • Vapor

  • Standard: (Kein Skin)
  • Kein Skin
Einklappen
ioBroker Logo

Community Forum

donate donate
  1. ioBroker Community Home
  2. Deutsch
  3. Skripten / Logik
  4. [Vorlage] Anwesenheitssimulation mit dauerhaftem Lernen

NEWS

  • Monatsrückblick Januar/Februar 2026 ist online!
    BluefoxB
    Bluefox
    17
    1
    435

  • Jahresrückblick 2025 – unser neuer Blogbeitrag ist online! ✨
    BluefoxB
    Bluefox
    17
    1
    5.0k

  • Neuer Blogbeitrag: Monatsrückblick - Dezember 2025 🎄
    BluefoxB
    Bluefox
    13
    1
    1.4k

[Vorlage] Anwesenheitssimulation mit dauerhaftem Lernen

Geplant Angeheftet Gesperrt Verschoben Skripten / Logik
javascripttemplatesecurity
7 Beiträge 4 Kommentatoren 183 Aufrufe 11 Watching
  • Älteste zuerst
  • Neuste zuerst
  • Meiste Stimmen
Antworten
  • In einem neuen Thema antworten
Anmelden zum Antworten
Dieses Thema wurde gelöscht. Nur Nutzer mit entsprechenden Rechten können es sehen.
  • M Offline
    M Offline
    mrMuppet
    schrieb am zuletzt editiert von mrMuppet
    #1

    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

    1. Datenpunkte 0_userdata.0.Anwesenheit (Urlaubs-Trigger) und 0_userdata.0.Anwesenheitssimulation_Snapshots_DB (JSON-Speicher) anlegen.
    2. 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.

    ioBroker auf NUC (Celeron mit Ubuntu-Server)

    Homematic, HMIP, Hue, Unifi, Plex, Nest, Roborock, Google Assistant

    NegaleinN 1 Antwort Letzte Antwort
    0
    • David G.D Online
      David G.D Online
      David G.
      schrieb am zuletzt editiert von David G.
      #2

      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.

      Zeigt eure Lovelace-Visualisierung klick
      (Auch ideal um sich Anregungen zu holen)

      Meine Tabellen für eure Visualisierung klick

      1 Antwort Letzte Antwort
      0
      • M Offline
        M Offline
        mrMuppet
        schrieb am zuletzt editiert von mrMuppet
        #3

        Hier ist ein kleines Update:

        1. 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.
        2. Helligkeits-Tracking: Das Skript sucht automatisch nach passenden Helligkeits-Datenpunkten im selben Kanal (z.B. .LEVEL, .ACTUAL, .BRIGHTNESS) und speichert den Dimmwert ab.
        3. 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)');
        });
        
        

        ioBroker auf NUC (Celeron mit Ubuntu-Server)

        Homematic, HMIP, Hue, Unifi, Plex, Nest, Roborock, Google Assistant

        1 Antwort Letzte Antwort
        2
        • C Online
          C Online
          Chrille1507
          schrieb zuletzt editiert von
          #4

          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?

          M 1 Antwort Letzte Antwort
          0
          • M mrMuppet

            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

            1. Datenpunkte 0_userdata.0.Anwesenheit (Urlaubs-Trigger) und 0_userdata.0.Anwesenheitssimulation_Snapshots_DB (JSON-Speicher) anlegen.
            2. 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.

            NegaleinN Offline
            NegaleinN Offline
            Negalein
            Global Moderator
            schrieb zuletzt editiert von
            #5

            @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. :)

            ° Node.js & System Update ---> sudo apt update, iob stop, sudo apt full-upgrade
            ° Node.js Fixer ---> iob nodejs-update
            ° Fixer ---> iob fix

            1 Antwort Letzte Antwort
            1
            • C Chrille1507

              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?

              M Offline
              M Offline
              mrMuppet
              schrieb zuletzt editiert von mrMuppet
              #6

              @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.

              ioBroker auf NUC (Celeron mit Ubuntu-Server)

              Homematic, HMIP, Hue, Unifi, Plex, Nest, Roborock, Google Assistant

              1 Antwort Letzte Antwort
              0
              • M Offline
                M Offline
                mrMuppet
                schrieb zuletzt editiert von
                #7

                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!

                ioBroker auf NUC (Celeron mit Ubuntu-Server)

                Homematic, HMIP, Hue, Unifi, Plex, Nest, Roborock, Google Assistant

                1 Antwort Letzte Antwort
                0
                Antworten
                • In einem neuen Thema antworten
                Anmelden zum Antworten
                • Älteste zuerst
                • Neuste zuerst
                • Meiste Stimmen


                Support us

                ioBroker
                Community Adapters
                Donate

                634

                Online

                32.7k

                Benutzer

                82.5k

                Themen

                1.3m

                Beiträge
                Community
                Impressum | Datenschutz-Bestimmungen | Nutzungsbedingungen | Einwilligungseinstellungen
                ioBroker Community 2014-2025
                logo
                • Anmelden

                • Du hast noch kein Konto? Registrieren

                • Anmelden oder registrieren, um zu suchen
                • Erster Beitrag
                  Letzter Beitrag
                0
                • Home
                • Aktuell
                • Tags
                • Ungelesen 0
                • Kategorien
                • Unreplied
                • Beliebt
                • GitHub
                • Docu
                • Hilfe