@ -1,15 +1,17 @@
import html
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
import httpx
# === PARAMETER START ===
METER_NUMBER = " Any - Als Parameter "
CHART_TYPE = " line "
GROUP_BY = " hour "
INCLUDE_STATISTICS = True
# === PARAMETER ENDE ===
# GraphQL Queries
FIND_SENSORS_QUERY = """
query FindSensors ( $ meterNumber : String ! ) {
sensorsForMeterNumber ( meterNumber : $ meterNumber ) {
@ -63,111 +65,138 @@ 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 ) :
""" 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 }
json = { " query " : query , " variables " : variables } ,
timeout = 30.0 ,
)
response . raise_for_status ( )
return response . json ( )
except Exception as e :
return { " errors " : [ str ( e ) ] }
except Exception as exc :
return { " errors " : [ str ( exc ) ] }
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 ]
values = [ obs . get ( " value " ) or 0 for obs in observations ]
meter_values = [ obs . get ( " meterValue " ) or 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
" 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 :
dt = datetime . fromisoformat ( dt_string . replace ( " Z " , " +00:00 " ) )
return dt . strftime ( " % Y- % m- %d % H: % M: % S " )
except Exception :
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
} )
def build_report ( ) :
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 ' , [ ] )
return build_message (
f " Fehler beim Abrufen der Sensoren: { sensors_response [ ' errors ' ] } " ,
" error " ,
)
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 ' ]
return build_message (
f " Keine Sensoren fuer Zaehlernummer ' { METER_NUMBER } ' gefunden. " ,
" warning " ,
)
# 2. Verfügbare Variablen abrufen
variables_response = make_graphql_request ( GET_VARIABLES_QUERY , {
" sensorId " : sensor_id
} )
sensor = sensors [ 0 ]
sensor_id = sensor [ " sensorId " ]
sensor_name = sensor [ " sensorName " ]
measure_concept = sensor . get ( " measureConcept " ) or { }
measure_concept_id = measure_concept . get ( " id " )
measure_concept_name = measure_concept . get ( " name " ) or " Unbekannt "
available_vars = variables_response . get ( ' data ' , { } ) . get ( ' availableVariableUnits ' , [ ] )
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 , {
observations_response = make_graphql_request (
FIND_OBSERVATIONS_QUERY ,
{
" measurementConceptId " : measure_concept_id ,
" sensorName " : sensor_name ,
" startTime " : start_time . isoformat ( ) ,
" endTime " : end_time . isoformat ( )
} )
" endTime " : end_time . isoformat ( ) ,
} ,
)
observations = observations_response . get ( ' data ' , { } ) . get ( ' findObservation ' , [ ] )
if " errors " in observations_response :
return build_message (
f " Fehler beim Abrufen der Messwerte: { observations_response [ ' errors ' ] } " ,
" error " ,
)
# 5. Statistiken berechnen
observations = observations_response . get ( " data " , { } ) . get ( " findObservation " , [ ] )
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 ) )
chart_labels . append ( format_datetime ( obs . get ( " moment " , " " ) ) )
chart_values . append ( obs . get ( " value " ) or 0 )
chart_meter_values . append ( obs . get ( " meterValue " ) or 0 )
safe_sensor_name = escape_text ( sensor_name , " Unbekannt " )
safe_meter_number = escape_text ( METER_NUMBER )
safe_measure_concept_name = escape_text ( measure_concept_name , " Unbekannt " )
# 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 >
< title > Messwerte Dashboard - { safe_ sensor_name} < / title >
< script src = " https://cdn.jsdelivr.net/npm/chart.js " > < / script >
< style >
* { {
@ -252,22 +281,28 @@ try:
border - radius : 10 px ;
margin - top : 20 px ;
} }
. warning { { background : #fef3cd; color: #856404; }}
. error { { background : #f8d7da; color: #721c24; }}
. 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 : { s ensor_name} | Zählernummer : { METER_NUMBER } < / div >
< h1 > Messwerte Dashboard < / h1 >
< div class = " subtitle " > Sensor : { s afe_sensor_name} | Zaehlernummer : { safe_meter_number } < / div >
< div class = " subtitle " > Zeitraum : { start_time . strftime ( ' %d . % m. % Y ' ) } - { end_time . strftime ( ' %d . % m. % Y ' ) } < / div >
< div class = " subtitle " > Gruppierung : { escape_text ( GROUP_BY ) } < / div >
< / div >
< div class = " content " >
"""
# Statistiken hinzufügen
if INCLUDE_STATISTICS and stats :
result_html + = f """
< div class = " stats-grid " >
@ -281,53 +316,52 @@ try:
< / div >
< div class = " stat-card " >
< div class = " stat-value " > { stats [ ' value_avg ' ] : , .2 f } < / div >
< div class = " stat-label " > Ø Messwert < / div >
< div class = " stat-label " > Durchschnitt Messwert < / div >
< / div >
< div class = " stat-card " >
< div class = " stat-value " > { stats [ ' meter_max ' ] : , .2 f } < / div >
< div class = " stat-label " > Max Z ä hlerstand< / div >
< div class = " stat-label " > Max Z ae hlerstand< / div >
< / div >
< / div >
"""
# Chart hinzufügen
result_html + = f """
< div class = " chart-container " >
< h2 class = " chart-title " > Messwerte Verlauf ( { CHART_TYPE. title ( ) } ) < / h2 >
< 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 " > Z ählerstä nde< / h2 >
< h2 class = " chart-title " > Z aehlerstae nde< / h2 >
< canvas id = " meterChart " width = " 400 " height = " 200 " > < / canvas >
< / div >
< div class = " info-box " >
< h3 > Verfü gbare Variablen : < / h3 >
< h3 > Verfue gbare Variablen : < / h3 >
< ul >
"""
for var in available_vars :
result_html + = f " <li> { var [ ' variableName ' ] } ( { var [ ' unitName ' ] } )</li> "
variable_name = escape_text ( var . get ( " variableName " ) , " Unbekannt " )
unit_name = escape_text ( var . get ( " unitName " ) , " - " )
result_html + = f " <li> { variable_name } ( { unit_name } )</li> \n "
result_html + = f """
< / ul >
< p > < strong > Anzahl Sensoren gefunden : < / strong > { len ( sensors ) } < / p >
< p > < strong > Measure Concept : < / strong > { s ensor[ ' measureConcept ' ] [ ' name ' ] } < / p >
< p > < strong > Measure Concept : < / strong > { s afe_measure_concept_name } < / p >
< / div >
< / div >
< / div >
< script >
/ / Chart . js Konfiguration
const chartLabels = { json . dumps ( chart_labels [ - 100 : ] ) } ; / / Nur letzten 100 Werte
const chartLabels = { json . dumps ( chart_labels [ - 100 : ] ) } ;
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} ' ,
type : { json . dumps ( CHART_TYPE ) } ,
data : { {
labels : chartLabels ,
datasets : [ { {
@ -363,14 +397,13 @@ try:
} }
} } ) ;
/ / Zählerstand Chart
const ctx2 = document . getElementById ( ' meterChart ' ) . getContext ( ' 2d ' ) ;
new Chart ( ctx2 , { {
type : ' line ' ,
data : { {
labels : chartLabels ,
datasets : [ { {
label : ' Zä hlerstand' ,
label : ' Zae hlerstand' ,
data : chartMeterValues ,
borderColor : ' rgb(34, 197, 94) ' ,
backgroundColor : ' rgba(34, 197, 94, 0.1) ' ,
@ -406,9 +439,12 @@ try:
< / html >
"""
result = result_html
return result_html
except Exception as e :
result = f " <div class= ' error ' >Allgemeiner Fehler: { str ( e ) } </div> "
try :
result = build_report ( )
except Exception as exc :
result = build_message ( f " Allgemeiner Fehler: { exc } " , " error " )
print ( f " Generated HTML dashboard with { len ( chart_labels ) if ' chart_labels ' in locals ( ) else 0 } data points " )
print ( result )