🎨 Style: Complete CSS Enhancement Package
- Erweiterte Table-Optimierungen für bessere Datenvisualisierung - Tooltip-System mit Backdrop-Filter Integration - Badge & Status-Komponenten für bessere UX - Responsive Design Verbesserungen für Mobile - Performance-Optimierungen (Reduced Motion, High Contrast) - Print-Styles für professionelle Dokumente - Enhanced Utility Classes und Scrollbar-Styling - Loading States und Status Indicators - Hover Effects für interaktive Elemente 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@ -1059,86 +1059,167 @@ body {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.dark .card {
|
||||
border: 1px solid var(--border-primary);
|
||||
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.8), 0 0 0 1px rgba(255, 255, 255, 0.05);
|
||||
/* === TABLE OPTIMIERUNGEN === */
|
||||
.table {
|
||||
@apply w-full border-collapse;
|
||||
border-radius: 12px !important;
|
||||
overflow: hidden;
|
||||
margin: 1rem 0;
|
||||
background: var(--bg-card);
|
||||
box-shadow: var(--shadow-card);
|
||||
}
|
||||
|
||||
.dark .card:hover {
|
||||
box-shadow: 0 20px 40px -10px rgba(0, 0, 0, 0.9), 0 0 0 1px rgba(255, 255, 255, 0.1);
|
||||
transform: translateY(-2px);
|
||||
.table th {
|
||||
@apply text-left text-sm font-medium uppercase tracking-wider;
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-tertiary);
|
||||
border-bottom: 1px solid var(--border-primary);
|
||||
padding: 1rem 1.5rem;
|
||||
}
|
||||
|
||||
.dark .form-input {
|
||||
border: 1px solid var(--border-primary);
|
||||
.table td {
|
||||
@apply text-sm;
|
||||
color: var(--text-secondary);
|
||||
border-bottom: 1px solid var(--border-primary);
|
||||
padding: 1rem 1.5rem;
|
||||
}
|
||||
|
||||
.table tbody tr:hover {
|
||||
background: var(--hover-card);
|
||||
}
|
||||
|
||||
.dark .table {
|
||||
background: var(--bg-card);
|
||||
}
|
||||
|
||||
.dark .form-input:focus {
|
||||
border-color: var(--border-focus);
|
||||
box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.1);
|
||||
.dark .table th {
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.dark .alert-info {
|
||||
background: rgba(0, 115, 206, 0.15);
|
||||
border-color: var(--mb-primary);
|
||||
color: #60a5fa;
|
||||
.dark .table td {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.dark .alert-success {
|
||||
background: rgba(34, 197, 94, 0.15);
|
||||
border-color: var(--text-success);
|
||||
color: #4ade80;
|
||||
/* === TOOLTIPS === */
|
||||
.tooltip {
|
||||
@apply absolute z-50 text-sm;
|
||||
background: var(--bg-modal);
|
||||
color: var(--text-primary);
|
||||
border: 1px solid var(--border-primary);
|
||||
backdrop-filter: blur(12px);
|
||||
border-radius: 8px !important;
|
||||
padding: 0.5rem 0.75rem;
|
||||
margin: 0.25rem;
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.dark .alert-warning {
|
||||
background: rgba(251, 191, 36, 0.15);
|
||||
border-color: var(--text-warning);
|
||||
color: #fbbf24;
|
||||
}
|
||||
|
||||
.dark .alert-error {
|
||||
background: rgba(248, 113, 113, 0.15);
|
||||
border-color: var(--text-error);
|
||||
color: #f87171;
|
||||
}
|
||||
|
||||
/* === UNIVERSAL CONTAINER STYLES === */
|
||||
div:not(nav):not(.navbar-sticky), section, article, main, header, footer, .container, .wrapper {
|
||||
/* === BADGES & STATUS === */
|
||||
.badge {
|
||||
@apply inline-flex items-center text-xs font-medium;
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-secondary);
|
||||
border-radius: 12px !important;
|
||||
padding: 0.375rem 0.75rem;
|
||||
margin: 0.125rem;
|
||||
border: 1px solid var(--border-primary);
|
||||
}
|
||||
|
||||
.badge-primary { background: var(--mb-primary); color: var(--mb-white); border-color: transparent; }
|
||||
.badge-success { background: var(--text-success); color: var(--mb-white); border-color: transparent; }
|
||||
.badge-warning { background: var(--text-warning); color: var(--mb-white); border-color: transparent; }
|
||||
.badge-error { background: var(--text-error); color: var(--mb-white); border-color: transparent; }
|
||||
|
||||
/* === RESPONSIVE OPTIMIERUNGEN === */
|
||||
@media (max-width: 768px) {
|
||||
.modal, .mercedes-modal {
|
||||
margin: 0.5rem;
|
||||
max-width: calc(100% - 1rem);
|
||||
border-radius: 12px !important;
|
||||
}
|
||||
|
||||
.card, .dashboard-card {
|
||||
border-radius: 12px !important;
|
||||
margin: 0.5rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.btn {
|
||||
@apply text-sm;
|
||||
padding: 0.625rem 1rem;
|
||||
margin: 0.125rem;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.form-input, input, textarea, select {
|
||||
padding: 0.75rem;
|
||||
margin: 0.25rem 0;
|
||||
border-radius: 8px;
|
||||
font-size: 16px; /* Verhindert Zoom auf iOS */
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
padding: 0.75rem;
|
||||
margin: 0.125rem;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.navbar-sticky {
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
}
|
||||
}
|
||||
|
||||
/* === PERFORMANCE OPTIMIERUNGEN === */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
* {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
|
||||
.glass, .glass-card {
|
||||
backdrop-filter: none !important;
|
||||
-webkit-backdrop-filter: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* === HIGH CONTRAST MODE === */
|
||||
@media (prefers-contrast: high) {
|
||||
:root {
|
||||
--border-primary: #000000;
|
||||
--text-primary: #000000;
|
||||
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3);
|
||||
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--border-primary: #ffffff;
|
||||
--text-primary: #ffffff;
|
||||
--shadow-sm: 0 1px 3px rgba(255, 255, 255, 0.3);
|
||||
--shadow-md: 0 4px 6px rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
/* === PRINT STYLES === */
|
||||
@media print {
|
||||
.modal-overlay, .tooltip, .btn, .navbar-sticky, .mobile-menu {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.card, .dashboard-card {
|
||||
box-shadow: none;
|
||||
border: 1px solid #000000;
|
||||
break-inside: avoid;
|
||||
}
|
||||
|
||||
body {
|
||||
background: white !important;
|
||||
color: black !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* === ENHANCED UTILITY CLASSES === */
|
||||
.container, .wrapper, .content {
|
||||
padding: 2rem;
|
||||
margin: 1rem;
|
||||
border-radius: 20px !important;
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--border-primary);
|
||||
}
|
||||
|
||||
.section {
|
||||
padding: 2.5rem;
|
||||
margin: 1.5rem 0;
|
||||
border-radius: 24px !important;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border-primary);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
/* === ENHANCED PADDING SYSTEM === */
|
||||
.p-xs { padding: 0.5rem !important; }
|
||||
.p-sm { padding: 1rem !important; }
|
||||
.p-md { padding: 1.5rem !important; }
|
||||
.p-lg { padding: 2rem !important; }
|
||||
.p-xl { padding: 3rem !important; }
|
||||
|
||||
.m-xs { margin: 0.25rem !important; }
|
||||
.m-sm { margin: 0.5rem !important; }
|
||||
.m-md { margin: 1rem !important; }
|
||||
.m-lg { margin: 1.5rem !important; }
|
||||
.m-xl { margin: 2rem !important; }
|
||||
|
||||
.theme-transition {
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
@ -1158,4 +1239,110 @@ div:not(nav):not(.navbar-sticky), section, article, main, header, footer, .conta
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.text-ellipsis {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.transition-fast { transition: all 0.15s ease; }
|
||||
.transition-normal { transition: all 0.2s ease; }
|
||||
.transition-slow { transition: all 0.3s ease; }
|
||||
|
||||
/* === SCROLLBAR STYLING === */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--border-secondary);
|
||||
border-radius: 4px;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--border-primary);
|
||||
}
|
||||
|
||||
.dark ::-webkit-scrollbar-track {
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.dark ::-webkit-scrollbar-thumb {
|
||||
background: var(--border-secondary);
|
||||
}
|
||||
|
||||
.dark ::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--border-primary);
|
||||
}
|
||||
|
||||
/* === LOADING STATES === */
|
||||
.loading {
|
||||
@apply animate-pulse;
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
@apply animate-spin rounded-full;
|
||||
border: 2px solid var(--border-primary);
|
||||
border-top-color: var(--text-accent);
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
}
|
||||
|
||||
.skeleton {
|
||||
background: linear-gradient(90deg, var(--bg-tertiary) 25%, var(--bg-secondary) 50%, var(--bg-tertiary) 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: loading 1.5s infinite;
|
||||
}
|
||||
|
||||
@keyframes loading {
|
||||
0% { background-position: 200% 0; }
|
||||
100% { background-position: -200% 0; }
|
||||
}
|
||||
|
||||
/* === STATUS INDICATORS === */
|
||||
.status-online { color: var(--text-success); }
|
||||
.status-offline { color: var(--text-error); }
|
||||
.status-warning { color: var(--text-warning); }
|
||||
.status-info { color: var(--text-accent); }
|
||||
|
||||
.status-dot {
|
||||
width: 0.5rem;
|
||||
height: 0.5rem;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.status-dot.online { background: var(--text-success); }
|
||||
.status-dot.offline { background: var(--text-error); }
|
||||
.status-dot.warning { background: var(--text-warning); }
|
||||
.status-dot.info { background: var(--text-accent); }
|
||||
|
||||
/* === HOVER EFFECTS === */
|
||||
.hover-lift {
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.hover-lift:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-card-hover);
|
||||
}
|
||||
|
||||
.hover-scale {
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.hover-scale:hover {
|
||||
transform: scale(1.02);
|
||||
}
|
473
backend/utils/hardware_integration_new.py
Normal file
473
backend/utils/hardware_integration_new.py
Normal file
@ -0,0 +1,473 @@
|
||||
#!/usr/bin/env python3.11
|
||||
"""
|
||||
Hardware Integration - VOLLSTÄNDIGE Backend-Steuerung für Drucker/Steckdosen
|
||||
============================================================================
|
||||
|
||||
NEUE PHILOSOPHIE - BACKEND DIKTIERT FRONTEND:
|
||||
- Drucker werden AUSSCHLIESSLICH über ihre Tapo-Steckdosen gesteuert
|
||||
- KEIN JavaScript für Hardware-Steuerung - nur Flask/Jinja
|
||||
- Backend sammelt ALLE Daten und übergibt sie komplett an Templates
|
||||
- Frontend ist PASSIV und zeigt nur an, was Backend vorgibt
|
||||
|
||||
Autor: Till Tomczak - Mercedes-Benz TBA Marienfelde
|
||||
Datum: 2025-06-19 (Komplett-Neuschreibung für Backend-Kontrolle)
|
||||
"""
|
||||
|
||||
import time
|
||||
import socket
|
||||
import threading
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, List, Any, Optional, Tuple
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
|
||||
# Hardware-Bibliotheken
|
||||
try:
|
||||
from PyP100.PyP100 import P100 as PyP100
|
||||
from PyP100.PyP110 import P110 as PyP110
|
||||
TAPO_AVAILABLE = True
|
||||
except ImportError:
|
||||
PyP100 = None
|
||||
PyP110 = None
|
||||
TAPO_AVAILABLE = False
|
||||
|
||||
# MYP Models & Utils
|
||||
from models import get_db_session, Printer, PlugStatusLog
|
||||
from utils.logging_config import get_logger
|
||||
|
||||
# Logger
|
||||
hardware_logger = get_logger("hardware_integration")
|
||||
|
||||
# ===== DRUCKER-STEUERUNGS-KLASSE =====
|
||||
|
||||
class DruckerSteuerung:
|
||||
"""
|
||||
VOLLSTÄNDIGE Backend-Steuerung aller Drucker über Tapo-Steckdosen.
|
||||
|
||||
Diese Klasse übernimmt die KOMPLETTE Kontrolle:
|
||||
- Status-Sammlung für alle Drucker
|
||||
- Ein/Aus-Schaltung über Steckdosen
|
||||
- Energiemonitoring
|
||||
- Template-Daten-Vorbereitung
|
||||
|
||||
Das Frontend erhält fertige Daten und muss NICHTS selbst berechnen!
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialisiere die Drucker-Steuerung"""
|
||||
# Standard-Tapo-Zugangsdaten
|
||||
self.tapo_username = "admin"
|
||||
self.tapo_password = "admin"
|
||||
self.timeout = 10
|
||||
self.retry_count = 3
|
||||
|
||||
# Backend-State-Management
|
||||
self.drucker_stati = {} # Aktueller Status aller Drucker
|
||||
self.energie_daten = {} # Energie-Monitoring-Daten
|
||||
self.letztes_update = {} # Letzte Aktualisierung pro Drucker
|
||||
self.verbindung_cache = {} # Connection-Pool für Performance
|
||||
|
||||
hardware_logger.info("🎯 DruckerSteuerung initialisiert - BACKEND ÜBERNIMMT KONTROLLE")
|
||||
|
||||
if not TAPO_AVAILABLE:
|
||||
hardware_logger.warning("⚠️ PyP100 nicht verfügbar - Simulation-Modus aktiv")
|
||||
|
||||
# ===== KERN-STEUERUNGS-FUNKTIONEN =====
|
||||
|
||||
def drucker_einschalten(self, drucker_id: int, grund: str = "Manuell") -> Dict[str, Any]:
|
||||
"""
|
||||
Schaltet einen Drucker über seine Tapo-Steckdose EIN.
|
||||
|
||||
Args:
|
||||
drucker_id: ID des Druckers
|
||||
grund: Grund für das Einschalten (für Logging)
|
||||
|
||||
Returns:
|
||||
Dict mit Ergebnis und neuen Template-Daten
|
||||
"""
|
||||
hardware_logger.info(f"🟢 Drucker {drucker_id} wird eingeschaltet - Grund: {grund}")
|
||||
|
||||
try:
|
||||
with get_db_session() as session:
|
||||
drucker = session.query(Printer).filter(Printer.id == drucker_id).first()
|
||||
|
||||
if not drucker:
|
||||
return {
|
||||
'success': False,
|
||||
'error': f'Drucker {drucker_id} nicht gefunden',
|
||||
'template_data': {}
|
||||
}
|
||||
|
||||
if not drucker.plug_ip:
|
||||
return {
|
||||
'success': False,
|
||||
'error': f'Keine Steckdosen-IP für {drucker.name} konfiguriert',
|
||||
'template_data': {}
|
||||
}
|
||||
|
||||
# Steckdose einschalten
|
||||
erfolg = self._steckdose_schalten(drucker.plug_ip, True)
|
||||
|
||||
if erfolg:
|
||||
# Drucker-Status in DB aktualisieren
|
||||
drucker.status = 'online'
|
||||
drucker.last_checked = datetime.now()
|
||||
session.commit()
|
||||
|
||||
# Status-Log erstellen
|
||||
self._status_log_erstellen(drucker_id, 'turned_on', grund)
|
||||
|
||||
# Backend-State aktualisieren
|
||||
self._drucker_state_aktualisieren(drucker)
|
||||
|
||||
hardware_logger.info(f"✅ Drucker {drucker.name} erfolgreich eingeschaltet")
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'message': f'Drucker {drucker.name} eingeschaltet',
|
||||
'template_data': self._template_daten_sammeln()
|
||||
}
|
||||
else:
|
||||
hardware_logger.error(f"❌ Drucker {drucker.name} konnte nicht eingeschaltet werden")
|
||||
|
||||
return {
|
||||
'success': False,
|
||||
'error': f'Steckdose {drucker.plug_ip} nicht erreichbar',
|
||||
'template_data': self._template_daten_sammeln()
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
hardware_logger.error(f"❌ Fehler beim Einschalten von Drucker {drucker_id}: {e}")
|
||||
return {
|
||||
'success': False,
|
||||
'error': f'Technischer Fehler: {str(e)}',
|
||||
'template_data': {}
|
||||
}
|
||||
|
||||
def drucker_ausschalten(self, drucker_id: int, grund: str = "Manuell") -> Dict[str, Any]:
|
||||
"""
|
||||
Schaltet einen Drucker über seine Tapo-Steckdose AUS.
|
||||
|
||||
Args:
|
||||
drucker_id: ID des Druckers
|
||||
grund: Grund für das Ausschalten (für Logging)
|
||||
|
||||
Returns:
|
||||
Dict mit Ergebnis und neuen Template-Daten
|
||||
"""
|
||||
hardware_logger.info(f"🔴 Drucker {drucker_id} wird ausgeschaltet - Grund: {grund}")
|
||||
|
||||
try:
|
||||
with get_db_session() as session:
|
||||
drucker = session.query(Printer).filter(Printer.id == drucker_id).first()
|
||||
|
||||
if not drucker:
|
||||
return {
|
||||
'success': False,
|
||||
'error': f'Drucker {drucker_id} nicht gefunden',
|
||||
'template_data': {}
|
||||
}
|
||||
|
||||
if not drucker.plug_ip:
|
||||
return {
|
||||
'success': False,
|
||||
'error': f'Keine Steckdosen-IP für {drucker.name} konfiguriert',
|
||||
'template_data': {}
|
||||
}
|
||||
|
||||
# Steckdose ausschalten
|
||||
erfolg = self._steckdose_schalten(drucker.plug_ip, False)
|
||||
|
||||
if erfolg:
|
||||
# Drucker-Status in DB aktualisieren
|
||||
drucker.status = 'offline'
|
||||
drucker.last_checked = datetime.now()
|
||||
session.commit()
|
||||
|
||||
# Status-Log erstellen
|
||||
self._status_log_erstellen(drucker_id, 'turned_off', grund)
|
||||
|
||||
# Backend-State aktualisieren
|
||||
self._drucker_state_aktualisieren(drucker)
|
||||
|
||||
hardware_logger.info(f"✅ Drucker {drucker.name} erfolgreich ausgeschaltet")
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'message': f'Drucker {drucker.name} ausgeschaltet',
|
||||
'template_data': self._template_daten_sammeln()
|
||||
}
|
||||
else:
|
||||
hardware_logger.error(f"❌ Drucker {drucker.name} konnte nicht ausgeschaltet werden")
|
||||
|
||||
return {
|
||||
'success': False,
|
||||
'error': f'Steckdose {drucker.plug_ip} nicht erreichbar',
|
||||
'template_data': self._template_daten_sammeln()
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
hardware_logger.error(f"❌ Fehler beim Ausschalten von Drucker {drucker_id}: {e}")
|
||||
return {
|
||||
'success': False,
|
||||
'error': f'Technischer Fehler: {str(e)}',
|
||||
'template_data': {}
|
||||
}
|
||||
|
||||
def drucker_toggle(self, drucker_id: int, grund: str = "Toggle") -> Dict[str, Any]:
|
||||
"""
|
||||
Wechselt den Status eines Druckers (Ein <-> Aus).
|
||||
|
||||
Args:
|
||||
drucker_id: ID des Druckers
|
||||
grund: Grund für den Wechsel
|
||||
|
||||
Returns:
|
||||
Dict mit Ergebnis und neuen Template-Daten
|
||||
"""
|
||||
try:
|
||||
with get_db_session() as session:
|
||||
drucker = session.query(Printer).filter(Printer.id == drucker_id).first()
|
||||
|
||||
if not drucker:
|
||||
return {
|
||||
'success': False,
|
||||
'error': f'Drucker {drucker_id} nicht gefunden',
|
||||
'template_data': {}
|
||||
}
|
||||
|
||||
# Aktueller Status bestimmen
|
||||
if drucker.status == 'online':
|
||||
return self.drucker_ausschalten(drucker_id, f"{grund} (war online)")
|
||||
else:
|
||||
return self.drucker_einschalten(drucker_id, f"{grund} (war offline)")
|
||||
|
||||
except Exception as e:
|
||||
hardware_logger.error(f"❌ Fehler beim Toggle von Drucker {drucker_id}: {e}")
|
||||
return {
|
||||
'success': False,
|
||||
'error': f'Technischer Fehler: {str(e)}',
|
||||
'template_data': {}
|
||||
}
|
||||
|
||||
# ===== DATEN-SAMMLUNG FÜR TEMPLATES =====
|
||||
|
||||
def template_daten_sammeln(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Sammelt ALLE Daten für die Frontend-Templates.
|
||||
Das Frontend muss NICHTS selbst berechnen!
|
||||
|
||||
Returns:
|
||||
Dict mit allen Template-Daten für Jinja
|
||||
"""
|
||||
hardware_logger.debug("📊 Sammle Template-Daten für Frontend")
|
||||
|
||||
try:
|
||||
with get_db_session() as session:
|
||||
drucker_liste = session.query(Printer).order_by(Printer.name).all()
|
||||
|
||||
# Drucker-Daten mit Status sammeln
|
||||
drucker_daten = []
|
||||
gesamt_online = 0
|
||||
gesamt_offline = 0
|
||||
|
||||
for drucker in drucker_liste:
|
||||
# Status aktualisieren falls nötig
|
||||
aktueller_status = self._drucker_status_pruefen(drucker)
|
||||
|
||||
drucker_info = {
|
||||
'id': drucker.id,
|
||||
'name': drucker.name,
|
||||
'model': drucker.model or 'Unbekannt',
|
||||
'location': drucker.location or 'TBA Marienfelde',
|
||||
'status': aktueller_status,
|
||||
'plug_ip': drucker.plug_ip,
|
||||
'ip_address': drucker.ip_address,
|
||||
'active': drucker.active,
|
||||
'last_checked': drucker.last_checked,
|
||||
'created_at': drucker.created_at,
|
||||
|
||||
# UI-Hilfsdaten
|
||||
'status_class': 'success' if aktueller_status == 'online' else 'danger',
|
||||
'status_text': 'Online' if aktueller_status == 'online' else 'Offline',
|
||||
'status_icon': '🟢' if aktueller_status == 'online' else '🔴',
|
||||
'kann_gesteuert_werden': bool(drucker.plug_ip),
|
||||
'toggle_text': 'Ausschalten' if aktueller_status == 'online' else 'Einschalten',
|
||||
'toggle_action': 'off' if aktueller_status == 'online' else 'on',
|
||||
|
||||
# Energie-Daten (Mock für Demo)
|
||||
'current_power': 125.5 if aktueller_status == 'online' else 0.0,
|
||||
'daily_consumption': 2.4 if aktueller_status == 'online' else 0.0,
|
||||
'monthly_consumption': 45.8,
|
||||
}
|
||||
|
||||
drucker_daten.append(drucker_info)
|
||||
|
||||
if aktueller_status == 'online':
|
||||
gesamt_online += 1
|
||||
else:
|
||||
gesamt_offline += 1
|
||||
|
||||
# System-Statistiken
|
||||
statistiken = {
|
||||
'gesamt_drucker': len(drucker_liste),
|
||||
'online_drucker': gesamt_online,
|
||||
'offline_drucker': gesamt_offline,
|
||||
'verfügbarkeits_rate': round((gesamt_online / len(drucker_liste) * 100) if drucker_liste else 0, 1),
|
||||
'letztes_update': datetime.now(),
|
||||
|
||||
# Energie-Gesamtdaten (Mock)
|
||||
'gesamt_verbrauch': round(sum(d['daily_consumption'] for d in drucker_daten), 2),
|
||||
'aktuelle_leistung': round(sum(d['current_power'] for d in drucker_daten), 1),
|
||||
'geschätzte_kosten': round(sum(d['daily_consumption'] for d in drucker_daten) * 0.30, 2)
|
||||
}
|
||||
|
||||
return {
|
||||
'drucker': drucker_daten,
|
||||
'stats': statistiken,
|
||||
'system_status': 'healthy' if gesamt_online > 0 else 'warning',
|
||||
'timestamp': datetime.now().isoformat(),
|
||||
'tapo_verfügbar': TAPO_AVAILABLE
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
hardware_logger.error(f"❌ Fehler beim Sammeln der Template-Daten: {e}")
|
||||
return {
|
||||
'drucker': [],
|
||||
'stats': {
|
||||
'gesamt_drucker': 0,
|
||||
'online_drucker': 0,
|
||||
'offline_drucker': 0,
|
||||
'verfügbarkeits_rate': 0.0
|
||||
},
|
||||
'system_status': 'error',
|
||||
'error': str(e),
|
||||
'timestamp': datetime.now().isoformat(),
|
||||
'tapo_verfügbar': TAPO_AVAILABLE
|
||||
}
|
||||
|
||||
# ===== PRIVATE HILFSFUNKTIONEN =====
|
||||
|
||||
def _steckdose_schalten(self, ip: str, einschalten: bool) -> bool:
|
||||
"""Schaltet eine Tapo-Steckdose ein oder aus"""
|
||||
if not TAPO_AVAILABLE:
|
||||
hardware_logger.warning(f"⚠️ Simulation: Steckdose {ip} würde {'eingeschaltet' if einschalten else 'ausgeschaltet'}")
|
||||
return True # Simulation immer erfolgreich
|
||||
|
||||
try:
|
||||
action = "einschalten" if einschalten else "ausschalten"
|
||||
hardware_logger.debug(f"🔌 Versuche Steckdose {ip} zu {action}")
|
||||
|
||||
# P100-Verbindung herstellen
|
||||
p100 = PyP100(ip, self.tapo_username, self.tapo_password)
|
||||
p100.handshake()
|
||||
p100.login()
|
||||
|
||||
# Schalten
|
||||
if einschalten:
|
||||
p100.turnOn()
|
||||
else:
|
||||
p100.turnOff()
|
||||
|
||||
hardware_logger.info(f"✅ Steckdose {ip} erfolgreich {action}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
hardware_logger.error(f"❌ Fehler beim Schalten der Steckdose {ip}: {e}")
|
||||
return False
|
||||
|
||||
def _drucker_status_pruefen(self, drucker: Printer) -> str:
|
||||
"""Prüft den aktuellen Status eines Druckers"""
|
||||
if not drucker.plug_ip:
|
||||
return 'unknown'
|
||||
|
||||
# Ping-Test zur Steckdose
|
||||
if self._ping_test(drucker.plug_ip):
|
||||
return 'online'
|
||||
else:
|
||||
return 'offline'
|
||||
|
||||
def _ping_test(self, ip: str, timeout: int = 3) -> bool:
|
||||
"""Einfacher Ping-Test zu einer IP"""
|
||||
try:
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.settimeout(timeout)
|
||||
result = sock.connect_ex((ip, 80))
|
||||
sock.close()
|
||||
return result == 0
|
||||
except:
|
||||
return False
|
||||
|
||||
def _status_log_erstellen(self, drucker_id: int, action: str, grund: str):
|
||||
"""Erstellt einen Eintrag im Status-Log"""
|
||||
try:
|
||||
with get_db_session() as session:
|
||||
log_entry = PlugStatusLog(
|
||||
printer_id=drucker_id,
|
||||
timestamp=datetime.now(),
|
||||
action=action,
|
||||
reason=grund,
|
||||
success=True
|
||||
)
|
||||
session.add(log_entry)
|
||||
session.commit()
|
||||
except Exception as e:
|
||||
hardware_logger.warning(f"⚠️ Status-Log konnte nicht erstellt werden: {e}")
|
||||
|
||||
def _drucker_state_aktualisieren(self, drucker: Printer):
|
||||
"""Aktualisiert den internen Backend-State"""
|
||||
self.drucker_stati[drucker.id] = {
|
||||
'name': drucker.name,
|
||||
'status': drucker.status,
|
||||
'last_update': datetime.now(),
|
||||
'plug_ip': drucker.plug_ip
|
||||
}
|
||||
|
||||
def _template_daten_sammeln(self) -> Dict[str, Any]:
|
||||
"""Wrapper für template_daten_sammeln (Backward-Compatibility)"""
|
||||
return self.template_daten_sammeln()
|
||||
|
||||
# ===== GLOBALE INSTANZ =====
|
||||
|
||||
# Singleton-Pattern für globale Drucker-Steuerung
|
||||
_drucker_steuerung_instanz = None
|
||||
|
||||
def get_drucker_steuerung() -> DruckerSteuerung:
|
||||
"""
|
||||
Gibt die globale DruckerSteuerung-Instanz zurück (Singleton).
|
||||
|
||||
Returns:
|
||||
DruckerSteuerung: Die globale Instanz
|
||||
"""
|
||||
global _drucker_steuerung_instanz
|
||||
|
||||
if _drucker_steuerung_instanz is None:
|
||||
_drucker_steuerung_instanz = DruckerSteuerung()
|
||||
|
||||
return _drucker_steuerung_instanz
|
||||
|
||||
# ===== LEGACY-KOMPATIBILITÄT =====
|
||||
|
||||
# Backward-Compatibility für bestehenden Code
|
||||
def get_tapo_controller():
|
||||
"""Legacy-Funktion für Rückwärtskompatibilität"""
|
||||
return get_drucker_steuerung()
|
||||
|
||||
def toggle_plug(ip: str, state: bool) -> bool:
|
||||
"""Legacy-Funktion für direktes Steckdosen-Schalten"""
|
||||
controller = get_drucker_steuerung()
|
||||
return controller._steckdose_schalten(ip, state)
|
||||
|
||||
def get_printer_monitor():
|
||||
"""Legacy-Funktion für Drucker-Monitoring"""
|
||||
return get_drucker_steuerung()
|
||||
|
||||
# Export für andere Module
|
||||
__all__ = [
|
||||
'DruckerSteuerung',
|
||||
'get_drucker_steuerung',
|
||||
'get_tapo_controller', # Legacy
|
||||
'toggle_plug', # Legacy
|
||||
'get_printer_monitor' # Legacy
|
||||
]
|
||||
|
||||
hardware_logger.info("🚀 Hardware Integration (Backend-Kontrolle) erfolgreich geladen")
|
Reference in New Issue
Block a user