feat(ucepsa): add Loss Intelligence v0 SQL layer
This commit is contained in:
parent
69f8416808
commit
85d478b0df
285
ucepsa/edge-oee-demo/sql/070_loss_intelligence_v0.sql
Normal file
285
ucepsa/edge-oee-demo/sql/070_loss_intelligence_v0.sql
Normal file
@ -0,0 +1,285 @@
|
||||
DROP VIEW IF EXISTS mv_reports_ucepsa_demo.loss_priority_board;
|
||||
DROP VIEW IF EXISTS mv_reports_ucepsa_demo.loss_events_current;
|
||||
|
||||
CREATE VIEW mv_reports_ucepsa_demo.loss_events_current AS
|
||||
WITH open_stop_losses AS (
|
||||
SELECT
|
||||
now()::timestamptz AS detected_at,
|
||||
s.machine_id::text AS machine_id,
|
||||
COALESCE(s.asset, '')::text AS asset,
|
||||
COALESCE(s.order_id, '')::text AS order_ref,
|
||||
'OPEN_STOP'::text AS loss_type,
|
||||
CASE
|
||||
WHEN COALESCE(s.current_duration_min, 0) >= 15 THEN 'ACTION_REQUIRED'
|
||||
WHEN COALESCE(s.current_duration_min, 0) >= 5 THEN 'ATTENTION'
|
||||
ELSE 'INFO'
|
||||
END AS severity,
|
||||
CASE
|
||||
WHEN COALESCE(s.current_duration_min, 0) >= 15 THEN 80
|
||||
WHEN COALESCE(s.current_duration_min, 0) >= 5 THEN 55
|
||||
ELSE 30
|
||||
END::int AS severity_score,
|
||||
COALESCE(s.current_duration_min, 0)::numeric AS duration_min,
|
||||
COALESCE(s.current_cost_eur, 0)::numeric AS cost_eur,
|
||||
COALESCE(s.status, 'OPEN')::text AS status,
|
||||
'Paro abierto pendiente de cierre o clasificación.'::text AS description,
|
||||
'Comprobar causa en máquina y clasificar el paro desde la consola.'::text AS recommended_action,
|
||||
jsonb_build_object(
|
||||
'stop_id', s.id,
|
||||
'classification_status', s.classification_status,
|
||||
'cause_name', s.cause_name,
|
||||
'started_at', s.started_at,
|
||||
'source', 'mv_reports_ucepsa_demo.open_stops'
|
||||
) AS evidence_json
|
||||
FROM mv_reports_ucepsa_demo.open_stops s
|
||||
),
|
||||
|
||||
unclassified_closed_stop_losses AS (
|
||||
SELECT
|
||||
now()::timestamptz AS detected_at,
|
||||
s.machine_id::text AS machine_id,
|
||||
COALESCE(s.asset, '')::text AS asset,
|
||||
COALESCE(s.order_id, '')::text AS order_ref,
|
||||
'UNCLASSIFIED_STOP'::text AS loss_type,
|
||||
CASE
|
||||
WHEN COALESCE(s.duration_min, 0) >= 15 THEN 'ACTION_REQUIRED'
|
||||
ELSE 'ATTENTION'
|
||||
END AS severity,
|
||||
CASE
|
||||
WHEN COALESCE(s.duration_min, 0) >= 15 THEN 75
|
||||
ELSE 55
|
||||
END::int AS severity_score,
|
||||
COALESCE(s.duration_min, 0)::numeric AS duration_min,
|
||||
COALESCE(s.cost_eur, 0)::numeric AS cost_eur,
|
||||
COALESCE(s.status, 'CLOSED')::text AS status,
|
||||
'Paro cerrado pendiente de clasificación.'::text AS description,
|
||||
'Asignar causa para evitar pérdida sin explicación.'::text AS recommended_action,
|
||||
jsonb_build_object(
|
||||
'stop_id', s.id,
|
||||
'classification_status', s.classification_status,
|
||||
'cause_name', s.cause_name,
|
||||
'started_at', s.started_at,
|
||||
'ended_at', s.ended_at,
|
||||
'source', 'mv_reports_ucepsa_demo.stops_recent'
|
||||
) AS evidence_json
|
||||
FROM mv_reports_ucepsa_demo.stops_recent s
|
||||
WHERE s.status = 'CLOSED'
|
||||
AND s.classification_status = 'PENDING'
|
||||
),
|
||||
|
||||
odoo_machine_losses AS (
|
||||
SELECT
|
||||
now()::timestamptz AS detected_at,
|
||||
o.machine_id::text AS machine_id,
|
||||
COALESCE(o.asset, '')::text AS asset,
|
||||
COALESCE(o.odoo_order_ref, '')::text AS order_ref,
|
||||
o.status_check::text AS loss_type,
|
||||
CASE
|
||||
WHEN o.status_check IN ('ORDEN_EN_CURSO_MAQUINA_PARADA', 'MAQUINA_EN_MARCHA_SIN_ORDEN_INICIADA') THEN 'ACTION_REQUIRED'
|
||||
WHEN o.status_check = 'SIN_TELEMETRIA' THEN 'ATTENTION'
|
||||
ELSE 'INFO'
|
||||
END AS severity,
|
||||
CASE
|
||||
WHEN o.status_check = 'ORDEN_EN_CURSO_MAQUINA_PARADA' THEN 85
|
||||
WHEN o.status_check = 'MAQUINA_EN_MARCHA_SIN_ORDEN_INICIADA' THEN 75
|
||||
WHEN o.status_check = 'SIN_TELEMETRIA' THEN 60
|
||||
ELSE 20
|
||||
END::int AS severity_score,
|
||||
COALESCE(o.stop_min, 0)::numeric AS duration_min,
|
||||
COALESCE(o.stop_cost_eur, 0)::numeric AS cost_eur,
|
||||
o.status_check::text AS status,
|
||||
CASE
|
||||
WHEN o.status_check = 'ORDEN_EN_CURSO_MAQUINA_PARADA'
|
||||
THEN 'Odoo indica orden en curso, pero la máquina está parada.'
|
||||
WHEN o.status_check = 'MAQUINA_EN_MARCHA_SIN_ORDEN_INICIADA'
|
||||
THEN 'La máquina está en marcha, pero la orden no aparece iniciada en Odoo.'
|
||||
WHEN o.status_check = 'SIN_TELEMETRIA'
|
||||
THEN 'No hay telemetría reciente para contrastar Odoo contra máquina.'
|
||||
ELSE 'Sin discrepancia relevante.'
|
||||
END AS description,
|
||||
CASE
|
||||
WHEN o.status_check = 'ORDEN_EN_CURSO_MAQUINA_PARADA'
|
||||
THEN 'Revisar paro real, operario, causa pendiente y estado de máquina.'
|
||||
WHEN o.status_check = 'MAQUINA_EN_MARCHA_SIN_ORDEN_INICIADA'
|
||||
THEN 'Revisar disciplina de inicio de orden en Odoo o desfase de sincronización.'
|
||||
WHEN o.status_check = 'SIN_TELEMETRIA'
|
||||
THEN 'Revisar RevPi, MQTT, VPN/red, contador y sink.'
|
||||
ELSE 'Sin acción.'
|
||||
END AS recommended_action,
|
||||
jsonb_build_object(
|
||||
'odoo_order_ref', o.odoo_order_ref,
|
||||
'odoo_state', o.odoo_state,
|
||||
'machine_running', o.machine_running,
|
||||
'telemetry_age_s', o.telemetry_age_s,
|
||||
'real_cycles', o.real_cycles,
|
||||
'planned_cycles', o.planned_cycles,
|
||||
'cycle_completion_pct', o.cycle_completion_pct,
|
||||
'source', 'mv_reports_ucepsa_demo.odoo_vs_machine_demo'
|
||||
) AS evidence_json
|
||||
FROM mv_reports_ucepsa_demo.odoo_vs_machine_demo o
|
||||
WHERE o.status_check IS DISTINCT FROM 'OK'
|
||||
),
|
||||
|
||||
telemetry_losses AS (
|
||||
SELECT
|
||||
now()::timestamptz AS detected_at,
|
||||
l.machine_id::text AS machine_id,
|
||||
COALESCE(l.asset, '')::text AS asset,
|
||||
COALESCE(l.order_id, '')::text AS order_ref,
|
||||
'STALE_OR_BAD_TELEMETRY'::text AS loss_type,
|
||||
CASE
|
||||
WHEN COALESCE(l.comm_ok, false) = false THEN 'ACTION_REQUIRED'
|
||||
WHEN COALESCE(l.age_s, 999999) > 120 THEN 'ACTION_REQUIRED'
|
||||
WHEN COALESCE(l.age_s, 999999) > 30 THEN 'ATTENTION'
|
||||
ELSE 'INFO'
|
||||
END AS severity,
|
||||
CASE
|
||||
WHEN COALESCE(l.comm_ok, false) = false THEN 80
|
||||
WHEN COALESCE(l.age_s, 999999) > 120 THEN 75
|
||||
WHEN COALESCE(l.age_s, 999999) > 30 THEN 45
|
||||
ELSE 10
|
||||
END::int AS severity_score,
|
||||
0::numeric AS duration_min,
|
||||
0::numeric AS cost_eur,
|
||||
CASE
|
||||
WHEN COALESCE(l.comm_ok, false) = false THEN 'BAD_COMMUNICATION'
|
||||
WHEN COALESCE(l.age_s, 999999) > 120 THEN 'STALE_TELEMETRY'
|
||||
WHEN COALESCE(l.age_s, 999999) > 30 THEN 'DELAYED_TELEMETRY'
|
||||
ELSE 'OK'
|
||||
END::text AS status,
|
||||
CASE
|
||||
WHEN COALESCE(l.comm_ok, false) = false THEN 'La RevPi publica, pero la lectura Modbus no es válida.'
|
||||
WHEN COALESCE(l.age_s, 999999) > 120 THEN 'No hay telemetría reciente de máquina.'
|
||||
WHEN COALESCE(l.age_s, 999999) > 30 THEN 'La telemetría llega con retraso.'
|
||||
ELSE 'Telemetría correcta.'
|
||||
END AS description,
|
||||
CASE
|
||||
WHEN COALESCE(l.comm_ok, false) = false THEN 'Revisar RS-485, SDM120-M, alimentación del contador y logs de la RevPi.'
|
||||
WHEN COALESCE(l.age_s, 999999) > 120 THEN 'Revisar RevPi, red/VPN, Mosquitto y sink MQTT.'
|
||||
WHEN COALESCE(l.age_s, 999999) > 30 THEN 'Revisar estabilidad de red y frecuencia de publicación.'
|
||||
ELSE 'Sin acción.'
|
||||
END AS recommended_action,
|
||||
jsonb_build_object(
|
||||
'age_s', l.age_s,
|
||||
'comm_ok', l.comm_ok,
|
||||
'error_text', l.error_text,
|
||||
'ingest_ts', l.ingest_ts,
|
||||
'source', 'mv_reports_ucepsa_demo.latest_machine_state_fast'
|
||||
) AS evidence_json
|
||||
FROM mv_reports_ucepsa_demo.latest_machine_state_fast l
|
||||
WHERE COALESCE(l.comm_ok, false) = false
|
||||
OR COALESCE(l.age_s, 999999) > 30
|
||||
),
|
||||
|
||||
low_cycle_rate_losses AS (
|
||||
SELECT
|
||||
now()::timestamptz AS detected_at,
|
||||
l.machine_id::text AS machine_id,
|
||||
COALESCE(l.asset, '')::text AS asset,
|
||||
COALESCE(o.odoo_order_ref, '')::text AS order_ref,
|
||||
'LOW_CYCLE_RATE'::text AS loss_type,
|
||||
CASE
|
||||
WHEN COALESCE(l.cycle_rate_ppm, 0) <= 0 THEN 'ATTENTION'
|
||||
WHEN COALESCE(l.cycle_rate_ppm, 0) < 7 THEN 'ATTENTION'
|
||||
ELSE 'INFO'
|
||||
END AS severity,
|
||||
CASE
|
||||
WHEN COALESCE(l.cycle_rate_ppm, 0) <= 0 THEN 50
|
||||
WHEN COALESCE(l.cycle_rate_ppm, 0) < 7 THEN 40
|
||||
ELSE 10
|
||||
END::int AS severity_score,
|
||||
0::numeric AS duration_min,
|
||||
0::numeric AS cost_eur,
|
||||
'LOW_RATE'::text AS status,
|
||||
'La máquina aparece en marcha, pero la velocidad de ciclos/golpes es baja respecto al objetivo inicial.'::text AS description,
|
||||
'Revisar golpes reales, sensor I_2, velocidad programada, material, ajuste de máquina y criterio de bolsas por golpe.'::text AS recommended_action,
|
||||
jsonb_build_object(
|
||||
'cycle_rate_ppm', l.cycle_rate_ppm,
|
||||
'cycle_pulse_count', l.cycle_pulse_count,
|
||||
'machine_running', l.machine_running,
|
||||
'target_cycle_rate_ppm_assumed', 10,
|
||||
'odoo_order_ref', o.odoo_order_ref,
|
||||
'source', 'mv_reports_ucepsa_demo.latest_machine_state_fast'
|
||||
) AS evidence_json
|
||||
FROM mv_reports_ucepsa_demo.latest_machine_state_fast l
|
||||
LEFT JOIN mv_reports_ucepsa_demo.odoo_current_order_demo o
|
||||
ON o.machine_id = l.machine_id
|
||||
WHERE COALESCE(l.age_s, 999999) <= 120
|
||||
AND COALESCE(l.machine_running, false) = true
|
||||
AND COALESCE(l.cycle_rate_ppm, 0) < 7
|
||||
)
|
||||
|
||||
SELECT *
|
||||
FROM open_stop_losses
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT *
|
||||
FROM unclassified_closed_stop_losses
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT *
|
||||
FROM odoo_machine_losses
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT *
|
||||
FROM telemetry_losses
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT *
|
||||
FROM low_cycle_rate_losses;
|
||||
|
||||
CREATE VIEW mv_reports_ucepsa_demo.loss_priority_board AS
|
||||
SELECT
|
||||
row_number() OVER (
|
||||
ORDER BY
|
||||
(
|
||||
severity_score
|
||||
+ LEAST((COALESCE(cost_eur, 0) * 10), 50)::int
|
||||
+ LEAST((COALESCE(duration_min, 0) * 2), 40)::int
|
||||
) DESC,
|
||||
detected_at DESC
|
||||
) AS priority_rank,
|
||||
(
|
||||
severity_score
|
||||
+ LEAST((COALESCE(cost_eur, 0) * 10), 50)::int
|
||||
+ LEAST((COALESCE(duration_min, 0) * 2), 40)::int
|
||||
) AS priority_score,
|
||||
CASE
|
||||
WHEN (
|
||||
severity_score
|
||||
+ LEAST((COALESCE(cost_eur, 0) * 10), 50)::int
|
||||
+ LEAST((COALESCE(duration_min, 0) * 2), 40)::int
|
||||
) >= 90 THEN 'CRÍTICA'
|
||||
WHEN (
|
||||
severity_score
|
||||
+ LEAST((COALESCE(cost_eur, 0) * 10), 50)::int
|
||||
+ LEAST((COALESCE(duration_min, 0) * 2), 40)::int
|
||||
) >= 60 THEN 'ALTA'
|
||||
WHEN (
|
||||
severity_score
|
||||
+ LEAST((COALESCE(cost_eur, 0) * 10), 50)::int
|
||||
+ LEAST((COALESCE(duration_min, 0) * 2), 40)::int
|
||||
) >= 35 THEN 'MEDIA'
|
||||
ELSE 'BAJA'
|
||||
END AS priority_level,
|
||||
detected_at,
|
||||
machine_id,
|
||||
asset,
|
||||
order_ref,
|
||||
loss_type,
|
||||
severity,
|
||||
duration_min,
|
||||
cost_eur,
|
||||
status,
|
||||
description,
|
||||
recommended_action,
|
||||
evidence_json
|
||||
FROM mv_reports_ucepsa_demo.loss_events_current
|
||||
ORDER BY priority_score DESC, detected_at DESC;
|
||||
|
||||
GRANT SELECT ON mv_reports_ucepsa_demo.loss_events_current TO grafana_ucepsa_ro;
|
||||
GRANT SELECT ON mv_reports_ucepsa_demo.loss_priority_board TO grafana_ucepsa_ro;
|
||||
Loading…
x
Reference in New Issue
Block a user