import json import httpx import base64 from datetime import datetime, date from typing import List, Dict, Any, Optional def make_graphql_request(query: str, variables: dict = None) -> dict: """Führt eine GraphQL-Anfrage aus mit Fehlerbehandlung""" try: response = httpx.post( f"{EXTERNAL_BASE_URL}/graphql", json={"query": query, "variables": variables or {}}, headers=AUTH_HEADERS, timeout=30 ) response.raise_for_status() data = response.json() if "errors" in data: error_msg = "; ".join([err.get("message", "Unbekannter Fehler") for err in data["errors"]]) return {"error": f"GraphQL Fehler: {error_msg}"} return data.get("data", {}) except httpx.TimeoutException: return {"error": "Timeout bei der Anfrage"} except httpx.HTTPStatusError as e: return {"error": f"HTTP Fehler {e.response.status_code}: {e.response.text}"} except Exception as e: return {"error": f"Unerwarteter Fehler: {str(e)}"} def search_sensors(meter_search: str) -> List[Dict]: """Sucht Sensoren basierend auf Zählernummer""" query = """ query SearchSensors($meterNumber: String!) { sensorsForMeterNumber(meterNumber: $meterNumber) { sensorId sensorName sensorNameExtern descr measureConcept { id name descr } } } """ data = make_graphql_request(query, {"meterNumber": meter_search}) if "error" in data: return [] return data.get("sensorsForMeterNumber", []) def get_variable_units(sensor_id: str) -> List[Dict]: """Holt verfügbare Variablen und Units für einen Sensor""" query = """ query GetVariableUnits($sensorId: ID!) { availableVariableUnits(sensorId: $sensorId) { variableUnitId variableName unitName } } """ data = make_graphql_request(query, {"sensorId": sensor_id}) if "error" in data: return [] return data.get("availableVariableUnits", []) def get_last_observation(sensor_id: str, variable_name: str) -> Optional[Dict]: """Holt den letzten Zählerstand für einen Sensor""" query = """ query GetLastObservation($sensorId: ID!, $variableName: String) { lastObservation(sensorId: $sensorId, variableName: $variableName) { id moment meterValue } } """ data = make_graphql_request(query, {"sensorId": sensor_id, "variableName": variable_name}) if "error" in data: return None return data.get("lastObservation") def record_single_reading(sensor_id: str, moment: str, value: float, variable_name: str, variable_unit: str) -> Dict: """Erfasst einen einzelnen Zählerstand""" mutation = """ mutation RecordSingleReading($input: MeterReadingInput!) { recordMeterReading(input: $input) { success observation { id moment value meterValue } errors { code message details } } } """ input_data = { "sensorId": sensor_id, "moment": moment, "value": value, "variableName": variable_name, "variableUnit": variable_unit } return make_graphql_request(mutation, {"input": input_data}) def record_ultimo_readings(sensor_id: str, variable_name: str, variable_unit: str, readings: List[Dict]) -> Dict: """Erfasst mehrere Ultimo-Zählerstände""" mutation = """ mutation RecordUltimoReadings($input: UltimoReadingsInput!) { recordUltimoReadings(input: $input) { success created { id moment value meterValue } errors { code message details } } } """ input_data = { "sensorId": sensor_id, "variableName": variable_name, "variableUnit": variable_unit, "readings": readings } return make_graphql_request(mutation, {"input": input_data}) def generate_month_list(start_year: int, start_month: int, end_year: int, end_month: int) -> List[str]: """Generiert eine Liste von Monaten im Format YYYY-MM""" months = [] current_year = start_year current_month = start_month while current_year < end_year or (current_year == end_year and current_month <= end_month): months.append(f"{current_year:04d}-{current_month:02d}") current_month += 1 if current_month > 12: current_month = 1 current_year += 1 return months # Phase Detection if "sensor_id" not in PARAMS: # PHASE 1: Sensor Suche meter_search = PARAMS.get("meter_search", "") if not meter_search: result = { "type": "error", "message": "Kein Suchbegriff angegeben" } else: sensors = search_sensors(meter_search) if not sensors: result = { "type": "html", "content": f"""

