|
|
|
@ -1,605 +0,0 @@
|
|
|
|
import json
|
|
|
|
|
|
|
|
import httpx
|
|
|
|
|
|
|
|
from datetime import datetime
|
|
|
|
|
|
|
|
import traceback
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# GraphQL Queries
|
|
|
|
|
|
|
|
FIND_SENSORS_QUERY = """
|
|
|
|
|
|
|
|
query FindSensors($meterNumber: String!) {
|
|
|
|
|
|
|
|
sensorsForMeterNumber(meterNumber: $meterNumber) {
|
|
|
|
|
|
|
|
sensorId
|
|
|
|
|
|
|
|
sensorName
|
|
|
|
|
|
|
|
sensorNameExtern
|
|
|
|
|
|
|
|
descr
|
|
|
|
|
|
|
|
measureConcept {
|
|
|
|
|
|
|
|
id
|
|
|
|
|
|
|
|
name
|
|
|
|
|
|
|
|
descr
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
GET_VARIABLE_UNITS_QUERY = """
|
|
|
|
|
|
|
|
query GetVariableUnits($sensorId: ID!) {
|
|
|
|
|
|
|
|
availableVariableUnits(sensorId: $sensorId) {
|
|
|
|
|
|
|
|
variableUnitId
|
|
|
|
|
|
|
|
variableName
|
|
|
|
|
|
|
|
unitName
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
GET_LAST_OBSERVATIONS_QUERY = """
|
|
|
|
|
|
|
|
query GetLastObservations($measurementConceptId: ID!, $sensorName: String, $observationVariableNamePattern: String, $startTime: String, $endTime: String) {
|
|
|
|
|
|
|
|
findObservation(
|
|
|
|
|
|
|
|
measurementConceptId: $measurementConceptId
|
|
|
|
|
|
|
|
sensorName: $sensorName
|
|
|
|
|
|
|
|
observationVariableNamePattern: $observationVariableNamePattern
|
|
|
|
|
|
|
|
startTime: $startTime
|
|
|
|
|
|
|
|
endTime: $endTime
|
|
|
|
|
|
|
|
) {
|
|
|
|
|
|
|
|
id
|
|
|
|
|
|
|
|
moment
|
|
|
|
|
|
|
|
value
|
|
|
|
|
|
|
|
meterValue
|
|
|
|
|
|
|
|
observationVariableUnit {
|
|
|
|
|
|
|
|
observationVariable {
|
|
|
|
|
|
|
|
name
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
unit {
|
|
|
|
|
|
|
|
name
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
RECORD_ULTIMO_MUTATION = """
|
|
|
|
|
|
|
|
mutation RecordUltimoReadings($input: UltimoReadingsInput!) {
|
|
|
|
|
|
|
|
recordUltimoReadings(input: $input) {
|
|
|
|
|
|
|
|
success
|
|
|
|
|
|
|
|
created {
|
|
|
|
|
|
|
|
id
|
|
|
|
|
|
|
|
moment
|
|
|
|
|
|
|
|
value
|
|
|
|
|
|
|
|
meterValue
|
|
|
|
|
|
|
|
observationVariableUnit {
|
|
|
|
|
|
|
|
observationVariable {
|
|
|
|
|
|
|
|
name
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
unit {
|
|
|
|
|
|
|
|
name
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
errors {
|
|
|
|
|
|
|
|
code
|
|
|
|
|
|
|
|
message
|
|
|
|
|
|
|
|
details
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def execute_graphql(query, variables=None):
|
|
|
|
|
|
|
|
"""Execute GraphQL query/mutation"""
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
|
|
response = httpx.post(
|
|
|
|
|
|
|
|
f"{EXTERNAL_BASE_URL}/graphql",
|
|
|
|
|
|
|
|
headers=AUTH_HEADERS,
|
|
|
|
|
|
|
|
json={"query": query, "variables": variables or {}},
|
|
|
|
|
|
|
|
timeout=30
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
response.raise_for_status()
|
|
|
|
|
|
|
|
data = response.json()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if "errors" in data:
|
|
|
|
|
|
|
|
raise Exception(f"GraphQL Error: {data['errors']}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return data["data"]
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
|
|
raise Exception(f"GraphQL Request failed: {str(e)}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def format_datetime(dt_str):
|
|
|
|
|
|
|
|
"""Format datetime string for display"""
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
|
|
dt = datetime.fromisoformat(dt_str.replace('T', ' '))
|
|
|
|
|
|
|
|
return dt.strftime("%d.%m.%Y %H:%M")
|
|
|
|
|
|
|
|
except:
|
|
|
|
|
|
|
|
return dt_str
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def format_number(num):
|
|
|
|
|
|
|
|
"""Format number with German locale"""
|
|
|
|
|
|
|
|
return f"{num:,.2f}".replace(',', ' ').replace('.', ',').replace(' ', '.')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Get parameters
|
|
|
|
|
|
|
|
sensor_search = PARAMS.get("sensor_search", "")
|
|
|
|
|
|
|
|
variable_name = PARAMS.get("variable_name", "ACTIVE_ENERGY_ABSORBED_10")
|
|
|
|
|
|
|
|
readings_data = PARAMS.get("readings_data", "")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
html_content = """
|
|
|
|
|
|
|
|
<!DOCTYPE html>
|
|
|
|
|
|
|
|
<html lang="de">
|
|
|
|
|
|
|
|
<head>
|
|
|
|
|
|
|
|
<meta charset="UTF-8">
|
|
|
|
|
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
|
|
|
|
|
<title>Ultimo-Zählerstände Eingabe</title>
|
|
|
|
|
|
|
|
<style>
|
|
|
|
|
|
|
|
body {
|
|
|
|
|
|
|
|
font-family: 'Segoe UI', system-ui, sans-serif;
|
|
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
|
|
padding: 20px;
|
|
|
|
|
|
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
|
|
|
|
|
|
min-height: 100vh;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.container {
|
|
|
|
|
|
|
|
max-width: 1200px;
|
|
|
|
|
|
|
|
margin: 0 auto;
|
|
|
|
|
|
|
|
background: white;
|
|
|
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
|
|
|
box-shadow: 0 8px 32px rgba(0,0,0,0.1);
|
|
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.header {
|
|
|
|
|
|
|
|
background: var(--color-primary, #2196F3);
|
|
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
|
|
padding: 30px;
|
|
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.header h1 {
|
|
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
|
|
font-size: 2.2em;
|
|
|
|
|
|
|
|
font-weight: 300;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.content {
|
|
|
|
|
|
|
|
padding: 30px;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.info-card {
|
|
|
|
|
|
|
|
background: #f8f9fa;
|
|
|
|
|
|
|
|
border-left: 4px solid var(--color-primary, #2196F3);
|
|
|
|
|
|
|
|
padding: 20px;
|
|
|
|
|
|
|
|
margin-bottom: 30px;
|
|
|
|
|
|
|
|
border-radius: 0 8px 8px 0;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.success-card {
|
|
|
|
|
|
|
|
background: #e8f5e8;
|
|
|
|
|
|
|
|
border-left: 4px solid var(--color-success, #4CAF50);
|
|
|
|
|
|
|
|
padding: 20px;
|
|
|
|
|
|
|
|
margin-bottom: 30px;
|
|
|
|
|
|
|
|
border-radius: 0 8px 8px 0;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.error-card {
|
|
|
|
|
|
|
|
background: #ffeaea;
|
|
|
|
|
|
|
|
border-left: 4px solid var(--color-danger, #f44336);
|
|
|
|
|
|
|
|
padding: 20px;
|
|
|
|
|
|
|
|
margin-bottom: 30px;
|
|
|
|
|
|
|
|
border-radius: 0 8px 8px 0;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.warning-card {
|
|
|
|
|
|
|
|
background: #fff3e0;
|
|
|
|
|
|
|
|
border-left: 4px solid var(--color-warning, #FF9800);
|
|
|
|
|
|
|
|
padding: 20px;
|
|
|
|
|
|
|
|
margin-bottom: 30px;
|
|
|
|
|
|
|
|
border-radius: 0 8px 8px 0;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.data-table {
|
|
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
|
|
border-collapse: collapse;
|
|
|
|
|
|
|
|
margin-top: 20px;
|
|
|
|
|
|
|
|
background: white;
|
|
|
|
|
|
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.data-table th {
|
|
|
|
|
|
|
|
background: var(--color-primary, #2196F3);
|
|
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
|
|
padding: 15px;
|
|
|
|
|
|
|
|
text-align: left;
|
|
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.data-table td {
|
|
|
|
|
|
|
|
padding: 12px 15px;
|
|
|
|
|
|
|
|
border-bottom: 1px solid #e0e0e0;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.data-table tr:hover {
|
|
|
|
|
|
|
|
background: #f5f5f5;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.data-table tr:last-child td {
|
|
|
|
|
|
|
|
border-bottom: none;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.value-cell {
|
|
|
|
|
|
|
|
text-align: right;
|
|
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.current-value {
|
|
|
|
|
|
|
|
background: #e8f5e8;
|
|
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.sensor-info {
|
|
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
|
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
|
|
|
|
|
|
|
gap: 20px;
|
|
|
|
|
|
|
|
margin-bottom: 30px;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.sensor-card {
|
|
|
|
|
|
|
|
background: #f8f9fa;
|
|
|
|
|
|
|
|
padding: 20px;
|
|
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
|
|
border: 1px solid #e0e0e0;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.sensor-card h3 {
|
|
|
|
|
|
|
|
margin-top: 0;
|
|
|
|
|
|
|
|
color: var(--color-primary, #2196F3);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.error-details {
|
|
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
|
|
border: 1px solid #ffcdd2;
|
|
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
|
|
padding: 15px;
|
|
|
|
|
|
|
|
margin-top: 15px;
|
|
|
|
|
|
|
|
font-family: monospace;
|
|
|
|
|
|
|
|
font-size: 0.9em;
|
|
|
|
|
|
|
|
white-space: pre-wrap;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.statistics {
|
|
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
|
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
|
|
|
|
|
|
gap: 20px;
|
|
|
|
|
|
|
|
margin: 30px 0;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.stat-card {
|
|
|
|
|
|
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
|
|
padding: 25px;
|
|
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.stat-number {
|
|
|
|
|
|
|
|
font-size: 2.5em;
|
|
|
|
|
|
|
|
font-weight: 300;
|
|
|
|
|
|
|
|
margin: 10px 0;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.stat-label {
|
|
|
|
|
|
|
|
font-size: 0.9em;
|
|
|
|
|
|
|
|
opacity: 0.9;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
</style>
|
|
|
|
|
|
|
|
</head>
|
|
|
|
|
|
|
|
<body>
|
|
|
|
|
|
|
|
<div class="container">
|
|
|
|
|
|
|
|
<div class="header">
|
|
|
|
|
|
|
|
<h1>🔌 Ultimo-Zählerstände Eingabe</h1>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="content">
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
|
|
# Step 1: Find sensors
|
|
|
|
|
|
|
|
if not sensor_search:
|
|
|
|
|
|
|
|
html_content += """
|
|
|
|
|
|
|
|
<div class="warning-card">
|
|
|
|
|
|
|
|
<h3>⚠️ Keine Zählernummer angegeben</h3>
|
|
|
|
|
|
|
|
<p>Bitte geben Sie eine Zählernummer ein, um fortzufahren.</p>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
# Search for sensors
|
|
|
|
|
|
|
|
sensor_data = execute_graphql(FIND_SENSORS_QUERY, {"meterNumber": sensor_search})
|
|
|
|
|
|
|
|
sensors = sensor_data.get("sensorsForMeterNumber", [])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if not sensors:
|
|
|
|
|
|
|
|
html_content += f"""
|
|
|
|
|
|
|
|
<div class="error-card">
|
|
|
|
|
|
|
|
<h3>❌ Keine Sensoren gefunden</h3>
|
|
|
|
|
|
|
|
<p>Für die Zählernummer <strong>{sensor_search}</strong> wurden keine Sensoren gefunden.</p>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
# Display found sensors
|
|
|
|
|
|
|
|
html_content += f"""
|
|
|
|
|
|
|
|
<div class="info-card">
|
|
|
|
|
|
|
|
<h3>📋 Gefundene Sensoren für '{sensor_search}'</h3>
|
|
|
|
|
|
|
|
<p>{len(sensors)} Sensor(en) gefunden</p>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<div class="sensor-info">
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for sensor in sensors:
|
|
|
|
|
|
|
|
html_content += f"""
|
|
|
|
|
|
|
|
<div class="sensor-card">
|
|
|
|
|
|
|
|
<h3>{sensor['sensorName'].strip()}</h3>
|
|
|
|
|
|
|
|
<p><strong>ID:</strong> {sensor['sensorId']}</p>
|
|
|
|
|
|
|
|
<p><strong>Extern:</strong> {sensor.get('sensorNameExtern', 'N/A')}</p>
|
|
|
|
|
|
|
|
<p><strong>Beschreibung:</strong> {sensor.get('descr', 'N/A')}</p>
|
|
|
|
|
|
|
|
<p><strong>Messkonzept:</strong> {sensor['measureConcept']['name'].strip()}</p>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
html_content += "</div>"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Use first sensor if multiple found
|
|
|
|
|
|
|
|
selected_sensor = sensors[0]
|
|
|
|
|
|
|
|
sensor_id = selected_sensor['sensorId']
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if len(sensors) > 1:
|
|
|
|
|
|
|
|
html_content += f"""
|
|
|
|
|
|
|
|
<div class="warning-card">
|
|
|
|
|
|
|
|
<h3>⚠️ Mehrere Sensoren gefunden</h3>
|
|
|
|
|
|
|
|
<p>Es wurden {len(sensors)} Sensoren gefunden. Verwende ersten Sensor: <strong>{selected_sensor['sensorName'].strip()}</strong></p>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Step 2: Parse readings data
|
|
|
|
|
|
|
|
if not readings_data:
|
|
|
|
|
|
|
|
html_content += """
|
|
|
|
|
|
|
|
<div class="warning-card">
|
|
|
|
|
|
|
|
<h3>⚠️ Keine Ultimo-Daten angegeben</h3>
|
|
|
|
|
|
|
|
<p>Bitte geben Sie die Ultimo-Stände im JSON-Format ein.</p>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
|
|
readings_list = json.loads(readings_data)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if not isinstance(readings_list, list):
|
|
|
|
|
|
|
|
raise ValueError("Daten müssen ein JSON-Array sein")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Validate readings format
|
|
|
|
|
|
|
|
for reading in readings_list:
|
|
|
|
|
|
|
|
if not isinstance(reading, dict):
|
|
|
|
|
|
|
|
raise ValueError("Jeder Eintrag muss ein Objekt sein")
|
|
|
|
|
|
|
|
if 'month' not in reading or 'meterValue' not in reading:
|
|
|
|
|
|
|
|
raise ValueError("Jeder Eintrag muss 'month' und 'meterValue' enthalten")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
html_content += f"""
|
|
|
|
|
|
|
|
<div class="success-card">
|
|
|
|
|
|
|
|
<h3>✅ Ultimo-Daten erfolgreich geparst</h3>
|
|
|
|
|
|
|
|
<p>{len(readings_list)} Ultimo-Stände zur Verarbeitung bereit</p>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Step 3: Execute Ultimo mutation
|
|
|
|
|
|
|
|
mutation_input = {
|
|
|
|
|
|
|
|
"sensorId": sensor_id,
|
|
|
|
|
|
|
|
"variableName": variable_name,
|
|
|
|
|
|
|
|
"variableUnit": "WH",
|
|
|
|
|
|
|
|
"readings": readings_list
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ultimo_result = execute_graphql(RECORD_ULTIMO_MUTATION, {"input": mutation_input})
|
|
|
|
|
|
|
|
ultimo_data = ultimo_result.get("recordUltimoReadings")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if ultimo_data["success"]:
|
|
|
|
|
|
|
|
created_observations = ultimo_data["created"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
html_content += f"""
|
|
|
|
|
|
|
|
<div class="success-card">
|
|
|
|
|
|
|
|
<h3>🎉 Ultimo-Stände erfolgreich eingetragen</h3>
|
|
|
|
|
|
|
|
<p>{len(created_observations)} neue Observations wurden erstellt</p>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Show statistics
|
|
|
|
|
|
|
|
if created_observations:
|
|
|
|
|
|
|
|
latest_observation = max(created_observations, key=lambda x: x['moment'])
|
|
|
|
|
|
|
|
earliest_observation = min(created_observations, key=lambda x: x['moment'])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
html_content += f"""
|
|
|
|
|
|
|
|
<div class="statistics">
|
|
|
|
|
|
|
|
<div class="stat-card">
|
|
|
|
|
|
|
|
<div class="stat-number">{len(created_observations)}</div>
|
|
|
|
|
|
|
|
<div class="stat-label">Einträge erstellt</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="stat-card">
|
|
|
|
|
|
|
|
<div class="stat-number">{format_number(latest_observation['meterValue'])}</div>
|
|
|
|
|
|
|
|
<div class="stat-label">Neuester Zählerstand</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="stat-card">
|
|
|
|
|
|
|
|
<div class="stat-number">{format_datetime(latest_observation['moment'])}</div>
|
|
|
|
|
|
|
|
<div class="stat-label">Letzter Zeitstempel</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Show created observations table
|
|
|
|
|
|
|
|
html_content += """
|
|
|
|
|
|
|
|
<h3>📊 Neu erstellte Zählerstände</h3>
|
|
|
|
|
|
|
|
<table class="data-table">
|
|
|
|
|
|
|
|
<thead>
|
|
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
|
|
<th>Zeitstempel</th>
|
|
|
|
|
|
|
|
<th>Zählerstand</th>
|
|
|
|
|
|
|
|
<th>Wert</th>
|
|
|
|
|
|
|
|
<th>Variable</th>
|
|
|
|
|
|
|
|
<th>Einheit</th>
|
|
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
|
|
</thead>
|
|
|
|
|
|
|
|
<tbody>
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Sort by moment descending
|
|
|
|
|
|
|
|
sorted_observations = sorted(created_observations, key=lambda x: x['moment'], reverse=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for obs in sorted_observations:
|
|
|
|
|
|
|
|
html_content += f"""
|
|
|
|
|
|
|
|
<tr class="current-value">
|
|
|
|
|
|
|
|
<td>{format_datetime(obs['moment'])}</td>
|
|
|
|
|
|
|
|
<td class="value-cell">{format_number(obs['meterValue'])}</td>
|
|
|
|
|
|
|
|
<td class="value-cell">{format_number(obs['value'])}</td>
|
|
|
|
|
|
|
|
<td>{obs['observationVariableUnit']['observationVariable']['name'].strip()}</td>
|
|
|
|
|
|
|
|
<td>{obs['observationVariableUnit']['unit']['name'].strip()}</td>
|
|
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
html_content += "</tbody></table>"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Step 4: Show last 10 observations for context
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
|
|
# Get measure concept ID from sensor data
|
|
|
|
|
|
|
|
measure_concept_id = selected_sensor['measureConcept']['id']
|
|
|
|
|
|
|
|
sensor_name = selected_sensor['sensorName'].strip()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Get historical observations
|
|
|
|
|
|
|
|
historical_data = execute_graphql(GET_LAST_OBSERVATIONS_QUERY, {
|
|
|
|
|
|
|
|
"measurementConceptId": measure_concept_id,
|
|
|
|
|
|
|
|
"sensorName": sensor_name,
|
|
|
|
|
|
|
|
"observationVariableNamePattern": variable_name,
|
|
|
|
|
|
|
|
"startTime": "2020-01-01T00:00:00",
|
|
|
|
|
|
|
|
"endTime": "2030-12-31T23:59:59"
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
observations = historical_data.get("findObservation", [])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if observations:
|
|
|
|
|
|
|
|
# Sort by moment descending and take first 10 (most recent)
|
|
|
|
|
|
|
|
recent_observations = sorted(observations, key=lambda x: x['moment'], reverse=True)[:10]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
html_content += f"""
|
|
|
|
|
|
|
|
<h3>📈 Letzte 10 Zählerstände (inkl. neue Einträge)</h3>
|
|
|
|
|
|
|
|
<table class="data-table">
|
|
|
|
|
|
|
|
<thead>
|
|
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
|
|
<th>Zeitstempel</th>
|
|
|
|
|
|
|
|
<th>Zählerstand</th>
|
|
|
|
|
|
|
|
<th>Wert</th>
|
|
|
|
|
|
|
|
<th>Variable</th>
|
|
|
|
|
|
|
|
<th>Einheit</th>
|
|
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
|
|
</thead>
|
|
|
|
|
|
|
|
<tbody>
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
created_ids = {obs['id'] for obs in created_observations}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for obs in recent_observations:
|
|
|
|
|
|
|
|
is_new = obs['id'] in created_ids
|
|
|
|
|
|
|
|
row_class = 'current-value' if is_new else ''
|
|
|
|
|
|
|
|
new_badge = '🆕 ' if is_new else ''
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
html_content += f"""
|
|
|
|
|
|
|
|
<tr class="{row_class}">
|
|
|
|
|
|
|
|
<td>{new_badge}{format_datetime(obs['moment'])}</td>
|
|
|
|
|
|
|
|
<td class="value-cell">{format_number(obs['meterValue'])}</td>
|
|
|
|
|
|
|
|
<td class="value-cell">{format_number(obs['value'])}</td>
|
|
|
|
|
|
|
|
<td>{obs['observationVariableUnit']['observationVariable']['name'].strip()}</td>
|
|
|
|
|
|
|
|
<td>{obs['observationVariableUnit']['unit']['name'].strip()}</td>
|
|
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
html_content += "</tbody></table>"
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
|
|
html_content += f"""
|
|
|
|
|
|
|
|
<div class="warning-card">
|
|
|
|
|
|
|
|
<h3>⚠️ Historische Daten nicht verfügbar</h3>
|
|
|
|
|
|
|
|
<p>Fehler beim Abrufen der historischen Zählerstände: {str(e)}</p>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
# Handle errors
|
|
|
|
|
|
|
|
errors = ultimo_data.get("errors", [])
|
|
|
|
|
|
|
|
html_content += f"""
|
|
|
|
|
|
|
|
<div class="error-card">
|
|
|
|
|
|
|
|
<h3>❌ Fehler beim Eintragen der Ultimo-Stände</h3>
|
|
|
|
|
|
|
|
<p>Die Ultimo-Stände konnten nicht eingetragen werden:</p>
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for error in errors:
|
|
|
|
|
|
|
|
html_content += f"""
|
|
|
|
|
|
|
|
<div class="error-details">
|
|
|
|
|
|
|
|
<strong>Fehlercode:</strong> {error['code']}<br>
|
|
|
|
|
|
|
|
<strong>Nachricht:</strong> {error['message']}<br>
|
|
|
|
|
|
|
|
<strong>Details:</strong> {error.get('details', 'Keine weiteren Details')}
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
html_content += "</div>"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Show detailed error explanation
|
|
|
|
|
|
|
|
html_content += """
|
|
|
|
|
|
|
|
<div class="info-card">
|
|
|
|
|
|
|
|
<h3>💡 Mögliche Lösungsansätze</h3>
|
|
|
|
|
|
|
|
<ul>
|
|
|
|
|
|
|
|
<li><strong>DUPLICATE_DAY:</strong> Für den angegebenen Tag existiert bereits ein Zählerstand</li>
|
|
|
|
|
|
|
|
<li><strong>MISSING_GAP:</strong> Es fehlen Ultimo-Stände für bestimmte Monate</li>
|
|
|
|
|
|
|
|
<li><strong>NO_INITIAL_READING:</strong> Kein Anfangswert vorhanden - bitte zuerst initialisieren</li>
|
|
|
|
|
|
|
|
<li><strong>INVALID_SEQUENCE:</strong> Die Zählerstände sind nicht chronologisch sortiert</li>
|
|
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
except json.JSONDecodeError as e:
|
|
|
|
|
|
|
|
html_content += f"""
|
|
|
|
|
|
|
|
<div class="error-card">
|
|
|
|
|
|
|
|
<h3>❌ JSON-Format Fehler</h3>
|
|
|
|
|
|
|
|
<p>Die eingegebenen Ultimo-Daten haben kein gültiges JSON-Format:</p>
|
|
|
|
|
|
|
|
<div class="error-details">{str(e)}</div>
|
|
|
|
|
|
|
|
<p><strong>Beispiel für korrektes Format:</strong></p>
|
|
|
|
|
|
|
|
<div class="error-details">[{{"month": "2024-01", "meterValue": 12345.5}}, {{"month": "2024-02", "meterValue": 12567.8}}]</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
except ValueError as e:
|
|
|
|
|
|
|
|
html_content += f"""
|
|
|
|
|
|
|
|
<div class="error-card">
|
|
|
|
|
|
|
|
<h3>❌ Datenformat Fehler</h3>
|
|
|
|
|
|
|
|
<p>Die Struktur der Ultimo-Daten ist ungültig:</p>
|
|
|
|
|
|
|
|
<div class="error-details">{str(e)}</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
|
|
html_content += f"""
|
|
|
|
|
|
|
|
<div class="error-card">
|
|
|
|
|
|
|
|
<h3>❌ Unbekannter Fehler</h3>
|
|
|
|
|
|
|
|
<p>Ein unerwarteter Fehler ist aufgetreten:</p>
|
|
|
|
|
|
|
|
<div class="error-details">{str(e)}</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
|
|
html_content += f"""
|
|
|
|
|
|
|
|
<div class="error-card">
|
|
|
|
|
|
|
|
<h3>❌ System-Fehler</h3>
|
|
|
|
|
|
|
|
<p>Ein kritischer Fehler ist aufgetreten:</p>
|
|
|
|
|
|
|
|
<div class="error-details">{str(e)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
{traceback.format_exc()}</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
html_content += """
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</body>
|
|
|
|
|
|
|
|
</html>
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
result = {
|
|
|
|
|
|
|
|
"type": "html",
|
|
|
|
|
|
|
|
"content": html_content
|
|
|
|
|
|
|
|
}
|
|
|
|
|