From 33e3200523c8e5c67c1ffdf8bdea25f9c5220b8c Mon Sep 17 00:00:00 2001 From: Till Tomczak Date: Thu, 12 Jun 2025 19:54:56 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=89=20Improved=20Energy=20Monitoring?= =?UTF-8?q?=20Dashboard=20&=20Backend=20Integration=20=F0=9F=8C=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app.py | 5 + backend/blueprints/energy_monitoring.py | 436 +++++++++++++++++ backend/templates/energy_dashboard.html | 602 ++++++++++++++++++++++++ backend/utils/hardware_integration.py | 198 ++++++++ 4 files changed, 1241 insertions(+) create mode 100644 backend/blueprints/energy_monitoring.py create mode 100644 backend/templates/energy_dashboard.html diff --git a/backend/app.py b/backend/app.py index e7e54e635..51674c2f6 100644 --- a/backend/app.py +++ b/backend/app.py @@ -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) diff --git a/backend/blueprints/energy_monitoring.py b/backend/blueprints/energy_monitoring.py new file mode 100644 index 000000000..0974a5292 --- /dev/null +++ b/backend/blueprints/energy_monitoring.py @@ -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/") +@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/", 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") \ No newline at end of file diff --git a/backend/templates/energy_dashboard.html b/backend/templates/energy_dashboard.html new file mode 100644 index 000000000..6b08f7f2f --- /dev/null +++ b/backend/templates/energy_dashboard.html @@ -0,0 +1,602 @@ +{% extends "base.html" %} +{% block title %}Energiemonitoring - Mercedes-Benz MYP Platform{% endblock %} + +{% block head %} + +{% endblock %} + +{% block content %} +
+ + +
+
+
+
+

🔋 Energiemonitoring

+

+ Überwachen Sie den Energieverbrauch Ihrer 3D-Drucker in Echtzeit +

+
+
+ + +
+
+
+
+ +
+ + +
+ +
+
+
+ + + +
+
+
{{ stats.total_current_power or 0 }}W
+
Gesamtverbrauch
+
+
+
+
+ Live-Daten +
+
+ + +
+
+
+ + + +
+
+
{{ stats.online_devices or 0 }}
+
Online Geräte
+
+
+
+ von {{ stats.total_devices or 0 }} Geräten +
+
+ + +
+
+
+ + + +
+
+
{{ stats.total_today_energy or 0 }}Wh
+
Heute
+
+
+
+ Ø {{ stats.avg_today_energy or 0 }}Wh pro Gerät +
+
+ + +
+
+
+ + + +
+
+
{{ stats.total_month_energy or 0 }}Wh
+
Diesen Monat
+
+
+
+ Ø {{ stats.avg_month_energy or 0 }}Wh pro Gerät +
+
+
+ + +
+ +
+
+

📈 Verbrauchstrend

+ +
+
+ +
+
+ + +
+
+

🔌 Geräteverbrauch

+
Live-Verbrauch
+
+
+ +
+
+
+ + +
+

🖨️ Geräteübersicht

+
+ +
+
+
+
+
+ +
+
+ + + +{% endblock %} + +{% block scripts %} + + + + +{% endblock %} \ No newline at end of file diff --git a/backend/utils/hardware_integration.py b/backend/utils/hardware_integration.py index dd9342438..f3599f66b 100644 --- a/backend/utils/hardware_integration.py +++ b/backend/utils/hardware_integration.py @@ -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: