40-clients/ucepsa/edge-oee-demo/grafana/scripts/patch_machine_dashboard_stops.py
2026-06-25 18:51:33 +02:00

508 lines
13 KiB
Python
Executable File

#!/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}"))