feat: background device status check every 240s instead of on page load

- Add online, leds, version, last_check columns to wled_devices table
- Background job checks all WLED devices every 4 minutes
- Initial check on startup
- Devices page reads status from DB (instant page load)
- 'Alle jetzt pruefen' button triggers manual check

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Christian Mueller 2026-04-23 23:03:12 +02:00
parent a7efb12d5c
commit e88ab3ab08
4 changed files with 88 additions and 63 deletions

24
app.py
View File

@ -38,9 +38,13 @@ async def lifespan(application: FastAPI):
# Start background polling
interval = int(database.get_setting("poll_interval", "15"))
bg_scheduler.add_job(scheduler.poll_status, "interval", seconds=interval, id="poll")
bg_scheduler.add_job(scheduler.check_devices, "interval", seconds=240, id="device_check")
bg_scheduler.start()
logger.info("Polling gestartet (Intervall: %ds)", interval)
# Initial device check
scheduler.check_devices()
yield
bg_scheduler.shutdown()
@ -195,8 +199,13 @@ async def devices_page(request: Request):
devices = conn.execute(
"SELECT * FROM wled_devices ORDER BY name, hostname"
).fetchall()
last_check_row = conn.execute(
"SELECT MAX(last_check) as lc FROM wled_devices WHERE last_check IS NOT NULL"
).fetchone()
conn.close()
return templates.TemplateResponse(*_ctx(request, "devices", devices=devices))
last_check = last_check_row["lc"] if last_check_row and last_check_row["lc"] else None
return templates.TemplateResponse(*_ctx(request, "devices",
devices=devices, last_check=last_check))
@app.post("/api/devices")
@ -241,15 +250,10 @@ async def delete_device(device_id: int):
return RedirectResponse("/devices", status_code=303)
@app.get("/api/devices/{device_id}/status")
async def device_status(device_id: int):
from wled_client import get_wled_info
conn = database.get_db()
dev = conn.execute("SELECT hostname FROM wled_devices WHERE id=?", (device_id,)).fetchone()
conn.close()
if not dev:
return {"online": False}
return get_wled_info(dev["hostname"])
@app.post("/api/devices/check-all")
async def check_all_devices():
scheduler.check_devices()
return RedirectResponse("/devices", status_code=303)
# ── Assignments ────────────────────────────────────────────────────────────

View File

