@ -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 ( 135 deg , #667eea 0%, #764ba2 100%);
< head >
min - height : 100 vh ;
< meta charset = " UTF-8 " >
padding : 20 px ;
< 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 : 1400 px ;
< style >
margin : 0 auto ;
* { {
background : rgba ( 255 , 255 , 255 , 0.95 ) ;
margin : 0 ;
border - radius : 20 px ;
padding : 0 ;
box - shadow : 0 20 px 40 px 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 ( 90 deg , var ( - - color - primary , #4f46e5) 0%, #7c3aed 100%);
background : linear - gradient ( 135 deg , #667eea 0%, #764ba2 100%);
color : white ;
min - height : 100 vh ;
padding : 30 px ;
padding : 20 px ;
text - align : center ;
} }
} }
. dashboard { {
. header h1 { {
max - width : 1400 px ;
font - size : 2.5 rem ;
margin : 0 auto ;
margin - bottom : 10 px ;
background : rgba ( 255 , 255 , 255 , 0.95 ) ;
} }
border - radius : 20 px ;
. header . subtitle { {
box - shadow : 0 20 px 40 px rgba ( 0 , 0 , 0 , 0.1 ) ;
opacity : 0.9 ;
overflow : hidden ;
font - size : 1.1 rem ;
} }
} }
. header { {
. content { {
background : linear - gradient ( 90 deg , var ( - - color - primary , #4f46e5) 0%, #7c3aed 100%);
padding : 30 px ;
color : white ;
} }
padding : 30 px ;
. stats - grid { {
text - align : center ;
display : grid ;
} }
grid - template - columns : repeat ( auto - fit , minmax ( 250 px , 1 fr ) ) ;
. header h1 { {
gap : 20 px ;
font - size : 2.5 rem ;
margin - bottom : 30 px ;
margin - bottom : 10 px ;
} }
} }
. stat - card { {
. header . subtitle { {
background : white ;
opacity : 0.9 ;
padding : 25 px ;
font - size : 1.1 rem ;
border - radius : 15 px ;
} }
box - shadow : 0 10 px 25 px rgba ( 0 , 0 , 0 , 0.05 ) ;
. content { {
border - left : 5 px solid var ( - - color - primary , #4f46e5);
padding : 30 px ;
transition : transform 0.2 s ;
} }
} }
. stats - grid { {
. stat - card : hover { {
display : grid ;
transform : translateY ( - 5 px ) ;
grid - template - columns : repeat ( auto - fit , minmax ( 250 px , 1 fr ) ) ;
} }
gap : 20 px ;
. stat - value { {
margin - bottom : 30 px ;
font - size : 2 rem ;
} }
font - weight : bold ;
. stat - card { {
color : var ( - - color - primary , #4f46e5);
background : white ;
margin - bottom : 5 px ;
padding : 25 px ;
} }
border - radius : 15 px ;
. stat - label { {
box - shadow : 0 10 px 25 px rgba ( 0 , 0 , 0 , 0.05 ) ;
color : #6b7280;
border - left : 5 px solid var ( - - color - primary , #4f46e5);
font - weight : 500 ;
transition : transform 0.2 s ;
} }
} }
. chart - container { {
. stat - card : hover { {
background : white ;
transform : translateY ( - 5 px ) ;
padding : 30 px ;
} }
border - radius : 15 px ;
. stat - value { {
box - shadow : 0 10 px 25 px rgba ( 0 , 0 , 0 , 0.05 ) ;
font - size : 2 rem ;
margin - bottom : 20 px ;
font - weight : bold ;
} }
color : var ( - - color - primary , #4f46e5);
. chart - title { {
margin - bottom : 5 px ;
font - size : 1.5 rem ;
} }
margin - bottom : 20 px ;
. stat - label { {
color : #374151;
color : #6b7280;
text - align : center ;
font - weight : 500 ;
} }
} }
. info - box { {
. chart - container { {
background : #f3f4f6;
background : white ;
padding : 20 px ;
padding : 30 px ;
border - radius : 10 px ;
border - radius : 15 px ;
margin - top : 20 px ;
box - shadow : 0 10 px 25 px rgba ( 0 , 0 , 0 , 0.05 ) ;
} }
margin - bottom : 20 px ;
. warning { { background : #fef3cd; color: #856404; }}
} }
. error { { background : #f8d7da; color: #721c24; }}
. chart - title { {
< / style >
font - size : 1.5 rem ;
< / head >
margin - bottom : 20 px ;
< 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 : 20 px ;
< / div >
border - radius : 10 px ;
margin - top : 20 px ;
< 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 ' ] : , .2 f } < / 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 ' ] : , .2 f } < / 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 ' ] : , .2 f } < / 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 ' ] : , .2 f } < / 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 ' ] : , .2 f } < / 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 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 )