#!/usr/bin/env python3 import base64 import json import sys import urllib.error import urllib.request from pathlib import Path BASE_URL = "http://127.0.0.1:3000" DASHBOARD_UID = "ucepsa-edge-oee-machine" STOP_PANEL_IDS = set(range(300, 320)) STOP_PANEL_TITLES = { "Informe de paros", "Paros abiertos", "Pendientes clasificar", "Nº paros", "Minutos paro", "Coste paros", "Duración media", "Pareto causas de paro", "Minutos de paro por hora", "Paros pendientes de clasificación", "Detalle completo de paros", } def read_env(path=".env"): env = {} p = Path(path) if not p.exists(): return env for raw in p.read_text().splitlines(): line = raw.strip() if not line or line.startswith("#") or "=" not in line: continue key, value = line.split("=", 1) env[key.strip()] = value.strip().strip('"').strip("'") return env env = read_env() user = env.get("GRAFANA_ADMIN_USER", "admin") password = env.get("GRAFANA_ADMIN_PASSWORD") if not password: print("ERROR: no encuentro GRAFANA_ADMIN_PASSWORD en .env", file=sys.stderr) sys.exit(1) token = base64.b64encode(f"{user}:{password}".encode()).decode() def api(method, path, payload=None): data = None headers = { "Authorization": f"Basic {token}", "Content-Type": "application/json", "Accept": "application/json", } if payload is not None: data = json.dumps(payload).encode("utf-8") req = urllib.request.Request( BASE_URL + path, data=data, headers=headers, method=method, ) try: with urllib.request.urlopen(req, timeout=30) as resp: body = resp.read().decode("utf-8") return json.loads(body) if body else {} except urllib.error.HTTPError as e: print("HTTP ERROR:", e.code, e.read().decode("utf-8"), file=sys.stderr) raise def flatten_panels(panels): result = [] for p in panels or []: result.append(p) if p.get("panels"): result.extend(flatten_panels(p.get("panels"))) return result def find_datasource_from_dashboard(dashboard): for panel in flatten_panels(dashboard.get("panels", [])): ds = panel.get("datasource") if isinstance(ds, dict) and ds.get("uid"): return { "type": ds.get("type", "postgres"), "uid": ds["uid"], } for target in panel.get("targets", []) or []: tds = target.get("datasource") if isinstance(tds, dict) and tds.get("uid"): return { "type": tds.get("type", "postgres"), "uid": tds["uid"], } return None def find_datasource_fallback(): datasources = api("GET", "/api/datasources") for d in datasources: if ( d.get("type") in ("postgres", "grafana-postgresql-datasource") or "postgres" in (d.get("type") or "").lower() or "postgres" in (d.get("name") or "").lower() or "mesavault" in (d.get("name") or "").lower() ): return { "type": d.get("type", "postgres"), "uid": d["uid"], } print("Datasources encontrados:") for d in datasources: print(f"- name={d.get('name')} type={d.get('type')} uid={d.get('uid')}") return None def target(sql, ref="A", fmt="table"): return { "refId": ref, "datasource": DS, "format": fmt, "rawQuery": True, "rawSql": sql, "editorMode": "code", } def stat_panel(panel_id, title, x, y, w, h, sql, unit="none"): return { "id": panel_id, "type": "stat", "title": title, "datasource": DS, "gridPos": {"x": x, "y": y, "w": w, "h": h}, "targets": [target(sql, fmt="table")], "options": { "reduceOptions": { "values": False, "calcs": ["lastNotNull"], "fields": "", }, "orientation": "auto", "textMode": "auto", "colorMode": "value", "graphMode": "none", "justifyMode": "auto", }, "fieldConfig": { "defaults": { "unit": unit, "thresholds": { "mode": "absolute", "steps": [ {"color": "green", "value": None}, {"color": "yellow", "value": 1}, {"color": "red", "value": 5}, ], }, }, "overrides": [], }, } def table_panel(panel_id, title, x, y, w, h, sql): return { "id": panel_id, "type": "table", "title": title, "datasource": DS, "gridPos": {"x": x, "y": y, "w": w, "h": h}, "targets": [target(sql, fmt="table")], "options": { "showHeader": True, "cellHeight": "sm", }, "fieldConfig": { "defaults": {}, "overrides": [], }, } def timeseries_panel(panel_id, title, x, y, w, h, sql, unit="none"): return { "id": panel_id, "type": "timeseries", "title": title, "datasource": DS, "gridPos": {"x": x, "y": y, "w": w, "h": h}, "targets": [target(sql, fmt="time_series")], "fieldConfig": { "defaults": { "unit": unit, "custom": { "drawStyle": "line", "lineInterpolation": "linear", "lineWidth": 1, "fillOpacity": 5, "showPoints": "never", }, }, "overrides": [], }, "options": { "legend": { "displayMode": "list", "placement": "bottom", "showLegend": True, }, "tooltip": { "mode": "multi", "sort": "none", }, }, } def barchart_panel(panel_id, title, x, y, w, h, sql, unit="none"): return { "id": panel_id, "type": "barchart", "title": title, "datasource": DS, "gridPos": {"x": x, "y": y, "w": w, "h": h}, "targets": [target(sql, fmt="table")], "fieldConfig": { "defaults": { "unit": unit, }, "overrides": [], }, "options": { "orientation": "horizontal", "xField": "Causa", "showValue": "always", "legend": { "displayMode": "list", "placement": "bottom", "showLegend": True, }, }, } def max_y(panels): y = 0 for p in panels: gp = p.get("gridPos") or {} y = max(y, int(gp.get("y", 0)) + int(gp.get("h", 1))) return y doc = api("GET", f"/api/dashboards/uid/{DASHBOARD_UID}") dashboard = doc["dashboard"] meta = doc.get("meta", {}) DS = find_datasource_from_dashboard(dashboard) or find_datasource_fallback() if not DS: print("ERROR: no encuentro datasource PostgreSQL/MESAVAULT", file=sys.stderr) sys.exit(1) print(f"Datasource usado: type={DS.get('type')} uid={DS.get('uid')}") existing = dashboard.get("panels", []) filtered = [] for p in existing: title = p.get("title") pid = p.get("id") if pid in STOP_PANEL_IDS or title in STOP_PANEL_TITLES: continue filtered.append(p) base_y = max_y(filtered) + 1 row = { "id": 300, "type": "row", "title": "Informe de paros", "collapsed": False, "gridPos": {"x": 0, "y": base_y, "w": 24, "h": 1}, "panels": [], } panels = [ row, stat_panel( 301, "Paros abiertos", 0, base_y + 1, 4, 4, """ SELECT COUNT(*) AS value FROM mv_reports_ucepsa_demo.machine_stops_grafana WHERE machine_id = '$machine_id' AND status = 'OPEN' """, ), stat_panel( 302, "Pendientes clasificar", 4, base_y + 1, 4, 4, """ SELECT COUNT(*) AS value FROM mv_reports_ucepsa_demo.machine_stops_grafana WHERE machine_id = '$machine_id' AND classification_status = 'PENDING' """, ), stat_panel( 303, "Nº paros", 8, base_y + 1, 4, 4, """ SELECT COUNT(*) AS value FROM mv_reports_ucepsa_demo.machine_stops_grafana WHERE machine_id = '$machine_id' AND $__timeFilter(started_at) """, ), stat_panel( 304, "Minutos paro", 12, base_y + 1, 4, 4, """ SELECT COALESCE(SUM(duration_min), 0) AS value FROM mv_reports_ucepsa_demo.machine_stops_grafana WHERE machine_id = '$machine_id' AND $__timeFilter(started_at) """, unit="min", ), stat_panel( 305, "Coste paros", 16, base_y + 1, 4, 4, """ SELECT COALESCE(SUM(cost_eur), 0) AS value FROM mv_reports_ucepsa_demo.machine_stops_grafana WHERE machine_id = '$machine_id' AND $__timeFilter(started_at) """, unit="currencyEUR", ), stat_panel( 306, "Duración media", 20, base_y + 1, 4, 4, """ SELECT COALESCE(AVG(duration_min), 0) AS value FROM mv_reports_ucepsa_demo.machine_stops_grafana WHERE machine_id = '$machine_id' AND $__timeFilter(started_at) """, unit="min", ), barchart_panel( 307, "Pareto causas de paro", 0, base_y + 5, 12, 8, """ SELECT cause_name AS "Causa", ROUND(SUM(duration_min)::numeric, 2) AS "Minutos", COUNT(*) AS "Nº paros", ROUND(SUM(cost_eur)::numeric, 2) AS "Coste €" FROM mv_reports_ucepsa_demo.machine_stops_grafana WHERE machine_id = '$machine_id' AND $__timeFilter(started_at) GROUP BY cause_name ORDER BY SUM(duration_min) DESC LIMIT 10 """, unit="min", ), timeseries_panel( 308, "Minutos de paro por hora", 12, base_y + 5, 12, 8, """ SELECT date_trunc('hour', started_at) AS "time", SUM(duration_min) AS "Minutos de paro" FROM mv_reports_ucepsa_demo.machine_stops_grafana WHERE machine_id = '$machine_id' AND $__timeFilter(started_at) GROUP BY 1 ORDER BY 1 """, unit="min", ), table_panel( 309, "Paros pendientes de clasificación", 0, base_y + 13, 24, 6, """ SELECT id AS "ID", started_at AS "Inicio", ROUND(duration_min::numeric, 2) AS "Min", cost_eur AS "Coste €", status AS "Estado", classification_status AS "Clasificación", cause_name AS "Causa", operator_note AS "Nota operario" FROM mv_reports_ucepsa_demo.machine_stops_grafana WHERE machine_id = '$machine_id' AND classification_status = 'PENDING' ORDER BY started_at DESC """, ), table_panel( 310, "Detalle completo de paros", 0, base_y + 19, 24, 10, """ SELECT id AS "ID", machine_id AS "Máquina", started_at AS "Inicio", ended_at AS "Fin", duration_min AS "Min", cost_eur AS "Coste €", stop_status_label AS "Estado", classification_status AS "Clasificación", cause_code AS "Código causa", cause_name AS "Causa", start_event_name AS "Evento inicio", end_event_name AS "Evento fin", operator_note AS "Nota operario" FROM mv_reports_ucepsa_demo.machine_stops_grafana WHERE machine_id = '$machine_id' AND $__timeFilter(started_at) ORDER BY started_at DESC LIMIT 100 """, ), ] dashboard["panels"] = filtered + panels payload = { "dashboard": dashboard, "overwrite": True, "message": "Add stop report section to machine dashboard", } if meta.get("folderUid"): payload["folderUid"] = meta["folderUid"] result = api("POST", "/api/dashboards/db", payload) print("Dashboard actualizado:") print(BASE_URL + result.get("url", f"/d/{DASHBOARD_UID}"))