508 lines
13 KiB
Python
Executable File
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}"))
|