🎉 Improved Energy Monitoring Dashboard & Backend Integration 🌍
This commit is contained in:
@ -683,6 +683,11 @@ app.register_blueprint(sessions_blueprint)
|
||||
app.register_blueprint(tapo_blueprint) # Tapo-Steckdosen-Steuerung
|
||||
app.register_blueprint(api_blueprint) # Einfache API-Endpunkte
|
||||
|
||||
# Energiemonitoring-Blueprints registrieren
|
||||
from blueprints.energy_monitoring import energy_blueprint, energy_api_blueprint
|
||||
app.register_blueprint(energy_blueprint) # Frontend-Routen für Energiemonitoring
|
||||
app.register_blueprint(energy_api_blueprint) # API-Endpunkte für Energiedaten
|
||||
|
||||
# ===== HILFSSYSTEME INITIALISIEREN =====
|
||||
init_security(app)
|
||||
init_permission_helpers(app)
|
||||
|
436
backend/blueprints/energy_monitoring.py
Normal file
436
backend/blueprints/energy_monitoring.py
Normal file
@ -0,0 +1,436 @@
|
||||
"""
|
||||
Energiemonitoring-Blueprint für MYP Platform
|
||||
============================================
|
||||
|
||||
Dieses Modul stellt alle Routen und Funktionen für die Energieüberwachung
|
||||
von TP-Link Tapo P110 Smart Plugs bereit.
|
||||
|
||||
Funktionalitäten:
|
||||
- Live-Energiedaten abrufen
|
||||
- Historische Verbrauchsstatistiken
|
||||
- Energieverbrauchsdiagramme
|
||||
- Energieeffizienz-Analysen
|
||||
"""
|
||||
|
||||
from flask import Blueprint, request, jsonify, render_template
|
||||
from flask_login import login_required, current_user
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, List, Any, Optional
|
||||
|
||||
from models import Printer, get_db_session, PlugStatusLog
|
||||
from utils.logging_config import get_logger, measure_execution_time
|
||||
from utils.security_suite import require_permission, Permission
|
||||
from utils.hardware_integration import get_tapo_controller
|
||||
|
||||
# Logger initialisieren
|
||||
energy_logger = get_logger("energy_monitoring")
|
||||
|
||||
# Blueprint erstellen
|
||||
energy_blueprint = Blueprint("energy", __name__, url_prefix="/energy")
|
||||
energy_api_blueprint = Blueprint("energy_api", __name__, url_prefix="/api/energy")
|
||||
|
||||
# ===== FRONTEND ROUTEN =====
|
||||
|
||||
@energy_blueprint.route("/")
|
||||
@login_required
|
||||
def energy_dashboard():
|
||||
"""
|
||||
Hauptseite für Energiemonitoring-Dashboard.
|
||||
|
||||
Returns:
|
||||
Rendered HTML template mit Energieübersicht
|
||||
"""
|
||||
energy_logger.info(f"🔋 Energiemonitoring-Dashboard aufgerufen von {current_user.username}")
|
||||
|
||||
try:
|
||||
# Basis-Statistiken für Template laden
|
||||
tapo_controller = get_tapo_controller()
|
||||
basic_stats = tapo_controller.get_energy_statistics()
|
||||
|
||||
return render_template(
|
||||
'energy_dashboard.html',
|
||||
stats=basic_stats,
|
||||
current_user=current_user,
|
||||
page_title="Energiemonitoring"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
energy_logger.error(f"❌ Fehler beim Laden des Energiemonitoring-Dashboards: {str(e)}")
|
||||
return render_template(
|
||||
'energy_dashboard.html',
|
||||
stats={'error': str(e)},
|
||||
current_user=current_user,
|
||||
page_title="Energiemonitoring"
|
||||
)
|
||||
|
||||
@energy_blueprint.route("/device/<int:device_id>")
|
||||
@login_required
|
||||
def device_details(device_id):
|
||||
"""
|
||||
Detailseite für ein spezifisches Energiemonitoring-Gerät.
|
||||
|
||||
Args:
|
||||
device_id: ID des Druckers/Geräts
|
||||
|
||||
Returns:
|
||||
Rendered HTML template mit Gerätedetails
|
||||
"""
|
||||
energy_logger.info(f"🔍 Gerätedetails für ID {device_id} aufgerufen von {current_user.username}")
|
||||
|
||||
try:
|
||||
with get_db_session() as db_session:
|
||||
printer = db_session.query(Printer).filter(Printer.id == device_id).first()
|
||||
|
||||
if not printer:
|
||||
energy_logger.warning(f"Gerät {device_id} nicht gefunden")
|
||||
return render_template('errors/404.html'), 404
|
||||
|
||||
return render_template(
|
||||
'energy_device_details.html',
|
||||
device=printer,
|
||||
current_user=current_user,
|
||||
page_title=f"Energiemonitoring - {printer.name}"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
energy_logger.error(f"❌ Fehler beim Laden der Gerätedetails: {str(e)}")
|
||||
return render_template('errors/500.html'), 500
|
||||
|
||||
# ===== API ENDPUNKTE =====
|
||||
|
||||
@energy_api_blueprint.route("/dashboard", methods=["GET"])
|
||||
@login_required
|
||||
@measure_execution_time(logger=energy_logger, task_name="API-Energiemonitoring-Dashboard")
|
||||
def api_energy_dashboard():
|
||||
"""
|
||||
API-Endpunkt für Energiemonitoring-Dashboard-Daten.
|
||||
|
||||
Returns:
|
||||
JSON: Umfassende Energiestatistiken für Dashboard
|
||||
"""
|
||||
energy_logger.info(f"📊 API-Energiemonitoring-Dashboard von {current_user.username}")
|
||||
|
||||
try:
|
||||
tapo_controller = get_tapo_controller()
|
||||
energy_stats = tapo_controller.get_energy_statistics()
|
||||
|
||||
# Zusätzliche Dashboard-Metriken berechnen
|
||||
dashboard_data = {
|
||||
'overview': {
|
||||
'total_devices': energy_stats['total_devices'],
|
||||
'online_devices': energy_stats['online_devices'],
|
||||
'offline_devices': energy_stats['total_devices'] - energy_stats['online_devices'],
|
||||
'availability_rate': round((energy_stats['online_devices'] / energy_stats['total_devices'] * 100) if energy_stats['total_devices'] > 0 else 0, 1)
|
||||
},
|
||||
'current_consumption': {
|
||||
'total_power': round(energy_stats['total_current_power'], 2),
|
||||
'average_power': round(energy_stats['avg_current_power'], 2),
|
||||
'max_power_device': None,
|
||||
'min_power_device': None
|
||||
},
|
||||
'energy_totals': {
|
||||
'today_total': round(energy_stats['total_today_energy'], 1),
|
||||
'month_total': round(energy_stats['total_month_energy'], 1),
|
||||
'today_average': round(energy_stats['avg_today_energy'], 1),
|
||||
'month_average': round(energy_stats['avg_month_energy'], 1)
|
||||
},
|
||||
'trend_data': {
|
||||
'hourly': energy_stats['hourly_consumption'],
|
||||
'daily': energy_stats['daily_consumption'],
|
||||
'monthly': energy_stats['monthly_consumption']
|
||||
},
|
||||
'devices': energy_stats['devices'],
|
||||
'timestamp': energy_stats['timestamp']
|
||||
}
|
||||
|
||||
# Max/Min Gerät finden
|
||||
if energy_stats['devices']:
|
||||
online_devices = [d for d in energy_stats['devices'] if d['online'] and d['current_power'] > 0]
|
||||
if online_devices:
|
||||
max_device = max(online_devices, key=lambda x: x['current_power'])
|
||||
min_device = min(online_devices, key=lambda x: x['current_power'])
|
||||
|
||||
dashboard_data['current_consumption']['max_power_device'] = {
|
||||
'name': max_device['name'],
|
||||
'power': round(max_device['current_power'], 2)
|
||||
}
|
||||
dashboard_data['current_consumption']['min_power_device'] = {
|
||||
'name': min_device['name'],
|
||||
'power': round(min_device['current_power'], 2)
|
||||
}
|
||||
|
||||
energy_logger.info(f"✅ Dashboard-Daten erfolgreich erstellt: {dashboard_data['overview']['online_devices']} Geräte online")
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'data': dashboard_data
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
energy_logger.error(f"❌ Fehler beim Erstellen der Dashboard-Daten: {str(e)}")
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Fehler beim Laden der Energiedaten',
|
||||
'message': str(e)
|
||||
}), 500
|
||||
|
||||
@energy_api_blueprint.route("/device/<int:device_id>", methods=["GET"])
|
||||
@login_required
|
||||
@measure_execution_time(logger=energy_logger, task_name="API-Gerätespezifische-Energiedaten")
|
||||
def api_device_energy_data(device_id):
|
||||
"""
|
||||
API-Endpunkt für gerätespezifische Energiedaten.
|
||||
|
||||
Args:
|
||||
device_id: ID des Druckers/Geräts
|
||||
|
||||
Returns:
|
||||
JSON: Detaillierte Energiedaten des Geräts
|
||||
"""
|
||||
energy_logger.info(f"🔍 API-Energiedaten für Gerät {device_id} von {current_user.username}")
|
||||
|
||||
try:
|
||||
with get_db_session() as db_session:
|
||||
printer = db_session.query(Printer).filter(Printer.id == device_id).first()
|
||||
|
||||
if not printer:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Gerät nicht gefunden'
|
||||
}), 404
|
||||
|
||||
if not printer.plug_ip:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Gerät hat keine Energiemonitoring-Steckdose konfiguriert'
|
||||
}), 400
|
||||
|
||||
# Energiedaten vom Tapo Controller abrufen
|
||||
tapo_controller = get_tapo_controller()
|
||||
reachable, status = tapo_controller.check_outlet_status(printer.plug_ip, printer_id=device_id)
|
||||
|
||||
if not reachable:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Gerät nicht erreichbar',
|
||||
'device_info': {
|
||||
'id': printer.id,
|
||||
'name': printer.name,
|
||||
'location': printer.location,
|
||||
'online': False
|
||||
}
|
||||
}), 503
|
||||
|
||||
# Detaillierte Energiedaten abrufen
|
||||
energy_stats = tapo_controller.get_energy_statistics()
|
||||
device_data = None
|
||||
|
||||
for device in energy_stats['devices']:
|
||||
if device['id'] == device_id:
|
||||
device_data = device
|
||||
break
|
||||
|
||||
if not device_data:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Energiedaten für Gerät nicht verfügbar'
|
||||
}), 404
|
||||
|
||||
# Historische Logs aus Datenbank
|
||||
recent_logs = db_session.query(PlugStatusLog).filter(
|
||||
PlugStatusLog.printer_id == device_id,
|
||||
PlugStatusLog.timestamp >= datetime.now() - timedelta(hours=24)
|
||||
).order_by(PlugStatusLog.timestamp.desc()).limit(100).all()
|
||||
|
||||
historical_data = []
|
||||
for log in recent_logs:
|
||||
historical_data.append({
|
||||
'timestamp': log.timestamp.isoformat(),
|
||||
'power_consumption': log.power_consumption,
|
||||
'voltage': log.voltage,
|
||||
'current': log.current,
|
||||
'status': log.status
|
||||
})
|
||||
|
||||
result = {
|
||||
'device_info': {
|
||||
'id': printer.id,
|
||||
'name': printer.name,
|
||||
'model': printer.model,
|
||||
'location': printer.location,
|
||||
'ip': printer.plug_ip,
|
||||
'online': device_data['online']
|
||||
},
|
||||
'current_metrics': {
|
||||
'power': device_data['current_power'],
|
||||
'voltage': device_data.get('voltage', 0),
|
||||
'current': device_data.get('current', 0),
|
||||
'status': 'on' if device_data['current_power'] > 0 else 'off'
|
||||
},
|
||||
'energy_consumption': {
|
||||
'today': device_data['today_energy'],
|
||||
'month': device_data['month_energy'],
|
||||
'past24h': device_data['past24h'],
|
||||
'past30d': device_data['past30d'],
|
||||
'past1y': device_data['past1y']
|
||||
},
|
||||
'historical_logs': historical_data,
|
||||
'timestamp': datetime.now().isoformat()
|
||||
}
|
||||
|
||||
energy_logger.info(f"✅ Energiedaten erfolgreich abgerufen für {printer.name}: {device_data['current_power']}W")
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'data': result
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
energy_logger.error(f"❌ Fehler beim Abrufen der Gerätedaten: {str(e)}")
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Fehler beim Abrufen der Energiedaten',
|
||||
'message': str(e)
|
||||
}), 500
|
||||
|
||||
@energy_api_blueprint.route("/statistics", methods=["GET"])
|
||||
@login_required
|
||||
@measure_execution_time(logger=energy_logger, task_name="API-Energiestatistiken")
|
||||
def api_energy_statistics():
|
||||
"""
|
||||
API-Endpunkt für erweiterte Energiestatistiken.
|
||||
|
||||
Query-Parameter:
|
||||
- period: Zeitraum (today, week, month, year)
|
||||
|
||||
Returns:
|
||||
JSON: Erweiterte Energiestatistiken
|
||||
"""
|
||||
period = request.args.get('period', 'today')
|
||||
|
||||
energy_logger.info(f"📈 API-Energiestatistiken ({period}) von {current_user.username}")
|
||||
|
||||
try:
|
||||
tapo_controller = get_tapo_controller()
|
||||
base_stats = tapo_controller.get_energy_statistics()
|
||||
|
||||
# Zeitraumspezifische Statistiken
|
||||
if period == 'today':
|
||||
chart_data = {
|
||||
'labels': [f"{i:02d}:00" for i in range(24)],
|
||||
'datasets': [{
|
||||
'label': 'Stündlicher Verbrauch (Wh)',
|
||||
'data': base_stats['hourly_consumption'],
|
||||
'borderColor': '#3b82f6',
|
||||
'backgroundColor': 'rgba(59, 130, 246, 0.1)',
|
||||
'fill': True
|
||||
}]
|
||||
}
|
||||
elif period == 'week' or period == 'month':
|
||||
chart_data = {
|
||||
'labels': [f"Tag {i+1}" for i in range(30)],
|
||||
'datasets': [{
|
||||
'label': 'Täglicher Verbrauch (Wh)',
|
||||
'data': base_stats['daily_consumption'],
|
||||
'borderColor': '#10b981',
|
||||
'backgroundColor': 'rgba(16, 185, 129, 0.1)',
|
||||
'fill': True
|
||||
}]
|
||||
}
|
||||
elif period == 'year':
|
||||
month_names = ['Jan', 'Feb', 'Mär', 'Apr', 'Mai', 'Jun',
|
||||
'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez']
|
||||
chart_data = {
|
||||
'labels': month_names,
|
||||
'datasets': [{
|
||||
'label': 'Monatlicher Verbrauch (Wh)',
|
||||
'data': base_stats['monthly_consumption'],
|
||||
'borderColor': '#f59e0b',
|
||||
'backgroundColor': 'rgba(245, 158, 11, 0.1)',
|
||||
'fill': True
|
||||
}]
|
||||
}
|
||||
else:
|
||||
chart_data = {'labels': [], 'datasets': []}
|
||||
|
||||
result = {
|
||||
'period': period,
|
||||
'summary': {
|
||||
'total_consumption': base_stats['total_today_energy'] if period == 'today' else base_stats['total_month_energy'],
|
||||
'average_power': base_stats['avg_current_power'],
|
||||
'device_count': base_stats['online_devices']
|
||||
},
|
||||
'chart_data': chart_data,
|
||||
'device_breakdown': base_stats['devices'],
|
||||
'timestamp': base_stats['timestamp']
|
||||
}
|
||||
|
||||
energy_logger.info(f"✅ Energiestatistiken erfolgreich erstellt für Zeitraum: {period}")
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'data': result
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
energy_logger.error(f"❌ Fehler beim Erstellen der Energiestatistiken: {str(e)}")
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Fehler beim Erstellen der Statistiken',
|
||||
'message': str(e)
|
||||
}), 500
|
||||
|
||||
@energy_api_blueprint.route("/live", methods=["GET"])
|
||||
@login_required
|
||||
@measure_execution_time(logger=energy_logger, task_name="API-Live-Energiedaten")
|
||||
def api_live_energy_data():
|
||||
"""
|
||||
API-Endpunkt für Live-Energiedaten (für Real-Time Updates).
|
||||
|
||||
Returns:
|
||||
JSON: Aktuelle Energiedaten aller Geräte
|
||||
"""
|
||||
energy_logger.debug(f"⚡ Live-Energiedaten von {current_user.username}")
|
||||
|
||||
try:
|
||||
tapo_controller = get_tapo_controller()
|
||||
live_stats = tapo_controller.get_energy_statistics()
|
||||
|
||||
# Nur Live-Daten für minimale Latenz
|
||||
live_data = {
|
||||
'total_power': round(live_stats['total_current_power'], 2),
|
||||
'device_count': live_stats['online_devices'],
|
||||
'devices': [
|
||||
{
|
||||
'id': d['id'],
|
||||
'name': d['name'],
|
||||
'power': round(d['current_power'], 2),
|
||||
'online': d['online']
|
||||
}
|
||||
for d in live_stats['devices']
|
||||
],
|
||||
'timestamp': live_stats['timestamp']
|
||||
}
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'data': live_data
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
energy_logger.error(f"❌ Fehler beim Abrufen der Live-Energiedaten: {str(e)}")
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Fehler beim Abrufen der Live-Daten',
|
||||
'message': str(e)
|
||||
}), 500
|
||||
|
||||
# Legacy-Funktionen für Rückwärtskompatibilität
|
||||
def get_energy_monitoring():
|
||||
"""Legacy-Funktion für Energiemonitoring-Zugriff."""
|
||||
return get_tapo_controller()
|
||||
|
||||
# Exportierte Blueprints
|
||||
__all__ = ['energy_blueprint', 'energy_api_blueprint', 'get_energy_monitoring']
|
||||
|
||||
energy_logger.info("✅ Energiemonitoring-Blueprint initialisiert")
|
602
backend/templates/energy_dashboard.html
Normal file
602
backend/templates/energy_dashboard.html
Normal file
@ -0,0 +1,602 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Energiemonitoring - Mercedes-Benz MYP Platform{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<style>
|
||||
/* Energiemonitoring Dashboard Styles */
|
||||
.energy-card {
|
||||
background: linear-gradient(135deg, rgba(255,255,255,0.9) 0%, rgba(255,255,255,0.8) 100%);
|
||||
border: 1px solid rgba(255,255,255,0.2);
|
||||
border-radius: 16px;
|
||||
backdrop-filter: blur(20px);
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.1);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.dark .energy-card {
|
||||
background: linear-gradient(135deg, rgba(51,65,85,0.9) 0%, rgba(51,65,85,0.8) 100%);
|
||||
border: 1px solid rgba(71,85,105,0.3);
|
||||
}
|
||||
|
||||
.energy-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 12px 40px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
.energy-metric {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(135deg, #3b82f6, #1d4ed8);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.device-status-indicator {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
.device-status-online {
|
||||
background-color: #10b981;
|
||||
box-shadow: 0 0 10px rgba(16, 185, 129, 0.6);
|
||||
}
|
||||
|
||||
.device-status-offline {
|
||||
background-color: #ef4444;
|
||||
box-shadow: 0 0 10px rgba(239, 68, 68, 0.6);
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
position: relative;
|
||||
height: 300px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.power-gradient {
|
||||
background: linear-gradient(90deg, #10b981 0%, #f59e0b 50%, #ef4444 100%);
|
||||
height: 6px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="min-h-screen bg-gradient-to-br from-blue-50 via-indigo-50 to-purple-50 dark:from-slate-900 dark:via-slate-900 dark:to-slate-800">
|
||||
|
||||
<!-- Header Section -->
|
||||
<div class="bg-gradient-to-r from-blue-600 via-purple-600 to-indigo-600 text-white">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-4xl font-bold tracking-tight">🔋 Energiemonitoring</h1>
|
||||
<p class="mt-2 text-xl text-blue-100">
|
||||
Überwachen Sie den Energieverbrauch Ihrer 3D-Drucker in Echtzeit
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
<button id="refreshData" class="bg-white/20 hover:bg-white/30 px-4 py-2 rounded-xl backdrop-blur-sm transition-all duration-300">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button id="exportData" class="bg-white/20 hover:bg-white/30 px-4 py-2 rounded-xl backdrop-blur-sm transition-all duration-300">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 -mt-8 relative z-10">
|
||||
|
||||
<!-- KPI Cards -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||
<!-- Gesamtverbrauch -->
|
||||
<div class="energy-card p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="p-3 bg-gradient-to-br from-blue-500 to-blue-600 rounded-xl">
|
||||
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<div id="totalPower" class="energy-metric">{{ stats.total_current_power or 0 }}W</div>
|
||||
<div class="text-sm text-slate-500 dark:text-slate-400">Gesamtverbrauch</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="device-status-indicator device-status-online"></div>
|
||||
<span class="text-xs text-green-600 dark:text-green-400 font-medium">Live-Daten</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Online Geräte -->
|
||||
<div class="energy-card p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="p-3 bg-gradient-to-br from-green-500 to-green-600 rounded-xl">
|
||||
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<div id="onlineDevices" class="energy-metric">{{ stats.online_devices or 0 }}</div>
|
||||
<div class="text-sm text-slate-500 dark:text-slate-400">Online Geräte</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-sm text-slate-600 dark:text-slate-300">
|
||||
von <span id="totalDevices">{{ stats.total_devices or 0 }}</span> Geräten
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Heute Verbrauch -->
|
||||
<div class="energy-card p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="p-3 bg-gradient-to-br from-purple-500 to-purple-600 rounded-xl">
|
||||
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<div id="todayEnergy" class="energy-metric">{{ stats.total_today_energy or 0 }}Wh</div>
|
||||
<div class="text-sm text-slate-500 dark:text-slate-400">Heute</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-sm text-slate-600 dark:text-slate-300">
|
||||
Ø <span id="avgTodayEnergy">{{ stats.avg_today_energy or 0 }}</span>Wh pro Gerät
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Monatsverbrauch -->
|
||||
<div class="energy-card p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="p-3 bg-gradient-to-br from-orange-500 to-orange-600 rounded-xl">
|
||||
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<div id="monthEnergy" class="energy-metric">{{ stats.total_month_energy or 0 }}Wh</div>
|
||||
<div class="text-sm text-slate-500 dark:text-slate-400">Diesen Monat</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-sm text-slate-600 dark:text-slate-300">
|
||||
Ø <span id="avgMonthEnergy">{{ stats.avg_month_energy or 0 }}</span>Wh pro Gerät
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Charts Section -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
|
||||
<!-- Verbrauchstrend Chart -->
|
||||
<div class="energy-card p-6">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h3 class="text-xl font-semibold text-slate-900 dark:text-white">📈 Verbrauchstrend</h3>
|
||||
<select id="periodSelector" class="px-3 py-2 bg-white dark:bg-slate-700 border border-slate-300 dark:border-slate-600 rounded-lg text-sm">
|
||||
<option value="today">Heute (24h)</option>
|
||||
<option value="week">Diese Woche</option>
|
||||
<option value="month">Dieser Monat</option>
|
||||
<option value="year">Dieses Jahr</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="chart-container">
|
||||
<canvas id="consumptionChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Geräteverbrauch Chart -->
|
||||
<div class="energy-card p-6">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h3 class="text-xl font-semibold text-slate-900 dark:text-white">🔌 Geräteverbrauch</h3>
|
||||
<div class="text-sm text-slate-500 dark:text-slate-400">Live-Verbrauch</div>
|
||||
</div>
|
||||
<div class="chart-container">
|
||||
<canvas id="deviceChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Device List -->
|
||||
<div class="energy-card p-6 mb-8">
|
||||
<h3 class="text-xl font-semibold text-slate-900 dark:text-white mb-6">🖨️ Geräteübersicht</h3>
|
||||
<div id="deviceList" class="space-y-4">
|
||||
<!-- Wird dynamisch gefüllt -->
|
||||
<div class="flex justify-center items-center py-8">
|
||||
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading Overlay -->
|
||||
<div id="loadingOverlay" class="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center hidden">
|
||||
<div class="bg-white dark:bg-slate-800 rounded-xl p-6 flex items-center space-x-4">
|
||||
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
||||
<span class="text-slate-900 dark:text-white">Lade Energiedaten...</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<!-- Chart.js -->
|
||||
<script src="{{ url_for('static', filename='js/charts/chart.min.js') }}"></script>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* MYP Energiemonitoring Dashboard
|
||||
* Echtzeit-Überwachung des Energieverbrauchs von Tapo P110 Smart Plugs
|
||||
*/
|
||||
|
||||
class EnergyMonitoringDashboard {
|
||||
constructor() {
|
||||
this.currentPeriod = 'today';
|
||||
this.charts = {};
|
||||
this.updateInterval = null;
|
||||
this.data = {};
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.setupEventListeners();
|
||||
this.loadInitialData();
|
||||
this.startAutoUpdate();
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
// Period Selector
|
||||
document.getElementById('periodSelector').addEventListener('change', (e) => {
|
||||
this.currentPeriod = e.target.value;
|
||||
this.loadConsumptionData();
|
||||
});
|
||||
|
||||
// Refresh Button
|
||||
document.getElementById('refreshData').addEventListener('click', () => {
|
||||
this.loadAllData();
|
||||
});
|
||||
|
||||
// Export Button
|
||||
document.getElementById('exportData').addEventListener('click', () => {
|
||||
this.exportData();
|
||||
});
|
||||
}
|
||||
|
||||
async loadInitialData() {
|
||||
this.showLoading();
|
||||
await this.loadAllData();
|
||||
this.hideLoading();
|
||||
}
|
||||
|
||||
async loadAllData() {
|
||||
try {
|
||||
await Promise.all([
|
||||
this.loadDashboardData(),
|
||||
this.loadConsumptionData(),
|
||||
this.loadDeviceData()
|
||||
]);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Energiedaten:', error);
|
||||
this.showError('Fehler beim Laden der Energiedaten');
|
||||
}
|
||||
}
|
||||
|
||||
async loadDashboardData() {
|
||||
try {
|
||||
const response = await fetch('/api/energy/dashboard');
|
||||
if (!response.ok) throw new Error('Dashboard-Daten konnten nicht geladen werden');
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
this.data.dashboard = result.data;
|
||||
this.updateKPIs(result.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Dashboard-Daten-Fehler:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async loadConsumptionData() {
|
||||
try {
|
||||
const response = await fetch(`/api/energy/statistics?period=${this.currentPeriod}`);
|
||||
if (!response.ok) throw new Error('Verbrauchsdaten konnten nicht geladen werden');
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
this.data.consumption = result.data;
|
||||
this.updateConsumptionChart(result.data.chart_data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Verbrauchsdaten-Fehler:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async loadDeviceData() {
|
||||
try {
|
||||
const response = await fetch('/api/energy/live');
|
||||
if (!response.ok) throw new Error('Gerätedaten konnten nicht geladen werden');
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
this.data.devices = result.data;
|
||||
this.updateDeviceChart(result.data.devices);
|
||||
this.updateDeviceList(result.data.devices);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Gerätedaten-Fehler:', error);
|
||||
}
|
||||
}
|
||||
|
||||
updateKPIs(dashboardData) {
|
||||
const overview = dashboardData.overview || {};
|
||||
const consumption = dashboardData.current_consumption || {};
|
||||
const totals = dashboardData.energy_totals || {};
|
||||
|
||||
// KPI Updates
|
||||
document.getElementById('totalPower').textContent = `${consumption.total_power || 0}W`;
|
||||
document.getElementById('onlineDevices').textContent = overview.online_devices || 0;
|
||||
document.getElementById('totalDevices').textContent = overview.total_devices || 0;
|
||||
document.getElementById('todayEnergy').textContent = `${totals.today_total || 0}Wh`;
|
||||
document.getElementById('avgTodayEnergy').textContent = totals.today_average || 0;
|
||||
document.getElementById('monthEnergy').textContent = `${totals.month_total || 0}Wh`;
|
||||
document.getElementById('avgMonthEnergy').textContent = totals.month_average || 0;
|
||||
}
|
||||
|
||||
updateConsumptionChart(chartData) {
|
||||
const canvas = document.getElementById('consumptionChart');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// Destroy existing chart
|
||||
if (this.charts.consumption) {
|
||||
this.charts.consumption.destroy();
|
||||
}
|
||||
|
||||
this.charts.consumption = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: chartData,
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true,
|
||||
position: 'top'
|
||||
},
|
||||
tooltip: {
|
||||
mode: 'index',
|
||||
intersect: false,
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
return `${context.dataset.label}: ${context.parsed.y}Wh`;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
display: true,
|
||||
grid: {
|
||||
color: 'rgba(0,0,0,0.1)'
|
||||
}
|
||||
},
|
||||
y: {
|
||||
display: true,
|
||||
beginAtZero: true,
|
||||
grid: {
|
||||
color: 'rgba(0,0,0,0.1)'
|
||||
},
|
||||
ticks: {
|
||||
callback: function(value) {
|
||||
return value + 'Wh';
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
interaction: {
|
||||
mode: 'nearest',
|
||||
axis: 'x',
|
||||
intersect: false
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateDeviceChart(devices) {
|
||||
const canvas = document.getElementById('deviceChart');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// Destroy existing chart
|
||||
if (this.charts.devices) {
|
||||
this.charts.devices.destroy();
|
||||
}
|
||||
|
||||
const onlineDevices = devices.filter(d => d.online && d.power > 0);
|
||||
|
||||
if (onlineDevices.length === 0) {
|
||||
// Zeige "Keine Daten" Nachricht
|
||||
ctx.font = '16px Arial';
|
||||
ctx.fillStyle = '#64748b';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText('Keine aktiven Geräte', canvas.width / 2, canvas.height / 2);
|
||||
return;
|
||||
}
|
||||
|
||||
const chartData = {
|
||||
labels: onlineDevices.map(d => d.name),
|
||||
datasets: [{
|
||||
label: 'Aktueller Verbrauch (W)',
|
||||
data: onlineDevices.map(d => d.power),
|
||||
backgroundColor: [
|
||||
'#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6',
|
||||
'#06b6d4', '#84cc16', '#f97316', '#ec4899', '#6366f1'
|
||||
],
|
||||
borderColor: '#ffffff',
|
||||
borderWidth: 2
|
||||
}]
|
||||
};
|
||||
|
||||
this.charts.devices = new Chart(ctx, {
|
||||
type: 'doughnut',
|
||||
data: chartData,
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'bottom',
|
||||
labels: {
|
||||
padding: 20,
|
||||
usePointStyle: true
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
const device = onlineDevices[context.dataIndex];
|
||||
return `${device.name}: ${device.power}W`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateDeviceList(devices) {
|
||||
const container = document.getElementById('deviceList');
|
||||
|
||||
if (devices.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="text-center py-8 text-slate-500 dark:text-slate-400">
|
||||
<svg class="w-12 h-12 mx-auto mb-4 opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.172 16.172a4 4 0 015.656 0M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
||||
</svg>
|
||||
<p>Keine Energiemonitoring-Geräte gefunden</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = devices.map(device => `
|
||||
<div class="flex items-center justify-between p-4 bg-white/60 dark:bg-slate-700/60 rounded-xl border border-slate-200 dark:border-slate-600">
|
||||
<div class="flex items-center space-x-4">
|
||||
<div class="device-status-indicator ${device.online ? 'device-status-online' : 'device-status-offline'}"></div>
|
||||
<div>
|
||||
<h4 class="font-semibold text-slate-900 dark:text-white">${device.name}</h4>
|
||||
<p class="text-sm text-slate-500 dark:text-slate-400">ID: ${device.id}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<div class="text-xl font-bold text-slate-900 dark:text-white">
|
||||
${device.power}W
|
||||
</div>
|
||||
<div class="text-sm text-slate-500 dark:text-slate-400">
|
||||
${device.online ? 'Online' : 'Offline'}
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-16">
|
||||
<div class="power-gradient"></div>
|
||||
<div class="text-xs text-center mt-1 text-slate-500 dark:text-slate-400">
|
||||
${Math.round((device.power / 100) * 100)}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
startAutoUpdate() {
|
||||
// Update alle 30 Sekunden
|
||||
this.updateInterval = setInterval(() => {
|
||||
this.loadDeviceData(); // Nur Live-Daten für bessere Performance
|
||||
}, 30000);
|
||||
}
|
||||
|
||||
stopAutoUpdate() {
|
||||
if (this.updateInterval) {
|
||||
clearInterval(this.updateInterval);
|
||||
this.updateInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
async exportData() {
|
||||
try {
|
||||
const response = await fetch(`/api/energy/statistics?period=${this.currentPeriod}&format=csv`);
|
||||
if (!response.ok) throw new Error('Export fehlgeschlagen');
|
||||
|
||||
const blob = await response.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `energy-monitoring-${this.currentPeriod}-${new Date().toISOString().split('T')[0]}.csv`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
window.URL.revokeObjectURL(url);
|
||||
|
||||
this.showSuccess('Daten erfolgreich exportiert');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Export-Fehler:', error);
|
||||
this.showError('Fehler beim Exportieren der Daten');
|
||||
}
|
||||
}
|
||||
|
||||
showLoading() {
|
||||
document.getElementById('loadingOverlay').classList.remove('hidden');
|
||||
}
|
||||
|
||||
hideLoading() {
|
||||
document.getElementById('loadingOverlay').classList.add('hidden');
|
||||
}
|
||||
|
||||
showError(message) {
|
||||
// Verwende bestehende Flash-Message-Funktion falls verfügbar
|
||||
if (typeof showFlashMessage === 'function') {
|
||||
showFlashMessage(message, 'error');
|
||||
} else {
|
||||
alert(message);
|
||||
}
|
||||
}
|
||||
|
||||
showSuccess(message) {
|
||||
if (typeof showFlashMessage === 'function') {
|
||||
showFlashMessage(message, 'success');
|
||||
} else {
|
||||
alert(message);
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.stopAutoUpdate();
|
||||
|
||||
// Charts zerstören
|
||||
Object.values(this.charts).forEach(chart => {
|
||||
if (chart) chart.destroy();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Dashboard initialisieren
|
||||
let energyDashboard;
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
energyDashboard = new EnergyMonitoringDashboard();
|
||||
});
|
||||
|
||||
// Cleanup bei Seitenwechsel
|
||||
window.addEventListener('beforeunload', () => {
|
||||
if (energyDashboard) {
|
||||
energyDashboard.destroy();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
@ -660,6 +660,204 @@ class TapoController:
|
||||
tapo_logger.error(f"❌ Fehler beim Speichern der Steckdose {ip_address} in Datenbank: {str(e)}")
|
||||
return False
|
||||
|
||||
def _collect_device_info(self, p110, device_info):
|
||||
"""
|
||||
Sammelt erweiterte Geräteinformationen einschließlich Energiedaten.
|
||||
|
||||
Args:
|
||||
p110: PyP110 Instanz
|
||||
device_info: Basis-Geräteinformationen
|
||||
|
||||
Returns:
|
||||
Dict: Erweiterte Geräteinformationen
|
||||
"""
|
||||
extra_info = {}
|
||||
|
||||
try:
|
||||
# Firmware-Version extrahieren
|
||||
if 'fw_ver' in device_info.get('result', {}):
|
||||
extra_info['firmware_version'] = device_info['result']['fw_ver']
|
||||
|
||||
# Energiedaten abrufen (nur für P110)
|
||||
if 'P110' in device_info.get('result', {}).get('model', ''):
|
||||
try:
|
||||
energy_usage = p110.getEnergyUsage()
|
||||
|
||||
if energy_usage and 'result' in energy_usage:
|
||||
energy_data = energy_usage['result']
|
||||
|
||||
# Aktuelle Leistungsdaten
|
||||
extra_info['current_power'] = energy_data.get('current_power', 0) / 1000 # mW zu W
|
||||
extra_info['power_consumption'] = extra_info['current_power']
|
||||
|
||||
# Historische Energiedaten
|
||||
extra_info['today_energy'] = energy_data.get('today_energy', 0)
|
||||
extra_info['month_energy'] = energy_data.get('month_energy', 0)
|
||||
extra_info['today_runtime'] = energy_data.get('today_runtime', 0)
|
||||
extra_info['month_runtime'] = energy_data.get('month_runtime', 0)
|
||||
|
||||
# 24h Verbrauchsdaten
|
||||
extra_info['past24h'] = energy_data.get('past24h', [])
|
||||
extra_info['past30d'] = energy_data.get('past30d', [])
|
||||
extra_info['past1y'] = energy_data.get('past1y', [])
|
||||
|
||||
# Zusätzliche Metriken
|
||||
if 'voltage' in energy_data:
|
||||
extra_info['voltage'] = energy_data['voltage'] / 1000 # mV zu V
|
||||
if 'current' in energy_data:
|
||||
extra_info['current'] = energy_data['current'] / 1000 # mA zu A
|
||||
|
||||
hardware_logger.debug(f"Energiedaten erfolgreich abgerufen: {extra_info['current_power']}W")
|
||||
|
||||
except Exception as e:
|
||||
hardware_logger.warning(f"Konnte Energiedaten nicht abrufen: {str(e)}")
|
||||
|
||||
except Exception as e:
|
||||
hardware_logger.warning(f"Fehler beim Sammeln erweiterter Geräteinformationen: {str(e)}")
|
||||
|
||||
return extra_info
|
||||
|
||||
def get_energy_statistics(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Sammelt Energiestatistiken von allen P110 Steckdosen.
|
||||
|
||||
Returns:
|
||||
Dict: Aggregierte Energiestatistiken
|
||||
"""
|
||||
hardware_logger.info("🔋 Sammle Energiestatistiken von allen P110 Steckdosen...")
|
||||
|
||||
try:
|
||||
db_session = get_db_session()
|
||||
printers = db_session.query(Printer).filter(
|
||||
Printer.active == True,
|
||||
Printer.plug_ip.isnot(None)
|
||||
).all()
|
||||
|
||||
statistics = {
|
||||
'total_devices': 0,
|
||||
'online_devices': 0,
|
||||
'total_current_power': 0.0,
|
||||
'total_today_energy': 0.0,
|
||||
'total_month_energy': 0.0,
|
||||
'devices': [],
|
||||
'hourly_consumption': [0] * 24, # 24 Stunden
|
||||
'daily_consumption': [0] * 30, # 30 Tage
|
||||
'monthly_consumption': [0] * 12, # 12 Monate
|
||||
'timestamp': datetime.now().isoformat()
|
||||
}
|
||||
|
||||
for printer in printers:
|
||||
device_stats = {
|
||||
'id': printer.id,
|
||||
'name': printer.name,
|
||||
'location': printer.location,
|
||||
'model': printer.model,
|
||||
'ip': printer.plug_ip,
|
||||
'online': False,
|
||||
'current_power': 0.0,
|
||||
'today_energy': 0.0,
|
||||
'month_energy': 0.0,
|
||||
'voltage': 0.0,
|
||||
'current': 0.0,
|
||||
'past24h': [],
|
||||
'past30d': [],
|
||||
'past1y': []
|
||||
}
|
||||
|
||||
statistics['total_devices'] += 1
|
||||
|
||||
try:
|
||||
# P110 Energiedaten abrufen
|
||||
p110 = PyP100.P110(printer.plug_ip, self.username, self.password)
|
||||
p110.handshake()
|
||||
p110.login()
|
||||
|
||||
# Geräteinformationen
|
||||
device_info = p110.getDeviceInfo()
|
||||
if not device_info or 'result' not in device_info:
|
||||
continue
|
||||
|
||||
# Nur P110 Geräte verarbeiten
|
||||
if 'P110' not in device_info['result'].get('model', ''):
|
||||
continue
|
||||
|
||||
# Energiedaten abrufen
|
||||
energy_usage = p110.getEnergyUsage()
|
||||
if energy_usage and 'result' in energy_usage:
|
||||
energy_data = energy_usage['result']
|
||||
|
||||
device_stats['online'] = True
|
||||
device_stats['current_power'] = energy_data.get('current_power', 0) / 1000 # mW zu W
|
||||
device_stats['today_energy'] = energy_data.get('today_energy', 0)
|
||||
device_stats['month_energy'] = energy_data.get('month_energy', 0)
|
||||
device_stats['past24h'] = energy_data.get('past24h', [])
|
||||
device_stats['past30d'] = energy_data.get('past30d', [])
|
||||
device_stats['past1y'] = energy_data.get('past1y', [])
|
||||
|
||||
# Aggregierte Werte
|
||||
statistics['online_devices'] += 1
|
||||
statistics['total_current_power'] += device_stats['current_power']
|
||||
statistics['total_today_energy'] += device_stats['today_energy']
|
||||
statistics['total_month_energy'] += device_stats['month_energy']
|
||||
|
||||
# Stündliche Daten aggregieren (letzten 24h)
|
||||
if device_stats['past24h']:
|
||||
for i, hourly_value in enumerate(device_stats['past24h'][:24]):
|
||||
if i < len(statistics['hourly_consumption']):
|
||||
statistics['hourly_consumption'][i] += hourly_value
|
||||
|
||||
# Tägliche Daten aggregieren (letzten 30 Tage)
|
||||
if device_stats['past30d']:
|
||||
for i, daily_value in enumerate(device_stats['past30d'][:30]):
|
||||
if i < len(statistics['daily_consumption']):
|
||||
statistics['daily_consumption'][i] += daily_value
|
||||
|
||||
# Monatliche Daten aggregieren (letzten 12 Monate)
|
||||
if device_stats['past1y']:
|
||||
for i, monthly_value in enumerate(device_stats['past1y'][:12]):
|
||||
if i < len(statistics['monthly_consumption']):
|
||||
statistics['monthly_consumption'][i] += monthly_value
|
||||
|
||||
hardware_logger.debug(f"✅ Energiedaten für {printer.name}: {device_stats['current_power']}W")
|
||||
|
||||
except Exception as e:
|
||||
hardware_logger.warning(f"⚠️ Konnte Energiedaten für {printer.name} nicht abrufen: {str(e)}")
|
||||
|
||||
statistics['devices'].append(device_stats)
|
||||
|
||||
db_session.close()
|
||||
|
||||
# Durchschnittswerte berechnen
|
||||
if statistics['online_devices'] > 0:
|
||||
statistics['avg_current_power'] = statistics['total_current_power'] / statistics['online_devices']
|
||||
statistics['avg_today_energy'] = statistics['total_today_energy'] / statistics['online_devices']
|
||||
statistics['avg_month_energy'] = statistics['total_month_energy'] / statistics['online_devices']
|
||||
else:
|
||||
statistics['avg_current_power'] = 0.0
|
||||
statistics['avg_today_energy'] = 0.0
|
||||
statistics['avg_month_energy'] = 0.0
|
||||
|
||||
hardware_logger.info(f"✅ Energiestatistiken erfolgreich gesammelt: {statistics['online_devices']}/{statistics['total_devices']} Geräte online")
|
||||
hardware_logger.info(f"📊 Gesamtverbrauch: {statistics['total_current_power']:.1f}W aktuell, {statistics['total_today_energy']}Wh heute")
|
||||
|
||||
return statistics
|
||||
|
||||
except Exception as e:
|
||||
hardware_logger.error(f"❌ Fehler beim Sammeln der Energiestatistiken: {str(e)}")
|
||||
return {
|
||||
'total_devices': 0,
|
||||
'online_devices': 0,
|
||||
'total_current_power': 0.0,
|
||||
'total_today_energy': 0.0,
|
||||
'total_month_energy': 0.0,
|
||||
'devices': [],
|
||||
'hourly_consumption': [0] * 24,
|
||||
'daily_consumption': [0] * 30,
|
||||
'monthly_consumption': [0] * 12,
|
||||
'timestamp': datetime.now().isoformat(),
|
||||
'error': str(e)
|
||||
}
|
||||
|
||||
# ===== PRINTER MONITOR =====
|
||||
|
||||
class PrinterMonitor:
|
||||
|
Reference in New Issue
Block a user