FEAT-DEEPSLEEP: Display-Refresh-Timer + CPU-Throttle + Admin Power-Tab
DeepSleep-Modus fuer das ePaper-Display:
- dashboard.py: _is_deepsleep(), _next_wake(), _seconds_until_wake(),
_apply_cpu_throttle() (Pi auf 600MHz im Sleep)
- 60s sleep-chunks in main loop statt festes Intervall
- render_full() mit deepsleep_active + next_wake params
- ZZZ-Indikator + CPU-Throttle-Badge wenn schlafend
- load_config() auto-filled 'power' defaults aus POWER_DEFAULTS
Admin UI (templates/power.html + admin.py):
- /power (GET+POST): DeepSleep-Toggle, Wake-Times, CPU-Throttle-Settings
- /api/power (JSON): live status polling fuer Sidebar-Badge
- /api/wake (POST): manuelles Display-Aufwecken aus dem Admin
- templates/index.html: Energie-Sidebar-Link + Live-Status-Badge
(Zz / wach / an / aus) via refreshPowerStatus()
This commit is contained in:
@@ -939,6 +939,11 @@
|
||||
<span class="icon"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg></span>
|
||||
Settings
|
||||
</button>
|
||||
<a class="sidebar-item" href="/power" style="text-decoration:none">
|
||||
<span class="icon"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18.36 6.64a9 9 0 1 1-12.73 0"/><line x1="12" y1="2" x2="12" y2="12"/></svg></span>
|
||||
Energie
|
||||
<span class="badge" id="sbPower">—</span>
|
||||
</a>
|
||||
<button class="sidebar-item" data-tab="network" onclick="switchTab('network')">
|
||||
<span class="icon"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12.55a11 11 0 0 1 14.08 0"/><path d="M1.42 9a16 16 0 0 1 21.16 0"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><line x1="12" y1="20" x2="12.01" y2="20"/></svg></span>
|
||||
Network
|
||||
@@ -2042,6 +2047,32 @@
|
||||
};
|
||||
document.getElementById('netHint').textContent = hints[s.mode] || '';
|
||||
}
|
||||
|
||||
// ---- Power-Status (DeepSleep) ----
|
||||
async function refreshPowerStatus() {
|
||||
try {
|
||||
const r = await fetch('/api/power');
|
||||
const d = await r.json();
|
||||
const sb = document.getElementById('sbPower');
|
||||
if (sb) {
|
||||
if (d.deepsleep_enabled && d.is_deepsleep) {
|
||||
sb.textContent = 'Zz';
|
||||
sb.style.background = '#fef3c7';
|
||||
sb.style.color = '#92400e';
|
||||
} else if (d.deepsleep_enabled) {
|
||||
sb.textContent = 'an';
|
||||
sb.style.background = '';
|
||||
sb.style.color = '';
|
||||
} else {
|
||||
sb.textContent = 'aus';
|
||||
sb.style.background = '';
|
||||
sb.style.color = '';
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
refreshPowerStatus();
|
||||
setInterval(refreshPowerStatus, 30000);
|
||||
async function refreshScan() {
|
||||
const r = await netFetch('/api/network/scan'); if (!r) return;
|
||||
const sel = document.getElementById('ssidSelect');
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Energie — ePaper Dashboard</title>
|
||||
<style>
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
background: #f4f5f7; color: #1a1a1a; margin: 0; padding: 24px; }
|
||||
h1 { font-size: 22px; margin: 0 0 4px; }
|
||||
h2 { font-size: 15px; font-weight: 600; color: #555; margin: 24px 0 8px;
|
||||
text-transform: uppercase; letter-spacing: 0.5px; }
|
||||
.back { float: right; font-size: 13px; color: #2563eb; text-decoration: none; }
|
||||
.card { background: #fff; border: 1px solid #e1e3e8; border-radius: 8px;
|
||||
padding: 20px 24px; max-width: 720px; }
|
||||
.row { display: flex; align-items: center; gap: 12px; padding: 12px 0;
|
||||
border-bottom: 1px solid #f0f1f3; }
|
||||
.row:last-child { border-bottom: none; }
|
||||
.row label { flex: 1; font-size: 14px; }
|
||||
.row .hint { font-size: 12px; color: #888; display: block; margin-top: 2px; }
|
||||
.toggle { position: relative; width: 44px; height: 24px; }
|
||||
.toggle input { opacity: 0; width: 0; height: 0; }
|
||||
.slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0;
|
||||
background: #d1d5db; border-radius: 12px; transition: 0.2s; }
|
||||
.slider::before { content: ""; position: absolute; height: 18px; width: 18px;
|
||||
left: 3px; bottom: 3px; background: white; border-radius: 50%;
|
||||
transition: 0.2s; }
|
||||
.toggle input:checked + .slider { background: #10b981; }
|
||||
.toggle input:checked + .slider::before { transform: translateX(20px); }
|
||||
input[type="time"] { padding: 6px 10px; border: 1px solid #d1d5db;
|
||||
border-radius: 6px; font-size: 14px; }
|
||||
button { background: #2563eb; color: white; border: none; padding: 10px 20px;
|
||||
border-radius: 6px; font-size: 14px; font-weight: 500; cursor: pointer; }
|
||||
button:hover { background: #1d4ed8; }
|
||||
button.secondary { background: #6b7280; }
|
||||
button.secondary:hover { background: #4b5563; }
|
||||
.status { display: inline-block; padding: 4px 10px; border-radius: 4px;
|
||||
font-size: 12px; font-weight: 600; }
|
||||
.status.wach { background: #d1fae5; color: #065f46; }
|
||||
.status.schlaft { background: #fef3c7; color: #92400e; }
|
||||
.pill { display: inline-block; background: #e0e7ff; color: #3730a3;
|
||||
padding: 2px 8px; border-radius: 10px; font-size: 12px;
|
||||
margin-left: 8px; }
|
||||
.footer { margin-top: 16px; font-size: 12px; color: #6b7280; }
|
||||
.pulse { animation: pulse 2s ease-in-out infinite; }
|
||||
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a class="back" href="/">← zurück zum Dashboard</a>
|
||||
<h1>Energie</h1>
|
||||
<p style="color:#6b7280; font-size:13px; margin:4px 0 0">
|
||||
DeepSleep spart Strom, weil das ePaper das letzte Bild ohne Strom haelt.
|
||||
WiFi bleibt an, damit du das Display jederzeit wecken kannst.
|
||||
</p>
|
||||
|
||||
<div class="card" style="margin-top:20px">
|
||||
<h2>Status <span id="status-pill" class="pill">…</span></h2>
|
||||
<div id="status-text" style="font-size:14px; padding:8px 0">lade…</div>
|
||||
<button class="secondary" id="wake-btn" type="button">Display jetzt wecken</button>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-top:20px">
|
||||
<h2>DeepSleep</h2>
|
||||
<form method="post" action="/power">
|
||||
<div class="row">
|
||||
<label>
|
||||
DeepSleep aktivieren
|
||||
<span class="hint">Display bleibt zwischen den Zeiten eingefroren, kein Refresh.</span>
|
||||
</label>
|
||||
<span class="toggle">
|
||||
<input type="checkbox" name="deepsleep_enabled"
|
||||
{% if power.deepsleep_enabled %}checked{% endif %}>
|
||||
<span class="slider"></span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>
|
||||
Schlafenszeit
|
||||
<span class="hint">Start und Ende im 24h-Format. Mitternacht-Überschreitung
|
||||
(z.B. 23:30 → 06:30) wird automatisch erkannt.</span>
|
||||
</label>
|
||||
<span>
|
||||
<input type="time" name="deepsleep_start"
|
||||
value="{{ power.deepsleep_start }}">
|
||||
bis
|
||||
<input type="time" name="deepsleep_end"
|
||||
value="{{ power.deepsleep_end }}">
|
||||
</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>
|
||||
CPU-Taktung drosseln (Pi)
|
||||
<span class="hint">Während DeepSleep auf 600 MHz takten. Spart ~0.5W.
|
||||
Beim Aufwachen wird automatisch auf Normal-Takt zurückgeschaltet.</span>
|
||||
</label>
|
||||
<span class="toggle">
|
||||
<input type="checkbox" name="cpu_throttle"
|
||||
{% if power.cpu_throttle %}checked{% endif %}>
|
||||
<span class="slider"></span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>
|
||||
WiFi im DeepSleep aus
|
||||
<span class="hint"><strong>Nicht empfohlen.</strong> Solange WiFi aus ist, ist
|
||||
das Admin-UI nicht erreichbar — du kannst das Display dann nicht wecken
|
||||
ausser durch physischen Neustart. Nur aktivieren wenn du weisst was du tust.</span>
|
||||
</label>
|
||||
<span class="toggle">
|
||||
<input type="checkbox" name="wifi_off"
|
||||
{% if power.wifi_off %}checked{% endif %}>
|
||||
<span class="slider"></span>
|
||||
</span>
|
||||
</div>
|
||||
<div style="padding-top:16px">
|
||||
<button type="submit">Speichern</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-top:20px">
|
||||
<h2>Wie es funktioniert</h2>
|
||||
<ul style="font-size:13px; line-height:1.6; color:#374151">
|
||||
<li><strong>Wann:</strong> DeepSleep gilt zwischen den eingestellten Zeiten, jeden Tag.</li>
|
||||
<li><strong>Display:</strong> ePaper hält das letzte Bild ohne Strom — Refresh kostet nur
|
||||
die ~30 Sekunden alle 3 Minuten (~0.5 Wh pro Refresh).</li>
|
||||
<li><strong>Daten:</strong> Plugin-Fetch läuft weiter, damit beim Aufwachen frische Daten da sind.</li>
|
||||
<li><strong>WiFi:</strong> Bleibt an (default), Admin-UI ist erreichbar.</li>
|
||||
<li><strong>Aufwachen:</strong> "Display jetzt wecken" Button, oder warten bis Ende der Schlafenszeit.</li>
|
||||
<li><strong>Z-Indikator:</strong> Auf dem Display erscheint oben rechts ein "Zz" + Aufwach-Zeit.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<p class="footer">
|
||||
Geschätzte Ersparnis: bei 7h DeepSleep/Tag + 3min Refresh-Intervall ca. 30% weniger
|
||||
Display-Refresh-Strom. Plus Pi-CPU-Throttle ~0.5-1W Dauerersparnis.
|
||||
</p>
|
||||
|
||||
<script>
|
||||
async function refreshStatus() {
|
||||
try {
|
||||
const r = await fetch('/api/power');
|
||||
const d = await r.json();
|
||||
const pill = document.getElementById('status-pill');
|
||||
const text = document.getElementById('status-text');
|
||||
if (d.is_deepsleep) {
|
||||
pill.className = 'pill';
|
||||
pill.style.background = '#fef3c7';
|
||||
pill.style.color = '#92400e';
|
||||
pill.textContent = 'schläft';
|
||||
pill.classList.add('pulse');
|
||||
text.innerHTML = `Display schläft bis <strong>${d.next_wake}</strong> `
|
||||
+ `(${formatDuration(d.seconds_until_wake)} verbleibend)`;
|
||||
} else {
|
||||
pill.className = 'pill';
|
||||
pill.style.background = '#d1fae5';
|
||||
pill.style.color = '#065f46';
|
||||
pill.textContent = 'wach';
|
||||
pill.classList.remove('pulse');
|
||||
text.innerHTML = `Display ist wach. Normaler Refresh-Modus aktiv.`
|
||||
+ (d.deepsleep_enabled ? ` Nächster DeepSleep um ${d.deepsleep_start}.` : '');
|
||||
}
|
||||
} catch (e) {
|
||||
document.getElementById('status-text').textContent = 'Status nicht abrufbar: ' + e;
|
||||
}
|
||||
}
|
||||
function formatDuration(secs) {
|
||||
const h = Math.floor(secs / 3600);
|
||||
const m = Math.floor((secs % 3600) / 60);
|
||||
if (h > 0) return `${h}h ${m}m`;
|
||||
return `${m}m`;
|
||||
}
|
||||
document.getElementById('wake-btn').addEventListener('click', async () => {
|
||||
const r = await fetch('/api/wake', { method: 'POST' });
|
||||
const d = await r.json();
|
||||
if (d.ok) {
|
||||
document.getElementById('status-text').textContent = 'Wake-Trigger gesendet — Display wacht auf.';
|
||||
setTimeout(refreshStatus, 1000);
|
||||
} else {
|
||||
alert('Wake fehlgeschlagen: ' + d.error);
|
||||
}
|
||||
});
|
||||
refreshStatus();
|
||||
setInterval(refreshStatus, 30000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user