Fix meter_reading_dashboard for standalone Piston execution

Anpassung von meter_reading_dashboard für den Piston-Workflow. Das Script liefert das generierte Dashboard jetzt direkt als HTML-Ausgabe, enthält eindeutige Marker für den automatisierbaren Parameterbereich und behandelt dynamische Inhalte sowie Fehlerfälle robuster
main
mathias.sillhengst 2 weeks ago
parent 277c2126e2
commit 28cbf1843a

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

Loading…
Cancel
Save