deploy script: meter_reading_dashboard
parent
ec64e42d05
commit
5daaa84f7e
@ -0,0 +1,414 @@
|
||||
import json
|
||||
import httpx
|
||||
from datetime import datetime, timedelta
|
||||
import math
|
||||
|
||||
# Parameter - diese können später als Eingabe konfiguriert werden
|
||||
METER_NUMBER = "Any - Als Parameter" # Wird durch tatsächliche Zählernummer ersetzt
|
||||
CHART_TYPE = "line" # line, bar, area
|
||||
GROUP_BY = "hour" # hour, day, week
|
||||
INCLUDE_STATISTICS = True
|
||||
|
||||
# GraphQL Queries
|
||||
FIND_SENSORS_QUERY = """
|
||||
query FindSensors($meterNumber: String!) {
|
||||
sensorsForMeterNumber(meterNumber: $meterNumber) {
|
||||
sensorId
|
||||
sensorName
|
||||
sensorNameExtern
|
||||
descr
|
||||
measureConcept {
|
||||
id
|
||||
name
|
||||
descr
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
GET_VARIABLES_QUERY = """
|
||||
query GetVariables($sensorId: ID!) {
|
||||
availableVariableUnits(sensorId: $sensorId) {
|
||||
variableUnitId
|
||||
variableName
|
||||
unitName
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
FIND_OBSERVATIONS_QUERY = """
|
||||
query FindObservations($measurementConceptId: ID!, $sensorName: String, $startTime: String, $endTime: String) {
|
||||
findObservation(
|
||||
measurementConceptId: $measurementConceptId
|
||||
sensorName: $sensorName
|
||||
startTime: $startTime
|
||||
endTime: $endTime
|
||||
) {
|
||||
id
|
||||
moment
|
||||
value
|
||||
meterValue
|
||||
observationVariableUnit {
|
||||
observationVariable {
|
||||
name
|
||||
description
|
||||
}
|
||||
unit {
|
||||
name
|
||||
description
|
||||
}
|
||||
}
|
||||
quality
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
def make_graphql_request(query, variables):
|
||||
"""GraphQL Request ausführen"""
|
||||
try:
|
||||
with httpx.Client() as client:
|
||||
response = client.post(
|
||||
f"{EXTERNAL_BASE_URL}/graphql",
|
||||
headers=AUTH_HEADERS,
|
||||
json={"query": query, "variables": variables}
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except Exception as e:
|
||||
return {"errors": [str(e)]}
|
||||
|
||||
def calculate_statistics(observations):
|
||||
"""Berechnet Statistiken für die Messwerte"""
|
||||
if not observations:
|
||||
return {}
|
||||
|
||||
values = [obs.get('value', 0) for obs in observations]
|
||||
meter_values = [obs.get('meterValue', 0) for obs in observations]
|
||||
|
||||
return {
|
||||
'total_readings': len(observations),
|
||||
'value_min': min(values) if values else 0,
|
||||
'value_max': max(values) if values else 0,
|
||||
'value_avg': sum(values) / len(values) if values else 0,
|
||||
'meter_min': min(meter_values) if meter_values else 0,
|
||||
'meter_max': max(meter_values) if meter_values else 0,
|
||||
'meter_avg': sum(meter_values) / len(meter_values) if meter_values else 0,
|
||||
'consumption': max(meter_values) - min(meter_values) if meter_values else 0
|
||||
}
|
||||
|
||||
def format_datetime(dt_string):
|
||||
"""Formatiert DateTime für Chart.js"""
|
||||
try:
|
||||
dt = datetime.fromisoformat(dt_string.replace('Z', '+00:00'))
|
||||
return dt.strftime('%Y-%m-%d %H:%M:%S')
|
||||
except:
|
||||
return dt_string
|
||||
|
||||
# Hauptlogik
|
||||
result_html = ""
|
||||
|
||||
try:
|
||||
# 1. Sensoren für Zählernummer finden
|
||||
sensors_response = make_graphql_request(FIND_SENSORS_QUERY, {
|
||||
"meterNumber": METER_NUMBER
|
||||
})
|
||||
|
||||
if "errors" in sensors_response:
|
||||
result = f"<div class='error'>Fehler beim Abrufen der Sensoren: {sensors_response['errors']}</div>"
|
||||
else:
|
||||
sensors = sensors_response.get('data', {}).get('sensorsForMeterNumber', [])
|
||||
|
||||
if not sensors:
|
||||
result = f"<div class='warning'>Keine Sensoren für Zählernummer '{METER_NUMBER}' gefunden.</div>"
|
||||
else:
|
||||
sensor = sensors[0] # Ersten Sensor verwenden
|
||||
sensor_id = sensor['sensorId']
|
||||
sensor_name = sensor['sensorName']
|
||||
measure_concept_id = sensor['measureConcept']['id']
|
||||
|
||||
# 2. Verfügbare Variablen abrufen
|
||||
variables_response = make_graphql_request(GET_VARIABLES_QUERY, {
|
||||
"sensorId": sensor_id
|
||||
})
|
||||
|
||||
available_vars = variables_response.get('data', {}).get('availableVariableUnits', [])
|
||||
|
||||
# 3. Zeitraum: Letzten 3 Monate
|
||||
end_time = datetime.now()
|
||||
start_time = end_time - timedelta(days=90)
|
||||
|
||||
# 4. Messwerte abrufen
|
||||
observations_response = make_graphql_request(FIND_OBSERVATIONS_QUERY, {
|
||||
"measurementConceptId": measure_concept_id,
|
||||
"sensorName": sensor_name,
|
||||
"startTime": start_time.isoformat(),
|
||||
"endTime": end_time.isoformat()
|
||||
})
|
||||
|
||||
observations = observations_response.get('data', {}).get('findObservation', [])
|
||||
|
||||
# 5. Statistiken berechnen
|
||||
stats = calculate_statistics(observations)
|
||||
|
||||
# 6. Daten für Chart vorbereiten
|
||||
chart_labels = []
|
||||
chart_values = []
|
||||
chart_meter_values = []
|
||||
|
||||
for obs in observations:
|
||||
chart_labels.append(format_datetime(obs['moment']))
|
||||
chart_values.append(obs.get('value', 0))
|
||||
chart_meter_values.append(obs.get('meterValue', 0))
|
||||
|
||||
# 7. HTML mit Chart.js generieren
|
||||
result_html = f"""
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Messwerte Dashboard - {sensor_name}</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<style>
|
||||
* {{
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}}
|
||||
body {{
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}}
|
||||
.dashboard {{
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
|
||||
overflow: hidden;
|
||||
}}
|
||||
.header {{
|
||||
background: linear-gradient(90deg, var(--color-primary, #4f46e5) 0%, #7c3aed 100%);
|
||||
color: white;
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
}}
|
||||
.header h1 {{
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 10px;
|
||||
}}
|
||||
.header .subtitle {{
|
||||
opacity: 0.9;
|
||||
font-size: 1.1rem;
|
||||
}}
|
||||
.content {{
|
||||
padding: 30px;
|
||||
}}
|
||||
.stats-grid {{
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 30px;
|
||||
}}
|
||||
.stat-card {{
|
||||
background: white;
|
||||
padding: 25px;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 10px 25px rgba(0,0,0,0.05);
|
||||
border-left: 5px solid var(--color-primary, #4f46e5);
|
||||
transition: transform 0.2s;
|
||||
}}
|
||||
.stat-card:hover {{
|
||||
transform: translateY(-5px);
|
||||
}}
|
||||
.stat-value {{
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
color: var(--color-primary, #4f46e5);
|
||||
margin-bottom: 5px;
|
||||
}}
|
||||
.stat-label {{
|
||||
color: #6b7280;
|
||||
font-weight: 500;
|
||||
}}
|
||||
.chart-container {{
|
||||
background: white;
|
||||
padding: 30px;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 10px 25px rgba(0,0,0,0.05);
|
||||
margin-bottom: 20px;
|
||||
}}
|
||||
.chart-title {{
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 20px;
|
||||
color: #374151;
|
||||
text-align: center;
|
||||
}}
|
||||
.info-box {{
|
||||
background: #f3f4f6;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
margin-top: 20px;
|
||||
}}
|
||||
.warning {{ background: #fef3cd; color: #856404; }}
|
||||
.error {{ background: #f8d7da; color: #721c24; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="dashboard">
|
||||
<div class="header">
|
||||
<h1>📊 Messwerte Dashboard</h1>
|
||||
<div class="subtitle">Sensor: {sensor_name} | Zählernummer: {METER_NUMBER}</div>
|
||||
<div class="subtitle">Zeitraum: {start_time.strftime('%d.%m.%Y')} - {end_time.strftime('%d.%m.%Y')}</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
"""
|
||||
|
||||
# Statistiken hinzufügen
|
||||
if INCLUDE_STATISTICS and stats:
|
||||
result_html += f"""
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">{stats['total_readings']:,}</div>
|
||||
<div class="stat-label">Gesamte Messwerte</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">{stats['consumption']:,.2f}</div>
|
||||
<div class="stat-label">Verbrauch (Differenz)</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">{stats['value_avg']:,.2f}</div>
|
||||
<div class="stat-label">Ø Messwert</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">{stats['meter_max']:,.2f}</div>
|
||||
<div class="stat-label">Max Zählerstand</div>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
# Chart hinzufügen
|
||||
result_html += f"""
|
||||
<div class="chart-container">
|
||||
<h2 class="chart-title">Messwerte Verlauf ({CHART_TYPE.title()})</h2>
|
||||
<canvas id="mainChart" width="400" height="200"></canvas>
|
||||
</div>
|
||||
|
||||
<div class="chart-container">
|
||||
<h2 class="chart-title">Zählerstände</h2>
|
||||
<canvas id="meterChart" width="400" height="200"></canvas>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<h3>Verfügbare Variablen:</h3>
|
||||
<ul>
|
||||
"""
|
||||
|
||||
for var in available_vars:
|
||||
result_html += f"<li>{var['variableName']} ({var['unitName']})</li>"
|
||||
|
||||
result_html += f"""
|
||||
</ul>
|
||||
<p><strong>Anzahl Sensoren gefunden:</strong> {len(sensors)}</p>
|
||||
<p><strong>Measure Concept:</strong> {sensor['measureConcept']['name']}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Chart.js Konfiguration
|
||||
const chartLabels = {json.dumps(chart_labels[-100:])}; // Nur letzten 100 Werte
|
||||
const chartValues = {json.dumps(chart_values[-100:])};
|
||||
const chartMeterValues = {json.dumps(chart_meter_values[-100:])};
|
||||
|
||||
// Hauptchart (Messwerte)
|
||||
const ctx1 = document.getElementById('mainChart').getContext('2d');
|
||||
new Chart(ctx1, {{
|
||||
type: '{CHART_TYPE}',
|
||||
data: {{
|
||||
labels: chartLabels,
|
||||
datasets: [{{
|
||||
label: 'Messwerte',
|
||||
data: chartValues,
|
||||
borderColor: 'rgb(79, 70, 229)',
|
||||
backgroundColor: 'rgba(79, 70, 229, 0.1)',
|
||||
borderWidth: 3,
|
||||
fill: true,
|
||||
tension: 0.4
|
||||
}}]
|
||||
}},
|
||||
options: {{
|
||||
responsive: true,
|
||||
plugins: {{
|
||||
legend: {{
|
||||
display: true,
|
||||
position: 'top'
|
||||
}}
|
||||
}},
|
||||
scales: {{
|
||||
x: {{
|
||||
display: true,
|
||||
ticks: {{
|
||||
maxTicksLimit: 10
|
||||
}}
|
||||
}},
|
||||
y: {{
|
||||
display: true,
|
||||
beginAtZero: false
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
}});
|
||||
|
||||
// Zählerstand Chart
|
||||
const ctx2 = document.getElementById('meterChart').getContext('2d');
|
||||
new Chart(ctx2, {{
|
||||
type: 'line',
|
||||
data: {{
|
||||
labels: chartLabels,
|
||||
datasets: [{{
|
||||
label: 'Zählerstand',
|
||||
data: chartMeterValues,
|
||||
borderColor: 'rgb(34, 197, 94)',
|
||||
backgroundColor: 'rgba(34, 197, 94, 0.1)',
|
||||
borderWidth: 3,
|
||||
fill: true,
|
||||
tension: 0.4
|
||||
}}]
|
||||
}},
|
||||
options: {{
|
||||
responsive: true,
|
||||
plugins: {{
|
||||
legend: {{
|
||||
display: true,
|
||||
position: 'top'
|
||||
}}
|
||||
}},
|
||||
scales: {{
|
||||
x: {{
|
||||
display: true,
|
||||
ticks: {{
|
||||
maxTicksLimit: 10
|
||||
}}
|
||||
}},
|
||||
y: {{
|
||||
display: true,
|
||||
beginAtZero: false
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
}});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
result = result_html
|
||||
|
||||
except Exception as e:
|
||||
result = f"<div class='error'>Allgemeiner Fehler: {str(e)}</div>"
|
||||
|
||||
print(f"Generated HTML dashboard with {len(chart_labels) if 'chart_labels' in locals() else 0} data points")
|
||||
Loading…
Reference in New Issue