deploy script: ultimo_meter_readings

main
martin.schweitzer 6 days ago
parent 36e3bbb165
commit 160f2d3ff5

@ -0,0 +1,835 @@
import json
import httpx
import re
from datetime import datetime
import traceback
def main():
try:
# Check if we have enough parameters to proceed
search_term = PARAMS.get('search_term', '').strip()
sensor_id = PARAMS.get('sensor_id', '').strip()
# Phase 1: If no sensor is selected, show sensor selection
if not sensor_id and not search_term:
return {"type": "error", "message": "Bitte geben Sie entweder einen Suchbegriff oder eine Sensor-ID ein."}
# Phase 2: If we have search term but no sensor_id, search for sensors
if search_term and not sensor_id:
sensors = search_sensors(search_term)
if not sensors:
return {"type": "error", "message": f"Keine Sensoren gefunden für Suchbegriff '{search_term}'."}
# Create dynamic form with found sensors
sensor_options = []
for sensor in sensors:
display_name = f"{sensor['sensorName'].strip()} (ID: {sensor['sensorId']})"
if sensor.get('sensorNameExtern') and sensor['sensorNameExtern'].strip() != '-':
display_name += f" - {sensor['sensorNameExtern'].strip()}"
if sensor['measureConcept']['name']:
display_name += f" [{sensor['measureConcept']['name'].strip()}]"
sensor_options.append({
"value": sensor['sensorId'],
"label": display_name
})
dynamic_form = {
"title": "Sensor auswählen und Ultimo-Stände eingeben",
"description": f"Gefundene Sensoren für '{search_term}'. Wählen Sie einen Sensor aus und geben Sie die Ultimo-Stände ein.",
"layout": "sections",
"fields": [
{
"name": "sensor_id",
"widget": "dropdown",
"label": "Sensor auswählen",
"options": sensor_options,
"validators": [{"type": "required"}]
},
{
"name": "variable_name",
"widget": "text_field",
"label": "Variable Name (optional)",
"hint_text": "Leer lassen für Standard-Variable",
"helper_text": "Z.B. ACTIVE_ENERGY_ABSORBED_10, ENERGY_INST_VAL"
},
{
"name": "variable_unit",
"widget": "text_field",
"label": "Einheit (optional)",
"hint_text": "Leer lassen für Standard-Einheit",
"helper_text": "Z.B. WH, KWH"
},
{
"name": "readings_input",
"widget": "text_field",
"label": "Zählerstände",
"hint_text": "Format: YYYY-MM:Wert (pro Zeile) oder YYYY-MM:Wert,YYYY-MM:Wert",
"helper_text": "Beispiel: 2024-01:1000.5 oder 2024-01:1000,2024-02:1100",
"text_field_config": {
"keyboard_type": "multiline",
"max_lines": 10,
"min_lines": 2
},
"validators": [
{"type": "required", "error_text": "Zählerstände sind erforderlich"}
]
}
],
"sections": [
{
"title": "Sensor",
"icon": "sensors",
"field_names": ["sensor_id"]
},
{
"title": "Variable & Einheit",
"icon": "settings",
"field_names": ["variable_name", "variable_unit"]
},
{
"title": "Zählerstände eingeben",
"icon": "edit",
"field_names": ["readings_input"]
}
]
}
return {"type": "form", "form_definition": dynamic_form}
# Phase 3: We have sensor_id, process the readings
if not sensor_id:
return {"type": "error", "message": "Sensor-ID ist erforderlich."}
readings_input = PARAMS.get('readings_input', '').strip()
if not readings_input:
return {"type": "error", "message": "Zählerstände sind erforderlich."}
# Parse readings input
readings = parse_readings_input(readings_input)
if not readings:
return {"type": "error", "message": "Keine gültigen Zählerstände gefunden. Format: YYYY-MM:Wert"}
# Get sensor info and available variables
sensor_info = get_sensor_info(sensor_id)
if not sensor_info:
return {"type": "error", "message": f"Sensor mit ID {sensor_id} nicht gefunden."}
# Get available variables for the sensor
available_variables = get_available_variables(sensor_id)
# Determine variable and unit to use
variable_name = PARAMS.get('variable_name', '').strip()
variable_unit = PARAMS.get('variable_unit', '').strip()
if not variable_name and available_variables:
# Use first available variable as default
variable_name = available_variables[0]['variableName'].strip()
variable_unit = available_variables[0]['unitName'].strip()
# Record the readings
result = record_ultimo_readings(sensor_id, variable_name, variable_unit, readings)
# Generate HTML report
html_content = generate_html_report(sensor_info, variable_name, variable_unit, readings, result, available_variables)
return {"type": "html", "content": html_content}
except Exception as e:
return {"type": "error", "message": f"Fehler: {str(e)}\n\nDetails: {traceback.format_exc()}"}
def search_sensors(search_term):
"""Search for sensors by meter number"""
try:
# First try sensorsForMeterNumber
query = '''
query SearchSensors($meterNumber: String!) {
sensorsForMeterNumber(meterNumber: $meterNumber) {
sensorId
sensorName
sensorNameExtern
descr
measureConcept {
id
name
descr
}
}
}
'''
with httpx.Client() as client:
response = client.post(
f"{EXTERNAL_BASE_URL}/graphql",
headers=AUTH_HEADERS,
json={"query": query, "variables": {"meterNumber": search_term}}
)
if response.status_code == 200:
data = response.json()
if 'errors' not in data:
sensors = data.get('data', {}).get('sensorsForMeterNumber', [])
if sensors:
return sensors
# If no results, try general sensor search
query = '''
query GetAllSensors {
sensors {
id
name
nameExtern
description
measureConcept {
id
name
description
}
}
}
'''
with httpx.Client() as client:
response = client.post(
f"{EXTERNAL_BASE_URL}/graphql",
headers=AUTH_HEADERS,
json={"query": query}
)
if response.status_code == 200:
data = response.json()
if 'errors' not in data:
all_sensors = data.get('data', {}).get('sensors', [])
# Filter sensors that match search term
filtered_sensors = []
for sensor in all_sensors:
if (search_term.lower() in sensor.get('name', '').lower() or
search_term.lower() in sensor.get('nameExtern', '').lower() or
search_term.lower() in sensor.get('description', '').lower()):
filtered_sensors.append({
'sensorId': sensor['id'],
'sensorName': sensor['name'],
'sensorNameExtern': sensor.get('nameExtern'),
'descr': sensor.get('description'),
'measureConcept': {
'id': sensor['measureConcept']['id'],
'name': sensor['measureConcept']['name'],
'descr': sensor['measureConcept'].get('description')
}
})
return filtered_sensors[:20] # Limit to first 20 results
return []
except Exception as e:
print(f"Error searching sensors: {e}")
return []
def get_sensor_info(sensor_id):
"""Get detailed sensor information"""
try:
query = '''
query GetSensor($id: ID!) {
sensor(id: $id) {
id
name
nameExtern
description
measureConcept {
id
name
description
}
}
}
'''
with httpx.Client() as client:
response = client.post(
f"{EXTERNAL_BASE_URL}/graphql",
headers=AUTH_HEADERS,
json={"query": query, "variables": {"id": sensor_id}}
)
if response.status_code == 200:
data = response.json()
if 'errors' not in data:
return data.get('data', {}).get('sensor')
return None
except Exception as e:
print(f"Error getting sensor info: {e}")
return None
def get_available_variables(sensor_id):
"""Get available variables and units for a sensor"""
try:
query = '''
query GetAvailableVariables($sensorId: ID!) {
availableVariableUnits(sensorId: $sensorId) {
variableUnitId
variableName
unitName
}
}
'''
with httpx.Client() as client:
response = client.post(
f"{EXTERNAL_BASE_URL}/graphql",
headers=AUTH_HEADERS,
json={"query": query, "variables": {"sensorId": sensor_id}}
)
if response.status_code == 200:
data = response.json()
if 'errors' not in data:
return data.get('data', {}).get('availableVariableUnits', [])
return []
except Exception as e:
print(f"Error getting available variables: {e}")
return []
def parse_readings_input(readings_input):
"""Parse the readings input string into a list of readings"""
readings = []
# Support both line-by-line and comma-separated format
lines = readings_input.replace(',', '\n').split('\n')
for line in lines:
line = line.strip()
if not line:
continue
# Expected format: YYYY-MM:value
if ':' not in line:
continue
parts = line.split(':', 1)
if len(parts) != 2:
continue
month_str = parts[0].strip()
value_str = parts[1].strip()
# Validate month format YYYY-MM
if not re.match(r'^\d{4}-\d{2}$', month_str):
continue
try:
value = float(value_str)
readings.append({
'month': month_str,
'meterValue': value
})
except ValueError:
continue
# Sort readings by month
readings.sort(key=lambda x: x['month'])
return readings
def record_ultimo_readings(sensor_id, variable_name, variable_unit, readings):
"""Record ultimo readings using GraphQL mutation"""
try:
mutation = '''
mutation RecordUltimoReadings($input: UltimoReadingsInput!) {
recordUltimoReadings(input: $input) {
success
errors {
code
message
details
}
created {
id
moment
value
meterValue
}
}
}
'''
input_data = {
'sensorId': sensor_id,
'readings': readings
}
if variable_name:
input_data['variableName'] = variable_name
if variable_unit:
input_data['variableUnit'] = variable_unit
with httpx.Client() as client:
response = client.post(
f"{EXTERNAL_BASE_URL}/graphql",
headers=AUTH_HEADERS,
json={"query": mutation, "variables": {"input": input_data}}
)
if response.status_code == 200:
data = response.json()
if 'errors' in data:
return {'success': False, 'errors': [{'message': str(data['errors'])}], 'created': []}
return data.get('data', {}).get('recordUltimoReadings', {'success': False, 'errors': [], 'created': []})
else:
return {'success': False, 'errors': [{'message': f'HTTP Error {response.status_code}'}], 'created': []}
except Exception as e:
return {'success': False, 'errors': [{'message': str(e)}], 'created': []}
def generate_html_report(sensor_info, variable_name, variable_unit, readings, result, available_variables):
"""Generate HTML report for the ultimo recording result"""
sensor_display = sensor_info.get('name', '').strip() if sensor_info else 'Unbekannt'
if sensor_info and sensor_info.get('nameExtern') and sensor_info['nameExtern'].strip() != '-':
sensor_display += f" ({sensor_info['nameExtern'].strip()})"
measure_concept_name = ''
if sensor_info and sensor_info.get('measureConcept', {}).get('name'):
measure_concept_name = sensor_info['measureConcept']['name'].strip()
# Status styling
if result['success']:
status_class = 'success'
status_text = 'Erfolgreich'
status_icon = ''
else:
status_class = 'error'
status_text = 'Fehler'
status_icon = ''
html_content = f'''
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Ultimo-Zählerstand Eingabe</title>
<style>
body {{
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 0;
padding: 20px;
background-color: #f5f5f5;
line-height: 1.6;
}}
.container {{
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
overflow: hidden;
}}
.header {{
background: linear-gradient(135deg, var(--color-primary, #2196F3), #1976D2);
color: white;
padding: 30px;
text-align: center;
}}
.header h1 {{
margin: 0;
font-size: 2.2em;
font-weight: 300;
}}
.content {{
padding: 30px;
}}
.status-card {{
border-radius: 8px;
padding: 20px;
margin-bottom: 25px;
border-left: 5px solid;
}}
.status-card.success {{
background-color: #E8F5E8;
border-left-color: var(--color-success, #4CAF50);
color: #2E7D32;
}}
.status-card.error {{
background-color: #FFEBEE;
border-left-color: var(--color-danger, #F44336);
color: #C62828;
}}
.status-title {{
font-size: 1.3em;
font-weight: bold;
margin-bottom: 10px;
}}
.info-grid {{
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin-bottom: 25px;
}}
.info-card {{
background: #f9f9f9;
border-radius: 8px;
padding: 20px;
border: 1px solid #e0e0e0;
}}
.info-card h3 {{
margin: 0 0 15px 0;
color: var(--color-primary, #2196F3);
font-size: 1.1em;
}}
.info-item {{
margin-bottom: 8px;
}}
.info-label {{
font-weight: bold;
display: inline-block;
width: 120px;
}}
.readings-table {{
width: 100%;
border-collapse: collapse;
margin-top: 20px;
background: white;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}}
.readings-table th {{
background: var(--color-primary, #2196F3);
color: white;
padding: 12px;
text-align: left;
font-weight: 600;
}}
.readings-table td {{
padding: 12px;
border-bottom: 1px solid #e0e0e0;
}}
.readings-table tr:nth-child(even) {{
background-color: #f9f9f9;
}}
.readings-table tr:hover {{
background-color: #f0f8ff;
}}
.created-badge {{
background-color: var(--color-success, #4CAF50);
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 0.85em;
}}
.failed-badge {{
background-color: var(--color-danger, #F44336);
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 0.85em;
}}
.error-list {{
background: #ffebee;
border-radius: 8px;
padding: 15px;
margin-top: 15px;
}}
.error-item {{
background: white;
border-left: 4px solid var(--color-danger, #F44336);
padding: 10px;
margin-bottom: 10px;
border-radius: 4px;
}}
.error-code {{
font-weight: bold;
color: var(--color-danger, #F44336);
}}
.summary-stats {{
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 15px;
margin-top: 25px;
}}
.stat-card {{
text-align: center;
background: linear-gradient(135deg, #f8f9fa, #e9ecef);
border-radius: 8px;
padding: 20px;
border: 1px solid #dee2e6;
}}
.stat-number {{
font-size: 2em;
font-weight: bold;
margin-bottom: 5px;
}}
.stat-label {{
color: #666;
font-size: 0.9em;
}}
.variables-list {{
max-height: 200px;
overflow-y: auto;
border: 1px solid #e0e0e0;
border-radius: 4px;
padding: 10px;
background: white;
}}
.variable-item {{
padding: 5px;
border-bottom: 1px solid #f0f0f0;
font-family: monospace;
font-size: 0.9em;
}}
.variable-item:last-child {{
border-bottom: none;
}}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>{status_icon} Ultimo-Zählerstand Eingabe</h1>
</div>
<div class="content">
<div class="status-card {status_class}">
<div class="status-title">{status_icon} Status: {status_text}</div>
'''
if result['success']:
html_content += f'''
<p>Ultimo-Zählerstände wurden erfolgreich eingegeben.</p>
<p><strong>Anzahl erfolgreich erstellt:</strong> {len(result.get('created', []))}</p>
'''
else:
html_content += f'''
<p>Bei der Eingabe der Ultimo-Zählerstände sind Fehler aufgetreten.</p>
'''
html_content += '''
</div>
<div class="info-grid">
<div class="info-card">
<h3>📊 Sensor Information</h3>
'''
if sensor_info:
html_content += f'''
<div class="info-item">
<span class="info-label">Sensor-ID:</span>
{sensor_info['id']}
</div>
<div class="info-item">
<span class="info-label">Name:</span>
{sensor_display}
</div>
'''
if measure_concept_name:
html_content += f'''
<div class="info-item">
<span class="info-label">Messkonzept:</span>
{measure_concept_name}
</div>
'''
html_content += f'''
</div>
<div class="info-card">
<h3> Variable & Einheit</h3>
<div class="info-item">
<span class="info-label">Variable:</span>
{variable_name if variable_name else 'Standard'}
</div>
<div class="info-item">
<span class="info-label">Einheit:</span>
{variable_unit if variable_unit else 'Standard'}
</div>
</div>
</div>
'''
# Show readings table
html_content += '''
<h3>📋 Eingegeben Zählerstände</h3>
<table class="readings-table">
<thead>
<tr>
<th>Monat</th>
<th>Zählerstand</th>
<th>Status</th>
</tr>
</thead>
<tbody>
'''
created_ids = [obs['id'] for obs in result.get('created', [])]
for reading in readings:
month = reading['month']
value = reading['meterValue']
# Check if this reading was successfully created
is_created = any(obs for obs in result.get('created', []) if month in obs.get('moment', ''))
status_badge = '<span class="created-badge">Erstellt</span>' if is_created else '<span class="failed-badge">Fehler</span>'
html_content += f'''
<tr>
<td>{month}</td>
<td>{value:,.2f}</td>
<td>{status_badge}</td>
</tr>
'''
html_content += '''
</tbody>
</table>
'''
# Show errors if any
if result.get('errors'):
html_content += '''
<h3> Fehler</h3>
<div class="error-list">
'''
for error in result['errors']:
html_content += f'''
<div class="error-item">
<div class="error-code">{error.get('code', 'ERROR')}</div>
<div>{error.get('message', 'Unbekannter Fehler')}</div>
'''
if error.get('details'):
html_content += f'<div><small>Details: {error["details"]}</small></div>'
html_content += '</div>'
html_content += '''
</div>
'''
# Show created observations details if any
if result.get('created'):
html_content += '''
<h3> Erfolgreich erstellte Beobachtungen</h3>
<table class="readings-table">
<thead>
<tr>
<th>ID</th>
<th>Zeitpunkt</th>
<th>Wert</th>
<th>Zählerstand</th>
</tr>
</thead>
<tbody>
'''
for obs in result['created']:
try:
moment = datetime.fromisoformat(obs['moment'].replace('Z', '+00:00'))
moment_str = moment.strftime('%d.%m.%Y %H:%M')
except:
moment_str = obs['moment']
html_content += f'''
<tr>
<td>{obs['id']}</td>
<td>{moment_str}</td>
<td>{obs.get('value', 0):,.2f}</td>
<td>{obs.get('meterValue', 0):,.2f}</td>
</tr>
'''
html_content += '''
</tbody>
</table>
'''
# Summary statistics
total_readings = len(readings)
successful_readings = len(result.get('created', []))
failed_readings = total_readings - successful_readings
html_content += f'''
<div class="summary-stats">
<div class="stat-card">
<div class="stat-number">{total_readings}</div>
<div class="stat-label">Gesamt eingegeben</div>
</div>
<div class="stat-card">
<div class="stat-number" style="color: var(--color-success, #4CAF50);">{successful_readings}</div>
<div class="stat-label">Erfolgreich</div>
</div>
<div class="stat-card">
<div class="stat-number" style="color: var(--color-danger, #F44336);">{failed_readings}</div>
<div class="stat-label">Fehlgeschlagen</div>
</div>
</div>
'''
# Show available variables
if available_variables:
html_content += '''
<h3>🔧 Verfügbare Variablen für diesen Sensor</h3>
<div class="variables-list">
'''
for var in available_variables[:20]: # Limit to first 20
var_name = var['variableName'].strip()
unit_name = var['unitName'].strip()
html_content += f'''
<div class="variable-item">{var_name} ({unit_name})</div>
'''
if len(available_variables) > 20:
html_content += f'<div class="variable-item">... und {len(available_variables) - 20} weitere</div>'
html_content += '''
</div>
'''
html_content += '''
</div>
</div>
</body>
</html>
'''
return html_content
# Execute main function
result = main()
Loading…
Cancel
Save