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 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.
// =====================================================================
// 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)');
});