⚠️ Keine Sensoren gefunden

Für die Suche "{meter_search}" wurden keine Sensoren gefunden.

💡 Suchtipps:

← Zurück zur Suche

""" } else: # Erstelle Sensor-Dropdown-Optionen mit Zusatzinformationen sensor_options = [] for sensor in sensors: clean_name = sensor["sensorName"].strip() measure_concept = sensor["measureConcept"] concept_name = measure_concept["name"].strip() if measure_concept["name"] else "Unbekannt" concept_desc = measure_concept.get("descr", "") or "" label = f"{clean_name} ({concept_name}" if concept_desc and concept_desc.strip(): label += f" - {concept_desc.strip()}" label += ")" sensor_options.append({ "value": sensor["sensorId"], "label": label }) # Sortiere Optionen alphabetisch sensor_options.sort(key=lambda x: x["label"]) result = { "type": "form", "form_definition": { "title": "Sensor Auswahl und Zählerstand Eingabe", "description": f"Gefundene Sensoren für Suche: '{meter_search}'", "layout": "sections", "sections": [ { "title": "Sensor Auswahl", "icon": "sensors", "field_names": ["sensor_id", "variable_selection"] }, { "title": "Erfassungsart", "icon": "input", "field_names": ["input_method"] }, { "title": "Einzelne Erfassung", "icon": "schedule", "field_names": ["single_datetime", "single_value"] }, { "title": "Ultimo Batch-Erfassung", "icon": "batch_prediction", "field_names": ["batch_start_year", "batch_start_month", "batch_end_year", "batch_end_month", "batch_values"] } ], "fields": [ { "name": "sensor_id", "widget": "dropdown", "label": "Sensor auswählen", "options": sensor_options, "validators": [ {"type": "required", "error_text": "Bitte wählen Sie einen Sensor aus"} ] }, { "name": "variable_selection", "widget": "text_field", "label": "Variable wird automatisch geladen...", "read_only": True, "initial_value": "Wählen Sie zuerst einen Sensor aus", "helper_text": "Die verfügbaren Variablen werden nach der Sensor-Auswahl angezeigt" }, { "name": "input_method", "widget": "segmented_control", "label": "Erfassungsart", "initial_value": "single", "options": [ {"value": "single", "label": "Einzeln"}, {"value": "batch", "label": "Batch (Ultimo)"} ], "validators": [ {"type": "required", "error_text": "Bitte wählen Sie eine Erfassungsart"} ] }, { "name": "single_datetime", "widget": "date_time_picker", "label": "Datum und Uhrzeit", "date_config": { "input_type": "both", "format": "dd.MM.yyyy HH:mm" }, "conditional": { "field_name": "input_method", "operator": "equals", "value": "single", "action": "show" }, "validators": [ {"type": "required", "error_text": "Bitte wählen Sie Datum und Uhrzeit"} ] }, { "name": "single_value", "widget": "text_field", "label": "Zählerstand", "text_field_config": { "keyboard_type": "number" }, "conditional": { "field_name": "input_method", "operator": "equals", "value": "single", "action": "show" }, "validators": [ {"type": "required", "error_text": "Bitte geben Sie einen Zählerstand ein"}, {"type": "numeric", "error_text": "Zählerstand muss eine Zahl sein"}, {"type": "min", "value": 0, "error_text": "Zählerstand darf nicht negativ sein"} ] }, { "name": "batch_start_year", "widget": "text_field", "label": "Start Jahr (YYYY)", "text_field_config": { "keyboard_type": "number" }, "initial_value": "2024", "conditional": { "field_name": "input_method", "operator": "equals", "value": "batch", "action": "show" }, "validators": [ {"type": "required", "error_text": "Jahr ist erforderlich"}, {"type": "integer", "error_text": "Jahr muss eine ganze Zahl sein"}, {"type": "between", "value": 2020, "value2": 2030, "error_text": "Jahr muss zwischen 2020 und 2030 liegen"} ] }, { "name": "batch_start_month", "widget": "dropdown", "label": "Start Monat", "initial_value": "1", "conditional": { "field_name": "input_method", "operator": "equals", "value": "batch", "action": "show" }, "options": [ {"value": "1", "label": "Januar"}, {"value": "2", "label": "Februar"}, {"value": "3", "label": "März"}, {"value": "4", "label": "April"}, {"value": "5", "label": "Mai"}, {"value": "6", "label": "Juni"}, {"value": "7", "label": "Juli"}, {"value": "8", "label": "August"}, {"value": "9", "label": "September"}, {"value": "10", "label": "Oktober"}, {"value": "11", "label": "November"}, {"value": "12", "label": "Dezember"} ], "validators": [ {"type": "required", "error_text": "Start Monat ist erforderlich"} ] }, { "name": "batch_end_year", "widget": "text_field", "label": "Ende Jahr (YYYY)", "text_field_config": { "keyboard_type": "number" }, "initial_value": "2024", "conditional": { "field_name": "input_method", "operator": "equals", "value": "batch", "action": "show" }, "validators": [ {"type": "required", "error_text": "Jahr ist erforderlich"}, {"type": "integer", "error_text": "Jahr muss eine ganze Zahl sein"}, {"type": "between", "value": 2020, "value2": 2030, "error_text": "Jahr muss zwischen 2020 und 2030 liegen"} ] }, { "name": "batch_end_month", "widget": "dropdown", "label": "Ende Monat", "initial_value": "12", "conditional": { "field_name": "input_method", "operator": "equals", "value": "batch", "action": "show" }, "options": [ {"value": "1", "label": "Januar"}, {"value": "2", "label": "Februar"}, {"value": "3", "label": "März"}, {"value": "4", "label": "April"}, {"value": "5", "label": "Mai"}, {"value": "6", "label": "Juni"}, {"value": "7", "label": "Juli"}, {"value": "8", "label": "August"}, {"value": "9", "label": "September"}, {"value": "10", "label": "Oktober"}, {"value": "11", "label": "November"}, {"value": "12", "label": "Dezember"} ], "validators": [ {"type": "required", "error_text": "Ende Monat ist erforderlich"} ] }, { "name": "batch_values", "widget": "text_field", "label": "Zählerstände (Komma-getrennt)", "hint_text": "z.B: 1000.5, 2000.2, 3000.8", "text_field_config": { "max_lines": 3 }, "conditional": { "field_name": "input_method", "operator": "equals", "value": "batch", "action": "show" }, "helper_text": "Geben Sie die Zählerstände in chronologischer Reihenfolge an, getrennt durch Kommas", "validators": [ {"type": "required", "error_text": "Bitte geben Sie die Zählerstände ein"} ] } ], "submit_label": "Nächster Schritt" } } else: # PHASE 2: Variable Auswahl und Verarbeitung sensor_id = PARAMS.get("sensor_id") if not sensor_id: result = { "type": "error", "message": "Sensor ID fehlt" } else: # Prüfe ob bereits variable_name vorhanden ist (Phase 3) if "variable_name" in PARAMS: # PHASE 3: Ausführung variable_name = PARAMS.get("variable_name") variable_unit = PARAMS.get("variable_unit") input_method = PARAMS.get("input_method", "single") try: if input_method == "single": # Einzelne Erfassung single_datetime = PARAMS.get("single_datetime") single_value = float(PARAMS.get("single_value", 0)) # Konvertiere Datetime zu ISO Format if isinstance(single_datetime, str): # Parse verschiedene Datetime-Formate try: dt = datetime.fromisoformat(single_datetime.replace('Z', '+00:00')) except: dt = datetime.strptime(single_datetime, "%Y-%m-%dT%H:%M:%S") else: dt = single_datetime iso_datetime = dt.isoformat() # Führe Einzelerfassung aus response = record_single_reading( sensor_id, iso_datetime, single_value, variable_name, variable_unit ) if "error" in response: result = { "type": "html", "content": f"""

