Flow zum Erkennen nicht erreichbarer Geräte

Hallo zusammen

Ich habe für das ein Script geschrieben (in Zusammenarbeit mit der Matrix :wink:)

=> das Script wird via Advanced Flow jeden Abend ausgeführt und gibt mir den entsprechenden Output wo ich was prüfen oder Batterie wechseln sollte

  • Neben der Batterie wir geprüft, wann das letzte Mal ein Wert rapportiert wurde
  • Hier bin ich fan der Shelly Blu Door Window sensoren, da die Signalstärke immer wieder gemeldet wird und ich somit weis, das Gerät ist “da”
    → geht z.B. bei einfacheren Sensoren nicht, da muss ich dann halt prüfen gehen (aber immer noch besser als wenn das Gerät keine Batterie hat und eh nichts meldet) :smiley:

Nachtrag: neue Devices sind dann auch mit dabei :wink:

Script (vereinfacht ohne meine Custom Rules)

/*
 * --- Homey Device Monitor & Battery Checker v1.0 ---
 * Editor: Rick_D
 * * BESCHREIBUNG:
 * Überwacht Homey-Geräte auf Erreichbarkeit und Batteriestand. 
 * Das Script erstellt eine übersichtliche Tabelle im Log und setzt Homey-Tags für Flows.
 * * * VERFÜGBARE REGEL-OPTIONEN (in DEVICE_RULES):
 * - notReportingDays:  Erlaubte Tage ohne Meldung (überschreibt Global).
 * - batteryThreshold:  Batteriewarnung in % (überschreibt Global).
 * - excludeBattery:    (true) Batterieprüfung für dieses Gerät deaktivieren.
 * - onlyCheckBattery:  (true) Ignoriert Erreichbarkeit, prüft nur Batterie.
 * - excludeAll:        (true) Gerät komplett ignorieren.
 * * * BEDEUTUNG DER REGEL-KÜRZEL (Spalte 'Rgl'):
 * - ID: Treffer über die Geräte-ID (Eindeutigste Zuordnung).
 * - NM: Treffer über den exakten Gerätenamen.
 * - PT: Treffer über ein Regex-Muster (Pattern).
 * - --: Keine Regel gefunden, globale Standardwerte werden angewendet.
 */

// --- KONFIGURATION ---

// Globale Standards (wenn keine spezifische Regel greift)
const NOT_REPORTING_THRESHOLD_HOURS = 24; 
const BATTERY_THRESHOLD_PERCENT = 30;     

const DEVICE_RULES = {
    // BEISPIEL: Match via ID - Erlaubt längere Inaktivität (z.B. 7 Tage) & ignoriert Batterie
    "id:00000000-0000-0000-0000-000000000000": { 
        notReportingDays: 7, 
        excludeBattery: true 
    },
    
    // BEISPIEL: Match via Name - Niedrige Batterieschwelle (Warnung erst bei 10%)
    "name:Mein Sensor im Garten": { 
        batteryThreshold: 10 
    },
    
    // BEISPIEL: Match via ID - Nur Batterie prüfen (z.B. für Saison-Geräte)
    "id:11111111-1111-1111-1111-111111111111": { 
        onlyCheckBattery: true 
    },
    
    // BEISPIEL: Match via Pattern (Regex) - Schließt alle Geräte mit diesen Begriffen im Namen aus
    "pattern:/Licht|Plug|Relay|Virtual/i": {
        excludeAll: true
    }
};

// Filter für den Scan
const EXCLUDED_ZONES = ['n-a']; // Zonen in Kleinbuchstaben
const EXCLUDED_DRIVER_URI_PATTERN = /vdevice|nl\.qluster-it\.DeviceCapabilities|nl\.fellownet\.chronograph|net\.i-dev\.betterlogic|com\.swttt\.devicegroups|com\.gruijter\.callmebot|com\.netscan/i;
const INCLUDED_DEVICE_CLASS_REGEX = /sensor|button|washer_and_dryer|thermostat|remote|socket|lights|lock|other|bulb|vacuumcleaner|camera|windowcoverings/i;

// --- SKRIPTLOGIK ---

let allDevices = [];
let DevicesNotReporting = [];
let DevicesLowBattery = [];
let ExcludedDevicesAll = [];

const devices = await Homey.devices.getDevices();
const zones = await Homey.zones.getZones();
const zoneMap = Object.fromEntries(Object.values(zones).map(zone => [zone.id, zone.name]));

// Hilfsfunktionen für Formatierung
function formatDate(date) {
    if (!date || isNaN(date.getTime())) return "Unbekannt";
    return date.toLocaleString('de-DE');
}

function padRight(str, width) {
    str = String(str);
    return str.length >= width ? str.slice(0, width) : str + ' '.repeat(width - str.length);
}

function printRows(devArray) {
    if (devArray.length === 0) { console.log("Keine Einträge."); return; }
    let header = [
        padRight('#', 3), 
        padRight('Name', 30), 
        padRight('Update', 18), 
        padRight('Batt', 5), 
        padRight('Stat', 4), 
        padRight('Rgl', 3),
        'Geräte-ID'
    ].join(' ');
    
    console.log(header);
    console.log('-'.repeat(header.length + 36));
    
    devArray.forEach((d, i) => {
        console.log([
            padRight(i+1, 3), 
            padRight(d.name, 30), 
            padRight(d.lastUpdated, 18), 
            padRight(d.batt, 5), 
            padRight(d.status, 4), 
            padRight(d.ruleApplied, 3),
            d.id
        ].join(' '));
    });
}

