FEAT-NETATMO-06: V1 Status-Update Redesign + shared helpers foundation
Netatmo-Plugin komplett auf V1 Status-Update Layout umgebaut:
- 4-Sektionen: Aussen / Wind+Regen / Indoor / Vorhersage
- Echte 12h-History fuer MIN/MAX (vorher estimated +-2/+-4)
- Dynamische Forecast-Daten (datetime.now() + timedelta)
- DE strings durchgaengig (AUSSEN, FEUCHTE, DRUCK, Bo, HEUTE, Boerse, ...)
- 7-Farben ACeP palette (kein Greyscale, kein Anti-Alias)
- Indoor-Sensoren: rote Tag-Box + 22pt+ Temp + CO2-Bar + Batt aligned
- Chart mit 2-Tick Y-Grid (min/max), 12h Linie in Gruen
- Wetter-Icons (WMO codes) in Vorhersage, Regen-Drop-Icon pro Tag
Foundation / shared helpers:
- palette.py: BATT_*, DE_WEEKDAYS/MONTHS, WMO_ICONS, format_*,
POWER_DEFAULTS, temp_color, co2_color, batt_color
- plugins/base.py: draw_battery, draw_compass, draw_trend,
draw_line_chart, draw_mini_bar, draw_weather_icon,
draw_deepsleep_indicator, draw_zzz_indicator,
_fit_size, _get_base_sans, de_* formatters
- designbase.py: THEMES dict (light/dark/retro/mag), Layout templates
(bereits importierbar)
Tools:
- tools/render_mockup.py: 3 design-Varianten Mockup-Generator
- tools/verify_netatmo.py: Self-verification mit dynamic mock data
This commit is contained in:
+186
@@ -235,3 +235,189 @@ def is_tall(w: int, h: int) -> bool:
|
||||
"""True wenn Slot hoch aber schmal ist (1x4, 1x2)."""
|
||||
return h >= 280 and w < 280
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Batterie-Farben — Single Source of Truth
|
||||
# ============================================================================
|
||||
BATT_LOW = RED # < 25% → kritisch
|
||||
BATT_MID = ORANGE # < 50% → bald wechseln
|
||||
BATT_OK = GREEN # >= 50% → OK
|
||||
|
||||
# Standard-Batterie-Schwellen (werden von Plugins einheitlich genutzt)
|
||||
BATT_THRESHOLDS = (
|
||||
(25, BATT_LOW),
|
||||
(50, BATT_MID),
|
||||
)
|
||||
|
||||
|
||||
def batt_color(pct: int | None) -> tuple:
|
||||
"""Liefert die passende Batterie-Farbe fuer einen Prozent-Wert.
|
||||
|
||||
None → FG (keine Anzeige), sonst BATT_LOW/MID/OK.
|
||||
"""
|
||||
if pct is None:
|
||||
return FG
|
||||
pct = max(0, min(100, pct))
|
||||
for threshold, color in BATT_THRESHOLDS:
|
||||
if pct < threshold:
|
||||
return color
|
||||
return BATT_OK
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Deutsche Formatter — Single Source of Truth (kein Englisches "Monday" mehr)
|
||||
# ============================================================================
|
||||
DE_WEEKDAYS_LONG = [
|
||||
"Montag", "Dienstag", "Mittwoch", "Donnerstag",
|
||||
"Freitag", "Samstag", "Sonntag",
|
||||
]
|
||||
DE_WEEKDAYS_SHORT = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"]
|
||||
DE_MONTHS_LONG = [
|
||||
"Januar", "Februar", "Maerz", "April", "Mai", "Juni",
|
||||
"Juli", "August", "September", "Oktober", "November", "Dezember",
|
||||
]
|
||||
DE_MONTHS_SHORT = ["Jan", "Feb", "Mae", "Apr", "Mai", "Jun",
|
||||
"Jul", "Aug", "Sep", "Okt", "Nov", "Dez"]
|
||||
|
||||
|
||||
def de_weekday_long(weekday: int) -> str:
|
||||
"""datetime.weekday() (0=Montag) → 'Montag'/'Dienstag'/..."""
|
||||
return DE_WEEKDAYS_LONG[weekday]
|
||||
|
||||
|
||||
def de_weekday_short(weekday: int) -> str:
|
||||
"""datetime.weekday() (0=Montag) → 'Mo'/'Di'/..."""
|
||||
return DE_WEEKDAYS_SHORT[weekday]
|
||||
|
||||
|
||||
def de_month_short(month: int) -> str:
|
||||
"""datetime.month (1-12) → 'Jan'/'Feb'/..."""
|
||||
return DE_MONTHS_SHORT[month - 1]
|
||||
|
||||
|
||||
def de_date(dt, short: bool = True) -> str:
|
||||
"""Datum als '30. Aug 2026' (default short=True)."""
|
||||
if short:
|
||||
return f"{dt.day:02d}. {de_month_short(dt.month)} {dt.year}"
|
||||
return f"{dt.day:02d}. {DE_MONTHS_LONG[dt.month - 1]} {dt.year}"
|
||||
|
||||
|
||||
def de_weekday_date(dt, short: bool = True) -> str:
|
||||
"""'Sa 30.08' wenn short, sonst 'Samstag 30. August 2026'."""
|
||||
if short:
|
||||
return f"{de_weekday_short(dt.weekday())} {dt.day:02d}.{dt.month:02d}"
|
||||
return f"{de_weekday_long(dt.weekday())} {de_date(dt, short=False)}"
|
||||
|
||||
|
||||
def de_time(dt, show_seconds: bool = False) -> str:
|
||||
"""24h-Zeit 'HH:MM' oder 'HH:MM:SS'."""
|
||||
return dt.strftime("%H:%M:%S" if show_seconds else "%H:%M")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Wind / Temp Helper — einheitliche Formatierung
|
||||
# ============================================================================
|
||||
def format_temp(value: float | None, unit: str = "C", decimals: int = 1) -> str:
|
||||
"""Temperatur formatieren. None → em-dash."""
|
||||
if value is None:
|
||||
return "—"
|
||||
return f"{value:.{decimals}f}°{unit}"
|
||||
|
||||
|
||||
def format_wind(speed: float | None, unit: str = "kmh", decimals: int = 0) -> str:
|
||||
"""Windgeschwindigkeit. 'kmh' → 'km/h', 'ms' → 'm/s'."""
|
||||
if speed is None:
|
||||
return "—"
|
||||
label = "km/h" if unit == "kmh" else "m/s"
|
||||
return f"{speed:.{decimals}f} {label}"
|
||||
|
||||
|
||||
def format_rain(rate_mm: float | None, decimals: int = 1) -> str:
|
||||
"""Regenrate 'X.X mm/h'."""
|
||||
if rate_mm is None:
|
||||
return "—"
|
||||
return f"{rate_mm:.{decimals}f} mm/h"
|
||||
|
||||
|
||||
def temp_color(value: float | None) -> tuple:
|
||||
"""Farbe nach Temperaturwert: kalt→Blau, normal→FG, warm→Orange, heiss→Rot."""
|
||||
if value is None:
|
||||
return FG
|
||||
if value >= 28:
|
||||
return RED
|
||||
if value >= 22:
|
||||
return ORANGE
|
||||
if value <= 5:
|
||||
return BLUE
|
||||
return FG
|
||||
|
||||
|
||||
def co2_color(ppm: int | float | None) -> tuple:
|
||||
"""CO2-Ampel: <600 gruen, <1000 orange, <1500 gelb, >=1500 rot."""
|
||||
if ppm is None:
|
||||
return FG
|
||||
if ppm >= 1500:
|
||||
return RED
|
||||
if ppm >= 1000:
|
||||
return YELLOW
|
||||
if ppm >= 600:
|
||||
return ORANGE
|
||||
return GREEN
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Power-Management — Defaults
|
||||
# ============================================================================
|
||||
POWER_DEFAULTS = {
|
||||
"deepsleep_enabled": False,
|
||||
"deepsleep_start": "23:30",
|
||||
"deepsleep_end": "06:30",
|
||||
"cpu_throttle": True, # waehrend DeepSleep auf 600MHz
|
||||
"wifi_off": False, # WiFi anlassen fuer Admin-Erreichbarkeit
|
||||
}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Weather-Code-Mapping (WMO Standard + Netatmo-Fallback)
|
||||
# ============================================================================
|
||||
# WMO Weather interpretation codes (https://open-meteo.com/en/docs)
|
||||
WMO_ICONS = {
|
||||
0: "sun", # Clear sky
|
||||
1: "partly", # Mainly clear
|
||||
2: "partly", # Partly cloudy
|
||||
3: "cloud", # Overcast
|
||||
45: "fog", # Fog
|
||||
48: "fog", # Depositing rime fog
|
||||
51: "rain", # Drizzle: light
|
||||
53: "rain", # Drizzle: moderate
|
||||
55: "rain", # Drizzle: dense
|
||||
56: "rain", # Freezing Drizzle: light
|
||||
57: "rain", # Freezing Drizzle: dense
|
||||
61: "rain", # Rain: slight
|
||||
63: "rain", # Rain: moderate
|
||||
65: "rain", # Rain: heavy
|
||||
66: "rain", # Freezing Rain: light
|
||||
67: "rain", # Freezing Rain: heavy
|
||||
71: "snow", # Snow fall: slight
|
||||
73: "snow", # Snow fall: moderate
|
||||
75: "snow", # Snow fall: heavy
|
||||
77: "snow", # Snow grains
|
||||
80: "rain", # Rain showers: slight
|
||||
81: "rain", # Rain showers: moderate
|
||||
82: "rain", # Rain showers: violent
|
||||
85: "snow", # Snow showers: slight
|
||||
86: "snow", # Snow showers: heavy
|
||||
95: "storm", # Thunderstorm
|
||||
96: "storm", # Thunderstorm with slight hail
|
||||
99: "storm", # Thunderstorm with heavy hail
|
||||
}
|
||||
|
||||
|
||||
def wmo_to_icon(code: int | str | None) -> str:
|
||||
"""WMO-Code (int) oder Symbol-String → 'sun'/'cloud'/'rain'/'storm'/..."""
|
||||
if code is None:
|
||||
return "cloud"
|
||||
if isinstance(code, str):
|
||||
return code # schon ein symbol
|
||||
return WMO_ICONS.get(code, "cloud")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user