❌ Fehler bei der Erfassung

{response['error']}

""" } else: record_result = response.get("recordMeterReading", {}) if record_result.get("success"): obs = record_result.get("observation", {}) result = { "type": "html", "content": f"""

✅ Zählerstand erfolgreich erfasst

Der Zählerstand wurde erfolgreich gespeichert.

📊 Erfasste Daten:

Observation ID:{obs.get('id', 'N/A')}
Zeitpunkt:{obs.get('moment', 'N/A')}
Zählerstand:{obs.get('meterValue', 'N/A')}
Berechneter Wert:{obs.get('value', 'N/A')}
""" } else: errors = record_result.get("errors", []) error_html = "" result = { "type": "html", "content": f"""

❌ Erfassung fehlgeschlagen

Die Erfassung konnte nicht durchgeführt werden:

{error_html}
""" } else: # Batch-Erfassung start_year = int(PARAMS.get("batch_start_year", 2024)) start_month = int(PARAMS.get("batch_start_month", 1)) end_year = int(PARAMS.get("batch_end_year", 2024)) end_month = int(PARAMS.get("batch_end_month", 12)) batch_values_str = PARAMS.get("batch_values", "") # Parse Batch-Werte try: values = [float(v.strip()) for v in batch_values_str.split(",") if v.strip()] except ValueError: result = { "type": "error", "message": "Ungültige Zahlenwerte in Batch-Eingabe. Verwenden Sie nur Zahlen getrennt durch Kommas." } else: # Generiere Monatsliste months = generate_month_list(start_year, start_month, end_year, end_month) if len(values) != len(months): result = { "type": "html", "content": f"""

