deploy script: ultimo_zaehlerstaende_eingabe

main
martin.schweitzer 7 days ago
parent 6c009e4571
commit 226a2e7a1e

@ -0,0 +1,605 @@
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
}
Loading…
Cancel
Save