diff --git a/scripts/ultimo_meter_readings_recorder.py b/scripts/ultimo_meter_readings_recorder.py new file mode 100644 index 0000000..7fa0d49 --- /dev/null +++ b/scripts/ultimo_meter_readings_recorder.py @@ -0,0 +1,969 @@ +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 += """ + +
IDZeitpunktZählerstandWert
{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 + } + ])