⚠️ Anzahl Unstimmigkeit

Anzahl der Werte ({len(values)}) stimmt nicht mit der Anzahl der Monate ({len(months)}) überein.

Erwartete Monate: {', '.join(months)}

""" } else: # Erstelle Readings-Liste readings = [] for i, month in enumerate(months): readings.append({ "month": month, "meterValue": values[i] }) # Führe Batch-Erfassung aus response = record_ultimo_readings( sensor_id, variable_name, variable_unit, readings ) if "error" in response: result = { "type": "html", "content": f"""

❌ Fehler bei der Batch-Erfassung

{response['error']}

""" } else: batch_result = response.get("recordUltimoReadings", {}) created = batch_result.get("created", []) errors = batch_result.get("errors", []) success = batch_result.get("success", False) html_content = f"""
""" if success and created: html_content += f"""

✅ Batch-Erfassung erfolgreich

{len(created)} Zählerstände wurden erfolgreich erfasst.

📊 Erfasste Readings:

""" for i, obs in enumerate(created): bg_color = "#f8f9fa" if i % 2 == 0 else "white" html_content += f""" """ html_content += """
ID Zeitpunkt Zählerstand Wert
{obs.get('id', 'N/A')} {obs.get('moment', 'N/A')} {obs.get('meterValue', 'N/A')} {obs.get('value', 'N/A')}
""" if errors: html_content += f"""

⚠️ Warnungen/Fehler:

" if not success and not created: html_content += f"""

❌ Batch-Erfassung fehlgeschlagen

Die Erfassung konnte nicht durchgeführt werden.

