hier mein Skript - chatgpt hat mit draufgeschaut - ich schließe hier.
// ===== Einstellungen =====
const OUT = "0_userdata.0.vis.BatterieListeDebug"; // Datenpunkt für VIS
const THRESH_LOW = 30;
const THRESH_MED = 60;
const MAX_HEIGHT = 660;
const WIDTH = 405; // Gesamtbreite
const STALE_HOURS = 168; // >7 Tage ohne Meldung = "nicht aktiv"
// === Helfer ===
function colorByVal(v) {
if (!isFinite(v)) return "#9e9e9e";
if (v < THRESH_LOW) return "#e53935"; // rot
if (v < THRESH_MED) return "#fdd835"; // gelb
return "#43a047"; // grün
}
function devRoot(id) {
const p = id.split(".");
return [p[0], p[1], p[2]].join(".");
}
function niceNameFromRoot(rootId) {
const obj = getObject(rootId);
return obj?.common?.name || rootId;
}
function voltageToPercent(v) {
if (!isFinite(v)) return null;
const min = 2.0, max = 3.0;
let pct = ((v - min) / (max - min)) * 100;
return Math.max(0, Math.min(100, Math.round(pct)));
}
// Liefert den letzten Änderungszeitpunkt aller States eines Geräts
function lastSeenDevice(root) {
let last = 0;
$('' + root + '.*').each(id => {
const st = getState(id);
if (st && st.lc && st.lc > last) last = st.lc;
});
return last;
}
// === Rendering ===
function render() {
const map = {};
// Zigbee: echte %
$('zigbee.0.*.battery').each(id => {
const root = devRoot(id);
const name = niceNameFromRoot(root);
const raw = getState(id)?.val;
const val = isFinite(raw) ? Math.max(0, Math.min(100, Math.round(Number(raw)))) : null;
if (val !== null) map[root] = { type: "percent", name, val, sys: "Zigbee" };
});
// HmIP: Operating Voltage -> %
$('hm-rpc.0.*.OPERATING_VOLTAGE').each(id => {
const root = devRoot(id);
const name = niceNameFromRoot(root);
const val = voltageToPercent(getState(id)?.val);
if (val !== null) map[root] = { type: "percent", name, val, sys: "HmIP" };
});
// Homematic: LOWBAT
$('hm-rpc.0.*.LOWBAT').each(id => {
const root = devRoot(id);
const name = niceNameFromRoot(root);
const lowbat = !!(getState(id)?.val);
const unreach = !!(getState(root + ".0.UNREACH")?.val);
const last = lastSeenDevice(root);
const ageH = (Date.now() - last) / (1000 * 60 * 60);
if (unreach) {
map[root] = { type: "unreach", name, sys: "HM" };
} else if (ageH > STALE_HOURS) {
map[root] = { type: "stale", name, sys: "HM" };
} else {
map[root] = { type: "lowbat", name, lowbat, sys: "HM" };
}
});
// Homematic: LOW_BAT
$('hm-rpc.0.*.LOW_BAT').each(id => {
const root = devRoot(id);
const name = niceNameFromRoot(root);
const lowbat = !!(getState(id)?.val);
const unreach = !!(getState(root + ".0.UNREACH")?.val);
const last = lastSeenDevice(root);
const ageH = (Date.now() - last) / (1000 * 60 * 60);
if (unreach) {
map[root] = { type: "unreach", name, sys: "HmIP" };
} else if (ageH > STALE_HOURS) {
map[root] = { type: "stale", name, sys: "HmIP" };
} else {
map[root] = { type: "lowbat", name, lowbat, sys: "HmIP" };
}
});
// Liste bauen
let list = Object.values(map);
// Sortierung (angepasst)
list.sort((a, b) => {
function weight(it) {
if (it.type === "unreach") return 0; // HM keine Meldung
if (it.type === "percent" && it.sys === "Zigbee" && it.val === 0) return 1; // Zigbee 0%
if (it.type === "stale") return 2; // HM nicht aktiv
if (it.type === "percent" && it.sys === "Zigbee" && it.val < THRESH_LOW) return 3; // Zigbee rot
if (it.type === "lowbat" && it.lowbat === true) return 4; // HM low
if (it.type === "percent" && it.sys === "Zigbee" && it.val < THRESH_MED) return 5; // Zigbee gelb
if (it.type === "percent" && it.sys === "Zigbee" && it.val >= THRESH_MED) return 6; // Zigbee grün
if (it.type === "lowbat" && it.lowbat === false) return 7; // HM grün
return 99;
}
const wa = weight(a), wb = weight(b);
if (wa !== wb) return wa - wb;
// Falls gleiche Kategorie: nach Wert sortieren
if (a.type === "percent" && b.type === "percent") {
if (a.val == null) return 1;
if (b.val == null) return -1;
return a.val - b.val;
}
return 0;
});
// Zeilen bauen
const rows = list.map(it => {
if (it.type === "percent") {
return `
<tr style="border-bottom:1px solid #444;">
<td style="width:55px; color:#f5f5f5;">${it.sys}</td>
<td style="width:120px; color:#f5f5f5; font-size:12px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;" title="${it.name}">${it.name}</td>
<td style="width:55px; text-align:right; color:#f5f5f5;">${it.val == null ? "-" : it.val + " %"}</td>
<td style="width:175px;">
<div style="width:100%; height:10px; border-radius:5px; background:#333; overflow:hidden;">
<div style="height:100%; width:${it.val ?? 0}%; background:${colorByVal(it.val)};"></div>
</div>
</td>
</tr>`;
}
if (it.type === "lowbat") {
const ok = it.lowbat === false;
const color = ok ? "#43a047" : "#e53935";
const text = ok ? "OK" : "LOW";
return `
<tr style="border-bottom:1px solid #444;">
<td style="width:55px; color:#f5f5f5;">${it.sys}</td>
<td style="width:120px; color:#f5f5f5; font-size:12px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;" title="${it.name}">${it.name}</td>
<td style="width:55px; text-align:right; color:#f5f5f5;">–</td>
<td style="width:175px; color:${color}; font-weight:bold;">${text}</td>
</tr>`;
}
if (it.type === "unreach") {
return `
<tr style="border-bottom:1px solid #444;">
<td style="width:55px; color:#f5f5f5;">${it.sys}</td>
<td style="width:120px; color:#f5f5f5; font-size:12px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;" title="${it.name}">${it.name}</td>
<td style="width:55px; text-align:right; color:#f5f5f5;">–</td>
<td style="width:175px; color:#ff9800; font-weight:bold;">KEINE MELDUNG</td>
</tr>`;
}
if (it.type === "stale") {
return `
<tr style="border-bottom:1px solid #444;">
<td style="width:55px; color:#f5f5f5;">${it.sys}</td>
<td style="width:120px; color:#f5f5f5; font-size:12px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;" title="${it.name}">${it.name}</td>
<td style="width:55px; text-align:right; color:#f5f5f5;">–</td>
<td style="width:175px; color:#ff5722; font-weight:bold;">NICHT AKTIV</td>
</tr>`;
}
}).join("");
// HTML mit sticky Header
const html = `
<div style="font-family: system-ui, Segoe UI, Roboto, Arial; font-size:13px;
width:${WIDTH}px; max-height:${MAX_HEIGHT}px; overflow-y:auto;
padding:0; background:transparent;">
<table style="width:${WIDTH}px; border-collapse:collapse; background:transparent;">
<thead style="background:#212121;">
<tr style="border-bottom:1px solid #666;">
<th style="width:55px; text-align:left; color:#f5f5f5; position:sticky; top:0; background:#212121;">Typ</th>
<th style="width:120px; text-align:left; color:#f5f5f5; position:sticky; top:0; background:#212121;">Gerät</th>
<th style="width:55px; text-align:right; color:#f5f5f5; position:sticky; top:0; background:#212121;">Batterie</th>
<th style="width:175px; text-align:left; color:#f5f5f5; position:sticky; top:0; background:#212121;">Status</th>
</tr>
</thead>
<tbody>${rows}</tbody>
</table>
</div>`;
setState(OUT, html, true);
}
// === Setup ===
createState(OUT, "", { type: "string", role: "html", read: true, write: false }, () => {
render();
$('zigbee.0.*.battery').each(id => on({ id, change: "ne" }, render));
$('hm-rpc.0.*.OPERATING_VOLTAGE').each(id => on({ id, change: "ne" }, render));
$('hm-rpc.0.*.LOWBAT').each(id => on({ id, change: "ne" }, render));
$('hm-rpc.0.*.LOW_BAT').each(id => on({ id, change: "ne" }, render));
$('hm-rpc.0.*.UNREACH').each(id => on({ id, change: "ne" }, render));
// Refresh alle 12 Stunden
schedule("0 */12 * * *", render);
});
[image: 1759269909407-6b5592ae-b6ff-4685-8d3d-b1c59447e64e-grafik.png]
Tabelle ist html und lässt sich scrollen in meiner VIS