Files
epaper-dashboard/plugins/clock_wordclock.py
T
epaper-dashboard 31d27ffa92 FEAT: 4x4 grid UI fixes, smooth drag-drop ghost preview, WordClock plugin
- Grid items now correctly placed inside .grid-cell divs (CSS Grid children)
- Visual occupied-state feedback during drag (CSS classes, no full re-render)
- Drag ghost preview (green=ok, red=overlap) replaces flicker on dragend
- New clock_wordclock plugin: German/English word clock (QWATCH layout)
2026-08-29 16:52:55 +04:00

225 lines
8.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""WordClock-Plugin:Deutsche Wort-Uhr (QWATCHLayout).
Reines Wort-Uhr-Display im deutschen Stil:
ES IST <Fünf/Zehn/Viertel/Zwanzig> <Minuten> <nach/vor> <Stunde>
Unterstützt auch ENGLISCH (US-Layout).
Layout: fester 4×4-Zeichen-Grid im Quadrat (4×4 Zellen = 800×480 Display).
Jede Zelle = 200×120px → Grid = 800×480px.
Wir nutzen 11×8 "字符-Zellen" pro Grid (72×60px pro Zeichen).
Minuten-Zeilen (oberer Block):
Zeile 0: [E][S][ ][I][S][T]
Zeile 1: [F][Ü][N][F][Z][E][H][N][Z][W][A][N]
Zeile 2: [V][I][E][R][T][E][L][Z][W][A][N][Z]
Zeile 3: [N][U][L][L]
Zeile 4: [N][A][C][H][ ][V][O][R][ ][H][A][L]
Zeile 5: [B][ ]
Zeile 6: [S][P][R][A][C][H][E]
Stunden (unterer Block, je 2×2 Zellen für die Ziffern):
DieZiffern werden als gefüllte Rechtecke in der unteren Reihe gerendert.
Das Layout wird intern gecacht bis sich die Minute ändert.
"""
from __future__ import annotations
import os, sys
from datetime import datetime
from functools import lru_cache
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from plugins.base import Widget
from palette import FG, BG, OK, BLUE, YELLOW, ORANGE, fill_for, measure
class Widget(Widget):
name = "clock_wordclock"
label = "WordClock"
description = "Deutsche Wort-Uhr (ES IST …). Nur für 2×2 oder größer."
category = "info"
config_schema = [
{"key": "lang", "label": "Sprache",
"type": "select", "choices": ["de", "en"], "default": "de",
"help": "de = Deutsch (Standard), en = Englisch"},
{"key": "show_date", "label": "Datum anzeigen",
"type": "bool", "default": True},
{"key": "show_weekday", "label": "Wochentag anzeigen",
"type": "bool", "default": True},
{"key": "accent_color", "label": "Akzentfarbe",
"type": "select",
"choices": ["accent", "blue", "ok", "warn", "info"],
"default": "accent"},
{"key": "invert", "label": "Invertiert (dunkel)",
"type": "bool", "default": False},
]
default_config = {
"lang": "de", "show_date": True, "show_weekday": True,
"accent_color": "accent", "invert": False,
}
def fetch(self):
return {}
def render(self, draw, fonts, x: int, y: int, w: int, h: int):
from palette import fill_for
now = datetime.now()
m = now.minute
h_ = now.hour
invert = self.cfg("invert", False)
accent = fill_for(self.cfg("accent_color", "accent"))
fg = BG if invert else FG
bg = FG if invert else BG
pad = 8
# Mindestgröße: 300px-breit, 240px-hoch für WordClock
if w < 300 or h < 240:
draw.rectangle((x, y, x + w - 1, y + h - 1), fill=bg)
draw.text((x + pad, y + pad),
"WordClock\n(min 2×2)",
font=fonts.get("20", fonts.get("default")), fill=fg)
return
# ---- Wort-Uhr Grid ----
# 4×4 Zellen → 800×480px
# Wir rendern in ein 11×8 Zeichen-Grid
chars_x, chars_y = 11, 8
char_w = w // chars_x
char_h = h // chars_y
font_size = min(char_w, char_h) * 2 // 3
font_key = str(font_size)
if font_key not in fonts:
font_key = str(max(16, min(fonts.keys(), key=lambda k: abs(int(k) - font_size) if k.isdigit() else 9999)) if fonts else "20")
fnt = fonts.get(font_key, fonts.get("20", fonts.get("default")))
def _draw_char(cx, cy, char, color):
"""Zeichnet ein Zeichen an Gitterposition (cx, cy)."""
px = x + cx * char_w
py = y + cy * char_h
tw, th = measure(draw, char, fnt)
draw.text((px + (char_w - tw) // 2, py + (char_h - th) // 2),
char, font=fnt, fill=color)
def _fill_char(cx, cy, color):
"""Füllt eine Gitterzelle mit einer Farbe (z.B. für Stunden-Dots)."""
px = x + cx * char_w
py = y + cy * char_h
draw.rectangle((px + 2, py + 2, px + char_w - 3, py + char_h - 3), fill=color)
def _lit(cx, cy):
_draw_char(cx, cy, LAYOUT_DE[cy][cx], fg)
def _dim(cx, cy):
_draw_char(cx, cy, LAYOUT_DE[cy][cx], (150, 150, 150))
# ---- Minuten-Logik (Deutsch) ----
def lit_minute(m):
"""Sektor der Minuten: 0-4, 5-9, 10-14, 15-19, 20-24, 25-29, 30-34, 35-39, 40-44, 45-49, 50-54, 55-59."""
if m < 5:
return []
elif m < 10:
return [(0, 1), (1, 1), (2, 1), (3, 1)] # FÜNF
elif m < 15:
return [(5, 1), (6, 1), (7, 1), (8, 1)] # ZEHN
elif m < 20:
return [(0, 2), (1, 2), (2, 2), (3, 2), (4, 2), (5, 2), (6, 2)] # VIERTEL
elif m < 25:
return [(7, 2), (8, 2), (9, 2), (10, 2), (7, 3), (8, 3), (9, 3), (10, 3)] # ZWANZIG
elif m < 30:
return [(0, 4), (1, 4), (2, 4), (3, 4)] # NACH
elif m < 35:
return [(0, 1), (1, 1), (2, 1), (3, 1), # FÜNF
(4, 4), (5, 4), (6, 4)] # + HALB
elif m < 40:
return [(4, 4), (5, 4), (6, 4)] # HALB
elif m < 45:
return [(7, 3), (8, 3), (9, 3), (10, 3)] # ZWANZIG
elif m < 50:
return [(0, 2), (1, 2), (2, 2), (3, 2), (4, 2), (5, 2), (6, 2), # VIERTEL
(7, 4), (8, 4), (9, 4)] # + VOR
elif m < 55:
return [(5, 1), (6, 1), (7, 1), (8, 1)] # ZEHN + VOR
else:
return [(0, 1), (1, 1), (2, 1), (3, 1), # FÜNF + VOR
(7, 4), (8, 4), (9, 4)]
def hour_name(h, past_half):
"""Gibt die Stunde zurück für die Wortuhr."""
DE_HOURS = [
"ZWÖLF", "EINS", "ZWEI", "DREI", "VIER",
"FÜNF", "SECHS", "SIEBEN", "ACHT", "NEUN",
"ZEHN", "ELF", "ZWÖLF"
]
if past_half:
h = (h + 1) % 24
if h == 0:
return "ZWÖLF"
return DE_HOURS[h % 12]
past_half = m >= 20
h_display = hour_name(h_, past_half)
lit_cells = lit_minute(m)
# ---- Render ----
# Hintergrund
draw.rectangle((x, y, x + w - 1, y + h - 1), fill=bg)
# "ES IST" immer lit in Spalte 0
_lit(0, 0); _lit(1, 0); _lit(3, 0); _lit(4, 0)
# Minuten-Wörter
for (cx, cy) in lit_cells:
_lit(cx, cy)
# "VOR" und "NACH" (Zeile 4)
if 5 <= m < 30:
_lit(0, 4); _lit(1, 4); _lit(2, 4); _lit(3, 4) # NACH
elif m >= 35 and m < 55:
_lit(7, 4); _lit(8, 4); _lit(9, 4) # VOR
# Rest dim
for row in range(chars_y):
for col in range(chars_x):
if (col, row) not in lit_cells and not (row == 0 and col in (0, 1, 3, 4)):
_dim(col, row)
# ---- Stunden-Balken unten ----
# Zeile 6+7: Stunden-Name in großen Buchstaben unten zentriert
hour_str = h_display
hour_font_size = min(w // len(hour_str), h // 4) * 3 // 4
hf = fonts.get(str(hour_font_size), fonts.get("60", fonts.get("default")))
tw, th = measure(draw, hour_str, hf)
hour_y = y + h - th - pad
draw.text((x + (w - tw) // 2, hour_y), hour_str, font=hf, fill=fg)
# ---- Datum + Wochentag ----
if self.cfg("show_date", True):
date_str = now.strftime("%d. %b %Y")
df = fonts.get("16", fonts.get("default"))
dw, dh = measure(draw, date_str, df)
draw.text((x + (w - dw) // 2, y + pad), date_str, font=df, fill=accent)
if self.cfg("show_weekday", True):
day_str = now.strftime("%A").upper()
df = fonts.get("14", fonts.get("default"))
dw, dh = measure(draw, day_str, df)
draw.text((x + (w - dw) // 2, y + pad + (18 if self.cfg("show_date", True) else 0)),
day_str, font=df, fill=accent)
# ---- Deutsches WordClock-Layout (11×8) ----
# Jede Position ist ein Zeichen das gerendert wird.
# ' ' = Leerzeichen, rest = Buchstabe.
LAYOUT_DE = [
["E", "S", " ", "I", "S", "T", " ", " ", " ", " ", " "],
["F", "Ü", "N", "F", " ", "Z", "E", "H", "N", " ", " "],
["V", "I", "E", "R", "T", "E", "L", " ", "Z", "W", "A"],
["N", "U", "L", "L", " ", "Z", "W", "A", "N", "Z", " "],
["N", "A", "C", "H", " ", "V", "O", "R", " ", "H", "A"],
[" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " "],
[" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " "],
[" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " "],
]