""" html_content += "
" result = { "type": "html", "content": html_content } except Exception as e: result = { "type": "error", "message": f"Fehler bei der Verarbeitung: {str(e)}" } else: # PHASE 2: Variable Auswahl variable_units = get_variable_units(sensor_id) if not variable_units: result = { "type": "error", "message": "Keine Variablen für diesen Sensor gefunden" } else: # Erstelle Variable-Dropdown-Optionen variable_options = [] for vu in variable_units: clean_var_name = vu["variableName"].strip() clean_unit_name = vu["unitName"].strip() label = f"{clean_var_name} ({clean_unit_name})" variable_options.append({ "value": f"{clean_var_name}|{clean_unit_name}", "label": label }) # Sortiere Optionen alphabetisch variable_options.sort(key=lambda x: x["label"]) # Hole letzten Zählerstand für Kontext last_obs = None if variable_options: # Verwende erste Variable für Kontext first_var = variable_options[0]["value"].split("|")[0] last_obs = get_last_observation(sensor_id, first_var) context_info = "" if last_obs: context_info = f"Letzter bekannter Zählerstand: {last_obs.get('meterValue', 'N/A')} am {last_obs.get('moment', 'N/A')}" else: context_info = "Kein vorheriger Zählerstand gefunden" # Übertrage alle Parameter aus der ersten Phase result = { "type": "form", "form_definition": { "title": "Variable Auswahl und Zählerstand Eingabe", "description": context_info, "layout": "sections", "sections": [ { "title": "Variable/Einheit", "icon": "analytics", "field_names": ["variable_selection"] }, { "title": "Erfassungsart", "icon": "input", "field_names": ["input_method"] }, { "title": "Einzelne Erfassung", "icon": "schedule", "field_names": ["single_datetime", "single_value"] }, { "title": "Ultimo Batch-Erfassung", "icon": "batch_prediction", "field_names": ["batch_start_year", "batch_start_month", "batch_end_year", "batch_end_month", "batch_values"] } ], "fields": [ # Versteckte Felder für übertragene Parameter { "name": "sensor_id", "widget": "text_field", "label": "Sensor ID", "initial_value": sensor_id, "read_only": True, "enabled": False }, { "name": "variable_selection", "widget": "dropdown", "label": "Variable und Einheit auswählen", "options": variable_options, "validators": [ {"type": "required", "error_text": "Bitte wählen Sie eine Variable aus"} ] }, { "name": "input_method", "widget": "segmented_control", "label": "Erfassungsart", "initial_value": PARAMS.get("input_method", "single"), "options": [ {"value": "single", "label": "Einzeln"}, {"value": "batch", "label": "Batch (Ultimo)"} ], "validators": [ {"type": "required", "error_text": "Bitte wählen Sie eine Erfassungsart"} ] }, { "name": "single_datetime", "widget": "date_time_picker", "label": "Datum und Uhrzeit", "initial_value": PARAMS.get("single_datetime", datetime.now().isoformat()), "date_config": { "input_type": "both", "format": "dd.MM.yyyy HH:mm" }, "conditional": { "field_name": "input_method", "operator": "equals", "value": "single", "action": "show" }, "validators": [ {"type": "required", "error_text": "Bitte wählen Sie Datum und Uhrzeit"} ] }, { "name": "single_value", "widget": "text_field", "label": "Zählerstand", "initial_value": PARAMS.get("single_value", ""), "text_field_config": { "keyboard_type": "number" }, "conditional": { "field_name": "input_method", "operator": "equals", "value": "single", "action": "show" }, "validators": [ {"type": "required", "error_text": "Bitte geben Sie einen Zählerstand ein"}, {"type": "numeric", "error_text": "Zählerstand muss eine Zahl sein"}, {"type": "min", "value": 0, "error_text": "Zählerstand darf nicht negativ sein"} ] }, { "name": "batch_start_year", "widget": "text_field", "label": "Start Jahr (YYYY)", "initial_value": PARAMS.get("batch_start_year", "2024"), "text_field_config": { "keyboard_type": "number" }, "conditional": { "field_name": "input_method", "operator": "equals", "value": "batch", "action": "show" }, "validators": [ {"type": "required", "error_text": "Jahr ist erforderlich"}, {"type": "integer", "error_text": "Jahr muss eine ganze Zahl sein"}, {"type": "between", "value": 2020, "value2": 2030, "error_text": "Jahr muss zwischen 2020 und 2030 liegen"} ] }, { "name": "batch_start_month", "widget": "dropdown", "label": "Start Monat", "initial_value": PARAMS.get("batch_start_month", "1"), "conditional": { "field_name": "input_method", "operator": "equals", "value": "batch", "action": "show" }, "options": [ {"value": "1", "label": "Januar"}, {"value": "2", "label": "Februar"}, {"value": "3", "label": "März"}, {"value": "4", "label": "April"}, {"value": "5", "label": "Mai"}, {"value": "6", "label": "Juni"}, {"value": "7", "label": "Juli"}, {"value": "8", "label": "August"}, {"value": "9", "label": "September"}, {"value": "10", "label": "Oktober"}, {"value": "11", "label": "November"}, {"value": "12", "label": "Dezember"} ], "validators": [ {"type": "required", "error_text": "Start Monat ist erforderlich"} ] }, { "name": "batch_end_year", "widget": "text_field", "label": "Ende Jahr (YYYY)", "initial_value": PARAMS.get("batch_end_year", "2024"), "text_field_config": { "keyboard_type": "number" }, "conditional": { "field_name": "input_method", "operator": "equals", "value": "batch", "action": "show" }, "validators": [ {"type": "required", "error_text": "Jahr ist erforderlich"}, {"type": "integer", "error_text": "Jahr muss eine ganze Zahl sein"}, {"type": "between", "value": 2020, "value2": 2030, "error_text": "Jahr muss zwischen 2020 und 2030 liegen"} ] }, { "name": "batch_end_month", "widget": "dropdown", "label": "Ende Monat", "initial_value": PARAMS.get("batch_end_month", "12"), "conditional": { "field_name": "input_method", "operator": "equals", "value": "batch", "action": "show" }, "options": [ {"value": "1", "label": "Januar"}, {"value": "2", "label": "Februar"}, {"value": "3", "label": "März"}, {"value": "4", "label": "April"}, {"value": "5", "label": "Mai"}, {"value": "6", "label": "Juni"}, {"value": "7", "label": "Juli"}, {"value": "8", "label": "August"}, {"value": "9", "label": "September"}, {"value": "10", "label": "Oktober"}, {"value": "11", "label": "November"}, {"value": "12", "label": "Dezember"} ], "validators": [ {"type": "required", "error_text": "Ende Monat ist erforderlich"} ] }, { "name": "batch_values", "widget": "text_field", "label": "Zählerstände (Komma-getrennt)", "initial_value": PARAMS.get("batch_values", ""), "hint_text": "z.B: 1000.5, 2000.2, 3000.8", "text_field_config": { "max_lines": 3 }, "conditional": { "field_name": "input_method", "operator": "equals", "value": "batch", "action": "show" }, "helper_text": "Geben Sie die Zählerstände in chronologischer Reihenfolge an, getrennt durch Kommas", "validators": [ {"type": "required", "error_text": "Bitte geben Sie die Zählerstände ein"} ] } ], "submit_label": "Zählerstände erfassen" } } # Erweitere das Form um die variable_name und variable_unit basierend auf variable_selection if "variable_selection" in PARAMS: var_selection = PARAMS["variable_selection"] if "|" in var_selection: var_name, var_unit = var_selection.split("|", 1) # Füge versteckte Felder hinzu result["form_definition"]["fields"].extend([ { "name": "variable_name", "widget": "text_field", "label": "Variable Name", "initial_value": var_name, "read_only": True, "enabled": False }, { "name": "variable_unit", "widget": "text_field", "label": "Variable Unit", "initial_value": var_unit, "read_only": True, "enabled": False } ])