🎉 New Feature: Integrated advanced data management capabilities with improved job queue system for seamless workflow. 📚 The updated data management module now offers robust features such as data validation, normalization, and efficient storage using optimized database queries. This ensures accurate and consistent data handling across the application. 💄 Additionally, the job queue system has been upgraded to handle complex tasks more efficiently, reducing latency and improving overall
594 lines
22 KiB
HTML
594 lines
22 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}Energiemonitoring - Mercedes-Benz MYP Platform{% endblock %}
|
|
|
|
{% block head %}
|
|
<style>
|
|
/* Energiemonitoring Dashboard Styles */
|
|
.energy-card {
|
|
@apply glass card;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
|
|
.energy-card:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: var(--shadow-xl);
|
|
}
|
|
|
|
.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="main-offset min-h-screen">
|
|
|
|
<!-- Header Section -->
|
|
<div class="glass mb-8">
|
|
<div class="max-w-7xl mx-auto">
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<h1 class="text-3xl font-bold text-primary mb-2">🔋 Energiemonitoring</h1>
|
|
<p class="text-secondary">
|
|
Überwachen Sie den Energieverbrauch Ihrer 3D-Drucker in Echtzeit
|
|
</p>
|
|
</div>
|
|
<div class="flex items-center space-x-4">
|
|
<button id="refreshData" class="btn btn-primary">
|
|
<i class="fas fa-sync-alt mr-2"></i>
|
|
Aktualisieren
|
|
</button>
|
|
<button id="exportData" class="btn">
|
|
<i class="fas fa-download mr-2"></i>
|
|
Export
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
|
|
<!-- 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">
|
|
<div class="flex items-center justify-between mb-4">
|
|
<div class="p-3 bg-gradient-to-br from-blue-500 to-blue-600 rounded-2xl shadow-lg">
|
|
<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">
|
|
<div class="flex items-center justify-between mb-4">
|
|
<div class="p-3 bg-gradient-to-br from-green-500 to-green-600 rounded-2xl shadow-lg">
|
|
<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">
|
|
<div class="flex items-center justify-between mb-4">
|
|
<div class="p-3 bg-gradient-to-br from-purple-500 to-purple-600 rounded-2xl shadow-lg">
|
|
<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">
|
|
<div class="flex items-center justify-between mb-4">
|
|
<div class="p-3 bg-gradient-to-br from-orange-500 to-orange-600 rounded-2xl shadow-lg">
|
|
<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-2xl text-sm shadow-lg transition-all duration-300 hover:shadow-xl">
|
|
<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">
|
|
<div class="flex items-center justify-between mb-6">
|
|
<h3 class="text-xl font-semibold text-primary">🔌 Geräteverbrauch</h3>
|
|
<div class="text-sm text-muted">Live-Verbrauch</div>
|
|
</div>
|
|
<div class="chart-container">
|
|
<canvas id="deviceChart"></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Device List -->
|
|
<div class="energy-card mb-8">
|
|
<h3 class="text-xl font-semibold text-primary 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="spinner w-8 h-8"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Loading Overlay -->
|
|
<div id="loadingOverlay" class="modal-overlay hidden">
|
|
<div class="glass p-6 flex items-center space-x-4">
|
|
<div class="spinner w-8 h-8"></div>
|
|
<span class="text-primary">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-muted">
|
|
<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="glass-card hover-lift">
|
|
<div class="flex items-center justify-between">
|
|
<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-primary">${device.name}</h4>
|
|
<p class="text-sm text-muted">ID: ${device.id}</p>
|
|
</div>
|
|
</div>
|
|
<div class="text-right">
|
|
<div class="text-xl font-bold text-primary">
|
|
${device.power}W
|
|
</div>
|
|
<div class="text-sm text-muted">
|
|
${device.online ? 'Online' : 'Offline'}
|
|
</div>
|
|
</div>
|
|
<div class="w-16">
|
|
<div class="power-gradient"></div>
|
|
<div class="text-xs text-center mt-1 text-muted">
|
|
${Math.round((device.power / 100) * 100)}%
|
|
</div>
|
|
</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 %} |