// Haupt-Scan
for (const device of Object.values(devices)) {
    let customRule = {};
    let ruleApplied = '--';
    let isExcludedAll = false;

    // Matching (ID -> Name -> Pattern)
    if (DEVICE_RULES[`id:${device.id}`]) {
        customRule = DEVICE_RULES[`id:${device.id}`];
        ruleApplied = 'ID';
    } else if (DEVICE_RULES[`name:${device.name}`]) {
        customRule = DEVICE_RULES[`name:${device.name}`];
        ruleApplied = 'NM';
    } else {
        for (const key in DEVICE_RULES) {
            if (key.startsWith('pattern:') && new RegExp(key.substring(8)).test(device.name)) {
                customRule = DEVICE_RULES[key];
                ruleApplied = 'PT';
                if (customRule.excludeAll) isExcludedAll = true;
                break;
            }
        }
    }

    if (isExcludedAll) {
        ExcludedDevicesAll.push({ name: device.name, id: device.id });
        continue;
    }

    // Filterkriterien prüfen
    if (device.driverUri && EXCLUDED_DRIVER_URI_PATTERN.test(device.driverUri)) continue;
    const zoneName = device.zone ? zoneMap[device.zone] : null;
    if (zoneName && EXCLUDED_ZONES.includes(zoneName.toLowerCase())) continue;
    if (!device.class || !INCLUDED_DEVICE_CLASS_REGEX.test(device.class)) continue;

    // Erreichbarkeit prüfen
    let maxLastUpdatedTime = null;
    let isReporting = true;
    if (!customRule.onlyCheckBattery) {
        for (const cap of Object.values(device.capabilitiesObj || {})) {
            const time = new Date(cap.lastUpdated).getTime();
            if (time > (maxLastUpdatedTime || 0)) maxLastUpdatedTime = time;
        }
        const thresholdHrs = customRule.notReportingDays ? customRule.notReportingDays * 24 : NOT_REPORTING_THRESHOLD_HOURS;
        isReporting = (Date.now() - (maxLastUpdatedTime || 0)) < (thresholdHrs * 3600000);
    }

    // Batterie prüfen
    let batteryStatus = 'N/A';
    if (customRule.excludeBattery) {
        batteryStatus = 'EXCL';
    } else {
        const measuredPct = device.capabilitiesObj?.measure_battery?.value;
        const alarmBattery = device.capabilitiesObj?.alarm_battery?.value;
        const threshold = customRule.batteryThreshold !== undefined ? customRule.batteryThreshold : BATTERY_THRESHOLD_PERCENT;

        if (typeof measuredPct === 'number' && !Number.isNaN(measuredPct)) {
            batteryStatus = `${Math.round(measuredPct)}%`;
            if (measuredPct <= threshold) {
                DevicesLowBattery.push({ name: device.name, status: batteryStatus, id: device.id });
            }
        } else if (alarmBattery) {
            batteryStatus = 'ALARM';
            DevicesLowBattery.push({ name: device.name, status: 'ALARM', id: device.id });
        } else {
            batteryStatus = device.capabilities.includes('measure_battery') ? 'OK' : 'N/A';
        }
    }

    allDevices.push({
        name: device.name,
        id: device.id,
        lastUpdated: maxLastUpdatedTime ? formatDate(new Date(maxLastUpdatedTime)) : 'Keine Daten',
        batt: batteryStatus,
        status: isReporting ? 'OK' : 'NOK',
        ruleApplied: ruleApplied
    });

    if (!isReporting) {
        DevicesNotReporting.push(`${device.name} (ID: ${device.id})`);
    }
}

// --- AUSGABE ---

console.log(`\n--- MONITORING ZUSAMMENFASSUNG ---`);
console.log(`Geräte im Scan: ${allDevices.length} | Offline: ${DevicesNotReporting.length} | Batterie leer: ${DevicesLowBattery.length}`);
console.log(`----------------------------------\n`);

console.log(`[!] KRITISCH: Offline oder meldet nicht:`);
printRows(allDevices.filter(d => d.status === 'NOK'));

console.log(`\n[!] BATTERIE-WARNUNGEN:`);
if (DevicesLowBattery.length > 0) {
    DevicesLowBattery.forEach((d, i) => {
        console.log(`${padRight(i+1, 3)} ${padRight(d.name, 30)} Status: ${padRight(d.status, 6)} ID: ${d.id}`);
    });
} else {
    console.log("Keine.");
}

console.log(`\n[i] ONLINE & OK:`);
printRows(allDevices.filter(d => d.status === 'OK'));

console.log(`\n[x] IGNORIERTE GERÄTE (excludeAll via Pattern):`);
if (ExcludedDevicesAll.length > 0) {
    ExcludedDevicesAll.forEach((d, i) => {
        console.log(`${padRight(i+1, 3)} ${padRight(d.name, 30)} ID: ${d.id}`);
    });
} else {
    console.log("Keine.");
}

// Homey Tags setzen (für Flows)
await tag('notReportingCount', DevicesNotReporting.length);
await tag('lowBatteryCount', DevicesLowBattery.length);
await tag('InvalidatedDevices', DevicesNotReporting.join('\n'));
await tag('LowBatteryDevices', DevicesLowBattery.map(d => `${d.name} (${d.status})`).join('\n'));

return `Scan abgeschlossen: ${allDevices.length} Geräte geprüft.`;

Ausgabe Bsp:

könnt ihr kopieren und mit ein paar Iterationen an Eure Bedürfnisse anpassen :slight_smile:

→ ich habe viele custom rules (die wachsen ständig)

Flow

Der ist dann ganz einfach

→ ich arbeite mit Simple Sys Log und alles mit Notice kriege ich eine Push-Notification (normale Benachrichtigung würde natürlich auch gehen).

2 Likes