@ -32,7 +32,11 @@ def init_db():
id INTEGER PRIMARY KEY AUTOINCREMENT,
hostname TEXT NOT NULL UNIQUE,
name TEXT DEFAULT '',
segment_count INTEGER NOT NULL DEFAULT 2
segment_count INTEGER NOT NULL DEFAULT 2,
online INTEGER DEFAULT 0,
leds INTEGER DEFAULT 0,
version TEXT DEFAULT '',
last_check TIMESTAMP
);
CREATE TABLE IF NOT EXISTS assignments (
@ -71,6 +75,24 @@ def init_db():
);
""")
# Migrate: add columns if missing (for existing databases)
try:
conn.execute("ALTER TABLE wled_devices ADD COLUMN online INTEGER DEFAULT 0")
except Exception:
pass
try:
conn.execute("ALTER TABLE wled_devices ADD COLUMN leds INTEGER DEFAULT 0")
except Exception:
pass
try:
conn.execute("ALTER TABLE wled_devices ADD COLUMN version TEXT DEFAULT ''")
except Exception:
pass
try:
conn.execute("ALTER TABLE wled_devices ADD COLUMN last_check TIMESTAMP")
except Exception:
pass
defaults = {
"azure_client_id": "",
"azure_client_secret": "",

View File

@ -3,7 +3,7 @@ import os
from datetime import datetime
from database import get_db, get_setting, log_activity
from wled_client import build_wled_url, call_wled
from wled_client import build_wled_url, call_wled, get_wled_info
logger = logging.getLogger(__name__)
@ -197,3 +197,29 @@ def _process_user(user, aida_lines, settings, conn):
display_name, aida_status or "-", teams_status,
"aktualisiert" if wled_changed else "nur Dashboard",
)
def check_devices():
"""Check all WLED devices and store status in DB. Called periodically."""
conn = get_db()
try:
devices = conn.execute("SELECT id, hostname FROM wled_devices").fetchall()
for dev in devices:
info = get_wled_info(dev["hostname"])
conn.execute("""
UPDATE wled_devices
SET online=?, leds=?, version=?, last_check=?
WHERE id=?
""", (
1 if info.get("online") else 0,
info.get("leds", 0) if info.get("online") else 0,
info.get("version", "") if info.get("online") else "",
datetime.now(),
dev["id"],
))
conn.commit()
logger.info("Device-Check: %d Geraete geprueft", len(devices))
except Exception as e:
logger.error("Device-Check Fehler: %s", e)
finally:
conn.close()

View File

@ -4,9 +4,16 @@
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h4 class="mb-0">WLED-Geraete</h4>
<button class="btn btn-sm btn-outline-secondary" onclick="checkAllDevices()">
<i class="bi bi-arrow-clockwise"></i> Alle pruefen
</button>
<div>
<form method="post" action="/api/devices/check-all" class="d-inline">
<button type="submit" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-arrow-clockwise"></i> Alle jetzt pruefen
</button>
</form>
{% if last_check %}
<small class="text-muted ms-2">Letzte Pruefung: {{ last_check }}</small>
{% endif %}
</div>
</div>
<div class="row g-4">
@ -30,20 +37,24 @@
{% for device in devices %}
<tr>
<td>
<span class="status-dot" id="status-{{ device.id }}"
style="background: #999;" title="Nicht geprueft"></span>
{% if device.last_check %}
{% if device.online %}
<span class="status-dot" style="background: #28a745;" title="Online"></span>
{% else %}
<span class="status-dot" style="background: #dc3545;" title="Offline"></span>
{% endif %}
{% else %}
<span class="status-dot" style="background: #999;" title="Noch nicht geprueft"></span>
{% endif %}
</td>
<td>
<code>{{ device.hostname }}</code>
</td>
<td>{{ device.name }}</td>
<td><span id="leds-{{ device.id }}" class="text-muted">-</span></td>
<td><span id="version-{{ device.id }}" class="text-muted">-</span></td>
<td>{{ device.leds if device.leds else '-' }}</td>
<td>{{ device.version if device.version else '-' }}</td>
<td>{{ device.segment_count }}</td>
<td>
<button class="btn btn-sm btn-outline-secondary" onclick="checkDevice({{ device.id }})" title="Status pruefen">
<i class="bi bi-wifi"></i>
</button>
<button class="btn btn-sm btn-outline-primary" data-bs-toggle="modal"
data-bs-target="#edit-{{ device.id }}">
<i class="bi bi-pencil"></i>
@ -106,6 +117,9 @@
</div>
</div>
</div>
<div class="mt-2 text-muted">
<small>{{ devices|length }} Geraete | Status wird alle 4 Minuten automatisch geprueft</small>
</div>
</div>
<div class="col-lg-4">
@ -139,44 +153,3 @@
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
const deviceIds = [{% for d in devices %}{{ d.id }}{% if not loop.last %},{% endif %}{% endfor %}];
function checkDevice(id) {
const dot = document.getElementById('status-' + id);
const leds = document.getElementById('leds-' + id);
const ver = document.getElementById('version-' + id);
dot.style.background = '#ffc107';
dot.title = 'Pruefe...';
fetch('/api/devices/' + id + '/status')
.then(r => r.json())
.then(data => {
if (data.online) {
dot.style.background = '#28a745';
dot.title = 'Online';
leds.textContent = data.leds || '-';
ver.textContent = data.version || '-';
} else {
dot.style.background = '#dc3545';
dot.title = 'Offline';
leds.textContent = '-';
ver.textContent = '-';
}
})
.catch(() => {
dot.style.background = '#dc3545';
dot.title = 'Fehler';
});
}
function checkAllDevices() {
deviceIds.forEach(id => checkDevice(id));
}
// Auto-check on page load
checkAllDevices();
</script>
{% endblock %}