From 42dc050fc71f29964b891fb93caa7339d0921879 Mon Sep 17 00:00:00 2001 From: Victor Fraile Garcia Date: Thu, 25 Jun 2026 18:13:18 +0200 Subject: [PATCH] backup: UCEPSA Edge-OEE piloto productivo activo 2026-06-25 --- ucepsa/edge-oee-demo/.gitignore | 67 + .../db/schema/postgres_schema_latest.sql | 8742 +++++++++++++++++ .../deploy/ucepsa-local/.env.example | 54 + .../ucepsa-local/docker-compose.edge.yml | 33 + .../ucepsa-local/docker-compose.odoo.yml | 35 + .../ucepsa-local/docker-compose.operator.yml | 28 + .../docker-compose.order-state.yml | 37 + .../deploy/ucepsa-local/docker-compose.yml | 49 + .../ucepsa-edge-oee-machine.dashboard.json | 1664 ++++ ...sa-edge-oee-local_2026-05-27_20-19-48.json | 6179 ++++++++++++ .../ucepsa-edge-oee-machine.dashboard.json | 1664 ++++ .../import/platform40-dashboard.raw.json | 5677 +++++++++++ .../import/ucepsa-edge-oee-local.import.json | 5677 +++++++++++ .../scripts/create_machine_dashboard.py | 524 + .../patch_machine_dashboard_pareto_clean.py | 114 + .../scripts/patch_machine_dashboard_stops.py | 507 + .../patch_machine_dashboard_stops_pareto.py | 151 + .../edge-oee-demo/revpi/edge-oee/.env.example | 30 + .../revpi/edge-oee/REVPI-INVENTORY.md | 41 + .../revpi/edge-oee/beacon_controller.py | 474 + .../revpi/edge-oee/beacon_manual_test.py | 182 + .../revpi/edge-oee/check_revpi_base.sh | 26 + .../edge-oee/clear_order_state_retained.py | 62 + .../revpi/edge-oee/debug_cort01_eastron.py | 51 + .../edge-oee-demo/revpi/edge-oee/di_probe.py | 99 + .../revpi/edge-oee/eastron_reader.py | 104 + .../revpi/edge-oee/eastron_reader_mqtt.py | 205 + .../edge-oee/eastron_reader_mqtt_loop.py | 419 + .../edge-oee/live_wise_counter_modbus.py | 53 + .../monitor_cort01_eastron_continuo.py | 212 + .../revpi/edge-oee/probe_3_wise_nodes.py | 55 + .../revpi/edge-oee/probe_cort01_eastron.py | 85 + .../edge-oee/probe_cort01_sdm630_3phase.py | 51 + .../revpi/edge-oee/probe_modbus_float.py | 97 + .../edge-oee/probe_wise_counter_modbus.py | 114 + .../revpi/edge-oee/probe_wise_tcp.py | 120 + .../revpi/edge-oee/publish_order_state.py | 129 + .../revpi/edge-oee/scan_wise_counter.py | 125 + .../edge-oee-demo/revpi/edge-oee/wait_mqtt.py | 33 + .../revpi/edge-oee/wise_di_edge_probe.py | 105 + .../revpi/edge-oee/wise_nodes.json | 37 + .../revpi/edge-oee/wise_reader_local_loop.py | 222 + .../edge-oee/wise_reader_local_loop_v2.py | 303 + .../revpi/edge-oee/wise_reader_mqtt_loop.py | 491 + .../mesavault-beacon-controller.service | 2 + .../systemd/mesavault-edge-oee-revpi.service | 2 + .../main.py.bak.2026-05-08_09-18-47 | 278 - .../main.py.bak.2026-05-08_09-32-17 | 410 - .../sql/065_fix_odoo_start_delay_scope.sql | 121 + .../sql/080_energy_3phase_views.sql | 114 + .../sql/082_machine_stops_grafana.sql | 81 + .../083_machine_stops_grafana_cause_fix.sql | 93 + ..._machine_stops_grafana_cause_names_fix.sql | 144 + .../070_order_execution_report_views.sql | 885 -- ...080_margin_intelligence_cost_readiness.sql | 1186 --- .../sql/reports/090_data_quality_contract.sql | 1166 --- .../versions/090_margin_prod_foundation.sql | 100 + ...91_margin_prod_current_order_readiness.sql | 136 + .../092_margin_prod_raw_material_costs.sql | 271 + ucepsa/edge-oee-demo/tools/mqtt_auth_probe.py | 92 + .../tools/odoo_margin_commercial_sync.py | 528 + .../tools/odoo_order_state_publisher.py | 388 + .../edge-oee-demo/tools/probe_odoo_cutters.py | 170 + .../tools/read_order_state_retained.py | 94 + .../tools/watch_odoo_active_workorders.py | 188 + 65 files changed, 37651 insertions(+), 3925 deletions(-) create mode 100644 ucepsa/edge-oee-demo/.gitignore create mode 100644 ucepsa/edge-oee-demo/db/schema/postgres_schema_latest.sql create mode 100644 ucepsa/edge-oee-demo/deploy/ucepsa-local/.env.example create mode 100644 ucepsa/edge-oee-demo/deploy/ucepsa-local/docker-compose.edge.yml create mode 100644 ucepsa/edge-oee-demo/deploy/ucepsa-local/docker-compose.odoo.yml create mode 100644 ucepsa/edge-oee-demo/deploy/ucepsa-local/docker-compose.operator.yml create mode 100644 ucepsa/edge-oee-demo/deploy/ucepsa-local/docker-compose.order-state.yml create mode 100644 ucepsa/edge-oee-demo/deploy/ucepsa-local/docker-compose.yml create mode 100644 ucepsa/edge-oee-demo/grafana/dashboards/ucepsa-edge-oee-machine.dashboard.json create mode 100644 ucepsa/edge-oee-demo/grafana/export/ucepsa-edge-oee-local_2026-05-27_20-19-48.json create mode 100644 ucepsa/edge-oee-demo/grafana/export/ucepsa-edge-oee-machine.dashboard.json create mode 100644 ucepsa/edge-oee-demo/grafana/import/platform40-dashboard.raw.json create mode 100644 ucepsa/edge-oee-demo/grafana/import/ucepsa-edge-oee-local.import.json create mode 100755 ucepsa/edge-oee-demo/grafana/scripts/create_machine_dashboard.py create mode 100755 ucepsa/edge-oee-demo/grafana/scripts/patch_machine_dashboard_pareto_clean.py create mode 100755 ucepsa/edge-oee-demo/grafana/scripts/patch_machine_dashboard_stops.py create mode 100755 ucepsa/edge-oee-demo/grafana/scripts/patch_machine_dashboard_stops_pareto.py create mode 100644 ucepsa/edge-oee-demo/revpi/edge-oee/.env.example create mode 100644 ucepsa/edge-oee-demo/revpi/edge-oee/REVPI-INVENTORY.md create mode 100755 ucepsa/edge-oee-demo/revpi/edge-oee/beacon_controller.py create mode 100755 ucepsa/edge-oee-demo/revpi/edge-oee/beacon_manual_test.py create mode 100755 ucepsa/edge-oee-demo/revpi/edge-oee/check_revpi_base.sh create mode 100755 ucepsa/edge-oee-demo/revpi/edge-oee/clear_order_state_retained.py create mode 100755 ucepsa/edge-oee-demo/revpi/edge-oee/debug_cort01_eastron.py create mode 100755 ucepsa/edge-oee-demo/revpi/edge-oee/di_probe.py create mode 100755 ucepsa/edge-oee-demo/revpi/edge-oee/eastron_reader.py create mode 100755 ucepsa/edge-oee-demo/revpi/edge-oee/eastron_reader_mqtt.py create mode 100755 ucepsa/edge-oee-demo/revpi/edge-oee/eastron_reader_mqtt_loop.py create mode 100755 ucepsa/edge-oee-demo/revpi/edge-oee/live_wise_counter_modbus.py create mode 100755 ucepsa/edge-oee-demo/revpi/edge-oee/monitor_cort01_eastron_continuo.py create mode 100755 ucepsa/edge-oee-demo/revpi/edge-oee/probe_3_wise_nodes.py create mode 100755 ucepsa/edge-oee-demo/revpi/edge-oee/probe_cort01_eastron.py create mode 100755 ucepsa/edge-oee-demo/revpi/edge-oee/probe_cort01_sdm630_3phase.py create mode 100755 ucepsa/edge-oee-demo/revpi/edge-oee/probe_modbus_float.py create mode 100755 ucepsa/edge-oee-demo/revpi/edge-oee/probe_wise_counter_modbus.py create mode 100755 ucepsa/edge-oee-demo/revpi/edge-oee/probe_wise_tcp.py create mode 100755 ucepsa/edge-oee-demo/revpi/edge-oee/publish_order_state.py create mode 100755 ucepsa/edge-oee-demo/revpi/edge-oee/scan_wise_counter.py create mode 100755 ucepsa/edge-oee-demo/revpi/edge-oee/wait_mqtt.py create mode 100755 ucepsa/edge-oee-demo/revpi/edge-oee/wise_di_edge_probe.py create mode 100644 ucepsa/edge-oee-demo/revpi/edge-oee/wise_nodes.json create mode 100755 ucepsa/edge-oee-demo/revpi/edge-oee/wise_reader_local_loop.py create mode 100755 ucepsa/edge-oee-demo/revpi/edge-oee/wise_reader_local_loop_v2.py create mode 100755 ucepsa/edge-oee-demo/revpi/edge-oee/wise_reader_mqtt_loop.py create mode 100644 ucepsa/edge-oee-demo/revpi/systemd/mesavault-beacon-controller.service create mode 100644 ucepsa/edge-oee-demo/revpi/systemd/mesavault-edge-oee-revpi.service delete mode 100644 ucepsa/edge-oee-demo/services/edge-oee-mqtt-pg-sink/main.py.bak.2026-05-08_09-18-47 delete mode 100644 ucepsa/edge-oee-demo/services/edge-oee-mqtt-pg-sink/main.py.bak.2026-05-08_09-32-17 create mode 100644 ucepsa/edge-oee-demo/sql/065_fix_odoo_start_delay_scope.sql create mode 100644 ucepsa/edge-oee-demo/sql/080_energy_3phase_views.sql create mode 100644 ucepsa/edge-oee-demo/sql/082_machine_stops_grafana.sql create mode 100644 ucepsa/edge-oee-demo/sql/083_machine_stops_grafana_cause_fix.sql create mode 100644 ucepsa/edge-oee-demo/sql/084_machine_stops_grafana_cause_names_fix.sql delete mode 100644 ucepsa/edge-oee-demo/sql/reports/070_order_execution_report_views.sql delete mode 100644 ucepsa/edge-oee-demo/sql/reports/080_margin_intelligence_cost_readiness.sql delete mode 100644 ucepsa/edge-oee-demo/sql/reports/090_data_quality_contract.sql create mode 100644 ucepsa/edge-oee-demo/sql/versions/090_margin_prod_foundation.sql create mode 100644 ucepsa/edge-oee-demo/sql/versions/091_margin_prod_current_order_readiness.sql create mode 100644 ucepsa/edge-oee-demo/sql/versions/092_margin_prod_raw_material_costs.sql create mode 100755 ucepsa/edge-oee-demo/tools/mqtt_auth_probe.py create mode 100755 ucepsa/edge-oee-demo/tools/odoo_margin_commercial_sync.py create mode 100755 ucepsa/edge-oee-demo/tools/odoo_order_state_publisher.py create mode 100755 ucepsa/edge-oee-demo/tools/probe_odoo_cutters.py create mode 100755 ucepsa/edge-oee-demo/tools/read_order_state_retained.py create mode 100755 ucepsa/edge-oee-demo/tools/watch_odoo_active_workorders.py diff --git a/ucepsa/edge-oee-demo/.gitignore b/ucepsa/edge-oee-demo/.gitignore new file mode 100644 index 0000000..70e317f --- /dev/null +++ b/ucepsa/edge-oee-demo/.gitignore @@ -0,0 +1,67 @@ +# Secrets +.env +.env.* +**/.env +**/.env.* +!**/.env.example + +# Python +**/__pycache__/ +**/*.pyc +**/.venv/ +**/venv/ + +# Logs +*.log +logs/ +**/logs/ + +# Runtime data / volumes +data/ +volumes/ +postgres_data/ +grafana/data/ +mosquitto/data/ +mosquitto/log/ +minio/data/ + +# Backups and dumps with data +backups/ +*.dump +*.backup +*.tar +*.tar.gz +*.tgz +*.zip +*.gz +*.sql.gz + +# Keys / certificates +*.pem +*.key +*.crt +*.p12 +id_rsa* +id_ed25519* + +# Local env files +**/.env.local +**/*.env.local + +# Backup / old files +*.bak +*.bak.* +*.bak_* +*_OLD + +# Python cache +**/__pycache__/ +**/*.pyc + +# Grafana runtime data +grafana/data/ +**/grafana/data/ + +# Audit packages +*_audit_*.tgz +*_audit_*.zip diff --git a/ucepsa/edge-oee-demo/db/schema/postgres_schema_latest.sql b/ucepsa/edge-oee-demo/db/schema/postgres_schema_latest.sql new file mode 100644 index 0000000..427af0a --- /dev/null +++ b/ucepsa/edge-oee-demo/db/schema/postgres_schema_latest.sql @@ -0,0 +1,8742 @@ +-- +-- PostgreSQL database dump +-- + +\restrict 1wX0I2K6nLSszbhqDMaBaJRF13jG1hT3a3HKImFGiykK1wx4riB6Rf5OY4lcf06 + +-- Dumped from database version 16.14 +-- Dumped by pg_dump version 16.14 + +SET statement_timeout = 0; +SET lock_timeout = 0; +SET idle_in_transaction_session_timeout = 0; +SET client_encoding = 'UTF8'; +SET standard_conforming_strings = on; +SELECT pg_catalog.set_config('search_path', '', false); +SET check_function_bodies = false; +SET xmloption = content; +SET client_min_messages = warning; +SET row_security = off; + +-- +-- Name: mv_edge_oee_ucepsa_demo; Type: SCHEMA; Schema: -; Owner: mesavault +-- + +CREATE SCHEMA mv_edge_oee_ucepsa_demo; + + +ALTER SCHEMA mv_edge_oee_ucepsa_demo OWNER TO mesavault; + +-- +-- Name: SCHEMA mv_edge_oee_ucepsa_demo; Type: COMMENT; Schema: -; Owner: mesavault +-- + +COMMENT ON SCHEMA mv_edge_oee_ucepsa_demo IS 'Capa normalizada de la demo Edge-OEE UCEPSA. Inicialmente contiene vistas puente hacia mv_hot.'; + + +-- +-- Name: mv_edge_oee_ucepsa_prod; Type: SCHEMA; Schema: -; Owner: mesavault +-- + +CREATE SCHEMA mv_edge_oee_ucepsa_prod; + + +ALTER SCHEMA mv_edge_oee_ucepsa_prod OWNER TO mesavault; + +-- +-- Name: mv_hot; Type: SCHEMA; Schema: -; Owner: mesavault +-- + +CREATE SCHEMA mv_hot; + + +ALTER SCHEMA mv_hot OWNER TO mesavault; + +-- +-- Name: mv_reports_ucepsa_demo; Type: SCHEMA; Schema: -; Owner: mesavault +-- + +CREATE SCHEMA mv_reports_ucepsa_demo; + + +ALTER SCHEMA mv_reports_ucepsa_demo OWNER TO mesavault; + +-- +-- Name: SCHEMA mv_reports_ucepsa_demo; Type: COMMENT; Schema: -; Owner: mesavault +-- + +COMMENT ON SCHEMA mv_reports_ucepsa_demo IS 'Capa de reporting para Grafana UCEPSA demo. Grafana debe consultar preferentemente este schema.'; + + +-- +-- Name: mv_reports_ucepsa_prod; Type: SCHEMA; Schema: -; Owner: mesavault +-- + +CREATE SCHEMA mv_reports_ucepsa_prod; + + +ALTER SCHEMA mv_reports_ucepsa_prod OWNER TO mesavault; + +-- +-- Name: oee_kpis(timestamp with time zone, timestamp with time zone, text, text); Type: FUNCTION; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +CREATE FUNCTION mv_reports_ucepsa_demo.oee_kpis(p_from timestamp with time zone, p_to timestamp with time zone, p_machine_id text DEFAULT 'CORT-01'::text, p_asset text DEFAULT 'revpi_oee_node_01'::text) RETURNS TABLE(oee_pct numeric, availability_pct numeric, performance_pct numeric, quality_pct numeric) + LANGUAGE sql STABLE + AS $$ +WITH dashboard_bounds AS ( + SELECT + p_from AS dashboard_from, + LEAST(p_to, now()) AS dashboard_to +), +telemetry_span AS ( + SELECT + MIN(ingest_ts) AS first_ts, + MAX(ingest_ts) AS last_ts + FROM mv_hot.edge_oee_live_telemetry + WHERE asset = p_asset + AND ingest_ts >= (SELECT dashboard_from FROM dashboard_bounds) + AND ingest_ts <= (SELECT dashboard_to FROM dashboard_bounds) +), +bounds AS ( + SELECT + CASE + WHEN first_ts IS NULL THEN dashboard_from + ELSE GREATEST(dashboard_from, first_ts) + END AS t_from, + CASE + WHEN last_ts IS NULL THEN dashboard_to + ELSE LEAST(dashboard_to, last_ts) + END AS t_to + FROM dashboard_bounds + CROSS JOIN telemetry_span +), +target AS ( + SELECT + COALESCE(( + SELECT mt.target_cycle_rate_ppm + FROM mv_hot.edge_oee_machine_targets mt + WHERE mt.machine_id = p_machine_id + LIMIT 1 + ), 10.00)::numeric AS target_cycle_rate_ppm, + + COALESCE(( + SELECT mt.quality_pct_default + FROM mv_hot.edge_oee_machine_targets mt + WHERE mt.machine_id = p_machine_id + LIMIT 1 + ), 100.00)::numeric AS quality_pct_default +), +planned AS ( + SELECT + GREATEST(EXTRACT(EPOCH FROM (t_to - t_from))::numeric, 0) AS planned_s + FROM bounds +), +stops AS ( + SELECT + COALESCE( + SUM( + GREATEST( + EXTRACT(EPOCH FROM ( + LEAST(COALESCE(s.ended_at, b.t_to), b.t_to) + - + GREATEST( + CASE + WHEN s.started_at > s.created_at + interval '5 seconds' + THEN s.created_at + ELSE s.started_at + END, + b.t_from + ) + ))::numeric, + 0 + ) + ) FILTER ( + WHERE s.started_at < b.t_to + AND COALESCE(s.ended_at, now()) > b.t_from + ), + 0 + )::numeric AS stop_s + FROM mv_hot.edge_oee_machine_stops s + CROSS JOIN bounds b + WHERE s.machine_id = p_machine_id +), +cycles AS ( + SELECT + COALESCE( + GREATEST( + MAX(cycle_pulse_count) - MIN(cycle_pulse_count), + 0 + ), + 0 + )::numeric AS actual_cycles + FROM mv_hot.edge_oee_live_telemetry l + CROSS JOIN bounds b + WHERE l.asset = p_asset + AND l.ingest_ts >= b.t_from + AND l.ingest_ts <= b.t_to + AND l.cycle_pulse_count IS NOT NULL +), +calc AS ( + SELECT + p.planned_s, + st.stop_s, + GREATEST(p.planned_s - st.stop_s, 0)::numeric AS running_s, + c.actual_cycles, + t.target_cycle_rate_ppm, + t.quality_pct_default, + + CASE + WHEN p.planned_s > 0 + THEN ROUND(((GREATEST(p.planned_s - st.stop_s, 0) / p.planned_s) * 100)::numeric, 2) + ELSE 0 + END AS availability_pct, + + CASE + WHEN GREATEST(p.planned_s - st.stop_s, 0) > 0 + AND t.target_cycle_rate_ppm > 0 + THEN ROUND( + LEAST( + 100::numeric, + ( + c.actual_cycles + / + NULLIF( + t.target_cycle_rate_ppm * (GREATEST(p.planned_s - st.stop_s, 0) / 60.0), + 0 + ) + ) * 100 + )::numeric, + 2 + ) + ELSE 0 + END AS performance_pct, + + t.quality_pct_default AS quality_pct + FROM planned p + CROSS JOIN stops st + CROSS JOIN cycles c + CROSS JOIN target t +) +SELECT + ROUND(((calc.availability_pct * calc.performance_pct * calc.quality_pct) / 10000.0)::numeric, 2) AS oee_pct, + calc.availability_pct, + calc.performance_pct, + calc.quality_pct +FROM calc; +$$; + + +ALTER FUNCTION mv_reports_ucepsa_demo.oee_kpis(p_from timestamp with time zone, p_to timestamp with time zone, p_machine_id text, p_asset text) OWNER TO mesavault; + +-- +-- Name: oee_operational_data(timestamp with time zone, timestamp with time zone, text, text); Type: FUNCTION; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +CREATE FUNCTION mv_reports_ucepsa_demo.oee_operational_data(p_from timestamp with time zone, p_to timestamp with time zone, p_machine_id text DEFAULT 'CORT-01'::text, p_asset text DEFAULT 'revpi_oee_node_01'::text) RETURNS TABLE(running_min numeric, stop_min numeric, actual_cycles numeric) + LANGUAGE sql STABLE + AS $$ +WITH dashboard_bounds AS ( + SELECT + p_from AS dashboard_from, + LEAST(p_to, now()) AS dashboard_to +), +telemetry_span AS ( + SELECT + MIN(ingest_ts) AS first_ts, + MAX(ingest_ts) AS last_ts + FROM mv_hot.edge_oee_live_telemetry + WHERE asset = p_asset + AND ingest_ts >= (SELECT dashboard_from FROM dashboard_bounds) + AND ingest_ts <= (SELECT dashboard_to FROM dashboard_bounds) +), +bounds AS ( + SELECT + CASE + WHEN first_ts IS NULL THEN dashboard_from + ELSE GREATEST(dashboard_from, first_ts) + END AS t_from, + CASE + WHEN last_ts IS NULL THEN dashboard_to + ELSE LEAST(dashboard_to, last_ts) + END AS t_to + FROM dashboard_bounds + CROSS JOIN telemetry_span +), +planned AS ( + SELECT + GREATEST(EXTRACT(EPOCH FROM (t_to - t_from))::numeric, 0) AS planned_s + FROM bounds +), +stops AS ( + SELECT + COALESCE( + SUM( + GREATEST( + EXTRACT(EPOCH FROM ( + LEAST(COALESCE(s.ended_at, b.t_to), b.t_to) + - + GREATEST( + CASE + WHEN s.started_at > s.created_at + interval '5 seconds' + THEN s.created_at + ELSE s.started_at + END, + b.t_from + ) + ))::numeric, + 0 + ) + ) FILTER ( + WHERE s.started_at < b.t_to + AND COALESCE(s.ended_at, now()) > b.t_from + ), + 0 + )::numeric AS stop_s + FROM mv_hot.edge_oee_machine_stops s + CROSS JOIN bounds b + WHERE s.machine_id = p_machine_id +), +cycles AS ( + SELECT + COALESCE( + GREATEST( + MAX(cycle_pulse_count) - MIN(cycle_pulse_count), + 0 + ), + 0 + )::numeric AS actual_cycles + FROM mv_hot.edge_oee_live_telemetry l + CROSS JOIN bounds b + WHERE l.asset = p_asset + AND l.ingest_ts >= b.t_from + AND l.ingest_ts <= b.t_to + AND l.cycle_pulse_count IS NOT NULL +), +calc AS ( + SELECT + p.planned_s, + st.stop_s, + GREATEST(p.planned_s - st.stop_s, 0)::numeric AS running_s, + c.actual_cycles + FROM planned p + CROSS JOIN stops st + CROSS JOIN cycles c +) +SELECT + ROUND((running_s / 60.0)::numeric, 2) AS running_min, + ROUND((stop_s / 60.0)::numeric, 2) AS stop_min, + actual_cycles +FROM calc; +$$; + + +ALTER FUNCTION mv_reports_ucepsa_demo.oee_operational_data(p_from timestamp with time zone, p_to timestamp with time zone, p_machine_id text, p_asset text) OWNER TO mesavault; + +SET default_tablespace = ''; + +SET default_table_access_method = heap; + +-- +-- Name: data_quality_contract_rules; Type: TABLE; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +CREATE TABLE mv_edge_oee_ucepsa_demo.data_quality_contract_rules ( + id bigint NOT NULL, + tenant text DEFAULT 'ucepsa'::text NOT NULL, + site text DEFAULT 'ucepsa_test'::text NOT NULL, + rule_code text NOT NULL, + rule_name text NOT NULL, + domain text NOT NULL, + dimension text NOT NULL, + severity text NOT NULL, + owner_area text NOT NULL, + applies_to text NOT NULL, + description text NOT NULL, + expected_condition text NOT NULL, + blocking_for text, + is_active boolean DEFAULT true NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL +); + + +ALTER TABLE mv_edge_oee_ucepsa_demo.data_quality_contract_rules OWNER TO mesavault; + +-- +-- Name: data_quality_contract_rules_id_seq; Type: SEQUENCE; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +CREATE SEQUENCE mv_edge_oee_ucepsa_demo.data_quality_contract_rules_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER SEQUENCE mv_edge_oee_ucepsa_demo.data_quality_contract_rules_id_seq OWNER TO mesavault; + +-- +-- Name: data_quality_contract_rules_id_seq; Type: SEQUENCE OWNED BY; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +ALTER SEQUENCE mv_edge_oee_ucepsa_demo.data_quality_contract_rules_id_seq OWNED BY mv_edge_oee_ucepsa_demo.data_quality_contract_rules.id; + + +-- +-- Name: edge_oee_eastron_readings; Type: TABLE; Schema: mv_hot; Owner: mesavault +-- + +CREATE TABLE mv_hot.edge_oee_eastron_readings ( + id bigint NOT NULL, + ts timestamp with time zone DEFAULT now() NOT NULL, + tenant text DEFAULT 'ucepsa'::text NOT NULL, + site text DEFAULT 'demo_edge_oee'::text NOT NULL, + asset text DEFAULT 'revpi_eastron_demo'::text NOT NULL, + source text DEFAULT 'eastron_reader'::text NOT NULL, + meter_model text, + meter_id text, + machine_id text, + order_id text, + voltage_l1_v numeric(10,3), + voltage_l2_v numeric(10,3), + voltage_l3_v numeric(10,3), + current_l1_a numeric(10,3), + current_l2_a numeric(10,3), + current_l3_a numeric(10,3), + power_l1_kw numeric(10,3), + power_l2_kw numeric(10,3), + power_l3_kw numeric(10,3), + power_total_kw numeric(10,3), + frequency_hz numeric(10,3), + import_kwh numeric(14,3), + export_kwh numeric(14,3), + comm_ok boolean DEFAULT true NOT NULL, + error_text text, + payload_json jsonb DEFAULT '{}'::jsonb NOT NULL, + ingest_ts timestamp with time zone DEFAULT now() NOT NULL +); + + +ALTER TABLE mv_hot.edge_oee_eastron_readings OWNER TO mesavault; + +-- +-- Name: edge_oee_eastron_latest; Type: VIEW; Schema: mv_hot; Owner: mesavault +-- + +CREATE VIEW mv_hot.edge_oee_eastron_latest AS + SELECT DISTINCT ON (asset) id, + ts, + tenant, + site, + asset, + source, + meter_model, + meter_id, + machine_id, + order_id, + voltage_l1_v, + voltage_l2_v, + voltage_l3_v, + current_l1_a, + current_l2_a, + current_l3_a, + power_l1_kw, + power_l2_kw, + power_l3_kw, + power_total_kw, + frequency_hz, + import_kwh, + export_kwh, + comm_ok, + error_text, + payload_json, + ingest_ts + FROM mv_hot.edge_oee_eastron_readings + ORDER BY asset, ingest_ts DESC; + + +ALTER VIEW mv_hot.edge_oee_eastron_latest OWNER TO mesavault; + +-- +-- Name: edge_oee_eastron_dashboard; Type: VIEW; Schema: mv_hot; Owner: mesavault +-- + +CREATE VIEW mv_hot.edge_oee_eastron_dashboard AS + SELECT ts, + ingest_ts, + GREATEST((EXTRACT(epoch FROM (now() - ingest_ts)))::integer, 0) AS age_s, + tenant, + site, + asset, + meter_model, + meter_id, + machine_id, + voltage_l1_v, + voltage_l2_v, + voltage_l3_v, + current_l1_a, + current_l2_a, + current_l3_a, + power_total_kw, + frequency_hz, + import_kwh, + export_kwh, + comm_ok, + error_text, + CASE + WHEN (comm_ok = false) THEN 2 + WHEN (GREATEST((EXTRACT(epoch FROM (now() - ingest_ts)))::integer, 0) > 180) THEN 2 + WHEN (GREATEST((EXTRACT(epoch FROM (now() - ingest_ts)))::integer, 0) > 60) THEN 1 + ELSE 0 + END AS comm_risk_level, + CASE + WHEN (comm_ok = false) THEN 'Sin comunicación Modbus'::text + WHEN (GREATEST((EXTRACT(epoch FROM (now() - ingest_ts)))::integer, 0) > 180) THEN 'Lectura antigua'::text + WHEN (GREATEST((EXTRACT(epoch FROM (now() - ingest_ts)))::integer, 0) > 60) THEN 'Lectura no reciente'::text + ELSE 'Comunicación correcta'::text + END AS comm_status + FROM mv_hot.edge_oee_eastron_latest; + + +ALTER VIEW mv_hot.edge_oee_eastron_dashboard OWNER TO mesavault; + +-- +-- Name: eastron_dashboard; Type: VIEW; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +CREATE VIEW mv_edge_oee_ucepsa_demo.eastron_dashboard AS + SELECT ts, + ingest_ts, + age_s, + tenant, + site, + asset, + meter_model, + meter_id, + machine_id, + voltage_l1_v, + voltage_l2_v, + voltage_l3_v, + current_l1_a, + current_l2_a, + current_l3_a, + power_total_kw, + frequency_hz, + import_kwh, + export_kwh, + comm_ok, + error_text, + comm_risk_level, + comm_status + FROM mv_hot.edge_oee_eastron_dashboard; + + +ALTER VIEW mv_edge_oee_ucepsa_demo.eastron_dashboard OWNER TO mesavault; + +-- +-- Name: edge_oee_eastron_diagnosis_latest; Type: VIEW; Schema: mv_hot; Owner: mesavault +-- + +CREATE VIEW mv_hot.edge_oee_eastron_diagnosis_latest AS + SELECT 1 AS sort_order, + 'Comunicación'::text AS group_name, + 'Edad último dato'::text AS metric, + (edge_oee_eastron_dashboard.age_s)::numeric AS value, + 's'::text AS unit, + CASE + WHEN (edge_oee_eastron_dashboard.comm_risk_level = 0) THEN 'OK'::text + WHEN (edge_oee_eastron_dashboard.comm_risk_level = 1) THEN 'Warning'::text + ELSE 'Peligro'::text + END AS status, + CASE + WHEN (edge_oee_eastron_dashboard.comm_risk_level = 0) THEN 'Lectura reciente recibida por PostgreSQL'::text + ELSE 'La lectura no es reciente o hay fallo de comunicación'::text + END AS problem, + CASE + WHEN (edge_oee_eastron_dashboard.comm_risk_level = 0) THEN 'La captura industrial está viva'::text + ELSE 'El dashboard puede estar mostrando un valor antiguo'::text + END AS likely_impact, + CASE + WHEN (edge_oee_eastron_dashboard.comm_risk_level = 0) THEN 'Sin acción'::text + ELSE 'Revisar RevPi, MQTT, sink, RS-485, SDM120-M y logs'::text + END AS recommended_action + FROM mv_hot.edge_oee_eastron_dashboard +UNION ALL + SELECT 2 AS sort_order, + 'Energía'::text AS group_name, + 'Potencia total'::text AS metric, + edge_oee_eastron_dashboard.power_total_kw AS value, + 'kW'::text AS unit, + CASE + WHEN (edge_oee_eastron_dashboard.power_total_kw IS NULL) THEN 'Warning'::text + ELSE 'OK'::text + END AS status, + CASE + WHEN (edge_oee_eastron_dashboard.power_total_kw IS NULL) THEN 'No hay potencia total disponible'::text + ELSE 'Potencia total leída correctamente'::text + END AS problem, + CASE + WHEN (edge_oee_eastron_dashboard.power_total_kw IS NULL) THEN 'No se puede validar carga eléctrica'::text + ELSE 'Permite demostrar captura energética industrial'::text + END AS likely_impact, + CASE + WHEN (edge_oee_eastron_dashboard.power_total_kw IS NULL) THEN 'Revisar mapa de registros Modbus'::text + ELSE 'Sin acción'::text + END AS recommended_action + FROM mv_hot.edge_oee_eastron_dashboard; + + +ALTER VIEW mv_hot.edge_oee_eastron_diagnosis_latest OWNER TO mesavault; + +-- +-- Name: eastron_diagnosis_latest; Type: VIEW; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +CREATE VIEW mv_edge_oee_ucepsa_demo.eastron_diagnosis_latest AS + SELECT sort_order, + group_name, + metric, + value, + unit, + status, + problem, + likely_impact, + recommended_action + FROM mv_hot.edge_oee_eastron_diagnosis_latest; + + +ALTER VIEW mv_edge_oee_ucepsa_demo.eastron_diagnosis_latest OWNER TO mesavault; + +-- +-- Name: eastron_latest; Type: VIEW; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +CREATE VIEW mv_edge_oee_ucepsa_demo.eastron_latest AS + SELECT id, + ts, + tenant, + site, + asset, + source, + meter_model, + meter_id, + machine_id, + order_id, + voltage_l1_v, + voltage_l2_v, + voltage_l3_v, + current_l1_a, + current_l2_a, + current_l3_a, + power_l1_kw, + power_l2_kw, + power_l3_kw, + power_total_kw, + frequency_hz, + import_kwh, + export_kwh, + comm_ok, + error_text, + payload_json, + ingest_ts + FROM mv_hot.edge_oee_eastron_latest; + + +ALTER VIEW mv_edge_oee_ucepsa_demo.eastron_latest OWNER TO mesavault; + +-- +-- Name: eastron_readings; Type: VIEW; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +CREATE VIEW mv_edge_oee_ucepsa_demo.eastron_readings AS + SELECT id, + ts, + tenant, + site, + asset, + source, + meter_model, + meter_id, + machine_id, + order_id, + voltage_l1_v, + voltage_l2_v, + voltage_l3_v, + current_l1_a, + current_l2_a, + current_l3_a, + power_l1_kw, + power_l2_kw, + power_l3_kw, + power_total_kw, + frequency_hz, + import_kwh, + export_kwh, + comm_ok, + error_text, + payload_json, + ingest_ts + FROM mv_hot.edge_oee_eastron_readings; + + +ALTER VIEW mv_edge_oee_ucepsa_demo.eastron_readings OWNER TO mesavault; + +-- +-- Name: edge_oee_live_telemetry; Type: VIEW; Schema: mv_hot; Owner: mesavault +-- + +CREATE VIEW mv_hot.edge_oee_live_telemetry AS + SELECT ts, + ingest_ts, + GREATEST((EXTRACT(epoch FROM (now() - ingest_ts)))::integer, 0) AS age_s, + tenant, + site, + asset, + COALESCE(machine_id, (payload_json ->> 'machine_id'::text)) AS machine_id, + COALESCE(order_id, (payload_json ->> 'order_id'::text)) AS order_id, + voltage_l1_v, + current_l1_a, + power_total_kw, + frequency_hz, + import_kwh, + export_kwh, + comm_ok, + error_text, + CASE + WHEN (payload_json ? 'machine_auto_signal'::text) THEN ((payload_json ->> 'machine_auto_signal'::text))::boolean + ELSE NULL::boolean + END AS machine_auto_signal, + CASE + WHEN (payload_json ? 'machine_running'::text) THEN ((payload_json ->> 'machine_running'::text))::boolean + ELSE NULL::boolean + END AS machine_running, + CASE + WHEN (payload_json ? 'cycle_pulse_count'::text) THEN ((payload_json ->> 'cycle_pulse_count'::text))::bigint + ELSE NULL::bigint + END AS cycle_pulse_count, + CASE + WHEN (payload_json ? 'cycle_rate_ppm'::text) THEN ((payload_json ->> 'cycle_rate_ppm'::text))::numeric + ELSE NULL::numeric + END AS cycle_rate_ppm, + CASE + WHEN ((payload_json ? 'last_cycle_age_s'::text) AND ((payload_json ->> 'last_cycle_age_s'::text) IS NOT NULL) AND ((payload_json ->> 'last_cycle_age_s'::text) <> 'null'::text)) THEN ((payload_json ->> 'last_cycle_age_s'::text))::numeric + ELSE NULL::numeric + END AS last_cycle_age_s, + (payload_json -> 'digital_inputs'::text) AS digital_inputs, + payload_json + FROM mv_hot.edge_oee_eastron_readings r + WHERE (asset = 'revpi_oee_node_01'::text) + ORDER BY ingest_ts DESC; + + +ALTER VIEW mv_hot.edge_oee_live_telemetry OWNER TO mesavault; + +-- +-- Name: edge_oee_latest_machine_snapshot; Type: VIEW; Schema: mv_hot; Owner: mesavault +-- + +CREATE VIEW mv_hot.edge_oee_latest_machine_snapshot AS + SELECT DISTINCT ON (asset) ts, + ingest_ts, + age_s, + tenant, + site, + asset, + machine_id, + order_id, + voltage_l1_v, + current_l1_a, + power_total_kw, + frequency_hz, + import_kwh, + export_kwh, + comm_ok, + error_text, + machine_auto_signal, + machine_running, + cycle_pulse_count, + cycle_rate_ppm, + last_cycle_age_s, + digital_inputs, + payload_json + FROM mv_hot.edge_oee_live_telemetry + ORDER BY asset, ingest_ts DESC; + + +ALTER VIEW mv_hot.edge_oee_latest_machine_snapshot OWNER TO mesavault; + +-- +-- Name: latest_machine_snapshot; Type: VIEW; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +CREATE VIEW mv_edge_oee_ucepsa_demo.latest_machine_snapshot AS + SELECT ts, + ingest_ts, + age_s, + tenant, + site, + asset, + machine_id, + order_id, + voltage_l1_v, + current_l1_a, + power_total_kw, + frequency_hz, + import_kwh, + export_kwh, + comm_ok, + error_text, + machine_auto_signal, + machine_running, + cycle_pulse_count, + cycle_rate_ppm, + last_cycle_age_s, + digital_inputs, + payload_json + FROM mv_hot.edge_oee_latest_machine_snapshot; + + +ALTER VIEW mv_edge_oee_ucepsa_demo.latest_machine_snapshot OWNER TO mesavault; + +-- +-- Name: live_telemetry; Type: VIEW; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +CREATE VIEW mv_edge_oee_ucepsa_demo.live_telemetry AS + SELECT ts, + ingest_ts, + age_s, + tenant, + site, + asset, + machine_id, + order_id, + voltage_l1_v, + current_l1_a, + power_total_kw, + frequency_hz, + import_kwh, + export_kwh, + comm_ok, + error_text, + machine_auto_signal, + machine_running, + cycle_pulse_count, + cycle_rate_ppm, + last_cycle_age_s, + digital_inputs, + payload_json + FROM mv_hot.edge_oee_live_telemetry; + + +ALTER VIEW mv_edge_oee_ucepsa_demo.live_telemetry OWNER TO mesavault; + +-- +-- Name: edge_oee_node_events; Type: TABLE; Schema: mv_hot; Owner: mesavault +-- + +CREATE TABLE mv_hot.edge_oee_node_events ( + id bigint NOT NULL, + received_at timestamp with time zone DEFAULT now() NOT NULL, + event_ts timestamp with time zone NOT NULL, + topic text NOT NULL, + tenant text NOT NULL, + site text NOT NULL, + vertical text NOT NULL, + asset text NOT NULL, + machine_id text, + order_id text, + event_type text NOT NULL, + event_name text NOT NULL, + previous_auto_signal boolean, + new_auto_signal boolean, + machine_running boolean, + reason_pending boolean, + cycle_pulse_count bigint, + cycle_rate_ppm numeric(10,2), + last_cycle_age_s numeric(10,2), + payload_json jsonb DEFAULT '{}'::jsonb NOT NULL +); + + +ALTER TABLE mv_hot.edge_oee_node_events OWNER TO mesavault; + +-- +-- Name: edge_oee_machine_state_latest; Type: VIEW; Schema: mv_hot; Owner: mesavault +-- + +CREATE VIEW mv_hot.edge_oee_machine_state_latest AS + SELECT DISTINCT ON (asset) received_at, + (EXTRACT(epoch FROM (now() - received_at)))::integer AS age_s, + event_ts, + tenant, + site, + vertical, + asset, + machine_id, + event_name, + previous_auto_signal, + new_auto_signal, + machine_running, + reason_pending, + cycle_pulse_count, + cycle_rate_ppm, + last_cycle_age_s + FROM mv_hot.edge_oee_node_events + WHERE (event_type = 'machine_state_changed'::text) + ORDER BY asset, event_ts DESC; + + +ALTER VIEW mv_hot.edge_oee_machine_state_latest OWNER TO mesavault; + +-- +-- Name: machine_state_latest; Type: VIEW; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +CREATE VIEW mv_edge_oee_ucepsa_demo.machine_state_latest AS + SELECT received_at, + age_s, + event_ts, + tenant, + site, + vertical, + asset, + machine_id, + event_name, + previous_auto_signal, + new_auto_signal, + machine_running, + reason_pending, + cycle_pulse_count, + cycle_rate_ppm, + last_cycle_age_s + FROM mv_hot.edge_oee_machine_state_latest; + + +ALTER VIEW mv_edge_oee_ucepsa_demo.machine_state_latest OWNER TO mesavault; + +-- +-- Name: edge_oee_machine_stops; Type: TABLE; Schema: mv_hot; Owner: mesavault +-- + +CREATE TABLE mv_hot.edge_oee_machine_stops ( + id bigint NOT NULL, + tenant text NOT NULL, + site text NOT NULL, + vertical text NOT NULL, + asset text NOT NULL, + machine_id text, + order_id text, + started_at timestamp with time zone NOT NULL, + ended_at timestamp with time zone, + status text DEFAULT 'OPEN'::text NOT NULL, + classification_status text DEFAULT 'PENDING'::text NOT NULL, + cause_code text, + operator_note text, + value_hour_eur numeric(10,2) DEFAULT 36.00 NOT NULL, + duration_s numeric(14,2), + cost_eur numeric(14,2), + start_event_name text, + end_event_name text, + start_payload_json jsonb DEFAULT '{}'::jsonb NOT NULL, + end_payload_json jsonb DEFAULT '{}'::jsonb NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL +); + + +ALTER TABLE mv_hot.edge_oee_machine_stops OWNER TO mesavault; + +-- +-- Name: machine_stops; Type: VIEW; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +CREATE VIEW mv_edge_oee_ucepsa_demo.machine_stops AS + SELECT id, + tenant, + site, + vertical, + asset, + machine_id, + order_id, + started_at, + ended_at, + status, + classification_status, + cause_code, + operator_note, + value_hour_eur, + duration_s, + cost_eur, + start_event_name, + end_event_name, + start_payload_json, + end_payload_json, + created_at, + updated_at + FROM mv_hot.edge_oee_machine_stops; + + +ALTER VIEW mv_edge_oee_ucepsa_demo.machine_stops OWNER TO mesavault; + +-- +-- Name: edge_oee_machine_targets; Type: TABLE; Schema: mv_hot; Owner: mesavault +-- + +CREATE TABLE mv_hot.edge_oee_machine_targets ( + machine_id text NOT NULL, + machine_name text NOT NULL, + asset text NOT NULL, + target_cycle_rate_ppm numeric(10,2) DEFAULT 10.00 NOT NULL, + value_hour_eur numeric(10,2) DEFAULT 36.00 NOT NULL, + quality_pct_default numeric(10,2) DEFAULT 100.00 NOT NULL, + notes text +); + + +ALTER TABLE mv_hot.edge_oee_machine_targets OWNER TO mesavault; + +-- +-- Name: machine_targets; Type: VIEW; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +CREATE VIEW mv_edge_oee_ucepsa_demo.machine_targets AS + SELECT machine_id, + machine_name, + asset, + target_cycle_rate_ppm, + value_hour_eur, + quality_pct_default, + notes + FROM mv_hot.edge_oee_machine_targets; + + +ALTER VIEW mv_edge_oee_ucepsa_demo.machine_targets OWNER TO mesavault; + +-- +-- Name: margin_cost_parameters; Type: TABLE; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +CREATE TABLE mv_edge_oee_ucepsa_demo.margin_cost_parameters ( + id bigint NOT NULL, + tenant text DEFAULT 'ucepsa'::text NOT NULL, + site text DEFAULT 'ucepsa_test'::text NOT NULL, + scope_level text DEFAULT 'GLOBAL'::text NOT NULL, + machine_id text, + product_default_code text, + energy_cost_eur_per_kwh numeric(14,6), + material_cost_eur_per_kg numeric(14,6), + machine_run_cost_eur_per_h numeric(14,4), + labor_cost_eur_per_h numeric(14,4), + overhead_cost_eur_per_h numeric(14,4), + valid_from timestamp with time zone DEFAULT now() NOT NULL, + valid_to timestamp with time zone, + is_active boolean DEFAULT true NOT NULL, + notes text, + created_at timestamp with time zone DEFAULT now() NOT NULL +); + + +ALTER TABLE mv_edge_oee_ucepsa_demo.margin_cost_parameters OWNER TO mesavault; + +-- +-- Name: margin_cost_parameters_id_seq; Type: SEQUENCE; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +CREATE SEQUENCE mv_edge_oee_ucepsa_demo.margin_cost_parameters_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER SEQUENCE mv_edge_oee_ucepsa_demo.margin_cost_parameters_id_seq OWNER TO mesavault; + +-- +-- Name: margin_cost_parameters_id_seq; Type: SEQUENCE OWNED BY; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +ALTER SEQUENCE mv_edge_oee_ucepsa_demo.margin_cost_parameters_id_seq OWNED BY mv_edge_oee_ucepsa_demo.margin_cost_parameters.id; + + +-- +-- Name: node_events; Type: VIEW; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +CREATE VIEW mv_edge_oee_ucepsa_demo.node_events AS + SELECT id, + received_at, + event_ts, + topic, + tenant, + site, + vertical, + asset, + machine_id, + order_id, + event_type, + event_name, + previous_auto_signal, + new_auto_signal, + machine_running, + reason_pending, + cycle_pulse_count, + cycle_rate_ppm, + last_cycle_age_s, + payload_json + FROM mv_hot.edge_oee_node_events; + + +ALTER VIEW mv_edge_oee_ucepsa_demo.node_events OWNER TO mesavault; + +-- +-- Name: edge_oee_node_events_recent; Type: VIEW; Schema: mv_hot; Owner: mesavault +-- + +CREATE VIEW mv_hot.edge_oee_node_events_recent AS + SELECT received_at, + (EXTRACT(epoch FROM (now() - received_at)))::integer AS age_s, + event_ts, + tenant, + site, + vertical, + asset, + machine_id, + order_id, + event_type, + event_name, + previous_auto_signal, + new_auto_signal, + machine_running, + reason_pending, + cycle_pulse_count, + cycle_rate_ppm, + last_cycle_age_s, + topic, + payload_json + FROM mv_hot.edge_oee_node_events + ORDER BY event_ts DESC; + + +ALTER VIEW mv_hot.edge_oee_node_events_recent OWNER TO mesavault; + +-- +-- Name: node_events_recent; Type: VIEW; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +CREATE VIEW mv_edge_oee_ucepsa_demo.node_events_recent AS + SELECT received_at, + age_s, + event_ts, + tenant, + site, + vertical, + asset, + machine_id, + order_id, + event_type, + event_name, + previous_auto_signal, + new_auto_signal, + machine_running, + reason_pending, + cycle_pulse_count, + cycle_rate_ppm, + last_cycle_age_s, + topic, + payload_json + FROM mv_hot.edge_oee_node_events_recent; + + +ALTER VIEW mv_edge_oee_ucepsa_demo.node_events_recent OWNER TO mesavault; + +-- +-- Name: odoo_order_commercials; Type: TABLE; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +CREATE TABLE mv_edge_oee_ucepsa_demo.odoo_order_commercials ( + id bigint NOT NULL, + tenant text DEFAULT 'ucepsa'::text NOT NULL, + site text DEFAULT 'ucepsa_test'::text NOT NULL, + odoo_instance text NOT NULL, + odoo_production_id integer NOT NULL, + odoo_order_ref text NOT NULL, + odoo_origin text, + odoo_state text, + product_id integer, + product_default_code text, + product_name text, + product_uom text, + product_standard_price numeric(14,4), + product_list_price numeric(14,4), + production_qty numeric(14,3), + qty_producing numeric(14,3), + production_uom text, + sale_line_id integer, + sale_order_id integer, + sale_order_ref text, + customer_id integer, + customer_name text, + sale_state text, + sale_date_order timestamp with time zone, + currency text, + sale_line_qty numeric(14,3), + sale_line_qty_delivered numeric(14,3), + sale_price_unit numeric(14,4), + sale_discount_pct numeric(10,2), + revenue_net_eur numeric(14,4), + revenue_total_eur numeric(14,4), + raw_material_value_eur numeric(14,4), + finished_value_eur numeric(14,4), + material_cost_status text DEFAULT 'UNAVAILABLE'::text NOT NULL, + stock_raw_moves_json jsonb DEFAULT '[]'::jsonb NOT NULL, + stock_finished_moves_json jsonb DEFAULT '[]'::jsonb NOT NULL, + raw_json jsonb DEFAULT '{}'::jsonb NOT NULL, + snapshot_at timestamp with time zone DEFAULT now() NOT NULL +); + + +ALTER TABLE mv_edge_oee_ucepsa_demo.odoo_order_commercials OWNER TO mesavault; + +-- +-- Name: odoo_order_commercials_id_seq; Type: SEQUENCE; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +CREATE SEQUENCE mv_edge_oee_ucepsa_demo.odoo_order_commercials_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER SEQUENCE mv_edge_oee_ucepsa_demo.odoo_order_commercials_id_seq OWNER TO mesavault; + +-- +-- Name: odoo_order_commercials_id_seq; Type: SEQUENCE OWNED BY; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +ALTER SEQUENCE mv_edge_oee_ucepsa_demo.odoo_order_commercials_id_seq OWNED BY mv_edge_oee_ucepsa_demo.odoo_order_commercials.id; + + +-- +-- Name: odoo_workcenter_map; Type: TABLE; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +CREATE TABLE mv_edge_oee_ucepsa_demo.odoo_workcenter_map ( + id bigint NOT NULL, + tenant text NOT NULL, + site text NOT NULL, + odoo_instance text NOT NULL, + odoo_workcenter_id integer, + odoo_workcenter_code text NOT NULL, + odoo_workcenter_name text NOT NULL, + machine_id text NOT NULL, + asset text NOT NULL, + is_active boolean DEFAULT true NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL +); + + +ALTER TABLE mv_edge_oee_ucepsa_demo.odoo_workcenter_map OWNER TO mesavault; + +-- +-- Name: odoo_workcenter_map_id_seq; Type: SEQUENCE; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +CREATE SEQUENCE mv_edge_oee_ucepsa_demo.odoo_workcenter_map_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER SEQUENCE mv_edge_oee_ucepsa_demo.odoo_workcenter_map_id_seq OWNER TO mesavault; + +-- +-- Name: odoo_workcenter_map_id_seq; Type: SEQUENCE OWNED BY; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +ALTER SEQUENCE mv_edge_oee_ucepsa_demo.odoo_workcenter_map_id_seq OWNED BY mv_edge_oee_ucepsa_demo.odoo_workcenter_map.id; + + +-- +-- Name: odoo_workorders_demo; Type: TABLE; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +CREATE TABLE mv_edge_oee_ucepsa_demo.odoo_workorders_demo ( + id bigint NOT NULL, + tenant text DEFAULT 'ucepsa'::text NOT NULL, + site text DEFAULT 'demo_edge_oee'::text NOT NULL, + odoo_instance text DEFAULT 'odoo_demo'::text NOT NULL, + odoo_workorder_id integer, + odoo_production_id integer, + odoo_order_ref text NOT NULL, + odoo_state text NOT NULL, + odoo_workcenter_code text NOT NULL, + odoo_workcenter_name text NOT NULL, + machine_id text NOT NULL, + asset text NOT NULL, + product text NOT NULL, + qty_planned numeric(14,2), + qty_done numeric(14,2), + uom text, + planned_cycles numeric(14,2), + target_cycle_rate_ppm numeric(10,2), + odoo_start timestamp with time zone NOT NULL, + odoo_end timestamp with time zone, + notes text, + snapshot_at timestamp with time zone DEFAULT now() NOT NULL +); + + +ALTER TABLE mv_edge_oee_ucepsa_demo.odoo_workorders_demo OWNER TO mesavault; + +-- +-- Name: odoo_workorders_demo_id_seq; Type: SEQUENCE; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +CREATE SEQUENCE mv_edge_oee_ucepsa_demo.odoo_workorders_demo_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER SEQUENCE mv_edge_oee_ucepsa_demo.odoo_workorders_demo_id_seq OWNER TO mesavault; + +-- +-- Name: odoo_workorders_demo_id_seq; Type: SEQUENCE OWNED BY; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +ALTER SEQUENCE mv_edge_oee_ucepsa_demo.odoo_workorders_demo_id_seq OWNED BY mv_edge_oee_ucepsa_demo.odoo_workorders_demo.id; + + +-- +-- Name: edge_oee_stop_causes; Type: TABLE; Schema: mv_hot; Owner: mesavault +-- + +CREATE TABLE mv_hot.edge_oee_stop_causes ( + cause_code text NOT NULL, + cause_name text NOT NULL, + description text, + is_planned boolean DEFAULT false NOT NULL, + is_active boolean DEFAULT true NOT NULL, + sort_order integer DEFAULT 100 NOT NULL +); + + +ALTER TABLE mv_hot.edge_oee_stop_causes OWNER TO mesavault; + +-- +-- Name: edge_oee_open_stops; Type: VIEW; Schema: mv_hot; Owner: mesavault +-- + +CREATE VIEW mv_hot.edge_oee_open_stops AS + WITH src AS ( + SELECT s_1.id, + s_1.tenant, + s_1.site, + s_1.vertical, + s_1.asset, + s_1.machine_id, + s_1.order_id, + s_1.started_at, + s_1.ended_at, + s_1.status, + s_1.classification_status, + s_1.cause_code, + s_1.operator_note, + s_1.value_hour_eur, + s_1.duration_s, + s_1.cost_eur, + s_1.start_event_name, + s_1.end_event_name, + s_1.start_payload_json, + s_1.end_payload_json, + s_1.created_at, + s_1.updated_at, + CASE + WHEN (s_1.started_at > (s_1.created_at + '00:00:05'::interval)) THEN s_1.created_at + ELSE s_1.started_at + END AS effective_started_at + FROM mv_hot.edge_oee_machine_stops s_1 + ) + SELECT s.id, + s.tenant, + s.site, + s.vertical, + s.asset, + s.machine_id, + s.order_id, + s.started_at, + (GREATEST(EXTRACT(epoch FROM (now() - s.effective_started_at)), (0)::numeric))::numeric(14,2) AS current_duration_s, + round((GREATEST(EXTRACT(epoch FROM (now() - s.effective_started_at)), (0)::numeric) / 60.0), 2) AS current_duration_min, + round(((GREATEST(EXTRACT(epoch FROM (now() - s.effective_started_at)), (0)::numeric) / 3600.0) * s.value_hour_eur), 2) AS current_cost_eur, + s.status, + s.classification_status, + s.cause_code, + c.cause_name, + s.operator_note, + s.value_hour_eur + FROM (src s + LEFT JOIN mv_hot.edge_oee_stop_causes c ON ((c.cause_code = s.cause_code))) + WHERE (s.status = 'OPEN'::text) + ORDER BY s.started_at DESC; + + +ALTER VIEW mv_hot.edge_oee_open_stops OWNER TO mesavault; + +-- +-- Name: open_stops; Type: VIEW; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +CREATE VIEW mv_edge_oee_ucepsa_demo.open_stops AS + SELECT id, + tenant, + site, + vertical, + asset, + machine_id, + order_id, + started_at, + current_duration_s, + current_duration_min, + current_cost_eur, + status, + classification_status, + cause_code, + cause_name, + operator_note, + value_hour_eur + FROM mv_hot.edge_oee_open_stops; + + +ALTER VIEW mv_edge_oee_ucepsa_demo.open_stops OWNER TO mesavault; + +-- +-- Name: raw_material_cost_parameters; Type: TABLE; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +CREATE TABLE mv_edge_oee_ucepsa_demo.raw_material_cost_parameters ( + id bigint NOT NULL, + tenant text DEFAULT 'ucepsa'::text NOT NULL, + site text DEFAULT 'ucepsa_test'::text NOT NULL, + raw_product_default_code text NOT NULL, + raw_product_name text, + material_cost_eur_per_kg numeric(14,6) NOT NULL, + source text DEFAULT 'MANUAL'::text NOT NULL, + valid_from timestamp with time zone DEFAULT now() NOT NULL, + valid_to timestamp with time zone, + is_active boolean DEFAULT true NOT NULL, + notes text, + created_at timestamp with time zone DEFAULT now() NOT NULL +); + + +ALTER TABLE mv_edge_oee_ucepsa_demo.raw_material_cost_parameters OWNER TO mesavault; + +-- +-- Name: raw_material_cost_parameters_id_seq; Type: SEQUENCE; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +CREATE SEQUENCE mv_edge_oee_ucepsa_demo.raw_material_cost_parameters_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER SEQUENCE mv_edge_oee_ucepsa_demo.raw_material_cost_parameters_id_seq OWNER TO mesavault; + +-- +-- Name: raw_material_cost_parameters_id_seq; Type: SEQUENCE OWNED BY; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +ALTER SEQUENCE mv_edge_oee_ucepsa_demo.raw_material_cost_parameters_id_seq OWNED BY mv_edge_oee_ucepsa_demo.raw_material_cost_parameters.id; + + +-- +-- Name: edge_oee_stop_cause_options; Type: VIEW; Schema: mv_hot; Owner: mesavault +-- + +CREATE VIEW mv_hot.edge_oee_stop_cause_options AS + SELECT cause_code, + cause_name, + description, + is_planned, + sort_order + FROM mv_hot.edge_oee_stop_causes + WHERE (is_active = true) + ORDER BY sort_order, cause_name; + + +ALTER VIEW mv_hot.edge_oee_stop_cause_options OWNER TO mesavault; + +-- +-- Name: stop_cause_options; Type: VIEW; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +CREATE VIEW mv_edge_oee_ucepsa_demo.stop_cause_options AS + SELECT cause_code, + cause_name, + description, + is_planned, + sort_order + FROM mv_hot.edge_oee_stop_cause_options; + + +ALTER VIEW mv_edge_oee_ucepsa_demo.stop_cause_options OWNER TO mesavault; + +-- +-- Name: stop_causes; Type: VIEW; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +CREATE VIEW mv_edge_oee_ucepsa_demo.stop_causes AS + SELECT cause_code, + cause_name, + description, + is_planned, + is_active, + sort_order + FROM mv_hot.edge_oee_stop_causes; + + +ALTER VIEW mv_edge_oee_ucepsa_demo.stop_causes OWNER TO mesavault; + +-- +-- Name: edge_oee_stop_summary_demo; Type: VIEW; Schema: mv_hot; Owner: mesavault +-- + +CREATE VIEW mv_hot.edge_oee_stop_summary_demo AS + WITH src AS ( + SELECT s.id, + s.tenant, + s.site, + s.vertical, + s.asset, + s.machine_id, + s.order_id, + s.started_at, + s.ended_at, + s.status, + s.classification_status, + s.cause_code, + s.operator_note, + s.value_hour_eur, + s.duration_s, + s.cost_eur, + s.start_event_name, + s.end_event_name, + s.start_payload_json, + s.end_payload_json, + s.created_at, + s.updated_at, + c.cause_name, + CASE + WHEN (s.started_at > (s.created_at + '00:00:05'::interval)) THEN s.created_at + ELSE s.started_at + END AS effective_started_at + FROM (mv_hot.edge_oee_machine_stops s + LEFT JOIN mv_hot.edge_oee_stop_causes c ON ((c.cause_code = s.cause_code))) + ), base AS ( + SELECT src.id, + src.tenant, + src.site, + src.vertical, + src.asset, + src.machine_id, + src.order_id, + src.started_at, + src.ended_at, + src.status, + src.classification_status, + src.cause_code, + src.operator_note, + src.value_hour_eur, + src.duration_s, + src.cost_eur, + src.start_event_name, + src.end_event_name, + src.start_payload_json, + src.end_payload_json, + src.created_at, + src.updated_at, + src.cause_name, + src.effective_started_at, + CASE + WHEN (src.duration_s IS NOT NULL) THEN GREATEST(src.duration_s, (0)::numeric) + WHEN (src.ended_at IS NOT NULL) THEN GREATEST(EXTRACT(epoch FROM (src.ended_at - src.effective_started_at)), (0)::numeric) + ELSE GREATEST(EXTRACT(epoch FROM (now() - src.effective_started_at)), (0)::numeric) + END AS safe_duration_s + FROM src + ) + SELECT COALESCE(cause_name, 'Pendiente de clasificar'::text) AS cause_name, + COALESCE(cause_code, 'pending'::text) AS cause_code, + count(*) AS stop_count, + round((sum(safe_duration_s) / 60.0), 2) AS total_min, + round(sum(COALESCE(cost_eur, ((safe_duration_s / 3600.0) * value_hour_eur))), 2) AS total_cost_eur + FROM base + GROUP BY COALESCE(cause_name, 'Pendiente de clasificar'::text), COALESCE(cause_code, 'pending'::text) + ORDER BY (round(sum(COALESCE(cost_eur, ((safe_duration_s / 3600.0) * value_hour_eur))), 2)) DESC; + + +ALTER VIEW mv_hot.edge_oee_stop_summary_demo OWNER TO mesavault; + +-- +-- Name: stop_summary; Type: VIEW; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +CREATE VIEW mv_edge_oee_ucepsa_demo.stop_summary AS + SELECT cause_name, + cause_code, + stop_count, + total_min, + total_cost_eur + FROM mv_hot.edge_oee_stop_summary_demo; + + +ALTER VIEW mv_edge_oee_ucepsa_demo.stop_summary OWNER TO mesavault; + +-- +-- Name: edge_oee_stops_recent; Type: VIEW; Schema: mv_hot; Owner: mesavault +-- + +CREATE VIEW mv_hot.edge_oee_stops_recent AS + WITH src AS ( + SELECT s.id, + s.tenant, + s.site, + s.vertical, + s.asset, + s.machine_id, + s.order_id, + s.started_at, + s.ended_at, + s.status, + s.classification_status, + s.cause_code, + s.operator_note, + s.value_hour_eur, + s.duration_s, + s.cost_eur, + s.start_event_name, + s.end_event_name, + s.start_payload_json, + s.end_payload_json, + s.created_at, + s.updated_at, + c.cause_name, + c.is_planned, + CASE + WHEN (s.started_at > (s.created_at + '00:00:05'::interval)) THEN s.created_at + ELSE s.started_at + END AS effective_started_at + FROM (mv_hot.edge_oee_machine_stops s + LEFT JOIN mv_hot.edge_oee_stop_causes c ON ((c.cause_code = s.cause_code))) + ), base AS ( + SELECT src.id, + src.tenant, + src.site, + src.vertical, + src.asset, + src.machine_id, + src.order_id, + src.started_at, + src.ended_at, + src.status, + src.classification_status, + src.cause_code, + src.operator_note, + src.value_hour_eur, + src.duration_s, + src.cost_eur, + src.start_event_name, + src.end_event_name, + src.start_payload_json, + src.end_payload_json, + src.created_at, + src.updated_at, + src.cause_name, + src.is_planned, + src.effective_started_at, + CASE + WHEN (src.duration_s IS NOT NULL) THEN GREATEST(src.duration_s, (0)::numeric) + WHEN (src.ended_at IS NOT NULL) THEN GREATEST(EXTRACT(epoch FROM (src.ended_at - src.effective_started_at)), (0)::numeric) + ELSE GREATEST(EXTRACT(epoch FROM (now() - src.effective_started_at)), (0)::numeric) + END AS safe_duration_s + FROM src + ) + SELECT id, + tenant, + site, + vertical, + asset, + machine_id, + order_id, + started_at, + ended_at, + (safe_duration_s)::numeric(14,2) AS duration_s, + round((safe_duration_s / 60.0), 2) AS duration_min, + COALESCE(cost_eur, round(((safe_duration_s / 3600.0) * value_hour_eur), 2)) AS cost_eur, + status, + classification_status, + cause_code, + cause_name, + is_planned, + operator_note, + value_hour_eur, + start_event_name, + end_event_name, + created_at, + updated_at + FROM base + ORDER BY started_at DESC; + + +ALTER VIEW mv_hot.edge_oee_stops_recent OWNER TO mesavault; + +-- +-- Name: stops_recent; Type: VIEW; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +CREATE VIEW mv_edge_oee_ucepsa_demo.stops_recent AS + SELECT id, + tenant, + site, + vertical, + asset, + machine_id, + order_id, + started_at, + ended_at, + duration_s, + duration_min, + cost_eur, + status, + classification_status, + cause_code, + cause_name, + is_planned, + operator_note, + value_hour_eur, + start_event_name, + end_event_name, + created_at, + updated_at + FROM mv_hot.edge_oee_stops_recent; + + +ALTER VIEW mv_edge_oee_ucepsa_demo.stops_recent OWNER TO mesavault; + +-- +-- Name: data_quality_contract_rules; Type: TABLE; Schema: mv_edge_oee_ucepsa_prod; Owner: mesavault +-- + +CREATE TABLE mv_edge_oee_ucepsa_prod.data_quality_contract_rules ( + id bigint NOT NULL, + tenant text DEFAULT 'ucepsa'::text NOT NULL, + site text DEFAULT 'ucepsa_test'::text NOT NULL, + rule_code text NOT NULL, + rule_name text NOT NULL, + domain text NOT NULL, + dimension text NOT NULL, + severity text NOT NULL, + owner_area text NOT NULL, + applies_to text NOT NULL, + description text NOT NULL, + expected_condition text NOT NULL, + blocking_for text, + is_active boolean DEFAULT true NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL +); + + +ALTER TABLE mv_edge_oee_ucepsa_prod.data_quality_contract_rules OWNER TO mesavault; + +-- +-- Name: data_quality_contract_rules_id_seq; Type: SEQUENCE; Schema: mv_edge_oee_ucepsa_prod; Owner: mesavault +-- + +CREATE SEQUENCE mv_edge_oee_ucepsa_prod.data_quality_contract_rules_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER SEQUENCE mv_edge_oee_ucepsa_prod.data_quality_contract_rules_id_seq OWNER TO mesavault; + +-- +-- Name: data_quality_contract_rules_id_seq; Type: SEQUENCE OWNED BY; Schema: mv_edge_oee_ucepsa_prod; Owner: mesavault +-- + +ALTER SEQUENCE mv_edge_oee_ucepsa_prod.data_quality_contract_rules_id_seq OWNED BY mv_edge_oee_ucepsa_prod.data_quality_contract_rules.id; + + +-- +-- Name: margin_cost_parameters; Type: TABLE; Schema: mv_edge_oee_ucepsa_prod; Owner: mesavault +-- + +CREATE TABLE mv_edge_oee_ucepsa_prod.margin_cost_parameters ( + id bigint NOT NULL, + tenant text DEFAULT 'ucepsa'::text NOT NULL, + site text DEFAULT 'ucepsa_test'::text NOT NULL, + scope_level text DEFAULT 'GLOBAL'::text NOT NULL, + machine_id text, + product_default_code text, + energy_cost_eur_per_kwh numeric(14,6), + material_cost_eur_per_kg numeric(14,6), + machine_run_cost_eur_per_h numeric(14,4), + labor_cost_eur_per_h numeric(14,4), + overhead_cost_eur_per_h numeric(14,4), + valid_from timestamp with time zone DEFAULT now() NOT NULL, + valid_to timestamp with time zone, + is_active boolean DEFAULT true NOT NULL, + notes text, + created_at timestamp with time zone DEFAULT now() NOT NULL +); + + +ALTER TABLE mv_edge_oee_ucepsa_prod.margin_cost_parameters OWNER TO mesavault; + +-- +-- Name: margin_cost_parameters_id_seq; Type: SEQUENCE; Schema: mv_edge_oee_ucepsa_prod; Owner: mesavault +-- + +CREATE SEQUENCE mv_edge_oee_ucepsa_prod.margin_cost_parameters_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER SEQUENCE mv_edge_oee_ucepsa_prod.margin_cost_parameters_id_seq OWNER TO mesavault; + +-- +-- Name: margin_cost_parameters_id_seq; Type: SEQUENCE OWNED BY; Schema: mv_edge_oee_ucepsa_prod; Owner: mesavault +-- + +ALTER SEQUENCE mv_edge_oee_ucepsa_prod.margin_cost_parameters_id_seq OWNED BY mv_edge_oee_ucepsa_prod.margin_cost_parameters.id; + + +-- +-- Name: odoo_order_commercials; Type: TABLE; Schema: mv_edge_oee_ucepsa_prod; Owner: mesavault +-- + +CREATE TABLE mv_edge_oee_ucepsa_prod.odoo_order_commercials ( + id bigint NOT NULL, + tenant text DEFAULT 'ucepsa'::text NOT NULL, + site text DEFAULT 'ucepsa_test'::text NOT NULL, + odoo_instance text NOT NULL, + odoo_production_id integer NOT NULL, + odoo_order_ref text NOT NULL, + odoo_origin text, + odoo_state text, + product_id integer, + product_default_code text, + product_name text, + product_uom text, + product_standard_price numeric(14,4), + product_list_price numeric(14,4), + production_qty numeric(14,3), + qty_producing numeric(14,3), + production_uom text, + sale_line_id integer, + sale_order_id integer, + sale_order_ref text, + customer_id integer, + customer_name text, + sale_state text, + sale_date_order timestamp with time zone, + currency text, + sale_line_qty numeric(14,3), + sale_line_qty_delivered numeric(14,3), + sale_price_unit numeric(14,4), + sale_discount_pct numeric(10,2), + revenue_net_eur numeric(14,4), + revenue_total_eur numeric(14,4), + raw_material_value_eur numeric(14,4), + finished_value_eur numeric(14,4), + material_cost_status text DEFAULT 'UNAVAILABLE'::text NOT NULL, + stock_raw_moves_json jsonb DEFAULT '[]'::jsonb NOT NULL, + stock_finished_moves_json jsonb DEFAULT '[]'::jsonb NOT NULL, + raw_json jsonb DEFAULT '{}'::jsonb NOT NULL, + snapshot_at timestamp with time zone DEFAULT now() NOT NULL +); + + +ALTER TABLE mv_edge_oee_ucepsa_prod.odoo_order_commercials OWNER TO mesavault; + +-- +-- Name: odoo_order_commercials_id_seq; Type: SEQUENCE; Schema: mv_edge_oee_ucepsa_prod; Owner: mesavault +-- + +CREATE SEQUENCE mv_edge_oee_ucepsa_prod.odoo_order_commercials_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER SEQUENCE mv_edge_oee_ucepsa_prod.odoo_order_commercials_id_seq OWNER TO mesavault; + +-- +-- Name: odoo_order_commercials_id_seq; Type: SEQUENCE OWNED BY; Schema: mv_edge_oee_ucepsa_prod; Owner: mesavault +-- + +ALTER SEQUENCE mv_edge_oee_ucepsa_prod.odoo_order_commercials_id_seq OWNED BY mv_edge_oee_ucepsa_prod.odoo_order_commercials.id; + + +-- +-- Name: raw_material_cost_parameters; Type: TABLE; Schema: mv_edge_oee_ucepsa_prod; Owner: mesavault +-- + +CREATE TABLE mv_edge_oee_ucepsa_prod.raw_material_cost_parameters ( + id bigint NOT NULL, + tenant text DEFAULT 'ucepsa'::text NOT NULL, + site text DEFAULT 'ucepsa_test'::text NOT NULL, + raw_product_default_code text NOT NULL, + raw_product_name text, + material_cost_eur_per_kg numeric(14,6) NOT NULL, + source text DEFAULT 'MANUAL'::text NOT NULL, + valid_from timestamp with time zone DEFAULT now() NOT NULL, + valid_to timestamp with time zone, + is_active boolean DEFAULT true NOT NULL, + notes text, + created_at timestamp with time zone DEFAULT now() NOT NULL, + raw_product_id integer, + raw_uom text +); + + +ALTER TABLE mv_edge_oee_ucepsa_prod.raw_material_cost_parameters OWNER TO mesavault; + +-- +-- Name: raw_material_cost_parameters_id_seq; Type: SEQUENCE; Schema: mv_edge_oee_ucepsa_prod; Owner: mesavault +-- + +CREATE SEQUENCE mv_edge_oee_ucepsa_prod.raw_material_cost_parameters_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER SEQUENCE mv_edge_oee_ucepsa_prod.raw_material_cost_parameters_id_seq OWNER TO mesavault; + +-- +-- Name: raw_material_cost_parameters_id_seq; Type: SEQUENCE OWNED BY; Schema: mv_edge_oee_ucepsa_prod; Owner: mesavault +-- + +ALTER SEQUENCE mv_edge_oee_ucepsa_prod.raw_material_cost_parameters_id_seq OWNED BY mv_edge_oee_ucepsa_prod.raw_material_cost_parameters.id; + + +-- +-- Name: edge_oee_loss_summary_demo; Type: TABLE; Schema: mv_hot; Owner: mesavault +-- + +CREATE TABLE mv_hot.edge_oee_loss_summary_demo ( + cause text NOT NULL, + hours_month numeric(10,2) NOT NULL, + value_hour_eur numeric(10,2) NOT NULL, + monthly_cost_eur numeric(10,2) NOT NULL, + recommended_action text NOT NULL, + effort text, + priority text, + machine_focus text +); + + +ALTER TABLE mv_hot.edge_oee_loss_summary_demo OWNER TO mesavault; + +-- +-- Name: edge_oee_actions_demo; Type: VIEW; Schema: mv_hot; Owner: mesavault +-- + +CREATE VIEW mv_hot.edge_oee_actions_demo AS + SELECT cause AS "Causa", + monthly_cost_eur AS "Impacto estimado €/mes", + effort AS "Esfuerzo", + priority AS "Prioridad", + recommended_action AS "Acción recomendada", + CASE + WHEN (cause = 'Ajuste de sellado'::text) THEN (4000)::numeric + ELSE NULL::numeric + END AS "Inversión ejemplo €", + CASE + WHEN (cause = 'Ajuste de sellado'::text) THEN round(((4000)::numeric / NULLIF(monthly_cost_eur, (0)::numeric)), 1) + ELSE NULL::numeric + END AS "Payback ejemplo meses" + FROM mv_hot.edge_oee_loss_summary_demo + ORDER BY monthly_cost_eur DESC; + + +ALTER VIEW mv_hot.edge_oee_actions_demo OWNER TO mesavault; + +-- +-- Name: edge_oee_data_quality_demo; Type: TABLE; Schema: mv_hot; Owner: mesavault +-- + +CREATE TABLE mv_hot.edge_oee_data_quality_demo ( + metric_id text NOT NULL, + metric text NOT NULL, + value_numeric numeric(10,2), + value_text text, + unit text, + status text NOT NULL, + problem text, + likely_impact text, + recommended_action text, + sort_order integer NOT NULL +); + + +ALTER TABLE mv_hot.edge_oee_data_quality_demo OWNER TO mesavault; + +-- +-- Name: edge_oee_data_quality_dashboard_demo; Type: VIEW; Schema: mv_hot; Owner: mesavault +-- + +CREATE VIEW mv_hot.edge_oee_data_quality_dashboard_demo AS + SELECT sort_order, + metric AS "Indicador", + COALESCE((value_numeric)::text, value_text) AS "Valor", + unit AS "Unidad", + status AS "Estado", + problem AS "Qué está mal", + likely_impact AS "Impacto probable", + recommended_action AS "Qué revisar / acción" + FROM mv_hot.edge_oee_data_quality_demo + ORDER BY sort_order; + + +ALTER VIEW mv_hot.edge_oee_data_quality_dashboard_demo OWNER TO mesavault; + +-- +-- Name: edge_oee_eastron_readings_id_seq; Type: SEQUENCE; Schema: mv_hot; Owner: mesavault +-- + +CREATE SEQUENCE mv_hot.edge_oee_eastron_readings_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER SEQUENCE mv_hot.edge_oee_eastron_readings_id_seq OWNER TO mesavault; + +-- +-- Name: edge_oee_eastron_readings_id_seq; Type: SEQUENCE OWNED BY; Schema: mv_hot; Owner: mesavault +-- + +ALTER SEQUENCE mv_hot.edge_oee_eastron_readings_id_seq OWNED BY mv_hot.edge_oee_eastron_readings.id; + + +-- +-- Name: edge_oee_energy_demo; Type: TABLE; Schema: mv_hot; Owner: mesavault +-- + +CREATE TABLE mv_hot.edge_oee_energy_demo ( + ts timestamp with time zone NOT NULL, + tenant text DEFAULT 'ucepsa'::text NOT NULL, + site text DEFAULT 'demo_edge_oee'::text NOT NULL, + machine_id text NOT NULL, + order_id text, + power_kw numeric(10,3), + kwh_total numeric(14,3), + source text DEFAULT 'demo'::text, + payload_json jsonb DEFAULT '{}'::jsonb +); + + +ALTER TABLE mv_hot.edge_oee_energy_demo OWNER TO mesavault; + +-- +-- Name: edge_oee_events_demo; Type: TABLE; Schema: mv_hot; Owner: mesavault +-- + +CREATE TABLE mv_hot.edge_oee_events_demo ( + event_id bigint NOT NULL, + tenant text DEFAULT 'ucepsa'::text NOT NULL, + site text DEFAULT 'demo_edge_oee'::text NOT NULL, + machine_id text NOT NULL, + order_id text, + event_type text NOT NULL, + cause text NOT NULL, + start_time timestamp with time zone NOT NULL, + end_time timestamp with time zone NOT NULL, + duration_min numeric(10,2) NOT NULL, + cost_eur numeric(10,2) DEFAULT 0 NOT NULL, + source text DEFAULT 'mesavault_demo'::text NOT NULL, + notes text +); + + +ALTER TABLE mv_hot.edge_oee_events_demo OWNER TO mesavault; + +-- +-- Name: edge_oee_events_demo_event_id_seq; Type: SEQUENCE; Schema: mv_hot; Owner: mesavault +-- + +CREATE SEQUENCE mv_hot.edge_oee_events_demo_event_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER SEQUENCE mv_hot.edge_oee_events_demo_event_id_seq OWNER TO mesavault; + +-- +-- Name: edge_oee_events_demo_event_id_seq; Type: SEQUENCE OWNED BY; Schema: mv_hot; Owner: mesavault +-- + +ALTER SEQUENCE mv_hot.edge_oee_events_demo_event_id_seq OWNED BY mv_hot.edge_oee_events_demo.event_id; + + +-- +-- Name: edge_oee_executive_summary_demo; Type: VIEW; Schema: mv_hot; Owner: mesavault +-- + +CREATE VIEW mv_hot.edge_oee_executive_summary_demo AS + WITH losses AS ( + SELECT sum(edge_oee_loss_summary_demo.hours_month) AS classified_loss_hours, + sum(edge_oee_loss_summary_demo.monthly_cost_eur) AS estimated_loss_cost_eur + FROM mv_hot.edge_oee_loss_summary_demo + ), principal AS ( + SELECT edge_oee_loss_summary_demo.cause + FROM mv_hot.edge_oee_loss_summary_demo + ORDER BY edge_oee_loss_summary_demo.monthly_cost_eur DESC + LIMIT 1 + ) + SELECT now() AS evaluated_at, + 816.0::numeric(10,1) AS hours_analyzed, + losses.classified_loss_hours, + losses.estimated_loss_cost_eur, + principal.cause AS main_loss_cause, + 'Cortadora 2'::text AS machine_with_more_losses, + 5.0::numeric(10,1) AS unclassified_stop_hours, + 7 AS orders_with_odoo_machine_discrepancy + FROM losses, + principal; + + +ALTER VIEW mv_hot.edge_oee_executive_summary_demo OWNER TO mesavault; + +-- +-- Name: edge_oee_kpi_cards_demo; Type: VIEW; Schema: mv_hot; Owner: mesavault +-- + +CREATE VIEW mv_hot.edge_oee_kpi_cards_demo AS + SELECT 1 AS sort_order, + 'Horas analizadas'::text AS kpi, + edge_oee_executive_summary_demo.hours_analyzed AS value, + 'h'::text AS unit, + 'OK'::text AS status + FROM mv_hot.edge_oee_executive_summary_demo +UNION ALL + SELECT 2 AS sort_order, + 'Pérdidas clasificadas'::text AS kpi, + edge_oee_executive_summary_demo.classified_loss_hours AS value, + 'h/mes'::text AS unit, + 'Warning'::text AS status + FROM mv_hot.edge_oee_executive_summary_demo +UNION ALL + SELECT 3 AS sort_order, + 'Coste estimado de pérdidas'::text AS kpi, + edge_oee_executive_summary_demo.estimated_loss_cost_eur AS value, + '€/mes'::text AS unit, + 'Warning'::text AS status + FROM mv_hot.edge_oee_executive_summary_demo +UNION ALL + SELECT 4 AS sort_order, + 'Paros sin causa'::text AS kpi, + edge_oee_executive_summary_demo.unclassified_stop_hours AS value, + 'h/mes'::text AS unit, + 'Warning'::text AS status + FROM mv_hot.edge_oee_executive_summary_demo +UNION ALL + SELECT 5 AS sort_order, + 'Órdenes con discrepancia Odoo/máquina'::text AS kpi, + edge_oee_executive_summary_demo.orders_with_odoo_machine_discrepancy AS value, + 'órdenes'::text AS unit, + 'Warning'::text AS status + FROM mv_hot.edge_oee_executive_summary_demo; + + +ALTER VIEW mv_hot.edge_oee_kpi_cards_demo OWNER TO mesavault; + +-- +-- Name: edge_oee_machine_stops_backup_cort00_20260622_175800; Type: TABLE; Schema: mv_hot; Owner: mesavault +-- + +CREATE TABLE mv_hot.edge_oee_machine_stops_backup_cort00_20260622_175800 ( + id bigint, + tenant text, + site text, + vertical text, + asset text, + machine_id text, + order_id text, + started_at timestamp with time zone, + ended_at timestamp with time zone, + status text, + classification_status text, + cause_code text, + operator_note text, + value_hour_eur numeric(10,2), + duration_s numeric(14,2), + cost_eur numeric(14,2), + start_event_name text, + end_event_name text, + start_payload_json jsonb, + end_payload_json jsonb, + created_at timestamp with time zone, + updated_at timestamp with time zone +); + + +ALTER TABLE mv_hot.edge_oee_machine_stops_backup_cort00_20260622_175800 OWNER TO mesavault; + +-- +-- Name: edge_oee_machine_stops_id_seq; Type: SEQUENCE; Schema: mv_hot; Owner: mesavault +-- + +CREATE SEQUENCE mv_hot.edge_oee_machine_stops_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER SEQUENCE mv_hot.edge_oee_machine_stops_id_seq OWNER TO mesavault; + +-- +-- Name: edge_oee_machine_stops_id_seq; Type: SEQUENCE OWNED BY; Schema: mv_hot; Owner: mesavault +-- + +ALTER SEQUENCE mv_hot.edge_oee_machine_stops_id_seq OWNED BY mv_hot.edge_oee_machine_stops.id; + + +-- +-- Name: edge_oee_machines; Type: TABLE; Schema: mv_hot; Owner: mesavault +-- + +CREATE TABLE mv_hot.edge_oee_machines ( + machine_id text NOT NULL, + tenant text DEFAULT 'ucepsa'::text NOT NULL, + site text DEFAULT 'demo_edge_oee'::text NOT NULL, + name text NOT NULL, + area text DEFAULT 'corte'::text NOT NULL, + machine_type text DEFAULT 'cortadora'::text NOT NULL, + value_hour_eur numeric(10,2) NOT NULL, + standard_kg_h numeric(10,2), + sale_price_eur_kg numeric(10,2), + transformation_cost_eur_kg numeric(10,2), + raw_material_cost_eur_kg numeric(10,2), + notes text +); + + +ALTER TABLE mv_hot.edge_oee_machines OWNER TO mesavault; + +-- +-- Name: edge_oee_node_events_id_seq; Type: SEQUENCE; Schema: mv_hot; Owner: mesavault +-- + +CREATE SEQUENCE mv_hot.edge_oee_node_events_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER SEQUENCE mv_hot.edge_oee_node_events_id_seq OWNER TO mesavault; + +-- +-- Name: edge_oee_node_events_id_seq; Type: SEQUENCE OWNED BY; Schema: mv_hot; Owner: mesavault +-- + +ALTER SEQUENCE mv_hot.edge_oee_node_events_id_seq OWNED BY mv_hot.edge_oee_node_events.id; + + +-- +-- Name: edge_oee_orders_demo; Type: TABLE; Schema: mv_hot; Owner: mesavault +-- + +CREATE TABLE mv_hot.edge_oee_orders_demo ( + order_id text NOT NULL, + tenant text DEFAULT 'ucepsa'::text NOT NULL, + site text DEFAULT 'demo_edge_oee'::text NOT NULL, + machine_id text NOT NULL, + product text NOT NULL, + odoo_start timestamp with time zone NOT NULL, + odoo_end timestamp with time zone NOT NULL, + qty_planned numeric(14,2), + qty_done numeric(14,2), + scrap_qty numeric(14,2), + uom text DEFAULT 'unidades'::text, + commercial_note text +); + + +ALTER TABLE mv_hot.edge_oee_orders_demo OWNER TO mesavault; + +-- +-- Name: edge_oee_order_timeline_demo; Type: VIEW; Schema: mv_hot; Owner: mesavault +-- + +CREATE VIEW mv_hot.edge_oee_order_timeline_demo AS + SELECT o.odoo_start AS "time", + (o.odoo_start + '00:01:00'::interval) AS time_end, + o.order_id, + o.machine_id, + m.name AS machine_name, + o.product, + 'ODOO'::text AS source, + 'ODOO_START'::text AS state, + 'Odoo inicia la orden'::text AS detail, + (0)::numeric AS duration_min, + (0)::numeric AS cost_eur, + 1 AS sort_order + FROM (mv_hot.edge_oee_orders_demo o + JOIN mv_hot.edge_oee_machines m ON ((m.machine_id = o.machine_id))) +UNION ALL + SELECT e.start_time AS "time", + e.end_time AS time_end, + e.order_id, + e.machine_id, + m.name AS machine_name, + COALESCE(o.product, 'Sin orden activa'::text) AS product, + 'MESAVAULT'::text AS source, + e.event_type AS state, + e.cause AS detail, + e.duration_min, + e.cost_eur, + 2 AS sort_order + FROM ((mv_hot.edge_oee_events_demo e + JOIN mv_hot.edge_oee_machines m ON ((m.machine_id = e.machine_id))) + LEFT JOIN mv_hot.edge_oee_orders_demo o ON ((o.order_id = e.order_id))) +UNION ALL + SELECT o.odoo_end AS "time", + (o.odoo_end + '00:01:00'::interval) AS time_end, + o.order_id, + o.machine_id, + m.name AS machine_name, + o.product, + 'ODOO'::text AS source, + 'ODOO_END'::text AS state, + 'Odoo finaliza la orden'::text AS detail, + (0)::numeric AS duration_min, + (0)::numeric AS cost_eur, + 3 AS sort_order + FROM (mv_hot.edge_oee_orders_demo o + JOIN mv_hot.edge_oee_machines m ON ((m.machine_id = o.machine_id))); + + +ALTER VIEW mv_hot.edge_oee_order_timeline_demo OWNER TO mesavault; + +-- +-- Name: edge_oee_pareto_demo; Type: VIEW; Schema: mv_hot; Owner: mesavault +-- + +CREATE VIEW mv_hot.edge_oee_pareto_demo AS + SELECT rank() OVER (ORDER BY monthly_cost_eur DESC) AS ranking, + cause, + hours_month, + value_hour_eur, + monthly_cost_eur, + recommended_action, + effort, + priority, + machine_focus + FROM mv_hot.edge_oee_loss_summary_demo + ORDER BY monthly_cost_eur DESC; + + +ALTER VIEW mv_hot.edge_oee_pareto_demo OWNER TO mesavault; + +-- +-- Name: edge_oee_quality_cases_demo; Type: TABLE; Schema: mv_hot; Owner: mesavault +-- + +CREATE TABLE mv_hot.edge_oee_quality_cases_demo ( + case_id text NOT NULL, + order_id text, + machine_id text, + product text, + claim text, + sealing_adjustment_stops integer, + sealing_adjustment_min numeric(10,2), + microstops_count integer, + scrap_pct numeric(10,2), + energy_vs_avg_pct numeric(10,2), + interpretation text, + recommended_action text +); + + +ALTER TABLE mv_hot.edge_oee_quality_cases_demo OWNER TO mesavault; + +-- +-- Name: edge_oee_quality_case_demo; Type: VIEW; Schema: mv_hot; Owner: mesavault +-- + +CREATE VIEW mv_hot.edge_oee_quality_case_demo AS + SELECT qc.case_id, + qc.order_id, + m.name AS machine_name, + qc.product, + qc.claim, + qc.sealing_adjustment_stops, + qc.sealing_adjustment_min, + qc.microstops_count, + qc.scrap_pct, + qc.energy_vs_avg_pct, + qc.interpretation, + qc.recommended_action + FROM (mv_hot.edge_oee_quality_cases_demo qc + JOIN mv_hot.edge_oee_machines m ON ((m.machine_id = qc.machine_id))); + + +ALTER VIEW mv_hot.edge_oee_quality_case_demo OWNER TO mesavault; + +-- +-- Name: odoo_order_state_current; Type: TABLE; Schema: mv_hot; Owner: mesavault +-- + +CREATE TABLE mv_hot.odoo_order_state_current ( + tenant text NOT NULL, + site text NOT NULL, + machine_id text NOT NULL, + topic text, + mqtt_ts timestamp with time zone, + received_at timestamp with time zone DEFAULT now() NOT NULL, + source text, + workcenter_id bigint, + workcenter_name text, + workcenter_display_name text, + order_present boolean, + order_coherent boolean, + odoo_state text, + reason text, + workorder_id text, + production_order text, + product text, + waiting_count integer, + ready_count integer, + progress_count integer, + payload_hash text, + payload_json jsonb NOT NULL +); + + +ALTER TABLE mv_hot.odoo_order_state_current OWNER TO mesavault; + +-- +-- Name: odoo_order_state_events; Type: TABLE; Schema: mv_hot; Owner: mesavault +-- + +CREATE TABLE mv_hot.odoo_order_state_events ( + id bigint NOT NULL, + tenant text NOT NULL, + site text NOT NULL, + machine_id text NOT NULL, + topic text, + mqtt_ts timestamp with time zone, + received_at timestamp with time zone DEFAULT now() NOT NULL, + odoo_state text, + order_present boolean, + order_coherent boolean, + production_order text, + product text, + reason text, + waiting_count integer, + ready_count integer, + progress_count integer, + payload_hash text NOT NULL, + payload_json jsonb NOT NULL +); + + +ALTER TABLE mv_hot.odoo_order_state_events OWNER TO mesavault; + +-- +-- Name: odoo_order_state_events_id_seq; Type: SEQUENCE; Schema: mv_hot; Owner: mesavault +-- + +CREATE SEQUENCE mv_hot.odoo_order_state_events_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER SEQUENCE mv_hot.odoo_order_state_events_id_seq OWNER TO mesavault; + +-- +-- Name: odoo_order_state_events_id_seq; Type: SEQUENCE OWNED BY; Schema: mv_hot; Owner: mesavault +-- + +ALTER SEQUENCE mv_hot.odoo_order_state_events_id_seq OWNED BY mv_hot.odoo_order_state_events.id; + + +-- +-- Name: order_execution_report_v1; Type: VIEW; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +CREATE VIEW mv_reports_ucepsa_demo.order_execution_report_v1 AS + WITH orders AS ( + SELECT o_1.id, + o_1.tenant, + o_1.site, + o_1.odoo_instance, + o_1.odoo_workorder_id, + o_1.odoo_production_id, + o_1.odoo_order_ref, + o_1.odoo_state, + o_1.odoo_workcenter_code, + o_1.odoo_workcenter_name, + o_1.machine_id, + o_1.asset, + o_1.product, + o_1.qty_planned, + o_1.qty_done, + o_1.uom, + o_1.planned_cycles, + o_1.target_cycle_rate_ppm, + o_1.odoo_start, + o_1.odoo_end, + o_1.notes, + o_1.snapshot_at, + o_1.odoo_start AS start_ts, + CASE + WHEN (o_1.odoo_state = ANY (ARRAY['done'::text, 'to_close'::text, 'cancel'::text])) THEN COALESCE(o_1.odoo_end, o_1.snapshot_at) + ELSE COALESCE(( SELECT max(r.ingest_ts) AS max + FROM mv_hot.edge_oee_eastron_readings r + WHERE ((r.machine_id = o_1.machine_id) AND (r.ingest_ts >= o_1.odoo_start) AND (r.comm_ok = true) AND (r.meter_model = 'Eastron SDM120-M'::text))), o_1.snapshot_at) + END AS end_ts + FROM mv_edge_oee_ucepsa_demo.odoo_workorders_demo o_1 + WHERE ((o_1.odoo_instance = 'odoo_ucepsa_test'::text) AND (o_1.machine_id IS NOT NULL) AND (o_1.odoo_start IS NOT NULL)) + ), energy AS ( + SELECT o_1.id AS order_row_id, + count(r.id) AS energy_samples, + min(r.ingest_ts) AS energy_first_ts, + max(r.ingest_ts) AS energy_last_ts, + min(r.import_kwh) AS start_import_kwh, + max(r.import_kwh) AS end_import_kwh, + round((max(r.import_kwh) - min(r.import_kwh)), 3) AS kwh_consumed, + round(avg(r.power_total_kw), 3) AS avg_kw, + round(max(r.power_total_kw), 3) AS max_kw + FROM (orders o_1 + LEFT JOIN mv_hot.edge_oee_eastron_readings r ON (((r.machine_id = o_1.machine_id) AND (r.ingest_ts >= o_1.start_ts) AND (r.ingest_ts <= o_1.end_ts) AND (r.comm_ok = true) AND (r.meter_model = 'Eastron SDM120-M'::text)))) + GROUP BY o_1.id + ), cycles AS ( + SELECT o_1.id AS order_row_id, + count(r.id) AS telemetry_samples, + min(r.ingest_ts) AS telemetry_first_ts, + max(r.ingest_ts) AS telemetry_last_ts, + min(((r.payload_json ->> 'cycle_pulse_count'::text))::bigint) AS min_cycle_pulse_count, + max(((r.payload_json ->> 'cycle_pulse_count'::text))::bigint) AS max_cycle_pulse_count, + GREATEST((max(((r.payload_json ->> 'cycle_pulse_count'::text))::bigint) - min(((r.payload_json ->> 'cycle_pulse_count'::text))::bigint)), (0)::bigint) AS real_cycle_delta, + round(avg(((r.payload_json ->> 'cycle_rate_ppm'::text))::numeric), 2) AS avg_cycle_rate_ppm, + round(avg(NULLIF(((r.payload_json ->> 'cycle_rate_ppm'::text))::numeric, (0)::numeric)), 2) AS avg_nonzero_cycle_rate_ppm + FROM (orders o_1 + LEFT JOIN mv_hot.edge_oee_eastron_readings r ON (((r.machine_id = o_1.machine_id) AND (r.ingest_ts >= o_1.start_ts) AND (r.ingest_ts <= o_1.end_ts) AND (r.comm_ok = true) AND (r.meter_model = 'Eastron SDM120-M'::text) AND (r.payload_json ? 'cycle_pulse_count'::text)))) + GROUP BY o_1.id + ), stops AS ( + SELECT o_1.id AS order_row_id, + count(ms.id) AS stop_count, + round((COALESCE(sum(GREATEST(EXTRACT(epoch FROM (LEAST(COALESCE(ms.ended_at, o_1.end_ts), o_1.end_ts) - GREATEST(ms.started_at, o_1.start_ts))), (0)::numeric)), (0)::numeric) / 60.0), 2) AS stop_min, + count(ms.id) FILTER (WHERE (ms.classification_status = 'PENDING'::text)) AS pending_stops, + count(ms.id) FILTER (WHERE (ms.classification_status = 'CLASSIFIED'::text)) AS classified_stops, + round(COALESCE(sum(((GREATEST(EXTRACT(epoch FROM (LEAST(COALESCE(ms.ended_at, o_1.end_ts), o_1.end_ts) - GREATEST(ms.started_at, o_1.start_ts))), (0)::numeric) / 3600.0) * ms.value_hour_eur)), (0)::numeric), 2) AS stop_cost_eur + FROM (orders o_1 + LEFT JOIN mv_hot.edge_oee_machine_stops ms ON (((ms.machine_id = o_1.machine_id) AND (ms.started_at < o_1.end_ts) AND (COALESCE(ms.ended_at, now()) > o_1.start_ts)))) + GROUP BY o_1.id + ) + SELECT o.id AS order_row_id, + o.tenant, + o.site, + o.odoo_instance, + o.odoo_workorder_id, + o.odoo_production_id, + o.odoo_order_ref, + o.odoo_state, + o.odoo_workcenter_code, + o.odoo_workcenter_name, + o.machine_id, + o.asset, + o.product, + o.qty_planned, + o.qty_done, + o.uom, + o.planned_cycles, + o.target_cycle_rate_ppm, + o.start_ts, + o.end_ts, + round((EXTRACT(epoch FROM (o.end_ts - o.start_ts)) / 60.0), 2) AS window_min, + COALESCE(c.telemetry_samples, (0)::bigint) AS telemetry_samples, + c.telemetry_first_ts, + c.telemetry_last_ts, + COALESCE(c.min_cycle_pulse_count, (0)::bigint) AS min_cycle_pulse_count, + COALESCE(c.max_cycle_pulse_count, (0)::bigint) AS max_cycle_pulse_count, + COALESCE(c.real_cycle_delta, (0)::bigint) AS real_cycle_delta, + c.avg_cycle_rate_ppm, + c.avg_nonzero_cycle_rate_ppm, + COALESCE(e.energy_samples, (0)::bigint) AS energy_samples, + e.energy_first_ts, + e.energy_last_ts, + e.start_import_kwh, + e.end_import_kwh, + e.kwh_consumed, + e.avg_kw, + e.max_kw, + COALESCE(s.stop_count, (0)::bigint) AS stop_count, + COALESCE(s.stop_min, (0)::numeric) AS stop_min, + COALESCE(s.classified_stops, (0)::bigint) AS classified_stops, + COALESCE(s.pending_stops, (0)::bigint) AS pending_stops, + COALESCE(s.stop_cost_eur, (0)::numeric) AS stop_cost_eur, + CASE + WHEN (COALESCE(c.telemetry_samples, (0)::bigint) = 0) THEN 'SIN_TELEMETRIA_EN_VENTANA'::text + WHEN (COALESCE(e.energy_samples, (0)::bigint) = 0) THEN 'SIN_ENERGIA_EN_VENTANA'::text + WHEN (o.odoo_state = ANY (ARRAY['progress'::text, 'in_progress'::text, 'started'::text])) THEN 'PARCIAL_ORDEN_EN_CURSO'::text + WHEN (COALESCE(s.pending_stops, (0)::bigint) > 0) THEN 'PARCIAL_PAROS_PENDIENTES'::text + ELSE 'CERRADO_CALCULABLE'::text + END AS report_status, + CASE + WHEN (COALESCE(c.telemetry_samples, (0)::bigint) = 0) THEN 'No hay telemetría Edge-OEE dentro de la ventana de la orden.'::text + WHEN (COALESCE(e.energy_samples, (0)::bigint) = 0) THEN 'No hay muestras energéticas válidas dentro de la ventana de la orden.'::text + WHEN (o.odoo_state = ANY (ARRAY['progress'::text, 'in_progress'::text, 'started'::text])) THEN 'La orden sigue en curso. El informe es parcial.'::text + WHEN (COALESCE(s.pending_stops, (0)::bigint) > 0) THEN 'Existen paros pendientes de clasificación.'::text + ELSE 'Orden cerrada con datos suficientes para informe.'::text + END AS report_status_description + FROM (((orders o + LEFT JOIN cycles c ON ((c.order_row_id = o.id))) + LEFT JOIN energy e ON ((e.order_row_id = o.id))) + LEFT JOIN stops s ON ((s.order_row_id = o.id))); + + +ALTER VIEW mv_reports_ucepsa_demo.order_execution_report_v1 OWNER TO mesavault; + +-- +-- Name: order_execution_energy_timeseries_v1; Type: VIEW; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +CREATE VIEW mv_reports_ucepsa_demo.order_execution_energy_timeseries_v1 AS + WITH base AS ( + SELECT r.order_row_id, + r.tenant, + r.site, + r.odoo_instance, + r.odoo_workorder_id, + r.odoo_production_id, + r.odoo_order_ref, + r.odoo_state, + r.product, + r.machine_id, + r.asset, + r.start_ts AS order_start_ts, + r.end_ts AS order_end_ts, + r.report_status, + e.id AS reading_id, + e.ts AS meter_ts, + e.ingest_ts AS "time", + e.voltage_l1_v, + e.current_l1_a, + e.power_total_kw, + e.frequency_hz, + e.import_kwh, + e.export_kwh, + e.comm_ok, + e.error_text, + CASE + WHEN (e.payload_json ? 'machine_running'::text) THEN ((e.payload_json ->> 'machine_running'::text))::boolean + ELSE NULL::boolean + END AS machine_running, + CASE + WHEN (e.payload_json ? 'machine_auto_signal'::text) THEN ((e.payload_json ->> 'machine_auto_signal'::text))::boolean + ELSE NULL::boolean + END AS machine_auto_signal, + CASE + WHEN (e.payload_json ? 'cycle_pulse_count'::text) THEN (NULLIF((e.payload_json ->> 'cycle_pulse_count'::text), ''::text))::bigint + ELSE NULL::bigint + END AS cycle_pulse_count, + CASE + WHEN (e.payload_json ? 'cycle_rate_ppm'::text) THEN (NULLIF((e.payload_json ->> 'cycle_rate_ppm'::text), ''::text))::numeric + ELSE NULL::numeric + END AS cycle_rate_ppm, + CASE + WHEN (e.payload_json ? 'last_cycle_age_s'::text) THEN (NULLIF((e.payload_json ->> 'last_cycle_age_s'::text), ''::text))::numeric + ELSE NULL::numeric + END AS last_cycle_age_s, + e.payload_json + FROM (mv_reports_ucepsa_demo.order_execution_report_v1 r + JOIN mv_hot.edge_oee_eastron_readings e ON (((e.machine_id = r.machine_id) AND (e.ingest_ts >= r.start_ts) AND (e.ingest_ts <= r.end_ts) AND (e.meter_model = 'Eastron SDM120-M'::text)))) + ), enriched AS ( + SELECT b.order_row_id, + b.tenant, + b.site, + b.odoo_instance, + b.odoo_workorder_id, + b.odoo_production_id, + b.odoo_order_ref, + b.odoo_state, + b.product, + b.machine_id, + b.asset, + b.order_start_ts, + b.order_end_ts, + b.report_status, + b.reading_id, + b.meter_ts, + b."time", + b.voltage_l1_v, + b.current_l1_a, + b.power_total_kw, + b.frequency_hz, + b.import_kwh, + b.export_kwh, + b.comm_ok, + b.error_text, + b.machine_running, + b.machine_auto_signal, + b.cycle_pulse_count, + b.cycle_rate_ppm, + b.last_cycle_age_s, + b.payload_json, + round((EXTRACT(epoch FROM (b."time" - b.order_start_ts)) / 60.0), 2) AS minute_from_order_start, + round(EXTRACT(epoch FROM (b."time" - lag(b."time") OVER (PARTITION BY b.order_row_id ORDER BY b."time"))), 2) AS sample_gap_s, + min(b.import_kwh) FILTER (WHERE ((b.comm_ok = true) AND (b.import_kwh IS NOT NULL))) OVER (PARTITION BY b.order_row_id) AS order_start_import_kwh, + min(b.cycle_pulse_count) FILTER (WHERE (b.cycle_pulse_count IS NOT NULL)) OVER (PARTITION BY b.order_row_id) AS order_start_cycle_pulse_count + FROM base b + ) + SELECT order_row_id, + tenant, + site, + odoo_instance, + odoo_workorder_id, + odoo_production_id, + odoo_order_ref, + odoo_state, + product, + machine_id, + asset, + order_start_ts, + order_end_ts, + report_status, + reading_id, + meter_ts, + "time", + minute_from_order_start, + sample_gap_s, + voltage_l1_v, + current_l1_a, + power_total_kw, + frequency_hz, + import_kwh, + export_kwh, + round(GREATEST((import_kwh - order_start_import_kwh), (0)::numeric), 3) AS kwh_from_order_start, + comm_ok, + error_text, + machine_running, + machine_auto_signal, + cycle_pulse_count, + GREATEST((cycle_pulse_count - order_start_cycle_pulse_count), (0)::bigint) AS cycles_from_order_start, + cycle_rate_ppm, + last_cycle_age_s, + CASE + WHEN (comm_ok = false) THEN 'BAD_COMMUNICATION'::text + WHEN ((sample_gap_s IS NOT NULL) AND (sample_gap_s > (30)::numeric)) THEN 'TELEMETRY_GAP'::text + WHEN (power_total_kw IS NULL) THEN 'NO_POWER_VALUE'::text + WHEN (import_kwh IS NULL) THEN 'NO_IMPORT_KWH'::text + ELSE 'OK'::text + END AS sample_quality_status, + payload_json + FROM enriched; + + +ALTER VIEW mv_reports_ucepsa_demo.order_execution_energy_timeseries_v1 OWNER TO mesavault; + +-- +-- Name: order_execution_data_quality_v1; Type: VIEW; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +CREATE VIEW mv_reports_ucepsa_demo.order_execution_data_quality_v1 AS + WITH base AS ( + SELECT r.order_row_id, + r.tenant, + r.site, + r.odoo_instance, + r.odoo_workorder_id, + r.odoo_production_id, + r.odoo_order_ref, + r.odoo_state, + r.odoo_workcenter_code, + r.odoo_workcenter_name, + r.machine_id, + r.asset, + r.product, + r.qty_planned, + r.qty_done, + r.uom, + r.planned_cycles, + r.target_cycle_rate_ppm, + r.start_ts, + r.end_ts, + r.window_min, + r.telemetry_samples, + r.telemetry_first_ts, + r.telemetry_last_ts, + r.min_cycle_pulse_count, + r.max_cycle_pulse_count, + r.real_cycle_delta, + r.avg_cycle_rate_ppm, + r.avg_nonzero_cycle_rate_ppm, + r.energy_samples, + r.energy_first_ts, + r.energy_last_ts, + r.start_import_kwh, + r.end_import_kwh, + r.kwh_consumed, + r.avg_kw, + r.max_kw, + r.stop_count, + r.stop_min, + r.classified_stops, + r.pending_stops, + r.stop_cost_eur, + r.report_status, + r.report_status_description, + GREATEST(((floor((EXTRACT(epoch FROM (r.end_ts - r.start_ts)) / 10.0)))::integer + 1), 1) AS expected_samples_10s + FROM mv_reports_ucepsa_demo.order_execution_report_v1 r + ), gap_stats AS ( + SELECT t.order_row_id, + count(*) FILTER (WHERE ((t.sample_gap_s IS NOT NULL) AND (t.sample_gap_s > (30)::numeric))) AS telemetry_gap_count_30s, + round(max(t.sample_gap_s), 2) AS max_sample_gap_s, + round(avg(t.sample_gap_s) FILTER (WHERE (t.sample_gap_s IS NOT NULL)), 2) AS avg_sample_gap_s, + count(*) FILTER (WHERE (t.sample_quality_status <> 'OK'::text)) AS non_ok_samples + FROM mv_reports_ucepsa_demo.order_execution_energy_timeseries_v1 t + GROUP BY t.order_row_id + ), scored AS ( + SELECT b.order_row_id, + b.tenant, + b.site, + b.odoo_instance, + b.odoo_workorder_id, + b.odoo_production_id, + b.odoo_order_ref, + b.odoo_state, + b.product, + b.machine_id, + b.asset, + b.start_ts, + b.end_ts, + b.window_min, + b.qty_planned, + b.qty_done, + b.uom, + b.planned_cycles, + b.target_cycle_rate_ppm, + b.telemetry_samples, + b.energy_samples, + b.expected_samples_10s, + round((((COALESCE(b.telemetry_samples, (0)::bigint))::numeric / (NULLIF(b.expected_samples_10s, 0))::numeric) * 100.0), 2) AS telemetry_sample_coverage_pct, + b.telemetry_first_ts, + b.telemetry_last_ts, + b.energy_first_ts, + b.energy_last_ts, + b.real_cycle_delta, + b.avg_cycle_rate_ppm, + b.avg_nonzero_cycle_rate_ppm, + b.kwh_consumed, + b.avg_kw, + b.max_kw, + b.stop_count, + b.stop_min, + b.classified_stops, + b.pending_stops, + b.stop_cost_eur, + COALESCE(g.telemetry_gap_count_30s, (0)::bigint) AS telemetry_gap_count_30s, + COALESCE(g.max_sample_gap_s, (0)::numeric) AS max_sample_gap_s, + COALESCE(g.avg_sample_gap_s, (0)::numeric) AS avg_sample_gap_s, + COALESCE(g.non_ok_samples, (0)::bigint) AS non_ok_samples, + b.report_status, + b.report_status_description, + CASE + WHEN (COALESCE(b.planned_cycles, (0)::numeric) <= (0)::numeric) THEN 'SIN_OBJETIVO_CICLOS'::text + WHEN ((COALESCE(b.real_cycle_delta, (0)::bigint) = 0) AND (COALESCE(b.qty_done, (0)::numeric) > (0)::numeric)) THEN 'CANTIDAD_ODOO_SIN_CICLOS_EDGE'::text + WHEN ((COALESCE(b.real_cycle_delta, (0)::bigint) > 0) AND (COALESCE(b.planned_cycles, (0)::numeric) > (0)::numeric) AND ((((COALESCE(b.real_cycle_delta, (0)::bigint))::numeric / NULLIF(b.planned_cycles, (0)::numeric)) >= 0.5) AND (((COALESCE(b.real_cycle_delta, (0)::bigint))::numeric / NULLIF(b.planned_cycles, (0)::numeric)) <= 1.5))) THEN 'CICLOS_COHERENTES_CON_OBJETIVO'::text + ELSE 'REVISAR_CONVERSION_ODOO_CICLOS'::text + END AS production_conversion_status, + GREATEST(0, (((((((100 - + CASE + WHEN (COALESCE(b.telemetry_samples, (0)::bigint) = 0) THEN 60 + ELSE 0 + END) - + CASE + WHEN (COALESCE(b.energy_samples, (0)::bigint) = 0) THEN 50 + ELSE 0 + END) - + CASE + WHEN (b.odoo_state = ANY (ARRAY['progress'::text, 'in_progress'::text, 'started'::text])) THEN 20 + ELSE 0 + END) - + CASE + WHEN (COALESCE(b.pending_stops, (0)::bigint) > 0) THEN 20 + ELSE 0 + END) - + CASE + WHEN (COALESCE(g.telemetry_gap_count_30s, (0)::bigint) > 0) THEN LEAST(((COALESCE(g.telemetry_gap_count_30s, (0)::bigint))::integer * 2), 15) + ELSE 0 + END) - + CASE + WHEN (COALESCE(g.max_sample_gap_s, (0)::numeric) > (60)::numeric) THEN 10 + ELSE 0 + END) - + CASE + WHEN (COALESCE(g.non_ok_samples, (0)::bigint) > 0) THEN 10 + ELSE 0 + END)) AS technical_quality_score + FROM (base b + LEFT JOIN gap_stats g ON ((g.order_row_id = b.order_row_id))) + ) + SELECT order_row_id, + tenant, + site, + odoo_instance, + odoo_workorder_id, + odoo_production_id, + odoo_order_ref, + odoo_state, + product, + machine_id, + asset, + start_ts, + end_ts, + window_min, + qty_planned, + qty_done, + uom, + planned_cycles, + target_cycle_rate_ppm, + telemetry_samples, + energy_samples, + expected_samples_10s, + telemetry_sample_coverage_pct, + telemetry_first_ts, + telemetry_last_ts, + energy_first_ts, + energy_last_ts, + real_cycle_delta, + avg_cycle_rate_ppm, + avg_nonzero_cycle_rate_ppm, + kwh_consumed, + avg_kw, + max_kw, + stop_count, + stop_min, + classified_stops, + pending_stops, + stop_cost_eur, + telemetry_gap_count_30s, + max_sample_gap_s, + avg_sample_gap_s, + non_ok_samples, + report_status, + report_status_description, + production_conversion_status, + technical_quality_score, + CASE + WHEN (technical_quality_score >= 90) THEN 'FIABLE'::text + WHEN (technical_quality_score >= 70) THEN 'ACEPTABLE_CON_OBSERVACIONES'::text + WHEN (technical_quality_score >= 40) THEN 'PARCIAL'::text + ELSE 'NO_DEFENDIBLE'::text + END AS technical_quality_level, + CASE + WHEN (COALESCE(telemetry_samples, (0)::bigint) = 0) THEN 'No hay telemetría Edge-OEE dentro de la ventana de la orden. El informe solo puede apoyarse en Odoo.'::text + WHEN (COALESCE(energy_samples, (0)::bigint) = 0) THEN 'Faltan muestras energéticas válidas dentro de la ventana de la orden.'::text + WHEN (odoo_state = ANY (ARRAY['progress'::text, 'in_progress'::text, 'started'::text])) THEN 'La orden sigue abierta. El informe debe tratarse como parcial.'::text + WHEN (COALESCE(pending_stops, (0)::bigint) > 0) THEN 'Hay paros pendientes de clasificación. Conviene clasificarlos antes de cerrar el informe.'::text + WHEN (COALESCE(telemetry_gap_count_30s, (0)::bigint) > 0) THEN 'Hay gaps de telemetría superiores a 30 segundos. Revisar continuidad de señal.'::text + WHEN (production_conversion_status = 'REVISAR_CONVERSION_ODOO_CICLOS'::text) THEN 'El informe técnico es válido para paros, energía y ciclos reales; revisar la conversión entre cantidad Odoo y ciclos/golpes.'::text + ELSE 'Datos suficientes para generar informe técnico de orden.'::text + END AS recommended_action + FROM scored; + + +ALTER VIEW mv_reports_ucepsa_demo.order_execution_data_quality_v1 OWNER TO mesavault; + +-- +-- Name: order_execution_stops_v1; Type: VIEW; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +CREATE VIEW mv_reports_ucepsa_demo.order_execution_stops_v1 AS + SELECT r.order_row_id, + r.tenant, + r.site, + r.odoo_instance, + r.odoo_workorder_id, + r.odoo_production_id, + r.odoo_order_ref, + r.odoo_state, + r.product, + r.machine_id, + r.asset, + r.start_ts AS order_start_ts, + r.end_ts AS order_end_ts, + r.report_status AS order_report_status, + ms.id AS stop_id, + ms.started_at AS stop_started_at, + ms.ended_at AS stop_ended_at, + GREATEST(ms.started_at, r.start_ts) AS attributed_started_at, + LEAST(COALESCE(ms.ended_at, r.end_ts), r.end_ts) AS attributed_ended_at, + round(ms.duration_s, 2) AS original_duration_s, + round((ms.duration_s / 60.0), 2) AS original_duration_min, + round(EXTRACT(epoch FROM (LEAST(COALESCE(ms.ended_at, r.end_ts), r.end_ts) - GREATEST(ms.started_at, r.start_ts))), 2) AS attributed_duration_s, + round((EXTRACT(epoch FROM (LEAST(COALESCE(ms.ended_at, r.end_ts), r.end_ts) - GREATEST(ms.started_at, r.start_ts))) / 60.0), 2) AS attributed_duration_min, + ms.status AS stop_status, + ms.classification_status, + ms.cause_code, + ms.operator_note, + ms.value_hour_eur, + round(((EXTRACT(epoch FROM (LEAST(COALESCE(ms.ended_at, r.end_ts), r.end_ts) - GREATEST(ms.started_at, r.start_ts))) / 3600.0) * ms.value_hour_eur), 2) AS attributed_cost_eur, + ms.start_event_name, + ms.end_event_name, + ms.created_at, + ms.updated_at, + CASE + WHEN (ms.status = 'OPEN'::text) THEN 'PARO_ABIERTO'::text + WHEN (ms.classification_status = 'PENDING'::text) THEN 'PENDIENTE_CLASIFICACION'::text + WHEN (ms.classification_status = 'CLASSIFIED'::text) THEN 'CLASIFICADO'::text + ELSE 'SIN_ESTADO_CLARO'::text + END AS stop_quality_status + FROM (mv_reports_ucepsa_demo.order_execution_report_v1 r + JOIN mv_hot.edge_oee_machine_stops ms ON (((ms.machine_id = r.machine_id) AND (ms.started_at < r.end_ts) AND (COALESCE(ms.ended_at, r.end_ts) > r.start_ts)))) + WHERE (LEAST(COALESCE(ms.ended_at, r.end_ts), r.end_ts) > GREATEST(ms.started_at, r.start_ts)); + + +ALTER VIEW mv_reports_ucepsa_demo.order_execution_stops_v1 OWNER TO mesavault; + +-- +-- Name: order_execution_stop_causes_v1; Type: VIEW; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +CREATE VIEW mv_reports_ucepsa_demo.order_execution_stop_causes_v1 AS + SELECT order_row_id, + tenant, + site, + odoo_instance, + odoo_workorder_id, + odoo_production_id, + odoo_order_ref, + product, + machine_id, + cause_code, + count(*) AS stop_count, + round(sum(attributed_duration_min), 2) AS total_stop_min, + round(sum(attributed_cost_eur), 2) AS total_stop_cost_eur, + count(*) FILTER (WHERE (classification_status = 'CLASSIFIED'::text)) AS classified_stop_count, + count(*) FILTER (WHERE (classification_status = 'PENDING'::text)) AS pending_stop_count, + min(stop_started_at) AS first_stop_at, + max(stop_ended_at) AS last_stop_at + FROM mv_reports_ucepsa_demo.order_execution_stops_v1 + GROUP BY order_row_id, tenant, site, odoo_instance, odoo_workorder_id, odoo_production_id, odoo_order_ref, product, machine_id, cause_code; + + +ALTER VIEW mv_reports_ucepsa_demo.order_execution_stop_causes_v1 OWNER TO mesavault; + +-- +-- Name: order_execution_timeline_v1; Type: VIEW; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +CREATE VIEW mv_reports_ucepsa_demo.order_execution_timeline_v1 AS + SELECT r.order_row_id, + r.tenant, + r.site, + r.odoo_instance, + r.odoo_workorder_id, + r.odoo_production_id, + r.odoo_order_ref, + r.product, + r.machine_id, + r.asset, + r.start_ts AS event_ts, + 'ORDER_START'::text AS event_type, + 'Inicio de orden'::text AS event_label, + 'INFO'::text AS severity, + NULL::bigint AS stop_id, + NULL::bigint AS node_event_id, + NULL::text AS cause_code, + NULL::numeric AS duration_min, + NULL::numeric AS cost_eur, + 'Inicio de la ventana de ejecución de la orden según Odoo.'::text AS description, + jsonb_build_object('source', 'mv_reports_ucepsa_demo.order_execution_report_v1', 'odoo_state', r.odoo_state, 'qty_planned', r.qty_planned, 'qty_done', r.qty_done, 'uom', r.uom) AS evidence_json + FROM mv_reports_ucepsa_demo.order_execution_report_v1 r + WHERE (r.start_ts IS NOT NULL) +UNION ALL + SELECT r.order_row_id, + r.tenant, + r.site, + r.odoo_instance, + r.odoo_workorder_id, + r.odoo_production_id, + r.odoo_order_ref, + r.product, + r.machine_id, + r.asset, + r.telemetry_first_ts AS event_ts, + 'FIRST_TELEMETRY'::text AS event_type, + 'Primera telemetría Edge-OEE'::text AS event_label, + 'INFO'::text AS severity, + NULL::bigint AS stop_id, + NULL::bigint AS node_event_id, + NULL::text AS cause_code, + NULL::numeric AS duration_min, + NULL::numeric AS cost_eur, + 'Primera muestra de telemetría recibida dentro de la ventana de la orden.'::text AS description, + jsonb_build_object('source', 'mv_reports_ucepsa_demo.order_execution_report_v1', 'telemetry_samples', r.telemetry_samples, 'energy_samples', r.energy_samples) AS evidence_json + FROM mv_reports_ucepsa_demo.order_execution_report_v1 r + WHERE (r.telemetry_first_ts IS NOT NULL) +UNION ALL + SELECT s.order_row_id, + s.tenant, + s.site, + s.odoo_instance, + s.odoo_workorder_id, + s.odoo_production_id, + s.odoo_order_ref, + s.product, + s.machine_id, + s.asset, + s.attributed_started_at AS event_ts, + 'STOP_START'::text AS event_type, + 'Inicio de paro'::text AS event_label, + CASE + WHEN (s.classification_status = 'PENDING'::text) THEN 'WARNING'::text + ELSE 'LOSS'::text + END AS severity, + s.stop_id, + NULL::bigint AS node_event_id, + s.cause_code, + s.attributed_duration_min AS duration_min, + s.attributed_cost_eur AS cost_eur, + concat('Inicio de paro imputado a la orden. Causa: ', COALESCE(s.cause_code, 'sin clasificar'::text), '. Duración imputada: ', COALESCE((s.attributed_duration_min)::text, '0'::text), ' min.') AS description, + jsonb_build_object('source', 'mv_reports_ucepsa_demo.order_execution_stops_v1', 'stop_id', s.stop_id, 'classification_status', s.classification_status, 'operator_note', s.operator_note, 'original_started_at', s.stop_started_at, 'original_ended_at', s.stop_ended_at) AS evidence_json + FROM mv_reports_ucepsa_demo.order_execution_stops_v1 s +UNION ALL + SELECT s.order_row_id, + s.tenant, + s.site, + s.odoo_instance, + s.odoo_workorder_id, + s.odoo_production_id, + s.odoo_order_ref, + s.product, + s.machine_id, + s.asset, + s.attributed_ended_at AS event_ts, + 'STOP_END'::text AS event_type, + 'Fin de paro'::text AS event_label, + CASE + WHEN (s.classification_status = 'PENDING'::text) THEN 'WARNING'::text + ELSE 'INFO'::text + END AS severity, + s.stop_id, + NULL::bigint AS node_event_id, + s.cause_code, + s.attributed_duration_min AS duration_min, + s.attributed_cost_eur AS cost_eur, + concat('Fin de paro imputado a la orden. Causa: ', COALESCE(s.cause_code, 'sin clasificar'::text), '. Coste imputado: ', COALESCE((s.attributed_cost_eur)::text, '0'::text), ' €.') AS description, + jsonb_build_object('source', 'mv_reports_ucepsa_demo.order_execution_stops_v1', 'stop_id', s.stop_id, 'classification_status', s.classification_status, 'operator_note', s.operator_note, 'value_hour_eur', s.value_hour_eur) AS evidence_json + FROM mv_reports_ucepsa_demo.order_execution_stops_v1 s +UNION ALL + SELECT r.order_row_id, + r.tenant, + r.site, + r.odoo_instance, + r.odoo_workorder_id, + r.odoo_production_id, + r.odoo_order_ref, + r.product, + r.machine_id, + r.asset, + r.telemetry_last_ts AS event_ts, + 'LAST_TELEMETRY'::text AS event_type, + 'Última telemetría Edge-OEE'::text AS event_label, + CASE + WHEN (r.report_status = 'PARCIAL_ORDEN_EN_CURSO'::text) THEN 'WARNING'::text + ELSE 'INFO'::text + END AS severity, + NULL::bigint AS stop_id, + NULL::bigint AS node_event_id, + NULL::text AS cause_code, + NULL::numeric AS duration_min, + NULL::numeric AS cost_eur, + 'Última muestra de telemetría recibida dentro de la ventana analizada.'::text AS description, + jsonb_build_object('source', 'mv_reports_ucepsa_demo.order_execution_report_v1', 'real_cycle_delta', r.real_cycle_delta, 'kwh_consumed', r.kwh_consumed, 'avg_kw', r.avg_kw, 'max_kw', r.max_kw) AS evidence_json + FROM mv_reports_ucepsa_demo.order_execution_report_v1 r + WHERE (r.telemetry_last_ts IS NOT NULL) +UNION ALL + SELECT r.order_row_id, + r.tenant, + r.site, + r.odoo_instance, + r.odoo_workorder_id, + r.odoo_production_id, + r.odoo_order_ref, + r.product, + r.machine_id, + r.asset, + r.end_ts AS event_ts, + CASE + WHEN (r.odoo_state = ANY (ARRAY['progress'::text, 'in_progress'::text, 'started'::text])) THEN 'REPORT_CUTOFF'::text + ELSE 'ORDER_END'::text + END AS event_type, + CASE + WHEN (r.odoo_state = ANY (ARRAY['progress'::text, 'in_progress'::text, 'started'::text])) THEN 'Corte provisional del informe'::text + ELSE 'Fin de orden'::text + END AS event_label, + CASE + WHEN (r.report_status = 'CERRADO_CALCULABLE'::text) THEN 'INFO'::text + ELSE 'WARNING'::text + END AS severity, + NULL::bigint AS stop_id, + NULL::bigint AS node_event_id, + NULL::text AS cause_code, + NULL::numeric AS duration_min, + NULL::numeric AS cost_eur, + r.report_status_description AS description, + jsonb_build_object('source', 'mv_reports_ucepsa_demo.order_execution_report_v1', 'report_status', r.report_status, 'window_min', r.window_min, 'stop_count', r.stop_count, 'stop_min', r.stop_min, 'stop_cost_eur', r.stop_cost_eur) AS evidence_json + FROM mv_reports_ucepsa_demo.order_execution_report_v1 r + WHERE (r.end_ts IS NOT NULL); + + +ALTER VIEW mv_reports_ucepsa_demo.order_execution_timeline_v1 OWNER TO mesavault; + +-- +-- Name: order_execution_report_full_v1; Type: VIEW; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +CREATE VIEW mv_reports_ucepsa_demo.order_execution_report_full_v1 AS + WITH stop_causes_json AS ( + SELECT order_execution_stop_causes_v1.order_row_id, + jsonb_agg(jsonb_build_object('cause_code', order_execution_stop_causes_v1.cause_code, 'stop_count', order_execution_stop_causes_v1.stop_count, 'total_stop_min', order_execution_stop_causes_v1.total_stop_min, 'total_stop_cost_eur', order_execution_stop_causes_v1.total_stop_cost_eur, 'classified_stop_count', order_execution_stop_causes_v1.classified_stop_count, 'pending_stop_count', order_execution_stop_causes_v1.pending_stop_count, 'first_stop_at', order_execution_stop_causes_v1.first_stop_at, 'last_stop_at', order_execution_stop_causes_v1.last_stop_at) ORDER BY order_execution_stop_causes_v1.total_stop_min DESC) AS stop_causes_json, + string_agg(concat(COALESCE(order_execution_stop_causes_v1.cause_code, 'sin_clasificar'::text), ': ', order_execution_stop_causes_v1.stop_count, ' paros / ', order_execution_stop_causes_v1.total_stop_min, ' min / ', order_execution_stop_causes_v1.total_stop_cost_eur, ' €'), '; '::text ORDER BY order_execution_stop_causes_v1.total_stop_min DESC) AS stop_causes_summary + FROM mv_reports_ucepsa_demo.order_execution_stop_causes_v1 + GROUP BY order_execution_stop_causes_v1.order_row_id + ), stops_json AS ( + SELECT order_execution_stops_v1.order_row_id, + jsonb_agg(jsonb_build_object('stop_id', order_execution_stops_v1.stop_id, 'stop_started_at', order_execution_stops_v1.stop_started_at, 'stop_ended_at', order_execution_stops_v1.stop_ended_at, 'attributed_started_at', order_execution_stops_v1.attributed_started_at, 'attributed_ended_at', order_execution_stops_v1.attributed_ended_at, 'attributed_duration_min', order_execution_stops_v1.attributed_duration_min, 'cause_code', order_execution_stops_v1.cause_code, 'operator_note', order_execution_stops_v1.operator_note, 'classification_status', order_execution_stops_v1.classification_status, 'attributed_cost_eur', order_execution_stops_v1.attributed_cost_eur, 'stop_quality_status', order_execution_stops_v1.stop_quality_status) ORDER BY order_execution_stops_v1.attributed_started_at) AS stops_json + FROM mv_reports_ucepsa_demo.order_execution_stops_v1 + GROUP BY order_execution_stops_v1.order_row_id + ), timeline_json AS ( + SELECT order_execution_timeline_v1.order_row_id, + jsonb_agg(jsonb_build_object('event_ts', order_execution_timeline_v1.event_ts, 'event_type', order_execution_timeline_v1.event_type, 'event_label', order_execution_timeline_v1.event_label, 'severity', order_execution_timeline_v1.severity, 'stop_id', order_execution_timeline_v1.stop_id, 'cause_code', order_execution_timeline_v1.cause_code, 'duration_min', order_execution_timeline_v1.duration_min, 'cost_eur', order_execution_timeline_v1.cost_eur, 'description', order_execution_timeline_v1.description) ORDER BY order_execution_timeline_v1.event_ts, order_execution_timeline_v1.event_type) AS timeline_json + FROM mv_reports_ucepsa_demo.order_execution_timeline_v1 + GROUP BY order_execution_timeline_v1.order_row_id + ) + SELECT r.order_row_id, + r.tenant, + r.site, + r.odoo_instance, + r.odoo_workorder_id, + r.odoo_production_id, + r.odoo_order_ref, + r.odoo_state, + r.odoo_workcenter_code, + r.odoo_workcenter_name, + r.machine_id, + r.asset, + r.product, + r.qty_planned, + r.qty_done, + r.uom, + r.planned_cycles, + r.target_cycle_rate_ppm, + r.start_ts, + r.end_ts, + r.window_min, + GREATEST((r.window_min - r.stop_min), (0)::numeric) AS run_min, + CASE + WHEN (r.window_min > (0)::numeric) THEN round(((r.stop_min / r.window_min) * 100.0), 2) + ELSE NULL::numeric + END AS stop_pct, + CASE + WHEN (r.window_min > (0)::numeric) THEN round(((GREATEST((r.window_min - r.stop_min), (0)::numeric) / r.window_min) * 100.0), 2) + ELSE NULL::numeric + END AS availability_pct, + r.telemetry_samples, + r.telemetry_first_ts, + r.telemetry_last_ts, + r.energy_samples, + r.energy_first_ts, + r.energy_last_ts, + r.start_import_kwh, + r.end_import_kwh, + r.kwh_consumed, + r.avg_kw, + r.max_kw, + r.min_cycle_pulse_count, + r.max_cycle_pulse_count, + r.real_cycle_delta, + r.avg_cycle_rate_ppm, + r.avg_nonzero_cycle_rate_ppm, + CASE + WHEN (r.real_cycle_delta > 0) THEN round((r.kwh_consumed / (NULLIF(r.real_cycle_delta, 0))::numeric), 6) + ELSE NULL::numeric + END AS kwh_per_cycle, + CASE + WHEN (GREATEST((r.window_min - r.stop_min), (0)::numeric) > (0)::numeric) THEN round(((r.real_cycle_delta)::numeric / NULLIF(GREATEST((r.window_min - r.stop_min), (0)::numeric), (0)::numeric)), 2) + ELSE NULL::numeric + END AS cycles_per_run_min, + r.stop_count, + r.stop_min, + r.classified_stops, + r.pending_stops, + r.stop_cost_eur, + COALESCE(sc.stop_causes_summary, 'Sin paros imputados'::text) AS stop_causes_summary, + COALESCE(sc.stop_causes_json, '[]'::jsonb) AS stop_causes_json, + COALESCE(sj.stops_json, '[]'::jsonb) AS stops_json, + COALESCE(tj.timeline_json, '[]'::jsonb) AS timeline_json, + q.expected_samples_10s, + q.telemetry_sample_coverage_pct, + q.telemetry_gap_count_30s, + q.max_sample_gap_s, + q.avg_sample_gap_s, + q.non_ok_samples, + q.production_conversion_status, + q.technical_quality_score, + q.technical_quality_level, + r.report_status, + r.report_status_description, + q.recommended_action, + CASE + WHEN ((r.report_status = 'CERRADO_CALCULABLE'::text) AND (q.technical_quality_level = 'FIABLE'::text) AND (q.production_conversion_status = 'CICLOS_COHERENTES_CON_OBJETIVO'::text)) THEN 'LISTO_INFORME_FINAL'::text + WHEN ((r.report_status = 'CERRADO_CALCULABLE'::text) AND (q.technical_quality_level = 'FIABLE'::text) AND (q.production_conversion_status = 'REVISAR_CONVERSION_ODOO_CICLOS'::text)) THEN 'LISTO_TECNICO_REVISAR_CONVERSION'::text + WHEN ((r.report_status = 'CERRADO_CALCULABLE'::text) AND (q.technical_quality_level = ANY (ARRAY['FIABLE'::text, 'ACEPTABLE_CON_OBSERVACIONES'::text]))) THEN 'LISTO_CON_OBSERVACIONES'::text + WHEN (r.report_status = 'PARCIAL_ORDEN_EN_CURSO'::text) THEN 'INFORME_PARCIAL_ORDEN_EN_CURSO'::text + WHEN (r.report_status = 'SIN_TELEMETRIA_EN_VENTANA'::text) THEN 'BLOQUEADO_SIN_TELEMETRIA'::text + WHEN (r.report_status = 'SIN_ENERGIA_EN_VENTANA'::text) THEN 'BLOQUEADO_SIN_ENERGIA'::text + WHEN (r.pending_stops > 0) THEN 'PENDIENTE_CLASIFICAR_PAROS'::text + ELSE 'REVISAR'::text + END AS final_report_status, + concat('Orden ', r.odoo_order_ref, ' en ', r.machine_id, ': duración ', r.window_min, ' min; ciclos reales ', r.real_cycle_delta, '; energía ', COALESCE((r.kwh_consumed)::text, 'sin dato'::text), ' kWh; paros ', r.stop_count, ' (', r.stop_min, ' min); coste estimado ', r.stop_cost_eur, ' €. Estado técnico: ', q.technical_quality_level, '.') AS executive_summary + FROM ((((mv_reports_ucepsa_demo.order_execution_report_v1 r + LEFT JOIN mv_reports_ucepsa_demo.order_execution_data_quality_v1 q ON ((q.order_row_id = r.order_row_id))) + LEFT JOIN stop_causes_json sc ON ((sc.order_row_id = r.order_row_id))) + LEFT JOIN stops_json sj ON ((sj.order_row_id = r.order_row_id))) + LEFT JOIN timeline_json tj ON ((tj.order_row_id = r.order_row_id))); + + +ALTER VIEW mv_reports_ucepsa_demo.order_execution_report_full_v1 OWNER TO mesavault; + +-- +-- Name: order_raw_material_costs_v1; Type: VIEW; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +CREATE VIEW mv_reports_ucepsa_demo.order_raw_material_costs_v1 AS + WITH raw_moves AS ( + SELECT c.id AS commercial_row_id, + c.tenant, + c.site, + c.odoo_instance, + c.odoo_production_id, + c.odoo_order_ref, + c.sale_order_ref, + c.customer_name, + c.product_default_code AS finished_product_default_code, + c.product_name AS finished_product_name, + ((rm.raw_move ->> 'id'::text))::integer AS raw_move_id, + (NULLIF(((rm.raw_move -> 'product_id'::text) ->> 0), ''::text))::integer AS raw_product_id, + ((rm.raw_move -> 'product_id'::text) ->> 1) AS raw_product_name, + "substring"(((rm.raw_move -> 'product_id'::text) ->> 1), '\[([^\]]+)\]'::text) AS raw_product_default_code, + (NULLIF((rm.raw_move ->> 'product_uom_qty'::text), ''::text))::numeric AS planned_qty, + (NULLIF((rm.raw_move ->> 'quantity'::text), ''::text))::numeric AS consumed_qty, + ((rm.raw_move -> 'product_uom'::text) ->> 1) AS raw_uom, + (NULLIF((rm.raw_move ->> 'price_unit'::text), ''::text))::numeric AS odoo_price_unit, + (NULLIF((rm.raw_move ->> 'value'::text), ''::text))::numeric AS odoo_value, + (rm.raw_move ->> 'state'::text) AS move_state, + rm.raw_move AS raw_move_json + FROM (mv_edge_oee_ucepsa_demo.odoo_order_commercials c + CROSS JOIN LATERAL jsonb_array_elements(c.stock_raw_moves_json) rm(raw_move)) + ), costed AS ( + SELECT r.commercial_row_id, + r.tenant, + r.site, + r.odoo_instance, + r.odoo_production_id, + r.odoo_order_ref, + r.sale_order_ref, + r.customer_name, + r.finished_product_default_code, + r.finished_product_name, + r.raw_move_id, + r.raw_product_id, + r.raw_product_name, + r.raw_product_default_code, + r.planned_qty, + r.consumed_qty, + r.raw_uom, + r.odoo_price_unit, + r.odoo_value, + r.move_state, + r.raw_move_json, + p.id AS raw_material_cost_parameter_id, + p.material_cost_eur_per_kg, + p.source AS raw_material_cost_source, + p.notes AS raw_material_cost_notes, + CASE + WHEN (COALESCE(r.odoo_value, (0)::numeric) <> (0)::numeric) THEN abs(r.odoo_value) + WHEN ((lower(COALESCE(r.raw_uom, ''::text)) = 'kg'::text) AND (p.material_cost_eur_per_kg IS NOT NULL) AND (r.consumed_qty IS NOT NULL)) THEN (r.consumed_qty * p.material_cost_eur_per_kg) + ELSE NULL::numeric + END AS material_cost_estimated_eur, + CASE + WHEN (COALESCE(r.odoo_value, (0)::numeric) <> (0)::numeric) THEN 'FROM_ODOO_STOCK_MOVE_VALUE'::text + WHEN (r.raw_product_default_code IS NULL) THEN 'MISSING_RAW_PRODUCT_CODE'::text + WHEN (lower(COALESCE(r.raw_uom, ''::text)) <> 'kg'::text) THEN 'UNSUPPORTED_RAW_UOM'::text + WHEN (p.material_cost_eur_per_kg IS NULL) THEN 'MISSING_RAW_MATERIAL_COST_PARAMETER'::text + WHEN (r.consumed_qty IS NULL) THEN 'MISSING_CONSUMED_QTY'::text + ELSE 'FROM_RAW_MATERIAL_COST_PARAMETER'::text + END AS material_cost_status + FROM (raw_moves r + LEFT JOIN LATERAL ( SELECT p_1.id, + p_1.tenant, + p_1.site, + p_1.raw_product_default_code, + p_1.raw_product_name, + p_1.material_cost_eur_per_kg, + p_1.source, + p_1.valid_from, + p_1.valid_to, + p_1.is_active, + p_1.notes, + p_1.created_at + FROM mv_edge_oee_ucepsa_demo.raw_material_cost_parameters p_1 + WHERE ((p_1.tenant = r.tenant) AND (p_1.site = r.site) AND (p_1.is_active = true) AND (p_1.raw_product_default_code = r.raw_product_default_code) AND (p_1.valid_from <= now()) AND ((p_1.valid_to IS NULL) OR (p_1.valid_to > now()))) + ORDER BY p_1.valid_from DESC, p_1.id DESC + LIMIT 1) p ON (true)) + ) + SELECT commercial_row_id, + tenant, + site, + odoo_instance, + odoo_production_id, + odoo_order_ref, + sale_order_ref, + customer_name, + finished_product_default_code, + finished_product_name, + raw_move_id, + raw_product_id, + raw_product_name, + raw_product_default_code, + planned_qty, + consumed_qty, + raw_uom, + odoo_price_unit, + odoo_value, + move_state, + raw_move_json, + raw_material_cost_parameter_id, + material_cost_eur_per_kg, + raw_material_cost_source, + raw_material_cost_notes, + material_cost_estimated_eur, + material_cost_status, + round(material_cost_estimated_eur, 4) AS material_cost_estimated_eur_rounded + FROM costed; + + +ALTER VIEW mv_reports_ucepsa_demo.order_raw_material_costs_v1 OWNER TO mesavault; + +-- +-- Name: order_raw_material_costs_summary_v1; Type: VIEW; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +CREATE VIEW mv_reports_ucepsa_demo.order_raw_material_costs_summary_v1 AS + SELECT tenant, + site, + odoo_instance, + odoo_production_id, + odoo_order_ref, + sale_order_ref, + customer_name, + finished_product_default_code, + finished_product_name, + count(*) AS raw_material_line_count, + round(sum(COALESCE(consumed_qty, (0)::numeric)), 3) AS total_consumed_qty, + string_agg(concat(COALESCE(raw_product_default_code, 'sin_codigo'::text), ' - ', COALESCE(raw_product_name, 'sin_nombre'::text), ': ', COALESCE((consumed_qty)::text, 'sin_cantidad'::text), ' ', COALESCE(raw_uom, ''::text)), '; '::text ORDER BY raw_product_default_code) AS raw_materials_summary, + round(sum(COALESCE(material_cost_estimated_eur, (0)::numeric)), 4) AS material_cost_estimated_eur, + count(*) FILTER (WHERE (material_cost_estimated_eur IS NOT NULL)) AS costed_raw_material_line_count, + count(*) FILTER (WHERE ((material_cost_status <> 'FROM_RAW_MATERIAL_COST_PARAMETER'::text) AND (material_cost_status <> 'FROM_ODOO_STOCK_MOVE_VALUE'::text))) AS missing_or_invalid_raw_material_cost_count, + string_agg(DISTINCT material_cost_status, ', '::text ORDER BY material_cost_status) AS material_cost_status_summary + FROM mv_reports_ucepsa_demo.order_raw_material_costs_v1 + GROUP BY tenant, site, odoo_instance, odoo_production_id, odoo_order_ref, sale_order_ref, customer_name, finished_product_default_code, finished_product_name; + + +ALTER VIEW mv_reports_ucepsa_demo.order_raw_material_costs_summary_v1 OWNER TO mesavault; + +-- +-- Name: order_margin_report_v1; Type: VIEW; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +CREATE VIEW mv_reports_ucepsa_demo.order_margin_report_v1 AS + WITH joined AS ( + SELECT r.order_row_id, + r.tenant, + r.site, + r.odoo_instance, + r.odoo_workorder_id, + r.odoo_production_id, + r.odoo_order_ref, + r.odoo_state, + r.machine_id, + r.asset, + r.product AS edge_product, + r.start_ts, + r.end_ts, + r.window_min, + r.run_min, + r.stop_min, + r.stop_cost_eur, + r.real_cycle_delta, + r.kwh_consumed, + r.kwh_per_cycle, + r.technical_quality_level, + r.final_report_status, + c.odoo_origin, + c.sale_order_ref, + c.customer_id, + c.customer_name, + c.sale_state, + c.sale_date_order, + c.currency, + c.product_id, + c.product_default_code, + c.product_name, + c.production_qty, + c.qty_producing, + c.production_uom, + c.sale_line_id, + c.sale_order_id, + c.sale_line_qty, + c.sale_line_qty_delivered, + c.sale_price_unit, + c.sale_discount_pct, + c.revenue_net_eur, + c.revenue_total_eur, + c.product_standard_price, + c.product_list_price, + c.raw_material_value_eur, + c.finished_value_eur, + c.material_cost_status AS odoo_material_cost_status, + c.snapshot_at AS commercial_snapshot_at, + rms.raw_material_line_count, + rms.total_consumed_qty AS raw_total_consumed_qty, + rms.raw_materials_summary, + rms.material_cost_estimated_eur AS raw_material_cost_estimated_eur, + rms.costed_raw_material_line_count, + rms.missing_or_invalid_raw_material_cost_count, + rms.material_cost_status_summary AS raw_material_cost_status_summary + FROM ((mv_reports_ucepsa_demo.order_execution_report_full_v1 r + LEFT JOIN mv_edge_oee_ucepsa_demo.odoo_order_commercials c ON (((c.odoo_instance = r.odoo_instance) AND (c.odoo_production_id = r.odoo_production_id)))) + LEFT JOIN mv_reports_ucepsa_demo.order_raw_material_costs_summary_v1 rms ON (((rms.odoo_instance = r.odoo_instance) AND (rms.odoo_production_id = r.odoo_production_id)))) + ), with_params AS ( + SELECT j.order_row_id, + j.tenant, + j.site, + j.odoo_instance, + j.odoo_workorder_id, + j.odoo_production_id, + j.odoo_order_ref, + j.odoo_state, + j.machine_id, + j.asset, + j.edge_product, + j.start_ts, + j.end_ts, + j.window_min, + j.run_min, + j.stop_min, + j.stop_cost_eur, + j.real_cycle_delta, + j.kwh_consumed, + j.kwh_per_cycle, + j.technical_quality_level, + j.final_report_status, + j.odoo_origin, + j.sale_order_ref, + j.customer_id, + j.customer_name, + j.sale_state, + j.sale_date_order, + j.currency, + j.product_id, + j.product_default_code, + j.product_name, + j.production_qty, + j.qty_producing, + j.production_uom, + j.sale_line_id, + j.sale_order_id, + j.sale_line_qty, + j.sale_line_qty_delivered, + j.sale_price_unit, + j.sale_discount_pct, + j.revenue_net_eur, + j.revenue_total_eur, + j.product_standard_price, + j.product_list_price, + j.raw_material_value_eur, + j.finished_value_eur, + j.odoo_material_cost_status, + j.commercial_snapshot_at, + j.raw_material_line_count, + j.raw_total_consumed_qty, + j.raw_materials_summary, + j.raw_material_cost_estimated_eur, + j.costed_raw_material_line_count, + j.missing_or_invalid_raw_material_cost_count, + j.raw_material_cost_status_summary, + p.id AS cost_parameter_id, + p.scope_level AS cost_parameter_scope_level, + p.energy_cost_eur_per_kwh, + p.material_cost_eur_per_kg AS fallback_material_cost_eur_per_kg, + p.machine_run_cost_eur_per_h, + p.labor_cost_eur_per_h, + p.overhead_cost_eur_per_h, + p.notes AS cost_parameter_notes + FROM (joined j + LEFT JOIN LATERAL ( SELECT p_1.id, + p_1.tenant, + p_1.site, + p_1.scope_level, + p_1.machine_id, + p_1.product_default_code, + p_1.energy_cost_eur_per_kwh, + p_1.material_cost_eur_per_kg, + p_1.machine_run_cost_eur_per_h, + p_1.labor_cost_eur_per_h, + p_1.overhead_cost_eur_per_h, + p_1.valid_from, + p_1.valid_to, + p_1.is_active, + p_1.notes, + p_1.created_at + FROM mv_edge_oee_ucepsa_demo.margin_cost_parameters p_1 + WHERE ((p_1.is_active = true) AND (p_1.tenant = j.tenant) AND (p_1.site = j.site) AND (p_1.valid_from <= now()) AND ((p_1.valid_to IS NULL) OR (p_1.valid_to > now())) AND ((p_1.machine_id IS NULL) OR (p_1.machine_id = j.machine_id)) AND ((p_1.product_default_code IS NULL) OR (p_1.product_default_code = j.product_default_code))) + ORDER BY + CASE + WHEN ((p_1.product_default_code IS NOT NULL) AND (p_1.machine_id IS NOT NULL)) THEN 1 + WHEN (p_1.product_default_code IS NOT NULL) THEN 2 + WHEN (p_1.machine_id IS NOT NULL) THEN 3 + ELSE 4 + END, p_1.valid_from DESC, p_1.id DESC + LIMIT 1) p ON (true)) + ), costs AS ( + SELECT with_params.order_row_id, + with_params.tenant, + with_params.site, + with_params.odoo_instance, + with_params.odoo_workorder_id, + with_params.odoo_production_id, + with_params.odoo_order_ref, + with_params.odoo_state, + with_params.machine_id, + with_params.asset, + with_params.edge_product, + with_params.start_ts, + with_params.end_ts, + with_params.window_min, + with_params.run_min, + with_params.stop_min, + with_params.stop_cost_eur, + with_params.real_cycle_delta, + with_params.kwh_consumed, + with_params.kwh_per_cycle, + with_params.technical_quality_level, + with_params.final_report_status, + with_params.odoo_origin, + with_params.sale_order_ref, + with_params.customer_id, + with_params.customer_name, + with_params.sale_state, + with_params.sale_date_order, + with_params.currency, + with_params.product_id, + with_params.product_default_code, + with_params.product_name, + with_params.production_qty, + with_params.qty_producing, + with_params.production_uom, + with_params.sale_line_id, + with_params.sale_order_id, + with_params.sale_line_qty, + with_params.sale_line_qty_delivered, + with_params.sale_price_unit, + with_params.sale_discount_pct, + with_params.revenue_net_eur, + with_params.revenue_total_eur, + with_params.product_standard_price, + with_params.product_list_price, + with_params.raw_material_value_eur, + with_params.finished_value_eur, + with_params.odoo_material_cost_status, + with_params.commercial_snapshot_at, + with_params.raw_material_line_count, + with_params.raw_total_consumed_qty, + with_params.raw_materials_summary, + with_params.raw_material_cost_estimated_eur, + with_params.costed_raw_material_line_count, + with_params.missing_or_invalid_raw_material_cost_count, + with_params.raw_material_cost_status_summary, + with_params.cost_parameter_id, + with_params.cost_parameter_scope_level, + with_params.energy_cost_eur_per_kwh, + with_params.fallback_material_cost_eur_per_kg, + with_params.machine_run_cost_eur_per_h, + with_params.labor_cost_eur_per_h, + with_params.overhead_cost_eur_per_h, + with_params.cost_parameter_notes, + COALESCE(NULLIF(with_params.qty_producing, (0)::numeric), NULLIF(with_params.production_qty, (0)::numeric)) AS costing_qty_kg, + CASE + WHEN ((with_params.raw_material_line_count > 0) AND (COALESCE(with_params.missing_or_invalid_raw_material_cost_count, (0)::bigint) = 0) AND (with_params.raw_material_cost_estimated_eur IS NOT NULL)) THEN with_params.raw_material_cost_estimated_eur + WHEN ((with_params.raw_material_line_count > 0) AND (COALESCE(with_params.missing_or_invalid_raw_material_cost_count, (0)::bigint) > 0)) THEN NULL::numeric + WHEN ((with_params.raw_material_value_eur IS NOT NULL) AND (with_params.raw_material_value_eur <> (0)::numeric)) THEN abs(with_params.raw_material_value_eur) + WHEN ((with_params.product_standard_price IS NOT NULL) AND (with_params.product_standard_price <> (0)::numeric) AND (COALESCE(NULLIF(with_params.qty_producing, (0)::numeric), NULLIF(with_params.production_qty, (0)::numeric)) IS NOT NULL)) THEN (with_params.product_standard_price * COALESCE(NULLIF(with_params.qty_producing, (0)::numeric), NULLIF(with_params.production_qty, (0)::numeric))) + WHEN ((with_params.fallback_material_cost_eur_per_kg IS NOT NULL) AND (COALESCE(NULLIF(with_params.qty_producing, (0)::numeric), NULLIF(with_params.production_qty, (0)::numeric)) IS NOT NULL)) THEN (with_params.fallback_material_cost_eur_per_kg * COALESCE(NULLIF(with_params.qty_producing, (0)::numeric), NULLIF(with_params.production_qty, (0)::numeric))) + ELSE NULL::numeric + END AS material_cost_estimated_eur, + CASE + WHEN ((with_params.raw_material_line_count > 0) AND (COALESCE(with_params.missing_or_invalid_raw_material_cost_count, (0)::bigint) = 0)) THEN 'FROM_RAW_MATERIAL_COSTS'::text + WHEN ((with_params.raw_material_line_count > 0) AND (COALESCE(with_params.missing_or_invalid_raw_material_cost_count, (0)::bigint) > 0)) THEN with_params.raw_material_cost_status_summary + WHEN ((with_params.raw_material_value_eur IS NOT NULL) AND (with_params.raw_material_value_eur <> (0)::numeric)) THEN 'FROM_ODOO_RAW_MATERIAL_VALUE'::text + WHEN ((with_params.product_standard_price IS NOT NULL) AND (with_params.product_standard_price <> (0)::numeric)) THEN 'FROM_PRODUCT_STANDARD_PRICE'::text + WHEN (with_params.fallback_material_cost_eur_per_kg IS NOT NULL) THEN 'FROM_FALLBACK_MARGIN_PARAMETER'::text + ELSE 'MISSING_MATERIAL_COST'::text + END AS material_cost_source_status, + CASE + WHEN ((with_params.kwh_consumed IS NOT NULL) AND (with_params.energy_cost_eur_per_kwh IS NOT NULL)) THEN (with_params.kwh_consumed * with_params.energy_cost_eur_per_kwh) + ELSE NULL::numeric + END AS energy_cost_estimated_eur, + CASE + WHEN ((with_params.run_min IS NOT NULL) AND (with_params.machine_run_cost_eur_per_h IS NOT NULL)) THEN ((with_params.run_min / 60.0) * with_params.machine_run_cost_eur_per_h) + ELSE NULL::numeric + END AS machine_run_cost_estimated_eur, + CASE + WHEN ((with_params.window_min IS NOT NULL) AND (with_params.labor_cost_eur_per_h IS NOT NULL)) THEN ((with_params.window_min / 60.0) * with_params.labor_cost_eur_per_h) + ELSE NULL::numeric + END AS labor_cost_estimated_eur, + CASE + WHEN ((with_params.window_min IS NOT NULL) AND (with_params.overhead_cost_eur_per_h IS NOT NULL)) THEN ((with_params.window_min / 60.0) * with_params.overhead_cost_eur_per_h) + ELSE NULL::numeric + END AS overhead_cost_estimated_eur + FROM with_params + ) + SELECT order_row_id, + tenant, + site, + odoo_instance, + odoo_workorder_id, + odoo_production_id, + odoo_order_ref, + odoo_state, + odoo_origin, + sale_order_ref, + customer_id, + customer_name, + sale_state, + sale_date_order, + currency, + machine_id, + asset, + product_id, + product_default_code, + COALESCE(product_name, edge_product) AS product_name, + production_qty, + qty_producing, + production_uom, + costing_qty_kg, + sale_line_id, + sale_order_id, + sale_line_qty, + sale_line_qty_delivered, + sale_price_unit, + sale_discount_pct, + revenue_net_eur, + revenue_total_eur, + start_ts, + end_ts, + window_min, + run_min, + stop_min, + stop_cost_eur, + real_cycle_delta, + kwh_consumed, + kwh_per_cycle, + product_standard_price, + product_list_price, + raw_material_value_eur, + finished_value_eur, + odoo_material_cost_status, + raw_material_line_count, + raw_total_consumed_qty, + raw_materials_summary, + raw_material_cost_status_summary, + costed_raw_material_line_count, + missing_or_invalid_raw_material_cost_count, + cost_parameter_id, + cost_parameter_scope_level, + energy_cost_eur_per_kwh, + fallback_material_cost_eur_per_kg, + machine_run_cost_eur_per_h, + labor_cost_eur_per_h, + overhead_cost_eur_per_h, + round(material_cost_estimated_eur, 4) AS material_cost_estimated_eur, + material_cost_source_status, + round(energy_cost_estimated_eur, 4) AS energy_cost_estimated_eur, + round(machine_run_cost_estimated_eur, 4) AS machine_run_cost_estimated_eur, + round(labor_cost_estimated_eur, 4) AS labor_cost_estimated_eur, + round(overhead_cost_estimated_eur, 4) AS overhead_cost_estimated_eur, + round((((((COALESCE(material_cost_estimated_eur, (0)::numeric) + COALESCE(energy_cost_estimated_eur, (0)::numeric)) + COALESCE(machine_run_cost_estimated_eur, (0)::numeric)) + COALESCE(labor_cost_estimated_eur, (0)::numeric)) + COALESCE(overhead_cost_estimated_eur, (0)::numeric)) + COALESCE(stop_cost_eur, (0)::numeric)), 4) AS known_or_estimated_cost_eur, + round( + CASE + WHEN (revenue_net_eur IS NOT NULL) THEN (revenue_net_eur - (((((COALESCE(material_cost_estimated_eur, (0)::numeric) + COALESCE(energy_cost_estimated_eur, (0)::numeric)) + COALESCE(machine_run_cost_estimated_eur, (0)::numeric)) + COALESCE(labor_cost_estimated_eur, (0)::numeric)) + COALESCE(overhead_cost_estimated_eur, (0)::numeric)) + COALESCE(stop_cost_eur, (0)::numeric))) + ELSE NULL::numeric + END, 4) AS partial_margin_after_known_costs_eur, + round( + CASE + WHEN ((revenue_net_eur IS NOT NULL) AND (material_cost_estimated_eur IS NOT NULL)) THEN (revenue_net_eur - (((((COALESCE(material_cost_estimated_eur, (0)::numeric) + COALESCE(energy_cost_estimated_eur, (0)::numeric)) + COALESCE(machine_run_cost_estimated_eur, (0)::numeric)) + COALESCE(labor_cost_estimated_eur, (0)::numeric)) + COALESCE(overhead_cost_estimated_eur, (0)::numeric)) + COALESCE(stop_cost_eur, (0)::numeric))) + ELSE NULL::numeric + END, 4) AS estimated_margin_eur, + round( + CASE + WHEN ((revenue_net_eur IS NOT NULL) AND (revenue_net_eur <> (0)::numeric) AND (material_cost_estimated_eur IS NOT NULL)) THEN (((revenue_net_eur - (((((COALESCE(material_cost_estimated_eur, (0)::numeric) + COALESCE(energy_cost_estimated_eur, (0)::numeric)) + COALESCE(machine_run_cost_estimated_eur, (0)::numeric)) + COALESCE(labor_cost_estimated_eur, (0)::numeric)) + COALESCE(overhead_cost_estimated_eur, (0)::numeric)) + COALESCE(stop_cost_eur, (0)::numeric))) / revenue_net_eur) * 100.0) + ELSE NULL::numeric + END, 2) AS estimated_margin_pct, + CASE + WHEN (revenue_net_eur IS NULL) THEN 'NO_REVENUE_LINKED'::text + ELSE 'REVENUE_LINKED'::text + END AS revenue_status, + CASE + WHEN ((raw_material_line_count > 0) AND (COALESCE(missing_or_invalid_raw_material_cost_count, (0)::bigint) > 0)) THEN 'MISSING_RAW_MATERIAL_COST_PARAMETER'::text + WHEN (material_cost_estimated_eur IS NULL) THEN 'MISSING_MATERIAL_COST'::text + WHEN (energy_cost_eur_per_kwh IS NULL) THEN 'MISSING_ENERGY_COST'::text + WHEN (machine_run_cost_eur_per_h IS NULL) THEN 'MISSING_MACHINE_RUN_COST'::text + ELSE 'COST_PARAMETERS_AVAILABLE'::text + END AS cost_status, + CASE + WHEN (revenue_net_eur IS NULL) THEN 'NO_MARGIN_WITHOUT_REVENUE'::text + WHEN ((raw_material_line_count > 0) AND (COALESCE(missing_or_invalid_raw_material_cost_count, (0)::bigint) > 0)) THEN 'PARTIAL_MARGIN_MISSING_RAW_MATERIAL_COST'::text + WHEN (material_cost_estimated_eur IS NULL) THEN 'PARTIAL_MARGIN_MISSING_MATERIAL'::text + ELSE 'ESTIMATED_MARGIN_AVAILABLE'::text + END AS margin_status, + technical_quality_level, + final_report_status, + commercial_snapshot_at + FROM costs; + + +ALTER VIEW mv_reports_ucepsa_demo.order_margin_report_v1 OWNER TO mesavault; + +-- +-- Name: cost_readiness_orders_v1; Type: VIEW; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +CREATE VIEW mv_reports_ucepsa_demo.cost_readiness_orders_v1 AS + WITH base AS ( + SELECT m.order_row_id, + m.tenant, + m.site, + m.odoo_instance, + m.odoo_workorder_id, + m.odoo_production_id, + m.odoo_order_ref, + m.odoo_state, + m.odoo_origin, + m.sale_order_ref, + m.customer_id, + m.customer_name, + m.sale_state, + m.sale_date_order, + m.currency, + m.machine_id, + m.asset, + m.product_id, + m.product_default_code, + m.product_name, + m.production_qty, + m.qty_producing, + m.production_uom, + m.costing_qty_kg, + m.sale_line_id, + m.sale_order_id, + m.sale_line_qty, + m.sale_line_qty_delivered, + m.sale_price_unit, + m.sale_discount_pct, + m.revenue_net_eur, + m.revenue_total_eur, + m.start_ts, + m.end_ts, + m.window_min, + m.run_min, + m.stop_min, + m.stop_cost_eur, + m.real_cycle_delta, + m.kwh_consumed, + m.kwh_per_cycle, + m.product_standard_price, + m.product_list_price, + m.raw_material_value_eur, + m.finished_value_eur, + m.odoo_material_cost_status, + m.raw_material_line_count, + m.raw_total_consumed_qty, + m.raw_materials_summary, + m.raw_material_cost_status_summary, + m.costed_raw_material_line_count, + m.missing_or_invalid_raw_material_cost_count, + m.cost_parameter_id, + m.cost_parameter_scope_level, + m.energy_cost_eur_per_kwh, + m.fallback_material_cost_eur_per_kg, + m.machine_run_cost_eur_per_h, + m.labor_cost_eur_per_h, + m.overhead_cost_eur_per_h, + m.material_cost_estimated_eur, + m.material_cost_source_status, + m.energy_cost_estimated_eur, + m.machine_run_cost_estimated_eur, + m.labor_cost_estimated_eur, + m.overhead_cost_estimated_eur, + m.known_or_estimated_cost_eur, + m.partial_margin_after_known_costs_eur, + m.estimated_margin_eur, + m.estimated_margin_pct, + m.revenue_status, + m.cost_status, + m.margin_status, + m.technical_quality_level, + m.final_report_status, + m.commercial_snapshot_at, + CASE + WHEN (m.revenue_net_eur IS NOT NULL) THEN true + ELSE false + END AS has_revenue, + CASE + WHEN (NULLIF(TRIM(BOTH FROM COALESCE(m.customer_name, ''::text)), ''::text) IS NOT NULL) THEN true + ELSE false + END AS has_customer, + CASE + WHEN (NULLIF(TRIM(BOTH FROM COALESCE(m.sale_order_ref, ''::text)), ''::text) IS NOT NULL) THEN true + ELSE false + END AS has_sale_order, + CASE + WHEN (COALESCE(m.raw_material_line_count, (0)::bigint) > 0) THEN true + ELSE false + END AS has_raw_materials, + CASE + WHEN ((COALESCE(m.raw_material_line_count, (0)::bigint) > 0) AND (COALESCE(m.missing_or_invalid_raw_material_cost_count, (0)::bigint) = 0) AND (m.material_cost_estimated_eur IS NOT NULL)) THEN true + ELSE false + END AS has_raw_material_cost, + CASE + WHEN (m.technical_quality_level = ANY (ARRAY['FIABLE'::text, 'ACEPTABLE_CON_OBSERVACIONES'::text])) THEN true + ELSE false + END AS has_usable_technical_data, + CASE + WHEN (m.final_report_status = ANY (ARRAY['LISTO_INFORME_FINAL'::text, 'LISTO_TECNICO_REVISAR_CONVERSION'::text, 'LISTO_CON_OBSERVACIONES'::text])) THEN true + ELSE false + END AS has_closed_or_usable_order_report, + CASE + WHEN ((m.kwh_consumed IS NULL) OR (m.energy_cost_eur_per_kwh IS NOT NULL)) THEN true + ELSE false + END AS has_energy_cost_parameter, + CASE + WHEN ((m.run_min IS NULL) OR (m.machine_run_cost_eur_per_h IS NOT NULL)) THEN true + ELSE false + END AS has_machine_cost_parameter + FROM mv_reports_ucepsa_demo.order_margin_report_v1 m + ), scored AS ( + SELECT b.order_row_id, + b.tenant, + b.site, + b.odoo_instance, + b.odoo_workorder_id, + b.odoo_production_id, + b.odoo_order_ref, + b.odoo_state, + b.odoo_origin, + b.sale_order_ref, + b.customer_id, + b.customer_name, + b.sale_state, + b.sale_date_order, + b.currency, + b.machine_id, + b.asset, + b.product_id, + b.product_default_code, + b.product_name, + b.production_qty, + b.qty_producing, + b.production_uom, + b.costing_qty_kg, + b.sale_line_id, + b.sale_order_id, + b.sale_line_qty, + b.sale_line_qty_delivered, + b.sale_price_unit, + b.sale_discount_pct, + b.revenue_net_eur, + b.revenue_total_eur, + b.start_ts, + b.end_ts, + b.window_min, + b.run_min, + b.stop_min, + b.stop_cost_eur, + b.real_cycle_delta, + b.kwh_consumed, + b.kwh_per_cycle, + b.product_standard_price, + b.product_list_price, + b.raw_material_value_eur, + b.finished_value_eur, + b.odoo_material_cost_status, + b.raw_material_line_count, + b.raw_total_consumed_qty, + b.raw_materials_summary, + b.raw_material_cost_status_summary, + b.costed_raw_material_line_count, + b.missing_or_invalid_raw_material_cost_count, + b.cost_parameter_id, + b.cost_parameter_scope_level, + b.energy_cost_eur_per_kwh, + b.fallback_material_cost_eur_per_kg, + b.machine_run_cost_eur_per_h, + b.labor_cost_eur_per_h, + b.overhead_cost_eur_per_h, + b.material_cost_estimated_eur, + b.material_cost_source_status, + b.energy_cost_estimated_eur, + b.machine_run_cost_estimated_eur, + b.labor_cost_estimated_eur, + b.overhead_cost_estimated_eur, + b.known_or_estimated_cost_eur, + b.partial_margin_after_known_costs_eur, + b.estimated_margin_eur, + b.estimated_margin_pct, + b.revenue_status, + b.cost_status, + b.margin_status, + b.technical_quality_level, + b.final_report_status, + b.commercial_snapshot_at, + b.has_revenue, + b.has_customer, + b.has_sale_order, + b.has_raw_materials, + b.has_raw_material_cost, + b.has_usable_technical_data, + b.has_closed_or_usable_order_report, + b.has_energy_cost_parameter, + b.has_machine_cost_parameter, + GREATEST(0, (((((((((100 - + CASE + WHEN (b.has_revenue = false) THEN 35 + ELSE 0 + END) - + CASE + WHEN (b.has_customer = false) THEN 5 + ELSE 0 + END) - + CASE + WHEN (b.has_sale_order = false) THEN 5 + ELSE 0 + END) - + CASE + WHEN (b.has_usable_technical_data = false) THEN 25 + ELSE 0 + END) - + CASE + WHEN (b.has_closed_or_usable_order_report = false) THEN 15 + ELSE 0 + END) - + CASE + WHEN (b.has_raw_materials = false) THEN 20 + ELSE 0 + END) - + CASE + WHEN ((b.has_raw_materials = true) AND (b.has_raw_material_cost = false)) THEN 35 + ELSE 0 + END) - + CASE + WHEN (b.has_energy_cost_parameter = false) THEN 5 + ELSE 0 + END) - + CASE + WHEN (b.has_machine_cost_parameter = false) THEN 5 + ELSE 0 + END)) AS cost_readiness_score, + concat_ws('; '::text, + CASE + WHEN (b.has_revenue = false) THEN 'Falta ingreso de venta vinculado'::text + ELSE NULL::text + END, + CASE + WHEN (b.has_customer = false) THEN 'Falta cliente'::text + ELSE NULL::text + END, + CASE + WHEN (b.has_sale_order = false) THEN 'Falta pedido de venta'::text + ELSE NULL::text + END, + CASE + WHEN (b.has_usable_technical_data = false) THEN 'Datos técnicos no defendibles'::text + ELSE NULL::text + END, + CASE + WHEN (b.has_closed_or_usable_order_report = false) THEN 'Informe técnico parcial o bloqueado'::text + ELSE NULL::text + END, + CASE + WHEN (b.has_raw_materials = false) THEN 'No se detecta materia prima consumida'::text + ELSE NULL::text + END, + CASE + WHEN ((b.has_raw_materials = true) AND (b.has_raw_material_cost = false)) THEN 'Falta coste de materia prima'::text + ELSE NULL::text + END, + CASE + WHEN (b.has_energy_cost_parameter = false) THEN 'Falta coste €/kWh'::text + ELSE NULL::text + END, + CASE + WHEN (b.has_machine_cost_parameter = false) THEN 'Falta coste hora máquina'::text + ELSE NULL::text + END) AS cost_readiness_blockers + FROM base b + ) + SELECT order_row_id, + tenant, + site, + odoo_instance, + odoo_order_ref, + odoo_state, + sale_order_ref, + customer_name, + product_default_code, + product_name, + machine_id, + revenue_net_eur, + revenue_total_eur, + raw_material_line_count, + raw_total_consumed_qty, + raw_materials_summary, + raw_material_cost_status_summary, + missing_or_invalid_raw_material_cost_count, + material_cost_estimated_eur, + material_cost_source_status, + stop_cost_eur, + kwh_consumed, + run_min, + window_min, + technical_quality_level, + final_report_status, + margin_status, + has_revenue, + has_customer, + has_sale_order, + has_raw_materials, + has_raw_material_cost, + has_usable_technical_data, + has_closed_or_usable_order_report, + has_energy_cost_parameter, + has_machine_cost_parameter, + cost_readiness_score, + CASE + WHEN (has_revenue = false) THEN 'BLOQUEADO_SIN_INGRESO'::text + WHEN (has_usable_technical_data = false) THEN 'BLOQUEADO_DATOS_TECNICOS'::text + WHEN (has_closed_or_usable_order_report = false) THEN 'PARCIAL_INFORME_TECNICO'::text + WHEN (has_raw_materials = false) THEN 'BLOQUEADO_SIN_MATERIA_PRIMA'::text + WHEN ((has_raw_materials = true) AND (has_raw_material_cost = false)) THEN 'BLOQUEADO_FALTA_COSTE_MATERIA_PRIMA'::text + WHEN ((has_energy_cost_parameter = false) OR (has_machine_cost_parameter = false)) THEN 'PARCIAL_FALTAN_COSTES_OPERATIVOS'::text + ELSE 'LISTO_PARA_MARGEN'::text + END AS cost_readiness_status, + CASE + WHEN (cost_readiness_blockers = ''::text) THEN 'Orden lista para cálculo de margen estimado.'::text + ELSE cost_readiness_blockers + END AS cost_readiness_description + FROM scored; + + +ALTER VIEW mv_reports_ucepsa_demo.cost_readiness_orders_v1 OWNER TO mesavault; + +-- +-- Name: cost_readiness_raw_materials_v1; Type: VIEW; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +CREATE VIEW mv_reports_ucepsa_demo.cost_readiness_raw_materials_v1 AS + SELECT tenant, + site, + raw_product_default_code, + raw_product_name, + raw_uom, + count(DISTINCT odoo_order_ref) AS affected_order_count, + string_agg(DISTINCT odoo_order_ref, ', '::text ORDER BY odoo_order_ref) AS affected_orders, + string_agg(DISTINCT COALESCE(sale_order_ref, 'sin_pedido_venta'::text), ', '::text ORDER BY COALESCE(sale_order_ref, 'sin_pedido_venta'::text)) AS affected_sale_orders, + string_agg(DISTINCT COALESCE(customer_name, 'sin_cliente'::text), ', '::text ORDER BY COALESCE(customer_name, 'sin_cliente'::text)) AS affected_customers, + round(sum(COALESCE(consumed_qty, (0)::numeric)), 3) AS total_consumed_qty, + count(*) AS raw_material_line_count, + count(*) FILTER (WHERE (material_cost_status = 'MISSING_RAW_MATERIAL_COST_PARAMETER'::text)) AS missing_cost_line_count, + max(material_cost_eur_per_kg) AS current_material_cost_eur_per_kg, + CASE + WHEN (count(*) FILTER (WHERE (material_cost_status = 'MISSING_RAW_MATERIAL_COST_PARAMETER'::text)) > 0) THEN 'PENDIENTE_COSTE_MATERIA_PRIMA'::text + ELSE 'COSTE_DISPONIBLE'::text + END AS raw_material_readiness_status, + CASE + WHEN (count(*) FILTER (WHERE (material_cost_status = 'MISSING_RAW_MATERIAL_COST_PARAMETER'::text)) > 0) THEN concat('Falta coste €/kg para ', COALESCE(raw_product_default_code, 'sin_codigo'::text), ' - ', COALESCE(raw_product_name, 'sin_nombre'::text)) + ELSE 'Coste de materia prima disponible.'::text + END AS recommended_action + FROM mv_reports_ucepsa_demo.order_raw_material_costs_v1 + GROUP BY tenant, site, raw_product_default_code, raw_product_name, raw_uom; + + +ALTER VIEW mv_reports_ucepsa_demo.cost_readiness_raw_materials_v1 OWNER TO mesavault; + +-- +-- Name: cost_readiness_summary_v1; Type: VIEW; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +CREATE VIEW mv_reports_ucepsa_demo.cost_readiness_summary_v1 AS + SELECT tenant, + site, + count(*) AS total_orders, + count(*) FILTER (WHERE (has_revenue = true)) AS orders_with_revenue, + count(*) FILTER (WHERE (has_revenue = false)) AS orders_without_revenue, + count(*) FILTER (WHERE (has_raw_materials = true)) AS orders_with_raw_materials, + count(*) FILTER (WHERE (has_raw_materials = false)) AS orders_without_raw_materials, + count(*) FILTER (WHERE ((has_raw_materials = true) AND (has_raw_material_cost = false))) AS orders_missing_raw_material_cost, + count(*) FILTER (WHERE (has_usable_technical_data = false)) AS orders_with_bad_technical_data, + count(*) FILTER (WHERE (cost_readiness_status = 'LISTO_PARA_MARGEN'::text)) AS orders_ready_for_margin, + count(*) FILTER (WHERE (cost_readiness_status = 'BLOQUEADO_FALTA_COSTE_MATERIA_PRIMA'::text)) AS orders_blocked_missing_raw_material_cost, + count(*) FILTER (WHERE (cost_readiness_status = 'BLOQUEADO_SIN_INGRESO'::text)) AS orders_blocked_no_revenue, + round(avg(cost_readiness_score), 2) AS avg_cost_readiness_score, + min(cost_readiness_score) AS min_cost_readiness_score, + max(cost_readiness_score) AS max_cost_readiness_score + FROM mv_reports_ucepsa_demo.cost_readiness_orders_v1 + GROUP BY tenant, site; + + +ALTER VIEW mv_reports_ucepsa_demo.cost_readiness_summary_v1 OWNER TO mesavault; + +-- +-- Name: cycle_rate_timeseries; Type: VIEW; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +CREATE VIEW mv_reports_ucepsa_demo.cycle_rate_timeseries AS + SELECT ts AS "time", + cycle_rate_ppm, + 10.0 AS target_cycle_rate_ppm + FROM mv_hot.edge_oee_live_telemetry + WHERE ((asset = 'revpi_oee_node_01'::text) AND (cycle_rate_ppm IS NOT NULL)) + ORDER BY ts; + + +ALTER VIEW mv_reports_ucepsa_demo.cycle_rate_timeseries OWNER TO mesavault; + +-- +-- Name: data_quality_contract_order_checks_v1; Type: VIEW; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +CREATE VIEW mv_reports_ucepsa_demo.data_quality_contract_order_checks_v1 AS + WITH raw_stats AS ( + SELECT order_raw_material_costs_v1.odoo_instance, + order_raw_material_costs_v1.odoo_production_id, + count(*) AS raw_line_count, + count(*) FILTER (WHERE ((order_raw_material_costs_v1.raw_product_default_code IS NULL) OR (TRIM(BOTH FROM order_raw_material_costs_v1.raw_product_default_code) = ''::text))) AS raw_missing_code_count, + string_agg(DISTINCT concat(COALESCE(order_raw_material_costs_v1.raw_product_default_code, 'sin_codigo'::text), ' - ', COALESCE(order_raw_material_costs_v1.raw_product_name, 'sin_nombre'::text)), '; '::text ORDER BY (concat(COALESCE(order_raw_material_costs_v1.raw_product_default_code, 'sin_codigo'::text), ' - ', COALESCE(order_raw_material_costs_v1.raw_product_name, 'sin_nombre'::text)))) AS raw_materials_detected + FROM mv_reports_ucepsa_demo.order_raw_material_costs_v1 + GROUP BY order_raw_material_costs_v1.odoo_instance, order_raw_material_costs_v1.odoo_production_id + ), base AS ( + SELECT m.order_row_id, + m.tenant, + m.site, + m.odoo_instance, + m.odoo_workorder_id, + m.odoo_production_id, + m.odoo_order_ref, + m.odoo_state, + m.sale_order_ref, + m.customer_name, + m.revenue_net_eur, + m.machine_id, + m.asset, + m.product_default_code, + m.product_name, + m.raw_material_line_count, + m.raw_total_consumed_qty, + m.raw_materials_summary, + m.raw_material_cost_status_summary, + m.missing_or_invalid_raw_material_cost_count, + m.material_cost_estimated_eur, + m.material_cost_source_status, + m.energy_cost_eur_per_kwh, + m.machine_run_cost_eur_per_h, + m.kwh_consumed, + m.run_min, + m.stop_cost_eur, + m.technical_quality_level, + m.final_report_status, + m.margin_status, + r_1.pending_stops, + r_1.production_conversion_status, + COALESCE(rs.raw_line_count, (0)::bigint) AS dqc_raw_line_count, + COALESCE(rs.raw_missing_code_count, (0)::bigint) AS dqc_raw_missing_code_count, + rs.raw_materials_detected + FROM ((mv_reports_ucepsa_demo.order_margin_report_v1 m + LEFT JOIN mv_reports_ucepsa_demo.order_execution_report_full_v1 r_1 ON ((r_1.order_row_id = m.order_row_id))) + LEFT JOIN raw_stats rs ON (((rs.odoo_instance = m.odoo_instance) AND (rs.odoo_production_id = m.odoo_production_id)))) + ), evaluations AS ( + SELECT b.order_row_id, + b.tenant, + b.site, + b.odoo_instance, + b.odoo_workorder_id, + b.odoo_production_id, + b.odoo_order_ref, + b.odoo_state, + b.sale_order_ref, + b.customer_name, + b.revenue_net_eur, + b.machine_id, + b.asset, + b.product_default_code, + b.product_name, + b.raw_material_line_count, + b.raw_total_consumed_qty, + b.raw_materials_summary, + b.raw_material_cost_status_summary, + b.missing_or_invalid_raw_material_cost_count, + b.material_cost_estimated_eur, + b.material_cost_source_status, + b.energy_cost_eur_per_kwh, + b.machine_run_cost_eur_per_h, + b.kwh_consumed, + b.run_min, + b.stop_cost_eur, + b.technical_quality_level, + b.final_report_status, + b.margin_status, + b.pending_stops, + b.production_conversion_status, + b.dqc_raw_line_count, + b.dqc_raw_missing_code_count, + b.raw_materials_detected, + 'DQC-ODOO-001'::text AS rule_code, + CASE + WHEN ((b.odoo_order_ref IS NOT NULL) AND (b.odoo_production_id IS NOT NULL)) THEN 'PASS'::text + ELSE 'FAIL'::text + END AS check_status, + CASE + WHEN ((b.odoo_order_ref IS NOT NULL) AND (b.odoo_production_id IS NOT NULL)) THEN 'Orden de fabricación correctamente identificada.'::text + ELSE 'Falta referencia de orden o identificador de producción Odoo.'::text + END AS finding, + CASE + WHEN ((b.odoo_order_ref IS NOT NULL) AND (b.odoo_production_id IS NOT NULL)) THEN 'Sin acción.'::text + ELSE 'Revisar sincronización de mrp.production / mrp.workorder desde Odoo.'::text + END AS recommended_action, + jsonb_build_object('odoo_order_ref', b.odoo_order_ref, 'odoo_production_id', b.odoo_production_id) AS evidence_json + FROM base b + UNION ALL + SELECT b.order_row_id, + b.tenant, + b.site, + b.odoo_instance, + b.odoo_workorder_id, + b.odoo_production_id, + b.odoo_order_ref, + b.odoo_state, + b.sale_order_ref, + b.customer_name, + b.revenue_net_eur, + b.machine_id, + b.asset, + b.product_default_code, + b.product_name, + b.raw_material_line_count, + b.raw_total_consumed_qty, + b.raw_materials_summary, + b.raw_material_cost_status_summary, + b.missing_or_invalid_raw_material_cost_count, + b.material_cost_estimated_eur, + b.material_cost_source_status, + b.energy_cost_eur_per_kwh, + b.machine_run_cost_eur_per_h, + b.kwh_consumed, + b.run_min, + b.stop_cost_eur, + b.technical_quality_level, + b.final_report_status, + b.margin_status, + b.pending_stops, + b.production_conversion_status, + b.dqc_raw_line_count, + b.dqc_raw_missing_code_count, + b.raw_materials_detected, + 'DQC-ODOO-002'::text AS rule_code, + CASE + WHEN ((b.machine_id IS NOT NULL) AND (TRIM(BOTH FROM b.machine_id) <> ''::text)) THEN 'PASS'::text + ELSE 'FAIL'::text + END AS check_status, + CASE + WHEN ((b.machine_id IS NOT NULL) AND (TRIM(BOTH FROM b.machine_id) <> ''::text)) THEN 'Centro de trabajo mapeado a máquina MESAVAULT.'::text + ELSE 'Falta machine_id o mapeo de centro de trabajo.'::text + END AS finding, + CASE + WHEN ((b.machine_id IS NOT NULL) AND (TRIM(BOTH FROM b.machine_id) <> ''::text)) THEN 'Sin acción.'::text + ELSE 'Revisar tabla de mapeo Odoo workcenter → machine_id.'::text + END AS recommended_action, + jsonb_build_object('machine_id', b.machine_id, 'asset', b.asset) AS evidence_json + FROM base b + UNION ALL + SELECT b.order_row_id, + b.tenant, + b.site, + b.odoo_instance, + b.odoo_workorder_id, + b.odoo_production_id, + b.odoo_order_ref, + b.odoo_state, + b.sale_order_ref, + b.customer_name, + b.revenue_net_eur, + b.machine_id, + b.asset, + b.product_default_code, + b.product_name, + b.raw_material_line_count, + b.raw_total_consumed_qty, + b.raw_materials_summary, + b.raw_material_cost_status_summary, + b.missing_or_invalid_raw_material_cost_count, + b.material_cost_estimated_eur, + b.material_cost_source_status, + b.energy_cost_eur_per_kwh, + b.machine_run_cost_eur_per_h, + b.kwh_consumed, + b.run_min, + b.stop_cost_eur, + b.technical_quality_level, + b.final_report_status, + b.margin_status, + b.pending_stops, + b.production_conversion_status, + b.dqc_raw_line_count, + b.dqc_raw_missing_code_count, + b.raw_materials_detected, + 'DQC-ODOO-003'::text AS rule_code, + CASE + WHEN (COALESCE(NULLIF(TRIM(BOTH FROM b.product_name), ''::text), NULLIF(TRIM(BOTH FROM b.product_default_code), ''::text)) IS NULL) THEN 'FAIL'::text + WHEN ((b.product_default_code IS NULL) OR (TRIM(BOTH FROM b.product_default_code) = ''::text)) THEN 'WARN'::text + ELSE 'PASS'::text + END AS check_status, + CASE + WHEN (COALESCE(NULLIF(TRIM(BOTH FROM b.product_name), ''::text), NULLIF(TRIM(BOTH FROM b.product_default_code), ''::text)) IS NULL) THEN 'Falta identificación del producto.'::text + WHEN ((b.product_default_code IS NULL) OR (TRIM(BOTH FROM b.product_default_code) = ''::text)) THEN 'Producto identificado por nombre, pero sin código interno.'::text + ELSE 'Producto identificado con nombre y código interno.'::text + END AS finding, + CASE + WHEN (COALESCE(NULLIF(TRIM(BOTH FROM b.product_name), ''::text), NULLIF(TRIM(BOTH FROM b.product_default_code), ''::text)) IS NULL) THEN 'Revisar maestro de producto en Odoo.'::text + WHEN ((b.product_default_code IS NULL) OR (TRIM(BOTH FROM b.product_default_code) = ''::text)) THEN 'Completar código interno/default_code del producto en Odoo.'::text + ELSE 'Sin acción.'::text + END AS recommended_action, + jsonb_build_object('product_default_code', b.product_default_code, 'product_name', b.product_name) AS evidence_json + FROM base b + UNION ALL + SELECT b.order_row_id, + b.tenant, + b.site, + b.odoo_instance, + b.odoo_workorder_id, + b.odoo_production_id, + b.odoo_order_ref, + b.odoo_state, + b.sale_order_ref, + b.customer_name, + b.revenue_net_eur, + b.machine_id, + b.asset, + b.product_default_code, + b.product_name, + b.raw_material_line_count, + b.raw_total_consumed_qty, + b.raw_materials_summary, + b.raw_material_cost_status_summary, + b.missing_or_invalid_raw_material_cost_count, + b.material_cost_estimated_eur, + b.material_cost_source_status, + b.energy_cost_eur_per_kwh, + b.machine_run_cost_eur_per_h, + b.kwh_consumed, + b.run_min, + b.stop_cost_eur, + b.technical_quality_level, + b.final_report_status, + b.margin_status, + b.pending_stops, + b.production_conversion_status, + b.dqc_raw_line_count, + b.dqc_raw_missing_code_count, + b.raw_materials_detected, + 'DQC-SALES-001'::text AS rule_code, + CASE + WHEN ((b.sale_order_ref IS NOT NULL) AND (b.revenue_net_eur IS NOT NULL)) THEN 'PASS'::text + ELSE 'FAIL'::text + END AS check_status, + CASE + WHEN ((b.sale_order_ref IS NOT NULL) AND (b.revenue_net_eur IS NOT NULL)) THEN 'Ingreso de venta vinculado a la orden.'::text + ELSE 'La orden no tiene ingreso de venta vinculado.'::text + END AS finding, + CASE + WHEN ((b.sale_order_ref IS NOT NULL) AND (b.revenue_net_eur IS NOT NULL)) THEN 'Sin acción.'::text + ELSE 'Revisar sale_line_id, pedido de venta u origen de la orden de fabricación.'::text + END AS recommended_action, + jsonb_build_object('sale_order_ref', b.sale_order_ref, 'revenue_net_eur', b.revenue_net_eur) AS evidence_json + FROM base b + UNION ALL + SELECT b.order_row_id, + b.tenant, + b.site, + b.odoo_instance, + b.odoo_workorder_id, + b.odoo_production_id, + b.odoo_order_ref, + b.odoo_state, + b.sale_order_ref, + b.customer_name, + b.revenue_net_eur, + b.machine_id, + b.asset, + b.product_default_code, + b.product_name, + b.raw_material_line_count, + b.raw_total_consumed_qty, + b.raw_materials_summary, + b.raw_material_cost_status_summary, + b.missing_or_invalid_raw_material_cost_count, + b.material_cost_estimated_eur, + b.material_cost_source_status, + b.energy_cost_eur_per_kwh, + b.machine_run_cost_eur_per_h, + b.kwh_consumed, + b.run_min, + b.stop_cost_eur, + b.technical_quality_level, + b.final_report_status, + b.margin_status, + b.pending_stops, + b.production_conversion_status, + b.dqc_raw_line_count, + b.dqc_raw_missing_code_count, + b.raw_materials_detected, + 'DQC-SALES-002'::text AS rule_code, + CASE + WHEN (b.sale_order_ref IS NULL) THEN 'NOT_APPLICABLE'::text + WHEN ((b.customer_name IS NOT NULL) AND (TRIM(BOTH FROM b.customer_name) <> ''::text)) THEN 'PASS'::text + ELSE 'FAIL'::text + END AS check_status, + CASE + WHEN (b.sale_order_ref IS NULL) THEN 'No aplica porque no hay pedido de venta vinculado.'::text + WHEN ((b.customer_name IS NOT NULL) AND (TRIM(BOTH FROM b.customer_name) <> ''::text)) THEN 'Cliente vinculado correctamente.'::text + ELSE 'Hay pedido de venta, pero falta cliente.'::text + END AS finding, + CASE + WHEN (b.sale_order_ref IS NULL) THEN 'Resolver primero el vínculo de venta.'::text + WHEN ((b.customer_name IS NOT NULL) AND (TRIM(BOTH FROM b.customer_name) <> ''::text)) THEN 'Sin acción.'::text + ELSE 'Revisar partner_id del pedido de venta en Odoo.'::text + END AS recommended_action, + jsonb_build_object('sale_order_ref', b.sale_order_ref, 'customer_name', b.customer_name) AS evidence_json + FROM base b + UNION ALL + SELECT b.order_row_id, + b.tenant, + b.site, + b.odoo_instance, + b.odoo_workorder_id, + b.odoo_production_id, + b.odoo_order_ref, + b.odoo_state, + b.sale_order_ref, + b.customer_name, + b.revenue_net_eur, + b.machine_id, + b.asset, + b.product_default_code, + b.product_name, + b.raw_material_line_count, + b.raw_total_consumed_qty, + b.raw_materials_summary, + b.raw_material_cost_status_summary, + b.missing_or_invalid_raw_material_cost_count, + b.material_cost_estimated_eur, + b.material_cost_source_status, + b.energy_cost_eur_per_kwh, + b.machine_run_cost_eur_per_h, + b.kwh_consumed, + b.run_min, + b.stop_cost_eur, + b.technical_quality_level, + b.final_report_status, + b.margin_status, + b.pending_stops, + b.production_conversion_status, + b.dqc_raw_line_count, + b.dqc_raw_missing_code_count, + b.raw_materials_detected, + 'DQC-RAW-001'::text AS rule_code, + CASE + WHEN (COALESCE(b.raw_material_line_count, (0)::bigint) > 0) THEN 'PASS'::text + ELSE 'FAIL'::text + END AS check_status, + CASE + WHEN (COALESCE(b.raw_material_line_count, (0)::bigint) > 0) THEN 'Materia prima consumida detectada.'::text + ELSE 'No se detecta materia prima consumida para la orden.'::text + END AS finding, + CASE + WHEN (COALESCE(b.raw_material_line_count, (0)::bigint) > 0) THEN 'Sin acción.'::text + ELSE 'Revisar movimientos de stock/MRP asociados a la orden.'::text + END AS recommended_action, + jsonb_build_object('raw_material_line_count', b.raw_material_line_count, 'raw_materials_summary', b.raw_materials_summary) AS evidence_json + FROM base b + UNION ALL + SELECT b.order_row_id, + b.tenant, + b.site, + b.odoo_instance, + b.odoo_workorder_id, + b.odoo_production_id, + b.odoo_order_ref, + b.odoo_state, + b.sale_order_ref, + b.customer_name, + b.revenue_net_eur, + b.machine_id, + b.asset, + b.product_default_code, + b.product_name, + b.raw_material_line_count, + b.raw_total_consumed_qty, + b.raw_materials_summary, + b.raw_material_cost_status_summary, + b.missing_or_invalid_raw_material_cost_count, + b.material_cost_estimated_eur, + b.material_cost_source_status, + b.energy_cost_eur_per_kwh, + b.machine_run_cost_eur_per_h, + b.kwh_consumed, + b.run_min, + b.stop_cost_eur, + b.technical_quality_level, + b.final_report_status, + b.margin_status, + b.pending_stops, + b.production_conversion_status, + b.dqc_raw_line_count, + b.dqc_raw_missing_code_count, + b.raw_materials_detected, + 'DQC-RAW-002'::text AS rule_code, + CASE + WHEN (COALESCE(b.dqc_raw_line_count, (0)::bigint) = 0) THEN 'NOT_APPLICABLE'::text + WHEN (COALESCE(b.dqc_raw_missing_code_count, (0)::bigint) = 0) THEN 'PASS'::text + ELSE 'FAIL'::text + END AS check_status, + CASE + WHEN (COALESCE(b.dqc_raw_line_count, (0)::bigint) = 0) THEN 'No aplica porque no hay materia prima detectada.'::text + WHEN (COALESCE(b.dqc_raw_missing_code_count, (0)::bigint) = 0) THEN 'Todas las materias primas detectadas tienen código identificable.'::text + ELSE 'Hay materias primas sin código identificable.'::text + END AS finding, + CASE + WHEN (COALESCE(b.dqc_raw_line_count, (0)::bigint) = 0) THEN 'Resolver primero la detección de materia prima.'::text + WHEN (COALESCE(b.dqc_raw_missing_code_count, (0)::bigint) = 0) THEN 'Sin acción.'::text + ELSE 'Completar código interno/default_code de materias primas en Odoo.'::text + END AS recommended_action, + jsonb_build_object('raw_line_count', b.dqc_raw_line_count, 'raw_missing_code_count', b.dqc_raw_missing_code_count, 'raw_materials_detected', b.raw_materials_detected) AS evidence_json + FROM base b + UNION ALL + SELECT b.order_row_id, + b.tenant, + b.site, + b.odoo_instance, + b.odoo_workorder_id, + b.odoo_production_id, + b.odoo_order_ref, + b.odoo_state, + b.sale_order_ref, + b.customer_name, + b.revenue_net_eur, + b.machine_id, + b.asset, + b.product_default_code, + b.product_name, + b.raw_material_line_count, + b.raw_total_consumed_qty, + b.raw_materials_summary, + b.raw_material_cost_status_summary, + b.missing_or_invalid_raw_material_cost_count, + b.material_cost_estimated_eur, + b.material_cost_source_status, + b.energy_cost_eur_per_kwh, + b.machine_run_cost_eur_per_h, + b.kwh_consumed, + b.run_min, + b.stop_cost_eur, + b.technical_quality_level, + b.final_report_status, + b.margin_status, + b.pending_stops, + b.production_conversion_status, + b.dqc_raw_line_count, + b.dqc_raw_missing_code_count, + b.raw_materials_detected, + 'DQC-COST-001'::text AS rule_code, + CASE + WHEN (b.material_cost_estimated_eur IS NOT NULL) THEN 'PASS'::text + ELSE 'FAIL'::text + END AS check_status, + CASE + WHEN (b.material_cost_estimated_eur IS NOT NULL) THEN 'Coste de materia prima disponible.'::text + ELSE 'Falta coste de materia prima.'::text + END AS finding, + CASE + WHEN (b.material_cost_estimated_eur IS NOT NULL) THEN 'Sin acción.'::text + ELSE COALESCE(concat('Cargar coste €/kg para materia prima: ', b.raw_materials_summary), 'Cargar coste de materia prima o revisar valoración en Odoo.'::text) + END AS recommended_action, + jsonb_build_object('material_cost_estimated_eur', b.material_cost_estimated_eur, 'material_cost_source_status', b.material_cost_source_status, 'raw_materials_summary', b.raw_materials_summary) AS evidence_json + FROM base b + UNION ALL + SELECT b.order_row_id, + b.tenant, + b.site, + b.odoo_instance, + b.odoo_workorder_id, + b.odoo_production_id, + b.odoo_order_ref, + b.odoo_state, + b.sale_order_ref, + b.customer_name, + b.revenue_net_eur, + b.machine_id, + b.asset, + b.product_default_code, + b.product_name, + b.raw_material_line_count, + b.raw_total_consumed_qty, + b.raw_materials_summary, + b.raw_material_cost_status_summary, + b.missing_or_invalid_raw_material_cost_count, + b.material_cost_estimated_eur, + b.material_cost_source_status, + b.energy_cost_eur_per_kwh, + b.machine_run_cost_eur_per_h, + b.kwh_consumed, + b.run_min, + b.stop_cost_eur, + b.technical_quality_level, + b.final_report_status, + b.margin_status, + b.pending_stops, + b.production_conversion_status, + b.dqc_raw_line_count, + b.dqc_raw_missing_code_count, + b.raw_materials_detected, + 'DQC-COST-002'::text AS rule_code, + CASE + WHEN (b.kwh_consumed IS NULL) THEN 'NOT_APPLICABLE'::text + WHEN (b.energy_cost_eur_per_kwh IS NOT NULL) THEN 'PASS'::text + ELSE 'FAIL'::text + END AS check_status, + CASE + WHEN (b.kwh_consumed IS NULL) THEN 'No aplica porque no hay consumo energético medido.'::text + WHEN (b.energy_cost_eur_per_kwh IS NOT NULL) THEN 'Coste energético parametrizado.'::text + ELSE 'Hay consumo energético, pero falta coste €/kWh.'::text + END AS finding, + CASE + WHEN (b.kwh_consumed IS NULL) THEN 'Sin acción.'::text + WHEN (b.energy_cost_eur_per_kwh IS NOT NULL) THEN 'Sin acción.'::text + ELSE 'Cargar coste €/kWh en margin_cost_parameters.'::text + END AS recommended_action, + jsonb_build_object('kwh_consumed', b.kwh_consumed, 'energy_cost_eur_per_kwh', b.energy_cost_eur_per_kwh) AS evidence_json + FROM base b + UNION ALL + SELECT b.order_row_id, + b.tenant, + b.site, + b.odoo_instance, + b.odoo_workorder_id, + b.odoo_production_id, + b.odoo_order_ref, + b.odoo_state, + b.sale_order_ref, + b.customer_name, + b.revenue_net_eur, + b.machine_id, + b.asset, + b.product_default_code, + b.product_name, + b.raw_material_line_count, + b.raw_total_consumed_qty, + b.raw_materials_summary, + b.raw_material_cost_status_summary, + b.missing_or_invalid_raw_material_cost_count, + b.material_cost_estimated_eur, + b.material_cost_source_status, + b.energy_cost_eur_per_kwh, + b.machine_run_cost_eur_per_h, + b.kwh_consumed, + b.run_min, + b.stop_cost_eur, + b.technical_quality_level, + b.final_report_status, + b.margin_status, + b.pending_stops, + b.production_conversion_status, + b.dqc_raw_line_count, + b.dqc_raw_missing_code_count, + b.raw_materials_detected, + 'DQC-COST-003'::text AS rule_code, + CASE + WHEN (b.run_min IS NULL) THEN 'NOT_APPLICABLE'::text + WHEN (b.machine_run_cost_eur_per_h IS NOT NULL) THEN 'PASS'::text + ELSE 'FAIL'::text + END AS check_status, + CASE + WHEN (b.run_min IS NULL) THEN 'No aplica porque no hay tiempo de máquina utilizable.'::text + WHEN (b.machine_run_cost_eur_per_h IS NOT NULL) THEN 'Coste hora máquina parametrizado.'::text + ELSE 'Hay tiempo de máquina, pero falta coste hora máquina.'::text + END AS finding, + CASE + WHEN (b.run_min IS NULL) THEN 'Sin acción.'::text + WHEN (b.machine_run_cost_eur_per_h IS NOT NULL) THEN 'Sin acción.'::text + ELSE 'Cargar coste hora máquina para la máquina correspondiente.'::text + END AS recommended_action, + jsonb_build_object('machine_id', b.machine_id, 'run_min', b.run_min, 'machine_run_cost_eur_per_h', b.machine_run_cost_eur_per_h) AS evidence_json + FROM base b + UNION ALL + SELECT b.order_row_id, + b.tenant, + b.site, + b.odoo_instance, + b.odoo_workorder_id, + b.odoo_production_id, + b.odoo_order_ref, + b.odoo_state, + b.sale_order_ref, + b.customer_name, + b.revenue_net_eur, + b.machine_id, + b.asset, + b.product_default_code, + b.product_name, + b.raw_material_line_count, + b.raw_total_consumed_qty, + b.raw_materials_summary, + b.raw_material_cost_status_summary, + b.missing_or_invalid_raw_material_cost_count, + b.material_cost_estimated_eur, + b.material_cost_source_status, + b.energy_cost_eur_per_kwh, + b.machine_run_cost_eur_per_h, + b.kwh_consumed, + b.run_min, + b.stop_cost_eur, + b.technical_quality_level, + b.final_report_status, + b.margin_status, + b.pending_stops, + b.production_conversion_status, + b.dqc_raw_line_count, + b.dqc_raw_missing_code_count, + b.raw_materials_detected, + 'DQC-EDGE-001'::text AS rule_code, + CASE + WHEN (b.technical_quality_level = ANY (ARRAY['FIABLE'::text, 'ACEPTABLE_CON_OBSERVACIONES'::text])) THEN 'PASS'::text + ELSE 'FAIL'::text + END AS check_status, + CASE + WHEN (b.technical_quality_level = ANY (ARRAY['FIABLE'::text, 'ACEPTABLE_CON_OBSERVACIONES'::text])) THEN 'Datos técnicos defendibles.'::text + ELSE 'Datos técnicos no defendibles para análisis.'::text + END AS finding, + CASE + WHEN (b.technical_quality_level = ANY (ARRAY['FIABLE'::text, 'ACEPTABLE_CON_OBSERVACIONES'::text])) THEN 'Sin acción.'::text + ELSE 'Revisar telemetría, cobertura Edge-OEE y calidad del informe técnico.'::text + END AS recommended_action, + jsonb_build_object('technical_quality_level', b.technical_quality_level) AS evidence_json + FROM base b + UNION ALL + SELECT b.order_row_id, + b.tenant, + b.site, + b.odoo_instance, + b.odoo_workorder_id, + b.odoo_production_id, + b.odoo_order_ref, + b.odoo_state, + b.sale_order_ref, + b.customer_name, + b.revenue_net_eur, + b.machine_id, + b.asset, + b.product_default_code, + b.product_name, + b.raw_material_line_count, + b.raw_total_consumed_qty, + b.raw_materials_summary, + b.raw_material_cost_status_summary, + b.missing_or_invalid_raw_material_cost_count, + b.material_cost_estimated_eur, + b.material_cost_source_status, + b.energy_cost_eur_per_kwh, + b.machine_run_cost_eur_per_h, + b.kwh_consumed, + b.run_min, + b.stop_cost_eur, + b.technical_quality_level, + b.final_report_status, + b.margin_status, + b.pending_stops, + b.production_conversion_status, + b.dqc_raw_line_count, + b.dqc_raw_missing_code_count, + b.raw_materials_detected, + 'DQC-EDGE-002'::text AS rule_code, + CASE + WHEN (b.final_report_status = ANY (ARRAY['LISTO_INFORME_FINAL'::text, 'LISTO_TECNICO_REVISAR_CONVERSION'::text, 'LISTO_CON_OBSERVACIONES'::text])) THEN 'PASS'::text + ELSE 'FAIL'::text + END AS check_status, + CASE + WHEN (b.final_report_status = ANY (ARRAY['LISTO_INFORME_FINAL'::text, 'LISTO_TECNICO_REVISAR_CONVERSION'::text, 'LISTO_CON_OBSERVACIONES'::text])) THEN 'Informe técnico cerrado o utilizable.'::text + ELSE 'Informe técnico parcial o bloqueado.'::text + END AS finding, + CASE + WHEN (b.final_report_status = ANY (ARRAY['LISTO_INFORME_FINAL'::text, 'LISTO_TECNICO_REVISAR_CONVERSION'::text, 'LISTO_CON_OBSERVACIONES'::text])) THEN 'Sin acción.'::text + ELSE 'Cerrar orden, revisar telemetría o completar clasificación antes de análisis económico final.'::text + END AS recommended_action, + jsonb_build_object('final_report_status', b.final_report_status) AS evidence_json + FROM base b + UNION ALL + SELECT b.order_row_id, + b.tenant, + b.site, + b.odoo_instance, + b.odoo_workorder_id, + b.odoo_production_id, + b.odoo_order_ref, + b.odoo_state, + b.sale_order_ref, + b.customer_name, + b.revenue_net_eur, + b.machine_id, + b.asset, + b.product_default_code, + b.product_name, + b.raw_material_line_count, + b.raw_total_consumed_qty, + b.raw_materials_summary, + b.raw_material_cost_status_summary, + b.missing_or_invalid_raw_material_cost_count, + b.material_cost_estimated_eur, + b.material_cost_source_status, + b.energy_cost_eur_per_kwh, + b.machine_run_cost_eur_per_h, + b.kwh_consumed, + b.run_min, + b.stop_cost_eur, + b.technical_quality_level, + b.final_report_status, + b.margin_status, + b.pending_stops, + b.production_conversion_status, + b.dqc_raw_line_count, + b.dqc_raw_missing_code_count, + b.raw_materials_detected, + 'DQC-STOPS-001'::text AS rule_code, + CASE + WHEN (COALESCE(b.pending_stops, (0)::bigint) = 0) THEN 'PASS'::text + ELSE 'FAIL'::text + END AS check_status, + CASE + WHEN (COALESCE(b.pending_stops, (0)::bigint) = 0) THEN 'No hay paros pendientes de clasificación.'::text + ELSE 'Hay paros pendientes de clasificación.'::text + END AS finding, + CASE + WHEN (COALESCE(b.pending_stops, (0)::bigint) = 0) THEN 'Sin acción.'::text + ELSE 'Clasificar paros pendientes desde la consola de paros.'::text + END AS recommended_action, + jsonb_build_object('pending_stops', b.pending_stops) AS evidence_json + FROM base b + UNION ALL + SELECT b.order_row_id, + b.tenant, + b.site, + b.odoo_instance, + b.odoo_workorder_id, + b.odoo_production_id, + b.odoo_order_ref, + b.odoo_state, + b.sale_order_ref, + b.customer_name, + b.revenue_net_eur, + b.machine_id, + b.asset, + b.product_default_code, + b.product_name, + b.raw_material_line_count, + b.raw_total_consumed_qty, + b.raw_materials_summary, + b.raw_material_cost_status_summary, + b.missing_or_invalid_raw_material_cost_count, + b.material_cost_estimated_eur, + b.material_cost_source_status, + b.energy_cost_eur_per_kwh, + b.machine_run_cost_eur_per_h, + b.kwh_consumed, + b.run_min, + b.stop_cost_eur, + b.technical_quality_level, + b.final_report_status, + b.margin_status, + b.pending_stops, + b.production_conversion_status, + b.dqc_raw_line_count, + b.dqc_raw_missing_code_count, + b.raw_materials_detected, + 'DQC-CONV-001'::text AS rule_code, + CASE + WHEN (b.production_conversion_status = 'CICLOS_COHERENTES_CON_OBJETIVO'::text) THEN 'PASS'::text + WHEN (b.production_conversion_status = 'CANTIDAD_ODOO_SIN_CICLOS_EDGE'::text) THEN 'FAIL'::text + ELSE 'WARN'::text + END AS check_status, + CASE + WHEN (b.production_conversion_status = 'CICLOS_COHERENTES_CON_OBJETIVO'::text) THEN 'Conversión producción-ciclos coherente.'::text + WHEN (b.production_conversion_status = 'CANTIDAD_ODOO_SIN_CICLOS_EDGE'::text) THEN 'Odoo muestra cantidad producida, pero Edge no detecta ciclos.'::text + ELSE 'Conversión producción-ciclos pendiente de revisión o no concluyente.'::text + END AS finding, + CASE + WHEN (b.production_conversion_status = 'CICLOS_COHERENTES_CON_OBJETIVO'::text) THEN 'Sin acción.'::text + ELSE 'Validar relación entre kg, bolsas, unidad comercial y ciclos/golpes.'::text + END AS recommended_action, + jsonb_build_object('production_conversion_status', b.production_conversion_status) AS evidence_json + FROM base b + ) + SELECT e.order_row_id, + e.tenant, + e.site, + e.odoo_instance, + e.odoo_workorder_id, + e.odoo_production_id, + e.odoo_order_ref, + e.odoo_state, + e.sale_order_ref, + e.customer_name, + e.product_default_code, + e.product_name, + e.machine_id, + r.rule_code, + r.rule_name, + r.domain, + r.dimension, + r.severity, + r.owner_area, + r.applies_to, + r.blocking_for, + e.check_status, + CASE + WHEN (e.check_status = 'PASS'::text) THEN true + WHEN (e.check_status = 'NOT_APPLICABLE'::text) THEN NULL::boolean + ELSE false + END AS check_passed, + e.finding, + e.recommended_action, + e.evidence_json, + CASE + WHEN ((e.check_status = 'FAIL'::text) AND (r.severity = 'CRITICAL'::text)) THEN 100 + WHEN ((e.check_status = 'FAIL'::text) AND (r.severity = 'HIGH'::text)) THEN 75 + WHEN ((e.check_status = 'FAIL'::text) AND (r.severity = 'MEDIUM'::text)) THEN 50 + WHEN ((e.check_status = 'WARN'::text) AND (r.severity = 'CRITICAL'::text)) THEN 60 + WHEN ((e.check_status = 'WARN'::text) AND (r.severity = 'HIGH'::text)) THEN 45 + WHEN ((e.check_status = 'WARN'::text) AND (r.severity = 'MEDIUM'::text)) THEN 30 + ELSE 0 + END AS issue_score + FROM (evaluations e + JOIN mv_edge_oee_ucepsa_demo.data_quality_contract_rules r ON (((r.tenant = e.tenant) AND (r.site = e.site) AND (r.rule_code = e.rule_code) AND (r.is_active = true)))); + + +ALTER VIEW mv_reports_ucepsa_demo.data_quality_contract_order_checks_v1 OWNER TO mesavault; + +-- +-- Name: data_quality_contract_order_summary_v1; Type: VIEW; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +CREATE VIEW mv_reports_ucepsa_demo.data_quality_contract_order_summary_v1 AS + WITH checks AS ( + SELECT data_quality_contract_order_checks_v1.order_row_id, + data_quality_contract_order_checks_v1.tenant, + data_quality_contract_order_checks_v1.site, + data_quality_contract_order_checks_v1.odoo_instance, + data_quality_contract_order_checks_v1.odoo_workorder_id, + data_quality_contract_order_checks_v1.odoo_production_id, + data_quality_contract_order_checks_v1.odoo_order_ref, + data_quality_contract_order_checks_v1.odoo_state, + data_quality_contract_order_checks_v1.sale_order_ref, + data_quality_contract_order_checks_v1.customer_name, + data_quality_contract_order_checks_v1.product_default_code, + data_quality_contract_order_checks_v1.product_name, + data_quality_contract_order_checks_v1.machine_id, + data_quality_contract_order_checks_v1.rule_code, + data_quality_contract_order_checks_v1.rule_name, + data_quality_contract_order_checks_v1.domain, + data_quality_contract_order_checks_v1.dimension, + data_quality_contract_order_checks_v1.severity, + data_quality_contract_order_checks_v1.owner_area, + data_quality_contract_order_checks_v1.applies_to, + data_quality_contract_order_checks_v1.blocking_for, + data_quality_contract_order_checks_v1.check_status, + data_quality_contract_order_checks_v1.check_passed, + data_quality_contract_order_checks_v1.finding, + data_quality_contract_order_checks_v1.recommended_action, + data_quality_contract_order_checks_v1.evidence_json, + data_quality_contract_order_checks_v1.issue_score + FROM mv_reports_ucepsa_demo.data_quality_contract_order_checks_v1 + ), agg AS ( + SELECT checks.order_row_id, + checks.tenant, + checks.site, + checks.odoo_instance, + checks.odoo_workorder_id, + checks.odoo_production_id, + checks.odoo_order_ref, + checks.odoo_state, + checks.sale_order_ref, + checks.customer_name, + checks.product_default_code, + checks.product_name, + checks.machine_id, + count(*) AS total_rules, + count(*) FILTER (WHERE (checks.check_status = 'PASS'::text)) AS passed_rules, + count(*) FILTER (WHERE (checks.check_status = 'FAIL'::text)) AS failed_rules, + count(*) FILTER (WHERE (checks.check_status = 'WARN'::text)) AS warn_rules, + count(*) FILTER (WHERE (checks.check_status = 'NOT_APPLICABLE'::text)) AS not_applicable_rules, + count(*) FILTER (WHERE ((checks.check_status = 'FAIL'::text) AND (checks.severity = 'CRITICAL'::text))) AS critical_failures, + count(*) FILTER (WHERE ((checks.check_status = 'FAIL'::text) AND (checks.severity = 'HIGH'::text))) AS high_failures, + count(*) FILTER (WHERE ((checks.check_status = 'FAIL'::text) AND (checks.severity = 'MEDIUM'::text))) AS medium_failures, + count(*) FILTER (WHERE ((checks.check_status = 'WARN'::text) AND (checks.severity = 'CRITICAL'::text))) AS critical_warnings, + count(*) FILTER (WHERE ((checks.check_status = 'WARN'::text) AND (checks.severity = 'HIGH'::text))) AS high_warnings, + count(*) FILTER (WHERE ((checks.check_status = 'WARN'::text) AND (checks.severity = 'MEDIUM'::text))) AS medium_warnings, + COALESCE(sum(checks.issue_score), (0)::bigint) AS total_issue_score, + max(checks.issue_score) AS max_issue_score + FROM checks + GROUP BY checks.order_row_id, checks.tenant, checks.site, checks.odoo_instance, checks.odoo_workorder_id, checks.odoo_production_id, checks.odoo_order_ref, checks.odoo_state, checks.sale_order_ref, checks.customer_name, checks.product_default_code, checks.product_name, checks.machine_id + ), main_issue AS ( + SELECT DISTINCT ON (checks.order_row_id) checks.order_row_id, + checks.rule_code AS main_blocker_rule_code, + checks.rule_name AS main_blocker_rule_name, + checks.domain AS main_blocker_domain, + checks.severity AS main_blocker_severity, + checks.owner_area AS owner_area_to_act, + checks.blocking_for, + checks.check_status AS main_blocker_status, + checks.finding AS main_blocker_finding, + checks.recommended_action AS main_recommended_action, + checks.issue_score AS main_issue_score + FROM checks + WHERE (checks.check_status = ANY (ARRAY['FAIL'::text, 'WARN'::text])) + ORDER BY checks.order_row_id, checks.issue_score DESC, + CASE checks.severity + WHEN 'CRITICAL'::text THEN 1 + WHEN 'HIGH'::text THEN 2 + WHEN 'MEDIUM'::text THEN 3 + ELSE 4 + END, checks.rule_code + ) + SELECT a.order_row_id, + a.tenant, + a.site, + a.odoo_instance, + a.odoo_workorder_id, + a.odoo_production_id, + a.odoo_order_ref, + a.odoo_state, + a.sale_order_ref, + a.customer_name, + a.product_default_code, + a.product_name, + a.machine_id, + a.total_rules, + a.passed_rules, + a.failed_rules, + a.warn_rules, + a.not_applicable_rules, + a.critical_failures, + a.high_failures, + a.medium_failures, + a.critical_warnings, + a.high_warnings, + a.medium_warnings, + a.total_issue_score, + a.max_issue_score, + GREATEST((0)::bigint, (100 - LEAST(a.total_issue_score, (100)::bigint))) AS contract_score, + CASE + WHEN (a.critical_failures > 0) THEN 'BLOQUEADO_CRITICO'::text + WHEN (a.high_failures > 0) THEN 'BLOQUEADO_ALTO'::text + WHEN (a.medium_failures > 0) THEN 'BLOQUEADO_MEDIO'::text + WHEN (a.warn_rules > 0) THEN 'VALIDO_CON_OBSERVACIONES'::text + WHEN ((a.failed_rules = 0) AND (a.warn_rules = 0)) THEN 'CONTRATO_CUMPLIDO'::text + ELSE 'REVISAR'::text + END AS contract_status, + mi.main_blocker_rule_code, + mi.main_blocker_rule_name, + mi.main_blocker_domain, + mi.main_blocker_severity, + mi.owner_area_to_act, + mi.blocking_for, + mi.main_blocker_status, + mi.main_blocker_finding, + mi.main_recommended_action, + mi.main_issue_score, + CASE + WHEN (a.critical_failures > 0) THEN concat('Contrato bloqueado por fallo crítico: ', COALESCE(mi.main_blocker_finding, 'sin detalle'::text)) + WHEN (a.high_failures > 0) THEN concat('Contrato bloqueado por fallo alto: ', COALESCE(mi.main_blocker_finding, 'sin detalle'::text)) + WHEN (a.medium_failures > 0) THEN concat('Contrato bloqueado por fallo medio: ', COALESCE(mi.main_blocker_finding, 'sin detalle'::text)) + WHEN (a.warn_rules > 0) THEN concat('Contrato válido con observaciones: ', COALESCE(mi.main_blocker_finding, 'sin detalle'::text)) + ELSE 'Contrato de datos cumplido para esta orden.'::text + END AS contract_summary + FROM (agg a + LEFT JOIN main_issue mi ON ((mi.order_row_id = a.order_row_id))); + + +ALTER VIEW mv_reports_ucepsa_demo.data_quality_contract_order_summary_v1 OWNER TO mesavault; + +-- +-- Name: data_quality_contract_owner_summary_v1; Type: VIEW; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +CREATE VIEW mv_reports_ucepsa_demo.data_quality_contract_owner_summary_v1 AS + SELECT tenant, + site, + owner_area, + count(*) FILTER (WHERE (check_status = 'FAIL'::text)) AS failed_checks, + count(*) FILTER (WHERE (check_status = 'WARN'::text)) AS warn_checks, + count(*) FILTER (WHERE ((check_status = 'FAIL'::text) AND (severity = 'CRITICAL'::text))) AS critical_failed_checks, + count(*) FILTER (WHERE ((check_status = 'FAIL'::text) AND (severity = 'HIGH'::text))) AS high_failed_checks, + count(DISTINCT odoo_order_ref) FILTER (WHERE (check_status = ANY (ARRAY['FAIL'::text, 'WARN'::text]))) AS affected_orders, + sum(issue_score) FILTER (WHERE (check_status = ANY (ARRAY['FAIL'::text, 'WARN'::text]))) AS total_issue_score, + string_agg(DISTINCT rule_code, ', '::text ORDER BY rule_code) FILTER (WHERE (check_status = ANY (ARRAY['FAIL'::text, 'WARN'::text]))) AS affected_rules, + string_agg(DISTINCT odoo_order_ref, ', '::text ORDER BY odoo_order_ref) FILTER (WHERE (check_status = ANY (ARRAY['FAIL'::text, 'WARN'::text]))) AS affected_order_refs + FROM mv_reports_ucepsa_demo.data_quality_contract_order_checks_v1 + GROUP BY tenant, site, owner_area; + + +ALTER VIEW mv_reports_ucepsa_demo.data_quality_contract_owner_summary_v1 OWNER TO mesavault; + +-- +-- Name: energy_3phase_timeseries; Type: VIEW; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +CREATE VIEW mv_reports_ucepsa_demo.energy_3phase_timeseries AS + SELECT ingest_ts AS ts, + ingest_ts, + tenant, + site, + asset, + machine_id, + (EXTRACT(epoch FROM (now() - ingest_ts)))::double precision AS age_s, + comm_ok, + CASE + WHEN (lower((payload_json ->> 'sample_valid'::text)) = ANY (ARRAY['true'::text, 't'::text, '1'::text, 'yes'::text])) THEN true + WHEN (lower((payload_json ->> 'sample_valid'::text)) = ANY (ARRAY['false'::text, 'f'::text, '0'::text, 'no'::text])) THEN false + ELSE NULL::boolean + END AS sample_valid, + (payload_json ->> 'energy_meter_model'::text) AS energy_meter_model, + (payload_json ->> 'energy_map'::text) AS energy_map, + voltage_l1_v, + (NULLIF((payload_json ->> 'voltage_l2_v'::text), ''::text))::double precision AS voltage_l2_v, + (NULLIF((payload_json ->> 'voltage_l3_v'::text), ''::text))::double precision AS voltage_l3_v, + current_l1_a, + (NULLIF((payload_json ->> 'current_l2_a'::text), ''::text))::double precision AS current_l2_a, + (NULLIF((payload_json ->> 'current_l3_a'::text), ''::text))::double precision AS current_l3_a, + (NULLIF((payload_json ->> 'active_power_l1_w'::text), ''::text))::double precision AS active_power_l1_w, + (NULLIF((payload_json ->> 'active_power_l2_w'::text), ''::text))::double precision AS active_power_l2_w, + (NULLIF((payload_json ->> 'active_power_l3_w'::text), ''::text))::double precision AS active_power_l3_w, + COALESCE((NULLIF((payload_json ->> 'active_power_w'::text), ''::text))::double precision, ((power_total_kw * 1000.0))::double precision) AS active_power_w, + power_total_kw, + (NULLIF((payload_json ->> 'apparent_power_va'::text), ''::text))::double precision AS apparent_power_va, + (NULLIF((payload_json ->> 'reactive_power_var'::text), ''::text))::double precision AS reactive_power_var, + (NULLIF((payload_json ->> 'power_factor'::text), ''::text))::double precision AS power_factor, + frequency_hz, + import_kwh, + (NULLIF((payload_json ->> 'export_kwh'::text), ''::text))::double precision AS export_kwh, + (NULLIF((payload_json ->> 'total_active_energy_kwh'::text), ''::text))::double precision AS total_active_energy_kwh, + payload_json + FROM mv_hot.edge_oee_eastron_readings r; + + +ALTER VIEW mv_reports_ucepsa_demo.energy_3phase_timeseries OWNER TO mesavault; + +-- +-- Name: energy_3phase_latest; Type: VIEW; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +CREATE VIEW mv_reports_ucepsa_demo.energy_3phase_latest AS + SELECT DISTINCT ON (machine_id) ts, + ingest_ts, + tenant, + site, + asset, + machine_id, + age_s, + comm_ok, + sample_valid, + energy_meter_model, + energy_map, + voltage_l1_v, + voltage_l2_v, + voltage_l3_v, + current_l1_a, + current_l2_a, + current_l3_a, + active_power_l1_w, + active_power_l2_w, + active_power_l3_w, + active_power_w, + power_total_kw, + apparent_power_va, + reactive_power_var, + power_factor, + frequency_hz, + import_kwh, + export_kwh, + total_active_energy_kwh, + payload_json + FROM mv_reports_ucepsa_demo.energy_3phase_timeseries + WHERE (site = 'ucepsa_onpremise'::text) + ORDER BY machine_id, ingest_ts DESC; + + +ALTER VIEW mv_reports_ucepsa_demo.energy_3phase_latest OWNER TO mesavault; + +-- +-- Name: energy_diagnosis; Type: VIEW; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +CREATE VIEW mv_reports_ucepsa_demo.energy_diagnosis AS + SELECT sort_order, + group_name, + metric, + value, + unit, + status, + problem, + likely_impact, + recommended_action + FROM mv_hot.edge_oee_eastron_diagnosis_latest; + + +ALTER VIEW mv_reports_ucepsa_demo.energy_diagnosis OWNER TO mesavault; + +-- +-- Name: energy_import_timeseries; Type: VIEW; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +CREATE VIEW mv_reports_ucepsa_demo.energy_import_timeseries AS + SELECT ingest_ts AS "time", + round((import_kwh * (1000)::numeric), 2) AS import_wh + FROM mv_hot.edge_oee_eastron_readings + WHERE (asset = 'revpi_oee_node_01'::text) + ORDER BY ingest_ts; + + +ALTER VIEW mv_reports_ucepsa_demo.energy_import_timeseries OWNER TO mesavault; + +-- +-- Name: energy_latest; Type: VIEW; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +CREATE VIEW mv_reports_ucepsa_demo.energy_latest AS + SELECT ts, + ingest_ts, + age_s, + tenant, + site, + asset, + meter_model, + meter_id, + machine_id, + voltage_l1_v, + current_l1_a, + power_total_kw, + frequency_hz, + import_kwh, + export_kwh, + comm_ok, + error_text, + comm_risk_level, + comm_status + FROM mv_hot.edge_oee_eastron_dashboard + WHERE (asset = 'revpi_oee_node_01'::text); + + +ALTER VIEW mv_reports_ucepsa_demo.energy_latest OWNER TO mesavault; + +-- +-- Name: energy_power_timeseries; Type: VIEW; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +CREATE VIEW mv_reports_ucepsa_demo.energy_power_timeseries AS + SELECT ingest_ts AS "time", + round((power_total_kw * (1000)::numeric), 2) AS power_w + FROM mv_hot.edge_oee_eastron_readings + WHERE (asset = 'revpi_oee_node_01'::text) + ORDER BY ingest_ts; + + +ALTER VIEW mv_reports_ucepsa_demo.energy_power_timeseries OWNER TO mesavault; + +-- +-- Name: latest_machine_snapshot; Type: VIEW; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +CREATE VIEW mv_reports_ucepsa_demo.latest_machine_snapshot AS + SELECT ts, + ingest_ts, + age_s, + tenant, + site, + asset, + machine_id, + order_id, + voltage_l1_v, + current_l1_a, + power_total_kw, + frequency_hz, + import_kwh, + export_kwh, + comm_ok, + error_text, + machine_auto_signal, + machine_running, + cycle_pulse_count, + cycle_rate_ppm, + last_cycle_age_s, + digital_inputs, + payload_json + FROM mv_hot.edge_oee_latest_machine_snapshot + WHERE (asset = 'revpi_oee_node_01'::text); + + +ALTER VIEW mv_reports_ucepsa_demo.latest_machine_snapshot OWNER TO mesavault; + +-- +-- Name: latest_machine_state_fast; Type: VIEW; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +CREATE VIEW mv_reports_ucepsa_demo.latest_machine_state_fast AS + SELECT ts, + ingest_ts, + age_s, + tenant, + site, + asset, + machine_id, + order_id, + voltage_l1_v, + current_l1_a, + power_total_kw, + frequency_hz, + import_kwh, + export_kwh, + comm_ok, + error_text, + machine_auto_signal, + machine_running, + cycle_pulse_count, + cycle_rate_ppm, + last_cycle_age_s, + digital_inputs, + payload_json + FROM mv_reports_ucepsa_demo.latest_machine_snapshot; + + +ALTER VIEW mv_reports_ucepsa_demo.latest_machine_state_fast OWNER TO mesavault; + +-- +-- Name: live_telemetry; Type: VIEW; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +CREATE VIEW mv_reports_ucepsa_demo.live_telemetry AS + SELECT ts, + ingest_ts, + age_s, + tenant, + site, + asset, + machine_id, + order_id, + voltage_l1_v, + current_l1_a, + power_total_kw, + frequency_hz, + import_kwh, + export_kwh, + comm_ok, + error_text, + machine_auto_signal, + machine_running, + cycle_pulse_count, + cycle_rate_ppm, + last_cycle_age_s, + digital_inputs, + payload_json + FROM mv_hot.edge_oee_live_telemetry + WHERE (asset = 'revpi_oee_node_01'::text); + + +ALTER VIEW mv_reports_ucepsa_demo.live_telemetry OWNER TO mesavault; + +-- +-- Name: odoo_current_order_demo; Type: VIEW; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +CREATE VIEW mv_reports_ucepsa_demo.odoo_current_order_demo AS + WITH latest AS ( + SELECT w.id, + w.tenant, + w.site, + w.odoo_instance, + w.odoo_workorder_id, + w.odoo_production_id, + w.odoo_order_ref, + w.odoo_state, + w.odoo_workcenter_code, + w.odoo_workcenter_name, + w.machine_id, + w.asset, + w.product, + w.qty_planned, + w.qty_done, + w.uom, + w.planned_cycles, + w.target_cycle_rate_ppm, + w.odoo_start, + w.odoo_end, + w.notes, + w.snapshot_at, + row_number() OVER (PARTITION BY w.odoo_instance, w.odoo_workorder_id ORDER BY w.snapshot_at DESC) AS rn + FROM mv_edge_oee_ucepsa_demo.odoo_workorders_demo w + WHERE ((w.odoo_instance = 'odoo_ucepsa_test'::text) AND (w.machine_id = 'CORT-01'::text)) + ), candidates AS ( + SELECT latest.id, + latest.tenant, + latest.site, + latest.odoo_instance, + latest.odoo_workorder_id, + latest.odoo_production_id, + latest.odoo_order_ref, + latest.odoo_state, + latest.odoo_workcenter_code, + latest.odoo_workcenter_name, + latest.machine_id, + latest.asset, + latest.product, + latest.qty_planned, + latest.qty_done, + latest.uom, + latest.planned_cycles, + latest.target_cycle_rate_ppm, + latest.odoo_start, + latest.odoo_end, + latest.notes, + latest.snapshot_at, + latest.rn + FROM latest + WHERE ((latest.rn = 1) AND (COALESCE(latest.odoo_state, ''::text) <> ALL (ARRAY['done'::text, 'cancel'::text, 'cancelled'::text]))) + ) + SELECT id, + tenant, + site, + odoo_instance, + odoo_workorder_id, + odoo_production_id, + odoo_order_ref, + odoo_state, + odoo_workcenter_code, + odoo_workcenter_name, + machine_id, + asset, + product, + qty_planned, + qty_done, + uom, + planned_cycles, + target_cycle_rate_ppm, + odoo_start, + odoo_start AS odoo_reference_start, + CASE + WHEN (odoo_state = ANY (ARRAY['progress'::text, 'in_progress'::text, 'done'::text])) THEN true + ELSE false + END AS odoo_is_started, + odoo_end, + notes, + snapshot_at + FROM candidates + ORDER BY + CASE + WHEN (odoo_state = ANY (ARRAY['progress'::text, 'in_progress'::text])) THEN 1 + WHEN (odoo_state = 'ready'::text) THEN 2 + WHEN (odoo_state = ANY (ARRAY['waiting'::text, 'pending'::text, 'confirmed'::text, 'planned'::text])) THEN 3 + ELSE 9 + END, COALESCE(odoo_start, snapshot_at) DESC + LIMIT 1; + + +ALTER VIEW mv_reports_ucepsa_demo.odoo_current_order_demo OWNER TO mesavault; + +-- +-- Name: stops_recent; Type: VIEW; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +CREATE VIEW mv_reports_ucepsa_demo.stops_recent AS + SELECT id, + tenant, + site, + vertical, + asset, + machine_id, + order_id, + started_at, + ended_at, + duration_s, + duration_min, + cost_eur, + status, + classification_status, + cause_code, + cause_name, + is_planned, + operator_note, + value_hour_eur, + start_event_name, + end_event_name, + created_at, + updated_at + FROM mv_hot.edge_oee_stops_recent + WHERE (asset = 'revpi_oee_node_01'::text); + + +ALTER VIEW mv_reports_ucepsa_demo.stops_recent OWNER TO mesavault; + +-- +-- Name: odoo_vs_machine_demo; Type: VIEW; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +CREATE VIEW mv_reports_ucepsa_demo.odoo_vs_machine_demo AS + WITH odoo AS ( + SELECT odoo_current_order_demo.id, + odoo_current_order_demo.tenant, + odoo_current_order_demo.site, + odoo_current_order_demo.odoo_instance, + odoo_current_order_demo.odoo_workorder_id, + odoo_current_order_demo.odoo_production_id, + odoo_current_order_demo.odoo_order_ref, + odoo_current_order_demo.odoo_state, + odoo_current_order_demo.odoo_workcenter_code, + odoo_current_order_demo.odoo_workcenter_name, + odoo_current_order_demo.machine_id, + odoo_current_order_demo.asset, + odoo_current_order_demo.product, + odoo_current_order_demo.qty_planned, + odoo_current_order_demo.qty_done, + odoo_current_order_demo.uom, + odoo_current_order_demo.planned_cycles, + odoo_current_order_demo.target_cycle_rate_ppm, + odoo_current_order_demo.odoo_start, + odoo_current_order_demo.odoo_reference_start, + odoo_current_order_demo.odoo_is_started, + odoo_current_order_demo.odoo_end, + odoo_current_order_demo.notes, + odoo_current_order_demo.snapshot_at + FROM mv_reports_ucepsa_demo.odoo_current_order_demo + ), machine AS ( + SELECT latest_machine_state_fast.ts, + latest_machine_state_fast.ingest_ts, + latest_machine_state_fast.age_s, + latest_machine_state_fast.tenant, + latest_machine_state_fast.site, + latest_machine_state_fast.asset, + latest_machine_state_fast.machine_id, + latest_machine_state_fast.order_id, + latest_machine_state_fast.voltage_l1_v, + latest_machine_state_fast.current_l1_a, + latest_machine_state_fast.power_total_kw, + latest_machine_state_fast.frequency_hz, + latest_machine_state_fast.import_kwh, + latest_machine_state_fast.export_kwh, + latest_machine_state_fast.comm_ok, + latest_machine_state_fast.error_text, + latest_machine_state_fast.machine_auto_signal, + latest_machine_state_fast.machine_running, + latest_machine_state_fast.cycle_pulse_count, + latest_machine_state_fast.cycle_rate_ppm, + latest_machine_state_fast.last_cycle_age_s, + latest_machine_state_fast.digital_inputs, + latest_machine_state_fast.payload_json + FROM mv_reports_ucepsa_demo.latest_machine_state_fast + WHERE (latest_machine_state_fast.machine_id = 'CORT-01'::text) + ORDER BY latest_machine_state_fast.ingest_ts DESC + LIMIT 1 + ), stops AS ( + SELECT o_1.odoo_workorder_id, + (count(s_1.*))::integer AS stop_count, + COALESCE(round(sum(s_1.duration_min), 2), (0)::numeric) AS stop_min, + COALESCE(round(sum(s_1.cost_eur), 2), (0)::numeric) AS stop_cost_eur + FROM (odoo o_1 + LEFT JOIN mv_reports_ucepsa_demo.stops_recent s_1 ON (((s_1.machine_id = o_1.machine_id) AND (s_1.started_at >= COALESCE(o_1.odoo_reference_start, o_1.odoo_start, o_1.snapshot_at)) AND (s_1.started_at <= COALESCE(o_1.odoo_end, now()))))) + GROUP BY o_1.odoo_workorder_id + ) + SELECT o.odoo_workorder_id, + o.odoo_production_id, + o.odoo_order_ref, + o.odoo_state, + o.odoo_is_started, + o.product, + o.machine_id, + o.asset, + o.odoo_start, + o.odoo_reference_start, + o.odoo_end, + COALESCE(m.machine_running, false) AS machine_running, + m.age_s AS telemetry_age_s, + COALESCE(m.cycle_pulse_count, (0)::bigint) AS cycle_pulse_count, + COALESCE(m.cycle_rate_ppm, (0)::numeric) AS cycle_rate_ppm, + ma.first_machine_activity, + CASE + WHEN ((o.odoo_is_started = true) AND (ma.first_machine_activity IS NOT NULL) AND (COALESCE(o.odoo_reference_start, o.odoo_start) IS NOT NULL)) THEN round((GREATEST(EXTRACT(epoch FROM (ma.first_machine_activity - COALESCE(o.odoo_reference_start, o.odoo_start))), (0)::numeric) / 60.0), 2) + ELSE NULL::numeric + END AS start_delay_min, + COALESCE(s.stop_count, 0) AS stop_count, + COALESCE(s.stop_min, (0)::numeric) AS stop_min, + COALESCE(s.stop_cost_eur, (0)::numeric) AS stop_cost_eur, + (COALESCE(m.cycle_pulse_count, (0)::bigint))::numeric AS real_cycles, + COALESCE(o.planned_cycles, o.qty_planned, (0)::numeric) AS planned_cycles, + CASE + WHEN (COALESCE(o.planned_cycles, o.qty_planned, (0)::numeric) > (0)::numeric) THEN round((((COALESCE(m.cycle_pulse_count, (0)::bigint))::numeric / COALESCE(o.planned_cycles, o.qty_planned, (0)::numeric)) * (100)::numeric), 2) + ELSE (0)::numeric + END AS cycle_completion_pct, + CASE + WHEN ((m.age_s IS NULL) OR (m.age_s > 120)) THEN 'SIN_TELEMETRIA'::text + WHEN ((o.odoo_state = 'ready'::text) AND (COALESCE(m.machine_running, false) = true)) THEN 'MAQUINA_EN_MARCHA_SIN_ORDEN_INICIADA'::text + WHEN ((o.odoo_state = 'ready'::text) AND (COALESCE(m.machine_running, false) = false)) THEN 'ORDEN_PREPARADA_MAQUINA_PARADA'::text + WHEN ((o.odoo_state = ANY (ARRAY['progress'::text, 'in_progress'::text])) AND (COALESCE(m.machine_running, false) = false)) THEN 'ORDEN_EN_CURSO_MAQUINA_PARADA'::text + ELSE 'OK'::text + END AS status_check + FROM (((odoo o + LEFT JOIN machine m ON (true)) + LEFT JOIN LATERAL ( SELECT min(r.ingest_ts) AS first_machine_activity + FROM mv_hot.edge_oee_eastron_readings r + WHERE ((r.machine_id = o.machine_id) AND (((r.payload_json ->> 'machine_running'::text))::boolean = true) AND (r.ingest_ts >= COALESCE(o.odoo_reference_start, o.odoo_start, o.snapshot_at)) AND (r.ingest_ts <= COALESCE(o.odoo_end, now())))) ma ON (true)) + LEFT JOIN stops s ON ((s.odoo_workorder_id = o.odoo_workorder_id))); + + +ALTER VIEW mv_reports_ucepsa_demo.odoo_vs_machine_demo OWNER TO mesavault; + +-- +-- Name: open_stops; Type: VIEW; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +CREATE VIEW mv_reports_ucepsa_demo.open_stops AS + SELECT id, + tenant, + site, + vertical, + asset, + machine_id, + order_id, + started_at, + current_duration_s, + current_duration_min, + current_cost_eur, + status, + classification_status, + cause_code, + cause_name, + operator_note, + value_hour_eur + FROM mv_hot.edge_oee_open_stops + WHERE (asset = 'revpi_oee_node_01'::text); + + +ALTER VIEW mv_reports_ucepsa_demo.open_stops OWNER TO mesavault; + +-- +-- Name: loss_events_current; Type: VIEW; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +CREATE VIEW mv_reports_ucepsa_demo.loss_events_current AS + WITH open_stop_losses AS ( + SELECT now() AS detected_at, + s.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)::numeric) >= (15)::numeric) THEN 'ACTION_REQUIRED'::text + WHEN (COALESCE(s.current_duration_min, (0)::numeric) >= (5)::numeric) THEN 'ATTENTION'::text + ELSE 'INFO'::text + END AS severity, + CASE + WHEN (COALESCE(s.current_duration_min, (0)::numeric) >= (15)::numeric) THEN 80 + WHEN (COALESCE(s.current_duration_min, (0)::numeric) >= (5)::numeric) THEN 55 + ELSE 30 + END 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() AS detected_at, + s.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)::numeric) >= (15)::numeric) THEN 'ACTION_REQUIRED'::text + ELSE 'ATTENTION'::text + END AS severity, + CASE + WHEN (COALESCE(s.duration_min, (0)::numeric) >= (15)::numeric) THEN 75 + ELSE 55 + END 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'::text) AND (s.classification_status = 'PENDING'::text)) + ), odoo_machine_losses AS ( + SELECT now() AS detected_at, + o.machine_id, + COALESCE(o.asset, ''::text) AS asset, + COALESCE(o.odoo_order_ref, ''::text) AS order_ref, + o.status_check AS loss_type, + CASE + WHEN (o.status_check = ANY (ARRAY['ORDEN_EN_CURSO_MAQUINA_PARADA'::text, 'MAQUINA_EN_MARCHA_SIN_ORDEN_INICIADA'::text])) THEN 'ACTION_REQUIRED'::text + WHEN (o.status_check = 'SIN_TELEMETRIA'::text) THEN 'ATTENTION'::text + ELSE 'INFO'::text + END AS severity, + CASE + WHEN (o.status_check = 'ORDEN_EN_CURSO_MAQUINA_PARADA'::text) THEN 85 + WHEN (o.status_check = 'MAQUINA_EN_MARCHA_SIN_ORDEN_INICIADA'::text) THEN 75 + WHEN (o.status_check = 'SIN_TELEMETRIA'::text) THEN 60 + ELSE 20 + END 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 AS status, + CASE + WHEN (o.status_check = 'ORDEN_EN_CURSO_MAQUINA_PARADA'::text) THEN 'Odoo indica orden en curso, pero la máquina está parada.'::text + WHEN (o.status_check = 'MAQUINA_EN_MARCHA_SIN_ORDEN_INICIADA'::text) THEN 'La máquina está en marcha, pero la orden no aparece iniciada en Odoo.'::text + WHEN (o.status_check = 'SIN_TELEMETRIA'::text) THEN 'No hay telemetría reciente para contrastar Odoo contra máquina.'::text + ELSE 'Sin discrepancia relevante.'::text + END AS description, + CASE + WHEN (o.status_check = 'ORDEN_EN_CURSO_MAQUINA_PARADA'::text) THEN 'Revisar paro real, operario, causa pendiente y estado de máquina.'::text + WHEN (o.status_check = 'MAQUINA_EN_MARCHA_SIN_ORDEN_INICIADA'::text) THEN 'Revisar disciplina de inicio de orden en Odoo o desfase de sincronización.'::text + WHEN (o.status_check = 'SIN_TELEMETRIA'::text) THEN 'Revisar RevPi, MQTT, VPN/red, contador y sink.'::text + ELSE 'Sin acción.'::text + 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'::text) + ), telemetry_losses AS ( + SELECT now() AS detected_at, + l.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'::text + WHEN (COALESCE(l.age_s, 999999) > 120) THEN 'ACTION_REQUIRED'::text + WHEN (COALESCE(l.age_s, 999999) > 30) THEN 'ATTENTION'::text + ELSE 'INFO'::text + 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 AS severity_score, + (0)::numeric AS duration_min, + (0)::numeric AS cost_eur, + CASE + WHEN (COALESCE(l.comm_ok, false) = false) THEN 'BAD_COMMUNICATION'::text + WHEN (COALESCE(l.age_s, 999999) > 120) THEN 'STALE_TELEMETRY'::text + WHEN (COALESCE(l.age_s, 999999) > 30) THEN 'DELAYED_TELEMETRY'::text + ELSE 'OK'::text + END AS status, + CASE + WHEN (COALESCE(l.comm_ok, false) = false) THEN 'La RevPi publica, pero la lectura Modbus no es válida.'::text + WHEN (COALESCE(l.age_s, 999999) > 120) THEN 'No hay telemetría reciente de máquina.'::text + WHEN (COALESCE(l.age_s, 999999) > 30) THEN 'La telemetría llega con retraso.'::text + ELSE 'Telemetría correcta.'::text + 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.'::text + WHEN (COALESCE(l.age_s, 999999) > 120) THEN 'Revisar RevPi, red/VPN, Mosquitto y sink MQTT.'::text + WHEN (COALESCE(l.age_s, 999999) > 30) THEN 'Revisar estabilidad de red y frecuencia de publicación.'::text + ELSE 'Sin acción.'::text + 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() AS detected_at, + l.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)::numeric) <= (0)::numeric) THEN 'ATTENTION'::text + WHEN (COALESCE(l.cycle_rate_ppm, (0)::numeric) < (7)::numeric) THEN 'ATTENTION'::text + ELSE 'INFO'::text + END AS severity, + CASE + WHEN (COALESCE(l.cycle_rate_ppm, (0)::numeric) <= (0)::numeric) THEN 50 + WHEN (COALESCE(l.cycle_rate_ppm, (0)::numeric) < (7)::numeric) THEN 40 + ELSE 10 + END 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)::numeric) < (7)::numeric)) + ) + SELECT open_stop_losses.detected_at, + open_stop_losses.machine_id, + open_stop_losses.asset, + open_stop_losses.order_ref, + open_stop_losses.loss_type, + open_stop_losses.severity, + open_stop_losses.severity_score, + open_stop_losses.duration_min, + open_stop_losses.cost_eur, + open_stop_losses.status, + open_stop_losses.description, + open_stop_losses.recommended_action, + open_stop_losses.evidence_json + FROM open_stop_losses +UNION ALL + SELECT unclassified_closed_stop_losses.detected_at, + unclassified_closed_stop_losses.machine_id, + unclassified_closed_stop_losses.asset, + unclassified_closed_stop_losses.order_ref, + unclassified_closed_stop_losses.loss_type, + unclassified_closed_stop_losses.severity, + unclassified_closed_stop_losses.severity_score, + unclassified_closed_stop_losses.duration_min, + unclassified_closed_stop_losses.cost_eur, + unclassified_closed_stop_losses.status, + unclassified_closed_stop_losses.description, + unclassified_closed_stop_losses.recommended_action, + unclassified_closed_stop_losses.evidence_json + FROM unclassified_closed_stop_losses +UNION ALL + SELECT odoo_machine_losses.detected_at, + odoo_machine_losses.machine_id, + odoo_machine_losses.asset, + odoo_machine_losses.order_ref, + odoo_machine_losses.loss_type, + odoo_machine_losses.severity, + odoo_machine_losses.severity_score, + odoo_machine_losses.duration_min, + odoo_machine_losses.cost_eur, + odoo_machine_losses.status, + odoo_machine_losses.description, + odoo_machine_losses.recommended_action, + odoo_machine_losses.evidence_json + FROM odoo_machine_losses +UNION ALL + SELECT telemetry_losses.detected_at, + telemetry_losses.machine_id, + telemetry_losses.asset, + telemetry_losses.order_ref, + telemetry_losses.loss_type, + telemetry_losses.severity, + telemetry_losses.severity_score, + telemetry_losses.duration_min, + telemetry_losses.cost_eur, + telemetry_losses.status, + telemetry_losses.description, + telemetry_losses.recommended_action, + telemetry_losses.evidence_json + FROM telemetry_losses +UNION ALL + SELECT low_cycle_rate_losses.detected_at, + low_cycle_rate_losses.machine_id, + low_cycle_rate_losses.asset, + low_cycle_rate_losses.order_ref, + low_cycle_rate_losses.loss_type, + low_cycle_rate_losses.severity, + low_cycle_rate_losses.severity_score, + low_cycle_rate_losses.duration_min, + low_cycle_rate_losses.cost_eur, + low_cycle_rate_losses.status, + low_cycle_rate_losses.description, + low_cycle_rate_losses.recommended_action, + low_cycle_rate_losses.evidence_json + FROM low_cycle_rate_losses; + + +ALTER VIEW mv_reports_ucepsa_demo.loss_events_current OWNER TO mesavault; + +-- +-- Name: loss_priority_board; Type: VIEW; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +CREATE VIEW mv_reports_ucepsa_demo.loss_priority_board AS + WITH active_orders AS ( + SELECT DISTINCT odoo_current_order_demo.machine_id + FROM mv_reports_ucepsa_demo.odoo_current_order_demo + ), filtered AS ( + SELECT e.detected_at, + e.machine_id, + e.asset, + e.order_ref, + e.loss_type, + e.severity, + e.severity_score, + e.duration_min, + e.cost_eur, + e.status, + e.description, + e.recommended_action, + e.evidence_json + FROM mv_reports_ucepsa_demo.loss_events_current e + WHERE ((NOT ((e.loss_type = ANY (ARRAY['STALE_OR_BAD_TELEMETRY'::text, 'LOW_CYCLE_RATE'::text])) AND (NOT (EXISTS ( SELECT 1 + FROM active_orders a + WHERE (a.machine_id = e.machine_id)))))) AND (NOT ((e.loss_type = 'STALE_OR_BAD_TELEMETRY'::text) AND (EXISTS ( SELECT 1 + FROM mv_reports_ucepsa_demo.loss_events_current x + WHERE ((x.machine_id = e.machine_id) AND (x.loss_type = 'SIN_TELEMETRIA'::text))))))) + ) + SELECT row_number() OVER (ORDER BY ((severity_score + (LEAST((COALESCE(cost_eur, (0)::numeric) * (10)::numeric), (50)::numeric))::integer) + (LEAST((COALESCE(duration_min, (0)::numeric) * (2)::numeric), (40)::numeric))::integer) DESC, detected_at DESC) AS priority_rank, + ((severity_score + (LEAST((COALESCE(cost_eur, (0)::numeric) * (10)::numeric), (50)::numeric))::integer) + (LEAST((COALESCE(duration_min, (0)::numeric) * (2)::numeric), (40)::numeric))::integer) AS priority_score, + CASE + WHEN (((severity_score + (LEAST((COALESCE(cost_eur, (0)::numeric) * (10)::numeric), (50)::numeric))::integer) + (LEAST((COALESCE(duration_min, (0)::numeric) * (2)::numeric), (40)::numeric))::integer) >= 90) THEN 'CRÍTICA'::text + WHEN (((severity_score + (LEAST((COALESCE(cost_eur, (0)::numeric) * (10)::numeric), (50)::numeric))::integer) + (LEAST((COALESCE(duration_min, (0)::numeric) * (2)::numeric), (40)::numeric))::integer) >= 60) THEN 'ALTA'::text + WHEN (((severity_score + (LEAST((COALESCE(cost_eur, (0)::numeric) * (10)::numeric), (50)::numeric))::integer) + (LEAST((COALESCE(duration_min, (0)::numeric) * (2)::numeric), (40)::numeric))::integer) >= 35) THEN 'MEDIA'::text + ELSE 'BAJA'::text + 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 filtered + ORDER BY ((severity_score + (LEAST((COALESCE(cost_eur, (0)::numeric) * (10)::numeric), (50)::numeric))::integer) + (LEAST((COALESCE(duration_min, (0)::numeric) * (2)::numeric), (40)::numeric))::integer) DESC, detected_at DESC; + + +ALTER VIEW mv_reports_ucepsa_demo.loss_priority_board OWNER TO mesavault; + +-- +-- Name: machine_state_latest; Type: VIEW; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +CREATE VIEW mv_reports_ucepsa_demo.machine_state_latest AS + SELECT received_at, + age_s, + event_ts, + tenant, + site, + vertical, + asset, + machine_id, + event_name, + previous_auto_signal, + new_auto_signal, + machine_running, + reason_pending, + cycle_pulse_count, + cycle_rate_ppm, + last_cycle_age_s + FROM mv_hot.edge_oee_machine_state_latest + WHERE (asset = 'revpi_oee_node_01'::text); + + +ALTER VIEW mv_reports_ucepsa_demo.machine_state_latest OWNER TO mesavault; + +-- +-- Name: machine_state_latest_multi; Type: VIEW; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +CREATE VIEW mv_reports_ucepsa_demo.machine_state_latest_multi AS + SELECT ingest_ts, + ts, + tenant, + site, + asset, + machine_id, + age_s, + comm_ok, + sample_valid, + energy_map, + CASE + WHEN (lower((payload_json ->> 'machine_running'::text)) = ANY (ARRAY['true'::text, 't'::text, '1'::text, 'yes'::text])) THEN true + WHEN (lower((payload_json ->> 'machine_running'::text)) = ANY (ARRAY['false'::text, 'f'::text, '0'::text, 'no'::text])) THEN false + ELSE false + END AS machine_running, + COALESCE(((NULLIF((payload_json ->> 'cycle_pulse_count'::text), ''::text))::numeric)::bigint, (0)::bigint) AS cycle_pulse_count, + COALESCE((NULLIF((payload_json ->> 'cycle_rate_ppm'::text), ''::text))::double precision, (0.0)::double precision) AS cycle_rate_ppm, + ((NULLIF((payload_json ->> 'wise_cycle_counter_raw'::text), ''::text))::numeric)::bigint AS wise_cycle_counter_raw, + CASE + WHEN (lower((payload_json ->> 'cycle_input_state'::text)) = ANY (ARRAY['true'::text, 't'::text, '1'::text, 'yes'::text])) THEN true + WHEN (lower((payload_json ->> 'cycle_input_state'::text)) = ANY (ARRAY['false'::text, 'f'::text, '0'::text, 'no'::text])) THEN false + ELSE NULL::boolean + END AS cycle_input_state, + (payload_json -> 'digital_inputs'::text) AS digital_inputs, + voltage_l1_v, + voltage_l2_v, + voltage_l3_v, + current_l1_a, + current_l2_a, + current_l3_a, + active_power_l1_w, + active_power_l2_w, + active_power_l3_w, + power_total_kw, + frequency_hz, + import_kwh, + payload_json + FROM mv_reports_ucepsa_demo.energy_3phase_latest l; + + +ALTER VIEW mv_reports_ucepsa_demo.machine_state_latest_multi OWNER TO mesavault; + +-- +-- Name: machine_stops_grafana; Type: VIEW; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +CREATE VIEW mv_reports_ucepsa_demo.machine_stops_grafana AS + WITH base AS ( + SELECT s.id, + s.tenant, + s.site, + s.vertical, + s.asset, + s.machine_id, + s.order_id, + s.started_at, + s.ended_at, + s.status, + s.classification_status, + s.cause_code, + s.operator_note, + s.value_hour_eur, + s.duration_s, + s.cost_eur, + s.start_event_name, + s.end_event_name, + s.start_payload_json, + s.end_payload_json, + s.created_at, + s.updated_at, + to_jsonb(s.*) AS sj, + COALESCE((to_jsonb(s.*) ->> 'cause_code'::text), (to_jsonb(s.*) ->> 'stop_cause_code'::text), (to_jsonb(s.*) ->> 'reason_code'::text)) AS raw_cause_code, + COALESCE((to_jsonb(s.*) ->> 'cause_id'::text), (to_jsonb(s.*) ->> 'stop_cause_id'::text), (to_jsonb(s.*) ->> 'reason_id'::text)) AS raw_cause_id, + COALESCE((to_jsonb(s.*) ->> 'cause_name'::text), (to_jsonb(s.*) ->> 'stop_cause_name'::text), (to_jsonb(s.*) ->> 'reason_name'::text)) AS raw_cause_name + FROM mv_hot.edge_oee_machine_stops s + ), base_with_cause AS ( + SELECT b_1.id, + b_1.tenant, + b_1.site, + b_1.vertical, + b_1.asset, + b_1.machine_id, + b_1.order_id, + b_1.started_at, + b_1.ended_at, + b_1.status, + b_1.classification_status, + b_1.cause_code, + b_1.operator_note, + b_1.value_hour_eur, + b_1.duration_s, + b_1.cost_eur, + b_1.start_event_name, + b_1.end_event_name, + b_1.start_payload_json, + b_1.end_payload_json, + b_1.created_at, + b_1.updated_at, + b_1.sj, + b_1.raw_cause_code, + b_1.raw_cause_id, + b_1.raw_cause_name, + cause.cj AS cause_json + FROM (base b_1 + LEFT JOIN LATERAL ( SELECT to_jsonb(sc.*) AS cj + FROM mv_hot.edge_oee_stop_causes sc + WHERE (((b_1.raw_cause_id IS NOT NULL) AND ((to_jsonb(sc.*) ->> 'id'::text) = b_1.raw_cause_id)) OR ((b_1.raw_cause_code IS NOT NULL) AND (((to_jsonb(sc.*) ->> 'code'::text) = b_1.raw_cause_code) OR ((to_jsonb(sc.*) ->> 'cause_code'::text) = b_1.raw_cause_code) OR ((to_jsonb(sc.*) ->> 'name'::text) = b_1.raw_cause_code)))) + LIMIT 1) cause ON (true)) + ) + SELECT id, + machine_id, + started_at, + ended_at, + COALESCE(ended_at, now()) AS effective_ended_at, + status, + classification_status, + start_event_name, + end_event_name, + COALESCE(duration_s, EXTRACT(epoch FROM (COALESCE(ended_at, now()) - started_at))) AS duration_s_current, + round((COALESCE(duration_s, EXTRACT(epoch FROM (COALESCE(ended_at, now()) - started_at))) / 60.0), 2) AS duration_min, + COALESCE(cost_eur, (0)::numeric) AS cost_eur, + COALESCE(NULLIF(raw_cause_code, ''::text), NULLIF((cause_json ->> 'code'::text), ''::text), NULLIF((cause_json ->> 'cause_code'::text), ''::text), + CASE + WHEN (classification_status = 'PENDING'::text) THEN 'PENDIENTE'::text + ELSE 'SIN_CAUSA'::text + END) AS cause_code, + COALESCE(NULLIF(raw_cause_name, ''::text), NULLIF((cause_json ->> 'name'::text), ''::text), NULLIF((cause_json ->> 'description'::text), ''::text), NULLIF((cause_json ->> 'label'::text), ''::text), + CASE COALESCE(NULLIF(raw_cause_code, ''::text), NULLIF((cause_json ->> 'code'::text), ''::text)) + WHEN 'cambio_bobina'::text THEN 'Cambio de bobina'::text + WHEN 'espera_instrucciones'::text THEN 'Espera de instrucciones'::text + WHEN 'fallo_empalme'::text THEN 'Fallo de empalme'::text + WHEN 'falta_material'::text THEN 'Falta de material'::text + WHEN 'ajuste_sellado'::text THEN 'Ajuste de sellado'::text + ELSE NULL::text + END, + CASE + WHEN (classification_status = 'PENDING'::text) THEN 'Pendiente de clasificar'::text + ELSE 'Sin causa registrada'::text + END) AS cause_name, + (sj ->> 'operator_note'::text) AS operator_note, + (sj ->> 'classified_by'::text) AS classified_by, + (sj ->> 'classified_at'::text) AS classified_at, + (sj ->> 'order_ref'::text) AS order_ref, + (sj ->> 'production_order'::text) AS production_order, + CASE + WHEN (status = 'OPEN'::text) THEN true + ELSE false + END AS is_open, + CASE + WHEN (classification_status = 'PENDING'::text) THEN true + ELSE false + END AS is_pending_classification, + CASE + WHEN (status = 'OPEN'::text) THEN 'ABIERTO'::text + WHEN (classification_status = 'PENDING'::text) THEN 'CERRADO SIN CLASIFICAR'::text + WHEN (classification_status = 'CLASSIFIED'::text) THEN 'CLASIFICADO'::text + ELSE COALESCE(status, 'SIN_ESTADO'::text) + END AS stop_status_label, + CASE + WHEN (status = 'OPEN'::text) THEN 3 + WHEN (classification_status = 'PENDING'::text) THEN 2 + WHEN (classification_status = 'CLASSIFIED'::text) THEN 1 + ELSE 0 + END AS stop_priority_score + FROM base_with_cause b; + + +ALTER VIEW mv_reports_ucepsa_demo.machine_stops_grafana OWNER TO mesavault; + +-- +-- Name: machine_targets; Type: VIEW; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +CREATE VIEW mv_reports_ucepsa_demo.machine_targets AS + SELECT machine_id, + machine_name, + asset, + target_cycle_rate_ppm, + value_hour_eur, + quality_pct_default, + notes + FROM mv_hot.edge_oee_machine_targets + WHERE (machine_id = 'CORT-01'::text); + + +ALTER VIEW mv_reports_ucepsa_demo.machine_targets OWNER TO mesavault; + +-- +-- Name: node_events_recent; Type: VIEW; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +CREATE VIEW mv_reports_ucepsa_demo.node_events_recent AS + SELECT received_at, + age_s, + event_ts, + tenant, + site, + vertical, + asset, + machine_id, + order_id, + event_type, + event_name, + previous_auto_signal, + new_auto_signal, + machine_running, + reason_pending, + cycle_pulse_count, + cycle_rate_ppm, + last_cycle_age_s, + topic, + payload_json + FROM mv_hot.edge_oee_node_events_recent + WHERE (asset = 'revpi_oee_node_01'::text); + + +ALTER VIEW mv_reports_ucepsa_demo.node_events_recent OWNER TO mesavault; + +-- +-- Name: odoo_discrepancies_demo; Type: VIEW; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +CREATE VIEW mv_reports_ucepsa_demo.odoo_discrepancies_demo AS + SELECT now() AS checked_at, + odoo_order_ref, + product, + machine_id, + status_check, + CASE + WHEN (status_check = 'MAQUINA_EN_MARCHA_SIN_ORDEN_INICIADA'::text) THEN 'La máquina está en marcha, pero la orden de Odoo está preparada o en espera, no iniciada.'::text + WHEN (status_check = 'ORDEN_PREPARADA_MAQUINA_PARADA'::text) THEN 'La orden está preparada/en espera y la máquina está parada. No hay desviación productiva activa.'::text + WHEN (status_check = 'ORDEN_EN_CURSO_MAQUINA_PARADA'::text) THEN 'La orden está en curso en Odoo, pero la máquina está parada.'::text + WHEN (status_check = 'SIN_TELEMETRIA'::text) THEN 'No hay telemetría reciente de máquina.'::text + ELSE 'Sin discrepancia relevante.'::text + END AS descripcion, + start_delay_min, + stop_count, + stop_min, + stop_cost_eur, + cycle_completion_pct + FROM mv_reports_ucepsa_demo.odoo_vs_machine_demo; + + +ALTER VIEW mv_reports_ucepsa_demo.odoo_discrepancies_demo OWNER TO mesavault; + +-- +-- Name: odoo_workorders_reality_history; Type: VIEW; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +CREATE VIEW mv_reports_ucepsa_demo.odoo_workorders_reality_history AS + WITH orders AS ( + SELECT o_1.id, + o_1.tenant, + o_1.site, + o_1.odoo_instance, + o_1.odoo_workorder_id, + o_1.odoo_production_id, + o_1.odoo_order_ref, + o_1.odoo_state, + o_1.odoo_workcenter_code, + o_1.odoo_workcenter_name, + o_1.machine_id, + o_1.asset, + o_1.product, + o_1.qty_planned, + o_1.qty_done, + o_1.uom, + o_1.planned_cycles, + o_1.target_cycle_rate_ppm, + o_1.odoo_start, + o_1.odoo_end, + o_1.notes, + o_1.snapshot_at, + CASE + WHEN ((o_1.odoo_state = ANY (ARRAY['progress'::text, 'in_progress'::text, 'started'::text, 'done'::text, 'to_close'::text])) AND (o_1.odoo_start IS NOT NULL)) THEN true + ELSE false + END AS odoo_is_started_calc, + COALESCE(o_1.odoo_end, now()) AS analysis_end + FROM mv_edge_oee_ucepsa_demo.odoo_workorders_demo o_1 + WHERE ((o_1.machine_id = 'CORT-01'::text) AND (o_1.odoo_instance = 'odoo_demo'::text) AND (o_1.odoo_workorder_id IS NOT NULL)) + ), first_activity AS ( + SELECT o_1.id AS order_row_id, + min(l.ingest_ts) AS first_machine_activity + FROM (orders o_1 + LEFT JOIN mv_reports_ucepsa_demo.live_telemetry l ON (((o_1.odoo_is_started_calc = true) AND (l.asset = o_1.asset) AND (l.ingest_ts >= o_1.odoo_start) AND (l.ingest_ts <= o_1.analysis_end) AND ((l.machine_running = true) OR (COALESCE(l.cycle_pulse_count, (0)::bigint) > 0))))) + GROUP BY o_1.id + ), cycle_calc AS ( + SELECT o_1.id AS order_row_id, + (COALESCE(GREATEST((max(l.cycle_pulse_count) - min(l.cycle_pulse_count)), (0)::bigint), (0)::bigint))::numeric AS real_cycles + FROM (orders o_1 + LEFT JOIN mv_reports_ucepsa_demo.live_telemetry l ON (((o_1.odoo_is_started_calc = true) AND (l.asset = o_1.asset) AND (l.ingest_ts >= o_1.odoo_start) AND (l.ingest_ts <= o_1.analysis_end) AND (l.cycle_pulse_count IS NOT NULL)))) + GROUP BY o_1.id + ), stop_calc AS ( + SELECT o_1.id AS order_row_id, + count(s.stop_id) AS stop_count, + round((COALESCE(sum(s.overlap_s), (0)::numeric) / 60.0), 2) AS stop_min, + round(COALESCE(sum(((s.overlap_s / 3600.0) * s.value_hour_eur)), (0)::numeric), 2) AS stop_cost_eur + FROM (orders o_1 + LEFT JOIN LATERAL ( SELECT ms.id AS stop_id, + ms.value_hour_eur, + GREATEST(EXTRACT(epoch FROM (LEAST(COALESCE(ms.ended_at, o_1.analysis_end), o_1.analysis_end) - GREATEST( + CASE + WHEN (ms.started_at > (ms.created_at + '00:00:05'::interval)) THEN ms.created_at + ELSE ms.started_at + END, o_1.odoo_start))), (0)::numeric) AS overlap_s + FROM mv_hot.edge_oee_machine_stops ms + WHERE ((o_1.odoo_is_started_calc = true) AND (ms.machine_id = o_1.machine_id) AND ( + CASE + WHEN (ms.started_at > (ms.created_at + '00:00:05'::interval)) THEN ms.created_at + ELSE ms.started_at + END < o_1.analysis_end) AND (COALESCE(ms.ended_at, now()) > o_1.odoo_start))) s ON (true)) + GROUP BY o_1.id + ) + SELECT o.id, + o.tenant, + o.site, + o.odoo_instance, + o.odoo_workorder_id, + o.odoo_production_id, + o.odoo_order_ref, + o.odoo_state, + o.odoo_workcenter_code, + o.odoo_workcenter_name, + o.machine_id, + o.asset, + o.product, + o.qty_planned, + o.qty_done, + o.uom, + o.planned_cycles, + o.target_cycle_rate_ppm, + o.odoo_start, + o.odoo_end, + o.snapshot_at, + o.odoo_is_started_calc AS odoo_is_started, + fa.first_machine_activity, + CASE + WHEN (o.odoo_is_started_calc = false) THEN NULL::numeric + WHEN (fa.first_machine_activity IS NULL) THEN NULL::numeric + ELSE round((EXTRACT(epoch FROM (fa.first_machine_activity - o.odoo_start)) / 60.0), 2) + END AS start_delay_min, + COALESCE(sc.stop_count, (0)::bigint) AS stop_count, + COALESCE(sc.stop_min, (0)::numeric) AS stop_min, + COALESCE(sc.stop_cost_eur, (0)::numeric) AS stop_cost_eur, + CASE + WHEN (o.odoo_is_started_calc = true) THEN COALESCE(cc.real_cycles, (0)::numeric) + ELSE (0)::numeric + END AS real_cycles, + CASE + WHEN ((o.odoo_is_started_calc = true) AND (o.planned_cycles > (0)::numeric)) THEN round(((COALESCE(cc.real_cycles, (0)::numeric) / o.planned_cycles) * (100)::numeric), 2) + WHEN (o.odoo_is_started_calc = false) THEN (0)::numeric + ELSE NULL::numeric + END AS cycle_completion_pct, + CASE + WHEN (o.odoo_state = ANY (ARRAY['ready'::text, 'waiting'::text])) THEN 'ORDEN_NO_INICIADA'::text + WHEN ((o.odoo_is_started_calc = true) AND (fa.first_machine_activity IS NULL)) THEN 'ORDEN_SIN_ACTIVIDAD_REAL'::text + WHEN ((o.odoo_is_started_calc = true) AND (COALESCE(sc.stop_count, (0)::bigint) > 0)) THEN 'OK_CON_PAROS'::text + WHEN (o.odoo_is_started_calc = true) THEN 'OK'::text + ELSE 'SIN_EVALUAR'::text + END AS status_check, + CASE + WHEN (o.odoo_state = ANY (ARRAY['ready'::text, 'waiting'::text])) THEN 'Orden preparada o en espera. Todavía no debe imputar ciclos, paros ni retrasos.'::text + WHEN ((o.odoo_is_started_calc = true) AND (fa.first_machine_activity IS NULL)) THEN 'La orden está iniciada/cerrada en Odoo, pero no se detectó actividad real de máquina en su ventana.'::text + WHEN ((o.odoo_is_started_calc = true) AND (COALESCE(sc.stop_count, (0)::bigint) > 0)) THEN 'La orden tuvo actividad real y paros imputados durante su ventana.'::text + WHEN (o.odoo_is_started_calc = true) THEN 'La orden tuvo actividad real sin paros imputados.'::text + ELSE 'Orden sin evaluación suficiente.'::text + END AS descripcion + FROM (((orders o + LEFT JOIN first_activity fa ON ((fa.order_row_id = o.id))) + LEFT JOIN cycle_calc cc ON ((cc.order_row_id = o.id))) + LEFT JOIN stop_calc sc ON ((sc.order_row_id = o.id))) + ORDER BY COALESCE(o.odoo_end, o.odoo_start, o.snapshot_at) DESC, o.odoo_workorder_id DESC; + + +ALTER VIEW mv_reports_ucepsa_demo.odoo_workorders_reality_history OWNER TO mesavault; + +-- +-- Name: odoo_last_finished_order_demo; Type: VIEW; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +CREATE VIEW mv_reports_ucepsa_demo.odoo_last_finished_order_demo AS + SELECT id, + tenant, + site, + odoo_instance, + odoo_workorder_id, + odoo_production_id, + odoo_order_ref, + odoo_state, + odoo_workcenter_code, + odoo_workcenter_name, + machine_id, + asset, + product, + qty_planned, + qty_done, + uom, + planned_cycles, + target_cycle_rate_ppm, + odoo_start, + odoo_end, + snapshot_at, + odoo_is_started, + first_machine_activity, + start_delay_min, + stop_count, + stop_min, + stop_cost_eur, + real_cycles, + cycle_completion_pct, + status_check, + descripcion + FROM mv_reports_ucepsa_demo.odoo_workorders_reality_history + WHERE (odoo_state = ANY (ARRAY['done'::text, 'to_close'::text])) + ORDER BY COALESCE(odoo_end, snapshot_at) DESC + LIMIT 1; + + +ALTER VIEW mv_reports_ucepsa_demo.odoo_last_finished_order_demo OWNER TO mesavault; + +-- +-- Name: odoo_order_state_machine_grafana; Type: VIEW; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +CREATE VIEW mv_reports_ucepsa_demo.odoo_order_state_machine_grafana AS + SELECT COALESCE(m.machine_id, o.machine_id) AS machine_id, + o.received_at AS odoo_state_received_at, + o.mqtt_ts AS odoo_state_ts, + (EXTRACT(epoch FROM (now() - o.received_at)))::double precision AS order_age_s, + o.workcenter_id, + o.workcenter_name, + o.workcenter_display_name, + o.production_order AS order_ref, + o.product AS product_name, + o.odoo_state, + o.order_present, + o.order_coherent, + o.reason, + o.waiting_count, + o.ready_count, + o.progress_count, + m.ingest_ts AS telemetry_ingest_ts, + m.age_s AS telemetry_age_s, + m.comm_ok, + m.sample_valid, + m.machine_running, + m.cycle_rate_ppm, + m.cycle_pulse_count, + m.digital_inputs, + CASE + WHEN (o.machine_id IS NULL) THEN 'SIN_ESTADO_ODOO'::text + WHEN (EXTRACT(epoch FROM (now() - o.received_at)) > (30)::numeric) THEN 'ODOO_STATE_ANTIGUO'::text + WHEN (o.odoo_state = 'multiple_loaded_orders'::text) THEN 'MULTIPLES_ORDENES_EN_PROGRESS'::text + WHEN ((o.order_present = true) AND (o.order_coherent = true) AND (COALESCE(m.machine_running, false) = true)) THEN 'ORDEN_CARGADA_MAQUINA_EN_MARCHA'::text + WHEN ((o.order_present = true) AND (o.order_coherent = true) AND (COALESCE(m.machine_running, false) = false)) THEN 'ORDEN_CARGADA_MAQUINA_PARADA'::text + WHEN ((COALESCE(o.order_present, false) = false) AND (COALESCE(m.machine_running, false) = true)) THEN 'MAQUINA_EN_MARCHA_SIN_ORDEN_ODOO'::text + WHEN ((COALESCE(o.order_present, false) = false) AND (COALESCE(m.machine_running, false) = false)) THEN 'SIN_ORDEN_MAQUINA_PARADA'::text + ELSE 'REVISAR'::text + END AS edge_odoo_status, + o.payload_json AS odoo_payload_json + FROM (mv_reports_ucepsa_demo.machine_state_latest_multi m + FULL JOIN mv_hot.odoo_order_state_current o ON (((o.tenant = 'ucepsa'::text) AND (o.site = 'ucepsa_onpremise'::text) AND (o.machine_id = m.machine_id)))) + WHERE (COALESCE(m.site, o.site) = 'ucepsa_onpremise'::text); + + +ALTER VIEW mv_reports_ucepsa_demo.odoo_order_state_machine_grafana OWNER TO mesavault; + +-- +-- Name: stop_summary; Type: VIEW; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +CREATE VIEW mv_reports_ucepsa_demo.stop_summary AS + SELECT cause_name, + cause_code, + stop_count, + total_min, + total_cost_eur + FROM mv_hot.edge_oee_stop_summary_demo; + + +ALTER VIEW mv_reports_ucepsa_demo.stop_summary OWNER TO mesavault; + +-- +-- Name: current_order_margin_readiness_v1; Type: VIEW; Schema: mv_reports_ucepsa_prod; Owner: mesavault +-- + +CREATE VIEW mv_reports_ucepsa_prod.current_order_margin_readiness_v1 AS + WITH current_orders AS ( + SELECT odoo_order_state_current.tenant, + odoo_order_state_current.site, + odoo_order_state_current.machine_id, + odoo_order_state_current.workcenter_id, + odoo_order_state_current.workcenter_name, + odoo_order_state_current.order_present, + odoo_order_state_current.order_coherent, + odoo_order_state_current.odoo_state AS order_state_from_shopfloor, + odoo_order_state_current.workorder_id AS odoo_workorder_id, + odoo_order_state_current.production_order AS odoo_order_ref, + odoo_order_state_current.product AS order_state_product, + odoo_order_state_current.waiting_count, + odoo_order_state_current.ready_count, + odoo_order_state_current.progress_count, + odoo_order_state_current.mqtt_ts, + odoo_order_state_current.received_at, + odoo_order_state_current.payload_json, + (NULLIF((odoo_order_state_current.payload_json ->> 'production_id'::text), ''::text))::integer AS odoo_production_id, + (NULLIF((odoo_order_state_current.payload_json ->> 'product_id'::text), ''::text))::integer AS order_state_product_id, + (NULLIF((odoo_order_state_current.payload_json ->> 'qty_production'::text), ''::text))::numeric AS order_state_qty_production, + (odoo_order_state_current.payload_json ->> 'date_start'::text) AS order_state_date_start, + (odoo_order_state_current.payload_json ->> 'date_finished'::text) AS order_state_date_finished + FROM mv_hot.odoo_order_state_current + WHERE (odoo_order_state_current.site = 'ucepsa_onpremise'::text) + ), commercial AS ( + SELECT odoo_order_commercials.id, + odoo_order_commercials.tenant, + odoo_order_commercials.site, + odoo_order_commercials.odoo_instance, + odoo_order_commercials.odoo_production_id, + odoo_order_commercials.odoo_order_ref, + odoo_order_commercials.odoo_origin, + odoo_order_commercials.odoo_state, + odoo_order_commercials.product_id, + odoo_order_commercials.product_default_code, + odoo_order_commercials.product_name, + odoo_order_commercials.product_uom, + odoo_order_commercials.product_standard_price, + odoo_order_commercials.product_list_price, + odoo_order_commercials.production_qty, + odoo_order_commercials.qty_producing, + odoo_order_commercials.production_uom, + odoo_order_commercials.sale_line_id, + odoo_order_commercials.sale_order_id, + odoo_order_commercials.sale_order_ref, + odoo_order_commercials.customer_id, + odoo_order_commercials.customer_name, + odoo_order_commercials.sale_state, + odoo_order_commercials.sale_date_order, + odoo_order_commercials.currency, + odoo_order_commercials.sale_line_qty, + odoo_order_commercials.sale_line_qty_delivered, + odoo_order_commercials.sale_price_unit, + odoo_order_commercials.sale_discount_pct, + odoo_order_commercials.revenue_net_eur, + odoo_order_commercials.revenue_total_eur, + odoo_order_commercials.raw_material_value_eur, + odoo_order_commercials.finished_value_eur, + odoo_order_commercials.material_cost_status, + odoo_order_commercials.stock_raw_moves_json, + odoo_order_commercials.stock_finished_moves_json, + odoo_order_commercials.raw_json, + odoo_order_commercials.snapshot_at + FROM mv_edge_oee_ucepsa_prod.odoo_order_commercials + WHERE ((odoo_order_commercials.site = 'ucepsa_onpremise'::text) AND (odoo_order_commercials.odoo_instance = 'odoo_ucepsa_prod'::text)) + ), joined AS ( + SELECT co.tenant, + co.site, + co.machine_id, + co.workcenter_id, + co.workcenter_name, + co.order_present, + co.order_coherent, + co.order_state_from_shopfloor, + co.odoo_workorder_id, + co.odoo_production_id, + co.odoo_order_ref, + co.order_state_product, + co.order_state_product_id, + co.order_state_qty_production, + co.waiting_count, + co.ready_count, + co.progress_count, + co.mqtt_ts, + co.received_at, + c.odoo_instance, + c.odoo_state AS production_state, + c.odoo_origin, + c.product_id, + c.product_default_code, + COALESCE(c.product_name, co.order_state_product) AS product_name, + c.product_uom, + c.production_qty, + c.qty_producing, + c.production_uom, + c.sale_line_id, + c.sale_order_id, + c.sale_order_ref, + c.customer_id, + c.customer_name, + c.sale_state, + c.sale_date_order, + c.currency, + c.sale_line_qty, + c.sale_line_qty_delivered, + c.sale_price_unit, + c.sale_discount_pct, + c.revenue_net_eur, + c.revenue_total_eur, + c.raw_material_value_eur, + c.finished_value_eur, + c.material_cost_status, + jsonb_array_length(COALESCE(c.stock_raw_moves_json, '[]'::jsonb)) AS raw_material_line_count, + jsonb_array_length(COALESCE(c.stock_finished_moves_json, '[]'::jsonb)) AS finished_move_line_count, + c.stock_raw_moves_json, + c.stock_finished_moves_json, + c.snapshot_at AS commercial_snapshot_at + FROM (current_orders co + LEFT JOIN commercial c ON ((c.odoo_production_id = co.odoo_production_id))) + ) + SELECT tenant, + site, + machine_id, + workcenter_id, + workcenter_name, + order_present, + order_coherent, + order_state_from_shopfloor, + odoo_workorder_id, + odoo_production_id, + odoo_order_ref, + order_state_product, + order_state_product_id, + order_state_qty_production, + waiting_count, + ready_count, + progress_count, + mqtt_ts, + received_at, + odoo_instance, + production_state, + odoo_origin, + product_id, + product_default_code, + product_name, + product_uom, + production_qty, + qty_producing, + production_uom, + sale_line_id, + sale_order_id, + sale_order_ref, + customer_id, + customer_name, + sale_state, + sale_date_order, + currency, + sale_line_qty, + sale_line_qty_delivered, + sale_price_unit, + sale_discount_pct, + revenue_net_eur, + revenue_total_eur, + raw_material_value_eur, + finished_value_eur, + material_cost_status, + raw_material_line_count, + finished_move_line_count, + stock_raw_moves_json, + stock_finished_moves_json, + commercial_snapshot_at, + CASE + WHEN (order_present IS FALSE) THEN 'NO_ORDER_LOADED'::text + WHEN (order_coherent IS FALSE) THEN 'ODOO_ORDER_INCOHERENT'::text + WHEN (odoo_production_id IS NULL) THEN 'MISSING_PRODUCTION_ID_FROM_ORDER_STATE'::text + WHEN (odoo_instance IS NULL) THEN 'MISSING_COMMERCIAL_SNAPSHOT'::text + WHEN ((sale_order_ref IS NULL) OR (revenue_net_eur IS NULL)) THEN 'MISSING_REVENUE_LINK'::text + WHEN (customer_name IS NULL) THEN 'MISSING_CUSTOMER'::text + WHEN (raw_material_line_count = 0) THEN 'MISSING_RAW_MATERIALS'::text + WHEN (material_cost_status = 'UNAVAILABLE_OR_ZERO'::text) THEN 'MISSING_MATERIAL_COST'::text + ELSE 'READY_FOR_MARGIN_BASE'::text + END AS margin_readiness_status, + CASE + WHEN (order_present IS FALSE) THEN 'No hay orden cargada en Odoo Shop Floor para esta máquina.'::text + WHEN (order_coherent IS FALSE) THEN 'Hay estado Odoo incoherente para esta máquina.'::text + WHEN (odoo_production_id IS NULL) THEN 'El order_state no trae production_id; revisar publicador Odoo.'::text + WHEN (odoo_instance IS NULL) THEN 'La orden existe en order_state, pero aún no tiene snapshot comercial en Edge-OEE.'::text + WHEN ((sale_order_ref IS NULL) OR (revenue_net_eur IS NULL)) THEN 'Falta vínculo económico con pedido/línea de venta.'::text + WHEN (customer_name IS NULL) THEN 'Falta cliente asociado al pedido de venta.'::text + WHEN (raw_material_line_count = 0) THEN 'No se detectan movimientos de materia prima para la orden.'::text + WHEN (material_cost_status = 'UNAVAILABLE_OR_ZERO'::text) THEN 'Hay materia prima, pero Odoo no aporta valoración económica útil. Cargar coste €/kg o corregir valoración.'::text + ELSE 'Datos comerciales mínimos disponibles para calcular margen base.'::text + END AS margin_readiness_description, + (((((( + CASE + WHEN (order_present IS TRUE) THEN 20 + ELSE 0 + END + + CASE + WHEN (order_coherent IS TRUE) THEN 15 + ELSE 0 + END) + + CASE + WHEN (odoo_instance IS NOT NULL) THEN 15 + ELSE 0 + END) + + CASE + WHEN ((sale_order_ref IS NOT NULL) AND (revenue_net_eur IS NOT NULL)) THEN 20 + ELSE 0 + END) + + CASE + WHEN (customer_name IS NOT NULL) THEN 10 + ELSE 0 + END) + + CASE + WHEN (raw_material_line_count > 0) THEN 10 + ELSE 0 + END) + + CASE + WHEN (material_cost_status <> 'UNAVAILABLE_OR_ZERO'::text) THEN 10 + ELSE 0 + END) AS margin_readiness_score + FROM joined; + + +ALTER VIEW mv_reports_ucepsa_prod.current_order_margin_readiness_v1 OWNER TO mesavault; + +-- +-- Name: order_raw_material_costs_v1; Type: VIEW; Schema: mv_reports_ucepsa_prod; Owner: mesavault +-- + +CREATE VIEW mv_reports_ucepsa_prod.order_raw_material_costs_v1 AS + WITH raw_moves AS ( + SELECT c.id AS commercial_row_id, + c.tenant, + c.site, + c.odoo_instance, + c.odoo_production_id, + c.odoo_order_ref, + c.sale_order_ref, + c.customer_name, + c.product_default_code AS finished_product_default_code, + c.product_name AS finished_product_name, + (NULLIF((rm.raw_move ->> 'id'::text), ''::text))::integer AS raw_move_id, + (NULLIF(((rm.raw_move -> 'product_id'::text) ->> 0), ''::text))::integer AS raw_product_id, + ((rm.raw_move -> 'product_id'::text) ->> 1) AS raw_product_name, + "substring"(((rm.raw_move -> 'product_id'::text) ->> 1), '\[([^\]]+)\]'::text) AS raw_product_default_code, + (NULLIF((rm.raw_move ->> 'product_uom_qty'::text), ''::text))::numeric AS planned_qty, + (NULLIF((rm.raw_move ->> 'quantity'::text), ''::text))::numeric AS consumed_qty, + ((rm.raw_move -> 'product_uom'::text) ->> 1) AS raw_uom, + (NULLIF((rm.raw_move ->> 'price_unit'::text), ''::text))::numeric AS odoo_price_unit, + (NULLIF((rm.raw_move ->> 'value'::text), ''::text))::numeric AS odoo_value, + (rm.raw_move ->> 'state'::text) AS move_state, + rm.raw_move AS raw_move_json + FROM (mv_edge_oee_ucepsa_prod.odoo_order_commercials c + CROSS JOIN LATERAL jsonb_array_elements(c.stock_raw_moves_json) rm(raw_move)) + ), costed AS ( + SELECT r.commercial_row_id, + r.tenant, + r.site, + r.odoo_instance, + r.odoo_production_id, + r.odoo_order_ref, + r.sale_order_ref, + r.customer_name, + r.finished_product_default_code, + r.finished_product_name, + r.raw_move_id, + r.raw_product_id, + r.raw_product_name, + r.raw_product_default_code, + r.planned_qty, + r.consumed_qty, + r.raw_uom, + r.odoo_price_unit, + r.odoo_value, + r.move_state, + r.raw_move_json, + p.id AS raw_material_cost_parameter_id, + p.material_cost_eur_per_kg, + p.source AS raw_material_cost_source, + p.notes AS raw_material_cost_notes, + CASE + WHEN (COALESCE(r.odoo_value, (0)::numeric) <> (0)::numeric) THEN abs(r.odoo_value) + WHEN ((lower(COALESCE(r.raw_uom, ''::text)) = 'kg'::text) AND (p.material_cost_eur_per_kg IS NOT NULL) AND (r.consumed_qty IS NOT NULL)) THEN (r.consumed_qty * p.material_cost_eur_per_kg) + ELSE NULL::numeric + END AS material_cost_estimated_eur, + CASE + WHEN (COALESCE(r.odoo_value, (0)::numeric) <> (0)::numeric) THEN 'FROM_ODOO_STOCK_MOVE_VALUE'::text + WHEN (r.raw_product_id IS NULL) THEN 'MISSING_RAW_PRODUCT_ID'::text + WHEN (lower(COALESCE(r.raw_uom, ''::text)) <> 'kg'::text) THEN 'UNSUPPORTED_RAW_UOM'::text + WHEN (p.material_cost_eur_per_kg IS NULL) THEN 'MISSING_RAW_MATERIAL_COST_PARAMETER'::text + WHEN (r.consumed_qty IS NULL) THEN 'MISSING_CONSUMED_QTY'::text + ELSE 'FROM_RAW_MATERIAL_COST_PARAMETER'::text + END AS material_cost_status + FROM (raw_moves r + LEFT JOIN LATERAL ( SELECT p1.id, + p1.tenant, + p1.site, + p1.raw_product_default_code, + p1.raw_product_name, + p1.material_cost_eur_per_kg, + p1.source, + p1.valid_from, + p1.valid_to, + p1.is_active, + p1.notes, + p1.created_at, + p1.raw_product_id, + p1.raw_uom + FROM mv_edge_oee_ucepsa_prod.raw_material_cost_parameters p1 + WHERE ((p1.tenant = r.tenant) AND (p1.site = r.site) AND (p1.is_active = true) AND (p1.valid_from <= now()) AND ((p1.valid_to IS NULL) OR (p1.valid_to > now())) AND (((p1.raw_product_id IS NOT NULL) AND (p1.raw_product_id = r.raw_product_id)) OR ((p1.raw_product_id IS NULL) AND (p1.raw_product_default_code IS NOT NULL) AND (p1.raw_product_default_code = r.raw_product_default_code)) OR ((p1.raw_product_id IS NULL) AND (p1.raw_product_default_code IS NULL) AND (p1.raw_product_name IS NOT NULL) AND (p1.raw_product_name = r.raw_product_name)))) + ORDER BY + CASE + WHEN ((p1.raw_product_id IS NOT NULL) AND (p1.raw_product_id = r.raw_product_id)) THEN 1 + WHEN ((p1.raw_product_default_code IS NOT NULL) AND (p1.raw_product_default_code = r.raw_product_default_code)) THEN 2 + WHEN ((p1.raw_product_name IS NOT NULL) AND (p1.raw_product_name = r.raw_product_name)) THEN 3 + ELSE 9 + END, p1.valid_from DESC, p1.id DESC + LIMIT 1) p ON (true)) + ) + SELECT commercial_row_id, + tenant, + site, + odoo_instance, + odoo_production_id, + odoo_order_ref, + sale_order_ref, + customer_name, + finished_product_default_code, + finished_product_name, + raw_move_id, + raw_product_id, + raw_product_name, + raw_product_default_code, + planned_qty, + consumed_qty, + raw_uom, + odoo_price_unit, + odoo_value, + move_state, + raw_material_cost_parameter_id, + material_cost_eur_per_kg, + raw_material_cost_source, + raw_material_cost_notes, + material_cost_estimated_eur, + round(material_cost_estimated_eur, 4) AS material_cost_estimated_eur_rounded, + material_cost_status, + raw_move_json + FROM costed; + + +ALTER VIEW mv_reports_ucepsa_prod.order_raw_material_costs_v1 OWNER TO mesavault; + +-- +-- Name: order_raw_material_costs_summary_v1; Type: VIEW; Schema: mv_reports_ucepsa_prod; Owner: mesavault +-- + +CREATE VIEW mv_reports_ucepsa_prod.order_raw_material_costs_summary_v1 AS + SELECT tenant, + site, + odoo_instance, + odoo_production_id, + odoo_order_ref, + sale_order_ref, + customer_name, + finished_product_default_code, + finished_product_name, + count(*) AS raw_material_line_count, + round(sum(COALESCE(consumed_qty, (0)::numeric)), 3) AS total_consumed_qty, + string_agg(concat(COALESCE(raw_product_default_code, (raw_product_id)::text, 'sin_codigo'::text), ' - ', COALESCE(raw_product_name, 'sin_nombre'::text), ': ', COALESCE((consumed_qty)::text, 'sin_cantidad'::text), ' ', COALESCE(raw_uom, ''::text)), '; '::text ORDER BY raw_product_name) AS raw_materials_summary, + round(sum(COALESCE(material_cost_estimated_eur, (0)::numeric)), 4) AS material_cost_estimated_eur, + count(*) FILTER (WHERE (material_cost_estimated_eur IS NOT NULL)) AS costed_raw_material_line_count, + count(*) FILTER (WHERE ((material_cost_status <> 'FROM_RAW_MATERIAL_COST_PARAMETER'::text) AND (material_cost_status <> 'FROM_ODOO_STOCK_MOVE_VALUE'::text))) AS missing_or_invalid_raw_material_cost_count, + string_agg(DISTINCT material_cost_status, ', '::text ORDER BY material_cost_status) AS material_cost_status_summary + FROM mv_reports_ucepsa_prod.order_raw_material_costs_v1 + GROUP BY tenant, site, odoo_instance, odoo_production_id, odoo_order_ref, sale_order_ref, customer_name, finished_product_default_code, finished_product_name; + + +ALTER VIEW mv_reports_ucepsa_prod.order_raw_material_costs_summary_v1 OWNER TO mesavault; + +-- +-- Name: raw_material_cost_requirements_v1; Type: VIEW; Schema: mv_reports_ucepsa_prod; Owner: mesavault +-- + +CREATE VIEW mv_reports_ucepsa_prod.raw_material_cost_requirements_v1 AS + SELECT tenant, + site, + raw_product_id, + raw_product_default_code, + raw_product_name, + raw_uom, + count(*) AS affected_order_lines, + count(DISTINCT odoo_order_ref) AS affected_orders, + string_agg(DISTINCT odoo_order_ref, ', '::text ORDER BY odoo_order_ref) AS affected_order_refs, + round(sum(COALESCE(consumed_qty, (0)::numeric)), 3) AS total_consumed_qty, + string_agg(DISTINCT customer_name, ', '::text ORDER BY customer_name) AS affected_customers, + max(material_cost_status) AS current_status, + 'Cargar coste €/kg en mv_edge_oee_ucepsa_prod.raw_material_cost_parameters'::text AS recommended_action + FROM mv_reports_ucepsa_prod.order_raw_material_costs_v1 + WHERE (material_cost_status = ANY (ARRAY['MISSING_RAW_MATERIAL_COST_PARAMETER'::text, 'MISSING_RAW_PRODUCT_ID'::text, 'MISSING_CONSUMED_QTY'::text, 'UNSUPPORTED_RAW_UOM'::text])) + GROUP BY tenant, site, raw_product_id, raw_product_default_code, raw_product_name, raw_uom; + + +ALTER VIEW mv_reports_ucepsa_prod.raw_material_cost_requirements_v1 OWNER TO mesavault; + +-- +-- Name: data_quality_contract_rules id; Type: DEFAULT; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +ALTER TABLE ONLY mv_edge_oee_ucepsa_demo.data_quality_contract_rules ALTER COLUMN id SET DEFAULT nextval('mv_edge_oee_ucepsa_demo.data_quality_contract_rules_id_seq'::regclass); + + +-- +-- Name: margin_cost_parameters id; Type: DEFAULT; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +ALTER TABLE ONLY mv_edge_oee_ucepsa_demo.margin_cost_parameters ALTER COLUMN id SET DEFAULT nextval('mv_edge_oee_ucepsa_demo.margin_cost_parameters_id_seq'::regclass); + + +-- +-- Name: odoo_order_commercials id; Type: DEFAULT; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +ALTER TABLE ONLY mv_edge_oee_ucepsa_demo.odoo_order_commercials ALTER COLUMN id SET DEFAULT nextval('mv_edge_oee_ucepsa_demo.odoo_order_commercials_id_seq'::regclass); + + +-- +-- Name: odoo_workcenter_map id; Type: DEFAULT; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +ALTER TABLE ONLY mv_edge_oee_ucepsa_demo.odoo_workcenter_map ALTER COLUMN id SET DEFAULT nextval('mv_edge_oee_ucepsa_demo.odoo_workcenter_map_id_seq'::regclass); + + +-- +-- Name: odoo_workorders_demo id; Type: DEFAULT; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +ALTER TABLE ONLY mv_edge_oee_ucepsa_demo.odoo_workorders_demo ALTER COLUMN id SET DEFAULT nextval('mv_edge_oee_ucepsa_demo.odoo_workorders_demo_id_seq'::regclass); + + +-- +-- Name: raw_material_cost_parameters id; Type: DEFAULT; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +ALTER TABLE ONLY mv_edge_oee_ucepsa_demo.raw_material_cost_parameters ALTER COLUMN id SET DEFAULT nextval('mv_edge_oee_ucepsa_demo.raw_material_cost_parameters_id_seq'::regclass); + + +-- +-- Name: data_quality_contract_rules id; Type: DEFAULT; Schema: mv_edge_oee_ucepsa_prod; Owner: mesavault +-- + +ALTER TABLE ONLY mv_edge_oee_ucepsa_prod.data_quality_contract_rules ALTER COLUMN id SET DEFAULT nextval('mv_edge_oee_ucepsa_prod.data_quality_contract_rules_id_seq'::regclass); + + +-- +-- Name: margin_cost_parameters id; Type: DEFAULT; Schema: mv_edge_oee_ucepsa_prod; Owner: mesavault +-- + +ALTER TABLE ONLY mv_edge_oee_ucepsa_prod.margin_cost_parameters ALTER COLUMN id SET DEFAULT nextval('mv_edge_oee_ucepsa_prod.margin_cost_parameters_id_seq'::regclass); + + +-- +-- Name: odoo_order_commercials id; Type: DEFAULT; Schema: mv_edge_oee_ucepsa_prod; Owner: mesavault +-- + +ALTER TABLE ONLY mv_edge_oee_ucepsa_prod.odoo_order_commercials ALTER COLUMN id SET DEFAULT nextval('mv_edge_oee_ucepsa_prod.odoo_order_commercials_id_seq'::regclass); + + +-- +-- Name: raw_material_cost_parameters id; Type: DEFAULT; Schema: mv_edge_oee_ucepsa_prod; Owner: mesavault +-- + +ALTER TABLE ONLY mv_edge_oee_ucepsa_prod.raw_material_cost_parameters ALTER COLUMN id SET DEFAULT nextval('mv_edge_oee_ucepsa_prod.raw_material_cost_parameters_id_seq'::regclass); + + +-- +-- Name: edge_oee_eastron_readings id; Type: DEFAULT; Schema: mv_hot; Owner: mesavault +-- + +ALTER TABLE ONLY mv_hot.edge_oee_eastron_readings ALTER COLUMN id SET DEFAULT nextval('mv_hot.edge_oee_eastron_readings_id_seq'::regclass); + + +-- +-- Name: edge_oee_events_demo event_id; Type: DEFAULT; Schema: mv_hot; Owner: mesavault +-- + +ALTER TABLE ONLY mv_hot.edge_oee_events_demo ALTER COLUMN event_id SET DEFAULT nextval('mv_hot.edge_oee_events_demo_event_id_seq'::regclass); + + +-- +-- Name: edge_oee_machine_stops id; Type: DEFAULT; Schema: mv_hot; Owner: mesavault +-- + +ALTER TABLE ONLY mv_hot.edge_oee_machine_stops ALTER COLUMN id SET DEFAULT nextval('mv_hot.edge_oee_machine_stops_id_seq'::regclass); + + +-- +-- Name: edge_oee_node_events id; Type: DEFAULT; Schema: mv_hot; Owner: mesavault +-- + +ALTER TABLE ONLY mv_hot.edge_oee_node_events ALTER COLUMN id SET DEFAULT nextval('mv_hot.edge_oee_node_events_id_seq'::regclass); + + +-- +-- Name: odoo_order_state_events id; Type: DEFAULT; Schema: mv_hot; Owner: mesavault +-- + +ALTER TABLE ONLY mv_hot.odoo_order_state_events ALTER COLUMN id SET DEFAULT nextval('mv_hot.odoo_order_state_events_id_seq'::regclass); + + +-- +-- Name: data_quality_contract_rules data_quality_contract_rules_pkey; Type: CONSTRAINT; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +ALTER TABLE ONLY mv_edge_oee_ucepsa_demo.data_quality_contract_rules + ADD CONSTRAINT data_quality_contract_rules_pkey PRIMARY KEY (id); + + +-- +-- Name: margin_cost_parameters margin_cost_parameters_pkey; Type: CONSTRAINT; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +ALTER TABLE ONLY mv_edge_oee_ucepsa_demo.margin_cost_parameters + ADD CONSTRAINT margin_cost_parameters_pkey PRIMARY KEY (id); + + +-- +-- Name: odoo_order_commercials odoo_order_commercials_pkey; Type: CONSTRAINT; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +ALTER TABLE ONLY mv_edge_oee_ucepsa_demo.odoo_order_commercials + ADD CONSTRAINT odoo_order_commercials_pkey PRIMARY KEY (id); + + +-- +-- Name: odoo_workcenter_map odoo_workcenter_map_pkey; Type: CONSTRAINT; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +ALTER TABLE ONLY mv_edge_oee_ucepsa_demo.odoo_workcenter_map + ADD CONSTRAINT odoo_workcenter_map_pkey PRIMARY KEY (id); + + +-- +-- Name: odoo_workcenter_map odoo_workcenter_map_tenant_site_machine_id_key; Type: CONSTRAINT; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +ALTER TABLE ONLY mv_edge_oee_ucepsa_demo.odoo_workcenter_map + ADD CONSTRAINT odoo_workcenter_map_tenant_site_machine_id_key UNIQUE (tenant, site, machine_id); + + +-- +-- Name: odoo_workcenter_map odoo_workcenter_map_tenant_site_odoo_instance_odoo_workcent_key; Type: CONSTRAINT; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +ALTER TABLE ONLY mv_edge_oee_ucepsa_demo.odoo_workcenter_map + ADD CONSTRAINT odoo_workcenter_map_tenant_site_odoo_instance_odoo_workcent_key UNIQUE (tenant, site, odoo_instance, odoo_workcenter_code); + + +-- +-- Name: odoo_workorders_demo odoo_workorders_demo_pkey; Type: CONSTRAINT; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +ALTER TABLE ONLY mv_edge_oee_ucepsa_demo.odoo_workorders_demo + ADD CONSTRAINT odoo_workorders_demo_pkey PRIMARY KEY (id); + + +-- +-- Name: raw_material_cost_parameters raw_material_cost_parameters_pkey; Type: CONSTRAINT; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +ALTER TABLE ONLY mv_edge_oee_ucepsa_demo.raw_material_cost_parameters + ADD CONSTRAINT raw_material_cost_parameters_pkey PRIMARY KEY (id); + + +-- +-- Name: data_quality_contract_rules ux_data_quality_contract_rules_code; Type: CONSTRAINT; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +ALTER TABLE ONLY mv_edge_oee_ucepsa_demo.data_quality_contract_rules + ADD CONSTRAINT ux_data_quality_contract_rules_code UNIQUE (tenant, site, rule_code); + + +-- +-- Name: odoo_order_commercials ux_odoo_order_commercials_instance_production; Type: CONSTRAINT; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +ALTER TABLE ONLY mv_edge_oee_ucepsa_demo.odoo_order_commercials + ADD CONSTRAINT ux_odoo_order_commercials_instance_production UNIQUE (odoo_instance, odoo_production_id); + + +-- +-- Name: data_quality_contract_rules data_quality_contract_rules_pkey; Type: CONSTRAINT; Schema: mv_edge_oee_ucepsa_prod; Owner: mesavault +-- + +ALTER TABLE ONLY mv_edge_oee_ucepsa_prod.data_quality_contract_rules + ADD CONSTRAINT data_quality_contract_rules_pkey PRIMARY KEY (id); + + +-- +-- Name: data_quality_contract_rules data_quality_contract_rules_tenant_site_rule_code_key; Type: CONSTRAINT; Schema: mv_edge_oee_ucepsa_prod; Owner: mesavault +-- + +ALTER TABLE ONLY mv_edge_oee_ucepsa_prod.data_quality_contract_rules + ADD CONSTRAINT data_quality_contract_rules_tenant_site_rule_code_key UNIQUE (tenant, site, rule_code); + + +-- +-- Name: margin_cost_parameters margin_cost_parameters_pkey; Type: CONSTRAINT; Schema: mv_edge_oee_ucepsa_prod; Owner: mesavault +-- + +ALTER TABLE ONLY mv_edge_oee_ucepsa_prod.margin_cost_parameters + ADD CONSTRAINT margin_cost_parameters_pkey PRIMARY KEY (id); + + +-- +-- Name: odoo_order_commercials odoo_order_commercials_odoo_instance_odoo_production_id_key; Type: CONSTRAINT; Schema: mv_edge_oee_ucepsa_prod; Owner: mesavault +-- + +ALTER TABLE ONLY mv_edge_oee_ucepsa_prod.odoo_order_commercials + ADD CONSTRAINT odoo_order_commercials_odoo_instance_odoo_production_id_key UNIQUE (odoo_instance, odoo_production_id); + + +-- +-- Name: odoo_order_commercials odoo_order_commercials_pkey; Type: CONSTRAINT; Schema: mv_edge_oee_ucepsa_prod; Owner: mesavault +-- + +ALTER TABLE ONLY mv_edge_oee_ucepsa_prod.odoo_order_commercials + ADD CONSTRAINT odoo_order_commercials_pkey PRIMARY KEY (id); + + +-- +-- Name: raw_material_cost_parameters raw_material_cost_parameters_pkey; Type: CONSTRAINT; Schema: mv_edge_oee_ucepsa_prod; Owner: mesavault +-- + +ALTER TABLE ONLY mv_edge_oee_ucepsa_prod.raw_material_cost_parameters + ADD CONSTRAINT raw_material_cost_parameters_pkey PRIMARY KEY (id); + + +-- +-- Name: edge_oee_data_quality_demo edge_oee_data_quality_demo_pkey; Type: CONSTRAINT; Schema: mv_hot; Owner: mesavault +-- + +ALTER TABLE ONLY mv_hot.edge_oee_data_quality_demo + ADD CONSTRAINT edge_oee_data_quality_demo_pkey PRIMARY KEY (metric_id); + + +-- +-- Name: edge_oee_eastron_readings edge_oee_eastron_readings_pkey; Type: CONSTRAINT; Schema: mv_hot; Owner: mesavault +-- + +ALTER TABLE ONLY mv_hot.edge_oee_eastron_readings + ADD CONSTRAINT edge_oee_eastron_readings_pkey PRIMARY KEY (id); + + +-- +-- Name: edge_oee_events_demo edge_oee_events_demo_pkey; Type: CONSTRAINT; Schema: mv_hot; Owner: mesavault +-- + +ALTER TABLE ONLY mv_hot.edge_oee_events_demo + ADD CONSTRAINT edge_oee_events_demo_pkey PRIMARY KEY (event_id); + + +-- +-- Name: edge_oee_loss_summary_demo edge_oee_loss_summary_demo_pkey; Type: CONSTRAINT; Schema: mv_hot; Owner: mesavault +-- + +ALTER TABLE ONLY mv_hot.edge_oee_loss_summary_demo + ADD CONSTRAINT edge_oee_loss_summary_demo_pkey PRIMARY KEY (cause); + + +-- +-- Name: edge_oee_machine_stops edge_oee_machine_stops_pkey; Type: CONSTRAINT; Schema: mv_hot; Owner: mesavault +-- + +ALTER TABLE ONLY mv_hot.edge_oee_machine_stops + ADD CONSTRAINT edge_oee_machine_stops_pkey PRIMARY KEY (id); + + +-- +-- Name: edge_oee_machine_targets edge_oee_machine_targets_pkey; Type: CONSTRAINT; Schema: mv_hot; Owner: mesavault +-- + +ALTER TABLE ONLY mv_hot.edge_oee_machine_targets + ADD CONSTRAINT edge_oee_machine_targets_pkey PRIMARY KEY (machine_id); + + +-- +-- Name: edge_oee_machines edge_oee_machines_pkey; Type: CONSTRAINT; Schema: mv_hot; Owner: mesavault +-- + +ALTER TABLE ONLY mv_hot.edge_oee_machines + ADD CONSTRAINT edge_oee_machines_pkey PRIMARY KEY (machine_id); + + +-- +-- Name: edge_oee_node_events edge_oee_node_events_pkey; Type: CONSTRAINT; Schema: mv_hot; Owner: mesavault +-- + +ALTER TABLE ONLY mv_hot.edge_oee_node_events + ADD CONSTRAINT edge_oee_node_events_pkey PRIMARY KEY (id); + + +-- +-- Name: edge_oee_orders_demo edge_oee_orders_demo_pkey; Type: CONSTRAINT; Schema: mv_hot; Owner: mesavault +-- + +ALTER TABLE ONLY mv_hot.edge_oee_orders_demo + ADD CONSTRAINT edge_oee_orders_demo_pkey PRIMARY KEY (order_id); + + +-- +-- Name: edge_oee_quality_cases_demo edge_oee_quality_cases_demo_pkey; Type: CONSTRAINT; Schema: mv_hot; Owner: mesavault +-- + +ALTER TABLE ONLY mv_hot.edge_oee_quality_cases_demo + ADD CONSTRAINT edge_oee_quality_cases_demo_pkey PRIMARY KEY (case_id); + + +-- +-- Name: edge_oee_stop_causes edge_oee_stop_causes_pkey; Type: CONSTRAINT; Schema: mv_hot; Owner: mesavault +-- + +ALTER TABLE ONLY mv_hot.edge_oee_stop_causes + ADD CONSTRAINT edge_oee_stop_causes_pkey PRIMARY KEY (cause_code); + + +-- +-- Name: odoo_order_state_current odoo_order_state_current_pkey; Type: CONSTRAINT; Schema: mv_hot; Owner: mesavault +-- + +ALTER TABLE ONLY mv_hot.odoo_order_state_current + ADD CONSTRAINT odoo_order_state_current_pkey PRIMARY KEY (tenant, site, machine_id); + + +-- +-- Name: odoo_order_state_events odoo_order_state_events_pkey; Type: CONSTRAINT; Schema: mv_hot; Owner: mesavault +-- + +ALTER TABLE ONLY mv_hot.odoo_order_state_events + ADD CONSTRAINT odoo_order_state_events_pkey PRIMARY KEY (id); + + +-- +-- Name: idx_data_quality_contract_rules_active; Type: INDEX; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +CREATE INDEX idx_data_quality_contract_rules_active ON mv_edge_oee_ucepsa_demo.data_quality_contract_rules USING btree (tenant, site, is_active, domain, severity); + + +-- +-- Name: idx_margin_cost_parameters_scope; Type: INDEX; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +CREATE INDEX idx_margin_cost_parameters_scope ON mv_edge_oee_ucepsa_demo.margin_cost_parameters USING btree (tenant, site, is_active, machine_id, product_default_code); + + +-- +-- Name: idx_odoo_order_commercials_customer; Type: INDEX; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +CREATE INDEX idx_odoo_order_commercials_customer ON mv_edge_oee_ucepsa_demo.odoo_order_commercials USING btree (customer_name); + + +-- +-- Name: idx_odoo_order_commercials_order_ref; Type: INDEX; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +CREATE INDEX idx_odoo_order_commercials_order_ref ON mv_edge_oee_ucepsa_demo.odoo_order_commercials USING btree (odoo_order_ref); + + +-- +-- Name: idx_odoo_order_commercials_sale_order_ref; Type: INDEX; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +CREATE INDEX idx_odoo_order_commercials_sale_order_ref ON mv_edge_oee_ucepsa_demo.odoo_order_commercials USING btree (sale_order_ref); + + +-- +-- Name: idx_odoo_workorders_demo_machine_time; Type: INDEX; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +CREATE INDEX idx_odoo_workorders_demo_machine_time ON mv_edge_oee_ucepsa_demo.odoo_workorders_demo USING btree (machine_id, odoo_start DESC); + + +-- +-- Name: idx_odoo_workorders_demo_state; Type: INDEX; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +CREATE INDEX idx_odoo_workorders_demo_state ON mv_edge_oee_ucepsa_demo.odoo_workorders_demo USING btree (odoo_state); + + +-- +-- Name: idx_raw_material_cost_parameters_lookup; Type: INDEX; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +CREATE INDEX idx_raw_material_cost_parameters_lookup ON mv_edge_oee_ucepsa_demo.raw_material_cost_parameters USING btree (tenant, site, raw_product_default_code, is_active, valid_from DESC); + + +-- +-- Name: ux_odoo_workcenter_map_instance_workcenter; Type: INDEX; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +CREATE UNIQUE INDEX ux_odoo_workcenter_map_instance_workcenter ON mv_edge_oee_ucepsa_demo.odoo_workcenter_map USING btree (odoo_instance, odoo_workcenter_id); + + +-- +-- Name: ux_odoo_workcenter_map_tenant_site_instance_workcenter; Type: INDEX; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +CREATE UNIQUE INDEX ux_odoo_workcenter_map_tenant_site_instance_workcenter ON mv_edge_oee_ucepsa_demo.odoo_workcenter_map USING btree (tenant, site, odoo_instance, odoo_workcenter_id); + + +-- +-- Name: ux_odoo_workorders_demo_instance_workorder; Type: INDEX; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +CREATE UNIQUE INDEX ux_odoo_workorders_demo_instance_workorder ON mv_edge_oee_ucepsa_demo.odoo_workorders_demo USING btree (odoo_instance, odoo_workorder_id); + + +-- +-- Name: ux_odoo_workorders_demo_tenant_site_instance_workorder; Type: INDEX; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +CREATE UNIQUE INDEX ux_odoo_workorders_demo_tenant_site_instance_workorder ON mv_edge_oee_ucepsa_demo.odoo_workorders_demo USING btree (tenant, site, odoo_instance, odoo_workorder_id); + + +-- +-- Name: data_quality_contract_rules_tenant_site_is_active_domain_se_idx; Type: INDEX; Schema: mv_edge_oee_ucepsa_prod; Owner: mesavault +-- + +CREATE INDEX data_quality_contract_rules_tenant_site_is_active_domain_se_idx ON mv_edge_oee_ucepsa_prod.data_quality_contract_rules USING btree (tenant, site, is_active, domain, severity); + + +-- +-- Name: idx_raw_material_cost_params_prod_code; Type: INDEX; Schema: mv_edge_oee_ucepsa_prod; Owner: mesavault +-- + +CREATE INDEX idx_raw_material_cost_params_prod_code ON mv_edge_oee_ucepsa_prod.raw_material_cost_parameters USING btree (raw_product_default_code); + + +-- +-- Name: idx_raw_material_cost_params_prod_name; Type: INDEX; Schema: mv_edge_oee_ucepsa_prod; Owner: mesavault +-- + +CREATE INDEX idx_raw_material_cost_params_prod_name ON mv_edge_oee_ucepsa_prod.raw_material_cost_parameters USING btree (raw_product_name); + + +-- +-- Name: idx_raw_material_cost_params_prod_product_id; Type: INDEX; Schema: mv_edge_oee_ucepsa_prod; Owner: mesavault +-- + +CREATE INDEX idx_raw_material_cost_params_prod_product_id ON mv_edge_oee_ucepsa_prod.raw_material_cost_parameters USING btree (raw_product_id); + + +-- +-- Name: margin_cost_parameters_tenant_site_is_active_machine_id_pro_idx; Type: INDEX; Schema: mv_edge_oee_ucepsa_prod; Owner: mesavault +-- + +CREATE INDEX margin_cost_parameters_tenant_site_is_active_machine_id_pro_idx ON mv_edge_oee_ucepsa_prod.margin_cost_parameters USING btree (tenant, site, is_active, machine_id, product_default_code); + + +-- +-- Name: odoo_order_commercials_customer_name_idx; Type: INDEX; Schema: mv_edge_oee_ucepsa_prod; Owner: mesavault +-- + +CREATE INDEX odoo_order_commercials_customer_name_idx ON mv_edge_oee_ucepsa_prod.odoo_order_commercials USING btree (customer_name); + + +-- +-- Name: odoo_order_commercials_odoo_order_ref_idx; Type: INDEX; Schema: mv_edge_oee_ucepsa_prod; Owner: mesavault +-- + +CREATE INDEX odoo_order_commercials_odoo_order_ref_idx ON mv_edge_oee_ucepsa_prod.odoo_order_commercials USING btree (odoo_order_ref); + + +-- +-- Name: odoo_order_commercials_sale_order_ref_idx; Type: INDEX; Schema: mv_edge_oee_ucepsa_prod; Owner: mesavault +-- + +CREATE INDEX odoo_order_commercials_sale_order_ref_idx ON mv_edge_oee_ucepsa_prod.odoo_order_commercials USING btree (sale_order_ref); + + +-- +-- Name: raw_material_cost_parameters_tenant_site_raw_product_defaul_idx; Type: INDEX; Schema: mv_edge_oee_ucepsa_prod; Owner: mesavault +-- + +CREATE INDEX raw_material_cost_parameters_tenant_site_raw_product_defaul_idx ON mv_edge_oee_ucepsa_prod.raw_material_cost_parameters USING btree (tenant, site, raw_product_default_code, is_active, valid_from DESC); + + +-- +-- Name: idx_edge_oee_eastron_readings_asset_ts; Type: INDEX; Schema: mv_hot; Owner: mesavault +-- + +CREATE INDEX idx_edge_oee_eastron_readings_asset_ts ON mv_hot.edge_oee_eastron_readings USING btree (asset, ts DESC); + + +-- +-- Name: idx_edge_oee_eastron_readings_ts; Type: INDEX; Schema: mv_hot; Owner: mesavault +-- + +CREATE INDEX idx_edge_oee_eastron_readings_ts ON mv_hot.edge_oee_eastron_readings USING btree (ts DESC); + + +-- +-- Name: idx_edge_oee_energy_demo_time; Type: INDEX; Schema: mv_hot; Owner: mesavault +-- + +CREATE INDEX idx_edge_oee_energy_demo_time ON mv_hot.edge_oee_energy_demo USING btree (ts DESC); + + +-- +-- Name: idx_edge_oee_events_demo_machine_time; Type: INDEX; Schema: mv_hot; Owner: mesavault +-- + +CREATE INDEX idx_edge_oee_events_demo_machine_time ON mv_hot.edge_oee_events_demo USING btree (machine_id, start_time DESC); + + +-- +-- Name: idx_edge_oee_events_demo_time; Type: INDEX; Schema: mv_hot; Owner: mesavault +-- + +CREATE INDEX idx_edge_oee_events_demo_time ON mv_hot.edge_oee_events_demo USING btree (start_time DESC); + + +-- +-- Name: idx_edge_oee_machine_stops_asset_status; Type: INDEX; Schema: mv_hot; Owner: mesavault +-- + +CREATE INDEX idx_edge_oee_machine_stops_asset_status ON mv_hot.edge_oee_machine_stops USING btree (asset, status); + + +-- +-- Name: idx_edge_oee_machine_stops_machine_started; Type: INDEX; Schema: mv_hot; Owner: mesavault +-- + +CREATE INDEX idx_edge_oee_machine_stops_machine_started ON mv_hot.edge_oee_machine_stops USING btree (machine_id, started_at DESC); + + +-- +-- Name: idx_edge_oee_machine_stops_started_at; Type: INDEX; Schema: mv_hot; Owner: mesavault +-- + +CREATE INDEX idx_edge_oee_machine_stops_started_at ON mv_hot.edge_oee_machine_stops USING btree (started_at DESC); + + +-- +-- Name: idx_edge_oee_node_events_asset_ts; Type: INDEX; Schema: mv_hot; Owner: mesavault +-- + +CREATE INDEX idx_edge_oee_node_events_asset_ts ON mv_hot.edge_oee_node_events USING btree (asset, event_ts DESC); + + +-- +-- Name: idx_edge_oee_node_events_event_ts; Type: INDEX; Schema: mv_hot; Owner: mesavault +-- + +CREATE INDEX idx_edge_oee_node_events_event_ts ON mv_hot.edge_oee_node_events USING btree (event_ts DESC); + + +-- +-- Name: idx_edge_oee_node_events_machine_ts; Type: INDEX; Schema: mv_hot; Owner: mesavault +-- + +CREATE INDEX idx_edge_oee_node_events_machine_ts ON mv_hot.edge_oee_node_events USING btree (machine_id, event_ts DESC); + + +-- +-- Name: idx_edge_oee_node_events_received_at; Type: INDEX; Schema: mv_hot; Owner: mesavault +-- + +CREATE INDEX idx_edge_oee_node_events_received_at ON mv_hot.edge_oee_node_events USING btree (received_at DESC); + + +-- +-- Name: ux_odoo_order_state_events_state_hash; Type: INDEX; Schema: mv_hot; Owner: mesavault +-- + +CREATE UNIQUE INDEX ux_odoo_order_state_events_state_hash ON mv_hot.odoo_order_state_events USING btree (tenant, site, machine_id, payload_hash); + + +-- +-- Name: edge_oee_energy_demo edge_oee_energy_demo_machine_id_fkey; Type: FK CONSTRAINT; Schema: mv_hot; Owner: mesavault +-- + +ALTER TABLE ONLY mv_hot.edge_oee_energy_demo + ADD CONSTRAINT edge_oee_energy_demo_machine_id_fkey FOREIGN KEY (machine_id) REFERENCES mv_hot.edge_oee_machines(machine_id); + + +-- +-- Name: edge_oee_energy_demo edge_oee_energy_demo_order_id_fkey; Type: FK CONSTRAINT; Schema: mv_hot; Owner: mesavault +-- + +ALTER TABLE ONLY mv_hot.edge_oee_energy_demo + ADD CONSTRAINT edge_oee_energy_demo_order_id_fkey FOREIGN KEY (order_id) REFERENCES mv_hot.edge_oee_orders_demo(order_id); + + +-- +-- Name: edge_oee_events_demo edge_oee_events_demo_machine_id_fkey; Type: FK CONSTRAINT; Schema: mv_hot; Owner: mesavault +-- + +ALTER TABLE ONLY mv_hot.edge_oee_events_demo + ADD CONSTRAINT edge_oee_events_demo_machine_id_fkey FOREIGN KEY (machine_id) REFERENCES mv_hot.edge_oee_machines(machine_id); + + +-- +-- Name: edge_oee_events_demo edge_oee_events_demo_order_id_fkey; Type: FK CONSTRAINT; Schema: mv_hot; Owner: mesavault +-- + +ALTER TABLE ONLY mv_hot.edge_oee_events_demo + ADD CONSTRAINT edge_oee_events_demo_order_id_fkey FOREIGN KEY (order_id) REFERENCES mv_hot.edge_oee_orders_demo(order_id); + + +-- +-- Name: edge_oee_machine_stops edge_oee_machine_stops_cause_code_fkey; Type: FK CONSTRAINT; Schema: mv_hot; Owner: mesavault +-- + +ALTER TABLE ONLY mv_hot.edge_oee_machine_stops + ADD CONSTRAINT edge_oee_machine_stops_cause_code_fkey FOREIGN KEY (cause_code) REFERENCES mv_hot.edge_oee_stop_causes(cause_code); + + +-- +-- Name: edge_oee_orders_demo edge_oee_orders_demo_machine_id_fkey; Type: FK CONSTRAINT; Schema: mv_hot; Owner: mesavault +-- + +ALTER TABLE ONLY mv_hot.edge_oee_orders_demo + ADD CONSTRAINT edge_oee_orders_demo_machine_id_fkey FOREIGN KEY (machine_id) REFERENCES mv_hot.edge_oee_machines(machine_id); + + +-- +-- Name: edge_oee_quality_cases_demo edge_oee_quality_cases_demo_machine_id_fkey; Type: FK CONSTRAINT; Schema: mv_hot; Owner: mesavault +-- + +ALTER TABLE ONLY mv_hot.edge_oee_quality_cases_demo + ADD CONSTRAINT edge_oee_quality_cases_demo_machine_id_fkey FOREIGN KEY (machine_id) REFERENCES mv_hot.edge_oee_machines(machine_id); + + +-- +-- Name: edge_oee_quality_cases_demo edge_oee_quality_cases_demo_order_id_fkey; Type: FK CONSTRAINT; Schema: mv_hot; Owner: mesavault +-- + +ALTER TABLE ONLY mv_hot.edge_oee_quality_cases_demo + ADD CONSTRAINT edge_oee_quality_cases_demo_order_id_fkey FOREIGN KEY (order_id) REFERENCES mv_hot.edge_oee_orders_demo(order_id); + + +-- +-- Name: SCHEMA mv_edge_oee_ucepsa_demo; Type: ACL; Schema: -; Owner: mesavault +-- + +GRANT USAGE ON SCHEMA mv_edge_oee_ucepsa_demo TO grafana_ucepsa_ro; +GRANT USAGE ON SCHEMA mv_edge_oee_ucepsa_demo TO edge_oee_odoo_sync_writer; +GRANT USAGE ON SCHEMA mv_edge_oee_ucepsa_demo TO edge_oee_operator_writer; + + +-- +-- Name: SCHEMA mv_edge_oee_ucepsa_prod; Type: ACL; Schema: -; Owner: mesavault +-- + +GRANT USAGE ON SCHEMA mv_edge_oee_ucepsa_prod TO grafana_ucepsa_ro; +GRANT USAGE ON SCHEMA mv_edge_oee_ucepsa_prod TO edge_oee_odoo_sync_writer; + + +-- +-- Name: SCHEMA mv_hot; Type: ACL; Schema: -; Owner: mesavault +-- + +GRANT USAGE ON SCHEMA mv_hot TO grafana_ucepsa_ro; +GRANT USAGE ON SCHEMA mv_hot TO edge_oee_odoo_sync_writer; +GRANT USAGE ON SCHEMA mv_hot TO edge_oee_operator_writer; + + +-- +-- Name: SCHEMA mv_reports_ucepsa_demo; Type: ACL; Schema: -; Owner: mesavault +-- + +GRANT USAGE ON SCHEMA mv_reports_ucepsa_demo TO grafana_ucepsa_ro; +GRANT USAGE ON SCHEMA mv_reports_ucepsa_demo TO edge_oee_odoo_sync_writer; +GRANT USAGE ON SCHEMA mv_reports_ucepsa_demo TO edge_oee_operator_writer; + + +-- +-- Name: SCHEMA mv_reports_ucepsa_prod; Type: ACL; Schema: -; Owner: mesavault +-- + +GRANT USAGE ON SCHEMA mv_reports_ucepsa_prod TO grafana_ucepsa_ro; + + +-- +-- Name: FUNCTION oee_kpis(p_from timestamp with time zone, p_to timestamp with time zone, p_machine_id text, p_asset text); Type: ACL; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +GRANT ALL ON FUNCTION mv_reports_ucepsa_demo.oee_kpis(p_from timestamp with time zone, p_to timestamp with time zone, p_machine_id text, p_asset text) TO grafana_ucepsa_ro; + + +-- +-- Name: FUNCTION oee_operational_data(p_from timestamp with time zone, p_to timestamp with time zone, p_machine_id text, p_asset text); Type: ACL; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +GRANT ALL ON FUNCTION mv_reports_ucepsa_demo.oee_operational_data(p_from timestamp with time zone, p_to timestamp with time zone, p_machine_id text, p_asset text) TO grafana_ucepsa_ro; + + +-- +-- Name: TABLE data_quality_contract_rules; Type: ACL; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_edge_oee_ucepsa_demo.data_quality_contract_rules TO grafana_ucepsa_ro; +GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE mv_edge_oee_ucepsa_demo.data_quality_contract_rules TO edge_oee_odoo_sync_writer; + + +-- +-- Name: SEQUENCE data_quality_contract_rules_id_seq; Type: ACL; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +GRANT ALL ON SEQUENCE mv_edge_oee_ucepsa_demo.data_quality_contract_rules_id_seq TO edge_oee_odoo_sync_writer; + + +-- +-- Name: TABLE edge_oee_eastron_readings; Type: ACL; Schema: mv_hot; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_hot.edge_oee_eastron_readings TO grafana_ucepsa_ro; +GRANT SELECT ON TABLE mv_hot.edge_oee_eastron_readings TO edge_oee_odoo_sync_writer; +GRANT SELECT ON TABLE mv_hot.edge_oee_eastron_readings TO edge_oee_operator_writer; + + +-- +-- Name: TABLE edge_oee_eastron_latest; Type: ACL; Schema: mv_hot; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_hot.edge_oee_eastron_latest TO grafana_ucepsa_ro; +GRANT SELECT ON TABLE mv_hot.edge_oee_eastron_latest TO edge_oee_odoo_sync_writer; +GRANT SELECT ON TABLE mv_hot.edge_oee_eastron_latest TO edge_oee_operator_writer; + + +-- +-- Name: TABLE edge_oee_eastron_dashboard; Type: ACL; Schema: mv_hot; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_hot.edge_oee_eastron_dashboard TO grafana_ucepsa_ro; +GRANT SELECT ON TABLE mv_hot.edge_oee_eastron_dashboard TO edge_oee_odoo_sync_writer; +GRANT SELECT ON TABLE mv_hot.edge_oee_eastron_dashboard TO edge_oee_operator_writer; + + +-- +-- Name: TABLE eastron_dashboard; Type: ACL; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_edge_oee_ucepsa_demo.eastron_dashboard TO grafana_ucepsa_ro; +GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE mv_edge_oee_ucepsa_demo.eastron_dashboard TO edge_oee_odoo_sync_writer; +GRANT SELECT ON TABLE mv_edge_oee_ucepsa_demo.eastron_dashboard TO edge_oee_operator_writer; + + +-- +-- Name: TABLE edge_oee_eastron_diagnosis_latest; Type: ACL; Schema: mv_hot; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_hot.edge_oee_eastron_diagnosis_latest TO grafana_ucepsa_ro; +GRANT SELECT ON TABLE mv_hot.edge_oee_eastron_diagnosis_latest TO edge_oee_odoo_sync_writer; +GRANT SELECT ON TABLE mv_hot.edge_oee_eastron_diagnosis_latest TO edge_oee_operator_writer; + + +-- +-- Name: TABLE eastron_diagnosis_latest; Type: ACL; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_edge_oee_ucepsa_demo.eastron_diagnosis_latest TO grafana_ucepsa_ro; +GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE mv_edge_oee_ucepsa_demo.eastron_diagnosis_latest TO edge_oee_odoo_sync_writer; +GRANT SELECT ON TABLE mv_edge_oee_ucepsa_demo.eastron_diagnosis_latest TO edge_oee_operator_writer; + + +-- +-- Name: TABLE eastron_latest; Type: ACL; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_edge_oee_ucepsa_demo.eastron_latest TO grafana_ucepsa_ro; +GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE mv_edge_oee_ucepsa_demo.eastron_latest TO edge_oee_odoo_sync_writer; +GRANT SELECT ON TABLE mv_edge_oee_ucepsa_demo.eastron_latest TO edge_oee_operator_writer; + + +-- +-- Name: TABLE eastron_readings; Type: ACL; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_edge_oee_ucepsa_demo.eastron_readings TO grafana_ucepsa_ro; +GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE mv_edge_oee_ucepsa_demo.eastron_readings TO edge_oee_odoo_sync_writer; +GRANT SELECT ON TABLE mv_edge_oee_ucepsa_demo.eastron_readings TO edge_oee_operator_writer; + + +-- +-- Name: TABLE edge_oee_live_telemetry; Type: ACL; Schema: mv_hot; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_hot.edge_oee_live_telemetry TO grafana_ucepsa_ro; +GRANT SELECT ON TABLE mv_hot.edge_oee_live_telemetry TO edge_oee_odoo_sync_writer; +GRANT SELECT ON TABLE mv_hot.edge_oee_live_telemetry TO edge_oee_operator_writer; + + +-- +-- Name: TABLE edge_oee_latest_machine_snapshot; Type: ACL; Schema: mv_hot; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_hot.edge_oee_latest_machine_snapshot TO grafana_ucepsa_ro; +GRANT SELECT ON TABLE mv_hot.edge_oee_latest_machine_snapshot TO edge_oee_odoo_sync_writer; +GRANT SELECT ON TABLE mv_hot.edge_oee_latest_machine_snapshot TO edge_oee_operator_writer; + + +-- +-- Name: TABLE latest_machine_snapshot; Type: ACL; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_edge_oee_ucepsa_demo.latest_machine_snapshot TO grafana_ucepsa_ro; +GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE mv_edge_oee_ucepsa_demo.latest_machine_snapshot TO edge_oee_odoo_sync_writer; +GRANT SELECT ON TABLE mv_edge_oee_ucepsa_demo.latest_machine_snapshot TO edge_oee_operator_writer; + + +-- +-- Name: TABLE live_telemetry; Type: ACL; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_edge_oee_ucepsa_demo.live_telemetry TO grafana_ucepsa_ro; +GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE mv_edge_oee_ucepsa_demo.live_telemetry TO edge_oee_odoo_sync_writer; +GRANT SELECT ON TABLE mv_edge_oee_ucepsa_demo.live_telemetry TO edge_oee_operator_writer; + + +-- +-- Name: TABLE edge_oee_node_events; Type: ACL; Schema: mv_hot; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_hot.edge_oee_node_events TO grafana_ucepsa_ro; +GRANT SELECT ON TABLE mv_hot.edge_oee_node_events TO edge_oee_odoo_sync_writer; +GRANT SELECT ON TABLE mv_hot.edge_oee_node_events TO edge_oee_operator_writer; + + +-- +-- Name: TABLE edge_oee_machine_state_latest; Type: ACL; Schema: mv_hot; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_hot.edge_oee_machine_state_latest TO grafana_ucepsa_ro; +GRANT SELECT ON TABLE mv_hot.edge_oee_machine_state_latest TO edge_oee_odoo_sync_writer; +GRANT SELECT ON TABLE mv_hot.edge_oee_machine_state_latest TO edge_oee_operator_writer; + + +-- +-- Name: TABLE machine_state_latest; Type: ACL; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_edge_oee_ucepsa_demo.machine_state_latest TO grafana_ucepsa_ro; +GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE mv_edge_oee_ucepsa_demo.machine_state_latest TO edge_oee_odoo_sync_writer; +GRANT SELECT ON TABLE mv_edge_oee_ucepsa_demo.machine_state_latest TO edge_oee_operator_writer; + + +-- +-- Name: TABLE edge_oee_machine_stops; Type: ACL; Schema: mv_hot; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_hot.edge_oee_machine_stops TO grafana_ucepsa_ro; +GRANT SELECT ON TABLE mv_hot.edge_oee_machine_stops TO edge_oee_odoo_sync_writer; +GRANT SELECT,UPDATE ON TABLE mv_hot.edge_oee_machine_stops TO edge_oee_operator_writer; + + +-- +-- Name: TABLE machine_stops; Type: ACL; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_edge_oee_ucepsa_demo.machine_stops TO grafana_ucepsa_ro; +GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE mv_edge_oee_ucepsa_demo.machine_stops TO edge_oee_odoo_sync_writer; +GRANT SELECT ON TABLE mv_edge_oee_ucepsa_demo.machine_stops TO edge_oee_operator_writer; + + +-- +-- Name: TABLE edge_oee_machine_targets; Type: ACL; Schema: mv_hot; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_hot.edge_oee_machine_targets TO grafana_ucepsa_ro; +GRANT SELECT ON TABLE mv_hot.edge_oee_machine_targets TO edge_oee_odoo_sync_writer; +GRANT SELECT ON TABLE mv_hot.edge_oee_machine_targets TO edge_oee_operator_writer; + + +-- +-- Name: TABLE machine_targets; Type: ACL; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_edge_oee_ucepsa_demo.machine_targets TO grafana_ucepsa_ro; +GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE mv_edge_oee_ucepsa_demo.machine_targets TO edge_oee_odoo_sync_writer; +GRANT SELECT ON TABLE mv_edge_oee_ucepsa_demo.machine_targets TO edge_oee_operator_writer; + + +-- +-- Name: TABLE margin_cost_parameters; Type: ACL; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_edge_oee_ucepsa_demo.margin_cost_parameters TO grafana_ucepsa_ro; +GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE mv_edge_oee_ucepsa_demo.margin_cost_parameters TO edge_oee_odoo_sync_writer; + + +-- +-- Name: SEQUENCE margin_cost_parameters_id_seq; Type: ACL; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +GRANT ALL ON SEQUENCE mv_edge_oee_ucepsa_demo.margin_cost_parameters_id_seq TO edge_oee_odoo_sync_writer; + + +-- +-- Name: TABLE node_events; Type: ACL; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_edge_oee_ucepsa_demo.node_events TO grafana_ucepsa_ro; +GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE mv_edge_oee_ucepsa_demo.node_events TO edge_oee_odoo_sync_writer; +GRANT SELECT ON TABLE mv_edge_oee_ucepsa_demo.node_events TO edge_oee_operator_writer; + + +-- +-- Name: TABLE edge_oee_node_events_recent; Type: ACL; Schema: mv_hot; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_hot.edge_oee_node_events_recent TO grafana_ucepsa_ro; +GRANT SELECT ON TABLE mv_hot.edge_oee_node_events_recent TO edge_oee_odoo_sync_writer; +GRANT SELECT ON TABLE mv_hot.edge_oee_node_events_recent TO edge_oee_operator_writer; + + +-- +-- Name: TABLE node_events_recent; Type: ACL; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_edge_oee_ucepsa_demo.node_events_recent TO grafana_ucepsa_ro; +GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE mv_edge_oee_ucepsa_demo.node_events_recent TO edge_oee_odoo_sync_writer; +GRANT SELECT ON TABLE mv_edge_oee_ucepsa_demo.node_events_recent TO edge_oee_operator_writer; + + +-- +-- Name: TABLE odoo_order_commercials; Type: ACL; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_edge_oee_ucepsa_demo.odoo_order_commercials TO grafana_ucepsa_ro; +GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE mv_edge_oee_ucepsa_demo.odoo_order_commercials TO edge_oee_odoo_sync_writer; + + +-- +-- Name: SEQUENCE odoo_order_commercials_id_seq; Type: ACL; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +GRANT ALL ON SEQUENCE mv_edge_oee_ucepsa_demo.odoo_order_commercials_id_seq TO edge_oee_odoo_sync_writer; + + +-- +-- Name: TABLE odoo_workcenter_map; Type: ACL; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_edge_oee_ucepsa_demo.odoo_workcenter_map TO grafana_ucepsa_ro; +GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE mv_edge_oee_ucepsa_demo.odoo_workcenter_map TO edge_oee_odoo_sync_writer; +GRANT SELECT ON TABLE mv_edge_oee_ucepsa_demo.odoo_workcenter_map TO edge_oee_operator_writer; + + +-- +-- Name: SEQUENCE odoo_workcenter_map_id_seq; Type: ACL; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +GRANT ALL ON SEQUENCE mv_edge_oee_ucepsa_demo.odoo_workcenter_map_id_seq TO edge_oee_odoo_sync_writer; + + +-- +-- Name: TABLE odoo_workorders_demo; Type: ACL; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_edge_oee_ucepsa_demo.odoo_workorders_demo TO grafana_ucepsa_ro; +GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE mv_edge_oee_ucepsa_demo.odoo_workorders_demo TO edge_oee_odoo_sync_writer; +GRANT SELECT ON TABLE mv_edge_oee_ucepsa_demo.odoo_workorders_demo TO edge_oee_operator_writer; + + +-- +-- Name: SEQUENCE odoo_workorders_demo_id_seq; Type: ACL; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +GRANT ALL ON SEQUENCE mv_edge_oee_ucepsa_demo.odoo_workorders_demo_id_seq TO edge_oee_odoo_sync_writer; + + +-- +-- Name: TABLE edge_oee_stop_causes; Type: ACL; Schema: mv_hot; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_hot.edge_oee_stop_causes TO grafana_ucepsa_ro; +GRANT SELECT ON TABLE mv_hot.edge_oee_stop_causes TO edge_oee_odoo_sync_writer; +GRANT SELECT ON TABLE mv_hot.edge_oee_stop_causes TO edge_oee_operator_writer; + + +-- +-- Name: TABLE edge_oee_open_stops; Type: ACL; Schema: mv_hot; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_hot.edge_oee_open_stops TO grafana_ucepsa_ro; +GRANT SELECT ON TABLE mv_hot.edge_oee_open_stops TO edge_oee_odoo_sync_writer; +GRANT SELECT ON TABLE mv_hot.edge_oee_open_stops TO edge_oee_operator_writer; + + +-- +-- Name: TABLE open_stops; Type: ACL; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_edge_oee_ucepsa_demo.open_stops TO grafana_ucepsa_ro; +GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE mv_edge_oee_ucepsa_demo.open_stops TO edge_oee_odoo_sync_writer; +GRANT SELECT ON TABLE mv_edge_oee_ucepsa_demo.open_stops TO edge_oee_operator_writer; + + +-- +-- Name: TABLE raw_material_cost_parameters; Type: ACL; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_edge_oee_ucepsa_demo.raw_material_cost_parameters TO grafana_ucepsa_ro; +GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE mv_edge_oee_ucepsa_demo.raw_material_cost_parameters TO edge_oee_odoo_sync_writer; + + +-- +-- Name: SEQUENCE raw_material_cost_parameters_id_seq; Type: ACL; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +GRANT ALL ON SEQUENCE mv_edge_oee_ucepsa_demo.raw_material_cost_parameters_id_seq TO edge_oee_odoo_sync_writer; + + +-- +-- Name: TABLE edge_oee_stop_cause_options; Type: ACL; Schema: mv_hot; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_hot.edge_oee_stop_cause_options TO grafana_ucepsa_ro; +GRANT SELECT ON TABLE mv_hot.edge_oee_stop_cause_options TO edge_oee_odoo_sync_writer; +GRANT SELECT ON TABLE mv_hot.edge_oee_stop_cause_options TO edge_oee_operator_writer; + + +-- +-- Name: TABLE stop_cause_options; Type: ACL; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_edge_oee_ucepsa_demo.stop_cause_options TO grafana_ucepsa_ro; +GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE mv_edge_oee_ucepsa_demo.stop_cause_options TO edge_oee_odoo_sync_writer; +GRANT SELECT ON TABLE mv_edge_oee_ucepsa_demo.stop_cause_options TO edge_oee_operator_writer; + + +-- +-- Name: TABLE stop_causes; Type: ACL; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_edge_oee_ucepsa_demo.stop_causes TO grafana_ucepsa_ro; +GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE mv_edge_oee_ucepsa_demo.stop_causes TO edge_oee_odoo_sync_writer; +GRANT SELECT ON TABLE mv_edge_oee_ucepsa_demo.stop_causes TO edge_oee_operator_writer; + + +-- +-- Name: TABLE edge_oee_stop_summary_demo; Type: ACL; Schema: mv_hot; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_hot.edge_oee_stop_summary_demo TO grafana_ucepsa_ro; +GRANT SELECT ON TABLE mv_hot.edge_oee_stop_summary_demo TO edge_oee_odoo_sync_writer; +GRANT SELECT ON TABLE mv_hot.edge_oee_stop_summary_demo TO edge_oee_operator_writer; + + +-- +-- Name: TABLE stop_summary; Type: ACL; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_edge_oee_ucepsa_demo.stop_summary TO grafana_ucepsa_ro; +GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE mv_edge_oee_ucepsa_demo.stop_summary TO edge_oee_odoo_sync_writer; +GRANT SELECT ON TABLE mv_edge_oee_ucepsa_demo.stop_summary TO edge_oee_operator_writer; + + +-- +-- Name: TABLE edge_oee_stops_recent; Type: ACL; Schema: mv_hot; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_hot.edge_oee_stops_recent TO grafana_ucepsa_ro; +GRANT SELECT ON TABLE mv_hot.edge_oee_stops_recent TO edge_oee_odoo_sync_writer; +GRANT SELECT ON TABLE mv_hot.edge_oee_stops_recent TO edge_oee_operator_writer; + + +-- +-- Name: TABLE stops_recent; Type: ACL; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_edge_oee_ucepsa_demo.stops_recent TO grafana_ucepsa_ro; +GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE mv_edge_oee_ucepsa_demo.stops_recent TO edge_oee_odoo_sync_writer; +GRANT SELECT ON TABLE mv_edge_oee_ucepsa_demo.stops_recent TO edge_oee_operator_writer; + + +-- +-- Name: TABLE data_quality_contract_rules; Type: ACL; Schema: mv_edge_oee_ucepsa_prod; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_edge_oee_ucepsa_prod.data_quality_contract_rules TO grafana_ucepsa_ro; +GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE mv_edge_oee_ucepsa_prod.data_quality_contract_rules TO edge_oee_odoo_sync_writer; + + +-- +-- Name: TABLE margin_cost_parameters; Type: ACL; Schema: mv_edge_oee_ucepsa_prod; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_edge_oee_ucepsa_prod.margin_cost_parameters TO grafana_ucepsa_ro; +GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE mv_edge_oee_ucepsa_prod.margin_cost_parameters TO edge_oee_odoo_sync_writer; + + +-- +-- Name: TABLE odoo_order_commercials; Type: ACL; Schema: mv_edge_oee_ucepsa_prod; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_edge_oee_ucepsa_prod.odoo_order_commercials TO grafana_ucepsa_ro; +GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE mv_edge_oee_ucepsa_prod.odoo_order_commercials TO edge_oee_odoo_sync_writer; + + +-- +-- Name: SEQUENCE odoo_order_commercials_id_seq; Type: ACL; Schema: mv_edge_oee_ucepsa_prod; Owner: mesavault +-- + +GRANT ALL ON SEQUENCE mv_edge_oee_ucepsa_prod.odoo_order_commercials_id_seq TO edge_oee_odoo_sync_writer; + + +-- +-- Name: TABLE raw_material_cost_parameters; Type: ACL; Schema: mv_edge_oee_ucepsa_prod; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_edge_oee_ucepsa_prod.raw_material_cost_parameters TO grafana_ucepsa_ro; +GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE mv_edge_oee_ucepsa_prod.raw_material_cost_parameters TO edge_oee_odoo_sync_writer; + + +-- +-- Name: TABLE edge_oee_loss_summary_demo; Type: ACL; Schema: mv_hot; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_hot.edge_oee_loss_summary_demo TO grafana_ucepsa_ro; +GRANT SELECT ON TABLE mv_hot.edge_oee_loss_summary_demo TO edge_oee_odoo_sync_writer; +GRANT SELECT ON TABLE mv_hot.edge_oee_loss_summary_demo TO edge_oee_operator_writer; + + +-- +-- Name: TABLE edge_oee_actions_demo; Type: ACL; Schema: mv_hot; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_hot.edge_oee_actions_demo TO grafana_ucepsa_ro; +GRANT SELECT ON TABLE mv_hot.edge_oee_actions_demo TO edge_oee_odoo_sync_writer; +GRANT SELECT ON TABLE mv_hot.edge_oee_actions_demo TO edge_oee_operator_writer; + + +-- +-- Name: TABLE edge_oee_data_quality_demo; Type: ACL; Schema: mv_hot; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_hot.edge_oee_data_quality_demo TO grafana_ucepsa_ro; +GRANT SELECT ON TABLE mv_hot.edge_oee_data_quality_demo TO edge_oee_odoo_sync_writer; +GRANT SELECT ON TABLE mv_hot.edge_oee_data_quality_demo TO edge_oee_operator_writer; + + +-- +-- Name: TABLE edge_oee_data_quality_dashboard_demo; Type: ACL; Schema: mv_hot; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_hot.edge_oee_data_quality_dashboard_demo TO grafana_ucepsa_ro; +GRANT SELECT ON TABLE mv_hot.edge_oee_data_quality_dashboard_demo TO edge_oee_odoo_sync_writer; +GRANT SELECT ON TABLE mv_hot.edge_oee_data_quality_dashboard_demo TO edge_oee_operator_writer; + + +-- +-- Name: SEQUENCE edge_oee_eastron_readings_id_seq; Type: ACL; Schema: mv_hot; Owner: mesavault +-- + +GRANT ALL ON SEQUENCE mv_hot.edge_oee_eastron_readings_id_seq TO edge_oee_odoo_sync_writer; +GRANT ALL ON SEQUENCE mv_hot.edge_oee_eastron_readings_id_seq TO edge_oee_operator_writer; + + +-- +-- Name: TABLE edge_oee_energy_demo; Type: ACL; Schema: mv_hot; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_hot.edge_oee_energy_demo TO grafana_ucepsa_ro; +GRANT SELECT ON TABLE mv_hot.edge_oee_energy_demo TO edge_oee_odoo_sync_writer; +GRANT SELECT ON TABLE mv_hot.edge_oee_energy_demo TO edge_oee_operator_writer; + + +-- +-- Name: TABLE edge_oee_events_demo; Type: ACL; Schema: mv_hot; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_hot.edge_oee_events_demo TO grafana_ucepsa_ro; +GRANT SELECT ON TABLE mv_hot.edge_oee_events_demo TO edge_oee_odoo_sync_writer; +GRANT SELECT ON TABLE mv_hot.edge_oee_events_demo TO edge_oee_operator_writer; + + +-- +-- Name: SEQUENCE edge_oee_events_demo_event_id_seq; Type: ACL; Schema: mv_hot; Owner: mesavault +-- + +GRANT ALL ON SEQUENCE mv_hot.edge_oee_events_demo_event_id_seq TO edge_oee_odoo_sync_writer; +GRANT ALL ON SEQUENCE mv_hot.edge_oee_events_demo_event_id_seq TO edge_oee_operator_writer; + + +-- +-- Name: TABLE edge_oee_executive_summary_demo; Type: ACL; Schema: mv_hot; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_hot.edge_oee_executive_summary_demo TO grafana_ucepsa_ro; +GRANT SELECT ON TABLE mv_hot.edge_oee_executive_summary_demo TO edge_oee_odoo_sync_writer; +GRANT SELECT ON TABLE mv_hot.edge_oee_executive_summary_demo TO edge_oee_operator_writer; + + +-- +-- Name: TABLE edge_oee_kpi_cards_demo; Type: ACL; Schema: mv_hot; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_hot.edge_oee_kpi_cards_demo TO grafana_ucepsa_ro; +GRANT SELECT ON TABLE mv_hot.edge_oee_kpi_cards_demo TO edge_oee_odoo_sync_writer; +GRANT SELECT ON TABLE mv_hot.edge_oee_kpi_cards_demo TO edge_oee_operator_writer; + + +-- +-- Name: TABLE edge_oee_machine_stops_backup_cort00_20260622_175800; Type: ACL; Schema: mv_hot; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_hot.edge_oee_machine_stops_backup_cort00_20260622_175800 TO grafana_ucepsa_ro; +GRANT SELECT ON TABLE mv_hot.edge_oee_machine_stops_backup_cort00_20260622_175800 TO edge_oee_odoo_sync_writer; + + +-- +-- Name: SEQUENCE edge_oee_machine_stops_id_seq; Type: ACL; Schema: mv_hot; Owner: mesavault +-- + +GRANT ALL ON SEQUENCE mv_hot.edge_oee_machine_stops_id_seq TO edge_oee_odoo_sync_writer; +GRANT ALL ON SEQUENCE mv_hot.edge_oee_machine_stops_id_seq TO edge_oee_operator_writer; + + +-- +-- Name: TABLE edge_oee_machines; Type: ACL; Schema: mv_hot; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_hot.edge_oee_machines TO grafana_ucepsa_ro; +GRANT SELECT ON TABLE mv_hot.edge_oee_machines TO edge_oee_odoo_sync_writer; +GRANT SELECT ON TABLE mv_hot.edge_oee_machines TO edge_oee_operator_writer; + + +-- +-- Name: SEQUENCE edge_oee_node_events_id_seq; Type: ACL; Schema: mv_hot; Owner: mesavault +-- + +GRANT ALL ON SEQUENCE mv_hot.edge_oee_node_events_id_seq TO edge_oee_odoo_sync_writer; +GRANT ALL ON SEQUENCE mv_hot.edge_oee_node_events_id_seq TO edge_oee_operator_writer; + + +-- +-- Name: TABLE edge_oee_orders_demo; Type: ACL; Schema: mv_hot; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_hot.edge_oee_orders_demo TO grafana_ucepsa_ro; +GRANT SELECT ON TABLE mv_hot.edge_oee_orders_demo TO edge_oee_odoo_sync_writer; +GRANT SELECT ON TABLE mv_hot.edge_oee_orders_demo TO edge_oee_operator_writer; + + +-- +-- Name: TABLE edge_oee_order_timeline_demo; Type: ACL; Schema: mv_hot; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_hot.edge_oee_order_timeline_demo TO grafana_ucepsa_ro; +GRANT SELECT ON TABLE mv_hot.edge_oee_order_timeline_demo TO edge_oee_odoo_sync_writer; +GRANT SELECT ON TABLE mv_hot.edge_oee_order_timeline_demo TO edge_oee_operator_writer; + + +-- +-- Name: TABLE edge_oee_pareto_demo; Type: ACL; Schema: mv_hot; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_hot.edge_oee_pareto_demo TO grafana_ucepsa_ro; +GRANT SELECT ON TABLE mv_hot.edge_oee_pareto_demo TO edge_oee_odoo_sync_writer; +GRANT SELECT ON TABLE mv_hot.edge_oee_pareto_demo TO edge_oee_operator_writer; + + +-- +-- Name: TABLE edge_oee_quality_cases_demo; Type: ACL; Schema: mv_hot; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_hot.edge_oee_quality_cases_demo TO grafana_ucepsa_ro; +GRANT SELECT ON TABLE mv_hot.edge_oee_quality_cases_demo TO edge_oee_odoo_sync_writer; +GRANT SELECT ON TABLE mv_hot.edge_oee_quality_cases_demo TO edge_oee_operator_writer; + + +-- +-- Name: TABLE edge_oee_quality_case_demo; Type: ACL; Schema: mv_hot; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_hot.edge_oee_quality_case_demo TO grafana_ucepsa_ro; +GRANT SELECT ON TABLE mv_hot.edge_oee_quality_case_demo TO edge_oee_odoo_sync_writer; +GRANT SELECT ON TABLE mv_hot.edge_oee_quality_case_demo TO edge_oee_operator_writer; + + +-- +-- Name: TABLE odoo_order_state_current; Type: ACL; Schema: mv_hot; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_hot.odoo_order_state_current TO grafana_ucepsa_ro; +GRANT SELECT ON TABLE mv_hot.odoo_order_state_current TO edge_oee_odoo_sync_writer; + + +-- +-- Name: TABLE odoo_order_state_events; Type: ACL; Schema: mv_hot; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_hot.odoo_order_state_events TO grafana_ucepsa_ro; +GRANT SELECT ON TABLE mv_hot.odoo_order_state_events TO edge_oee_odoo_sync_writer; + + +-- +-- Name: SEQUENCE odoo_order_state_events_id_seq; Type: ACL; Schema: mv_hot; Owner: mesavault +-- + +GRANT ALL ON SEQUENCE mv_hot.odoo_order_state_events_id_seq TO edge_oee_odoo_sync_writer; + + +-- +-- Name: TABLE order_execution_report_v1; Type: ACL; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.order_execution_report_v1 TO grafana_ucepsa_ro; +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.order_execution_report_v1 TO edge_oee_odoo_sync_writer; + + +-- +-- Name: TABLE order_execution_energy_timeseries_v1; Type: ACL; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.order_execution_energy_timeseries_v1 TO grafana_ucepsa_ro; +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.order_execution_energy_timeseries_v1 TO edge_oee_odoo_sync_writer; + + +-- +-- Name: TABLE order_execution_data_quality_v1; Type: ACL; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.order_execution_data_quality_v1 TO grafana_ucepsa_ro; +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.order_execution_data_quality_v1 TO edge_oee_odoo_sync_writer; + + +-- +-- Name: TABLE order_execution_stops_v1; Type: ACL; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.order_execution_stops_v1 TO grafana_ucepsa_ro; +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.order_execution_stops_v1 TO edge_oee_odoo_sync_writer; + + +-- +-- Name: TABLE order_execution_stop_causes_v1; Type: ACL; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.order_execution_stop_causes_v1 TO grafana_ucepsa_ro; +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.order_execution_stop_causes_v1 TO edge_oee_odoo_sync_writer; + + +-- +-- Name: TABLE order_execution_timeline_v1; Type: ACL; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.order_execution_timeline_v1 TO grafana_ucepsa_ro; +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.order_execution_timeline_v1 TO edge_oee_odoo_sync_writer; + + +-- +-- Name: TABLE order_execution_report_full_v1; Type: ACL; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.order_execution_report_full_v1 TO grafana_ucepsa_ro; +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.order_execution_report_full_v1 TO edge_oee_odoo_sync_writer; + + +-- +-- Name: TABLE order_raw_material_costs_v1; Type: ACL; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.order_raw_material_costs_v1 TO grafana_ucepsa_ro; +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.order_raw_material_costs_v1 TO edge_oee_odoo_sync_writer; + + +-- +-- Name: TABLE order_raw_material_costs_summary_v1; Type: ACL; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.order_raw_material_costs_summary_v1 TO grafana_ucepsa_ro; +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.order_raw_material_costs_summary_v1 TO edge_oee_odoo_sync_writer; + + +-- +-- Name: TABLE order_margin_report_v1; Type: ACL; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.order_margin_report_v1 TO grafana_ucepsa_ro; +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.order_margin_report_v1 TO edge_oee_odoo_sync_writer; + + +-- +-- Name: TABLE cost_readiness_orders_v1; Type: ACL; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.cost_readiness_orders_v1 TO grafana_ucepsa_ro; +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.cost_readiness_orders_v1 TO edge_oee_odoo_sync_writer; + + +-- +-- Name: TABLE cost_readiness_raw_materials_v1; Type: ACL; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.cost_readiness_raw_materials_v1 TO grafana_ucepsa_ro; +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.cost_readiness_raw_materials_v1 TO edge_oee_odoo_sync_writer; + + +-- +-- Name: TABLE cost_readiness_summary_v1; Type: ACL; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.cost_readiness_summary_v1 TO grafana_ucepsa_ro; +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.cost_readiness_summary_v1 TO edge_oee_odoo_sync_writer; + + +-- +-- Name: TABLE cycle_rate_timeseries; Type: ACL; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.cycle_rate_timeseries TO grafana_ucepsa_ro; +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.cycle_rate_timeseries TO edge_oee_odoo_sync_writer; +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.cycle_rate_timeseries TO edge_oee_operator_writer; + + +-- +-- Name: TABLE data_quality_contract_order_checks_v1; Type: ACL; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.data_quality_contract_order_checks_v1 TO grafana_ucepsa_ro; +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.data_quality_contract_order_checks_v1 TO edge_oee_odoo_sync_writer; + + +-- +-- Name: TABLE data_quality_contract_order_summary_v1; Type: ACL; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.data_quality_contract_order_summary_v1 TO grafana_ucepsa_ro; +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.data_quality_contract_order_summary_v1 TO edge_oee_odoo_sync_writer; + + +-- +-- Name: TABLE data_quality_contract_owner_summary_v1; Type: ACL; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.data_quality_contract_owner_summary_v1 TO grafana_ucepsa_ro; +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.data_quality_contract_owner_summary_v1 TO edge_oee_odoo_sync_writer; + + +-- +-- Name: TABLE energy_3phase_timeseries; Type: ACL; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.energy_3phase_timeseries TO grafana_ucepsa_ro; +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.energy_3phase_timeseries TO edge_oee_odoo_sync_writer; + + +-- +-- Name: TABLE energy_3phase_latest; Type: ACL; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.energy_3phase_latest TO grafana_ucepsa_ro; +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.energy_3phase_latest TO edge_oee_odoo_sync_writer; + + +-- +-- Name: TABLE energy_diagnosis; Type: ACL; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.energy_diagnosis TO grafana_ucepsa_ro; +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.energy_diagnosis TO edge_oee_odoo_sync_writer; +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.energy_diagnosis TO edge_oee_operator_writer; + + +-- +-- Name: TABLE energy_import_timeseries; Type: ACL; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.energy_import_timeseries TO grafana_ucepsa_ro; +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.energy_import_timeseries TO edge_oee_odoo_sync_writer; +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.energy_import_timeseries TO edge_oee_operator_writer; + + +-- +-- Name: TABLE energy_latest; Type: ACL; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.energy_latest TO grafana_ucepsa_ro; +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.energy_latest TO edge_oee_odoo_sync_writer; +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.energy_latest TO edge_oee_operator_writer; + + +-- +-- Name: TABLE energy_power_timeseries; Type: ACL; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.energy_power_timeseries TO grafana_ucepsa_ro; +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.energy_power_timeseries TO edge_oee_odoo_sync_writer; +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.energy_power_timeseries TO edge_oee_operator_writer; + + +-- +-- Name: TABLE latest_machine_snapshot; Type: ACL; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.latest_machine_snapshot TO grafana_ucepsa_ro; +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.latest_machine_snapshot TO edge_oee_odoo_sync_writer; +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.latest_machine_snapshot TO edge_oee_operator_writer; + + +-- +-- Name: TABLE latest_machine_state_fast; Type: ACL; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.latest_machine_state_fast TO grafana_ucepsa_ro; +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.latest_machine_state_fast TO edge_oee_odoo_sync_writer; +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.latest_machine_state_fast TO edge_oee_operator_writer; + + +-- +-- Name: TABLE live_telemetry; Type: ACL; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.live_telemetry TO grafana_ucepsa_ro; +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.live_telemetry TO edge_oee_odoo_sync_writer; +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.live_telemetry TO edge_oee_operator_writer; + + +-- +-- Name: TABLE odoo_current_order_demo; Type: ACL; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.odoo_current_order_demo TO grafana_ucepsa_ro; +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.odoo_current_order_demo TO edge_oee_odoo_sync_writer; +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.odoo_current_order_demo TO edge_oee_operator_writer; + + +-- +-- Name: TABLE stops_recent; Type: ACL; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.stops_recent TO grafana_ucepsa_ro; +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.stops_recent TO edge_oee_odoo_sync_writer; +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.stops_recent TO edge_oee_operator_writer; + + +-- +-- Name: TABLE odoo_vs_machine_demo; Type: ACL; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.odoo_vs_machine_demo TO grafana_ucepsa_ro; +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.odoo_vs_machine_demo TO edge_oee_odoo_sync_writer; + + +-- +-- Name: TABLE open_stops; Type: ACL; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.open_stops TO grafana_ucepsa_ro; +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.open_stops TO edge_oee_odoo_sync_writer; +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.open_stops TO edge_oee_operator_writer; + + +-- +-- Name: TABLE loss_events_current; Type: ACL; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.loss_events_current TO grafana_ucepsa_ro; +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.loss_events_current TO edge_oee_odoo_sync_writer; + + +-- +-- Name: TABLE loss_priority_board; Type: ACL; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.loss_priority_board TO grafana_ucepsa_ro; +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.loss_priority_board TO edge_oee_odoo_sync_writer; + + +-- +-- Name: TABLE machine_state_latest; Type: ACL; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.machine_state_latest TO grafana_ucepsa_ro; +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.machine_state_latest TO edge_oee_odoo_sync_writer; +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.machine_state_latest TO edge_oee_operator_writer; + + +-- +-- Name: TABLE machine_state_latest_multi; Type: ACL; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.machine_state_latest_multi TO grafana_ucepsa_ro; +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.machine_state_latest_multi TO edge_oee_odoo_sync_writer; + + +-- +-- Name: TABLE machine_stops_grafana; Type: ACL; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.machine_stops_grafana TO grafana_ucepsa_ro; +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.machine_stops_grafana TO edge_oee_odoo_sync_writer; + + +-- +-- Name: TABLE machine_targets; Type: ACL; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.machine_targets TO grafana_ucepsa_ro; +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.machine_targets TO edge_oee_odoo_sync_writer; +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.machine_targets TO edge_oee_operator_writer; + + +-- +-- Name: TABLE node_events_recent; Type: ACL; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.node_events_recent TO grafana_ucepsa_ro; +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.node_events_recent TO edge_oee_odoo_sync_writer; +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.node_events_recent TO edge_oee_operator_writer; + + +-- +-- Name: TABLE odoo_discrepancies_demo; Type: ACL; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.odoo_discrepancies_demo TO grafana_ucepsa_ro; +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.odoo_discrepancies_demo TO edge_oee_odoo_sync_writer; + + +-- +-- Name: TABLE odoo_workorders_reality_history; Type: ACL; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.odoo_workorders_reality_history TO grafana_ucepsa_ro; +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.odoo_workorders_reality_history TO edge_oee_odoo_sync_writer; +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.odoo_workorders_reality_history TO edge_oee_operator_writer; + + +-- +-- Name: TABLE odoo_last_finished_order_demo; Type: ACL; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.odoo_last_finished_order_demo TO grafana_ucepsa_ro; +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.odoo_last_finished_order_demo TO edge_oee_odoo_sync_writer; +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.odoo_last_finished_order_demo TO edge_oee_operator_writer; + + +-- +-- Name: TABLE odoo_order_state_machine_grafana; Type: ACL; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.odoo_order_state_machine_grafana TO grafana_ucepsa_ro; +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.odoo_order_state_machine_grafana TO edge_oee_odoo_sync_writer; + + +-- +-- Name: TABLE stop_summary; Type: ACL; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.stop_summary TO grafana_ucepsa_ro; +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.stop_summary TO edge_oee_odoo_sync_writer; +GRANT SELECT ON TABLE mv_reports_ucepsa_demo.stop_summary TO edge_oee_operator_writer; + + +-- +-- Name: TABLE current_order_margin_readiness_v1; Type: ACL; Schema: mv_reports_ucepsa_prod; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_reports_ucepsa_prod.current_order_margin_readiness_v1 TO grafana_ucepsa_ro; + + +-- +-- Name: TABLE order_raw_material_costs_v1; Type: ACL; Schema: mv_reports_ucepsa_prod; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_reports_ucepsa_prod.order_raw_material_costs_v1 TO grafana_ucepsa_ro; + + +-- +-- Name: TABLE order_raw_material_costs_summary_v1; Type: ACL; Schema: mv_reports_ucepsa_prod; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_reports_ucepsa_prod.order_raw_material_costs_summary_v1 TO grafana_ucepsa_ro; + + +-- +-- Name: TABLE raw_material_cost_requirements_v1; Type: ACL; Schema: mv_reports_ucepsa_prod; Owner: mesavault +-- + +GRANT SELECT ON TABLE mv_reports_ucepsa_prod.raw_material_cost_requirements_v1 TO grafana_ucepsa_ro; + + +-- +-- Name: DEFAULT PRIVILEGES FOR SEQUENCES; Type: DEFAULT ACL; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +ALTER DEFAULT PRIVILEGES FOR ROLE mesavault IN SCHEMA mv_edge_oee_ucepsa_demo GRANT ALL ON SEQUENCES TO edge_oee_odoo_sync_writer; + + +-- +-- Name: DEFAULT PRIVILEGES FOR TABLES; Type: DEFAULT ACL; Schema: mv_edge_oee_ucepsa_demo; Owner: mesavault +-- + +ALTER DEFAULT PRIVILEGES FOR ROLE mesavault IN SCHEMA mv_edge_oee_ucepsa_demo GRANT SELECT ON TABLES TO grafana_ucepsa_ro; +ALTER DEFAULT PRIVILEGES FOR ROLE mesavault IN SCHEMA mv_edge_oee_ucepsa_demo GRANT SELECT,INSERT,DELETE,UPDATE ON TABLES TO edge_oee_odoo_sync_writer; + + +-- +-- Name: DEFAULT PRIVILEGES FOR SEQUENCES; Type: DEFAULT ACL; Schema: mv_hot; Owner: mesavault +-- + +ALTER DEFAULT PRIVILEGES FOR ROLE mesavault IN SCHEMA mv_hot GRANT ALL ON SEQUENCES TO edge_oee_odoo_sync_writer; + + +-- +-- Name: DEFAULT PRIVILEGES FOR TABLES; Type: DEFAULT ACL; Schema: mv_hot; Owner: mesavault +-- + +ALTER DEFAULT PRIVILEGES FOR ROLE mesavault IN SCHEMA mv_hot GRANT SELECT ON TABLES TO grafana_ucepsa_ro; +ALTER DEFAULT PRIVILEGES FOR ROLE mesavault IN SCHEMA mv_hot GRANT SELECT ON TABLES TO edge_oee_odoo_sync_writer; + + +-- +-- Name: DEFAULT PRIVILEGES FOR FUNCTIONS; Type: DEFAULT ACL; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +ALTER DEFAULT PRIVILEGES FOR ROLE mesavault IN SCHEMA mv_reports_ucepsa_demo GRANT ALL ON FUNCTIONS TO grafana_ucepsa_ro; + + +-- +-- Name: DEFAULT PRIVILEGES FOR TABLES; Type: DEFAULT ACL; Schema: mv_reports_ucepsa_demo; Owner: mesavault +-- + +ALTER DEFAULT PRIVILEGES FOR ROLE mesavault IN SCHEMA mv_reports_ucepsa_demo GRANT SELECT ON TABLES TO grafana_ucepsa_ro; +ALTER DEFAULT PRIVILEGES FOR ROLE mesavault IN SCHEMA mv_reports_ucepsa_demo GRANT SELECT ON TABLES TO edge_oee_odoo_sync_writer; + + +-- +-- PostgreSQL database dump complete +-- + +\unrestrict 1wX0I2K6nLSszbhqDMaBaJRF13jG1hT3a3HKImFGiykK1wx4riB6Rf5OY4lcf06 + diff --git a/ucepsa/edge-oee-demo/deploy/ucepsa-local/.env.example b/ucepsa/edge-oee-demo/deploy/ucepsa-local/.env.example new file mode 100644 index 0000000..a12f819 --- /dev/null +++ b/ucepsa/edge-oee-demo/deploy/ucepsa-local/.env.example @@ -0,0 +1,54 @@ +# Identidad +TENANT=ucepsa +SITE=demo_edge_oee +VERTICAL=edge_oee +MACHINE_ID=CORT-01 +ASSET=revpi_oee_node_01 + +# PostgreSQL MESAVAULT +POSTGRES_DB=mesavault_ucepsa_hot +POSTGRES_USER=mesavault + +# Grafana +GRAFANA_ADMIN_USER=admin + +# MQTT local +MQTT_PORT=1883 + +# MQTT credentials for local Edge-OEE sink +MQTT_USER_REVPI=revpi_ucepsa_edge_oee_01 +POSTGRES_PASSWORD=__SET_IN_ENV_OR_VAULTWARDEN__ + +# PostgreSQL writer for Odoo sync + +# Odoo UCEPSA +#ODOO_URL=https://17.126.1.61 +ODOO_URL=http://17.126.1.61:8069 +ODOO_DB=ucepsa_test_jp +ODOO_USER=__SET_IN_ENV_OR_VAULTWARDEN__ +ODOO_PASSWORD=__SET_IN_ENV_OR_VAULTWARDEN__ +ODOO_WORKCENTER_CODE=CORT-01 +ODOO_INSTANCE=odoo_ucepsa_test + +ODOO_SYNC_INTERVAL_SECONDS=30 +ODOO_WORKORDER_LIMIT=100 +ODOO_WORKORDER_STATES=waiting,ready,progress,done + +# PostgreSQL writer for Odoo sync +ODOO_SYNC_PGUSER=edge_oee_odoo_sync_writer +ODOO_SYNC_PGPASSWORD=__SET_IN_ENV_OR_VAULTWARDEN__ + +# Operator stop console +OPERATOR_PGUSER=edge_oee_operator_writer +OPERATOR_PGPASSWORD=__SET_IN_ENV_OR_VAULTWARDEN__ +OPERATOR_APP_PORT=8080 +OPERATOR_HOST_PORT=8088 +OPERATOR_DEFAULT_MACHINE_ID=CORT-01 +OPERATOR_DEFAULT_ASSET=revpi_oee_node_01 +OPERATOR_ALLOWED_MACHINES=CORT-00,CORT-01,CORT-02,CORT-03,CORT-04,CORT-05,CORT-06,CORT-08,CORT-09,CORT-10,CORT-FAS1,CORT-FAS2 + +# Operator console auto refresh +OPERATOR_AUTO_REFRESH_SECONDS=5 +MQTT_PASSWORD_REVPI=__SET_IN_ENV_OR_VAULTWARDEN__ +OPERATOR_DEFAULT_MACHINE=CORT-00 +GRAFANA_ADMIN_PASSWORD=__SET_IN_ENV_OR_VAULTWARDEN__ diff --git a/ucepsa/edge-oee-demo/deploy/ucepsa-local/docker-compose.edge.yml b/ucepsa/edge-oee-demo/deploy/ucepsa-local/docker-compose.edge.yml new file mode 100644 index 0000000..4e3f3f7 --- /dev/null +++ b/ucepsa/edge-oee-demo/deploy/ucepsa-local/docker-compose.edge.yml @@ -0,0 +1,33 @@ +services: + mv_ucepsa_edge_oee_mqtt_pg_sink: + build: + context: ./services/edge-oee-mqtt-pg-sink + container_name: mv_ucepsa_edge_oee_mqtt_pg_sink + restart: unless-stopped + environment: + PYTHONUNBUFFERED: "1" + + MQTT_HOST: mv_ucepsa_mosquitto + MQTT_PORT: 1883 + MQTT_USER: ${MQTT_USER_REVPI} + MQTT_PASSWORD: ${MQTT_PASSWORD_REVPI} + MQTT_TOPIC: vertical/ucepsa/ucepsa_onpremise/edge_oee/revpi_oee_node_01/+ + + PGHOST: mv_ucepsa_postgres_hot + PGPORT: 5432 + PGDATABASE: ${POSTGRES_DB} + PGUSER: ${POSTGRES_USER} + PGPASSWORD: ${POSTGRES_PASSWORD} + + TENANT: ucepsa + SITE: ucepsa_onpremise + VERTICAL: edge_oee + ASSET: revpi_oee_node_01 + MACHINE_ID: CORT-01 + VALUE_HOUR_EUR: 36 + TZ: Europe/Madrid + depends_on: + - mv_ucepsa_postgres_hot + - mv_ucepsa_mosquitto + networks: + - mv_ucepsa_net diff --git a/ucepsa/edge-oee-demo/deploy/ucepsa-local/docker-compose.odoo.yml b/ucepsa/edge-oee-demo/deploy/ucepsa-local/docker-compose.odoo.yml new file mode 100644 index 0000000..3b752c5 --- /dev/null +++ b/ucepsa/edge-oee-demo/deploy/ucepsa-local/docker-compose.odoo.yml @@ -0,0 +1,35 @@ +services: + mv_ucepsa_odoo_workorders_sync: + build: + context: ./services/odoo-workorders-sync + container_name: mv_ucepsa_odoo_workorders_sync + restart: unless-stopped + environment: + PYTHONUNBUFFERED: "1" + + ODOO_URL: ${ODOO_URL} + ODOO_DB: ${ODOO_DB} + ODOO_USER: ${ODOO_USER} + ODOO_PASSWORD: ${ODOO_PASSWORD} + ODOO_WORKCENTER_CODE: ${ODOO_WORKCENTER_CODE} + ODOO_INSTANCE: ${ODOO_INSTANCE} + ODOO_SYNC_INTERVAL_SECONDS: ${ODOO_SYNC_INTERVAL_SECONDS} + ODOO_WORKORDER_LIMIT: ${ODOO_WORKORDER_LIMIT} + ODOO_WORKORDER_STATES: ${ODOO_WORKORDER_STATES} + + PGHOST: mv_ucepsa_postgres_hot + PGPORT: 5432 + PGDATABASE: ${POSTGRES_DB} + PGUSER: ${ODOO_SYNC_PGUSER} + PGPASSWORD: ${ODOO_SYNC_PGPASSWORD} + + TENANT: ucepsa + SITE: ucepsa_test + MACHINE_ID: CORT-01 + ASSET: revpi_oee_node_01 + TARGET_CYCLE_RATE_PPM: 10 + TZ: Europe/Madrid + depends_on: + - mv_ucepsa_postgres_hot + networks: + - mv_ucepsa_net diff --git a/ucepsa/edge-oee-demo/deploy/ucepsa-local/docker-compose.operator.yml b/ucepsa/edge-oee-demo/deploy/ucepsa-local/docker-compose.operator.yml new file mode 100644 index 0000000..e063a41 --- /dev/null +++ b/ucepsa/edge-oee-demo/deploy/ucepsa-local/docker-compose.operator.yml @@ -0,0 +1,28 @@ +services: + mv_ucepsa_operator_stop_console: + build: + context: ./services/operator-stop-console + container_name: mv_ucepsa_operator_stop_console + restart: unless-stopped + ports: + - "${OPERATOR_HOST_PORT:-8088}:${OPERATOR_APP_PORT:-8080}" + environment: + PYTHONUNBUFFERED: "1" + + PGHOST: mv_ucepsa_postgres_hot + PGPORT: 5432 + PGDATABASE: ${POSTGRES_DB} + PGUSER: ${OPERATOR_PGUSER} + PGPASSWORD: ${OPERATOR_PGPASSWORD} + + APP_PORT: ${OPERATOR_APP_PORT:-8080} + APP_TITLE: "MESAVAULT Edge-OEE - Consola de paros UCEPSA" + DEFAULT_MACHINE_ID: ${OPERATOR_DEFAULT_MACHINE_ID:-CORT-01} + DEFAULT_ASSET: ${OPERATOR_DEFAULT_ASSET:-revpi_oee_node_01} + ALLOWED_MACHINES: ${OPERATOR_ALLOWED_MACHINES:-CORT-01} + AUTO_REFRESH_SECONDS: ${OPERATOR_AUTO_REFRESH_SECONDS:-5} + TZ: Europe/Madrid + depends_on: + - mv_ucepsa_postgres_hot + networks: + - mv_ucepsa_net diff --git a/ucepsa/edge-oee-demo/deploy/ucepsa-local/docker-compose.order-state.yml b/ucepsa/edge-oee-demo/deploy/ucepsa-local/docker-compose.order-state.yml new file mode 100644 index 0000000..67eb843 --- /dev/null +++ b/ucepsa/edge-oee-demo/deploy/ucepsa-local/docker-compose.order-state.yml @@ -0,0 +1,37 @@ +services: + mv_ucepsa_odoo_order_state_publisher: + image: edge-oee-ucepsa-mv_ucepsa_edge_oee_mqtt_pg_sink + container_name: mv_ucepsa_odoo_order_state_publisher + restart: unless-stopped + env_file: + - .env + environment: + ODOO_DB_OVERRIDE: ucepsa_prod + SITE_OVERRIDE: ucepsa_onpremise + MQTT_HOST_OVERRIDE: mv_ucepsa_mosquitto + MQTT_USER: ${MQTT_USER_REVPI} + MQTT_PASSWORD: ${MQTT_PASSWORD_REVPI} + PYTHONUNBUFFERED: "1" + PYTHONDONTWRITEBYTECODE: "1" + volumes: + - ./:/app:ro + working_dir: /app + command: + - python + - tools/odoo_order_state_publisher.py + - --publish + - --loop + - --interval + - "5" + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + networks: + - mv_ucepsa_net + +networks: + mv_ucepsa_net: + external: true + name: mv_ucepsa_net diff --git a/ucepsa/edge-oee-demo/deploy/ucepsa-local/docker-compose.yml b/ucepsa/edge-oee-demo/deploy/ucepsa-local/docker-compose.yml new file mode 100644 index 0000000..2750d99 --- /dev/null +++ b/ucepsa/edge-oee-demo/deploy/ucepsa-local/docker-compose.yml @@ -0,0 +1,49 @@ +services: + mv_ucepsa_postgres_hot: + image: postgres:16-alpine + container_name: mv_ucepsa_postgres_hot + restart: unless-stopped + environment: + POSTGRES_DB: ${POSTGRES_DB} + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + TZ: Europe/Madrid + volumes: + - ./postgres/data:/var/lib/postgresql/data + networks: + - mv_ucepsa_net + + mv_ucepsa_mosquitto: + image: eclipse-mosquitto:2 + container_name: mv_ucepsa_mosquitto + restart: unless-stopped + ports: + - "1883:1883" + volumes: + - ./mosquitto/config:/mosquitto/config + - ./mosquitto/data:/mosquitto/data + - ./mosquitto/log:/mosquitto/log + networks: + - mv_ucepsa_net + + mv_ucepsa_grafana: + image: grafana/grafana-oss:latest + container_name: mv_ucepsa_grafana + restart: unless-stopped + ports: + - "3000:3000" + environment: + GF_SECURITY_ADMIN_USER: ${GRAFANA_ADMIN_USER} + GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_ADMIN_PASSWORD} + GF_USERS_ALLOW_SIGN_UP: "false" + TZ: Europe/Madrid + volumes: + - ./grafana/data:/var/lib/grafana + depends_on: + - mv_ucepsa_postgres_hot + networks: + - mv_ucepsa_net + +networks: + mv_ucepsa_net: + name: mv_ucepsa_net diff --git a/ucepsa/edge-oee-demo/grafana/dashboards/ucepsa-edge-oee-machine.dashboard.json b/ucepsa/edge-oee-demo/grafana/dashboards/ucepsa-edge-oee-machine.dashboard.json new file mode 100644 index 0000000..19a21d8 --- /dev/null +++ b/ucepsa/edge-oee-demo/grafana/dashboards/ucepsa-edge-oee-machine.dashboard.json @@ -0,0 +1,1664 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 502804166483968, + "liveNow": false, + "panels": [ + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "mappings": [ + { + "options": { + "0": { + "color": "red", + "text": "NO OK" + }, + "1": { + "color": "green", + "text": "OK" + } + }, + "type": "value" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + } + ] + }, + "unit": "none" + } + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 0, + "y": 0 + }, + "id": 1, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "13.0.1+security-01", + "targets": [ + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "\n SELECT\n CASE\n WHEN age_s < 30 AND comm_ok = true AND COALESCE(sample_valid, true) = true THEN 1\n ELSE 0\n END AS value\n FROM mv_reports_ucepsa_demo.machine_state_latest_multi\n WHERE machine_id = '$machine_id'\n ", + "refId": "A" + } + ], + "title": "Comunicación", + "type": "stat" + }, + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + } + ] + }, + "unit": "s" + } + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 6, + "y": 0 + }, + "id": 2, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "13.0.1+security-01", + "targets": [ + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "\n SELECT age_s AS value\n FROM mv_reports_ucepsa_demo.machine_state_latest_multi\n WHERE machine_id = '$machine_id'\n ", + "refId": "A" + } + ], + "title": "Edad último dato", + "type": "stat" + }, + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "mappings": [ + { + "options": { + "0": { + "color": "yellow", + "text": "PARADA" + }, + "1": { + "color": "green", + "text": "EN MARCHA" + } + }, + "type": "value" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + } + ] + }, + "unit": "none" + } + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 12, + "y": 0 + }, + "id": 3, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "13.0.1+security-01", + "targets": [ + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "\n SELECT\n CASE WHEN machine_running = true THEN 1 ELSE 0 END AS value\n FROM mv_reports_ucepsa_demo.machine_state_latest_multi\n WHERE machine_id = '$machine_id'\n ", + "refId": "A" + } + ], + "title": "Estado máquina", + "type": "stat" + }, + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + } + ] + }, + "unit": "cpm" + } + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 18, + "y": 0 + }, + "id": 4, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "13.0.1+security-01", + "targets": [ + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "\n SELECT cycle_rate_ppm AS value\n FROM mv_reports_ucepsa_demo.machine_state_latest_multi\n WHERE machine_id = '$machine_id'\n ", + "refId": "A" + } + ], + "title": "Ciclos/min", + "type": "stat" + }, + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + } + ] + }, + "unit": "kwatt" + } + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 0, + "y": 4 + }, + "id": 5, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "13.0.1+security-01", + "targets": [ + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "\n SELECT power_total_kw AS value\n FROM mv_reports_ucepsa_demo.machine_state_latest_multi\n WHERE machine_id = '$machine_id'\n ", + "refId": "A" + } + ], + "title": "Potencia total", + "type": "stat" + }, + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + } + ] + }, + "unit": "kwatth" + } + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 6, + "y": 4 + }, + "id": 6, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "13.0.1+security-01", + "targets": [ + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "\n SELECT import_kwh AS value\n FROM mv_reports_ucepsa_demo.machine_state_latest_multi\n WHERE machine_id = '$machine_id'\n ", + "refId": "A" + } + ], + "title": "Energía importada", + "type": "stat" + }, + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + } + ] + }, + "unit": "hertz" + } + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 12, + "y": 4 + }, + "id": 7, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "13.0.1+security-01", + "targets": [ + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "\n SELECT frequency_hz AS value\n FROM mv_reports_ucepsa_demo.machine_state_latest_multi\n WHERE machine_id = '$machine_id'\n ", + "refId": "A" + } + ], + "title": "Frecuencia", + "type": "stat" + }, + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + } + ] + }, + "unit": "none" + } + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 18, + "y": 4 + }, + "id": 8, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "13.0.1+security-01", + "targets": [ + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "\n SELECT cycle_pulse_count AS value\n FROM mv_reports_ucepsa_demo.machine_state_latest_multi\n WHERE machine_id = '$machine_id'\n ", + "refId": "A" + } + ], + "title": "Pulsos acumulados", + "type": "stat" + }, + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 5, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "watt" + } + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 8 + }, + "id": 9, + "options": { + "annotations": { + "clustering": -1, + "multiLane": false + }, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "13.0.1+security-01", + "targets": [ + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "time_series", + "rawQuery": true, + "rawSql": "\n SELECT\n ts AS \"time\",\n active_power_l1_w AS \"L1 W\",\n active_power_l2_w AS \"L2 W\",\n active_power_l3_w AS \"L3 W\",\n active_power_w AS \"Total W\"\n FROM mv_reports_ucepsa_demo.energy_3phase_timeseries\n WHERE $__timeFilter(ts)\n AND site = 'ucepsa_onpremise'\n AND machine_id = '$machine_id'\n ORDER BY ts\n ", + "refId": "A" + } + ], + "title": "Potencia por fase", + "type": "timeseries" + }, + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 5, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "volt" + } + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 8 + }, + "id": 10, + "options": { + "annotations": { + "clustering": -1, + "multiLane": false + }, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "13.0.1+security-01", + "targets": [ + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "time_series", + "rawQuery": true, + "rawSql": "\n SELECT\n ts AS \"time\",\n voltage_l1_v AS \"L1 V\",\n voltage_l2_v AS \"L2 V\",\n voltage_l3_v AS \"L3 V\"\n FROM mv_reports_ucepsa_demo.energy_3phase_timeseries\n WHERE $__timeFilter(ts)\n AND site = 'ucepsa_onpremise'\n AND machine_id = '$machine_id'\n ORDER BY ts\n ", + "refId": "A" + } + ], + "title": "Tensiones L1/L2/L3", + "type": "timeseries" + }, + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 5, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "amp" + } + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 16 + }, + "id": 11, + "options": { + "annotations": { + "clustering": -1, + "multiLane": false + }, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "13.0.1+security-01", + "targets": [ + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "time_series", + "rawQuery": true, + "rawSql": "\n SELECT\n ts AS \"time\",\n current_l1_a AS \"L1 A\",\n current_l2_a AS \"L2 A\",\n current_l3_a AS \"L3 A\"\n FROM mv_reports_ucepsa_demo.energy_3phase_timeseries\n WHERE $__timeFilter(ts)\n AND site = 'ucepsa_onpremise'\n AND machine_id = '$machine_id'\n ORDER BY ts\n ", + "refId": "A" + } + ], + "title": "Corrientes L1/L2/L3", + "type": "timeseries" + }, + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "footer": { + "reducers": [] + }, + "inspect": false + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + } + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 16 + }, + "id": 12, + "options": { + "cellHeight": "sm", + "showHeader": true + }, + "pluginVersion": "13.0.1+security-01", + "targets": [ + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "\n SELECT\n machine_id,\n round(age_s::numeric, 1) AS age_s,\n comm_ok,\n sample_valid,\n energy_map,\n machine_running,\n cycle_input_state,\n cycle_pulse_count,\n wise_cycle_counter_raw,\n round(voltage_l1_v::numeric, 2) AS v_l1,\n round(voltage_l2_v::numeric, 2) AS v_l2,\n round(voltage_l3_v::numeric, 2) AS v_l3,\n round(current_l1_a::numeric, 3) AS i_l1,\n round(current_l2_a::numeric, 3) AS i_l2,\n round(current_l3_a::numeric, 3) AS i_l3,\n round(power_total_kw::numeric, 4) AS kw_total,\n digital_inputs\n FROM mv_reports_ucepsa_demo.machine_state_latest_multi\n WHERE machine_id = '$machine_id'\n ", + "refId": "A" + } + ], + "title": "Última lectura trifásica", + "type": "table" + }, + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "footer": { + "reducers": [] + }, + "inspect": false + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + } + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 24 + }, + "id": 13, + "options": { + "cellHeight": "sm", + "showHeader": true + }, + "pluginVersion": "13.0.1+security-01", + "targets": [ + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\n machine_id AS \"Máquina\",\n order_ref AS \"Orden\",\n product_name AS \"Producto\",\n odoo_state AS \"Estado Odoo\",\n order_present AS \"Orden presente\",\n order_coherent AS \"Coherente\",\n waiting_count AS \"Waiting\",\n ready_count AS \"Ready\",\n progress_count AS \"Progress\",\n machine_running AS \"Máquina en marcha\",\n cycle_rate_ppm AS \"Ciclos/min\",\n round(order_age_s::numeric, 1) AS \"Edad Odoo s\",\n round(telemetry_age_s::numeric, 1) AS \"Edad telemetría s\",\n edge_odoo_status AS \"Diagnóstico\",\n reason AS \"Motivo\"\nFROM mv_reports_ucepsa_demo.odoo_order_state_machine_grafana\nWHERE machine_id = '$machine_id';", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Odoo vs máquina", + "type": "table" + }, + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "footer": { + "reducers": [] + }, + "inspect": false + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + } + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 24 + }, + "id": 14, + "options": { + "cellHeight": "sm", + "showHeader": true + }, + "pluginVersion": "13.0.1+security-01", + "targets": [ + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "\n SELECT\n priority_rank,\n priority_score,\n priority_level,\n machine_id,\n order_ref,\n loss_type,\n severity,\n duration_min,\n cost_eur,\n status,\n description,\n recommended_action\n FROM mv_reports_ucepsa_demo.loss_priority_board\n WHERE machine_id = '$machine_id'\n ORDER BY priority_rank\n ", + "refId": "A" + } + ], + "title": "Loss Intelligence", + "type": "table" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 32 + }, + "id": 300, + "panels": [], + "title": "Informe de paros", + "type": "row" + }, + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 1 + }, + { + "color": "red", + "value": 5 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 0, + "y": 33 + }, + "id": 301, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "targets": [ + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "\n SELECT COUNT(*) AS value\n FROM mv_reports_ucepsa_demo.machine_stops_grafana\n WHERE machine_id = '$machine_id'\n AND status = 'OPEN'\n ", + "refId": "A" + } + ], + "title": "Paros abiertos", + "type": "stat" + }, + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 1 + }, + { + "color": "red", + "value": 5 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 4, + "y": 33 + }, + "id": 302, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "targets": [ + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "\n SELECT COUNT(*) AS value\n FROM mv_reports_ucepsa_demo.machine_stops_grafana\n WHERE machine_id = '$machine_id'\n AND classification_status = 'PENDING'\n ", + "refId": "A" + } + ], + "title": "Pendientes clasificar", + "type": "stat" + }, + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 1 + }, + { + "color": "red", + "value": 5 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 8, + "y": 33 + }, + "id": 303, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "targets": [ + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "\n SELECT COUNT(*) AS value\n FROM mv_reports_ucepsa_demo.machine_stops_grafana\n WHERE machine_id = '$machine_id'\n AND $__timeFilter(started_at)\n ", + "refId": "A" + } + ], + "title": "Nº paros en rango", + "type": "stat" + }, + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 1 + }, + { + "color": "red", + "value": 5 + } + ] + }, + "unit": "min" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 12, + "y": 33 + }, + "id": 304, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "targets": [ + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "\n SELECT COALESCE(SUM(duration_min), 0) AS value\n FROM mv_reports_ucepsa_demo.machine_stops_grafana\n WHERE machine_id = '$machine_id'\n AND $__timeFilter(started_at)\n ", + "refId": "A" + } + ], + "title": "Minutos paro en rango", + "type": "stat" + }, + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 1 + }, + { + "color": "red", + "value": 5 + } + ] + }, + "unit": "currencyEUR" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 16, + "y": 33 + }, + "id": 305, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "targets": [ + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "\n SELECT COALESCE(SUM(cost_eur), 0) AS value\n FROM mv_reports_ucepsa_demo.machine_stops_grafana\n WHERE machine_id = '$machine_id'\n AND $__timeFilter(started_at)\n ", + "refId": "A" + } + ], + "title": "Coste paros en rango", + "type": "stat" + }, + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 1 + }, + { + "color": "red", + "value": 5 + } + ] + }, + "unit": "min" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 20, + "y": 33 + }, + "id": 306, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "targets": [ + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "\n SELECT COALESCE(AVG(duration_min), 0) AS value\n FROM mv_reports_ucepsa_demo.machine_stops_grafana\n WHERE machine_id = '$machine_id'\n AND $__timeFilter(started_at)\n ", + "refId": "A" + } + ], + "title": "Duración media en rango", + "type": "stat" + }, + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "unit": "min" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 37 + }, + "id": 307, + "options": { + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "orientation": "horizontal", + "showValue": "always", + "xField": "Causa" + }, + "targets": [ + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "\n SELECT\n cause_name AS \"Causa\",\n ROUND(SUM(duration_min)::numeric, 2) AS \"Minutos\"\n FROM mv_reports_ucepsa_demo.machine_stops_grafana\n WHERE machine_id = '$machine_id'\n AND classification_status = 'CLASSIFIED'\n AND status = 'CLOSED'\n GROUP BY cause_name\n ORDER BY SUM(duration_min) DESC\n LIMIT 10\n ", + "refId": "A" + } + ], + "title": "Pareto causas clasificadas por minutos", + "type": "barchart" + }, + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "custom": { + "drawStyle": "line", + "fillOpacity": 5, + "lineInterpolation": "linear", + "lineWidth": 1, + "showPoints": "never" + }, + "unit": "min" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 37 + }, + "id": 308, + "options": { + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "time_series", + "rawQuery": true, + "rawSql": "\n SELECT\n date_trunc('hour', started_at) AS \"time\",\n SUM(duration_min) AS \"Minutos de paro\"\n FROM mv_reports_ucepsa_demo.machine_stops_grafana\n WHERE machine_id = '$machine_id'\n AND $__timeFilter(started_at)\n GROUP BY 1\n ORDER BY 1\n ", + "refId": "A" + } + ], + "title": "Minutos de paro por hora", + "type": "timeseries" + }, + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 24, + "x": 0, + "y": 45 + }, + "id": 309, + "options": { + "cellHeight": "sm", + "showHeader": true + }, + "targets": [ + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "\n SELECT\n id AS \"ID\",\n started_at AS \"Inicio\",\n ROUND(duration_min::numeric, 2) AS \"Min\",\n cost_eur AS \"Coste €\",\n status AS \"Estado\",\n classification_status AS \"Clasificación\",\n cause_name AS \"Causa\",\n operator_note AS \"Nota operario\"\n FROM mv_reports_ucepsa_demo.machine_stops_grafana\n WHERE machine_id = '$machine_id'\n AND classification_status = 'PENDING'\n ORDER BY started_at DESC\n ", + "refId": "A" + } + ], + "title": "Paros pendientes de clasificación", + "type": "table" + }, + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 24, + "x": 0, + "y": 51 + }, + "id": 310, + "options": { + "cellHeight": "sm", + "showHeader": true + }, + "targets": [ + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "\n SELECT\n id AS \"ID\",\n machine_id AS \"Máquina\",\n started_at AS \"Inicio\",\n ended_at AS \"Fin\",\n duration_min AS \"Min\",\n cost_eur AS \"Coste €\",\n stop_status_label AS \"Estado\",\n classification_status AS \"Clasificación\",\n cause_code AS \"Código causa\",\n cause_name AS \"Causa\",\n start_event_name AS \"Evento inicio\",\n end_event_name AS \"Evento fin\",\n operator_note AS \"Nota operario\"\n FROM mv_reports_ucepsa_demo.machine_stops_grafana\n WHERE machine_id = '$machine_id'\n AND $__timeFilter(started_at)\n ORDER BY started_at DESC\n LIMIT 100\n ", + "refId": "A" + } + ], + "title": "Detalle completo de paros", + "type": "table" + } + ], + "preload": false, + "refresh": "5s", + "schemaVersion": 42, + "tags": [ + "mesavault", + "ucepsa", + "edge-oee" + ], + "templating": { + "list": [ + { + "allowCustomValue": true, + "current": { + "text": "CORT-00", + "value": "CORT-00" + }, + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "hide": 0, + "includeAll": false, + "label": "Máquina", + "multi": false, + "name": "machine_id", + "options": [], + "query": "SELECT machine_id FROM mv_reports_ucepsa_demo.machine_state_latest_multi ORDER BY machine_id", + "refresh": 1, + "regex": "", + "regexApplyTo": "value", + "skipUrlSync": false, + "sort": 1, + "type": "query" + } + ] + }, + "time": { + "from": "now-30m", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ] + }, + "timezone": "browser", + "title": "MESAVAULT Edge-OEE — Máquina", + "uid": "ucepsa-edge-oee-machine", + "version": 5 +} diff --git a/ucepsa/edge-oee-demo/grafana/export/ucepsa-edge-oee-local_2026-05-27_20-19-48.json b/ucepsa/edge-oee-demo/grafana/export/ucepsa-edge-oee-local_2026-05-27_20-19-48.json new file mode 100644 index 0000000..ad519fa --- /dev/null +++ b/ucepsa/edge-oee-demo/grafana/export/ucepsa-edge-oee-local_2026-05-27_20-19-48.json @@ -0,0 +1,6179 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 1436127706652672, + "liveNow": true, + "panels": [ + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 75, + "panels": [ + { + "gridPos": { + "h": 5, + "w": 16, + "x": 0, + "y": 1 + }, + "id": 3, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "## Odoo dice qué orden está en curso. MESAVAULT muestra qué hizo realmente la máquina.\n\nLa demo convierte eventos de máquina en pérdidas, coste económico y acciones concretas:\n\n**dato → pérdida → euros → acción**", + "mode": "markdown" + }, + "pluginVersion": "12.3.2", + "targets": [], + "title": "Odoo vs máquina", + "type": "text" + } + ], + "title": "Mensaje de negocio", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 1 + }, + "id": 76, + "panels": [ + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "min": -1, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "horas" + } + }, + "gridPos": { + "h": 6, + "w": 4, + "x": 0, + "y": 2 + }, + "id": 5, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n value AS \"Horas analizadas\"\r\nFROM mv_hot.edge_oee_kpi_cards_demo\r\nWHERE kpi = 'Horas analizadas';", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Horas analizadas", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "horas" + } + }, + "gridPos": { + "h": 6, + "w": 4, + "x": 4, + "y": 2 + }, + "id": 6, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n value AS \"Pérdidas clasificadas\"\r\nFROM mv_hot.edge_oee_kpi_cards_demo\r\nWHERE kpi = 'Pérdidas clasificadas';", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Pérdidas clasificadas", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "currencyEUR" + } + }, + "gridPos": { + "h": 6, + "w": 4, + "x": 8, + "y": 2 + }, + "id": 7, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n value AS \"Coste estimado\"\r\nFROM mv_hot.edge_oee_kpi_cards_demo\r\nWHERE kpi = 'Coste estimado de pérdidas';", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Coste estimado de pérdidas", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + } + }, + "gridPos": { + "h": 6, + "w": 4, + "x": 12, + "y": 2 + }, + "id": 8, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n value AS \"Discrepancias\"\r\nFROM mv_hot.edge_oee_kpi_cards_demo\r\nWHERE kpi = 'Órdenes con discrepancia Odoo/máquina';", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Discrepancias Odoo/máquina", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "fillOpacity": 80, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineWidth": 1, + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + } + ] + }, + "unit": "currencyEUR" + } + }, + "gridPos": { + "h": 10, + "w": 16, + "x": 0, + "y": 686 + }, + "id": 9, + "options": { + "barRadius": 0, + "barWidth": 0.8, + "fullHighlight": false, + "groupWidth": 0.7, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "orientation": "auto", + "showValue": "auto", + "stacking": "none", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + }, + "xField": "Causa", + "xTickLabelRotation": 0, + "xTickLabelSpacing": 0 + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n cause AS \"Causa\",\r\n monthly_cost_eur AS \"Coste €/mes\"\r\nFROM mv_hot.edge_oee_pareto_demo\r\nORDER BY ranking;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Pareto económico de pérdidas", + "type": "barchart" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "footer": { + "reducers": [] + }, + "inspect": false + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Ranking" + }, + "properties": [ + { + "id": "custom.width", + "value": 67 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Causa" + }, + "properties": [ + { + "id": "custom.width", + "value": 216 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Horas/mes" + }, + "properties": [ + { + "id": "custom.width", + "value": 87 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Coste €/mes" + }, + "properties": [ + { + "id": "custom.width", + "value": 100 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Acción recomendada" + }, + "properties": [ + { + "id": "custom.width", + "value": 410 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Esfuerzo" + }, + "properties": [ + { + "id": "custom.width", + "value": 112 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Prioridad" + }, + "properties": [ + { + "id": "custom.width", + "value": 106 + } + ] + } + ] + }, + "gridPos": { + "h": 3, + "w": 16, + "x": 0, + "y": 696 + }, + "id": 10, + "options": { + "cellHeight": "sm", + "showHeader": true, + "sortBy": [] + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n ranking AS \"Ranking\",\r\n cause AS \"Causa\",\r\n hours_month AS \"Horas/mes\",\r\n monthly_cost_eur AS \"Coste €/mes\",\r\n recommended_action AS \"Acción recomendada\",\r\n effort AS \"Esfuerzo\",\r\n priority AS \"Prioridad\",\r\n machine_focus AS \"Máquina foco\"\r\nFROM mv_hot.edge_oee_pareto_demo\r\nORDER BY ranking;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "", + "type": "table" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-GrYlRd" + }, + "custom": { + "axisPlacement": "auto", + "fillOpacity": 70, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineWidth": 0, + "spanNulls": false + }, + "mappings": [ + { + "options": { + "0": { + "index": 0, + "text": "Sin evento" + }, + "10": { + "index": 1, + "text": "Orden activa en Odoo" + }, + "20": { + "index": 2, + "text": "Preparación / búsqueda de bobina" + }, + "30": { + "index": 3, + "text": "Marcha real" + }, + "40": { + "index": 4, + "text": "Microparos" + }, + "50": { + "index": 5, + "text": "Cambio bobina" + }, + "60": { + "index": 6, + "text": "Paro" + }, + "90": { + "index": 7, + "text": "Otro evento" + } + }, + "type": "value" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "purple", + "value": 0 + } + ] + } + } + }, + "gridPos": { + "h": 5, + "w": 16, + "x": 0, + "y": 699 + }, + "id": 11, + "options": { + "alignValue": "left", + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "mergeValues": true, + "rowHeight": 0.9, + "showValue": "auto", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "time_series", + "rawQuery": true, + "rawSql": "WITH odoo AS (\r\n SELECT\r\n o.odoo_start AS time,\r\n 'Odoo / OF00481' AS metric,\r\n 10::numeric AS value\r\n FROM mv_hot.edge_oee_orders_demo o\r\n WHERE o.order_id = 'OF00481'\r\n\r\n UNION ALL\r\n\r\n SELECT\r\n o.odoo_end + interval '1 second' AS time,\r\n 'Odoo / OF00481' AS metric,\r\n 0::numeric AS value\r\n FROM mv_hot.edge_oee_orders_demo o\r\n WHERE o.order_id = 'OF00481'\r\n),\r\n\r\nmachine_start AS (\r\n SELECT\r\n e.start_time AS time,\r\n 'Cortadora 0 / realidad máquina' AS metric,\r\n CASE\r\n WHEN e.event_type = 'PREPARATION' THEN 20\r\n WHEN e.event_type = 'RUNNING' THEN 30\r\n WHEN e.event_type = 'MICROSTOP' THEN 40\r\n WHEN e.event_type = 'STOP' AND e.cause = 'Cambio bobina' THEN 50\r\n WHEN e.event_type = 'STOP' THEN 60\r\n ELSE 90\r\n END::numeric AS value\r\n FROM mv_hot.edge_oee_events_demo e\r\n WHERE e.order_id = 'OF00481'\r\n),\r\n\r\nmachine_end AS (\r\n SELECT\r\n e.end_time + interval '1 second' AS time,\r\n 'Cortadora 0 / realidad máquina' AS metric,\r\n 0::numeric AS value\r\n FROM mv_hot.edge_oee_events_demo e\r\n WHERE e.order_id = 'OF00481'\r\n)\r\n\r\nSELECT\r\n time AS \"time\",\r\n metric,\r\n value\r\nFROM (\r\n SELECT * FROM odoo\r\n UNION ALL\r\n SELECT * FROM machine_start\r\n UNION ALL\r\n SELECT * FROM machine_end\r\n) q\r\nORDER BY \"time\", metric;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "OF00481 — Odoo inicia orden, máquina arranca 22 min después", + "type": "state-timeline" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "footer": { + "reducers": [] + }, + "inspect": false + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Indicador" + }, + "properties": [ + { + "id": "custom.width", + "value": 279 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Valor" + }, + "properties": [ + { + "id": "custom.width", + "value": 50 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Unidad" + }, + "properties": [ + { + "id": "custom.width", + "value": 67 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Estado" + }, + "properties": [ + { + "id": "custom.width", + "value": 68 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Qué está mal" + }, + "properties": [ + { + "id": "custom.width", + "value": 397 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Impacto probable" + }, + "properties": [ + { + "id": "custom.width", + "value": 489 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Qué revisar / acción" + }, + "properties": [ + { + "id": "custom.width", + "value": 388 + } + ] + } + ] + }, + "gridPos": { + "h": 12, + "w": 16, + "x": 0, + "y": 704 + }, + "id": 12, + "options": { + "cellHeight": "sm", + "showHeader": true + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n \"Indicador\",\r\n \"Valor\",\r\n \"Unidad\",\r\n \"Estado\",\r\n \"Qué está mal\",\r\n \"Impacto probable\",\r\n \"Qué revisar / acción\"\r\nFROM mv_hot.edge_oee_data_quality_dashboard_demo\r\nORDER BY sort_order;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Calidad del dato Odoo / máquina", + "type": "table" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "footer": { + "reducers": [] + }, + "inspect": false + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "time" + }, + "properties": [ + { + "id": "custom.width", + "value": 158 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "A-series" + }, + "properties": [ + { + "id": "custom.width", + "value": 69 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Causa" + }, + "properties": [ + { + "id": "custom.width", + "value": 177 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Impacto estimado €/mes" + }, + "properties": [ + { + "id": "custom.width", + "value": 182 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Esfuerzo" + }, + "properties": [ + { + "id": "custom.width", + "value": 73 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Prioridad" + }, + "properties": [ + { + "id": "custom.width", + "value": 75 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Payback ejemplo meses" + }, + "properties": [ + { + "id": "custom.width", + "value": 177 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Inversión ejemplo €" + }, + "properties": [ + { + "id": "custom.width", + "value": 146 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Acción recomendada" + }, + "properties": [ + { + "id": "custom.width", + "value": 410 + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 16, + "x": 0, + "y": 716 + }, + "id": 13, + "options": { + "cellHeight": "sm", + "showHeader": true, + "sortBy": [] + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n \"Causa\",\r\n \"Impacto estimado €/mes\",\r\n \"Esfuerzo\",\r\n \"Prioridad\",\r\n \"Acción recomendada\",\r\n \"Inversión ejemplo €\",\r\n \"Payback ejemplo meses\"\r\nFROM mv_hot.edge_oee_actions_demo;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Acciones recomendadas", + "type": "table" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "footer": { + "reducers": [] + }, + "inspect": false + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Orden" + }, + "properties": [ + { + "id": "custom.width", + "value": 76 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Máquina" + }, + "properties": [ + { + "id": "custom.width", + "value": 93 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Producto" + }, + "properties": [ + { + "id": "custom.width", + "value": 115 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Reclamación" + }, + "properties": [ + { + "id": "custom.width", + "value": 99 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Microparos" + }, + "properties": [ + { + "id": "custom.width", + "value": 90 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Merma %" + }, + "properties": [ + { + "id": "custom.width", + "value": 78 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Energía vs media %" + }, + "properties": [ + { + "id": "custom.width", + "value": 147 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Interpretación" + }, + "properties": [ + { + "id": "custom.width", + "value": 889 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Acción recomendada" + }, + "properties": [ + { + "id": "custom.width", + "value": 949 + } + ] + } + ] + }, + "gridPos": { + "h": 9, + "w": 16, + "x": 0, + "y": 726 + }, + "id": 14, + "options": { + "cellHeight": "sm", + "showHeader": true, + "sortBy": [] + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n order_id AS \"Orden\",\r\n machine_name AS \"Máquina\",\r\n product AS \"Producto\",\r\n claim AS \"Reclamación\",\r\n sealing_adjustment_stops AS \"Paros ajuste sellado\",\r\n sealing_adjustment_min AS \"Min ajuste sellado\",\r\n microstops_count AS \"Microparos\",\r\n scrap_pct AS \"Merma %\",\r\n energy_vs_avg_pct AS \"Energía vs media %\",\r\n interpretation AS \"Interpretación\",\r\n recommended_action AS \"Acción recomendada\"\r\nFROM mv_hot.edge_oee_quality_case_demo;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Caso calidad — Sellado débil OF00493", + "type": "table" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "kwatt" + } + }, + "gridPos": { + "h": 8, + "w": 16, + "x": 0, + "y": 735 + }, + "id": 15, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n ts AS \"time\",\r\n power_kw AS \"Potencia kW\"\r\nFROM mv_hot.edge_oee_energy_demo\r\nWHERE machine_id = 'C2'\r\nORDER BY ts;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Energía durante OF00493 — Cortadora 2", + "type": "timeseries" + } + ], + "title": "KPIs de dirección", + "type": "row" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 2 + }, + "id": 77, + "title": "OEE — CORTADORA 01", + "type": "row" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "red", + "value": 0 + }, + { + "color": "green", + "value": 80 + } + ] + }, + "unit": "percent" + } + }, + "gridPos": { + "h": 4, + "w": 9, + "x": 0, + "y": 3 + }, + "id": 39, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "13.0.1+security-01", + "targets": [ + { + "dataset": "mesavault_hot", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n oee_pct AS \"OEE %\",\r\n availability_pct AS \"Disponibilidad %\",\r\n performance_pct AS \"Rendimiento %\",\r\n quality_pct AS \"Calidad %\"\r\nFROM mv_reports_ucepsa_demo.oee_kpis(\r\n $__timeFrom()::timestamptz,\r\n $__timeTo()::timestamptz\r\n);", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "OEE", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 1, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "text", + "value": 0 + } + ] + } + } + }, + "gridPos": { + "h": 4, + "w": 10, + "x": 9, + "y": 3 + }, + "id": 31, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "13.0.1+security-01", + "targets": [ + { + "dataset": "mesavault_hot", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n running_min AS \"Min marcha\",\r\n stop_min AS \"Min paro\",\r\n actual_cycles AS \"Ciclos\"\r\nFROM mv_reports_ucepsa_demo.oee_operational_data(\r\n $__timeFrom()::timestamptz,\r\n $__timeTo()::timestamptz\r\n);", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Datos operativos", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "fillOpacity": 80, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineWidth": 1, + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + } + }, + "gridPos": { + "h": 9, + "w": 19, + "x": 0, + "y": 7 + }, + "id": 37, + "options": { + "barRadius": 0, + "barWidth": 0.97, + "fullHighlight": false, + "groupWidth": 0.7, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "orientation": "auto", + "showValue": "auto", + "stacking": "none", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + }, + "xTickLabelRotation": 0, + "xTickLabelSpacing": 0 + }, + "pluginVersion": "13.0.1+security-01", + "targets": [ + { + "dataset": "mesavault_hot", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n COALESCE(cause_name, 'Pendiente de clasificar') AS \"Causa\",\r\n ROUND(SUM(cost_eur)::numeric, 2) AS \"Coste €\",\r\n ROUND(SUM(duration_min)::numeric, 2) AS \"Minutos\"\r\nFROM mv_reports_ucepsa_demo.stops_recent\r\nWHERE machine_id = 'CORT-01'\r\n AND $__timeFilter(started_at)\r\nGROUP BY COALESCE(cause_name, 'Pendiente de clasificar')\r\nORDER BY \"Coste €\" DESC;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Pérdida real por causa", + "type": "barchart" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + } + }, + "gridPos": { + "h": 6, + "w": 19, + "x": 0, + "y": 16 + }, + "id": 33, + "options": { + "annotations": { + "clustering": -1, + "multiLane": false + }, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "13.0.1+security-01", + "targets": [ + { + "dataset": "mesavault_hot", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n ts AS \"time\",\r\n cycle_rate_ppm AS \"Ciclos/min\",\r\n 10.0 AS \"Objetivo ciclos/min\"\r\nFROM mv_hot.edge_oee_live_telemetry\r\nWHERE $__timeFilter(ts)\r\n AND asset = 'revpi_oee_node_01'\r\n AND cycle_rate_ppm IS NOT NULL\r\nORDER BY ts;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Velocidad real — ciclos/min", + "type": "timeseries" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "footer": { + "reducers": [] + }, + "inspect": false + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "ID" + }, + "properties": [ + { + "id": "custom.width", + "value": 50 + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 19, + "x": 0, + "y": 22 + }, + "id": 36, + "options": { + "cellHeight": "sm", + "showHeader": true, + "sortBy": [] + }, + "pluginVersion": "13.0.1+security-01", + "targets": [ + { + "dataset": "mesavault_hot", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n id AS \"ID\",\r\n machine_id AS \"Máquina\",\r\n started_at AS \"Inicio\",\r\n ended_at AS \"Fin\",\r\n status AS \"Estado\",\r\n classification_status AS \"Clasificación\",\r\n COALESCE(cause_name, 'Pendiente') AS \"Causa\",\r\n operator_note AS \"Nota\",\r\n duration_min AS \"Duración min\",\r\n cost_eur AS \"Coste €\"\r\nFROM mv_hot.edge_oee_stops_recent\r\nWHERE $__timeFilter(started_at)\r\nORDER BY started_at DESC\r\nLIMIT 20;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Últimos paros clasificados / pendientes", + "type": "table" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 28 + }, + "id": 78, + "title": "Odoo vs máquina — Cortadora 01", + "type": "row" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "mappings": [ + { + "options": { + "0": { + "color": "green", + "index": 0, + "text": "OK" + }, + "2": { + "color": "red", + "index": 1, + "text": "Discrepancia" + }, + "3": { + "color": "purple", + "index": 2, + "text": "Sin Telemetría" + } + }, + "type": "value" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + } + ] + } + } + }, + "gridPos": { + "h": 5, + "w": 4, + "x": 0, + "y": 29 + }, + "id": 57, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "13.0.1+security-01", + "targets": [ + { + "dataset": "mesavault_hot", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n CASE\r\n WHEN status_check IN ('OK', 'ORDEN_PREPARADA_MAQUINA_PARADA') THEN 0\r\n WHEN status_check = 'SIN_TELEMETRIA' THEN 3\r\n ELSE 2\r\n END AS \"Estado cruce\"\r\nFROM mv_reports_ucepsa_demo.odoo_vs_machine_demo;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Discrepancias Odoo/máquina", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "noValue": "No iniciada", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "m" + } + }, + "gridPos": { + "h": 5, + "w": 4, + "x": 4, + "y": 29 + }, + "id": 58, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "13.0.1+security-01", + "targets": [ + { + "dataset": "mesavault_hot", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n CASE\r\n WHEN odoo_is_started = true THEN start_delay_min\r\n ELSE NULL\r\n END AS \"Retraso inicio real\"\r\nFROM mv_reports_ucepsa_demo.odoo_vs_machine_demo;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Retraso inicio real", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 2, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 0.1 + } + ] + } + } + }, + "gridPos": { + "h": 5, + "w": 7, + "x": 0, + "y": 34 + }, + "id": 59, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "13.0.1+security-01", + "targets": [ + { + "dataset": "mesavault_hot", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n stop_min AS \"Min paro en orden\",\r\n stop_cost_eur AS \"Coste paro en orden\"\r\nFROM mv_reports_ucepsa_demo.odoo_vs_machine_demo;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Paros dentro de orden", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "text", + "value": 0 + } + ] + } + } + }, + "gridPos": { + "h": 5, + "w": 9, + "x": 7, + "y": 34 + }, + "id": 60, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "13.0.1+security-01", + "targets": [ + { + "dataset": "mesavault_hot", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n real_cycles AS \"Ciclos reales\",\r\n planned_cycles AS \"Ciclos planificados\",\r\n cycle_completion_pct AS \"% avance ciclos\"\r\nFROM mv_reports_ucepsa_demo.odoo_vs_machine_demo;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Ciclos reales vs planificados", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "footer": { + "reducers": [] + }, + "inspect": false + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Orden Odoo" + }, + "properties": [ + { + "id": "custom.width", + "value": 112 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Producto" + }, + "properties": [ + { + "id": "custom.width", + "value": 260 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Estado" + }, + "properties": [ + { + "id": "custom.width", + "value": 72 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Máquina" + }, + "properties": [ + { + "id": "custom.width", + "value": 72 + } + ] + } + ] + }, + "gridPos": { + "h": 4, + "w": 24, + "x": 0, + "y": 39 + }, + "id": 56, + "options": { + "cellHeight": "sm", + "showHeader": true, + "sortBy": [] + }, + "pluginVersion": "13.0.1+security-01", + "targets": [ + { + "dataset": "mesavault_hot", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n odoo_order_ref AS \"Orden Odoo\",\r\n product AS \"Producto\",\r\n odoo_state AS \"Estado\",\r\n machine_id AS \"Máquina\",\r\n CASE\r\n WHEN odoo_is_started = true THEN to_char(odoo_start, 'YYYY-MM-DD HH24:MI:SS')\r\n ELSE 'No iniciada'\r\n END AS \"Inicio Odoo\"\r\nFROM mv_reports_ucepsa_demo.odoo_current_order_demo;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Orden Odoo seleccionada", + "type": "table" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "footer": { + "reducers": [] + }, + "inspect": false + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Orden" + }, + "properties": [ + { + "id": "custom.width", + "value": 115 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Estado Odoo" + }, + "properties": [ + { + "id": "custom.width", + "value": 100 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Edad telemetría s" + }, + "properties": [ + { + "id": "custom.width", + "value": 131 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Estado máquina" + }, + "properties": [ + { + "id": "custom.width", + "value": 143 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Diagnóstico" + }, + "properties": [ + { + "id": "custom.width", + "value": 330 + } + ] + } + ] + }, + "gridPos": { + "h": 4, + "w": 24, + "x": 0, + "y": 43 + }, + "id": 62, + "options": { + "cellHeight": "sm", + "showHeader": true, + "sortBy": [ + { + "desc": false, + "displayName": "Estado máquina" + } + ] + }, + "pluginVersion": "13.0.1+security-01", + "targets": [ + { + "dataset": "mesavault_hot", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n odoo_order_ref AS \"Orden\",\r\n odoo_state AS \"Estado Odoo\",\r\n CASE\r\n WHEN machine_running THEN 'En marcha'\r\n ELSE 'Parada'\r\n END AS \"Estado máquina\",\r\n telemetry_age_s AS \"Edad telemetría s\",\r\n status_check AS \"Diagnóstico\"\r\nFROM mv_reports_ucepsa_demo.odoo_vs_machine_demo;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Estado Odoo / realidad", + "type": "table" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "footer": { + "reducers": [] + }, + "inspect": false + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Orden" + }, + "properties": [ + { + "id": "custom.width", + "value": 115 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Hora chequeo" + }, + "properties": [ + { + "id": "custom.width", + "value": 155 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Máquina" + }, + "properties": [ + { + "id": "custom.width", + "value": 72 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Estado" + }, + "properties": [ + { + "id": "custom.width", + "value": 331 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Descripción" + }, + "properties": [ + { + "id": "custom.width", + "value": 633 + } + ] + } + ] + }, + "gridPos": { + "h": 9, + "w": 24, + "x": 0, + "y": 47 + }, + "id": 61, + "options": { + "cellHeight": "sm", + "showHeader": true + }, + "pluginVersion": "13.0.1+security-01", + "targets": [ + { + "dataset": "mesavault_hot", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n checked_at AS \"Hora chequeo\",\r\n odoo_order_ref AS \"Orden\",\r\n product AS \"Producto\",\r\n machine_id AS \"Máquina\",\r\n status_check AS \"Estado\",\r\n descripcion AS \"Descripción\",\r\n start_delay_min AS \"Retraso min\",\r\n stop_count AS \"Nº paros\",\r\n stop_min AS \"Min paro\",\r\n stop_cost_eur AS \"Coste paro €\",\r\n cycle_completion_pct AS \"% avance ciclos\"\r\nFROM mv_reports_ucepsa_demo.odoo_discrepancies_demo;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Tabla de discrepancias", + "type": "table" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 56 + }, + "id": 79, + "panels": [ + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "fillOpacity": 80, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineWidth": 1, + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "currencyEUR" + } + }, + "gridPos": { + "h": 9, + "w": 11, + "x": 0, + "y": 57 + }, + "id": 66, + "options": { + "barRadius": 0, + "barWidth": 0.97, + "fullHighlight": false, + "groupWidth": 0.7, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "orientation": "auto", + "showValue": "auto", + "stacking": "none", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + }, + "xTickLabelRotation": 0, + "xTickLabelSpacing": 0 + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n odoo_order_ref AS \"Orden\",\r\n stop_cost_eur AS \"Coste paro €\"\r\nFROM mv_reports_ucepsa_demo.odoo_workorders_reality_history\r\nWHERE stop_cost_eur > 0\r\nORDER BY stop_cost_eur DESC\r\nLIMIT 10;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Coste de paros por orden", + "type": "barchart" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "fillOpacity": 80, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineWidth": 1, + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "m" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Min paro" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "yellow", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 9, + "w": 13, + "x": 11, + "y": 57 + }, + "id": 67, + "options": { + "barRadius": 0, + "barWidth": 0.97, + "fullHighlight": false, + "groupWidth": 0.7, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "orientation": "auto", + "showValue": "auto", + "stacking": "none", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + }, + "xTickLabelRotation": 0, + "xTickLabelSpacing": 0 + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n odoo_order_ref AS \"Orden\",\r\n stop_min AS \"Min paro\"\r\nFROM mv_reports_ucepsa_demo.odoo_workorders_reality_history\r\nWHERE stop_min > 0\r\nORDER BY stop_min DESC\r\nLIMIT 10;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Tiempo de paro por orden", + "type": "barchart" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "footer": { + "reducers": [] + }, + "inspect": false + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + } + }, + "gridPos": { + "h": 4, + "w": 24, + "x": 0, + "y": 66 + }, + "id": 63, + "options": { + "cellHeight": "sm", + "showHeader": true + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n odoo_order_ref AS \"Orden\",\r\n product AS \"Producto\",\r\n odoo_state AS \"Estado\",\r\n machine_id AS \"Máquina\",\r\n odoo_start AS \"Inicio Odoo\",\r\n odoo_end AS \"Fin Odoo\",\r\n real_cycles AS \"Ciclos reales\",\r\n planned_cycles AS \"Ciclos planificados\",\r\n cycle_completion_pct AS \"% avance\",\r\n stop_count AS \"Nº paros\",\r\n stop_min AS \"Min paro\",\r\n stop_cost_eur AS \"Coste paro €\",\r\n status_check AS \"Diagnóstico\"\r\nFROM mv_reports_ucepsa_demo.odoo_last_finished_order_demo;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Última orden finalizada", + "type": "table" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "footer": { + "reducers": [] + }, + "inspect": false + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + } + }, + "gridPos": { + "h": 17, + "w": 24, + "x": 0, + "y": 70 + }, + "id": 65, + "options": { + "cellHeight": "sm", + "showHeader": true + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n odoo_order_ref AS \"Orden\",\r\n odoo_state AS \"Estado\",\r\n product AS \"Producto\",\r\n machine_id AS \"Máquina\",\r\n odoo_start AS \"Inicio Odoo\",\r\n odoo_end AS \"Fin Odoo\",\r\n real_cycles AS \"Ciclos reales\",\r\n planned_cycles AS \"Ciclos planificados\",\r\n cycle_completion_pct AS \"% avance\",\r\n stop_count AS \"Nº paros\",\r\n stop_min AS \"Min paro\",\r\n stop_cost_eur AS \"Coste paro €\",\r\n status_check AS \"Diagnóstico\",\r\n descripcion AS \"Descripción\"\r\nFROM mv_reports_ucepsa_demo.odoo_workorders_reality_history\r\nORDER BY COALESCE(odoo_end, odoo_start, snapshot_at) DESC\r\nLIMIT 20;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Histórico de órdenes", + "type": "table" + } + ], + "title": "Histórico Odoo vs máquina — Cortadora 01", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 57 + }, + "id": 80, + "panels": [ + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [ + { + "options": { + "0": { + "color": "red", + "index": 0, + "text": "Parada" + }, + "1": { + "color": "green", + "index": 1, + "text": "En Marcha" + }, + "2": { + "color": "yellow", + "index": 2, + "text": "Desconocido" + }, + "3": { + "color": "semi-dark-purple", + "index": 3, + "text": "Sin telemetría" + } + }, + "type": "value" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + } + }, + "gridPos": { + "h": 6, + "w": 4, + "x": 0, + "y": 58 + }, + "id": 25, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n CASE\r\n WHEN age_s > 120 THEN 3\r\n WHEN machine_running = true THEN 1\r\n WHEN machine_running = false THEN 0\r\n ELSE 2\r\n END AS \"Estado máquina\"\r\nFROM mv_reports_ucepsa_demo.latest_machine_state_fast;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Estado actual máquina demo", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [ + { + "options": { + "": { + "index": 0, + "text": "Sin paro abierto" + } + }, + "type": "value" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "text", + "value": 0 + }, + { + "color": "semi-dark-yellow", + "value": 5 + }, + { + "color": "red", + "value": 15 + } + ] + }, + "unit": "m" + } + }, + "gridPos": { + "h": 6, + "w": 4, + "x": 4, + "y": 58 + }, + "id": 47, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n COALESCE((\r\n SELECT current_duration_min\r\n FROM mv_reports_ucepsa_demo.open_stops\r\n WHERE machine_id = 'CORT-01'\r\n ORDER BY started_at DESC\r\n LIMIT 1\r\n ), 0) AS \"Duración paro actual\";", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Duración paro actual", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "text", + "value": 0 + }, + { + "color": "red", + "value": 1 + } + ] + }, + "unit": "currencyEUR" + } + }, + "gridPos": { + "h": 6, + "w": 4, + "x": 8, + "y": 58 + }, + "id": 48, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n COALESCE((\r\n SELECT current_cost_eur\r\n FROM mv_reports_ucepsa_demo.open_stops\r\n WHERE machine_id = 'CORT-01'\r\n ORDER BY started_at DESC\r\n LIMIT 1\r\n ), 0) AS \"Coste paro actual\";", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Coste paro actual", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "text", + "value": 0 + }, + { + "color": "#EAB839", + "value": 5 + }, + { + "color": "green", + "value": 8 + } + ] + } + } + }, + "gridPos": { + "h": 6, + "w": 4, + "x": 12, + "y": 58 + }, + "id": 49, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n cycle_rate_ppm AS \"Ciclos/min\"\r\nFROM mv_reports_ucepsa_demo.latest_machine_snapshot;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Ciclos/min actual", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "text", + "value": 0 + }, + { + "color": "green", + "value": 1 + } + ] + } + } + }, + "gridPos": { + "h": 6, + "w": 4, + "x": 16, + "y": 58 + }, + "id": 50, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n cycle_pulse_count AS \"Pulsos acumulados\"\r\nFROM mv_reports_ucepsa_demo.latest_machine_snapshot;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Pulsos acumulados", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisPlacement": "hidden", + "fillOpacity": 70, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineWidth": 0, + "spanNulls": false + }, + "mappings": [ + { + "options": { + "0": { + "color": "purple", + "index": 0, + "text": "Sin Telemetría" + }, + "1": { + "color": "green", + "index": 1, + "text": "En marcha" + }, + "2": { + "color": "red", + "index": 2, + "text": "Parada - causa pendiente" + }, + "3": { + "color": "semi-dark-yellow", + "index": 3, + "text": "Parada - causa clasificada" + } + }, + "type": "value" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + } + }, + "gridPos": { + "h": 4, + "w": 24, + "x": 0, + "y": 64 + }, + "id": 51, + "options": { + "alignValue": "left", + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "mergeValues": true, + "rowHeight": 0.9, + "showValue": "auto", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "WITH limits AS (\r\n SELECT\r\n $__timeFrom()::timestamptz AS t_from,\r\n LEAST($__timeTo()::timestamptz, now()) AS t_to,\r\n $__timeTo()::timestamptz AS grafana_to\r\n),\r\nbuckets AS (\r\n SELECT\r\n generate_series(\r\n t_from,\r\n t_to,\r\n interval '10 seconds'\r\n ) AS bucket\r\n FROM limits\r\n),\r\nstates AS (\r\n SELECT\r\n b.bucket AS \"time\",\r\n CASE\r\n -- 0. Si no hay telemetría reciente, el estado de máquina es desconocido.\r\n WHEN NOT EXISTS (\r\n SELECT 1\r\n FROM mv_hot.edge_oee_eastron_readings r\r\n WHERE r.asset = 'revpi_oee_node_01'\r\n AND r.ingest_ts > b.bucket - interval '30 seconds'\r\n AND r.ingest_ts <= b.bucket + interval '1 second'\r\n )\r\n THEN 0\r\n\r\n -- 2/3. Si hay un paro solapando ese bucket, mostrar estado de paro.\r\n WHEN EXISTS (\r\n SELECT 1\r\n FROM mv_reports_ucepsa_demo.stops_recent s\r\n WHERE s.machine_id = 'CORT-01'\r\n AND s.started_at <= b.bucket\r\n AND COALESCE(s.ended_at, now()) > b.bucket\r\n AND s.classification_status = 'PENDING'\r\n )\r\n THEN 2\r\n\r\n WHEN EXISTS (\r\n SELECT 1\r\n FROM mv_reports_ucepsa_demo.stops_recent s\r\n WHERE s.machine_id = 'CORT-01'\r\n AND s.started_at <= b.bucket\r\n AND COALESCE(s.ended_at, now()) > b.bucket\r\n AND s.classification_status = 'CLASSIFIED'\r\n )\r\n THEN 3\r\n\r\n -- 1. Hay telemetría y no hay paro abierto en ese instante.\r\n ELSE 1\r\n END AS \"CORT-01\"\r\n FROM buckets b\r\n),\r\ncutoff AS (\r\n SELECT\r\n t_to + interval '1 second' AS \"time\",\r\n NULL::int AS \"CORT-01\"\r\n FROM limits\r\n WHERE t_to < grafana_to\r\n)\r\nSELECT *\r\nFROM states\r\n\r\nUNION ALL\r\n\r\nSELECT *\r\nFROM cutoff\r\n\r\nORDER BY \"time\";", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Timeline estado máquina CORT-01", + "type": "state-timeline" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisPlacement": "hidden", + "fillOpacity": 70, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineWidth": 0, + "spanNulls": false + }, + "mappings": [ + { + "options": { + "0": { + "color": "red", + "index": 0, + "text": "OFF / Paro" + }, + "1": { + "color": "green", + "index": 1, + "text": "ON / Marcha" + }, + "2": { + "color": "purple", + "index": 2, + "text": "Sin Telemetría" + } + }, + "type": "value" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + } + }, + "gridPos": { + "h": 4, + "w": 24, + "x": 0, + "y": 68 + }, + "id": 54, + "options": { + "alignValue": "left", + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "mergeValues": true, + "rowHeight": 0.9, + "showValue": "auto", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "WITH limits AS (\r\n SELECT\r\n $__timeFrom()::timestamptz AS t_from,\r\n LEAST($__timeTo()::timestamptz, now()) AS t_to,\r\n $__timeTo()::timestamptz AS grafana_to\r\n),\r\nbuckets AS (\r\n SELECT\r\n generate_series(\r\n t_from,\r\n t_to,\r\n interval '10 seconds'\r\n ) AS bucket\r\n FROM limits\r\n),\r\nstates AS (\r\n SELECT\r\n b.bucket AS \"time\",\r\n CASE\r\n WHEN NOT EXISTS (\r\n SELECT 1\r\n FROM mv_hot.edge_oee_eastron_readings r\r\n WHERE r.asset = 'revpi_oee_node_01'\r\n AND r.ingest_ts > b.bucket - interval '30 seconds'\r\n AND r.ingest_ts <= b.bucket + interval '1 second'\r\n )\r\n THEN 2\r\n\r\n WHEN (\r\n SELECT\r\n CASE\r\n WHEN (r.payload_json ->> 'machine_running')::boolean = true THEN 1\r\n WHEN (r.payload_json ->> 'machine_running')::boolean = false THEN 0\r\n ELSE 2\r\n END\r\n FROM mv_hot.edge_oee_eastron_readings r\r\n WHERE r.asset = 'revpi_oee_node_01'\r\n AND r.ingest_ts <= b.bucket\r\n ORDER BY r.ingest_ts DESC\r\n LIMIT 1\r\n ) = 1\r\n THEN 1\r\n\r\n ELSE 0\r\n END AS \"I_1 automático\"\r\n FROM buckets b\r\n),\r\ncutoff AS (\r\n SELECT\r\n t_to + interval '1 second' AS \"time\",\r\n NULL::int AS \"I_1 automático\"\r\n FROM limits\r\n WHERE t_to < grafana_to\r\n)\r\nSELECT *\r\nFROM states\r\n\r\nUNION ALL\r\n\r\nSELECT *\r\nFROM cutoff\r\n\r\nORDER BY \"time\";", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Eventos I_1 — Arranque / Paro", + "type": "state-timeline" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "footer": { + "reducers": [] + }, + "inspect": false + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + } + }, + "gridPos": { + "h": 6, + "w": 24, + "x": 0, + "y": 72 + }, + "id": 26, + "options": { + "cellHeight": "sm", + "showHeader": true + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n id AS \"ID paro\",\r\n machine_id AS \"Máquina\",\r\n started_at AS \"Inicio\",\r\n current_duration_min AS \"Duración actual min\",\r\n current_cost_eur AS \"Coste actual €\",\r\n classification_status AS \"Clasificación\",\r\n COALESCE(cause_name, 'Pendiente') AS \"Causa\",\r\n operator_note AS \"Nota\"\r\nFROM mv_reports_ucepsa_demo.open_stops\r\nWHERE machine_id = 'CORT-01'\r\nORDER BY started_at DESC;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Paro abierto", + "type": "table" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "footer": { + "reducers": [] + }, + "inspect": false + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + } + }, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 78 + }, + "id": 27, + "options": { + "cellHeight": "sm", + "showHeader": true + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n id AS \"ID\",\r\n machine_id AS \"Máquina\",\r\n started_at AS \"Inicio\",\r\n ended_at AS \"Fin\",\r\n status AS \"Estado\",\r\n classification_status AS \"Clasificación\",\r\n COALESCE(cause_name, 'Pendiente') AS \"Causa\",\r\n operator_note AS \"Nota\",\r\n duration_min AS \"Duración min\",\r\n cost_eur AS \"Coste €\"\r\nFROM mv_reports_ucepsa_demo.stops_recent\r\nWHERE machine_id = 'CORT-01'\r\n AND $__timeFilter(started_at)\r\nORDER BY started_at DESC\r\nLIMIT 20;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Últimos paros", + "type": "table" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "fillOpacity": 80, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineWidth": 1, + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + } + }, + "gridPos": { + "h": 12, + "w": 12, + "x": 0, + "y": 86 + }, + "id": 28, + "options": { + "barRadius": 0, + "barWidth": 0.97, + "fullHighlight": false, + "groupWidth": 0.7, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "orientation": "auto", + "showValue": "auto", + "stacking": "none", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + }, + "xTickLabelRotation": 0, + "xTickLabelSpacing": 0 + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n cause_name AS \"Causa\",\r\n total_cost_eur AS \"Coste €\"\r\nFROM mv_reports_ucepsa_demo.stop_summary\r\nORDER BY total_cost_eur DESC;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Coste por causa", + "type": "barchart" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "fillOpacity": 80, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineWidth": 1, + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Minutos" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "semi-dark-yellow", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 12, + "w": 12, + "x": 12, + "y": 86 + }, + "id": 52, + "options": { + "barRadius": 0, + "barWidth": 0.97, + "fullHighlight": false, + "groupWidth": 0.7, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "orientation": "auto", + "showValue": "auto", + "stacking": "none", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + }, + "xTickLabelRotation": 0, + "xTickLabelSpacing": 0 + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n cause_name AS \"Causa\",\r\n total_min AS \"Minutos\"\r\nFROM mv_reports_ucepsa_demo.stop_summary\r\nORDER BY total_min DESC;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Coste por causa", + "type": "barchart" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "footer": { + "reducers": [] + }, + "inspect": false + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + } + }, + "gridPos": { + "h": 13, + "w": 24, + "x": 0, + "y": 98 + }, + "id": 24, + "options": { + "cellHeight": "sm", + "showHeader": true + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n event_ts AS \"Hora evento\",\r\n asset AS \"Nodo\",\r\n machine_id AS \"Máquina\",\r\n CASE\r\n WHEN event_name = 'machine_started' THEN 'Arranque'\r\n WHEN event_name = 'machine_stopped' THEN 'Paro'\r\n ELSE event_name\r\n END AS \"Evento\",\r\n previous_auto_signal AS \"Estado anterior\",\r\n new_auto_signal AS \"Estado nuevo\",\r\n machine_running AS \"Máquina en marcha\",\r\n reason_pending AS \"Causa pendiente\",\r\n cycle_pulse_count AS \"Pulsos acumulados\",\r\n cycle_rate_ppm AS \"Ciclos/min\"\r\nFROM mv_reports_ucepsa_demo.node_events_recent\r\nWHERE $__timeFilter(event_ts)\r\nORDER BY event_ts DESC\r\nLIMIT 50;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Eventos reales de máquina — RevPi", + "type": "table" + } + ], + "title": "Estado de máquina CORTADORA 01", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 58 + }, + "id": 81, + "panels": [ + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [ + { + "options": { + "0": { + "color": "green", + "index": 0, + "text": "OK" + }, + "1": { + "color": "semi-dark-yellow", + "index": 1, + "text": "Warning" + }, + "2": { + "color": "purple", + "index": 2, + "text": "Sin Telemetría" + } + }, + "type": "value" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + } + }, + "gridPos": { + "h": 6, + "w": 4, + "x": 0, + "y": 59 + }, + "id": 17, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n CASE\r\n WHEN comm_ok = false THEN 3\r\n WHEN age_s > 120 THEN 2\r\n WHEN age_s > 30 THEN 1\r\n ELSE 0\r\n END AS \"Estado captura\"\r\nFROM mv_reports_ucepsa_demo.energy_latest;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Estado comunicación", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + } + }, + "gridPos": { + "h": 6, + "w": 4, + "x": 4, + "y": 59 + }, + "id": 18, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n age_s AS \"Edad último dato\"\r\nFROM mv_reports_ucepsa_demo.energy_latest;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Edad último dato", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "red", + "value": 0 + }, + { + "color": "green", + "value": 220 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "L1 V" + }, + "properties": [] + }, + { + "matcher": { + "id": "byName", + "options": "L2 V" + }, + "properties": [] + }, + { + "matcher": { + "id": "byName", + "options": "L3 V" + }, + "properties": [] + }, + { + "matcher": { + "id": "byName", + "options": "Hz" + }, + "properties": [] + } + ] + }, + "gridPos": { + "h": 6, + "w": 4, + "x": 8, + "y": 59 + }, + "id": 21, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n voltage_l1_v AS \"Tensión L1\"\r\nFROM mv_reports_ucepsa_demo.energy_latest;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Tensión L1", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "amp" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "L1 A" + }, + "properties": [] + }, + { + "matcher": { + "id": "byName", + "options": "L2 A" + }, + "properties": [] + }, + { + "matcher": { + "id": "byName", + "options": "L3 A" + }, + "properties": [] + } + ] + }, + "gridPos": { + "h": 6, + "w": 4, + "x": 12, + "y": 59 + }, + "id": 22, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n current_l1_a AS \"Corriente L1\"\r\nFROM mv_reports_ucepsa_demo.energy_latest;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Corriente L1", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "watt" + } + }, + "gridPos": { + "h": 6, + "w": 4, + "x": 0, + "y": 65 + }, + "id": 19, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n power_total_kw * 1000 AS \"Potencia total\"\r\nFROM mv_reports_ucepsa_demo.energy_latest;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Potencia total", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "watth" + } + }, + "gridPos": { + "h": 6, + "w": 4, + "x": 4, + "y": 65 + }, + "id": 20, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n import_kwh * 1000 AS \"Energía acumulada\"\r\nFROM mv_reports_ucepsa_demo.energy_latest;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Energía acumulada", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "watt" + } + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 65 + }, + "id": 41, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n $__time(time),\r\n power_w AS \"Potencia W\"\r\nFROM mv_reports_ucepsa_demo.energy_power_timeseries\r\nWHERE $__timeFilter(time)\r\nORDER BY time;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Evolución de potencia", + "type": "timeseries" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "watth" + } + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 0, + "y": 71 + }, + "id": 42, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n $__time(time),\r\n import_wh AS \"Energía acumulada Wh\"\r\nFROM mv_reports_ucepsa_demo.energy_import_timeseries\r\nWHERE $__timeFilter(time)\r\nORDER BY time;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Energía acumulada", + "type": "timeseries" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": -1, + "barWidthFactor": 1, + "drawStyle": "bars", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + } + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 8, + "y": 71 + }, + "id": 43, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "time_series", + "rawQuery": true, + "rawSql": "WITH raw AS (\r\n SELECT\r\n time,\r\n import_wh,\r\n LAG(import_wh) OVER (ORDER BY time) AS prev_import_wh\r\n FROM mv_reports_ucepsa_demo.energy_import_timeseries\r\n WHERE $__timeFilter(time)\r\n)\r\nSELECT\r\n $__time(time),\r\n ROUND(GREATEST(import_wh - prev_import_wh, 0)::numeric, 4) AS \"Consumo incremental Wh\"\r\nFROM raw\r\nWHERE prev_import_wh IS NOT NULL\r\nORDER BY time;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Consumo por intervalo", + "type": "timeseries" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "watt" + } + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 0, + "y": 78 + }, + "id": 40, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n $__time(time),\r\n power_w AS \"Potencia W\"\r\nFROM mv_reports_ucepsa_demo.energy_power_timeseries\r\nWHERE $__timeFilter(time)\r\nORDER BY time;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Evolución de potencia", + "type": "timeseries" + } + ], + "title": "ENERGÍA CORTADORA 01", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 59 + }, + "id": 82, + "panels": [ + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisPlacement": "auto", + "fillOpacity": 70, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineWidth": 0, + "spanNulls": false + }, + "mappings": [ + { + "options": { + "0": { + "color": "purple", + "index": 0, + "text": "Sin telemetría" + }, + "1": { + "color": "green", + "index": 1, + "text": "Recibiendo datos" + } + }, + "type": "value" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + } + ] + } + } + }, + "gridPos": { + "h": 3, + "w": 24, + "x": 0, + "y": 60 + }, + "id": 44, + "options": { + "alignValue": "left", + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "mergeValues": true, + "rowHeight": 0.9, + "showValue": "auto", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "time_series", + "rawQuery": true, + "rawSql": "WITH limits AS (\r\n SELECT\r\n $__timeFrom()::timestamptz AS t_from,\r\n LEAST($__timeTo()::timestamptz, now()) AS t_to,\r\n $__timeTo()::timestamptz AS grafana_to\r\n),\r\nbuckets AS (\r\n SELECT\r\n generate_series(\r\n t_from,\r\n t_to,\r\n interval '10 seconds'\r\n ) AS bucket\r\n FROM limits\r\n),\r\nstates AS (\r\n SELECT\r\n bucket AS \"time\",\r\n CASE\r\n WHEN EXISTS (\r\n SELECT 1\r\n FROM mv_reports_ucepsa_demo.live_telemetry r\r\n WHERE r.asset = 'revpi_oee_node_01'\r\n AND r.ingest_ts > bucket - interval '30 seconds'\r\n AND r.ingest_ts <= bucket + interval '1 second'\r\n )\r\n THEN 1\r\n ELSE 0\r\n END::int AS \"RevPi telemetría\"\r\n FROM buckets\r\n),\r\ncutoff AS (\r\n SELECT\r\n t_to + interval '1 second' AS \"time\",\r\n NULL::int AS \"RevPi telemetría\"\r\n FROM limits\r\n WHERE t_to < grafana_to\r\n)\r\nSELECT *\r\nFROM states\r\n\r\nUNION ALL\r\n\r\nSELECT *\r\nFROM cutoff\r\n\r\nORDER BY \"time\";", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Estado telemetría RevPi", + "type": "state-timeline" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "footer": { + "reducers": [] + }, + "hideFrom": { + "viz": false + }, + "inspect": false + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + } + }, + "gridPos": { + "h": 9, + "w": 24, + "x": 0, + "y": 63 + }, + "id": 45, + "options": { + "cellHeight": "sm", + "showHeader": true + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "WITH latest AS (\r\n SELECT\r\n age_s,\r\n asset,\r\n power_total_kw,\r\n comm_ok,\r\n error_text\r\n FROM mv_reports_ucepsa_demo.energy_latest\r\n)\r\nSELECT\r\n 'Comunicación' AS \"Grupo\",\r\n 'Edad último dato' AS \"Parámetro\",\r\n age_s::numeric AS \"Valor\",\r\n 's' AS \"Unidad\",\r\n CASE\r\n WHEN comm_ok = false THEN 'Peligro'\r\n WHEN age_s > 120 THEN 'Peligro'\r\n WHEN age_s > 30 THEN 'Warning'\r\n ELSE 'OK'\r\n END AS \"Estado\",\r\n CASE\r\n WHEN comm_ok = false THEN COALESCE(error_text, 'Error de lectura del Eastron')\r\n WHEN age_s > 120 THEN 'No se reciben datos recientes'\r\n WHEN age_s > 30 THEN 'La lectura llega con retraso'\r\n ELSE 'Lectura reciente desde RevPi'\r\n END AS \"Qué está mal\",\r\n CASE\r\n WHEN comm_ok = false THEN 'La RevPi publica, pero la lectura Modbus no es válida'\r\n WHEN age_s > 120 THEN 'La máquina/nodo puede estar sin corriente o sin conexión'\r\n WHEN age_s > 30 THEN 'Puede haber latencia, reinicio o pérdida temporal de red'\r\n ELSE 'La captura industrial está viva'\r\n END AS \"Impacto probable\",\r\n CASE\r\n WHEN comm_ok = false THEN 'Revisar RS-485, SDM120-M, alimentación del contador y logs RevPi'\r\n WHEN age_s > 120 THEN 'Revisar alimentación de RevPi, red, MQTT, bridge y sink'\r\n WHEN age_s > 30 THEN 'Revisar estabilidad de red/MQTT'\r\n ELSE 'Sin acción'\r\n END AS \"Qué revisar / acción\"\r\nFROM latest\r\n\r\nUNION ALL\r\n\r\nSELECT\r\n 'Energía',\r\n 'Potencia total',\r\n power_total_kw,\r\n 'kW',\r\n CASE\r\n WHEN power_total_kw IS NULL THEN 'Warning'\r\n ELSE 'OK'\r\n END,\r\n CASE\r\n WHEN power_total_kw IS NULL THEN 'No hay potencia disponible'\r\n ELSE 'Potencia total leída correctamente'\r\n END,\r\n CASE\r\n WHEN power_total_kw IS NULL THEN 'No se puede validar carga eléctrica'\r\n ELSE 'Permite demostrar captura energética industrial'\r\n END,\r\n CASE\r\n WHEN power_total_kw IS NULL THEN 'Revisar lectura Modbus'\r\n ELSE 'Sin acción'\r\n END\r\nFROM latest;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Diagnóstico de captura", + "type": "table" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "footer": { + "reducers": [] + }, + "inspect": false + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + } + }, + "gridPos": { + "h": 10, + "w": 24, + "x": 0, + "y": 72 + }, + "id": 53, + "options": { + "cellHeight": "sm", + "showHeader": true + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n event_ts AS \"Hora evento\",\r\n asset AS \"Nodo\",\r\n machine_id AS \"Máquina\",\r\n CASE\r\n WHEN event_name = 'machine_started' THEN 'Arranque'\r\n WHEN event_name = 'machine_stopped' THEN 'Paro'\r\n ELSE event_name\r\n END AS \"Evento\",\r\n previous_auto_signal AS \"Estado anterior\",\r\n new_auto_signal AS \"Estado nuevo\",\r\n machine_running AS \"Máquina en marcha\",\r\n reason_pending AS \"Causa pendiente\",\r\n cycle_pulse_count AS \"Pulsos acumulados\",\r\n cycle_rate_ppm AS \"Ciclos/min\"\r\nFROM mv_reports_ucepsa_demo.node_events_recent\r\nWHERE $__timeFilter(event_ts)\r\nORDER BY event_ts DESC\r\nLIMIT 50;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Eventos reales de máquina — RevPi", + "type": "table" + } + ], + "title": "RevPi", + "type": "row" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 60 + }, + "id": 83, + "title": "Loss Intelligence — CORTADORA 01", + "type": "row" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "yellow", + "value": 40 + }, + { + "color": "red", + "value": 70 + } + ] + } + } + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 0, + "y": 61 + }, + "id": 73, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "/^Pérdida prioritaria$/", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "13.0.1+security-01", + "targets": [ + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "\nSELECT\n COALESCE(\n (\n SELECT loss_type\n FROM mv_reports_ucepsa_demo.loss_priority_board\n WHERE machine_id = 'CORT-01'\n ORDER BY priority_rank\n LIMIT 1\n ),\n 'SIN PÉRDIDAS'\n ) AS \"Pérdida prioritaria\";\n", + "refId": "A" + } + ], + "title": "Pérdida prioritaria", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "yellow", + "value": 40 + }, + { + "color": "red", + "value": 70 + } + ] + } + } + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 6, + "y": 61 + }, + "id": 74, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "13.0.1+security-01", + "targets": [ + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "\nSELECT\n COALESCE(\n (\n SELECT priority_score\n FROM mv_reports_ucepsa_demo.loss_priority_board\n WHERE machine_id = 'CORT-01'\n ORDER BY priority_rank\n LIMIT 1\n ),\n 0\n ) AS \"Score prioridad\";\n", + "refId": "A" + } + ], + "title": "Score prioridad", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "yellow", + "value": 40 + }, + { + "color": "red", + "value": 70 + } + ] + }, + "unit": "currencyEUR" + } + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 12, + "y": 61 + }, + "id": 68, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "13.0.1+security-01", + "targets": [ + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "\nSELECT\n COALESCE(\n (\n SELECT cost_eur\n FROM mv_reports_ucepsa_demo.loss_priority_board\n WHERE machine_id = 'CORT-01'\n ORDER BY priority_rank\n LIMIT 1\n ),\n 0\n ) AS \"Coste €\";\n", + "refId": "A" + } + ], + "title": "Coste pérdida actual", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "yellow", + "value": 40 + }, + { + "color": "red", + "value": 70 + } + ] + }, + "unit": "m" + } + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 18, + "y": 61 + }, + "id": 69, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "13.0.1+security-01", + "targets": [ + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "\nSELECT\n COALESCE(\n (\n SELECT duration_min\n FROM mv_reports_ucepsa_demo.loss_priority_board\n WHERE machine_id = 'CORT-01'\n ORDER BY priority_rank\n LIMIT 1\n ),\n 0\n ) AS \"Minutos\";\n", + "refId": "A" + } + ], + "title": "Minutos afectados", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "footer": { + "reducers": [] + }, + "inspect": false + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + } + }, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 65 + }, + "id": 70, + "options": { + "cellHeight": "sm", + "showHeader": true, + "sortBy": [] + }, + "pluginVersion": "13.0.1+security-01", + "targets": [ + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "\nSELECT\n priority_rank AS \"Rank\",\n priority_score AS \"Score\",\n priority_level AS \"Prioridad\",\n machine_id AS \"Máquina\",\n order_ref AS \"Orden\",\n loss_type AS \"Pérdida\",\n severity AS \"Severidad\",\n ROUND(duration_min::numeric, 2) AS \"Min\",\n ROUND(cost_eur::numeric, 2) AS \"Coste €\",\n status AS \"Estado\",\n description AS \"Diagnóstico\",\n recommended_action AS \"Acción recomendada\"\nFROM mv_reports_ucepsa_demo.loss_priority_board\nWHERE machine_id = 'CORT-01'\nORDER BY priority_rank\nLIMIT 20;\n", + "refId": "A" + } + ], + "title": "Tablero de pérdidas vivas", + "type": "table" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "footer": { + "reducers": [] + }, + "inspect": false + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + } + }, + "gridPos": { + "h": 6, + "w": 24, + "x": 0, + "y": 73 + }, + "id": 71, + "options": { + "cellHeight": "sm", + "showHeader": true, + "sortBy": [] + }, + "pluginVersion": "13.0.1+security-01", + "targets": [ + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "\nSELECT\n priority_level AS \"Prioridad\",\n loss_type AS \"Pérdida\",\n description AS \"Diagnóstico\",\n recommended_action AS \"Acción recomendada\",\n evidence_json AS \"Evidencia\"\nFROM mv_reports_ucepsa_demo.loss_priority_board\nWHERE machine_id = 'CORT-01'\nORDER BY priority_rank\nLIMIT 1;\n", + "refId": "A" + } + ], + "title": "Acción recomendada", + "type": "table" + } + ], + "preload": false, + "refresh": "10s", + "schemaVersion": 42, + "time": { + "from": "now-1h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ] + }, + "timezone": "browser", + "title": "MESAVAULT Edge-OEE — UCEPSA local", + "uid": "ucepsa-edge-oee-local", + "version": 3, + "weekStart": "monday" +} diff --git a/ucepsa/edge-oee-demo/grafana/export/ucepsa-edge-oee-machine.dashboard.json b/ucepsa/edge-oee-demo/grafana/export/ucepsa-edge-oee-machine.dashboard.json new file mode 100644 index 0000000..19a21d8 --- /dev/null +++ b/ucepsa/edge-oee-demo/grafana/export/ucepsa-edge-oee-machine.dashboard.json @@ -0,0 +1,1664 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 502804166483968, + "liveNow": false, + "panels": [ + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "mappings": [ + { + "options": { + "0": { + "color": "red", + "text": "NO OK" + }, + "1": { + "color": "green", + "text": "OK" + } + }, + "type": "value" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + } + ] + }, + "unit": "none" + } + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 0, + "y": 0 + }, + "id": 1, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "13.0.1+security-01", + "targets": [ + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "\n SELECT\n CASE\n WHEN age_s < 30 AND comm_ok = true AND COALESCE(sample_valid, true) = true THEN 1\n ELSE 0\n END AS value\n FROM mv_reports_ucepsa_demo.machine_state_latest_multi\n WHERE machine_id = '$machine_id'\n ", + "refId": "A" + } + ], + "title": "Comunicación", + "type": "stat" + }, + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + } + ] + }, + "unit": "s" + } + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 6, + "y": 0 + }, + "id": 2, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "13.0.1+security-01", + "targets": [ + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "\n SELECT age_s AS value\n FROM mv_reports_ucepsa_demo.machine_state_latest_multi\n WHERE machine_id = '$machine_id'\n ", + "refId": "A" + } + ], + "title": "Edad último dato", + "type": "stat" + }, + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "mappings": [ + { + "options": { + "0": { + "color": "yellow", + "text": "PARADA" + }, + "1": { + "color": "green", + "text": "EN MARCHA" + } + }, + "type": "value" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + } + ] + }, + "unit": "none" + } + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 12, + "y": 0 + }, + "id": 3, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "13.0.1+security-01", + "targets": [ + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "\n SELECT\n CASE WHEN machine_running = true THEN 1 ELSE 0 END AS value\n FROM mv_reports_ucepsa_demo.machine_state_latest_multi\n WHERE machine_id = '$machine_id'\n ", + "refId": "A" + } + ], + "title": "Estado máquina", + "type": "stat" + }, + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + } + ] + }, + "unit": "cpm" + } + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 18, + "y": 0 + }, + "id": 4, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "13.0.1+security-01", + "targets": [ + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "\n SELECT cycle_rate_ppm AS value\n FROM mv_reports_ucepsa_demo.machine_state_latest_multi\n WHERE machine_id = '$machine_id'\n ", + "refId": "A" + } + ], + "title": "Ciclos/min", + "type": "stat" + }, + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + } + ] + }, + "unit": "kwatt" + } + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 0, + "y": 4 + }, + "id": 5, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "13.0.1+security-01", + "targets": [ + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "\n SELECT power_total_kw AS value\n FROM mv_reports_ucepsa_demo.machine_state_latest_multi\n WHERE machine_id = '$machine_id'\n ", + "refId": "A" + } + ], + "title": "Potencia total", + "type": "stat" + }, + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + } + ] + }, + "unit": "kwatth" + } + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 6, + "y": 4 + }, + "id": 6, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "13.0.1+security-01", + "targets": [ + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "\n SELECT import_kwh AS value\n FROM mv_reports_ucepsa_demo.machine_state_latest_multi\n WHERE machine_id = '$machine_id'\n ", + "refId": "A" + } + ], + "title": "Energía importada", + "type": "stat" + }, + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + } + ] + }, + "unit": "hertz" + } + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 12, + "y": 4 + }, + "id": 7, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "13.0.1+security-01", + "targets": [ + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "\n SELECT frequency_hz AS value\n FROM mv_reports_ucepsa_demo.machine_state_latest_multi\n WHERE machine_id = '$machine_id'\n ", + "refId": "A" + } + ], + "title": "Frecuencia", + "type": "stat" + }, + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + } + ] + }, + "unit": "none" + } + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 18, + "y": 4 + }, + "id": 8, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "13.0.1+security-01", + "targets": [ + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "\n SELECT cycle_pulse_count AS value\n FROM mv_reports_ucepsa_demo.machine_state_latest_multi\n WHERE machine_id = '$machine_id'\n ", + "refId": "A" + } + ], + "title": "Pulsos acumulados", + "type": "stat" + }, + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 5, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "watt" + } + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 8 + }, + "id": 9, + "options": { + "annotations": { + "clustering": -1, + "multiLane": false + }, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "13.0.1+security-01", + "targets": [ + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "time_series", + "rawQuery": true, + "rawSql": "\n SELECT\n ts AS \"time\",\n active_power_l1_w AS \"L1 W\",\n active_power_l2_w AS \"L2 W\",\n active_power_l3_w AS \"L3 W\",\n active_power_w AS \"Total W\"\n FROM mv_reports_ucepsa_demo.energy_3phase_timeseries\n WHERE $__timeFilter(ts)\n AND site = 'ucepsa_onpremise'\n AND machine_id = '$machine_id'\n ORDER BY ts\n ", + "refId": "A" + } + ], + "title": "Potencia por fase", + "type": "timeseries" + }, + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 5, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "volt" + } + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 8 + }, + "id": 10, + "options": { + "annotations": { + "clustering": -1, + "multiLane": false + }, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "13.0.1+security-01", + "targets": [ + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "time_series", + "rawQuery": true, + "rawSql": "\n SELECT\n ts AS \"time\",\n voltage_l1_v AS \"L1 V\",\n voltage_l2_v AS \"L2 V\",\n voltage_l3_v AS \"L3 V\"\n FROM mv_reports_ucepsa_demo.energy_3phase_timeseries\n WHERE $__timeFilter(ts)\n AND site = 'ucepsa_onpremise'\n AND machine_id = '$machine_id'\n ORDER BY ts\n ", + "refId": "A" + } + ], + "title": "Tensiones L1/L2/L3", + "type": "timeseries" + }, + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 5, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "amp" + } + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 16 + }, + "id": 11, + "options": { + "annotations": { + "clustering": -1, + "multiLane": false + }, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "13.0.1+security-01", + "targets": [ + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "time_series", + "rawQuery": true, + "rawSql": "\n SELECT\n ts AS \"time\",\n current_l1_a AS \"L1 A\",\n current_l2_a AS \"L2 A\",\n current_l3_a AS \"L3 A\"\n FROM mv_reports_ucepsa_demo.energy_3phase_timeseries\n WHERE $__timeFilter(ts)\n AND site = 'ucepsa_onpremise'\n AND machine_id = '$machine_id'\n ORDER BY ts\n ", + "refId": "A" + } + ], + "title": "Corrientes L1/L2/L3", + "type": "timeseries" + }, + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "footer": { + "reducers": [] + }, + "inspect": false + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + } + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 16 + }, + "id": 12, + "options": { + "cellHeight": "sm", + "showHeader": true + }, + "pluginVersion": "13.0.1+security-01", + "targets": [ + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "\n SELECT\n machine_id,\n round(age_s::numeric, 1) AS age_s,\n comm_ok,\n sample_valid,\n energy_map,\n machine_running,\n cycle_input_state,\n cycle_pulse_count,\n wise_cycle_counter_raw,\n round(voltage_l1_v::numeric, 2) AS v_l1,\n round(voltage_l2_v::numeric, 2) AS v_l2,\n round(voltage_l3_v::numeric, 2) AS v_l3,\n round(current_l1_a::numeric, 3) AS i_l1,\n round(current_l2_a::numeric, 3) AS i_l2,\n round(current_l3_a::numeric, 3) AS i_l3,\n round(power_total_kw::numeric, 4) AS kw_total,\n digital_inputs\n FROM mv_reports_ucepsa_demo.machine_state_latest_multi\n WHERE machine_id = '$machine_id'\n ", + "refId": "A" + } + ], + "title": "Última lectura trifásica", + "type": "table" + }, + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "footer": { + "reducers": [] + }, + "inspect": false + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + } + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 24 + }, + "id": 13, + "options": { + "cellHeight": "sm", + "showHeader": true + }, + "pluginVersion": "13.0.1+security-01", + "targets": [ + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\n machine_id AS \"Máquina\",\n order_ref AS \"Orden\",\n product_name AS \"Producto\",\n odoo_state AS \"Estado Odoo\",\n order_present AS \"Orden presente\",\n order_coherent AS \"Coherente\",\n waiting_count AS \"Waiting\",\n ready_count AS \"Ready\",\n progress_count AS \"Progress\",\n machine_running AS \"Máquina en marcha\",\n cycle_rate_ppm AS \"Ciclos/min\",\n round(order_age_s::numeric, 1) AS \"Edad Odoo s\",\n round(telemetry_age_s::numeric, 1) AS \"Edad telemetría s\",\n edge_odoo_status AS \"Diagnóstico\",\n reason AS \"Motivo\"\nFROM mv_reports_ucepsa_demo.odoo_order_state_machine_grafana\nWHERE machine_id = '$machine_id';", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Odoo vs máquina", + "type": "table" + }, + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "footer": { + "reducers": [] + }, + "inspect": false + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + } + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 24 + }, + "id": 14, + "options": { + "cellHeight": "sm", + "showHeader": true + }, + "pluginVersion": "13.0.1+security-01", + "targets": [ + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "\n SELECT\n priority_rank,\n priority_score,\n priority_level,\n machine_id,\n order_ref,\n loss_type,\n severity,\n duration_min,\n cost_eur,\n status,\n description,\n recommended_action\n FROM mv_reports_ucepsa_demo.loss_priority_board\n WHERE machine_id = '$machine_id'\n ORDER BY priority_rank\n ", + "refId": "A" + } + ], + "title": "Loss Intelligence", + "type": "table" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 32 + }, + "id": 300, + "panels": [], + "title": "Informe de paros", + "type": "row" + }, + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 1 + }, + { + "color": "red", + "value": 5 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 0, + "y": 33 + }, + "id": 301, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "targets": [ + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "\n SELECT COUNT(*) AS value\n FROM mv_reports_ucepsa_demo.machine_stops_grafana\n WHERE machine_id = '$machine_id'\n AND status = 'OPEN'\n ", + "refId": "A" + } + ], + "title": "Paros abiertos", + "type": "stat" + }, + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 1 + }, + { + "color": "red", + "value": 5 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 4, + "y": 33 + }, + "id": 302, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "targets": [ + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "\n SELECT COUNT(*) AS value\n FROM mv_reports_ucepsa_demo.machine_stops_grafana\n WHERE machine_id = '$machine_id'\n AND classification_status = 'PENDING'\n ", + "refId": "A" + } + ], + "title": "Pendientes clasificar", + "type": "stat" + }, + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 1 + }, + { + "color": "red", + "value": 5 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 8, + "y": 33 + }, + "id": 303, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "targets": [ + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "\n SELECT COUNT(*) AS value\n FROM mv_reports_ucepsa_demo.machine_stops_grafana\n WHERE machine_id = '$machine_id'\n AND $__timeFilter(started_at)\n ", + "refId": "A" + } + ], + "title": "Nº paros en rango", + "type": "stat" + }, + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 1 + }, + { + "color": "red", + "value": 5 + } + ] + }, + "unit": "min" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 12, + "y": 33 + }, + "id": 304, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "targets": [ + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "\n SELECT COALESCE(SUM(duration_min), 0) AS value\n FROM mv_reports_ucepsa_demo.machine_stops_grafana\n WHERE machine_id = '$machine_id'\n AND $__timeFilter(started_at)\n ", + "refId": "A" + } + ], + "title": "Minutos paro en rango", + "type": "stat" + }, + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 1 + }, + { + "color": "red", + "value": 5 + } + ] + }, + "unit": "currencyEUR" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 16, + "y": 33 + }, + "id": 305, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "targets": [ + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "\n SELECT COALESCE(SUM(cost_eur), 0) AS value\n FROM mv_reports_ucepsa_demo.machine_stops_grafana\n WHERE machine_id = '$machine_id'\n AND $__timeFilter(started_at)\n ", + "refId": "A" + } + ], + "title": "Coste paros en rango", + "type": "stat" + }, + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 1 + }, + { + "color": "red", + "value": 5 + } + ] + }, + "unit": "min" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 20, + "y": 33 + }, + "id": 306, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "targets": [ + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "\n SELECT COALESCE(AVG(duration_min), 0) AS value\n FROM mv_reports_ucepsa_demo.machine_stops_grafana\n WHERE machine_id = '$machine_id'\n AND $__timeFilter(started_at)\n ", + "refId": "A" + } + ], + "title": "Duración media en rango", + "type": "stat" + }, + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "unit": "min" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 37 + }, + "id": 307, + "options": { + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "orientation": "horizontal", + "showValue": "always", + "xField": "Causa" + }, + "targets": [ + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "\n SELECT\n cause_name AS \"Causa\",\n ROUND(SUM(duration_min)::numeric, 2) AS \"Minutos\"\n FROM mv_reports_ucepsa_demo.machine_stops_grafana\n WHERE machine_id = '$machine_id'\n AND classification_status = 'CLASSIFIED'\n AND status = 'CLOSED'\n GROUP BY cause_name\n ORDER BY SUM(duration_min) DESC\n LIMIT 10\n ", + "refId": "A" + } + ], + "title": "Pareto causas clasificadas por minutos", + "type": "barchart" + }, + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "custom": { + "drawStyle": "line", + "fillOpacity": 5, + "lineInterpolation": "linear", + "lineWidth": 1, + "showPoints": "never" + }, + "unit": "min" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 37 + }, + "id": 308, + "options": { + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "time_series", + "rawQuery": true, + "rawSql": "\n SELECT\n date_trunc('hour', started_at) AS \"time\",\n SUM(duration_min) AS \"Minutos de paro\"\n FROM mv_reports_ucepsa_demo.machine_stops_grafana\n WHERE machine_id = '$machine_id'\n AND $__timeFilter(started_at)\n GROUP BY 1\n ORDER BY 1\n ", + "refId": "A" + } + ], + "title": "Minutos de paro por hora", + "type": "timeseries" + }, + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 24, + "x": 0, + "y": 45 + }, + "id": 309, + "options": { + "cellHeight": "sm", + "showHeader": true + }, + "targets": [ + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "\n SELECT\n id AS \"ID\",\n started_at AS \"Inicio\",\n ROUND(duration_min::numeric, 2) AS \"Min\",\n cost_eur AS \"Coste €\",\n status AS \"Estado\",\n classification_status AS \"Clasificación\",\n cause_name AS \"Causa\",\n operator_note AS \"Nota operario\"\n FROM mv_reports_ucepsa_demo.machine_stops_grafana\n WHERE machine_id = '$machine_id'\n AND classification_status = 'PENDING'\n ORDER BY started_at DESC\n ", + "refId": "A" + } + ], + "title": "Paros pendientes de clasificación", + "type": "table" + }, + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 24, + "x": 0, + "y": 51 + }, + "id": 310, + "options": { + "cellHeight": "sm", + "showHeader": true + }, + "targets": [ + { + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "\n SELECT\n id AS \"ID\",\n machine_id AS \"Máquina\",\n started_at AS \"Inicio\",\n ended_at AS \"Fin\",\n duration_min AS \"Min\",\n cost_eur AS \"Coste €\",\n stop_status_label AS \"Estado\",\n classification_status AS \"Clasificación\",\n cause_code AS \"Código causa\",\n cause_name AS \"Causa\",\n start_event_name AS \"Evento inicio\",\n end_event_name AS \"Evento fin\",\n operator_note AS \"Nota operario\"\n FROM mv_reports_ucepsa_demo.machine_stops_grafana\n WHERE machine_id = '$machine_id'\n AND $__timeFilter(started_at)\n ORDER BY started_at DESC\n LIMIT 100\n ", + "refId": "A" + } + ], + "title": "Detalle completo de paros", + "type": "table" + } + ], + "preload": false, + "refresh": "5s", + "schemaVersion": 42, + "tags": [ + "mesavault", + "ucepsa", + "edge-oee" + ], + "templating": { + "list": [ + { + "allowCustomValue": true, + "current": { + "text": "CORT-00", + "value": "CORT-00" + }, + "datasource": { + "type": "postgres", + "uid": "bfnbcasbm6hhca" + }, + "hide": 0, + "includeAll": false, + "label": "Máquina", + "multi": false, + "name": "machine_id", + "options": [], + "query": "SELECT machine_id FROM mv_reports_ucepsa_demo.machine_state_latest_multi ORDER BY machine_id", + "refresh": 1, + "regex": "", + "regexApplyTo": "value", + "skipUrlSync": false, + "sort": 1, + "type": "query" + } + ] + }, + "time": { + "from": "now-30m", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ] + }, + "timezone": "browser", + "title": "MESAVAULT Edge-OEE — Máquina", + "uid": "ucepsa-edge-oee-machine", + "version": 5 +} diff --git a/ucepsa/edge-oee-demo/grafana/import/platform40-dashboard.raw.json b/ucepsa/edge-oee-demo/grafana/import/platform40-dashboard.raw.json new file mode 100644 index 0000000..1cb7421 --- /dev/null +++ b/ucepsa/edge-oee-demo/grafana/import/platform40-dashboard.raw.json @@ -0,0 +1,5677 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 1, + "links": [], + "liveNow": true, + "panels": [ + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 2, + "panels": [ + { + "description": "", + "fieldConfig": { + "defaults": {}, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 16, + "x": 0, + "y": 2302 + }, + "id": 3, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "## Odoo dice qué orden está en curso. MESAVAULT muestra qué hizo realmente la máquina.\n\nLa demo convierte eventos de máquina en pérdidas, coste económico y acciones concretas:\n\n**dato → pérdida → euros → acción**", + "mode": "markdown" + }, + "pluginVersion": "12.3.2", + "title": "Odoo vs máquina", + "type": "text" + } + ], + "title": "Mensaje de negocio", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 1 + }, + "id": 4, + "panels": [ + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "aflbyx1ifu8zke" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "mappings": [], + "min": -1, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "horas" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 4, + "x": 0, + "y": 247 + }, + "id": 5, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "cfflj1atyw6psd" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n value AS \"Horas analizadas\"\r\nFROM mv_hot.edge_oee_kpi_cards_demo\r\nWHERE kpi = 'Horas analizadas';", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Horas analizadas", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "aflbyx1ifu8zke" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "horas" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 4, + "x": 4, + "y": 247 + }, + "id": 6, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "cfflj1atyw6psd" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n value AS \"Pérdidas clasificadas\"\r\nFROM mv_hot.edge_oee_kpi_cards_demo\r\nWHERE kpi = 'Pérdidas clasificadas';", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Pérdidas clasificadas", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "aflbyx1ifu8zke" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "currencyEUR" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 4, + "x": 8, + "y": 247 + }, + "id": 7, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "cfflj1atyw6psd" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n value AS \"Coste estimado\"\r\nFROM mv_hot.edge_oee_kpi_cards_demo\r\nWHERE kpi = 'Coste estimado de pérdidas';", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Coste estimado de pérdidas", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "aflbyx1ifu8zke" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 4, + "x": 12, + "y": 247 + }, + "id": 8, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "cfflj1atyw6psd" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n value AS \"Discrepancias\"\r\nFROM mv_hot.edge_oee_kpi_cards_demo\r\nWHERE kpi = 'Órdenes con discrepancia Odoo/máquina';", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Discrepancias Odoo/máquina", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "aflbyx1ifu8zke" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "fillOpacity": 80, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineWidth": 1, + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + } + ] + }, + "unit": "currencyEUR" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 16, + "x": 0, + "y": 931 + }, + "id": 9, + "options": { + "barRadius": 0, + "barWidth": 0.8, + "fullHighlight": false, + "groupWidth": 0.7, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "orientation": "auto", + "showValue": "auto", + "stacking": "none", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + }, + "xField": "Causa", + "xTickLabelRotation": 0, + "xTickLabelSpacing": 0 + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "cfflj1atyw6psd" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n cause AS \"Causa\",\r\n monthly_cost_eur AS \"Coste €/mes\"\r\nFROM mv_hot.edge_oee_pareto_demo\r\nORDER BY ranking;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Pareto económico de pérdidas", + "type": "barchart" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "aflbyx1ifu8zke" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "footer": { + "reducers": [] + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Ranking" + }, + "properties": [ + { + "id": "custom.width", + "value": 67 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Causa" + }, + "properties": [ + { + "id": "custom.width", + "value": 216 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Horas/mes" + }, + "properties": [ + { + "id": "custom.width", + "value": 87 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Coste €/mes" + }, + "properties": [ + { + "id": "custom.width", + "value": 100 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Acción recomendada" + }, + "properties": [ + { + "id": "custom.width", + "value": 410 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Esfuerzo" + }, + "properties": [ + { + "id": "custom.width", + "value": 112 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Prioridad" + }, + "properties": [ + { + "id": "custom.width", + "value": 106 + } + ] + } + ] + }, + "gridPos": { + "h": 3, + "w": 16, + "x": 0, + "y": 941 + }, + "id": 10, + "options": { + "cellHeight": "sm", + "showHeader": true, + "sortBy": [] + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "cfflj1atyw6psd" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n ranking AS \"Ranking\",\r\n cause AS \"Causa\",\r\n hours_month AS \"Horas/mes\",\r\n monthly_cost_eur AS \"Coste €/mes\",\r\n recommended_action AS \"Acción recomendada\",\r\n effort AS \"Esfuerzo\",\r\n priority AS \"Prioridad\",\r\n machine_focus AS \"Máquina foco\"\r\nFROM mv_hot.edge_oee_pareto_demo\r\nORDER BY ranking;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "", + "type": "table" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "aflbyx1ifu8zke" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-GrYlRd" + }, + "custom": { + "axisPlacement": "auto", + "fillOpacity": 70, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineWidth": 0, + "spanNulls": false + }, + "mappings": [ + { + "options": { + "0": { + "index": 0, + "text": "Sin evento" + }, + "10": { + "index": 1, + "text": "Orden activa en Odoo" + }, + "20": { + "index": 2, + "text": "Preparación / búsqueda de bobina" + }, + "30": { + "index": 3, + "text": "Marcha real" + }, + "40": { + "index": 4, + "text": "Microparos" + }, + "50": { + "index": 5, + "text": "Cambio bobina" + }, + "60": { + "index": 6, + "text": "Paro" + }, + "90": { + "index": 7, + "text": "Otro evento" + } + }, + "type": "value" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "purple", + "value": 0 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 16, + "x": 0, + "y": 944 + }, + "id": 11, + "options": { + "alignValue": "left", + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "mergeValues": true, + "rowHeight": 0.9, + "showValue": "auto", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "cfflj1atyw6psd" + }, + "editorMode": "code", + "format": "time_series", + "rawQuery": true, + "rawSql": "WITH odoo AS (\r\n SELECT\r\n o.odoo_start AS time,\r\n 'Odoo / OF00481' AS metric,\r\n 10::numeric AS value\r\n FROM mv_hot.edge_oee_orders_demo o\r\n WHERE o.order_id = 'OF00481'\r\n\r\n UNION ALL\r\n\r\n SELECT\r\n o.odoo_end + interval '1 second' AS time,\r\n 'Odoo / OF00481' AS metric,\r\n 0::numeric AS value\r\n FROM mv_hot.edge_oee_orders_demo o\r\n WHERE o.order_id = 'OF00481'\r\n),\r\n\r\nmachine_start AS (\r\n SELECT\r\n e.start_time AS time,\r\n 'Cortadora 0 / realidad máquina' AS metric,\r\n CASE\r\n WHEN e.event_type = 'PREPARATION' THEN 20\r\n WHEN e.event_type = 'RUNNING' THEN 30\r\n WHEN e.event_type = 'MICROSTOP' THEN 40\r\n WHEN e.event_type = 'STOP' AND e.cause = 'Cambio bobina' THEN 50\r\n WHEN e.event_type = 'STOP' THEN 60\r\n ELSE 90\r\n END::numeric AS value\r\n FROM mv_hot.edge_oee_events_demo e\r\n WHERE e.order_id = 'OF00481'\r\n),\r\n\r\nmachine_end AS (\r\n SELECT\r\n e.end_time + interval '1 second' AS time,\r\n 'Cortadora 0 / realidad máquina' AS metric,\r\n 0::numeric AS value\r\n FROM mv_hot.edge_oee_events_demo e\r\n WHERE e.order_id = 'OF00481'\r\n)\r\n\r\nSELECT\r\n time AS \"time\",\r\n metric,\r\n value\r\nFROM (\r\n SELECT * FROM odoo\r\n UNION ALL\r\n SELECT * FROM machine_start\r\n UNION ALL\r\n SELECT * FROM machine_end\r\n) q\r\nORDER BY \"time\", metric;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "OF00481 — Odoo inicia orden, máquina arranca 22 min después", + "type": "state-timeline" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "aflbyx1ifu8zke" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "footer": { + "reducers": [] + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Indicador" + }, + "properties": [ + { + "id": "custom.width", + "value": 279 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Valor" + }, + "properties": [ + { + "id": "custom.width", + "value": 50 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Unidad" + }, + "properties": [ + { + "id": "custom.width", + "value": 67 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Estado" + }, + "properties": [ + { + "id": "custom.width", + "value": 68 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Qué está mal" + }, + "properties": [ + { + "id": "custom.width", + "value": 397 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Impacto probable" + }, + "properties": [ + { + "id": "custom.width", + "value": 489 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Qué revisar / acción" + }, + "properties": [ + { + "id": "custom.width", + "value": 388 + } + ] + } + ] + }, + "gridPos": { + "h": 12, + "w": 16, + "x": 0, + "y": 949 + }, + "id": 12, + "options": { + "cellHeight": "sm", + "showHeader": true + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "cfflj1atyw6psd" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n \"Indicador\",\r\n \"Valor\",\r\n \"Unidad\",\r\n \"Estado\",\r\n \"Qué está mal\",\r\n \"Impacto probable\",\r\n \"Qué revisar / acción\"\r\nFROM mv_hot.edge_oee_data_quality_dashboard_demo\r\nORDER BY sort_order;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Calidad del dato Odoo / máquina", + "type": "table" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "aflbyx1ifu8zke" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "footer": { + "reducers": [] + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "time" + }, + "properties": [ + { + "id": "custom.width", + "value": 158 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "A-series" + }, + "properties": [ + { + "id": "custom.width", + "value": 69 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Causa" + }, + "properties": [ + { + "id": "custom.width", + "value": 177 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Impacto estimado €/mes" + }, + "properties": [ + { + "id": "custom.width", + "value": 182 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Esfuerzo" + }, + "properties": [ + { + "id": "custom.width", + "value": 73 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Prioridad" + }, + "properties": [ + { + "id": "custom.width", + "value": 75 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Payback ejemplo meses" + }, + "properties": [ + { + "id": "custom.width", + "value": 177 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Inversión ejemplo €" + }, + "properties": [ + { + "id": "custom.width", + "value": 146 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Acción recomendada" + }, + "properties": [ + { + "id": "custom.width", + "value": 410 + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 16, + "x": 0, + "y": 961 + }, + "id": 13, + "options": { + "cellHeight": "sm", + "showHeader": true, + "sortBy": [] + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "cfflj1atyw6psd" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n \"Causa\",\r\n \"Impacto estimado €/mes\",\r\n \"Esfuerzo\",\r\n \"Prioridad\",\r\n \"Acción recomendada\",\r\n \"Inversión ejemplo €\",\r\n \"Payback ejemplo meses\"\r\nFROM mv_hot.edge_oee_actions_demo;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Acciones recomendadas", + "type": "table" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "aflbyx1ifu8zke" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "footer": { + "reducers": [] + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Orden" + }, + "properties": [ + { + "id": "custom.width", + "value": 76 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Máquina" + }, + "properties": [ + { + "id": "custom.width", + "value": 93 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Producto" + }, + "properties": [ + { + "id": "custom.width", + "value": 115 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Reclamación" + }, + "properties": [ + { + "id": "custom.width", + "value": 99 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Microparos" + }, + "properties": [ + { + "id": "custom.width", + "value": 90 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Merma %" + }, + "properties": [ + { + "id": "custom.width", + "value": 78 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Energía vs media %" + }, + "properties": [ + { + "id": "custom.width", + "value": 147 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Interpretación" + }, + "properties": [ + { + "id": "custom.width", + "value": 889 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Acción recomendada" + }, + "properties": [ + { + "id": "custom.width", + "value": 949 + } + ] + } + ] + }, + "gridPos": { + "h": 9, + "w": 16, + "x": 0, + "y": 971 + }, + "id": 14, + "options": { + "cellHeight": "sm", + "showHeader": true, + "sortBy": [] + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "cfflj1atyw6psd" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n order_id AS \"Orden\",\r\n machine_name AS \"Máquina\",\r\n product AS \"Producto\",\r\n claim AS \"Reclamación\",\r\n sealing_adjustment_stops AS \"Paros ajuste sellado\",\r\n sealing_adjustment_min AS \"Min ajuste sellado\",\r\n microstops_count AS \"Microparos\",\r\n scrap_pct AS \"Merma %\",\r\n energy_vs_avg_pct AS \"Energía vs media %\",\r\n interpretation AS \"Interpretación\",\r\n recommended_action AS \"Acción recomendada\"\r\nFROM mv_hot.edge_oee_quality_case_demo;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Caso calidad — Sellado débil OF00493", + "type": "table" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "aflbyx1ifu8zke" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "kwatt" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 16, + "x": 0, + "y": 980 + }, + "id": 15, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "cfflj1atyw6psd" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n ts AS \"time\",\r\n power_kw AS \"Potencia kW\"\r\nFROM mv_hot.edge_oee_energy_demo\r\nWHERE machine_id = 'C2'\r\nORDER BY ts;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Energía durante OF00493 — Cortadora 2", + "type": "timeseries" + } + ], + "title": "KPIs de dirección", + "type": "row" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 2 + }, + "id": 30, + "panels": [], + "title": "OEE — CORTADORA 01", + "type": "row" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "aflbyx1ifu8zke" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "red", + "value": 0 + }, + { + "color": "green", + "value": 80 + } + ] + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 9, + "x": 0, + "y": 3 + }, + "id": 39, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n oee_pct AS \"OEE %\",\r\n availability_pct AS \"Disponibilidad %\",\r\n performance_pct AS \"Rendimiento %\",\r\n quality_pct AS \"Calidad %\"\r\nFROM mv_reports_ucepsa_demo.oee_kpis(\r\n $__timeFrom()::timestamptz,\r\n $__timeTo()::timestamptz\r\n);", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "OEE", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "aflbyx1ifu8zke" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 1, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "text", + "value": 0 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 10, + "x": 9, + "y": 3 + }, + "id": 31, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n running_min AS \"Min marcha\",\r\n stop_min AS \"Min paro\",\r\n actual_cycles AS \"Ciclos\"\r\nFROM mv_reports_ucepsa_demo.oee_operational_data(\r\n $__timeFrom()::timestamptz,\r\n $__timeTo()::timestamptz\r\n);", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Datos operativos", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "aflbyx1ifu8zke" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "fillOpacity": 80, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineWidth": 1, + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 19, + "x": 0, + "y": 7 + }, + "id": 37, + "options": { + "barRadius": 0, + "barWidth": 0.97, + "fullHighlight": false, + "groupWidth": 0.7, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "orientation": "auto", + "showValue": "auto", + "stacking": "none", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + }, + "xTickLabelRotation": 0, + "xTickLabelSpacing": 0 + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n COALESCE(cause_name, 'Pendiente de clasificar') AS \"Causa\",\r\n ROUND(SUM(cost_eur)::numeric, 2) AS \"Coste €\",\r\n ROUND(SUM(duration_min)::numeric, 2) AS \"Minutos\"\r\nFROM mv_reports_ucepsa_demo.stops_recent\r\nWHERE machine_id = 'CORT-01'\r\n AND $__timeFilter(started_at)\r\nGROUP BY COALESCE(cause_name, 'Pendiente de clasificar')\r\nORDER BY \"Coste €\" DESC;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Pérdida real por causa", + "type": "barchart" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "aflbyx1ifu8zke" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 19, + "x": 0, + "y": 16 + }, + "id": 33, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n ts AS \"time\",\r\n cycle_rate_ppm AS \"Ciclos/min\",\r\n 10.0 AS \"Objetivo ciclos/min\"\r\nFROM mv_hot.edge_oee_live_telemetry\r\nWHERE $__timeFilter(ts)\r\n AND asset = 'revpi_oee_node_01'\r\n AND cycle_rate_ppm IS NOT NULL\r\nORDER BY ts;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Velocidad real — ciclos/min", + "type": "timeseries" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "aflbyx1ifu8zke" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "footer": { + "reducers": [] + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "ID" + }, + "properties": [ + { + "id": "custom.width", + "value": 50 + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 19, + "x": 0, + "y": 22 + }, + "id": 36, + "options": { + "cellHeight": "sm", + "showHeader": true, + "sortBy": [] + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n id AS \"ID\",\r\n machine_id AS \"Máquina\",\r\n started_at AS \"Inicio\",\r\n ended_at AS \"Fin\",\r\n status AS \"Estado\",\r\n classification_status AS \"Clasificación\",\r\n COALESCE(cause_name, 'Pendiente') AS \"Causa\",\r\n operator_note AS \"Nota\",\r\n duration_min AS \"Duración min\",\r\n cost_eur AS \"Coste €\"\r\nFROM mv_hot.edge_oee_stops_recent\r\nWHERE $__timeFilter(started_at)\r\nORDER BY started_at DESC\r\nLIMIT 20;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Últimos paros clasificados / pendientes", + "type": "table" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 28 + }, + "id": 55, + "panels": [], + "title": "Odoo vs máquina — Cortadora 01", + "type": "row" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "aflbyx1ifu8zke" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "mappings": [ + { + "options": { + "0": { + "color": "green", + "index": 0, + "text": "OK" + }, + "2": { + "color": "red", + "index": 1, + "text": "Discrepancia" + }, + "3": { + "color": "purple", + "index": 2, + "text": "Sin Telemetría" + } + }, + "type": "value" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 4, + "x": 0, + "y": 29 + }, + "id": 57, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n CASE\r\n WHEN status_check IN ('OK', 'ORDEN_PREPARADA_MAQUINA_PARADA') THEN 0\r\n WHEN status_check = 'SIN_TELEMETRIA' THEN 3\r\n ELSE 2\r\n END AS \"Estado cruce\"\r\nFROM mv_reports_ucepsa_demo.odoo_vs_machine_demo;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Discrepancias Odoo/máquina", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "aflbyx1ifu8zke" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "noValue": "No iniciada", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "m" + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 4, + "x": 4, + "y": 29 + }, + "id": 58, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n CASE\r\n WHEN odoo_is_started = true THEN start_delay_min\r\n ELSE NULL\r\n END AS \"Retraso inicio real\"\r\nFROM mv_reports_ucepsa_demo.odoo_vs_machine_demo;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Retraso inicio real", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "aflbyx1ifu8zke" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 0.1 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 7, + "x": 0, + "y": 34 + }, + "id": 59, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n stop_min AS \"Min paro en orden\",\r\n stop_cost_eur AS \"Coste paro en orden\"\r\nFROM mv_reports_ucepsa_demo.odoo_vs_machine_demo;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Paros dentro de orden", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "aflbyx1ifu8zke" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "text", + "value": 0 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 9, + "x": 7, + "y": 34 + }, + "id": 60, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n real_cycles AS \"Ciclos reales\",\r\n planned_cycles AS \"Ciclos planificados\",\r\n cycle_completion_pct AS \"% avance ciclos\"\r\nFROM mv_reports_ucepsa_demo.odoo_vs_machine_demo;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Ciclos reales vs planificados", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "aflbyx1ifu8zke" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "footer": { + "reducers": [] + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Orden Odoo" + }, + "properties": [ + { + "id": "custom.width", + "value": 112 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Producto" + }, + "properties": [ + { + "id": "custom.width", + "value": 260 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Estado" + }, + "properties": [ + { + "id": "custom.width", + "value": 72 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Máquina" + }, + "properties": [ + { + "id": "custom.width", + "value": 72 + } + ] + } + ] + }, + "gridPos": { + "h": 4, + "w": 24, + "x": 0, + "y": 39 + }, + "id": 56, + "options": { + "cellHeight": "sm", + "showHeader": true, + "sortBy": [] + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n odoo_order_ref AS \"Orden Odoo\",\r\n product AS \"Producto\",\r\n odoo_state AS \"Estado\",\r\n machine_id AS \"Máquina\",\r\n CASE\r\n WHEN odoo_is_started = true THEN to_char(odoo_start, 'YYYY-MM-DD HH24:MI:SS')\r\n ELSE 'No iniciada'\r\n END AS \"Inicio Odoo\"\r\nFROM mv_reports_ucepsa_demo.odoo_current_order_demo;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Orden Odoo seleccionada", + "type": "table" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "aflbyx1ifu8zke" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "footer": { + "reducers": [] + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Orden" + }, + "properties": [ + { + "id": "custom.width", + "value": 115 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Estado Odoo" + }, + "properties": [ + { + "id": "custom.width", + "value": 100 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Edad telemetría s" + }, + "properties": [ + { + "id": "custom.width", + "value": 131 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Estado máquina" + }, + "properties": [ + { + "id": "custom.width", + "value": 143 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Diagnóstico" + }, + "properties": [ + { + "id": "custom.width", + "value": 330 + } + ] + } + ] + }, + "gridPos": { + "h": 4, + "w": 24, + "x": 0, + "y": 43 + }, + "id": 62, + "options": { + "cellHeight": "sm", + "showHeader": true, + "sortBy": [ + { + "desc": false, + "displayName": "Estado máquina" + } + ] + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n odoo_order_ref AS \"Orden\",\r\n odoo_state AS \"Estado Odoo\",\r\n CASE\r\n WHEN machine_running THEN 'En marcha'\r\n ELSE 'Parada'\r\n END AS \"Estado máquina\",\r\n telemetry_age_s AS \"Edad telemetría s\",\r\n status_check AS \"Diagnóstico\"\r\nFROM mv_reports_ucepsa_demo.odoo_vs_machine_demo;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Estado Odoo / realidad", + "type": "table" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "aflbyx1ifu8zke" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "footer": { + "reducers": [] + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Orden" + }, + "properties": [ + { + "id": "custom.width", + "value": 115 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Hora chequeo" + }, + "properties": [ + { + "id": "custom.width", + "value": 155 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Máquina" + }, + "properties": [ + { + "id": "custom.width", + "value": 72 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Estado" + }, + "properties": [ + { + "id": "custom.width", + "value": 331 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Descripción" + }, + "properties": [ + { + "id": "custom.width", + "value": 633 + } + ] + } + ] + }, + "gridPos": { + "h": 9, + "w": 24, + "x": 0, + "y": 47 + }, + "id": 61, + "options": { + "cellHeight": "sm", + "showHeader": true + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n checked_at AS \"Hora chequeo\",\r\n odoo_order_ref AS \"Orden\",\r\n product AS \"Producto\",\r\n machine_id AS \"Máquina\",\r\n status_check AS \"Estado\",\r\n descripcion AS \"Descripción\",\r\n start_delay_min AS \"Retraso min\",\r\n stop_count AS \"Nº paros\",\r\n stop_min AS \"Min paro\",\r\n stop_cost_eur AS \"Coste paro €\",\r\n cycle_completion_pct AS \"% avance ciclos\"\r\nFROM mv_reports_ucepsa_demo.odoo_discrepancies_demo;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Tabla de discrepancias", + "type": "table" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 56 + }, + "id": 64, + "panels": [ + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "aflbyx1ifu8zke" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "fillOpacity": 80, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineWidth": 1, + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "currencyEUR" + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 11, + "x": 0, + "y": 248 + }, + "id": 66, + "options": { + "barRadius": 0, + "barWidth": 0.97, + "fullHighlight": false, + "groupWidth": 0.7, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "orientation": "auto", + "showValue": "auto", + "stacking": "none", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + }, + "xTickLabelRotation": 0, + "xTickLabelSpacing": 0 + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n odoo_order_ref AS \"Orden\",\r\n stop_cost_eur AS \"Coste paro €\"\r\nFROM mv_reports_ucepsa_demo.odoo_workorders_reality_history\r\nWHERE stop_cost_eur > 0\r\nORDER BY stop_cost_eur DESC\r\nLIMIT 10;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Coste de paros por orden", + "type": "barchart" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "aflbyx1ifu8zke" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "fillOpacity": 80, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineWidth": 1, + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "m" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Min paro" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "yellow", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 9, + "w": 13, + "x": 11, + "y": 248 + }, + "id": 67, + "options": { + "barRadius": 0, + "barWidth": 0.97, + "fullHighlight": false, + "groupWidth": 0.7, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "orientation": "auto", + "showValue": "auto", + "stacking": "none", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + }, + "xTickLabelRotation": 0, + "xTickLabelSpacing": 0 + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n odoo_order_ref AS \"Orden\",\r\n stop_min AS \"Min paro\"\r\nFROM mv_reports_ucepsa_demo.odoo_workorders_reality_history\r\nWHERE stop_min > 0\r\nORDER BY stop_min DESC\r\nLIMIT 10;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Tiempo de paro por orden", + "type": "barchart" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "aflbyx1ifu8zke" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "footer": { + "reducers": [] + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 24, + "x": 0, + "y": 257 + }, + "id": 63, + "options": { + "cellHeight": "sm", + "showHeader": true + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n odoo_order_ref AS \"Orden\",\r\n product AS \"Producto\",\r\n odoo_state AS \"Estado\",\r\n machine_id AS \"Máquina\",\r\n odoo_start AS \"Inicio Odoo\",\r\n odoo_end AS \"Fin Odoo\",\r\n real_cycles AS \"Ciclos reales\",\r\n planned_cycles AS \"Ciclos planificados\",\r\n cycle_completion_pct AS \"% avance\",\r\n stop_count AS \"Nº paros\",\r\n stop_min AS \"Min paro\",\r\n stop_cost_eur AS \"Coste paro €\",\r\n status_check AS \"Diagnóstico\"\r\nFROM mv_reports_ucepsa_demo.odoo_last_finished_order_demo;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Última orden finalizada", + "type": "table" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "aflbyx1ifu8zke" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "footer": { + "reducers": [] + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 17, + "w": 24, + "x": 0, + "y": 261 + }, + "id": 65, + "options": { + "cellHeight": "sm", + "showHeader": true + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n odoo_order_ref AS \"Orden\",\r\n odoo_state AS \"Estado\",\r\n product AS \"Producto\",\r\n machine_id AS \"Máquina\",\r\n odoo_start AS \"Inicio Odoo\",\r\n odoo_end AS \"Fin Odoo\",\r\n real_cycles AS \"Ciclos reales\",\r\n planned_cycles AS \"Ciclos planificados\",\r\n cycle_completion_pct AS \"% avance\",\r\n stop_count AS \"Nº paros\",\r\n stop_min AS \"Min paro\",\r\n stop_cost_eur AS \"Coste paro €\",\r\n status_check AS \"Diagnóstico\",\r\n descripcion AS \"Descripción\"\r\nFROM mv_reports_ucepsa_demo.odoo_workorders_reality_history\r\nORDER BY COALESCE(odoo_end, odoo_start, snapshot_at) DESC\r\nLIMIT 20;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Histórico de órdenes", + "type": "table" + } + ], + "title": "Histórico Odoo vs máquina — Cortadora 01", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 57 + }, + "id": 29, + "panels": [ + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "aflbyx1ifu8zke" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [ + { + "options": { + "0": { + "color": "red", + "index": 0, + "text": "Parada" + }, + "1": { + "color": "green", + "index": 1, + "text": "En Marcha" + }, + "2": { + "color": "yellow", + "index": 2, + "text": "Desconocido" + }, + "3": { + "color": "semi-dark-purple", + "index": 3, + "text": "Sin telemetría" + } + }, + "type": "value" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 4, + "x": 0, + "y": 700 + }, + "id": 25, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n CASE\r\n WHEN age_s > 120 THEN 3\r\n WHEN machine_running = true THEN 1\r\n WHEN machine_running = false THEN 0\r\n ELSE 2\r\n END AS \"Estado máquina\"\r\nFROM mv_reports_ucepsa_demo.latest_machine_state_fast;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Estado actual máquina demo", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "aflbyx1ifu8zke" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [ + { + "options": { + "": { + "index": 0, + "text": "Sin paro abierto" + } + }, + "type": "value" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "text", + "value": 0 + }, + { + "color": "semi-dark-yellow", + "value": 5 + }, + { + "color": "red", + "value": 15 + } + ] + }, + "unit": "m" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 4, + "x": 4, + "y": 700 + }, + "id": 47, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n COALESCE((\r\n SELECT current_duration_min\r\n FROM mv_reports_ucepsa_demo.open_stops\r\n WHERE machine_id = 'CORT-01'\r\n ORDER BY started_at DESC\r\n LIMIT 1\r\n ), 0) AS \"Duración paro actual\";", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Duración paro actual", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "aflbyx1ifu8zke" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "text", + "value": 0 + }, + { + "color": "red", + "value": 1 + } + ] + }, + "unit": "currencyEUR" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 4, + "x": 8, + "y": 700 + }, + "id": 48, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n COALESCE((\r\n SELECT current_cost_eur\r\n FROM mv_reports_ucepsa_demo.open_stops\r\n WHERE machine_id = 'CORT-01'\r\n ORDER BY started_at DESC\r\n LIMIT 1\r\n ), 0) AS \"Coste paro actual\";", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Coste paro actual", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "aflbyx1ifu8zke" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "text", + "value": 0 + }, + { + "color": "#EAB839", + "value": 5 + }, + { + "color": "green", + "value": 8 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 4, + "x": 12, + "y": 700 + }, + "id": 49, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n cycle_rate_ppm AS \"Ciclos/min\"\r\nFROM mv_reports_ucepsa_demo.latest_machine_snapshot;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Ciclos/min actual", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "aflbyx1ifu8zke" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "text", + "value": 0 + }, + { + "color": "green", + "value": 1 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 4, + "x": 16, + "y": 700 + }, + "id": 50, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n cycle_pulse_count AS \"Pulsos acumulados\"\r\nFROM mv_reports_ucepsa_demo.latest_machine_snapshot;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Pulsos acumulados", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "aflbyx1ifu8zke" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisPlacement": "hidden", + "fillOpacity": 70, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineWidth": 0, + "spanNulls": false + }, + "mappings": [ + { + "options": { + "0": { + "color": "purple", + "index": 0, + "text": "Sin Telemetría" + }, + "1": { + "color": "green", + "index": 1, + "text": "En marcha" + }, + "2": { + "color": "red", + "index": 2, + "text": "Parada - causa pendiente" + }, + "3": { + "color": "semi-dark-yellow", + "index": 3, + "text": "Parada - causa clasificada" + } + }, + "type": "value" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 24, + "x": 0, + "y": 706 + }, + "id": 51, + "options": { + "alignValue": "left", + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "mergeValues": true, + "rowHeight": 0.9, + "showValue": "auto", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "WITH limits AS (\r\n SELECT\r\n $__timeFrom()::timestamptz AS t_from,\r\n LEAST($__timeTo()::timestamptz, now()) AS t_to,\r\n $__timeTo()::timestamptz AS grafana_to\r\n),\r\nbuckets AS (\r\n SELECT\r\n generate_series(\r\n t_from,\r\n t_to,\r\n interval '10 seconds'\r\n ) AS bucket\r\n FROM limits\r\n),\r\nstates AS (\r\n SELECT\r\n b.bucket AS \"time\",\r\n CASE\r\n -- 0. Si no hay telemetría reciente, el estado de máquina es desconocido.\r\n WHEN NOT EXISTS (\r\n SELECT 1\r\n FROM mv_hot.edge_oee_eastron_readings r\r\n WHERE r.asset = 'revpi_oee_node_01'\r\n AND r.ingest_ts > b.bucket - interval '30 seconds'\r\n AND r.ingest_ts <= b.bucket + interval '1 second'\r\n )\r\n THEN 0\r\n\r\n -- 2/3. Si hay un paro solapando ese bucket, mostrar estado de paro.\r\n WHEN EXISTS (\r\n SELECT 1\r\n FROM mv_reports_ucepsa_demo.stops_recent s\r\n WHERE s.machine_id = 'CORT-01'\r\n AND s.started_at <= b.bucket\r\n AND COALESCE(s.ended_at, now()) > b.bucket\r\n AND s.classification_status = 'PENDING'\r\n )\r\n THEN 2\r\n\r\n WHEN EXISTS (\r\n SELECT 1\r\n FROM mv_reports_ucepsa_demo.stops_recent s\r\n WHERE s.machine_id = 'CORT-01'\r\n AND s.started_at <= b.bucket\r\n AND COALESCE(s.ended_at, now()) > b.bucket\r\n AND s.classification_status = 'CLASSIFIED'\r\n )\r\n THEN 3\r\n\r\n -- 1. Hay telemetría y no hay paro abierto en ese instante.\r\n ELSE 1\r\n END AS \"CORT-01\"\r\n FROM buckets b\r\n),\r\ncutoff AS (\r\n SELECT\r\n t_to + interval '1 second' AS \"time\",\r\n NULL::int AS \"CORT-01\"\r\n FROM limits\r\n WHERE t_to < grafana_to\r\n)\r\nSELECT *\r\nFROM states\r\n\r\nUNION ALL\r\n\r\nSELECT *\r\nFROM cutoff\r\n\r\nORDER BY \"time\";", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Timeline estado máquina CORT-01", + "type": "state-timeline" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "aflbyx1ifu8zke" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisPlacement": "hidden", + "fillOpacity": 70, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineWidth": 0, + "spanNulls": false + }, + "mappings": [ + { + "options": { + "0": { + "color": "red", + "index": 0, + "text": "OFF / Paro" + }, + "1": { + "color": "green", + "index": 1, + "text": "ON / Marcha" + }, + "2": { + "color": "purple", + "index": 2, + "text": "Sin Telemetría" + } + }, + "type": "value" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 24, + "x": 0, + "y": 710 + }, + "id": 54, + "options": { + "alignValue": "left", + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "mergeValues": true, + "rowHeight": 0.9, + "showValue": "auto", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "WITH limits AS (\r\n SELECT\r\n $__timeFrom()::timestamptz AS t_from,\r\n LEAST($__timeTo()::timestamptz, now()) AS t_to,\r\n $__timeTo()::timestamptz AS grafana_to\r\n),\r\nbuckets AS (\r\n SELECT\r\n generate_series(\r\n t_from,\r\n t_to,\r\n interval '10 seconds'\r\n ) AS bucket\r\n FROM limits\r\n),\r\nstates AS (\r\n SELECT\r\n b.bucket AS \"time\",\r\n CASE\r\n WHEN NOT EXISTS (\r\n SELECT 1\r\n FROM mv_hot.edge_oee_eastron_readings r\r\n WHERE r.asset = 'revpi_oee_node_01'\r\n AND r.ingest_ts > b.bucket - interval '30 seconds'\r\n AND r.ingest_ts <= b.bucket + interval '1 second'\r\n )\r\n THEN 2\r\n\r\n WHEN (\r\n SELECT\r\n CASE\r\n WHEN (r.payload_json ->> 'machine_running')::boolean = true THEN 1\r\n WHEN (r.payload_json ->> 'machine_running')::boolean = false THEN 0\r\n ELSE 2\r\n END\r\n FROM mv_hot.edge_oee_eastron_readings r\r\n WHERE r.asset = 'revpi_oee_node_01'\r\n AND r.ingest_ts <= b.bucket\r\n ORDER BY r.ingest_ts DESC\r\n LIMIT 1\r\n ) = 1\r\n THEN 1\r\n\r\n ELSE 0\r\n END AS \"I_1 automático\"\r\n FROM buckets b\r\n),\r\ncutoff AS (\r\n SELECT\r\n t_to + interval '1 second' AS \"time\",\r\n NULL::int AS \"I_1 automático\"\r\n FROM limits\r\n WHERE t_to < grafana_to\r\n)\r\nSELECT *\r\nFROM states\r\n\r\nUNION ALL\r\n\r\nSELECT *\r\nFROM cutoff\r\n\r\nORDER BY \"time\";", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Eventos I_1 — Arranque / Paro", + "type": "state-timeline" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "aflbyx1ifu8zke" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "footer": { + "reducers": [] + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 24, + "x": 0, + "y": 714 + }, + "id": 26, + "options": { + "cellHeight": "sm", + "showHeader": true + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n id AS \"ID paro\",\r\n machine_id AS \"Máquina\",\r\n started_at AS \"Inicio\",\r\n current_duration_min AS \"Duración actual min\",\r\n current_cost_eur AS \"Coste actual €\",\r\n classification_status AS \"Clasificación\",\r\n COALESCE(cause_name, 'Pendiente') AS \"Causa\",\r\n operator_note AS \"Nota\"\r\nFROM mv_reports_ucepsa_demo.open_stops\r\nWHERE machine_id = 'CORT-01'\r\nORDER BY started_at DESC;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Paro abierto", + "type": "table" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "aflbyx1ifu8zke" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "footer": { + "reducers": [] + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 720 + }, + "id": 27, + "options": { + "cellHeight": "sm", + "showHeader": true + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n id AS \"ID\",\r\n machine_id AS \"Máquina\",\r\n started_at AS \"Inicio\",\r\n ended_at AS \"Fin\",\r\n status AS \"Estado\",\r\n classification_status AS \"Clasificación\",\r\n COALESCE(cause_name, 'Pendiente') AS \"Causa\",\r\n operator_note AS \"Nota\",\r\n duration_min AS \"Duración min\",\r\n cost_eur AS \"Coste €\"\r\nFROM mv_reports_ucepsa_demo.stops_recent\r\nWHERE machine_id = 'CORT-01'\r\n AND $__timeFilter(started_at)\r\nORDER BY started_at DESC\r\nLIMIT 20;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Últimos paros", + "type": "table" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "aflbyx1ifu8zke" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "fillOpacity": 80, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineWidth": 1, + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 12, + "w": 12, + "x": 0, + "y": 728 + }, + "id": 28, + "options": { + "barRadius": 0, + "barWidth": 0.97, + "fullHighlight": false, + "groupWidth": 0.7, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "orientation": "auto", + "showValue": "auto", + "stacking": "none", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + }, + "xTickLabelRotation": 0, + "xTickLabelSpacing": 0 + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n cause_name AS \"Causa\",\r\n total_cost_eur AS \"Coste €\"\r\nFROM mv_reports_ucepsa_demo.stop_summary\r\nORDER BY total_cost_eur DESC;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Coste por causa", + "type": "barchart" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "aflbyx1ifu8zke" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "fillOpacity": 80, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineWidth": 1, + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Minutos" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "semi-dark-yellow", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 12, + "w": 12, + "x": 12, + "y": 728 + }, + "id": 52, + "options": { + "barRadius": 0, + "barWidth": 0.97, + "fullHighlight": false, + "groupWidth": 0.7, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "orientation": "auto", + "showValue": "auto", + "stacking": "none", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + }, + "xTickLabelRotation": 0, + "xTickLabelSpacing": 0 + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n cause_name AS \"Causa\",\r\n total_min AS \"Minutos\"\r\nFROM mv_reports_ucepsa_demo.stop_summary\r\nORDER BY total_min DESC;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Coste por causa", + "type": "barchart" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "aflbyx1ifu8zke" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "footer": { + "reducers": [] + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 13, + "w": 24, + "x": 0, + "y": 740 + }, + "id": 24, + "options": { + "cellHeight": "sm", + "showHeader": true + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n event_ts AS \"Hora evento\",\r\n asset AS \"Nodo\",\r\n machine_id AS \"Máquina\",\r\n CASE\r\n WHEN event_name = 'machine_started' THEN 'Arranque'\r\n WHEN event_name = 'machine_stopped' THEN 'Paro'\r\n ELSE event_name\r\n END AS \"Evento\",\r\n previous_auto_signal AS \"Estado anterior\",\r\n new_auto_signal AS \"Estado nuevo\",\r\n machine_running AS \"Máquina en marcha\",\r\n reason_pending AS \"Causa pendiente\",\r\n cycle_pulse_count AS \"Pulsos acumulados\",\r\n cycle_rate_ppm AS \"Ciclos/min\"\r\nFROM mv_reports_ucepsa_demo.node_events_recent\r\nWHERE $__timeFilter(event_ts)\r\nORDER BY event_ts DESC\r\nLIMIT 50;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Eventos reales de máquina — RevPi", + "type": "table" + } + ], + "title": "Estado de máquina CORTADORA 01", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 58 + }, + "id": 16, + "panels": [ + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "aflbyx1ifu8zke" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [ + { + "options": { + "0": { + "color": "green", + "index": 0, + "text": "OK" + }, + "1": { + "color": "semi-dark-yellow", + "index": 1, + "text": "Warning" + }, + "2": { + "color": "purple", + "index": 2, + "text": "Sin Telemetría" + } + }, + "type": "value" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 4, + "x": 0, + "y": 832 + }, + "id": 17, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n CASE\r\n WHEN comm_ok = false THEN 3\r\n WHEN age_s > 120 THEN 2\r\n WHEN age_s > 30 THEN 1\r\n ELSE 0\r\n END AS \"Estado captura\"\r\nFROM mv_reports_ucepsa_demo.energy_latest;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Estado comunicación", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "aflbyx1ifu8zke" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 4, + "x": 4, + "y": 832 + }, + "id": 18, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n age_s AS \"Edad último dato\"\r\nFROM mv_reports_ucepsa_demo.energy_latest;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Edad último dato", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "aflbyx1ifu8zke" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "red", + "value": 0 + }, + { + "color": "green", + "value": 220 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "L1 V" + }, + "properties": [] + }, + { + "matcher": { + "id": "byName", + "options": "L2 V" + }, + "properties": [] + }, + { + "matcher": { + "id": "byName", + "options": "L3 V" + }, + "properties": [] + }, + { + "matcher": { + "id": "byName", + "options": "Hz" + }, + "properties": [] + } + ] + }, + "gridPos": { + "h": 6, + "w": 4, + "x": 8, + "y": 832 + }, + "id": 21, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n voltage_l1_v AS \"Tensión L1\"\r\nFROM mv_reports_ucepsa_demo.energy_latest;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Tensión L1", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "aflbyx1ifu8zke" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "amp" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "L1 A" + }, + "properties": [] + }, + { + "matcher": { + "id": "byName", + "options": "L2 A" + }, + "properties": [] + }, + { + "matcher": { + "id": "byName", + "options": "L3 A" + }, + "properties": [] + } + ] + }, + "gridPos": { + "h": 6, + "w": 4, + "x": 12, + "y": 832 + }, + "id": 22, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n current_l1_a AS \"Corriente L1\"\r\nFROM mv_reports_ucepsa_demo.energy_latest;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Corriente L1", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "aflbyx1ifu8zke" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "watt" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 4, + "x": 0, + "y": 838 + }, + "id": 19, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n power_total_kw * 1000 AS \"Potencia total\"\r\nFROM mv_reports_ucepsa_demo.energy_latest;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Potencia total", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "aflbyx1ifu8zke" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "watth" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 4, + "x": 4, + "y": 838 + }, + "id": 20, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n import_kwh * 1000 AS \"Energía acumulada\"\r\nFROM mv_reports_ucepsa_demo.energy_latest;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Energía acumulada", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "aflbyx1ifu8zke" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "watt" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 838 + }, + "id": 41, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n $__time(time),\r\n power_w AS \"Potencia W\"\r\nFROM mv_reports_ucepsa_demo.energy_power_timeseries\r\nWHERE $__timeFilter(time)\r\nORDER BY time;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Evolución de potencia", + "type": "timeseries" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "aflbyx1ifu8zke" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "watth" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 0, + "y": 844 + }, + "id": 42, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n $__time(time),\r\n import_wh AS \"Energía acumulada Wh\"\r\nFROM mv_reports_ucepsa_demo.energy_import_timeseries\r\nWHERE $__timeFilter(time)\r\nORDER BY time;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Energía acumulada", + "type": "timeseries" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "aflbyx1ifu8zke" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": -1, + "barWidthFactor": 1, + "drawStyle": "bars", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 8, + "y": 844 + }, + "id": 43, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "editorMode": "code", + "format": "time_series", + "rawQuery": true, + "rawSql": "WITH raw AS (\r\n SELECT\r\n time,\r\n import_wh,\r\n LAG(import_wh) OVER (ORDER BY time) AS prev_import_wh\r\n FROM mv_reports_ucepsa_demo.energy_import_timeseries\r\n WHERE $__timeFilter(time)\r\n)\r\nSELECT\r\n $__time(time),\r\n ROUND(GREATEST(import_wh - prev_import_wh, 0)::numeric, 4) AS \"Consumo incremental Wh\"\r\nFROM raw\r\nWHERE prev_import_wh IS NOT NULL\r\nORDER BY time;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Consumo por intervalo", + "type": "timeseries" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "aflbyx1ifu8zke" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "watt" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 0, + "y": 851 + }, + "id": 40, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n $__time(time),\r\n power_w AS \"Potencia W\"\r\nFROM mv_reports_ucepsa_demo.energy_power_timeseries\r\nWHERE $__timeFilter(time)\r\nORDER BY time;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Evolución de potencia", + "type": "timeseries" + } + ], + "title": "ENERGÍA CORTADORA 01", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 59 + }, + "id": 46, + "panels": [ + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "aflbyx1ifu8zke" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisPlacement": "auto", + "fillOpacity": 70, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineWidth": 0, + "spanNulls": false + }, + "mappings": [ + { + "options": { + "0": { + "color": "purple", + "index": 0, + "text": "Sin telemetría" + }, + "1": { + "color": "green", + "index": 1, + "text": "Recibiendo datos" + } + }, + "type": "value" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 24, + "x": 0, + "y": 885 + }, + "id": 44, + "options": { + "alignValue": "left", + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "mergeValues": true, + "rowHeight": 0.9, + "showValue": "auto", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "editorMode": "code", + "format": "time_series", + "rawQuery": true, + "rawSql": "WITH limits AS (\r\n SELECT\r\n $__timeFrom()::timestamptz AS t_from,\r\n LEAST($__timeTo()::timestamptz, now()) AS t_to,\r\n $__timeTo()::timestamptz AS grafana_to\r\n),\r\nbuckets AS (\r\n SELECT\r\n generate_series(\r\n t_from,\r\n t_to,\r\n interval '10 seconds'\r\n ) AS bucket\r\n FROM limits\r\n),\r\nstates AS (\r\n SELECT\r\n bucket AS \"time\",\r\n CASE\r\n WHEN EXISTS (\r\n SELECT 1\r\n FROM mv_reports_ucepsa_demo.live_telemetry r\r\n WHERE r.asset = 'revpi_oee_node_01'\r\n AND r.ingest_ts > bucket - interval '30 seconds'\r\n AND r.ingest_ts <= bucket + interval '1 second'\r\n )\r\n THEN 1\r\n ELSE 0\r\n END::int AS \"RevPi telemetría\"\r\n FROM buckets\r\n),\r\ncutoff AS (\r\n SELECT\r\n t_to + interval '1 second' AS \"time\",\r\n NULL::int AS \"RevPi telemetría\"\r\n FROM limits\r\n WHERE t_to < grafana_to\r\n)\r\nSELECT *\r\nFROM states\r\n\r\nUNION ALL\r\n\r\nSELECT *\r\nFROM cutoff\r\n\r\nORDER BY \"time\";", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Estado telemetría RevPi", + "type": "state-timeline" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "aflbyx1ifu8zke" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "footer": { + "reducers": [] + }, + "hideFrom": { + "viz": false + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 24, + "x": 0, + "y": 888 + }, + "id": 45, + "options": { + "cellHeight": "sm", + "showHeader": true + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "WITH latest AS (\r\n SELECT\r\n age_s,\r\n asset,\r\n power_total_kw,\r\n comm_ok,\r\n error_text\r\n FROM mv_reports_ucepsa_demo.energy_latest\r\n)\r\nSELECT\r\n 'Comunicación' AS \"Grupo\",\r\n 'Edad último dato' AS \"Parámetro\",\r\n age_s::numeric AS \"Valor\",\r\n 's' AS \"Unidad\",\r\n CASE\r\n WHEN comm_ok = false THEN 'Peligro'\r\n WHEN age_s > 120 THEN 'Peligro'\r\n WHEN age_s > 30 THEN 'Warning'\r\n ELSE 'OK'\r\n END AS \"Estado\",\r\n CASE\r\n WHEN comm_ok = false THEN COALESCE(error_text, 'Error de lectura del Eastron')\r\n WHEN age_s > 120 THEN 'No se reciben datos recientes'\r\n WHEN age_s > 30 THEN 'La lectura llega con retraso'\r\n ELSE 'Lectura reciente desde RevPi'\r\n END AS \"Qué está mal\",\r\n CASE\r\n WHEN comm_ok = false THEN 'La RevPi publica, pero la lectura Modbus no es válida'\r\n WHEN age_s > 120 THEN 'La máquina/nodo puede estar sin corriente o sin conexión'\r\n WHEN age_s > 30 THEN 'Puede haber latencia, reinicio o pérdida temporal de red'\r\n ELSE 'La captura industrial está viva'\r\n END AS \"Impacto probable\",\r\n CASE\r\n WHEN comm_ok = false THEN 'Revisar RS-485, SDM120-M, alimentación del contador y logs RevPi'\r\n WHEN age_s > 120 THEN 'Revisar alimentación de RevPi, red, MQTT, bridge y sink'\r\n WHEN age_s > 30 THEN 'Revisar estabilidad de red/MQTT'\r\n ELSE 'Sin acción'\r\n END AS \"Qué revisar / acción\"\r\nFROM latest\r\n\r\nUNION ALL\r\n\r\nSELECT\r\n 'Energía',\r\n 'Potencia total',\r\n power_total_kw,\r\n 'kW',\r\n CASE\r\n WHEN power_total_kw IS NULL THEN 'Warning'\r\n ELSE 'OK'\r\n END,\r\n CASE\r\n WHEN power_total_kw IS NULL THEN 'No hay potencia disponible'\r\n ELSE 'Potencia total leída correctamente'\r\n END,\r\n CASE\r\n WHEN power_total_kw IS NULL THEN 'No se puede validar carga eléctrica'\r\n ELSE 'Permite demostrar captura energética industrial'\r\n END,\r\n CASE\r\n WHEN power_total_kw IS NULL THEN 'Revisar lectura Modbus'\r\n ELSE 'Sin acción'\r\n END\r\nFROM latest;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Diagnóstico de captura", + "type": "table" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "aflbyx1ifu8zke" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "footer": { + "reducers": [] + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 24, + "x": 0, + "y": 897 + }, + "id": 53, + "options": { + "cellHeight": "sm", + "showHeader": true + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n event_ts AS \"Hora evento\",\r\n asset AS \"Nodo\",\r\n machine_id AS \"Máquina\",\r\n CASE\r\n WHEN event_name = 'machine_started' THEN 'Arranque'\r\n WHEN event_name = 'machine_stopped' THEN 'Paro'\r\n ELSE event_name\r\n END AS \"Evento\",\r\n previous_auto_signal AS \"Estado anterior\",\r\n new_auto_signal AS \"Estado nuevo\",\r\n machine_running AS \"Máquina en marcha\",\r\n reason_pending AS \"Causa pendiente\",\r\n cycle_pulse_count AS \"Pulsos acumulados\",\r\n cycle_rate_ppm AS \"Ciclos/min\"\r\nFROM mv_reports_ucepsa_demo.node_events_recent\r\nWHERE $__timeFilter(event_ts)\r\nORDER BY event_ts DESC\r\nLIMIT 50;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Eventos reales de máquina — RevPi", + "type": "table" + } + ], + "title": "RevPi", + "type": "row" + } + ], + "preload": false, + "refresh": "", + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now/d", + "to": "now/d" + }, + "timepicker": {}, + "timezone": "browser", + "title": "Control de pérdidas — Cortadoras 0, 1 y 2", + "uid": "akfp67r", + "version": 30, + "weekStart": "monday" +} diff --git a/ucepsa/edge-oee-demo/grafana/import/ucepsa-edge-oee-local.import.json b/ucepsa/edge-oee-demo/grafana/import/ucepsa-edge-oee-local.import.json new file mode 100644 index 0000000..866bce2 --- /dev/null +++ b/ucepsa/edge-oee-demo/grafana/import/ucepsa-edge-oee-local.import.json @@ -0,0 +1,5677 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": null, + "links": [], + "liveNow": true, + "panels": [ + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 2, + "panels": [ + { + "description": "", + "fieldConfig": { + "defaults": {}, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 16, + "x": 0, + "y": 2302 + }, + "id": 3, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "## Odoo dice qué orden está en curso. MESAVAULT muestra qué hizo realmente la máquina.\n\nLa demo convierte eventos de máquina en pérdidas, coste económico y acciones concretas:\n\n**dato → pérdida → euros → acción**", + "mode": "markdown" + }, + "pluginVersion": "12.3.2", + "title": "Odoo vs máquina", + "type": "text" + } + ], + "title": "Mensaje de negocio", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 1 + }, + "id": 4, + "panels": [ + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "mappings": [], + "min": -1, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "horas" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 4, + "x": 0, + "y": 247 + }, + "id": 5, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n value AS \"Horas analizadas\"\r\nFROM mv_hot.edge_oee_kpi_cards_demo\r\nWHERE kpi = 'Horas analizadas';", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Horas analizadas", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "horas" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 4, + "x": 4, + "y": 247 + }, + "id": 6, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n value AS \"Pérdidas clasificadas\"\r\nFROM mv_hot.edge_oee_kpi_cards_demo\r\nWHERE kpi = 'Pérdidas clasificadas';", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Pérdidas clasificadas", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "currencyEUR" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 4, + "x": 8, + "y": 247 + }, + "id": 7, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n value AS \"Coste estimado\"\r\nFROM mv_hot.edge_oee_kpi_cards_demo\r\nWHERE kpi = 'Coste estimado de pérdidas';", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Coste estimado de pérdidas", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 4, + "x": 12, + "y": 247 + }, + "id": 8, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n value AS \"Discrepancias\"\r\nFROM mv_hot.edge_oee_kpi_cards_demo\r\nWHERE kpi = 'Órdenes con discrepancia Odoo/máquina';", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Discrepancias Odoo/máquina", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "fillOpacity": 80, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineWidth": 1, + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + } + ] + }, + "unit": "currencyEUR" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 16, + "x": 0, + "y": 931 + }, + "id": 9, + "options": { + "barRadius": 0, + "barWidth": 0.8, + "fullHighlight": false, + "groupWidth": 0.7, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "orientation": "auto", + "showValue": "auto", + "stacking": "none", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + }, + "xField": "Causa", + "xTickLabelRotation": 0, + "xTickLabelSpacing": 0 + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n cause AS \"Causa\",\r\n monthly_cost_eur AS \"Coste €/mes\"\r\nFROM mv_hot.edge_oee_pareto_demo\r\nORDER BY ranking;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Pareto económico de pérdidas", + "type": "barchart" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "footer": { + "reducers": [] + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Ranking" + }, + "properties": [ + { + "id": "custom.width", + "value": 67 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Causa" + }, + "properties": [ + { + "id": "custom.width", + "value": 216 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Horas/mes" + }, + "properties": [ + { + "id": "custom.width", + "value": 87 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Coste €/mes" + }, + "properties": [ + { + "id": "custom.width", + "value": 100 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Acción recomendada" + }, + "properties": [ + { + "id": "custom.width", + "value": 410 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Esfuerzo" + }, + "properties": [ + { + "id": "custom.width", + "value": 112 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Prioridad" + }, + "properties": [ + { + "id": "custom.width", + "value": 106 + } + ] + } + ] + }, + "gridPos": { + "h": 3, + "w": 16, + "x": 0, + "y": 941 + }, + "id": 10, + "options": { + "cellHeight": "sm", + "showHeader": true, + "sortBy": [] + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n ranking AS \"Ranking\",\r\n cause AS \"Causa\",\r\n hours_month AS \"Horas/mes\",\r\n monthly_cost_eur AS \"Coste €/mes\",\r\n recommended_action AS \"Acción recomendada\",\r\n effort AS \"Esfuerzo\",\r\n priority AS \"Prioridad\",\r\n machine_focus AS \"Máquina foco\"\r\nFROM mv_hot.edge_oee_pareto_demo\r\nORDER BY ranking;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "", + "type": "table" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-GrYlRd" + }, + "custom": { + "axisPlacement": "auto", + "fillOpacity": 70, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineWidth": 0, + "spanNulls": false + }, + "mappings": [ + { + "options": { + "0": { + "index": 0, + "text": "Sin evento" + }, + "10": { + "index": 1, + "text": "Orden activa en Odoo" + }, + "20": { + "index": 2, + "text": "Preparación / búsqueda de bobina" + }, + "30": { + "index": 3, + "text": "Marcha real" + }, + "40": { + "index": 4, + "text": "Microparos" + }, + "50": { + "index": 5, + "text": "Cambio bobina" + }, + "60": { + "index": 6, + "text": "Paro" + }, + "90": { + "index": 7, + "text": "Otro evento" + } + }, + "type": "value" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "purple", + "value": 0 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 16, + "x": 0, + "y": 944 + }, + "id": 11, + "options": { + "alignValue": "left", + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "mergeValues": true, + "rowHeight": 0.9, + "showValue": "auto", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "time_series", + "rawQuery": true, + "rawSql": "WITH odoo AS (\r\n SELECT\r\n o.odoo_start AS time,\r\n 'Odoo / OF00481' AS metric,\r\n 10::numeric AS value\r\n FROM mv_hot.edge_oee_orders_demo o\r\n WHERE o.order_id = 'OF00481'\r\n\r\n UNION ALL\r\n\r\n SELECT\r\n o.odoo_end + interval '1 second' AS time,\r\n 'Odoo / OF00481' AS metric,\r\n 0::numeric AS value\r\n FROM mv_hot.edge_oee_orders_demo o\r\n WHERE o.order_id = 'OF00481'\r\n),\r\n\r\nmachine_start AS (\r\n SELECT\r\n e.start_time AS time,\r\n 'Cortadora 0 / realidad máquina' AS metric,\r\n CASE\r\n WHEN e.event_type = 'PREPARATION' THEN 20\r\n WHEN e.event_type = 'RUNNING' THEN 30\r\n WHEN e.event_type = 'MICROSTOP' THEN 40\r\n WHEN e.event_type = 'STOP' AND e.cause = 'Cambio bobina' THEN 50\r\n WHEN e.event_type = 'STOP' THEN 60\r\n ELSE 90\r\n END::numeric AS value\r\n FROM mv_hot.edge_oee_events_demo e\r\n WHERE e.order_id = 'OF00481'\r\n),\r\n\r\nmachine_end AS (\r\n SELECT\r\n e.end_time + interval '1 second' AS time,\r\n 'Cortadora 0 / realidad máquina' AS metric,\r\n 0::numeric AS value\r\n FROM mv_hot.edge_oee_events_demo e\r\n WHERE e.order_id = 'OF00481'\r\n)\r\n\r\nSELECT\r\n time AS \"time\",\r\n metric,\r\n value\r\nFROM (\r\n SELECT * FROM odoo\r\n UNION ALL\r\n SELECT * FROM machine_start\r\n UNION ALL\r\n SELECT * FROM machine_end\r\n) q\r\nORDER BY \"time\", metric;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "OF00481 — Odoo inicia orden, máquina arranca 22 min después", + "type": "state-timeline" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "footer": { + "reducers": [] + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Indicador" + }, + "properties": [ + { + "id": "custom.width", + "value": 279 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Valor" + }, + "properties": [ + { + "id": "custom.width", + "value": 50 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Unidad" + }, + "properties": [ + { + "id": "custom.width", + "value": 67 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Estado" + }, + "properties": [ + { + "id": "custom.width", + "value": 68 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Qué está mal" + }, + "properties": [ + { + "id": "custom.width", + "value": 397 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Impacto probable" + }, + "properties": [ + { + "id": "custom.width", + "value": 489 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Qué revisar / acción" + }, + "properties": [ + { + "id": "custom.width", + "value": 388 + } + ] + } + ] + }, + "gridPos": { + "h": 12, + "w": 16, + "x": 0, + "y": 949 + }, + "id": 12, + "options": { + "cellHeight": "sm", + "showHeader": true + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n \"Indicador\",\r\n \"Valor\",\r\n \"Unidad\",\r\n \"Estado\",\r\n \"Qué está mal\",\r\n \"Impacto probable\",\r\n \"Qué revisar / acción\"\r\nFROM mv_hot.edge_oee_data_quality_dashboard_demo\r\nORDER BY sort_order;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Calidad del dato Odoo / máquina", + "type": "table" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "footer": { + "reducers": [] + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "time" + }, + "properties": [ + { + "id": "custom.width", + "value": 158 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "A-series" + }, + "properties": [ + { + "id": "custom.width", + "value": 69 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Causa" + }, + "properties": [ + { + "id": "custom.width", + "value": 177 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Impacto estimado €/mes" + }, + "properties": [ + { + "id": "custom.width", + "value": 182 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Esfuerzo" + }, + "properties": [ + { + "id": "custom.width", + "value": 73 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Prioridad" + }, + "properties": [ + { + "id": "custom.width", + "value": 75 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Payback ejemplo meses" + }, + "properties": [ + { + "id": "custom.width", + "value": 177 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Inversión ejemplo €" + }, + "properties": [ + { + "id": "custom.width", + "value": 146 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Acción recomendada" + }, + "properties": [ + { + "id": "custom.width", + "value": 410 + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 16, + "x": 0, + "y": 961 + }, + "id": 13, + "options": { + "cellHeight": "sm", + "showHeader": true, + "sortBy": [] + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n \"Causa\",\r\n \"Impacto estimado €/mes\",\r\n \"Esfuerzo\",\r\n \"Prioridad\",\r\n \"Acción recomendada\",\r\n \"Inversión ejemplo €\",\r\n \"Payback ejemplo meses\"\r\nFROM mv_hot.edge_oee_actions_demo;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Acciones recomendadas", + "type": "table" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "footer": { + "reducers": [] + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Orden" + }, + "properties": [ + { + "id": "custom.width", + "value": 76 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Máquina" + }, + "properties": [ + { + "id": "custom.width", + "value": 93 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Producto" + }, + "properties": [ + { + "id": "custom.width", + "value": 115 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Reclamación" + }, + "properties": [ + { + "id": "custom.width", + "value": 99 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Microparos" + }, + "properties": [ + { + "id": "custom.width", + "value": 90 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Merma %" + }, + "properties": [ + { + "id": "custom.width", + "value": 78 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Energía vs media %" + }, + "properties": [ + { + "id": "custom.width", + "value": 147 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Interpretación" + }, + "properties": [ + { + "id": "custom.width", + "value": 889 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Acción recomendada" + }, + "properties": [ + { + "id": "custom.width", + "value": 949 + } + ] + } + ] + }, + "gridPos": { + "h": 9, + "w": 16, + "x": 0, + "y": 971 + }, + "id": 14, + "options": { + "cellHeight": "sm", + "showHeader": true, + "sortBy": [] + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n order_id AS \"Orden\",\r\n machine_name AS \"Máquina\",\r\n product AS \"Producto\",\r\n claim AS \"Reclamación\",\r\n sealing_adjustment_stops AS \"Paros ajuste sellado\",\r\n sealing_adjustment_min AS \"Min ajuste sellado\",\r\n microstops_count AS \"Microparos\",\r\n scrap_pct AS \"Merma %\",\r\n energy_vs_avg_pct AS \"Energía vs media %\",\r\n interpretation AS \"Interpretación\",\r\n recommended_action AS \"Acción recomendada\"\r\nFROM mv_hot.edge_oee_quality_case_demo;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Caso calidad — Sellado débil OF00493", + "type": "table" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "kwatt" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 16, + "x": 0, + "y": 980 + }, + "id": 15, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n ts AS \"time\",\r\n power_kw AS \"Potencia kW\"\r\nFROM mv_hot.edge_oee_energy_demo\r\nWHERE machine_id = 'C2'\r\nORDER BY ts;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Energía durante OF00493 — Cortadora 2", + "type": "timeseries" + } + ], + "title": "KPIs de dirección", + "type": "row" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 2 + }, + "id": 30, + "panels": [], + "title": "OEE — CORTADORA 01", + "type": "row" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "red", + "value": 0 + }, + { + "color": "green", + "value": 80 + } + ] + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 9, + "x": 0, + "y": 3 + }, + "id": 39, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n oee_pct AS \"OEE %\",\r\n availability_pct AS \"Disponibilidad %\",\r\n performance_pct AS \"Rendimiento %\",\r\n quality_pct AS \"Calidad %\"\r\nFROM mv_reports_ucepsa_demo.oee_kpis(\r\n $__timeFrom()::timestamptz,\r\n $__timeTo()::timestamptz\r\n);", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "OEE", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 1, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "text", + "value": 0 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 10, + "x": 9, + "y": 3 + }, + "id": 31, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n running_min AS \"Min marcha\",\r\n stop_min AS \"Min paro\",\r\n actual_cycles AS \"Ciclos\"\r\nFROM mv_reports_ucepsa_demo.oee_operational_data(\r\n $__timeFrom()::timestamptz,\r\n $__timeTo()::timestamptz\r\n);", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Datos operativos", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "fillOpacity": 80, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineWidth": 1, + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 19, + "x": 0, + "y": 7 + }, + "id": 37, + "options": { + "barRadius": 0, + "barWidth": 0.97, + "fullHighlight": false, + "groupWidth": 0.7, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "orientation": "auto", + "showValue": "auto", + "stacking": "none", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + }, + "xTickLabelRotation": 0, + "xTickLabelSpacing": 0 + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n COALESCE(cause_name, 'Pendiente de clasificar') AS \"Causa\",\r\n ROUND(SUM(cost_eur)::numeric, 2) AS \"Coste €\",\r\n ROUND(SUM(duration_min)::numeric, 2) AS \"Minutos\"\r\nFROM mv_reports_ucepsa_demo.stops_recent\r\nWHERE machine_id = 'CORT-01'\r\n AND $__timeFilter(started_at)\r\nGROUP BY COALESCE(cause_name, 'Pendiente de clasificar')\r\nORDER BY \"Coste €\" DESC;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Pérdida real por causa", + "type": "barchart" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 19, + "x": 0, + "y": 16 + }, + "id": 33, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n ts AS \"time\",\r\n cycle_rate_ppm AS \"Ciclos/min\",\r\n 10.0 AS \"Objetivo ciclos/min\"\r\nFROM mv_hot.edge_oee_live_telemetry\r\nWHERE $__timeFilter(ts)\r\n AND asset = 'revpi_oee_node_01'\r\n AND cycle_rate_ppm IS NOT NULL\r\nORDER BY ts;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Velocidad real — ciclos/min", + "type": "timeseries" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "footer": { + "reducers": [] + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "ID" + }, + "properties": [ + { + "id": "custom.width", + "value": 50 + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 19, + "x": 0, + "y": 22 + }, + "id": 36, + "options": { + "cellHeight": "sm", + "showHeader": true, + "sortBy": [] + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n id AS \"ID\",\r\n machine_id AS \"Máquina\",\r\n started_at AS \"Inicio\",\r\n ended_at AS \"Fin\",\r\n status AS \"Estado\",\r\n classification_status AS \"Clasificación\",\r\n COALESCE(cause_name, 'Pendiente') AS \"Causa\",\r\n operator_note AS \"Nota\",\r\n duration_min AS \"Duración min\",\r\n cost_eur AS \"Coste €\"\r\nFROM mv_hot.edge_oee_stops_recent\r\nWHERE $__timeFilter(started_at)\r\nORDER BY started_at DESC\r\nLIMIT 20;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Últimos paros clasificados / pendientes", + "type": "table" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 28 + }, + "id": 55, + "panels": [], + "title": "Odoo vs máquina — Cortadora 01", + "type": "row" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "mappings": [ + { + "options": { + "0": { + "color": "green", + "index": 0, + "text": "OK" + }, + "2": { + "color": "red", + "index": 1, + "text": "Discrepancia" + }, + "3": { + "color": "purple", + "index": 2, + "text": "Sin Telemetría" + } + }, + "type": "value" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 4, + "x": 0, + "y": 29 + }, + "id": 57, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n CASE\r\n WHEN status_check IN ('OK', 'ORDEN_PREPARADA_MAQUINA_PARADA') THEN 0\r\n WHEN status_check = 'SIN_TELEMETRIA' THEN 3\r\n ELSE 2\r\n END AS \"Estado cruce\"\r\nFROM mv_reports_ucepsa_demo.odoo_vs_machine_demo;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Discrepancias Odoo/máquina", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "noValue": "No iniciada", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "m" + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 4, + "x": 4, + "y": 29 + }, + "id": 58, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n CASE\r\n WHEN odoo_is_started = true THEN start_delay_min\r\n ELSE NULL\r\n END AS \"Retraso inicio real\"\r\nFROM mv_reports_ucepsa_demo.odoo_vs_machine_demo;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Retraso inicio real", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 0.1 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 7, + "x": 0, + "y": 34 + }, + "id": 59, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n stop_min AS \"Min paro en orden\",\r\n stop_cost_eur AS \"Coste paro en orden\"\r\nFROM mv_reports_ucepsa_demo.odoo_vs_machine_demo;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Paros dentro de orden", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "text", + "value": 0 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 9, + "x": 7, + "y": 34 + }, + "id": 60, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n real_cycles AS \"Ciclos reales\",\r\n planned_cycles AS \"Ciclos planificados\",\r\n cycle_completion_pct AS \"% avance ciclos\"\r\nFROM mv_reports_ucepsa_demo.odoo_vs_machine_demo;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Ciclos reales vs planificados", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "footer": { + "reducers": [] + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Orden Odoo" + }, + "properties": [ + { + "id": "custom.width", + "value": 112 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Producto" + }, + "properties": [ + { + "id": "custom.width", + "value": 260 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Estado" + }, + "properties": [ + { + "id": "custom.width", + "value": 72 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Máquina" + }, + "properties": [ + { + "id": "custom.width", + "value": 72 + } + ] + } + ] + }, + "gridPos": { + "h": 4, + "w": 24, + "x": 0, + "y": 39 + }, + "id": 56, + "options": { + "cellHeight": "sm", + "showHeader": true, + "sortBy": [] + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n odoo_order_ref AS \"Orden Odoo\",\r\n product AS \"Producto\",\r\n odoo_state AS \"Estado\",\r\n machine_id AS \"Máquina\",\r\n CASE\r\n WHEN odoo_is_started = true THEN to_char(odoo_start, 'YYYY-MM-DD HH24:MI:SS')\r\n ELSE 'No iniciada'\r\n END AS \"Inicio Odoo\"\r\nFROM mv_reports_ucepsa_demo.odoo_current_order_demo;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Orden Odoo seleccionada", + "type": "table" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "footer": { + "reducers": [] + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Orden" + }, + "properties": [ + { + "id": "custom.width", + "value": 115 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Estado Odoo" + }, + "properties": [ + { + "id": "custom.width", + "value": 100 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Edad telemetría s" + }, + "properties": [ + { + "id": "custom.width", + "value": 131 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Estado máquina" + }, + "properties": [ + { + "id": "custom.width", + "value": 143 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Diagnóstico" + }, + "properties": [ + { + "id": "custom.width", + "value": 330 + } + ] + } + ] + }, + "gridPos": { + "h": 4, + "w": 24, + "x": 0, + "y": 43 + }, + "id": 62, + "options": { + "cellHeight": "sm", + "showHeader": true, + "sortBy": [ + { + "desc": false, + "displayName": "Estado máquina" + } + ] + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n odoo_order_ref AS \"Orden\",\r\n odoo_state AS \"Estado Odoo\",\r\n CASE\r\n WHEN machine_running THEN 'En marcha'\r\n ELSE 'Parada'\r\n END AS \"Estado máquina\",\r\n telemetry_age_s AS \"Edad telemetría s\",\r\n status_check AS \"Diagnóstico\"\r\nFROM mv_reports_ucepsa_demo.odoo_vs_machine_demo;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Estado Odoo / realidad", + "type": "table" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "footer": { + "reducers": [] + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Orden" + }, + "properties": [ + { + "id": "custom.width", + "value": 115 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Hora chequeo" + }, + "properties": [ + { + "id": "custom.width", + "value": 155 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Máquina" + }, + "properties": [ + { + "id": "custom.width", + "value": 72 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Estado" + }, + "properties": [ + { + "id": "custom.width", + "value": 331 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Descripción" + }, + "properties": [ + { + "id": "custom.width", + "value": 633 + } + ] + } + ] + }, + "gridPos": { + "h": 9, + "w": 24, + "x": 0, + "y": 47 + }, + "id": 61, + "options": { + "cellHeight": "sm", + "showHeader": true + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n checked_at AS \"Hora chequeo\",\r\n odoo_order_ref AS \"Orden\",\r\n product AS \"Producto\",\r\n machine_id AS \"Máquina\",\r\n status_check AS \"Estado\",\r\n descripcion AS \"Descripción\",\r\n start_delay_min AS \"Retraso min\",\r\n stop_count AS \"Nº paros\",\r\n stop_min AS \"Min paro\",\r\n stop_cost_eur AS \"Coste paro €\",\r\n cycle_completion_pct AS \"% avance ciclos\"\r\nFROM mv_reports_ucepsa_demo.odoo_discrepancies_demo;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Tabla de discrepancias", + "type": "table" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 56 + }, + "id": 64, + "panels": [ + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "fillOpacity": 80, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineWidth": 1, + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "currencyEUR" + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 11, + "x": 0, + "y": 248 + }, + "id": 66, + "options": { + "barRadius": 0, + "barWidth": 0.97, + "fullHighlight": false, + "groupWidth": 0.7, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "orientation": "auto", + "showValue": "auto", + "stacking": "none", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + }, + "xTickLabelRotation": 0, + "xTickLabelSpacing": 0 + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n odoo_order_ref AS \"Orden\",\r\n stop_cost_eur AS \"Coste paro €\"\r\nFROM mv_reports_ucepsa_demo.odoo_workorders_reality_history\r\nWHERE stop_cost_eur > 0\r\nORDER BY stop_cost_eur DESC\r\nLIMIT 10;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Coste de paros por orden", + "type": "barchart" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "fillOpacity": 80, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineWidth": 1, + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "m" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Min paro" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "yellow", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 9, + "w": 13, + "x": 11, + "y": 248 + }, + "id": 67, + "options": { + "barRadius": 0, + "barWidth": 0.97, + "fullHighlight": false, + "groupWidth": 0.7, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "orientation": "auto", + "showValue": "auto", + "stacking": "none", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + }, + "xTickLabelRotation": 0, + "xTickLabelSpacing": 0 + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n odoo_order_ref AS \"Orden\",\r\n stop_min AS \"Min paro\"\r\nFROM mv_reports_ucepsa_demo.odoo_workorders_reality_history\r\nWHERE stop_min > 0\r\nORDER BY stop_min DESC\r\nLIMIT 10;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Tiempo de paro por orden", + "type": "barchart" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "footer": { + "reducers": [] + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 24, + "x": 0, + "y": 257 + }, + "id": 63, + "options": { + "cellHeight": "sm", + "showHeader": true + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n odoo_order_ref AS \"Orden\",\r\n product AS \"Producto\",\r\n odoo_state AS \"Estado\",\r\n machine_id AS \"Máquina\",\r\n odoo_start AS \"Inicio Odoo\",\r\n odoo_end AS \"Fin Odoo\",\r\n real_cycles AS \"Ciclos reales\",\r\n planned_cycles AS \"Ciclos planificados\",\r\n cycle_completion_pct AS \"% avance\",\r\n stop_count AS \"Nº paros\",\r\n stop_min AS \"Min paro\",\r\n stop_cost_eur AS \"Coste paro €\",\r\n status_check AS \"Diagnóstico\"\r\nFROM mv_reports_ucepsa_demo.odoo_last_finished_order_demo;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Última orden finalizada", + "type": "table" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "footer": { + "reducers": [] + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 17, + "w": 24, + "x": 0, + "y": 261 + }, + "id": 65, + "options": { + "cellHeight": "sm", + "showHeader": true + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n odoo_order_ref AS \"Orden\",\r\n odoo_state AS \"Estado\",\r\n product AS \"Producto\",\r\n machine_id AS \"Máquina\",\r\n odoo_start AS \"Inicio Odoo\",\r\n odoo_end AS \"Fin Odoo\",\r\n real_cycles AS \"Ciclos reales\",\r\n planned_cycles AS \"Ciclos planificados\",\r\n cycle_completion_pct AS \"% avance\",\r\n stop_count AS \"Nº paros\",\r\n stop_min AS \"Min paro\",\r\n stop_cost_eur AS \"Coste paro €\",\r\n status_check AS \"Diagnóstico\",\r\n descripcion AS \"Descripción\"\r\nFROM mv_reports_ucepsa_demo.odoo_workorders_reality_history\r\nORDER BY COALESCE(odoo_end, odoo_start, snapshot_at) DESC\r\nLIMIT 20;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Histórico de órdenes", + "type": "table" + } + ], + "title": "Histórico Odoo vs máquina — Cortadora 01", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 57 + }, + "id": 29, + "panels": [ + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [ + { + "options": { + "0": { + "color": "red", + "index": 0, + "text": "Parada" + }, + "1": { + "color": "green", + "index": 1, + "text": "En Marcha" + }, + "2": { + "color": "yellow", + "index": 2, + "text": "Desconocido" + }, + "3": { + "color": "semi-dark-purple", + "index": 3, + "text": "Sin telemetría" + } + }, + "type": "value" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 4, + "x": 0, + "y": 700 + }, + "id": 25, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n CASE\r\n WHEN age_s > 120 THEN 3\r\n WHEN machine_running = true THEN 1\r\n WHEN machine_running = false THEN 0\r\n ELSE 2\r\n END AS \"Estado máquina\"\r\nFROM mv_reports_ucepsa_demo.latest_machine_state_fast;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Estado actual máquina demo", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [ + { + "options": { + "": { + "index": 0, + "text": "Sin paro abierto" + } + }, + "type": "value" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "text", + "value": 0 + }, + { + "color": "semi-dark-yellow", + "value": 5 + }, + { + "color": "red", + "value": 15 + } + ] + }, + "unit": "m" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 4, + "x": 4, + "y": 700 + }, + "id": 47, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n COALESCE((\r\n SELECT current_duration_min\r\n FROM mv_reports_ucepsa_demo.open_stops\r\n WHERE machine_id = 'CORT-01'\r\n ORDER BY started_at DESC\r\n LIMIT 1\r\n ), 0) AS \"Duración paro actual\";", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Duración paro actual", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "text", + "value": 0 + }, + { + "color": "red", + "value": 1 + } + ] + }, + "unit": "currencyEUR" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 4, + "x": 8, + "y": 700 + }, + "id": 48, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n COALESCE((\r\n SELECT current_cost_eur\r\n FROM mv_reports_ucepsa_demo.open_stops\r\n WHERE machine_id = 'CORT-01'\r\n ORDER BY started_at DESC\r\n LIMIT 1\r\n ), 0) AS \"Coste paro actual\";", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Coste paro actual", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "text", + "value": 0 + }, + { + "color": "#EAB839", + "value": 5 + }, + { + "color": "green", + "value": 8 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 4, + "x": 12, + "y": 700 + }, + "id": 49, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n cycle_rate_ppm AS \"Ciclos/min\"\r\nFROM mv_reports_ucepsa_demo.latest_machine_snapshot;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Ciclos/min actual", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "text", + "value": 0 + }, + { + "color": "green", + "value": 1 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 4, + "x": 16, + "y": 700 + }, + "id": 50, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n cycle_pulse_count AS \"Pulsos acumulados\"\r\nFROM mv_reports_ucepsa_demo.latest_machine_snapshot;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Pulsos acumulados", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisPlacement": "hidden", + "fillOpacity": 70, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineWidth": 0, + "spanNulls": false + }, + "mappings": [ + { + "options": { + "0": { + "color": "purple", + "index": 0, + "text": "Sin Telemetría" + }, + "1": { + "color": "green", + "index": 1, + "text": "En marcha" + }, + "2": { + "color": "red", + "index": 2, + "text": "Parada - causa pendiente" + }, + "3": { + "color": "semi-dark-yellow", + "index": 3, + "text": "Parada - causa clasificada" + } + }, + "type": "value" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 24, + "x": 0, + "y": 706 + }, + "id": 51, + "options": { + "alignValue": "left", + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "mergeValues": true, + "rowHeight": 0.9, + "showValue": "auto", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "WITH limits AS (\r\n SELECT\r\n $__timeFrom()::timestamptz AS t_from,\r\n LEAST($__timeTo()::timestamptz, now()) AS t_to,\r\n $__timeTo()::timestamptz AS grafana_to\r\n),\r\nbuckets AS (\r\n SELECT\r\n generate_series(\r\n t_from,\r\n t_to,\r\n interval '10 seconds'\r\n ) AS bucket\r\n FROM limits\r\n),\r\nstates AS (\r\n SELECT\r\n b.bucket AS \"time\",\r\n CASE\r\n -- 0. Si no hay telemetría reciente, el estado de máquina es desconocido.\r\n WHEN NOT EXISTS (\r\n SELECT 1\r\n FROM mv_hot.edge_oee_eastron_readings r\r\n WHERE r.asset = 'revpi_oee_node_01'\r\n AND r.ingest_ts > b.bucket - interval '30 seconds'\r\n AND r.ingest_ts <= b.bucket + interval '1 second'\r\n )\r\n THEN 0\r\n\r\n -- 2/3. Si hay un paro solapando ese bucket, mostrar estado de paro.\r\n WHEN EXISTS (\r\n SELECT 1\r\n FROM mv_reports_ucepsa_demo.stops_recent s\r\n WHERE s.machine_id = 'CORT-01'\r\n AND s.started_at <= b.bucket\r\n AND COALESCE(s.ended_at, now()) > b.bucket\r\n AND s.classification_status = 'PENDING'\r\n )\r\n THEN 2\r\n\r\n WHEN EXISTS (\r\n SELECT 1\r\n FROM mv_reports_ucepsa_demo.stops_recent s\r\n WHERE s.machine_id = 'CORT-01'\r\n AND s.started_at <= b.bucket\r\n AND COALESCE(s.ended_at, now()) > b.bucket\r\n AND s.classification_status = 'CLASSIFIED'\r\n )\r\n THEN 3\r\n\r\n -- 1. Hay telemetría y no hay paro abierto en ese instante.\r\n ELSE 1\r\n END AS \"CORT-01\"\r\n FROM buckets b\r\n),\r\ncutoff AS (\r\n SELECT\r\n t_to + interval '1 second' AS \"time\",\r\n NULL::int AS \"CORT-01\"\r\n FROM limits\r\n WHERE t_to < grafana_to\r\n)\r\nSELECT *\r\nFROM states\r\n\r\nUNION ALL\r\n\r\nSELECT *\r\nFROM cutoff\r\n\r\nORDER BY \"time\";", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Timeline estado máquina CORT-01", + "type": "state-timeline" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisPlacement": "hidden", + "fillOpacity": 70, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineWidth": 0, + "spanNulls": false + }, + "mappings": [ + { + "options": { + "0": { + "color": "red", + "index": 0, + "text": "OFF / Paro" + }, + "1": { + "color": "green", + "index": 1, + "text": "ON / Marcha" + }, + "2": { + "color": "purple", + "index": 2, + "text": "Sin Telemetría" + } + }, + "type": "value" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 24, + "x": 0, + "y": 710 + }, + "id": 54, + "options": { + "alignValue": "left", + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "mergeValues": true, + "rowHeight": 0.9, + "showValue": "auto", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "WITH limits AS (\r\n SELECT\r\n $__timeFrom()::timestamptz AS t_from,\r\n LEAST($__timeTo()::timestamptz, now()) AS t_to,\r\n $__timeTo()::timestamptz AS grafana_to\r\n),\r\nbuckets AS (\r\n SELECT\r\n generate_series(\r\n t_from,\r\n t_to,\r\n interval '10 seconds'\r\n ) AS bucket\r\n FROM limits\r\n),\r\nstates AS (\r\n SELECT\r\n b.bucket AS \"time\",\r\n CASE\r\n WHEN NOT EXISTS (\r\n SELECT 1\r\n FROM mv_hot.edge_oee_eastron_readings r\r\n WHERE r.asset = 'revpi_oee_node_01'\r\n AND r.ingest_ts > b.bucket - interval '30 seconds'\r\n AND r.ingest_ts <= b.bucket + interval '1 second'\r\n )\r\n THEN 2\r\n\r\n WHEN (\r\n SELECT\r\n CASE\r\n WHEN (r.payload_json ->> 'machine_running')::boolean = true THEN 1\r\n WHEN (r.payload_json ->> 'machine_running')::boolean = false THEN 0\r\n ELSE 2\r\n END\r\n FROM mv_hot.edge_oee_eastron_readings r\r\n WHERE r.asset = 'revpi_oee_node_01'\r\n AND r.ingest_ts <= b.bucket\r\n ORDER BY r.ingest_ts DESC\r\n LIMIT 1\r\n ) = 1\r\n THEN 1\r\n\r\n ELSE 0\r\n END AS \"I_1 automático\"\r\n FROM buckets b\r\n),\r\ncutoff AS (\r\n SELECT\r\n t_to + interval '1 second' AS \"time\",\r\n NULL::int AS \"I_1 automático\"\r\n FROM limits\r\n WHERE t_to < grafana_to\r\n)\r\nSELECT *\r\nFROM states\r\n\r\nUNION ALL\r\n\r\nSELECT *\r\nFROM cutoff\r\n\r\nORDER BY \"time\";", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Eventos I_1 — Arranque / Paro", + "type": "state-timeline" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "footer": { + "reducers": [] + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 24, + "x": 0, + "y": 714 + }, + "id": 26, + "options": { + "cellHeight": "sm", + "showHeader": true + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n id AS \"ID paro\",\r\n machine_id AS \"Máquina\",\r\n started_at AS \"Inicio\",\r\n current_duration_min AS \"Duración actual min\",\r\n current_cost_eur AS \"Coste actual €\",\r\n classification_status AS \"Clasificación\",\r\n COALESCE(cause_name, 'Pendiente') AS \"Causa\",\r\n operator_note AS \"Nota\"\r\nFROM mv_reports_ucepsa_demo.open_stops\r\nWHERE machine_id = 'CORT-01'\r\nORDER BY started_at DESC;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Paro abierto", + "type": "table" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "footer": { + "reducers": [] + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 720 + }, + "id": 27, + "options": { + "cellHeight": "sm", + "showHeader": true + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n id AS \"ID\",\r\n machine_id AS \"Máquina\",\r\n started_at AS \"Inicio\",\r\n ended_at AS \"Fin\",\r\n status AS \"Estado\",\r\n classification_status AS \"Clasificación\",\r\n COALESCE(cause_name, 'Pendiente') AS \"Causa\",\r\n operator_note AS \"Nota\",\r\n duration_min AS \"Duración min\",\r\n cost_eur AS \"Coste €\"\r\nFROM mv_reports_ucepsa_demo.stops_recent\r\nWHERE machine_id = 'CORT-01'\r\n AND $__timeFilter(started_at)\r\nORDER BY started_at DESC\r\nLIMIT 20;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Últimos paros", + "type": "table" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "fillOpacity": 80, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineWidth": 1, + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 12, + "w": 12, + "x": 0, + "y": 728 + }, + "id": 28, + "options": { + "barRadius": 0, + "barWidth": 0.97, + "fullHighlight": false, + "groupWidth": 0.7, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "orientation": "auto", + "showValue": "auto", + "stacking": "none", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + }, + "xTickLabelRotation": 0, + "xTickLabelSpacing": 0 + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n cause_name AS \"Causa\",\r\n total_cost_eur AS \"Coste €\"\r\nFROM mv_reports_ucepsa_demo.stop_summary\r\nORDER BY total_cost_eur DESC;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Coste por causa", + "type": "barchart" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "fillOpacity": 80, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineWidth": 1, + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Minutos" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "semi-dark-yellow", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 12, + "w": 12, + "x": 12, + "y": 728 + }, + "id": 52, + "options": { + "barRadius": 0, + "barWidth": 0.97, + "fullHighlight": false, + "groupWidth": 0.7, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "orientation": "auto", + "showValue": "auto", + "stacking": "none", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + }, + "xTickLabelRotation": 0, + "xTickLabelSpacing": 0 + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n cause_name AS \"Causa\",\r\n total_min AS \"Minutos\"\r\nFROM mv_reports_ucepsa_demo.stop_summary\r\nORDER BY total_min DESC;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Coste por causa", + "type": "barchart" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "footer": { + "reducers": [] + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 13, + "w": 24, + "x": 0, + "y": 740 + }, + "id": 24, + "options": { + "cellHeight": "sm", + "showHeader": true + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n event_ts AS \"Hora evento\",\r\n asset AS \"Nodo\",\r\n machine_id AS \"Máquina\",\r\n CASE\r\n WHEN event_name = 'machine_started' THEN 'Arranque'\r\n WHEN event_name = 'machine_stopped' THEN 'Paro'\r\n ELSE event_name\r\n END AS \"Evento\",\r\n previous_auto_signal AS \"Estado anterior\",\r\n new_auto_signal AS \"Estado nuevo\",\r\n machine_running AS \"Máquina en marcha\",\r\n reason_pending AS \"Causa pendiente\",\r\n cycle_pulse_count AS \"Pulsos acumulados\",\r\n cycle_rate_ppm AS \"Ciclos/min\"\r\nFROM mv_reports_ucepsa_demo.node_events_recent\r\nWHERE $__timeFilter(event_ts)\r\nORDER BY event_ts DESC\r\nLIMIT 50;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Eventos reales de máquina — RevPi", + "type": "table" + } + ], + "title": "Estado de máquina CORTADORA 01", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 58 + }, + "id": 16, + "panels": [ + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [ + { + "options": { + "0": { + "color": "green", + "index": 0, + "text": "OK" + }, + "1": { + "color": "semi-dark-yellow", + "index": 1, + "text": "Warning" + }, + "2": { + "color": "purple", + "index": 2, + "text": "Sin Telemetría" + } + }, + "type": "value" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 4, + "x": 0, + "y": 832 + }, + "id": 17, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n CASE\r\n WHEN comm_ok = false THEN 3\r\n WHEN age_s > 120 THEN 2\r\n WHEN age_s > 30 THEN 1\r\n ELSE 0\r\n END AS \"Estado captura\"\r\nFROM mv_reports_ucepsa_demo.energy_latest;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Estado comunicación", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 4, + "x": 4, + "y": 832 + }, + "id": 18, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n age_s AS \"Edad último dato\"\r\nFROM mv_reports_ucepsa_demo.energy_latest;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Edad último dato", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "red", + "value": 0 + }, + { + "color": "green", + "value": 220 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "L1 V" + }, + "properties": [] + }, + { + "matcher": { + "id": "byName", + "options": "L2 V" + }, + "properties": [] + }, + { + "matcher": { + "id": "byName", + "options": "L3 V" + }, + "properties": [] + }, + { + "matcher": { + "id": "byName", + "options": "Hz" + }, + "properties": [] + } + ] + }, + "gridPos": { + "h": 6, + "w": 4, + "x": 8, + "y": 832 + }, + "id": 21, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n voltage_l1_v AS \"Tensión L1\"\r\nFROM mv_reports_ucepsa_demo.energy_latest;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Tensión L1", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "amp" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "L1 A" + }, + "properties": [] + }, + { + "matcher": { + "id": "byName", + "options": "L2 A" + }, + "properties": [] + }, + { + "matcher": { + "id": "byName", + "options": "L3 A" + }, + "properties": [] + } + ] + }, + "gridPos": { + "h": 6, + "w": 4, + "x": 12, + "y": 832 + }, + "id": 22, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n current_l1_a AS \"Corriente L1\"\r\nFROM mv_reports_ucepsa_demo.energy_latest;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Corriente L1", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "watt" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 4, + "x": 0, + "y": 838 + }, + "id": 19, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n power_total_kw * 1000 AS \"Potencia total\"\r\nFROM mv_reports_ucepsa_demo.energy_latest;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Potencia total", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "watth" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 4, + "x": 4, + "y": 838 + }, + "id": 20, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n import_kwh * 1000 AS \"Energía acumulada\"\r\nFROM mv_reports_ucepsa_demo.energy_latest;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Energía acumulada", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "watt" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 838 + }, + "id": 41, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n $__time(time),\r\n power_w AS \"Potencia W\"\r\nFROM mv_reports_ucepsa_demo.energy_power_timeseries\r\nWHERE $__timeFilter(time)\r\nORDER BY time;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Evolución de potencia", + "type": "timeseries" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "watth" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 0, + "y": 844 + }, + "id": 42, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n $__time(time),\r\n import_wh AS \"Energía acumulada Wh\"\r\nFROM mv_reports_ucepsa_demo.energy_import_timeseries\r\nWHERE $__timeFilter(time)\r\nORDER BY time;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Energía acumulada", + "type": "timeseries" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": -1, + "barWidthFactor": 1, + "drawStyle": "bars", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 8, + "y": 844 + }, + "id": 43, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "editorMode": "code", + "format": "time_series", + "rawQuery": true, + "rawSql": "WITH raw AS (\r\n SELECT\r\n time,\r\n import_wh,\r\n LAG(import_wh) OVER (ORDER BY time) AS prev_import_wh\r\n FROM mv_reports_ucepsa_demo.energy_import_timeseries\r\n WHERE $__timeFilter(time)\r\n)\r\nSELECT\r\n $__time(time),\r\n ROUND(GREATEST(import_wh - prev_import_wh, 0)::numeric, 4) AS \"Consumo incremental Wh\"\r\nFROM raw\r\nWHERE prev_import_wh IS NOT NULL\r\nORDER BY time;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Consumo por intervalo", + "type": "timeseries" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "watt" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 0, + "y": 851 + }, + "id": 40, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n $__time(time),\r\n power_w AS \"Potencia W\"\r\nFROM mv_reports_ucepsa_demo.energy_power_timeseries\r\nWHERE $__timeFilter(time)\r\nORDER BY time;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Evolución de potencia", + "type": "timeseries" + } + ], + "title": "ENERGÍA CORTADORA 01", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 59 + }, + "id": 46, + "panels": [ + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisPlacement": "auto", + "fillOpacity": 70, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineWidth": 0, + "spanNulls": false + }, + "mappings": [ + { + "options": { + "0": { + "color": "purple", + "index": 0, + "text": "Sin telemetría" + }, + "1": { + "color": "green", + "index": 1, + "text": "Recibiendo datos" + } + }, + "type": "value" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 24, + "x": 0, + "y": 885 + }, + "id": 44, + "options": { + "alignValue": "left", + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "mergeValues": true, + "rowHeight": 0.9, + "showValue": "auto", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "editorMode": "code", + "format": "time_series", + "rawQuery": true, + "rawSql": "WITH limits AS (\r\n SELECT\r\n $__timeFrom()::timestamptz AS t_from,\r\n LEAST($__timeTo()::timestamptz, now()) AS t_to,\r\n $__timeTo()::timestamptz AS grafana_to\r\n),\r\nbuckets AS (\r\n SELECT\r\n generate_series(\r\n t_from,\r\n t_to,\r\n interval '10 seconds'\r\n ) AS bucket\r\n FROM limits\r\n),\r\nstates AS (\r\n SELECT\r\n bucket AS \"time\",\r\n CASE\r\n WHEN EXISTS (\r\n SELECT 1\r\n FROM mv_reports_ucepsa_demo.live_telemetry r\r\n WHERE r.asset = 'revpi_oee_node_01'\r\n AND r.ingest_ts > bucket - interval '30 seconds'\r\n AND r.ingest_ts <= bucket + interval '1 second'\r\n )\r\n THEN 1\r\n ELSE 0\r\n END::int AS \"RevPi telemetría\"\r\n FROM buckets\r\n),\r\ncutoff AS (\r\n SELECT\r\n t_to + interval '1 second' AS \"time\",\r\n NULL::int AS \"RevPi telemetría\"\r\n FROM limits\r\n WHERE t_to < grafana_to\r\n)\r\nSELECT *\r\nFROM states\r\n\r\nUNION ALL\r\n\r\nSELECT *\r\nFROM cutoff\r\n\r\nORDER BY \"time\";", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Estado telemetría RevPi", + "type": "state-timeline" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "footer": { + "reducers": [] + }, + "hideFrom": { + "viz": false + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 24, + "x": 0, + "y": 888 + }, + "id": 45, + "options": { + "cellHeight": "sm", + "showHeader": true + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "WITH latest AS (\r\n SELECT\r\n age_s,\r\n asset,\r\n power_total_kw,\r\n comm_ok,\r\n error_text\r\n FROM mv_reports_ucepsa_demo.energy_latest\r\n)\r\nSELECT\r\n 'Comunicación' AS \"Grupo\",\r\n 'Edad último dato' AS \"Parámetro\",\r\n age_s::numeric AS \"Valor\",\r\n 's' AS \"Unidad\",\r\n CASE\r\n WHEN comm_ok = false THEN 'Peligro'\r\n WHEN age_s > 120 THEN 'Peligro'\r\n WHEN age_s > 30 THEN 'Warning'\r\n ELSE 'OK'\r\n END AS \"Estado\",\r\n CASE\r\n WHEN comm_ok = false THEN COALESCE(error_text, 'Error de lectura del Eastron')\r\n WHEN age_s > 120 THEN 'No se reciben datos recientes'\r\n WHEN age_s > 30 THEN 'La lectura llega con retraso'\r\n ELSE 'Lectura reciente desde RevPi'\r\n END AS \"Qué está mal\",\r\n CASE\r\n WHEN comm_ok = false THEN 'La RevPi publica, pero la lectura Modbus no es válida'\r\n WHEN age_s > 120 THEN 'La máquina/nodo puede estar sin corriente o sin conexión'\r\n WHEN age_s > 30 THEN 'Puede haber latencia, reinicio o pérdida temporal de red'\r\n ELSE 'La captura industrial está viva'\r\n END AS \"Impacto probable\",\r\n CASE\r\n WHEN comm_ok = false THEN 'Revisar RS-485, SDM120-M, alimentación del contador y logs RevPi'\r\n WHEN age_s > 120 THEN 'Revisar alimentación de RevPi, red, MQTT, bridge y sink'\r\n WHEN age_s > 30 THEN 'Revisar estabilidad de red/MQTT'\r\n ELSE 'Sin acción'\r\n END AS \"Qué revisar / acción\"\r\nFROM latest\r\n\r\nUNION ALL\r\n\r\nSELECT\r\n 'Energía',\r\n 'Potencia total',\r\n power_total_kw,\r\n 'kW',\r\n CASE\r\n WHEN power_total_kw IS NULL THEN 'Warning'\r\n ELSE 'OK'\r\n END,\r\n CASE\r\n WHEN power_total_kw IS NULL THEN 'No hay potencia disponible'\r\n ELSE 'Potencia total leída correctamente'\r\n END,\r\n CASE\r\n WHEN power_total_kw IS NULL THEN 'No se puede validar carga eléctrica'\r\n ELSE 'Permite demostrar captura energética industrial'\r\n END,\r\n CASE\r\n WHEN power_total_kw IS NULL THEN 'Revisar lectura Modbus'\r\n ELSE 'Sin acción'\r\n END\r\nFROM latest;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Diagnóstico de captura", + "type": "table" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "bfnbcasbm6hhca" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "footer": { + "reducers": [] + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 24, + "x": 0, + "y": 897 + }, + "id": 53, + "options": { + "cellHeight": "sm", + "showHeader": true + }, + "pluginVersion": "12.3.2", + "targets": [ + { + "dataset": "mesavault_hot", + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\r\n event_ts AS \"Hora evento\",\r\n asset AS \"Nodo\",\r\n machine_id AS \"Máquina\",\r\n CASE\r\n WHEN event_name = 'machine_started' THEN 'Arranque'\r\n WHEN event_name = 'machine_stopped' THEN 'Paro'\r\n ELSE event_name\r\n END AS \"Evento\",\r\n previous_auto_signal AS \"Estado anterior\",\r\n new_auto_signal AS \"Estado nuevo\",\r\n machine_running AS \"Máquina en marcha\",\r\n reason_pending AS \"Causa pendiente\",\r\n cycle_pulse_count AS \"Pulsos acumulados\",\r\n cycle_rate_ppm AS \"Ciclos/min\"\r\nFROM mv_reports_ucepsa_demo.node_events_recent\r\nWHERE $__timeFilter(event_ts)\r\nORDER BY event_ts DESC\r\nLIMIT 50;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Eventos reales de máquina — RevPi", + "type": "table" + } + ], + "title": "RevPi", + "type": "row" + } + ], + "preload": false, + "refresh": "10s", + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-1h", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "MESAVAULT Edge-OEE — UCEPSA local", + "uid": "ucepsa-edge-oee-local", + "version": 30, + "weekStart": "monday" +} \ No newline at end of file diff --git a/ucepsa/edge-oee-demo/grafana/scripts/create_machine_dashboard.py b/ucepsa/edge-oee-demo/grafana/scripts/create_machine_dashboard.py new file mode 100755 index 0000000..6570a1a --- /dev/null +++ b/ucepsa/edge-oee-demo/grafana/scripts/create_machine_dashboard.py @@ -0,0 +1,524 @@ +#!/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" + + +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() + + req = urllib.request.Request( + BASE_URL + path, + data=data, + headers=headers, + method=method, + ) + + try: + with urllib.request.urlopen(req, timeout=20) as resp: + body = resp.read().decode() + return json.loads(body) if body else {} + except urllib.error.HTTPError as e: + print("HTTP ERROR:", e.code, e.read().decode(), file=sys.stderr) + raise + + +datasources = api("GET", "/api/datasources") + +print("Datasources encontrados:") +for d in datasources: + print(f"- name={d.get('name')} type={d.get('type')} uid={d.get('uid')} url={d.get('url')} database={d.get('database')}") + +pg = next( + ( + d 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() + ), + None, +) + +if not pg: + print("ERROR: no encuentro datasource PostgreSQL/MESAVAULT en Grafana", file=sys.stderr) + sys.exit(1) + +DS = { + "type": "postgres", + "uid": pg["uid"], +} + + +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", mappings=None): + field_defaults = { + "unit": unit, + "thresholds": { + "mode": "absolute", + "steps": [ + {"color": "green", "value": None}, + ], + }, + } + + if mappings: + field_defaults["mappings"] = mappings + + return { + "id": panel_id, + "type": "stat", + "title": title, + "datasource": DS, + "gridPos": {"x": x, "y": y, "w": w, "h": h}, + "targets": [target(sql)], + "options": { + "reduceOptions": { + "values": False, + "calcs": ["lastNotNull"], + "fields": "", + }, + "orientation": "auto", + "textMode": "auto", + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + }, + "fieldConfig": { + "defaults": field_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 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)], + "options": { + "showHeader": True, + "cellHeight": "sm", + }, + "fieldConfig": { + "defaults": {}, + "overrides": [], + }, + } + + +comm_mapping = [ + { + "type": "value", + "options": { + "1": {"text": "OK", "color": "green"}, + "0": {"text": "NO OK", "color": "red"}, + }, + } +] + +run_mapping = [ + { + "type": "value", + "options": { + "1": {"text": "EN MARCHA", "color": "green"}, + "0": {"text": "PARADA", "color": "yellow"}, + }, + } +] + +dashboard = { + "id": None, + "uid": DASHBOARD_UID, + "title": "MESAVAULT Edge-OEE — Máquina", + "tags": ["mesavault", "ucepsa", "edge-oee"], + "timezone": "browser", + "schemaVersion": 39, + "version": 1, + "refresh": "5s", + "time": { + "from": "now-30m", + "to": "now", + }, + "templating": { + "list": [ + { + "name": "machine_id", + "type": "query", + "label": "Máquina", + "datasource": DS, + "refresh": 1, + "sort": 1, + "multi": False, + "includeAll": False, + "query": "SELECT machine_id FROM mv_reports_ucepsa_demo.machine_state_latest_multi ORDER BY machine_id", + "current": { + "text": "CORT-00", + "value": "CORT-00", + "selected": True, + }, + } + ] + }, + "panels": [ + stat_panel( + 1, + "Comunicación", + 0, + 0, + 6, + 4, + """ + SELECT + CASE + WHEN age_s < 30 AND comm_ok = true AND COALESCE(sample_valid, true) = true THEN 1 + ELSE 0 + END AS value + FROM mv_reports_ucepsa_demo.machine_state_latest_multi + WHERE machine_id = '$machine_id' + """, + mappings=comm_mapping, + ), + stat_panel( + 2, + "Edad último dato", + 6, + 0, + 6, + 4, + """ + SELECT age_s AS value + FROM mv_reports_ucepsa_demo.machine_state_latest_multi + WHERE machine_id = '$machine_id' + """, + unit="s", + ), + stat_panel( + 3, + "Estado máquina", + 12, + 0, + 6, + 4, + """ + SELECT + CASE WHEN machine_running = true THEN 1 ELSE 0 END AS value + FROM mv_reports_ucepsa_demo.machine_state_latest_multi + WHERE machine_id = '$machine_id' + """, + mappings=run_mapping, + ), + stat_panel( + 4, + "Ciclos/min", + 18, + 0, + 6, + 4, + """ + SELECT cycle_rate_ppm AS value + FROM mv_reports_ucepsa_demo.machine_state_latest_multi + WHERE machine_id = '$machine_id' + """, + unit="cpm", + ), + stat_panel( + 5, + "Potencia total", + 0, + 4, + 6, + 4, + """ + SELECT power_total_kw AS value + FROM mv_reports_ucepsa_demo.machine_state_latest_multi + WHERE machine_id = '$machine_id' + """, + unit="kwatt", + ), + stat_panel( + 6, + "Energía importada", + 6, + 4, + 6, + 4, + """ + SELECT import_kwh AS value + FROM mv_reports_ucepsa_demo.machine_state_latest_multi + WHERE machine_id = '$machine_id' + """, + unit="kwatth", + ), + stat_panel( + 7, + "Frecuencia", + 12, + 4, + 6, + 4, + """ + SELECT frequency_hz AS value + FROM mv_reports_ucepsa_demo.machine_state_latest_multi + WHERE machine_id = '$machine_id' + """, + unit="hertz", + ), + stat_panel( + 8, + "Pulsos acumulados", + 18, + 4, + 6, + 4, + """ + SELECT cycle_pulse_count AS value + FROM mv_reports_ucepsa_demo.machine_state_latest_multi + WHERE machine_id = '$machine_id' + """, + unit="none", + ), + timeseries_panel( + 9, + "Potencia por fase", + 0, + 8, + 12, + 8, + """ + SELECT + ts AS "time", + active_power_l1_w AS "L1 W", + active_power_l2_w AS "L2 W", + active_power_l3_w AS "L3 W", + active_power_w AS "Total W" + FROM mv_reports_ucepsa_demo.energy_3phase_timeseries + WHERE $__timeFilter(ts) + AND site = 'ucepsa_onpremise' + AND machine_id = '$machine_id' + ORDER BY ts + """, + unit="watt", + ), + timeseries_panel( + 10, + "Tensiones L1/L2/L3", + 12, + 8, + 12, + 8, + """ + SELECT + ts AS "time", + voltage_l1_v AS "L1 V", + voltage_l2_v AS "L2 V", + voltage_l3_v AS "L3 V" + FROM mv_reports_ucepsa_demo.energy_3phase_timeseries + WHERE $__timeFilter(ts) + AND site = 'ucepsa_onpremise' + AND machine_id = '$machine_id' + ORDER BY ts + """, + unit="volt", + ), + timeseries_panel( + 11, + "Corrientes L1/L2/L3", + 0, + 16, + 12, + 8, + """ + SELECT + ts AS "time", + current_l1_a AS "L1 A", + current_l2_a AS "L2 A", + current_l3_a AS "L3 A" + FROM mv_reports_ucepsa_demo.energy_3phase_timeseries + WHERE $__timeFilter(ts) + AND site = 'ucepsa_onpremise' + AND machine_id = '$machine_id' + ORDER BY ts + """, + unit="amp", + ), + table_panel( + 12, + "Última lectura trifásica", + 12, + 16, + 12, + 8, + """ + SELECT + machine_id, + round(age_s::numeric, 1) AS age_s, + comm_ok, + sample_valid, + energy_map, + machine_running, + cycle_input_state, + cycle_pulse_count, + wise_cycle_counter_raw, + round(voltage_l1_v::numeric, 2) AS v_l1, + round(voltage_l2_v::numeric, 2) AS v_l2, + round(voltage_l3_v::numeric, 2) AS v_l3, + round(current_l1_a::numeric, 3) AS i_l1, + round(current_l2_a::numeric, 3) AS i_l2, + round(current_l3_a::numeric, 3) AS i_l3, + round(power_total_kw::numeric, 4) AS kw_total, + digital_inputs + FROM mv_reports_ucepsa_demo.machine_state_latest_multi + WHERE machine_id = '$machine_id' + """, + ), + table_panel( + 13, + "Odoo vs máquina", + 0, + 24, + 12, + 7, + """ + SELECT + machine_id, + order_ref, + product_name, + odoo_state, + machine_running, + telemetry_age_s, + cycle_rate_ppm, + status_check, + diagnostic_message + FROM mv_reports_ucepsa_demo.odoo_vs_machine_demo + WHERE machine_id = '$machine_id' + """, + ), + table_panel( + 14, + "Loss Intelligence", + 12, + 24, + 12, + 7, + """ + SELECT + priority_rank, + priority_score, + priority_level, + machine_id, + order_ref, + loss_type, + severity, + duration_min, + cost_eur, + status, + description, + recommended_action + FROM mv_reports_ucepsa_demo.loss_priority_board + WHERE machine_id = '$machine_id' + ORDER BY priority_rank + """, + ), + ], +} + +payload = { + "dashboard": dashboard, + "overwrite": True, + "message": "Create MESAVAULT Edge-OEE machine dashboard with machine selector", +} + +result = api("POST", "/api/dashboards/db", payload) + +print("Dashboard creado/actualizado:") +print(BASE_URL + result.get("url", f"/d/{DASHBOARD_UID}")) diff --git a/ucepsa/edge-oee-demo/grafana/scripts/patch_machine_dashboard_pareto_clean.py b/ucepsa/edge-oee-demo/grafana/scripts/patch_machine_dashboard_pareto_clean.py new file mode 100755 index 0000000..d4ad456 --- /dev/null +++ b/ucepsa/edge-oee-demo/grafana/scripts/patch_machine_dashboard_pareto_clean.py @@ -0,0 +1,114 @@ +#!/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" + + +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 walk_panels(panels): + for p in panels or []: + yield p + for child in walk_panels(p.get("panels", [])): + yield child + + +doc = api("GET", f"/api/dashboards/uid/{DASHBOARD_UID}") +dashboard = doc["dashboard"] +meta = doc.get("meta", {}) + +changed = 0 + +for panel in walk_panels(dashboard.get("panels", [])): + if panel.get("title") in ("Pareto causas de paro", "Pareto causas clasificadas"): + panel["title"] = "Pareto causas clasificadas por minutos" + + for target in panel.get("targets", []): + target["rawSql"] = """ + SELECT + cause_name AS "Causa", + ROUND(SUM(duration_min)::numeric, 2) AS "Minutos" + FROM mv_reports_ucepsa_demo.machine_stops_grafana + WHERE machine_id = '$machine_id' + AND classification_status = 'CLASSIFIED' + AND status = 'CLOSED' + GROUP BY cause_name + ORDER BY SUM(duration_min) DESC + LIMIT 10 + """ + target["format"] = "table" + + panel.setdefault("fieldConfig", {}).setdefault("defaults", {})["unit"] = "min" + changed += 1 + +payload = { + "dashboard": dashboard, + "overwrite": True, + "message": "Clean stop cause pareto panel", +} + +if meta.get("folderUid"): + payload["folderUid"] = meta["folderUid"] + +result = api("POST", "/api/dashboards/db", payload) + +print(f"Paneles modificados: {changed}") +print("Dashboard actualizado:") +print(BASE_URL + result.get("url", f"/d/{DASHBOARD_UID}")) diff --git a/ucepsa/edge-oee-demo/grafana/scripts/patch_machine_dashboard_stops.py b/ucepsa/edge-oee-demo/grafana/scripts/patch_machine_dashboard_stops.py new file mode 100755 index 0000000..02a2a04 --- /dev/null +++ b/ucepsa/edge-oee-demo/grafana/scripts/patch_machine_dashboard_stops.py @@ -0,0 +1,507 @@ +#!/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}")) diff --git a/ucepsa/edge-oee-demo/grafana/scripts/patch_machine_dashboard_stops_pareto.py b/ucepsa/edge-oee-demo/grafana/scripts/patch_machine_dashboard_stops_pareto.py new file mode 100755 index 0000000..52be783 --- /dev/null +++ b/ucepsa/edge-oee-demo/grafana/scripts/patch_machine_dashboard_stops_pareto.py @@ -0,0 +1,151 @@ +#!/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" + + +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 all_panels(panels): + out = [] + for p in panels or []: + out.append(p) + out.extend(all_panels(p.get("panels", []))) + return out + + +doc = api("GET", f"/api/dashboards/uid/{DASHBOARD_UID}") +dashboard = doc["dashboard"] +meta = doc.get("meta", {}) + +changed = 0 + +for panel in all_panels(dashboard.get("panels", [])): + title = panel.get("title", "") + + if title == "Nº paros": + panel["title"] = "Nº paros en rango" + changed += 1 + + elif title == "Minutos paro": + panel["title"] = "Minutos paro en rango" + changed += 1 + + elif title == "Coste paros": + panel["title"] = "Coste paros en rango" + changed += 1 + + elif title == "Duración media": + panel["title"] = "Duración media en rango" + changed += 1 + + elif title == "Pareto causas de paro": + panel["title"] = "Pareto causas clasificadas" + for target in panel.get("targets", []): + target["rawSql"] = """ + 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 classification_status = 'CLASSIFIED' + GROUP BY cause_name + ORDER BY SUM(duration_min) DESC + LIMIT 10 + """ + target["format"] = "table" + changed += 1 + + elif title == "Paros pendientes de clasificación": + # Dejamos este panel sin filtro temporal: es backlog operativo. + for target in panel.get("targets", []): + target["rawSql"] = """ + 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 + """ + changed += 1 + +payload = { + "dashboard": dashboard, + "overwrite": True, + "message": "Fix stop report pareto and clarify time-scoped panels", +} + +if meta.get("folderUid"): + payload["folderUid"] = meta["folderUid"] + +result = api("POST", "/api/dashboards/db", payload) + +print(f"Paneles modificados: {changed}") +print("Dashboard actualizado:") +print(BASE_URL + result.get("url", f"/d/{DASHBOARD_UID}")) diff --git a/ucepsa/edge-oee-demo/revpi/edge-oee/.env.example b/ucepsa/edge-oee-demo/revpi/edge-oee/.env.example new file mode 100644 index 0000000..b980b32 --- /dev/null +++ b/ucepsa/edge-oee-demo/revpi/edge-oee/.env.example @@ -0,0 +1,30 @@ +MQTT_HOST=17.126.1.70 +MQTT_PORT=1883 +MQTT_USER=revpi_ucepsa_edge_oee_01 +MQTT_PASSWORD=***REDACTED*** +MQTT_CLIENT_ID=ucepsa_edge_oee_revpi_cutters_node_01 + +TENANT=ucepsa +SITE=ucepsa_test +VERTICAL=edge_oee +ASSET=revpi_oee_node_01 + +MQTT_TOPIC_TELEMETRY=vertical/ucepsa/ucepsa_test/edge_oee/revpi_oee_node_01/telemetry +MQTT_TOPIC_STATUS=vertical/ucepsa/ucepsa_test/edge_oee/revpi_oee_node_01/status +MQTT_TOPIC_EVENT=vertical/ucepsa/ucepsa_test/edge_oee/revpi_oee_node_01/event + +MODBUS_PORT=/dev/ttyRS485 +MODBUS_SLAVE=1 +MODBUS_BAUD=9600 +MODBUS_PARITY=N +MODBUS_STOPBITS=1 +MODBUS_BYTESIZE=8 +MODBUS_TIMEOUT=1.5 + +DI_AUTO_INPUT=I_1 +DI_CYCLE_INPUT=I_2 +DI_DEBOUNCE_MS=50 +CYCLE_RATE_WINDOW_SECONDS=60 +PUBLISH_INTERVAL_SECONDS=10 + +MACHINE_ID=CORT-01 diff --git a/ucepsa/edge-oee-demo/revpi/edge-oee/REVPI-INVENTORY.md b/ucepsa/edge-oee-demo/revpi/edge-oee/REVPI-INVENTORY.md new file mode 100644 index 0000000..708d11a --- /dev/null +++ b/ucepsa/edge-oee-demo/revpi/edge-oee/REVPI-INVENTORY.md @@ -0,0 +1,41 @@ +# RevPi Edge-OEE Inventory + +## Identificación + +Hostname: + +IP: + +Modelo RevPi: + +Sistema operativo: + +Kernel: + +## Red + +Interfaz Ethernet: + +IP: + +Gateway: + +DNS: + +## Serie / RS-485 + +Dispositivos detectados: + +- /dev/... + +## Uso previsto + +Captura industrial real para demo MESAVAULT Edge-OEE. + +Primera señal objetivo: + +- Eastron Modbus RTU +- lectura de tensión +- corriente +- potencia +- kWh diff --git a/ucepsa/edge-oee-demo/revpi/edge-oee/beacon_controller.py b/ucepsa/edge-oee-demo/revpi/edge-oee/beacon_controller.py new file mode 100755 index 0000000..f0cd826 --- /dev/null +++ b/ucepsa/edge-oee-demo/revpi/edge-oee/beacon_controller.py @@ -0,0 +1,474 @@ +#!/usr/bin/env python3 +import json +import os +import time +from datetime import datetime +from pathlib import Path + +import paho.mqtt.client as mqtt +from pymodbus.client import ModbusTcpClient + + +DO0_BLUE_COIL = 16 +DO1_RED_COIL = 17 + +TELEMETRY_TTL_S = 30 +ORDER_TTL_S = 30 +BLINK_PERIOD_S = 1.0 +OFFLINE_LOG_INTERVAL_S = 30 + +WISE_NODES_FILE = Path("wise_nodes.json") + + +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 + k, v = line.split("=", 1) + env[k.strip()] = v.strip().strip('"').strip("'") + return env + + +def now_ts(): + return time.time() + + +def load_nodes(): + data = json.loads(WISE_NODES_FILE.read_text()) + nodes = {} + + for node in data.get("nodes", []): + if not node.get("enabled", True): + continue + + machine_id = node["machine_id"] + host = ( + node.get("wise_ip") + or node.get("wise_host") + or node.get("host") + or node.get("ip") + ) + + if not host: + raise RuntimeError(f"Nodo sin IP: {node}") + + nodes[machine_id] = { + "machine_id": machine_id, + "source_node": node.get("source_node", ""), + "host": host, + "port": int(node.get("wise_port", 502)), + "unit": int(node.get("unit", 1)), + } + + return nodes + + +def bool_from_payload(value): + return bool(value) + + +def order_is_fresh(order): + if not order: + return False + return (now_ts() - order.get("_rx_ts", 0)) <= ORDER_TTL_S + + +def telemetry_is_fresh(telemetry): + if not telemetry: + return False + return (now_ts() - telemetry.get("_rx_ts", 0)) <= TELEMETRY_TTL_S + + +class WiseOutputs: + def __init__(self, nodes): + self.nodes = nodes + self.clients = {} + self.last_written = {} + self.last_error_ts = {} + self.offline_until = {} + self.offline_retry_s = float(os.getenv("WISE_OFFLINE_RETRY_S", "15")) + + def get_client(self, machine_id): + client = self.clients.get(machine_id) + if client is not None: + return client + + node = self.nodes[machine_id] + client = ModbusTcpClient(node["host"], port=node["port"], timeout=2) + + if not client.connect(): + try: + client.close() + except Exception: + pass + raise RuntimeError(f"No conecta con {machine_id} {node['host']}:{node['port']}") + + self.clients[machine_id] = client + return client + + def close_client(self, machine_id): + client = self.clients.pop(machine_id, None) + if client is not None: + try: + client.close() + except Exception: + pass + + def in_offline_backoff(self, machine_id): + return now_ts() < self.offline_until.get(machine_id, 0) + + def mark_offline(self, machine_id): + self.offline_until[machine_id] = now_ts() + self.offline_retry_s + self.last_written.pop(machine_id, None) + + def write_coil(self, machine_id, address, value): + node = self.nodes[machine_id] + client = self.get_client(machine_id) + + rr = client.write_coil( + address=address, + value=bool(value), + device_id=node["unit"], + ) + + if rr.isError(): + self.close_client(machine_id) + raise RuntimeError(f"Error escribiendo {machine_id} coil {address}: {rr}") + + def set_beacon(self, machine_id, blue_on, red_on): + do0 = bool(blue_on) + + # Roja con contacto NC: + # rojo ON -> DO1 OFF + # rojo OFF -> DO1 ON + do1 = not bool(red_on) + + current = (do0, do1) + previous = self.last_written.get(machine_id) + + if previous == current: + return + + self.write_coil(machine_id, DO0_BLUE_COIL, do0) + self.write_coil(machine_id, DO1_RED_COIL, do1) + + self.last_written[machine_id] = current + self.offline_until.pop(machine_id, None) + + if os.getenv("BEACON_LOG_WRITES", "0").lower() in ("1", "true", "yes", "on"): + print( + f"BEACON {machine_id} " + f"blue_on={blue_on} red_on={red_on} " + f"DO0={do0} DO1={do1}", + flush=True, + ) + + def log_offline_error(self, machine_id, reason): + now = now_ts() + last = self.last_error_ts.get(machine_id, 0) + + if now - last >= OFFLINE_LOG_INTERVAL_S: + self.last_error_ts[machine_id] = now + print(f"WISE_OFFLINE {machine_id} {reason}", flush=True) + + def safe_connected_only(self): + for machine_id in list(self.clients.keys()): + try: + self.set_beacon(machine_id, blue_on=False, red_on=True) + except Exception as e: + print(f"ERROR dejando safe {machine_id}: {e}", flush=True) + self.close_client(machine_id) + + def close_all(self): + for machine_id in list(self.clients.keys()): + self.close_client(machine_id) + + +def mode_to_bool(mode, blink_on): + if mode == "ON": + return True + if mode == "OFF": + return False + if mode == "BLINK": + return blink_on + return False + + +def decide_modes(machine_id, telemetry, order): + """ + Devuelve: + blue_mode, red_mode, reason, should_write + + should_write=False significa: + máquina probablemente sin corriente y sin orden cargada. + No intentamos escribir en WISE para evitar ruido. + """ + + tel_fresh = telemetry_is_fresh(telemetry) + ord_fresh = order_is_fresh(order) + + order_present = False + order_coherent = False + force_machine_fault = False + + if ord_fresh: + order_present = bool_from_payload(order.get("order_present", False)) + order_coherent = bool_from_payload(order.get("order_coherent", False)) + force_machine_fault = bool_from_payload(order.get("force_machine_fault", False)) + + order_valid_loaded = order_present and order_coherent + + if not tel_fresh and not order_present: + return "OFF", "OFF", "machine_power_off_or_wise_offline_without_order", False + + if not tel_fresh and order_valid_loaded: + return "ON", "BLINK", "order_loaded_but_no_telemetry_or_wise_offline", True + + if not tel_fresh and order_present and not order_coherent: + return "BLINK", "BLINK", "odoo_incoherent_but_no_telemetry_or_wise_offline", True + + comm_ok = bool_from_payload(telemetry.get("comm_ok", False)) + sample_valid = bool_from_payload(telemetry.get("sample_valid", False)) + + if not comm_ok or not sample_valid: + return "OFF", "BLINK", "telemetry_invalid", True + + machine_running = bool_from_payload(telemetry.get("machine_running", False)) + last_cycle_age_s = telemetry.get("last_cycle_age_s") + + machine_fault = force_machine_fault + + if machine_running and isinstance(last_cycle_age_s, (int, float)) and last_cycle_age_s > 20: + machine_fault = True + + odoo_incoherent = False + + if machine_running and not order_valid_loaded: + odoo_incoherent = True + + if order_present and not order_coherent: + odoo_incoherent = True + + if machine_fault and odoo_incoherent: + return "BLINK", "BLINK", "machine_fault_and_odoo_incoherent", True + + if machine_fault and order_valid_loaded: + return "ON", "BLINK", "machine_fault_with_valid_order", True + + if odoo_incoherent and machine_running: + return "BLINK", "OFF", "machine_running_without_valid_odoo_order", True + + if odoo_incoherent and not machine_running: + return "BLINK", "ON", "odoo_incoherent_machine_stopped", True + + if order_valid_loaded and machine_running: + return "ON", "OFF", "valid_order_and_machine_running", True + + if order_valid_loaded and not machine_running: + return "ON", "ON", "valid_order_and_machine_stopped", True + + return "OFF", "ON", "no_valid_order_machine_stopped", True + + +def main(): + env = read_env() + nodes = load_nodes() + + machine_filter_raw = os.getenv("BEACON_MACHINE_FILTER", "").strip() + if machine_filter_raw: + allowed = {x.strip() for x in machine_filter_raw.split(",") if x.strip()} + nodes = {mid: node for mid, node in nodes.items() if mid in allowed} + + if not nodes: + raise RuntimeError( + f"BEACON_MACHINE_FILTER={machine_filter_raw} no coincide con ningún nodo" + ) + + print( + f"Filtro de máquinas activo: {', '.join(nodes.keys())}", + flush=True, + ) + + mqtt_host = ( + os.getenv("MQTT_HOST_OVERRIDE") + or env.get("MQTT_HOST") + or "127.0.0.1" + ) + mqtt_port = int( + os.getenv("MQTT_PORT_OVERRIDE") + or env.get("MQTT_PORT") + or "1883" + ) + + mqtt_user = ( + os.getenv("MQTT_USER_OVERRIDE") + or os.getenv("MQTT_USERNAME_OVERRIDE") + or env.get("MQTT_USER") + or env.get("MQTT_USERNAME") + or env.get("MQTT_USER_REVPI") + ) + + mqtt_password = ( + os.getenv("MQTT_PASSWORD_OVERRIDE") + or os.getenv("MQTT_PASS_OVERRIDE") + or env.get("MQTT_PASSWORD") + or env.get("MQTT_PASS") + or env.get("MQTT_PASSWORD_REVPI") + ) + + tenant = env.get("TENANT", "ucepsa") + site = env.get("SITE", "ucepsa_onpremise") + vertical = env.get("VERTICAL", "edge_oee") + + telemetry_topic = f"vertical/{tenant}/{site}/{vertical}/+/telemetry" + order_topic = f"vertical/{tenant}/{site}/{vertical}/order_state/+" + + latest_telemetry = {} + latest_order = {} + last_logged_order = {} + + outputs = WiseOutputs(nodes) + + def on_connect(client, userdata, flags, rc, properties=None): + print(f"MQTT conectado rc={rc}", flush=True) + + if rc == 0: + client.subscribe(telemetry_topic) + client.subscribe(order_topic) + print(f"SUB {telemetry_topic}", flush=True) + print(f"SUB {order_topic}", flush=True) + + def on_disconnect(client, userdata, rc, properties=None): + print(f"MQTT desconectado rc={rc}", flush=True) + + def on_message(client, userdata, msg): + try: + payload = json.loads(msg.payload.decode("utf-8")) + payload["_rx_ts"] = now_ts() + + topic = msg.topic + + if "/order_state/" in topic: + machine_id = payload.get("machine_id") or topic.rsplit("/", 1)[-1] + latest_order[machine_id] = payload + + diagnostics = payload.get("diagnostics") or {} + + order_signature = ( + bool(payload.get("order_present", False)), + bool(payload.get("order_coherent", False)), + payload.get("odoo_state"), + payload.get("workorder_id"), + payload.get("production_order"), + payload.get("product"), + diagnostics.get("waiting_count"), + diagnostics.get("ready_count"), + diagnostics.get("progress_count"), + ) + + if last_logged_order.get(machine_id) != order_signature: + last_logged_order[machine_id] = order_signature + print( + f"ORDER_CHANGE {machine_id} " + f"present={payload.get('order_present')} " + f"coherent={payload.get('order_coherent')} " + f"state={payload.get('odoo_state')} " + f"mo={payload.get('production_order')} " + f"product={payload.get('product')} " + f"diag={diagnostics}", + flush=True, + ) + + elif topic.endswith("/telemetry"): + machine_id = payload.get("machine_id") + if machine_id: + latest_telemetry[machine_id] = payload + + except Exception as e: + print(f"ERROR procesando MQTT {msg.topic}: {e}", flush=True) + + client = mqtt.Client(client_id="mesavault_beacon_controller_power_aware") + + if mqtt_user: + client.username_pw_set(mqtt_user, mqtt_password) + print(f"MQTT auth user={mqtt_user}", flush=True) + else: + print("WARN: MQTT sin usuario", flush=True) + + client.on_connect = on_connect + client.on_disconnect = on_disconnect + client.on_message = on_message + + client.connect(mqtt_host, mqtt_port, 60) + client.loop_start() + + print("Controlador de balizas power-aware arrancado", flush=True) + print(f"Máquinas: {', '.join(nodes.keys())}", flush=True) + print("Ctrl+C para parar", flush=True) + + last_reason = {} + + try: + while True: + blink_on = int(time.time() / BLINK_PERIOD_S) % 2 == 0 + + for machine_id in nodes: + telemetry = latest_telemetry.get(machine_id) + order = latest_order.get(machine_id) + + blue_mode, red_mode, reason, should_write = decide_modes( + machine_id, + telemetry, + order, + ) + + if last_reason.get(machine_id) != reason: + last_reason[machine_id] = reason + print( + f"STATE {machine_id} " + f"blue={blue_mode} red={red_mode} " + f"write={should_write} reason={reason}", + flush=True, + ) + + if not should_write: + continue + + blue_on = mode_to_bool(blue_mode, blink_on) + red_on = mode_to_bool(red_mode, blink_on) + + if outputs.in_offline_backoff(machine_id): + continue + + try: + outputs.set_beacon( + machine_id, + blue_on=blue_on, + red_on=red_on, + ) + except Exception as e: + outputs.close_client(machine_id) + outputs.mark_offline(machine_id) + outputs.log_offline_error(machine_id, str(e)) + + time.sleep(0.5) + + except KeyboardInterrupt: + print("Parando controlador de balizas", flush=True) + + finally: + outputs.safe_connected_only() + outputs.close_all() + client.loop_stop() + client.disconnect() + print("Controlador parado", flush=True) + + +if __name__ == "__main__": + main() diff --git a/ucepsa/edge-oee-demo/revpi/edge-oee/beacon_manual_test.py b/ucepsa/edge-oee-demo/revpi/edge-oee/beacon_manual_test.py new file mode 100755 index 0000000..356a7e1 --- /dev/null +++ b/ucepsa/edge-oee-demo/revpi/edge-oee/beacon_manual_test.py @@ -0,0 +1,182 @@ +#!/usr/bin/env python3 +import json +import sys +import time +from pathlib import Path + +from pymodbus.client import ModbusTcpClient + +UNIT = 1 + +DO0_BLUE_COIL = 16 +DO1_RED_COIL = 17 + +WISE_NODES_FILE = Path("wise_nodes.json") + + +def load_nodes(): + data = json.loads(WISE_NODES_FILE.read_text()) + nodes = data.get("nodes", []) + + result = {} + for node in nodes: + machine_id = node.get("machine_id") + host = node.get("wise_ip") or node.get("wise_host") or node.get("host") or node.get("ip") + + if machine_id and host: + result[machine_id] = { + "machine_id": machine_id, + "source_node": node.get("source_node", ""), + "host": host, + } + + return result + + +def write_coil_checked(client, address, value): + rr = client.write_coil(address=address, value=value, device_id=UNIT) + if rr.isError(): + raise RuntimeError(f"Error escribiendo coil {address}: {rr}") + + +def read_outputs(client): + rr = client.read_coils(address=DO0_BLUE_COIL, count=2, device_id=UNIT) + if rr.isError(): + raise RuntimeError(f"Error leyendo coils DO: {rr}") + + do0 = bool(rr.bits[0]) + do1 = bool(rr.bits[1]) + + blue_on = do0 + red_on = not do1 + + return do0, do1, blue_on, red_on + + +def set_beacon(client, blue_on, red_on): + # DO0 gobierna azul directamente. + write_coil_checked(client, DO0_BLUE_COIL, bool(blue_on)) + + # DO1 gobierna roja invertida por contacto NC. + # red_on=True -> DO1 False + # red_on=False -> DO1 True + write_coil_checked(client, DO1_RED_COIL, not bool(red_on)) + + +def print_state(client, label): + do0, do1, blue_on, red_on = read_outputs(client) + print( + f"{label}: " + f"DO0_BLUE={do0} DO1_RED={do1} " + f"=> blue_on={blue_on} red_on={red_on}" + ) + + +def apply_mode(client, mode, seconds): + blink_period = 1.0 + end = time.time() + seconds + + if mode == "safe": + set_beacon(client, blue_on=False, red_on=True) + print_state(client, "safe") + time.sleep(seconds) + + elif mode == "ok_running": + set_beacon(client, blue_on=True, red_on=False) + print_state(client, "ok_running") + time.sleep(seconds) + + elif mode == "ok_stopped": + set_beacon(client, blue_on=True, red_on=True) + print_state(client, "ok_stopped") + time.sleep(seconds) + + elif mode == "odoo_bad_running": + print("odoo_bad_running: azul intermitente, rojo apagado") + while time.time() < end: + set_beacon(client, blue_on=True, red_on=False) + time.sleep(blink_period) + set_beacon(client, blue_on=False, red_on=False) + time.sleep(blink_period) + + elif mode == "machine_fault": + print("machine_fault: azul fijo, rojo intermitente") + while time.time() < end: + set_beacon(client, blue_on=True, red_on=True) + time.sleep(blink_period) + set_beacon(client, blue_on=True, red_on=False) + time.sleep(blink_period) + + elif mode == "odoo_bad_and_fault": + print("odoo_bad_and_fault: azul intermitente, rojo intermitente") + while time.time() < end: + set_beacon(client, blue_on=True, red_on=True) + time.sleep(blink_period) + set_beacon(client, blue_on=False, red_on=False) + time.sleep(blink_period) + + elif mode == "system_fault": + print("system_fault: azul apagado, rojo intermitente") + while time.time() < end: + set_beacon(client, blue_on=False, red_on=True) + time.sleep(blink_period) + set_beacon(client, blue_on=False, red_on=False) + time.sleep(blink_period) + + else: + raise ValueError(f"Modo no reconocido: {mode}") + + +def test_machine(machine_id, host, mode, seconds): + print(f"Probando {machine_id} en {host} modo={mode} durante {seconds}s") + + client = ModbusTcpClient(host, port=502, timeout=3) + + if not client.connect(): + raise RuntimeError(f"No conecta con {machine_id} {host}:502") + + try: + apply_mode(client, mode, seconds) + print_state(client, "estado final antes de safe") + + # Dejamos siempre estado seguro al terminar la prueba manual. + set_beacon(client, blue_on=False, red_on=True) + print_state(client, "safe final") + + finally: + client.close() + + +def main(): + if len(sys.argv) < 3: + print("Uso:") + print(" python beacon_manual_test.py CORT-00 safe 5") + print(" python beacon_manual_test.py CORT-00 ok_running 5") + print(" python beacon_manual_test.py CORT-00 ok_stopped 5") + print(" python beacon_manual_test.py CORT-00 odoo_bad_running 10") + print(" python beacon_manual_test.py CORT-00 machine_fault 10") + print(" python beacon_manual_test.py CORT-00 odoo_bad_and_fault 10") + print(" python beacon_manual_test.py CORT-00 system_fault 10") + print(" python beacon_manual_test.py all safe 5") + raise SystemExit(1) + + target = sys.argv[1] + mode = sys.argv[2] + seconds = float(sys.argv[3]) if len(sys.argv) >= 4 else 5.0 + + nodes = load_nodes() + + if target == "all": + for machine_id, node in nodes.items(): + test_machine(machine_id, node["host"], mode, seconds) + return + + if target not in nodes: + raise SystemExit(f"No encuentro {target} en wise_nodes.json") + + node = nodes[target] + test_machine(target, node["host"], mode, seconds) + + +if __name__ == "__main__": + main() diff --git a/ucepsa/edge-oee-demo/revpi/edge-oee/check_revpi_base.sh b/ucepsa/edge-oee-demo/revpi/edge-oee/check_revpi_base.sh new file mode 100755 index 0000000..ce53b2c --- /dev/null +++ b/ucepsa/edge-oee-demo/revpi/edge-oee/check_revpi_base.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +set -euo pipefail + +echo "--- HOSTNAME ---" +hostname + +echo "--- OS ---" +cat /etc/os-release || true + +echo "--- KERNEL ---" +uname -a + +echo "--- IP ---" +ip -br a + +echo "--- ROUTES ---" +ip route + +echo "--- SERIAL DEVICES ---" +ls -l /dev/tty* | head -n 80 + +echo "--- DMESG TTY ---" +dmesg | grep -i tty || true + +echo "--- USB ---" +lsusb || true diff --git a/ucepsa/edge-oee-demo/revpi/edge-oee/clear_order_state_retained.py b/ucepsa/edge-oee-demo/revpi/edge-oee/clear_order_state_retained.py new file mode 100755 index 0000000..9f257d6 --- /dev/null +++ b/ucepsa/edge-oee-demo/revpi/edge-oee/clear_order_state_retained.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +import json +from pathlib import Path + +import paho.mqtt.client as mqtt + + +def read_env(path=".env"): + env = {} + p = Path(path) + + for raw in p.read_text().splitlines(): + line = raw.strip() + if not line or line.startswith("#") or "=" not in line: + continue + k, v = line.split("=", 1) + env[k.strip()] = v.strip().strip('"').strip("'") + + return env + + +def load_machine_ids(): + data = json.loads(Path("wise_nodes.json").read_text()) + return [ + n["machine_id"] + for n in data.get("nodes", []) + if n.get("enabled", True) + ] + + +env = read_env() + +mqtt_host = env.get("MQTT_HOST", "127.0.0.1") +mqtt_port = int(env.get("MQTT_PORT", "1883")) +mqtt_user = env.get("MQTT_USERNAME") or env.get("MQTT_USER") +mqtt_password = env.get("MQTT_PASSWORD") + +tenant = env.get("TENANT", "ucepsa") +site = env.get("SITE", "ucepsa_onpremise") +vertical = env.get("VERTICAL", "edge_oee") + +client = mqtt.Client(client_id="clear_order_state_retained") + +if mqtt_user: + client.username_pw_set(mqtt_user, mqtt_password) + +client.connect(mqtt_host, mqtt_port, 60) +client.loop_start() + +for machine_id in load_machine_ids(): + topic = f"vertical/{tenant}/{site}/{vertical}/order_state/{machine_id}" + + # Payload vacío + retain=True borra el mensaje retenido del broker. + info = client.publish(topic, payload=b"", qos=0, retain=True) + info.wait_for_publish(timeout=5) + + print(f"Retained eliminado: {topic}") + +client.loop_stop() +client.disconnect() + +print("OK: estados simulados de Odoo eliminados") diff --git a/ucepsa/edge-oee-demo/revpi/edge-oee/debug_cort01_eastron.py b/ucepsa/edge-oee-demo/revpi/edge-oee/debug_cort01_eastron.py new file mode 100755 index 0000000..d8c7b56 --- /dev/null +++ b/ucepsa/edge-oee-demo/revpi/edge-oee/debug_cort01_eastron.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +import struct +from pymodbus.client import ModbusTcpClient + +WISE_IP = "17.126.1.201" +UNIT = 1 + +def f32_abcd(hi, lo): + raw = hi.to_bytes(2, "big") + lo.to_bytes(2, "big") + return struct.unpack(">f", raw)[0] + +def read_holding(client, start, count, label): + rr = client.read_holding_registers(address=start, count=count, device_id=UNIT) + if rr.isError(): + print(f"{label}: ERROR {rr}") + return None + print(f"{label}: {rr.registers}") + return rr.registers + +client = ModbusTcpClient(WISE_IP, port=502, timeout=3) + +if not client.connect(): + raise SystemExit(f"No conecta con WISE {WISE_IP}:502") + +try: + print("CORT-01 / wise-cort-01 /", WISE_IP) + + data = read_holding(client, 1000, 20, "DATA 1000..1019") + bit_err = read_holding(client, 1100, 20, "BIT ERR 1100..1119") + word_err = read_holding(client, 1200, 20, "WORD ERR 1200..1219") + + if data: + print("\nValores float interpretados:") + labels = [ + ("voltage_l1_v", 0), + ("current_l1_a", 2), + ("active_power_w", 4), + ("apparent_power_va", 6), + ("reactive_power_var", 8), + ("power_factor", 10), + ("frequency_hz", 12), + ("import_kwh", 14), + ("export_kwh", 16), + ("total_active_energy_kwh", 18), + ] + + for name, offset in labels: + print(f"{name:28s}: {f32_abcd(data[offset], data[offset+1]):.6f}") + +finally: + client.close() diff --git a/ucepsa/edge-oee-demo/revpi/edge-oee/di_probe.py b/ucepsa/edge-oee-demo/revpi/edge-oee/di_probe.py new file mode 100755 index 0000000..c5e9ef5 --- /dev/null +++ b/ucepsa/edge-oee-demo/revpi/edge-oee/di_probe.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +import time +import signal +import sys +from collections import deque + +import revpimodio2 + + +RUNNING = True + + +def handle_signal(signum, frame): + global RUNNING + RUNNING = False + + +signal.signal(signal.SIGTERM, handle_signal) +signal.signal(signal.SIGINT, handle_signal) + + +def main(): + rpi = revpimodio2.RevPiModIO(autorefresh=True) + + auto_io = getattr(rpi.io, "I_1") + cycle_io = getattr(rpi.io, "I_2") + + cycle_pulse_count = 0 + previous_cycle = bool(cycle_io.value) + last_edge_ts = 0.0 + last_cycle_ts = None + + debounce_s = 0.05 + pulse_window_s = 60.0 + recent_pulses = deque() + + print("[START] DI probe") + print("I_1 = machine_auto_signal") + print("I_2 = cycle_pulse") + print("Pulsa CTRL+C para salir") + + try: + last_print = 0.0 + + while RUNNING: + now = time.monotonic() + + machine_auto_signal = bool(auto_io.value) + current_cycle = bool(cycle_io.value) + + # Flanco ascendente en I_2 + if current_cycle and not previous_cycle: + if now - last_edge_ts >= debounce_s: + cycle_pulse_count += 1 + last_edge_ts = now + last_cycle_ts = now + recent_pulses.append(now) + + previous_cycle = current_cycle + + # Limpiar pulsos fuera de la ventana + while recent_pulses and now - recent_pulses[0] > pulse_window_s: + recent_pulses.popleft() + + cycle_rate_ppm = len(recent_pulses) * (60.0 / pulse_window_s) + + if last_cycle_ts is None: + last_cycle_age_s = None + else: + last_cycle_age_s = round(now - last_cycle_ts, 1) + + # Para la demo, machine_running se asocia al interruptor I_1. + # Más adelante podemos hacerlo depender también de pulsos recientes. + machine_running = machine_auto_signal + + if now - last_print >= 0.5: + last_print = now + print( + "auto={auto} running={running} cycle_input={cycle} " + "pulse_count={count} cycle_rate_ppm={rate:.1f} last_cycle_age_s={age}".format( + auto=int(machine_auto_signal), + running=int(machine_running), + cycle=int(current_cycle), + count=cycle_pulse_count, + rate=cycle_rate_ppm, + age=last_cycle_age_s, + ), + flush=True, + ) + + time.sleep(0.02) + + finally: + rpi.exit() + print("[STOP] DI probe") + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/ucepsa/edge-oee-demo/revpi/edge-oee/eastron_reader.py b/ucepsa/edge-oee-demo/revpi/edge-oee/eastron_reader.py new file mode 100755 index 0000000..fa2db15 --- /dev/null +++ b/ucepsa/edge-oee-demo/revpi/edge-oee/eastron_reader.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +import argparse +import json +import struct +import sys +from datetime import datetime, timezone + +from pymodbus.client import ModbusSerialClient + + +def decode_float_abcd(registers): + if len(registers) != 2: + raise ValueError(f"Expected 2 registers, got {len(registers)}") + + raw = registers[0].to_bytes(2, "big") + registers[1].to_bytes(2, "big") + return struct.unpack(">f", raw)[0] + + +def read_float(client, address, device_id): + rr = client.read_input_registers( + address=address, + count=2, + device_id=device_id, + ) + + if rr.isError(): + raise RuntimeError(f"Modbus error at address {address}: {rr}") + + return decode_float_abcd(rr.registers) + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--port", default="/dev/ttyRS485") + parser.add_argument("--slave", type=int, default=1) + parser.add_argument("--baud", type=int, default=9600) + parser.add_argument("--parity", default="N") + parser.add_argument("--stopbits", type=int, default=1) + parser.add_argument("--bytesize", type=int, default=8) + parser.add_argument("--timeout", type=float, default=1.5) + args = parser.parse_args() + + client = ModbusSerialClient( + port=args.port, + baudrate=args.baud, + parity=args.parity, + stopbits=args.stopbits, + bytesize=args.bytesize, + timeout=args.timeout, + ) + + if not client.connect(): + print("[ERROR] Cannot open Modbus serial port", file=sys.stderr) + return 2 + + try: + voltage_v = read_float(client, 0, args.slave) + current_a = read_float(client, 6, args.slave) + active_power_w = read_float(client, 12, args.slave) + frequency_hz = read_float(client, 70, args.slave) + import_kwh = read_float(client, 72, args.slave) + export_kwh = read_float(client, 74, args.slave) + + payload = { + "ts": datetime.now(timezone.utc).isoformat(), + "source": "revpi_sdm120m", + "meter_model": "Eastron SDM120-M", + "meter_id": f"modbus_{args.slave}", + "port": args.port, + "slave": args.slave, + "baud": args.baud, + "voltage_l1_v": round(voltage_v, 3), + "current_l1_a": round(current_a, 3), + "power_total_kw": round(active_power_w / 1000.0, 6), + "frequency_hz": round(frequency_hz, 3), + "import_kwh": round(import_kwh, 3), + "export_kwh": round(export_kwh, 3), + "comm_ok": True, + } + + print(json.dumps(payload, indent=2, ensure_ascii=False)) + return 0 + + except Exception as exc: + error_payload = { + "ts": datetime.now(timezone.utc).isoformat(), + "source": "revpi_sdm120m", + "meter_model": "Eastron SDM120-M", + "meter_id": f"modbus_{args.slave}", + "port": args.port, + "slave": args.slave, + "baud": args.baud, + "comm_ok": False, + "error_text": f"{type(exc).__name__}: {exc}", + } + print(json.dumps(error_payload, indent=2, ensure_ascii=False)) + return 1 + + finally: + client.close() + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/ucepsa/edge-oee-demo/revpi/edge-oee/eastron_reader_mqtt.py b/ucepsa/edge-oee-demo/revpi/edge-oee/eastron_reader_mqtt.py new file mode 100755 index 0000000..5cfb65d --- /dev/null +++ b/ucepsa/edge-oee-demo/revpi/edge-oee/eastron_reader_mqtt.py @@ -0,0 +1,205 @@ +#!/usr/bin/env python3 +import json +import os +import struct +import sys +import time +from datetime import datetime, timezone +from pathlib import Path + +import paho.mqtt.client as mqtt +from pymodbus.client import ModbusSerialClient + + +BASE_DIR = Path(__file__).resolve().parent +ENV_FILE = BASE_DIR / ".env" +START_TIME = time.monotonic() + + +def load_env_file(path: Path): + if not path.exists(): + return + + for line in path.read_text().splitlines(): + line = line.strip() + + if not line or line.startswith("#") or "=" not in line: + continue + + key, value = line.split("=", 1) + os.environ.setdefault(key.strip(), value.strip().strip('"').strip("'")) + + +def getenv(name, default=None, required=False): + value = os.getenv(name, default) + if required and not value: + print(f"[ERROR] Missing required environment variable: {name}", file=sys.stderr) + sys.exit(2) + return value + + +def decode_float_abcd(registers): + raw = registers[0].to_bytes(2, "big") + registers[1].to_bytes(2, "big") + return struct.unpack(">f", raw)[0] + + +def read_float(client, address, device_id): + rr = client.read_input_registers( + address=address, + count=2, + device_id=device_id, + ) + + if rr.isError(): + raise RuntimeError(f"Modbus error at address {address}: {rr}") + + return decode_float_abcd(rr.registers) + + +def read_sdm120m(): + port = getenv("MODBUS_PORT", "/dev/ttyRS485") + slave = int(getenv("MODBUS_SLAVE", "1")) + baud = int(getenv("MODBUS_BAUD", "9600")) + parity = getenv("MODBUS_PARITY", "N") + stopbits = int(getenv("MODBUS_STOPBITS", "1")) + bytesize = int(getenv("MODBUS_BYTESIZE", "8")) + timeout = float(getenv("MODBUS_TIMEOUT", "1.5")) + + client = ModbusSerialClient( + port=port, + baudrate=baud, + parity=parity, + stopbits=stopbits, + bytesize=bytesize, + timeout=timeout, + ) + + if not client.connect(): + raise RuntimeError(f"Cannot open Modbus serial port {port}") + + try: + voltage_v = read_float(client, 0, slave) + current_a = read_float(client, 6, slave) + active_power_w = read_float(client, 12, slave) + frequency_hz = read_float(client, 70, slave) + import_kwh = read_float(client, 72, slave) + export_kwh = read_float(client, 74, slave) + + return { + "meter_model": "Eastron SDM120-M", + "meter_id": f"modbus_{slave}", + "modbus_port": port, + "modbus_slave": slave, + "modbus_baud": baud, + + "voltage_l1_v": round(voltage_v, 3), + "current_l1_a": round(current_a, 3), + "power_total_kw": round(active_power_w / 1000.0, 6), + "frequency_hz": round(frequency_hz, 3), + "import_kwh": round(import_kwh, 3), + "export_kwh": round(export_kwh, 3), + + "comm_ok": True, + "sample_valid": True, + "error_text": None, + } + + finally: + client.close() + + +def build_payload(): + tenant = getenv("TENANT", "ucepsa") + site = getenv("SITE", "demo_edge_oee") + vertical = getenv("VERTICAL", "edge_oee") + asset = getenv("ASSET", "revpi_oee_node_01") + + base = { + "asset": asset, + "tenant": tenant, + "site": site, + "vertical": vertical, + "node_type": "revpi_sdm120m_oee_gateway", + "fw": "0.1.0", + "ts": datetime.now(timezone.utc).isoformat(), + "uptime_s": round(time.monotonic() - START_TIME, 1), + + "machine_id": None, + "machine_running": None, + "cycle_pulse_count": None, + "cycle_rate_hz": None, + "digital_inputs": {}, + } + + try: + data = read_sdm120m() + base.update(data) + except Exception as exc: + base.update({ + "comm_ok": False, + "sample_valid": False, + "meter_model": "Eastron SDM120-M", + "meter_id": f"modbus_{getenv('MODBUS_SLAVE', '1')}", + "voltage_l1_v": None, + "current_l1_a": None, + "power_total_kw": None, + "frequency_hz": None, + "import_kwh": None, + "export_kwh": None, + "error_text": f"{type(exc).__name__}: {exc}", + }) + + return base + + +def publish_once(payload): + mqtt_host = getenv("MQTT_HOST", required=True) + mqtt_port = int(getenv("MQTT_PORT", "1883")) + mqtt_user = getenv("MQTT_USER", required=True) + mqtt_password = getenv("MQTT_PASSWORD", required=True) + mqtt_client_id = getenv("MQTT_CLIENT_ID", required=True) + topic_telemetry = getenv("MQTT_TOPIC_TELEMETRY", required=True) + topic_status = getenv("MQTT_TOPIC_STATUS", required=True) + + client = mqtt.Client( + mqtt.CallbackAPIVersion.VERSION2, + client_id=mqtt_client_id, + clean_session=True, + ) + + client.username_pw_set(mqtt_user, mqtt_password) + client.will_set(topic_status, payload="offline", qos=0, retain=True) + + client.connect(mqtt_host, mqtt_port, keepalive=30) + client.loop_start() + + client.publish(topic_status, payload="online", qos=0, retain=True) + result = client.publish( + topic_telemetry, + payload=json.dumps(payload, ensure_ascii=False), + qos=0, + retain=False, + ) + result.wait_for_publish(timeout=5) + + client.loop_stop() + client.disconnect() + + if result.rc != mqtt.MQTT_ERR_SUCCESS: + raise RuntimeError(f"MQTT publish failed rc={result.rc}") + + +def main(): + load_env_file(ENV_FILE) + + payload = build_payload() + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + publish_once(payload) + + print("[OK] MQTT telemetry published") + return 0 if payload.get("comm_ok") else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/ucepsa/edge-oee-demo/revpi/edge-oee/eastron_reader_mqtt_loop.py b/ucepsa/edge-oee-demo/revpi/edge-oee/eastron_reader_mqtt_loop.py new file mode 100755 index 0000000..2359078 --- /dev/null +++ b/ucepsa/edge-oee-demo/revpi/edge-oee/eastron_reader_mqtt_loop.py @@ -0,0 +1,419 @@ +#!/usr/bin/env python3 +import json +import os +import signal +import struct +import sys +import time +from collections import deque +from datetime import datetime, timezone +from pathlib import Path + +import paho.mqtt.client as mqtt +import revpimodio2 +from pymodbus.client import ModbusSerialClient + + +BASE_DIR = Path(__file__).resolve().parent +ENV_FILE = BASE_DIR / ".env" +START_TIME = time.monotonic() +RUNNING = True + + +def handle_signal(signum, frame): + global RUNNING + RUNNING = False + + +signal.signal(signal.SIGTERM, handle_signal) +signal.signal(signal.SIGINT, handle_signal) + + +def utc_now_iso(): + return datetime.now(timezone.utc).isoformat() + + +def load_env_file(path: Path): + if not path.exists(): + return + + for line in path.read_text().splitlines(): + line = line.strip() + + if not line or line.startswith("#") or "=" not in line: + continue + + key, value = line.split("=", 1) + os.environ.setdefault(key.strip(), value.strip().strip('"').strip("'")) + + +def getenv(name, default=None, required=False): + value = os.getenv(name, default) + if required and not value: + print(f"[ERROR] Missing required environment variable: {name}", file=sys.stderr, flush=True) + sys.exit(2) + return value + + +def decode_float_abcd(registers): + raw = registers[0].to_bytes(2, "big") + registers[1].to_bytes(2, "big") + return struct.unpack(">f", raw)[0] + + +def read_float(client, address, device_id): + rr = client.read_input_registers( + address=address, + count=2, + device_id=device_id, + ) + + if rr.isError(): + raise RuntimeError(f"Modbus error at address {address}: {rr}") + + return decode_float_abcd(rr.registers) + + +def read_sdm120m(): + port = getenv("MODBUS_PORT", "/dev/ttyRS485") + slave = int(getenv("MODBUS_SLAVE", "1")) + baud = int(getenv("MODBUS_BAUD", "9600")) + parity = getenv("MODBUS_PARITY", "N") + stopbits = int(getenv("MODBUS_STOPBITS", "1")) + bytesize = int(getenv("MODBUS_BYTESIZE", "8")) + timeout = float(getenv("MODBUS_TIMEOUT", "1.5")) + + client = ModbusSerialClient( + port=port, + baudrate=baud, + parity=parity, + stopbits=stopbits, + bytesize=bytesize, + timeout=timeout, + ) + + if not client.connect(): + raise RuntimeError(f"Cannot open Modbus serial port {port}") + + try: + voltage_v = read_float(client, 0, slave) + current_a = read_float(client, 6, slave) + active_power_w = read_float(client, 12, slave) + frequency_hz = read_float(client, 70, slave) + import_kwh = read_float(client, 72, slave) + export_kwh = read_float(client, 74, slave) + + return { + "meter_model": "Eastron SDM120-M", + "meter_id": f"modbus_{slave}", + "modbus_port": port, + "modbus_slave": slave, + "modbus_baud": baud, + + "voltage_l1_v": round(voltage_v, 3), + "current_l1_a": round(current_a, 3), + "power_total_kw": round(active_power_w / 1000.0, 6), + "frequency_hz": round(frequency_hz, 3), + "import_kwh": round(import_kwh, 3), + "export_kwh": round(export_kwh, 3), + + "comm_ok": True, + "sample_valid": True, + "error_text": None, + } + + finally: + client.close() + + +class DigitalInputTracker: + def __init__(self): + self.auto_input_name = getenv("DI_AUTO_INPUT", "I_1") + self.cycle_input_name = getenv("DI_CYCLE_INPUT", "I_2") + self.debounce_s = float(getenv("DI_DEBOUNCE_MS", "50")) / 1000.0 + self.window_s = float(getenv("CYCLE_RATE_WINDOW_SECONDS", "60")) + + self.rpi = revpimodio2.RevPiModIO(autorefresh=True) + + self.auto_io = getattr(self.rpi.io, self.auto_input_name) + self.cycle_io = getattr(self.rpi.io, self.cycle_input_name) + + now = time.monotonic() + + # Estado estable de I_1 con antirrebote. + self.stable_auto = bool(self.auto_io.value) + self.raw_auto = self.stable_auto + self.raw_auto_since = now + + # Pulsos de ciclo I_2. + self.cycle_pulse_count = 0 + self.previous_cycle = bool(self.cycle_io.value) + self.last_edge_ts = 0.0 + self.last_cycle_ts = None + self.recent_pulses = deque() + + self.pending_events = [] + + def update(self): + now = time.monotonic() + + # --- I_1: estado automático con antirrebote y evento inmediato --- + raw_auto = bool(self.auto_io.value) + + if raw_auto != self.raw_auto: + self.raw_auto = raw_auto + self.raw_auto_since = now + + elif raw_auto != self.stable_auto and (now - self.raw_auto_since) >= self.debounce_s: + previous = self.stable_auto + self.stable_auto = raw_auto + + event_name = "machine_started" if self.stable_auto else "machine_stopped" + + self.pending_events.append({ + "event_type": "machine_state_changed", + "event_name": event_name, + "previous_auto_signal": previous, + "new_auto_signal": self.stable_auto, + "machine_running": self.stable_auto, + "reason_pending": not self.stable_auto, + "event_monotonic_ts": now, + }) + + # --- I_2: flanco ascendente de ciclo con antirrebote --- + current_cycle = bool(self.cycle_io.value) + + if current_cycle and not self.previous_cycle: + if now - self.last_edge_ts >= self.debounce_s: + self.cycle_pulse_count += 1 + self.last_edge_ts = now + self.last_cycle_ts = now + self.recent_pulses.append(now) + + self.previous_cycle = current_cycle + + while self.recent_pulses and now - self.recent_pulses[0] > self.window_s: + self.recent_pulses.popleft() + + def snapshot(self): + now = time.monotonic() + + while self.recent_pulses and now - self.recent_pulses[0] > self.window_s: + self.recent_pulses.popleft() + + cycle_rate_ppm = len(self.recent_pulses) * (60.0 / self.window_s) + + if self.last_cycle_ts is None: + last_cycle_age_s = None + else: + last_cycle_age_s = round(now - self.last_cycle_ts, 1) + + current_cycle = bool(self.cycle_io.value) + + return { + "io_ok": True, + "machine_auto_signal": self.stable_auto, + "machine_running": self.stable_auto, + "cycle_input_state": current_cycle, + "cycle_pulse_count": self.cycle_pulse_count, + "cycle_rate_ppm": round(cycle_rate_ppm, 2), + "last_cycle_age_s": last_cycle_age_s, + "digital_inputs": { + self.auto_input_name: self.stable_auto, + self.cycle_input_name: current_cycle, + }, + } + + def pop_events(self): + events = list(self.pending_events) + self.pending_events.clear() + return events + + def close(self): + try: + self.rpi.exit() + except Exception: + pass + + +def base_payload(): + return { + "asset": getenv("ASSET", "revpi_oee_node_01"), + "tenant": getenv("TENANT", "ucepsa"), + "site": getenv("SITE", "demo_edge_oee"), + "vertical": getenv("VERTICAL", "edge_oee"), + "node_type": getenv("NODE_TYPE", "revpi_sdm120m_oee_gateway"), + "fw": getenv("FW", "0.1.0"), + "source": "revpi_edge_oee_node", + "ts": utc_now_iso(), + "uptime_s": round(time.monotonic() - START_TIME, 1), + "machine_id": getenv("MACHINE_ID", "CORT-01"), + "order_id": None, + } + + +def build_telemetry_payload(di_tracker): + payload = base_payload() + payload.update(di_tracker.snapshot()) + + try: + payload.update(read_sdm120m()) + + except Exception as exc: + payload.update({ + "comm_ok": False, + "sample_valid": False, + "meter_model": "Eastron SDM120-M", + "meter_id": f"modbus_{getenv('MODBUS_SLAVE', '1')}", + "voltage_l1_v": None, + "current_l1_a": None, + "power_total_kw": None, + "frequency_hz": None, + "import_kwh": None, + "export_kwh": None, + "error_text": f"{type(exc).__name__}: {exc}", + }) + + return payload + + +def build_event_payload(raw_event, di_tracker): + payload = base_payload() + payload.update(di_tracker.snapshot()) + + payload.update({ + "event_type": raw_event["event_type"], + "event_name": raw_event["event_name"], + "previous_auto_signal": raw_event["previous_auto_signal"], + "new_auto_signal": raw_event["new_auto_signal"], + "machine_running": raw_event["machine_running"], + "reason_pending": raw_event["reason_pending"], + "event_ts": utc_now_iso(), + "event_source": "digital_input", + "event_input": getenv("DI_AUTO_INPUT", "I_1"), + }) + + return payload + + +def create_mqtt_client(): + mqtt_host = getenv("MQTT_HOST", required=True) + mqtt_port = int(getenv("MQTT_PORT", "1883")) + mqtt_user = getenv("MQTT_USER", required=True) + mqtt_password = getenv("MQTT_PASSWORD", required=True) + mqtt_client_id = getenv("MQTT_CLIENT_ID", required=True) + topic_status = getenv("MQTT_TOPIC_STATUS", required=True) + + client = mqtt.Client( + mqtt.CallbackAPIVersion.VERSION2, + client_id=mqtt_client_id, + clean_session=True, + ) + + client.username_pw_set(mqtt_user, mqtt_password) + client.will_set(topic_status, payload="offline", qos=0, retain=True) + + client.connect(mqtt_host, mqtt_port, keepalive=30) + client.loop_start() + + client.publish(topic_status, payload="online", qos=0, retain=True) + + return client + + +def publish_json(client, topic, payload, retain=False): + result = client.publish( + topic, + payload=json.dumps(payload, ensure_ascii=False), + qos=0, + retain=retain, + ) + result.wait_for_publish(timeout=5) + + if result.rc != mqtt.MQTT_ERR_SUCCESS: + raise RuntimeError(f"MQTT publish failed rc={result.rc}") + + +def publish_loop(): + topic_telemetry = getenv("MQTT_TOPIC_TELEMETRY", required=True) + topic_event = getenv("MQTT_TOPIC_EVENT", required=True) + topic_status = getenv("MQTT_TOPIC_STATUS", required=True) + interval_s = float(getenv("PUBLISH_INTERVAL_SECONDS", "10")) + + di_tracker = DigitalInputTracker() + client = create_mqtt_client() + + print("[START] RevPi Edge-OEE MQTT loop", flush=True) + print(f"[CONFIG] topic_telemetry={topic_telemetry}", flush=True) + print(f"[CONFIG] topic_event={topic_event}", flush=True) + print(f"[CONFIG] interval_s={interval_s}", flush=True) + print(f"[CONFIG] auto_input={di_tracker.auto_input_name}", flush=True) + print(f"[CONFIG] cycle_input={di_tracker.cycle_input_name}", flush=True) + + next_publish = time.monotonic() + + try: + while RUNNING: + di_tracker.update() + + # Eventos inmediatos por cambio de I_1. + for raw_event in di_tracker.pop_events(): + event_payload = build_event_payload(raw_event, di_tracker) + publish_json(client, topic_event, event_payload, retain=False) + + print( + "[EVENT] " + f"{event_payload.get('event_name')} " + f"previous={event_payload.get('previous_auto_signal')} " + f"new={event_payload.get('new_auto_signal')} " + f"pulses={event_payload.get('cycle_pulse_count')} " + f"rate_ppm={event_payload.get('cycle_rate_ppm')}", + flush=True, + ) + + # Telemetría periódica. + now = time.monotonic() + + if now >= next_publish: + telemetry_payload = build_telemetry_payload(di_tracker) + publish_json(client, topic_telemetry, telemetry_payload, retain=False) + + print( + "[OK] telemetry " + f"auto={telemetry_payload.get('machine_auto_signal')} " + f"running={telemetry_payload.get('machine_running')} " + f"pulses={telemetry_payload.get('cycle_pulse_count')} " + f"rate_ppm={telemetry_payload.get('cycle_rate_ppm')} " + f"voltage={telemetry_payload.get('voltage_l1_v')} " + f"power_kw={telemetry_payload.get('power_total_kw')}", + flush=True, + ) + + next_publish = now + interval_s + + time.sleep(0.02) + + finally: + try: + client.publish(topic_status, payload="offline", qos=0, retain=True) + time.sleep(0.2) + except Exception: + pass + + try: + client.loop_stop() + client.disconnect() + except Exception: + pass + + di_tracker.close() + print("[STOP] RevPi Edge-OEE MQTT loop", flush=True) + + +def main(): + load_env_file(ENV_FILE) + publish_loop() + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/ucepsa/edge-oee-demo/revpi/edge-oee/live_wise_counter_modbus.py b/ucepsa/edge-oee-demo/revpi/edge-oee/live_wise_counter_modbus.py new file mode 100755 index 0000000..b8c5b9f --- /dev/null +++ b/ucepsa/edge-oee-demo/revpi/edge-oee/live_wise_counter_modbus.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +import time +from pymodbus.client import ModbusTcpClient + +WISE_IP = "10.43.54.249" +UNIT = 1 + + +def u32_lo_hi(lo, hi): + return (lo & 0xFFFF) + ((hi & 0xFFFF) << 16) + + +client = ModbusTcpClient(WISE_IP, port=502, timeout=2) + +if not client.connect(): + raise RuntimeError("No conecta con WISE") + +last_di1_counter = None + +try: + print("Leyendo contador DI1 por Modbus/TCP. Pulsa DI1 y observa cambios.") + while True: + rr = client.read_holding_registers( + address=0, + count=4, + device_id=UNIT, + ) + + if rr.isError(): + print("ERROR:", rr) + time.sleep(1) + continue + + regs = rr.registers + + di0_counter = u32_lo_hi(regs[0], regs[1]) + di1_counter = u32_lo_hi(regs[2], regs[3]) + + if di1_counter != last_di1_counter: + print( + f"DI0_counter={di0_counter} " + f"DI1_counter={di1_counter} " + f"raw={regs}" + ) + last_di1_counter = di1_counter + + time.sleep(0.2) + +except KeyboardInterrupt: + print("Parado") + +finally: + client.close() diff --git a/ucepsa/edge-oee-demo/revpi/edge-oee/monitor_cort01_eastron_continuo.py b/ucepsa/edge-oee-demo/revpi/edge-oee/monitor_cort01_eastron_continuo.py new file mode 100755 index 0000000..23eb328 --- /dev/null +++ b/ucepsa/edge-oee-demo/revpi/edge-oee/monitor_cort01_eastron_continuo.py @@ -0,0 +1,212 @@ +#!/usr/bin/env python3 +import csv +import math +import signal +import struct +import time +from datetime import datetime +from pathlib import Path + +from pymodbus.client import ModbusTcpClient + +WISE_IP = "17.126.1.200" +UNIT = 1 +INTERVAL_S = 2.0 + +DATA_BASE = 1000 +DATA_COUNT = 20 + +WORD_ERR_BASE = 1200 +WORD_ERR_COUNT = 20 + +LOG_DIR = Path("logs") +LOG_DIR.mkdir(exist_ok=True) + +csv_path = LOG_DIR / f"cort01_eastron_continuo_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv" + +running = True + + +def stop_handler(signum, frame): + global running + running = False + + +signal.signal(signal.SIGINT, stop_handler) +signal.signal(signal.SIGTERM, stop_handler) + + +def f32_abcd(hi, lo): + raw = hi.to_bytes(2, "big") + lo.to_bytes(2, "big") + return struct.unpack(">f", raw)[0] + + +def read_holding(client, addr, count): + rr = client.read_holding_registers(address=addr, count=count, device_id=UNIT) + if rr.isError(): + raise RuntimeError(rr) + return rr.registers + + +def valid_number(x): + return isinstance(x, float) and math.isfinite(x) + + +def open_client(): + client = ModbusTcpClient(WISE_IP, port=502, timeout=3) + if not client.connect(): + raise RuntimeError(f"No conecta con WISE {WISE_IP}:502") + return client + + +fields = [ + "local_ts", + "ok", + "fail_reason", + "voltage_l1_v", + "current_l1_a", + "active_power_w", + "apparent_power_va", + "reactive_power_var", + "power_factor", + "frequency_hz", + "import_kwh", + "export_kwh", + "total_active_energy_kwh", + "word_err_nonzero", + "word_err_values", +] + +ok_count = 0 +fail_count = 0 +client = None + +print(f"Monitor continuo CORT-01 / wise-cort-01 / {WISE_IP}") +print(f"CSV: {csv_path}") +print("Ctrl+C para parar") +print() + +with csv_path.open("w", newline="") as f: + writer = csv.DictWriter(f, fieldnames=fields) + writer.writeheader() + + while running: + ts = datetime.now().isoformat(timespec="seconds") + row = { + "local_ts": ts, + "ok": False, + "fail_reason": "", + "voltage_l1_v": "", + "current_l1_a": "", + "active_power_w": "", + "apparent_power_va": "", + "reactive_power_var": "", + "power_factor": "", + "frequency_hz": "", + "import_kwh": "", + "export_kwh": "", + "total_active_energy_kwh": "", + "word_err_nonzero": "", + "word_err_values": "", + } + + try: + if client is None: + client = open_client() + + data = read_holding(client, DATA_BASE, DATA_COUNT) + word_err = read_holding(client, WORD_ERR_BASE, WORD_ERR_COUNT) + + voltage_l1_v = f32_abcd(data[0], data[1]) + current_l1_a = f32_abcd(data[2], data[3]) + active_power_w = f32_abcd(data[4], data[5]) + apparent_power_va = f32_abcd(data[6], data[7]) + reactive_power_var = f32_abcd(data[8], data[9]) + power_factor = f32_abcd(data[10], data[11]) + frequency_hz = f32_abcd(data[12], data[13]) + import_kwh = f32_abcd(data[14], data[15]) + export_kwh = f32_abcd(data[16], data[17]) + total_active_energy_kwh = f32_abcd(data[18], data[19]) + + non_zero_errs = [(WORD_ERR_BASE + i, v) for i, v in enumerate(word_err) if v != 0] + + values_ok = ( + valid_number(voltage_l1_v) + and valid_number(current_l1_a) + and valid_number(active_power_w) + and valid_number(frequency_hz) + and 150 <= voltage_l1_v <= 260 + and 45 <= frequency_hz <= 55 + and current_l1_a >= 0 + and len(non_zero_errs) == 0 + ) + + row.update({ + "ok": values_ok, + "voltage_l1_v": round(voltage_l1_v, 4), + "current_l1_a": round(current_l1_a, 6), + "active_power_w": round(active_power_w, 4), + "apparent_power_va": round(apparent_power_va, 4), + "reactive_power_var": round(reactive_power_var, 4), + "power_factor": round(power_factor, 6), + "frequency_hz": round(frequency_hz, 4), + "import_kwh": round(import_kwh, 6), + "export_kwh": round(export_kwh, 6), + "total_active_energy_kwh": round(total_active_energy_kwh, 6), + "word_err_nonzero": len(non_zero_errs), + "word_err_values": str(non_zero_errs), + }) + + if values_ok: + ok_count += 1 + status = "OK" + else: + fail_count += 1 + status = "BAD" + row["fail_reason"] = "valores_fuera_rango_o_word_err" + + print( + f"{ts} {status} " + f"OK={ok_count} FAIL={fail_count} " + f"V={voltage_l1_v:7.2f} " + f"I={current_l1_a:7.3f} " + f"P={active_power_w:8.2f} " + f"F={frequency_hz:6.2f} " + f"kWh={import_kwh:.4f} " + f"WORD_ERR={non_zero_errs}" + ) + + except Exception as e: + fail_count += 1 + row["ok"] = False + row["fail_reason"] = str(e) + + print( + f"{ts} ERROR " + f"OK={ok_count} FAIL={fail_count} " + f"{e}" + ) + + if client is not None: + try: + client.close() + except Exception: + pass + client = None + + writer.writerow(row) + f.flush() + time.sleep(INTERVAL_S) + +if client is not None: + client.close() + +total = ok_count + fail_count +print() +print("Resumen") +print("-------") +print(f"OK={ok_count}") +print(f"FAIL={fail_count}") +if total: + print(f"Disponibilidad lectura={ok_count / total * 100:.2f}%") +print(f"CSV guardado en: {csv_path}") diff --git a/ucepsa/edge-oee-demo/revpi/edge-oee/probe_3_wise_nodes.py b/ucepsa/edge-oee-demo/revpi/edge-oee/probe_3_wise_nodes.py new file mode 100755 index 0000000..4fbcde1 --- /dev/null +++ b/ucepsa/edge-oee-demo/revpi/edge-oee/probe_3_wise_nodes.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 +from pymodbus.client import ModbusTcpClient + +NODES = [ + {"machine_id": "CORT-01", "source_node": "wise-cort-00", "ip": "10.43.54.249"}, + {"machine_id": "CORT-00", "source_node": "wise-cort-01", "ip": "10.43.54.251"}, + {"machine_id": "CORT-02", "source_node": "wise-cort-02", "ip": "10.43.54.55"}, +] + +UNIT = 1 + + +def u32_lo_hi(lo, hi): + return (lo & 0xFFFF) + ((hi & 0xFFFF) << 16) + + +for node in NODES: + print() + print("==================================================") + print(node["machine_id"], node["source_node"], node["ip"]) + print("==================================================") + + client = ModbusTcpClient(node["ip"], port=502, timeout=3) + + if not client.connect(): + print("ERROR: no conecta") + continue + + try: + rr_di = client.read_discrete_inputs(address=0, count=2, device_id=UNIT) + if rr_di.isError(): + print("ERROR DI:", rr_di) + else: + print("DI0 marcha:", bool(rr_di.bits[0])) + print("DI1 estado:", bool(rr_di.bits[1])) + + rr_cnt = client.read_holding_registers(address=0, count=4, device_id=UNIT) + if rr_cnt.isError(): + print("ERROR counter:", rr_cnt) + else: + regs = rr_cnt.registers + di0_counter = u32_lo_hi(regs[0], regs[1]) + di1_counter = u32_lo_hi(regs[2], regs[3]) + print("DI0 counter:", di0_counter) + print("DI1 counter:", di1_counter) + print("raw counter:", regs) + + rr_energy = client.read_holding_registers(address=1000, count=20, device_id=UNIT) + if rr_energy.isError(): + print("ERROR energy:", rr_energy) + else: + print("Energy registers OK, first values:", rr_energy.registers[:6]) + + finally: + client.close() diff --git a/ucepsa/edge-oee-demo/revpi/edge-oee/probe_cort01_eastron.py b/ucepsa/edge-oee-demo/revpi/edge-oee/probe_cort01_eastron.py new file mode 100755 index 0000000..d66f14a --- /dev/null +++ b/ucepsa/edge-oee-demo/revpi/edge-oee/probe_cort01_eastron.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python3 +import struct +from pymodbus.client import ModbusTcpClient + +WISE_IP = "17.126.1.201" +UNIT = 1 + +DATA_BASE = 1000 # WISE 4X 1001 en base cero +DATA_COUNT = 20 + +ERROR_BASE = 1200 # WISE 4X 1201 en base cero, error codes COM1 expansion word +ERROR_COUNT = 20 + + +def f32_abcd(hi, lo): + raw = hi.to_bytes(2, "big") + lo.to_bytes(2, "big") + return struct.unpack(">f", raw)[0] + + +def read_holding(client, start, count): + rr = client.read_holding_registers(address=start, count=count, device_id=UNIT) + if rr.isError(): + raise RuntimeError(rr) + return rr.registers + + +client = ModbusTcpClient(WISE_IP, port=502, timeout=3) + +if not client.connect(): + raise RuntimeError(f"No conecta con WISE {WISE_IP}:502") + +try: + print("==================================================") + print("CORT-01 / wise-cort-01 /", WISE_IP) + print("==================================================") + + regs = read_holding(client, DATA_BASE, DATA_COUNT) + + print("\nRegistros mapeados Eastron en WISE") + print("----------------------------------") + for i in range(0, len(regs), 2): + addr1 = DATA_BASE + i + addr2 = DATA_BASE + i + 1 + hi = regs[i] + lo = regs[i + 1] + val = f32_abcd(hi, lo) + print(f"{addr1}/{addr2}: raw=({hi},{lo}) float={val:.6f}") + + print("\nInterpretación según mapeo actual") + print("---------------------------------") + labels = [ + ("voltage_l1_v", 0), + ("current_l1_a", 2), + ("active_power_w", 4), + ("apparent_power_va", 6), + ("reactive_power_var", 8), + ("power_factor", 10), + ("frequency_hz", 12), + ("import_kwh", 14), + ("export_kwh", 16), + ("total_active_energy_kwh", 18), + ] + + for name, offset in labels: + if offset + 1 < len(regs): + print(f"{name:28s}: {f32_abcd(regs[offset], regs[offset + 1]):.6f}") + + print("\nRegistros de error COM1 Expansion Word") + print("--------------------------------------") + try: + errs = read_holding(client, ERROR_BASE, ERROR_COUNT) + non_zero = False + for i, value in enumerate(errs): + if value != 0: + non_zero = True + print(f"ERROR_REG {ERROR_BASE + i}: {value}") + + if not non_zero: + print("Todos los registros de error están a 0") + except Exception as e: + print("No se pudieron leer registros de error:", e) + +finally: + client.close() + diff --git a/ucepsa/edge-oee-demo/revpi/edge-oee/probe_cort01_sdm630_3phase.py b/ucepsa/edge-oee-demo/revpi/edge-oee/probe_cort01_sdm630_3phase.py new file mode 100755 index 0000000..909dff0 --- /dev/null +++ b/ucepsa/edge-oee-demo/revpi/edge-oee/probe_cort01_sdm630_3phase.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +import struct +from pymodbus.client import ModbusTcpClient + +WISE_IP = "17.126.1.202" # ajusta si cambia +UNIT = 1 + +def f32(hi, lo): + return struct.unpack(">f", hi.to_bytes(2, "big") + lo.to_bytes(2, "big"))[0] + +client = ModbusTcpClient(WISE_IP, port=502, timeout=3) + +if not client.connect(): + raise SystemExit("No conecta con WISE") + +try: + rr = client.read_holding_registers(address=1000, count=34, device_id=UNIT) + if rr.isError(): + raise SystemExit(rr) + + r = rr.registers + + values = { + "V_L1_N": f32(r[0], r[1]), + "V_L2_N": f32(r[2], r[3]), + "V_L3_N": f32(r[4], r[5]), + + "I_L1": f32(r[6], r[7]), + "I_L2": f32(r[8], r[9]), + "I_L3": f32(r[10], r[11]), + + "P_L1_W": f32(r[12], r[13]), + "P_L2_W": f32(r[14], r[15]), + "P_L3_W": f32(r[16], r[17]), + + "P_TOTAL_W": f32(r[18], r[19]), + "S_TOTAL_VA": f32(r[20], r[21]), + "Q_TOTAL_VAR": f32(r[22], r[23]), + "PF_TOTAL": f32(r[24], r[25]), + + "FREQ_HZ": f32(r[26], r[27]), + "IMPORT_KWH": f32(r[28], r[29]), + "EXPORT_KWH": f32(r[30], r[31]), + "TOTAL_ACTIVE_KWH": f32(r[32], r[33]), + } + + for k, v in values.items(): + print(f"{k:18s}: {v:.4f}") + +finally: + client.close() diff --git a/ucepsa/edge-oee-demo/revpi/edge-oee/probe_modbus_float.py b/ucepsa/edge-oee-demo/revpi/edge-oee/probe_modbus_float.py new file mode 100755 index 0000000..a2002a7 --- /dev/null +++ b/ucepsa/edge-oee-demo/revpi/edge-oee/probe_modbus_float.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python3 +import argparse +import struct +from pymodbus.client import ModbusSerialClient + + +def regs_to_bytes(regs, mode): + a = regs[0] + b = regs[1] + + ab = a.to_bytes(2, "big") + cd = b.to_bytes(2, "big") + + if mode == "ABCD": + return ab + cd + if mode == "CDAB": + return cd + ab + if mode == "BADC": + return ab[::-1] + cd[::-1] + if mode == "DCBA": + return cd[::-1] + ab[::-1] + + raise ValueError(mode) + + +def decode_float_candidates(regs): + out = {} + for mode in ["ABCD", "CDAB", "BADC", "DCBA"]: + try: + raw = regs_to_bytes(regs, mode) + out[mode] = struct.unpack(">f", raw)[0] + except Exception as exc: + out[mode] = f"ERR: {exc}" + return out + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--port", default="/dev/ttyRS485") + parser.add_argument("--slave", type=int, default=1) + parser.add_argument("--baud", type=int, default=9600) + parser.add_argument("--parity", default="N", choices=["N", "E", "O"]) + parser.add_argument("--stopbits", type=int, default=1) + parser.add_argument("--bytesize", type=int, default=8) + parser.add_argument("--address", type=int, default=0) + parser.add_argument("--count", type=int, default=2) + parser.add_argument("--timeout", type=float, default=1.5) + args = parser.parse_args() + + print("--- MODBUS RTU PROBE ---") + print(f"port={args.port}") + print(f"slave={args.slave}") + print(f"baud={args.baud}") + print(f"parity={args.parity}") + print(f"address={args.address}") + print(f"count={args.count}") + + client = ModbusSerialClient( + port=args.port, + baudrate=args.baud, + parity=args.parity, + stopbits=args.stopbits, + bytesize=args.bytesize, + timeout=args.timeout, + ) + + if not client.connect(): + print("[ERROR] Cannot open serial port") + return 2 + + try: + rr = client.read_input_registers( + address=args.address, + count=args.count, + device_id=args.slave, + ) + + if rr.isError(): + print(f"[ERROR] Modbus error: {rr}") + return 3 + + regs = rr.registers + print(f"registers={regs}") + print("float_candidates:") + for mode, value in decode_float_candidates(regs).items(): + print(f" {mode}: {value}") + + return 0 + except Exception as exc: + print(f"[ERROR] No Modbus response: {type(exc).__name__}: {exc}") + return 3 + finally: + client.close() + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/ucepsa/edge-oee-demo/revpi/edge-oee/probe_wise_counter_modbus.py b/ucepsa/edge-oee-demo/revpi/edge-oee/probe_wise_counter_modbus.py new file mode 100755 index 0000000..dc1f57d --- /dev/null +++ b/ucepsa/edge-oee-demo/revpi/edge-oee/probe_wise_counter_modbus.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +from pymodbus.client import ModbusTcpClient + + +WISE_IP = "10.43.54.249" +WISE_PORT = 502 +UNIT = 1 + + +def read_holding(client, start, count): + rr = client.read_holding_registers( + address=start, + count=count, + device_id=UNIT, + ) + + if rr.isError(): + raise RuntimeError(rr) + + return rr.registers + + +def u32_hi_lo(hi, lo): + return ((hi & 0xFFFF) << 16) | (lo & 0xFFFF) + + +def u32_lo_hi(hi, lo): + return ((lo & 0xFFFF) << 16) | (hi & 0xFFFF) + + +def print_block(name, start, regs): + print(f"\n{name} start={start}") + print("16-bit:") + for i, value in enumerate(regs): + print(f"addr {start+i:4d}: {value}") + + print("\n32-bit pairs:") + for i in range(0, len(regs) - 1, 2): + a = start + i + hi = regs[i] + lo = regs[i + 1] + print( + f"addr {a:4d}/{a+1:4d}: " + f"hi_lo={u32_hi_lo(hi, lo)} " + f"lo_hi={u32_lo_hi(hi, lo)}" + ) + + +def snapshot(client): + return { + "base0": read_holding(client, 0, 12), + "base1": read_holding(client, 1, 12), + } + + +def main(): + client = ModbusTcpClient(WISE_IP, port=WISE_PORT, timeout=3) + + if not client.connect(): + raise RuntimeError("No conecta con WISE") + + try: + print("Snapshot inicial") + before = snapshot(client) + + for name, regs in before.items(): + start = 0 if name == "base0" else 1 + print_block(name, start, regs) + + input("\nPulsa DI1 exactamente 10 veces y después pulsa ENTER...") + + print("\nSnapshot final") + after = snapshot(client) + + for name, regs in after.items(): + start = 0 if name == "base0" else 1 + print_block(name, start, regs) + + print("\nDeltas 16-bit:") + for name in before: + start = 0 if name == "base0" else 1 + for i, (b, a) in enumerate(zip(before[name], after[name])): + if a != b: + delta = (a - b) & 0xFFFF + print(f"{name} addr {start+i}: before={b} after={a} delta={delta}") + + print("\nDeltas 32-bit:") + for name in before: + start = 0 if name == "base0" else 1 + bregs = before[name] + aregs = after[name] + + for i in range(0, len(bregs) - 1, 2): + b_hi_lo = u32_hi_lo(bregs[i], bregs[i + 1]) + a_hi_lo = u32_hi_lo(aregs[i], aregs[i + 1]) + d_hi_lo = (a_hi_lo - b_hi_lo) & 0xFFFFFFFF + + b_lo_hi = u32_lo_hi(bregs[i], bregs[i + 1]) + a_lo_hi = u32_lo_hi(aregs[i], aregs[i + 1]) + d_lo_hi = (a_lo_hi - b_lo_hi) & 0xFFFFFFFF + + if d_hi_lo or d_lo_hi: + print( + f"{name} addr {start+i}/{start+i+1}: " + f"delta_hi_lo={d_hi_lo} " + f"delta_lo_hi={d_lo_hi}" + ) + + finally: + client.close() + + +if __name__ == "__main__": + main() diff --git a/ucepsa/edge-oee-demo/revpi/edge-oee/probe_wise_tcp.py b/ucepsa/edge-oee-demo/revpi/edge-oee/probe_wise_tcp.py new file mode 100755 index 0000000..8dbc5b2 --- /dev/null +++ b/ucepsa/edge-oee-demo/revpi/edge-oee/probe_wise_tcp.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +import argparse +import struct +from pymodbus.client import ModbusTcpClient + + +def call_with_unit(fn, unit, **kwargs): + """ + Compatible con varias versiones de pymodbus: + - pymodbus 3.x usa device_id + - versiones anteriores pueden usar slave o unit + """ + last_error = None + + for key in ("device_id", "slave", "unit"): + try: + return fn(**kwargs, **{key: unit}) + except TypeError as e: + last_error = e + + raise last_error + + +def f32_abcd(reg_hi, reg_lo): + raw = reg_hi.to_bytes(2, "big") + reg_lo.to_bytes(2, "big") + return struct.unpack(">f", raw)[0] + + +def print_holding(client, unit, start, count): + print(f"\n=== HOLDING REGISTERS start={start}, count={count}, unit={unit} ===") + + try: + rr = call_with_unit( + client.read_holding_registers, + unit, + address=start, + count=count + ) + except Exception as e: + print("EXCEPTION:", repr(e)) + return + + if rr.isError(): + print("MODBUS ERROR:", rr) + return + + regs = rr.registers + + for i, value in enumerate(regs): + print(f"{start + i}: {value}") + + print("\nFloat ABCD por parejas:") + for i in range(0, len(regs) - 1, 2): + try: + value = f32_abcd(regs[i], regs[i + 1]) + print(f"{start + i}/{start + i + 1}: {value:.6f}") + except Exception as e: + print(f"{start + i}/{start + i + 1}: ERROR {e}") + + +def print_bits(client, unit, kind, start, count): + print(f"\n=== {kind.upper()} start={start}, count={count}, unit={unit} ===") + + fn = client.read_discrete_inputs if kind == "discrete" else client.read_coils + + try: + rr = call_with_unit( + fn, + unit, + address=start, + count=count + ) + except Exception as e: + print("EXCEPTION:", repr(e)) + return + + if rr.isError(): + print("MODBUS ERROR:", rr) + return + + bits = rr.bits[:count] + + for i, value in enumerate(bits): + print(f"{start + i}: {int(value)}") + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--host", required=True) + parser.add_argument("--port", type=int, default=502) + parser.add_argument("--unit", type=int, default=1) + args = parser.parse_args() + + client = ModbusTcpClient(args.host, port=args.port, timeout=3) + + if not client.connect(): + print(f"ERROR: no conecta con {args.host}:{args.port}") + return + + print(f"OK: conectado a {args.host}:{args.port}") + + try: + # El WISE muestra Mapping Address(4X) 1001. + # Probamos base cero y base uno. + print_holding(client, args.unit, 1000, 20) + print_holding(client, args.unit, 1001, 20) + + # Intentos típicos para DI por Modbus/TCP. + # Luego afinamos según Bit Status del WISE. + print_bits(client, args.unit, "discrete", 0, 16) + print_bits(client, args.unit, "discrete", 1, 16) + print_bits(client, args.unit, "coils", 0, 16) + print_bits(client, args.unit, "coils", 1, 16) + + finally: + client.close() + + +if __name__ == "__main__": + main() diff --git a/ucepsa/edge-oee-demo/revpi/edge-oee/publish_order_state.py b/ucepsa/edge-oee-demo/revpi/edge-oee/publish_order_state.py new file mode 100755 index 0000000..f9eb3db --- /dev/null +++ b/ucepsa/edge-oee-demo/revpi/edge-oee/publish_order_state.py @@ -0,0 +1,129 @@ +#!/usr/bin/env python3 +import json +import os +import sys +from datetime import datetime, timezone +from pathlib import Path + +import paho.mqtt.client as mqtt + + +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 + k, v = line.split("=", 1) + env[k.strip()] = v.strip().strip('"').strip("'") + return env + + +def utc_now(): + return datetime.now(timezone.utc).isoformat(timespec="seconds").replace("+00:00", "Z") + + +def build_payload(machine_id, mode): + base = { + "machine_id": machine_id, + "ts": utc_now(), + "source": "odoo_simulator", + } + + if mode == "loaded": + base.update({ + "order_present": True, + "order_coherent": True, + "odoo_state": "ready", + "workorder_id": 1000, + "production_order": f"SIM-{machine_id}-OK", + "product": "Orden simulada correcta", + }) + + elif mode == "no_order": + base.update({ + "order_present": False, + "order_coherent": False, + "odoo_state": "none", + "workorder_id": None, + "production_order": None, + "product": None, + }) + + elif mode == "bad": + base.update({ + "order_present": True, + "order_coherent": False, + "odoo_state": "wrong_machine", + "workorder_id": 9999, + "production_order": f"SIM-{machine_id}-BAD", + "product": "Orden simulada incoherente", + "reason": "Orden asignada a otra máquina o estado no válido", + }) + + elif mode == "fault": + base.update({ + "order_present": True, + "order_coherent": True, + "odoo_state": "ready", + "workorder_id": 1001, + "production_order": f"SIM-{machine_id}-FAULT", + "product": "Orden simulada con fallo máquina", + "force_machine_fault": True, + }) + + else: + raise SystemExit(f"Modo no reconocido: {mode}") + + return base + + +def main(): + if len(sys.argv) != 3: + print("Uso:") + print(" python publish_order_state.py CORT-00 loaded") + print(" python publish_order_state.py CORT-00 no_order") + print(" python publish_order_state.py CORT-00 bad") + print(" python publish_order_state.py CORT-00 fault") + raise SystemExit(1) + + machine_id = sys.argv[1] + mode = sys.argv[2] + + env = read_env() + + mqtt_host = env.get("MQTT_HOST", "127.0.0.1") + mqtt_port = int(env.get("MQTT_PORT", "1883")) + mqtt_user = env.get("MQTT_USERNAME") or env.get("MQTT_USER") + mqtt_password = env.get("MQTT_PASSWORD") + + tenant = env.get("TENANT", "ucepsa") + site = env.get("SITE", "ucepsa_onpremise") + vertical = env.get("VERTICAL", "edge_oee") + + topic = f"vertical/{tenant}/{site}/{vertical}/order_state/{machine_id}" + payload = build_payload(machine_id, mode) + + client = mqtt.Client(client_id=f"order_state_sim_{machine_id}") + if mqtt_user: + client.username_pw_set(mqtt_user, mqtt_password) + + client.connect(mqtt_host, mqtt_port, 60) + client.loop_start() + + info = client.publish(topic, json.dumps(payload, separators=(",", ":")), qos=0, retain=True) + info.wait_for_publish(timeout=5) + + client.loop_stop() + client.disconnect() + + print(f"Publicado en {topic}") + print(json.dumps(payload, indent=2, ensure_ascii=False)) + + +if __name__ == "__main__": + main() diff --git a/ucepsa/edge-oee-demo/revpi/edge-oee/scan_wise_counter.py b/ucepsa/edge-oee-demo/revpi/edge-oee/scan_wise_counter.py new file mode 100755 index 0000000..a386190 --- /dev/null +++ b/ucepsa/edge-oee-demo/revpi/edge-oee/scan_wise_counter.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python3 +import argparse +import time +from pymodbus.client import ModbusTcpClient + + +def read_block(client, kind, start, count, unit): + fn = client.read_holding_registers if kind == "holding" else client.read_input_registers + + try: + rr = fn(address=start, count=count, device_id=unit) + except Exception: + return None + + if rr.isError(): + return None + + return rr.registers + + +def snapshot(client, unit): + ranges = [ + ("holding", 0, 200), + ("holding", 1000, 100), + ("holding", 2000, 200), + ("holding", 3000, 200), + ("holding", 4000, 200), + ("input", 0, 200), + ("input", 1000, 100), + ("input", 2000, 200), + ("input", 3000, 200), + ("input", 4000, 200), + ] + + data = {} + + for kind, start, count in ranges: + regs = read_block(client, kind, start, count, unit) + if regs is None: + continue + + for i, value in enumerate(regs): + data[(kind, start + i)] = value + + return data + + +def u16_delta(before, after): + return (after - before) & 0xFFFF + + +def u32(hi, lo): + return ((hi & 0xFFFF) << 16) | (lo & 0xFFFF) + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--host", required=True) + parser.add_argument("--unit", type=int, default=1) + parser.add_argument("--expected", type=int, default=10) + args = parser.parse_args() + + client = ModbusTcpClient(args.host, port=502, timeout=2) + + if not client.connect(): + raise RuntimeError("No conecta con WISE") + + try: + print("Tomando snapshot inicial...") + before = snapshot(client, args.unit) + + print(f"\nAhora pulsa DI1 exactamente {args.expected} veces.") + input("Cuando termines, pulsa ENTER aquí...") + + time.sleep(0.5) + + print("Tomando snapshot final...") + after = snapshot(client, args.unit) + + print("\nCambios 16-bit detectados:") + changes = [] + + for key, before_value in before.items(): + if key not in after: + continue + + after_value = after[key] + if after_value != before_value: + delta = u16_delta(before_value, after_value) + closeness = abs(delta - args.expected) + changes.append((closeness, key, before_value, after_value, delta)) + + for closeness, key, b, a, delta in sorted(changes, key=lambda x: x[0])[:40]: + kind, addr = key + print(f"{kind:7s} addr={addr:5d} before={b:6d} after={a:6d} delta={delta}") + + print("\nCambios 32-bit por parejas:") + pair_changes = [] + + keys = sorted(before.keys()) + + for kind, addr in keys: + key_hi = (kind, addr) + key_lo = (kind, addr + 1) + + if key_lo not in before or key_hi not in after or key_lo not in after: + continue + + before_u32 = u32(before[key_hi], before[key_lo]) + after_u32 = u32(after[key_hi], after[key_lo]) + + if before_u32 != after_u32: + delta = (after_u32 - before_u32) & 0xFFFFFFFF + closeness = abs(delta - args.expected) + pair_changes.append((closeness, kind, addr, before_u32, after_u32, delta)) + + for closeness, kind, addr, b, a, delta in sorted(pair_changes, key=lambda x: x[0])[:40]: + print(f"{kind:7s} addr={addr:5d}/{addr+1:5d} before={b} after={a} delta={delta}") + + finally: + client.close() + + +if __name__ == "__main__": + main() diff --git a/ucepsa/edge-oee-demo/revpi/edge-oee/wait_mqtt.py b/ucepsa/edge-oee-demo/revpi/edge-oee/wait_mqtt.py new file mode 100755 index 0000000..736157e --- /dev/null +++ b/ucepsa/edge-oee-demo/revpi/edge-oee/wait_mqtt.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +import os +import socket +import time +import sys +from pathlib import Path + +BASE_DIR = Path(__file__).resolve().parent +ENV_PATH = BASE_DIR / ".env" + +if ENV_PATH.exists(): + for raw in ENV_PATH.read_text().splitlines(): + line = raw.strip() + if line and not line.startswith("#") and "=" in line: + k, v = line.split("=", 1) + os.environ.setdefault(k.strip(), v.strip()) + +host = os.environ.get("MQTT_HOST", "17.126.1.70") +port = int(os.environ.get("MQTT_PORT", "1883")) + +deadline = time.time() + 120 + +while time.time() < deadline: + try: + with socket.create_connection((host, port), timeout=5): + print(f"[OK] MQTT reachable {host}:{port}") + sys.exit(0) + except OSError as exc: + print(f"[WAIT] MQTT {host}:{port} not reachable yet: {exc}") + time.sleep(5) + +print(f"[ERROR] MQTT {host}:{port} not reachable after timeout") +sys.exit(1) diff --git a/ucepsa/edge-oee-demo/revpi/edge-oee/wise_di_edge_probe.py b/ucepsa/edge-oee-demo/revpi/edge-oee/wise_di_edge_probe.py new file mode 100755 index 0000000..dcf6767 --- /dev/null +++ b/ucepsa/edge-oee-demo/revpi/edge-oee/wise_di_edge_probe.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 +import time +from datetime import datetime, timezone +from pymodbus.client import ModbusTcpClient + + +WISE_IP = "10.43.54.249" +WISE_PORT = 502 +WISE_UNIT = 1 + +POLL_INTERVAL_S = 0.02 +DEBOUNCE_S = 0.02 + + +def utc_now(): + return datetime.now(timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z") + + +def read_di(client): + rr = client.read_discrete_inputs( + address=0, + count=2, + device_id=WISE_UNIT, + ) + + if rr.isError(): + raise RuntimeError(f"Error leyendo DI: {rr}") + + di0 = bool(rr.bits[0]) + di1 = bool(rr.bits[1]) + + return di0, di1 + + +def main(): + client = ModbusTcpClient(WISE_IP, port=WISE_PORT, timeout=1) + + if not client.connect(): + raise RuntimeError(f"No conecta con WISE {WISE_IP}:{WISE_PORT}") + + print(f"Conectado a WISE {WISE_IP}:{WISE_PORT}") + print("DI0 = marcha / automático") + print("DI1 = pulso sellador") + print("Prueba: empieza con DI1 abierto, pulsa y suelta claramente.\n") + + last_di0 = None + last_di1 = None + last_rising_ts = 0.0 + count = 0 + + loops = 0 + stats_start = time.perf_counter() + max_loop_s = 0.0 + + try: + while True: + loop_start = time.perf_counter() + now = loop_start + + di0, di1 = read_di(client) + + if last_di0 is None: + last_di0 = di0 + last_di1 = di1 + print(f"{utc_now()} INIT DI0={int(di0)} DI1={int(di1)}") + + if di0 != last_di0: + print(f"{utc_now()} CHANGE DI0 {int(last_di0)} -> {int(di0)}") + last_di0 = di0 + + if di1 != last_di1: + print(f"{utc_now()} CHANGE DI1 {int(last_di1)} -> {int(di1)}") + + if di1 and not last_di1: + if (now - last_rising_ts) >= DEBOUNCE_S: + count += 1 + last_rising_ts = now + print(f"{utc_now()} RISING DI1 count={count}") + + last_di1 = di1 + + loops += 1 + loop_s = time.perf_counter() - loop_start + max_loop_s = max(max_loop_s, loop_s) + + if time.perf_counter() - stats_start >= 5.0: + elapsed = time.perf_counter() - stats_start + hz = loops / elapsed + print(f"{utc_now()} STATS poll_hz={hz:.1f} max_loop_ms={max_loop_s * 1000:.1f} count={count}") + loops = 0 + stats_start = time.perf_counter() + max_loop_s = 0.0 + + sleep_s = max(0.0, POLL_INTERVAL_S - loop_s) + time.sleep(sleep_s) + + except KeyboardInterrupt: + print(f"\nParado. Conteo final DI1={count}") + + finally: + client.close() + + +if __name__ == "__main__": + main() diff --git a/ucepsa/edge-oee-demo/revpi/edge-oee/wise_nodes.json b/ucepsa/edge-oee-demo/revpi/edge-oee/wise_nodes.json new file mode 100644 index 0000000..03b354b --- /dev/null +++ b/ucepsa/edge-oee-demo/revpi/edge-oee/wise_nodes.json @@ -0,0 +1,37 @@ +{ + "nodes": [ + { + "enabled": true, + "machine_id": "CORT-01", + "source_node": "wise-cort-01", + "wise_ip": "17.126.1.201", + "wise_port": 502, + "unit": 1, + "holding_base": 1000, + "holding_count": 34, + "session_env": "WISE_SESSION_ID_CORT_01" + }, + { + "enabled": true, + "machine_id": "CORT-00", + "source_node": "wise-cort-00", + "wise_ip": "17.126.1.200", + "wise_port": 502, + "unit": 1, + "holding_base": 1000, + "holding_count": 34, + "session_env": "WISE_SESSION_ID_CORT_00" + }, + { + "enabled": true, + "machine_id": "CORT-02", + "source_node": "wise-cort-02", + "wise_ip": "17.126.1.202", + "wise_port": 502, + "unit": 1, + "holding_base": 1000, + "holding_count": 34, + "session_env": "WISE_SESSION_ID_CORT_02" + } + ] +} diff --git a/ucepsa/edge-oee-demo/revpi/edge-oee/wise_reader_local_loop.py b/ucepsa/edge-oee-demo/revpi/edge-oee/wise_reader_local_loop.py new file mode 100755 index 0000000..6997a06 --- /dev/null +++ b/ucepsa/edge-oee-demo/revpi/edge-oee/wise_reader_local_loop.py @@ -0,0 +1,222 @@ +#!/usr/bin/env python3 +import json +import struct +import time +from collections import deque +from datetime import datetime, timezone + +from pymodbus.client import ModbusTcpClient + + +WISE_IP = "10.43.54.249" +WISE_PORT = 502 +WISE_UNIT = 1 + +HOLDING_BASE = 1000 +DI_BASE = 0 + +POLL_INTERVAL_S = 0.1 +PUBLISH_INTERVAL_S = 2.0 +DEBOUNCE_S = 0.05 +RATE_WINDOW_S = 60.0 + +TENANT = "ucepsa" +SITE = "demo_edge_oee" +VERTICAL = "edge_oee" +ASSET = "revpi_oee_node_01" +MACHINE_ID = "CORT-01" +SOURCE_NODE = "wise-banco-01" +NODE_TYPE = "revpi_wise_oee_gateway" +FW = "0.1.0" + + +def utc_now(): + return datetime.now(timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z") + + +def f32_abcd(reg_hi, reg_lo): + raw = int(reg_hi).to_bytes(2, "big") + int(reg_lo).to_bytes(2, "big") + return struct.unpack(">f", raw)[0] + + +def read_holdings(client): + rr = client.read_holding_registers( + address=HOLDING_BASE, + count=20, + device_id=WISE_UNIT, + ) + + if rr.isError(): + raise RuntimeError(f"Error leyendo holding registers: {rr}") + + return rr.registers + + +def read_discrete_inputs(client): + rr = client.read_discrete_inputs( + address=DI_BASE, + count=2, + device_id=WISE_UNIT, + ) + + if rr.isError(): + raise RuntimeError(f"Error leyendo discrete inputs: {rr}") + + di0 = bool(rr.bits[0]) + di1 = bool(rr.bits[1]) + + return di0, di1 + + +def decode_energy(regs): + voltage_l1_v = f32_abcd(regs[0], regs[1]) + current_l1_a = f32_abcd(regs[2], regs[3]) + active_power_w = f32_abcd(regs[4], regs[5]) + apparent_power_va = f32_abcd(regs[6], regs[7]) + reactive_power_var = f32_abcd(regs[8], regs[9]) + power_factor = f32_abcd(regs[10], regs[11]) + frequency_hz = f32_abcd(regs[12], regs[13]) + import_kwh = f32_abcd(regs[14], regs[15]) + export_kwh = f32_abcd(regs[16], regs[17]) + total_active_energy_kwh = f32_abcd(regs[18], regs[19]) + + return { + "voltage_l1_v": round(voltage_l1_v, 3), + "current_l1_a": round(current_l1_a, 6), + "active_power_w": round(active_power_w, 3), + "power_total_kw": round(active_power_w / 1000.0, 6), + "apparent_power_va": round(apparent_power_va, 3), + "reactive_power_var": round(reactive_power_var, 3), + "power_factor": round(power_factor, 6), + "frequency_hz": round(frequency_hz, 3), + "import_kwh": round(import_kwh, 6), + "export_kwh": round(export_kwh, 6), + "total_active_energy_kwh": round(total_active_energy_kwh, 6), + } + + +def main(): + client = ModbusTcpClient(WISE_IP, port=WISE_PORT, timeout=3) + + cycle_pulses = deque() + cycle_pulse_count = 0 + + last_cycle_state = None + last_auto_signal = None + last_pulse_ts = 0.0 + last_publish_ts = 0.0 + + print(f"Conectando con WISE {WISE_IP}:{WISE_PORT} unit={WISE_UNIT}") + + while True: + now = time.time() + + try: + if not client.connected: + if not client.connect(): + raise RuntimeError("No conecta con WISE") + + auto_signal, cycle_state = read_discrete_inputs(client) + + if last_cycle_state is None: + last_cycle_state = cycle_state + + rising_edge = cycle_state and not last_cycle_state + + if rising_edge and (now - last_pulse_ts) >= DEBOUNCE_S: + cycle_pulse_count += 1 + cycle_pulses.append(now) + last_pulse_ts = now + + last_cycle_state = cycle_state + + while cycle_pulses and (now - cycle_pulses[0]) > RATE_WINDOW_S: + cycle_pulses.popleft() + + cycle_rate_ppm = len(cycle_pulses) * 60.0 / RATE_WINDOW_S + last_cycle_age_s = None if not cycle_pulses else round(now - cycle_pulses[-1], 3) + + if last_auto_signal is not None and auto_signal != last_auto_signal: + event_name = "machine_started" if auto_signal else "machine_stopped" + + event = { + "ts": utc_now(), + "tenant": TENANT, + "site": SITE, + "vertical": VERTICAL, + "asset": ASSET, + "machine_id": MACHINE_ID, + "source_node": SOURCE_NODE, + "event_type": "machine_state_changed", + "event_name": event_name, + "previous_auto_signal": last_auto_signal, + "new_auto_signal": auto_signal, + "machine_running": auto_signal, + "reason_pending": not auto_signal, + } + + print("EVENT", json.dumps(event, ensure_ascii=False)) + + last_auto_signal = auto_signal + + if (now - last_publish_ts) >= PUBLISH_INTERVAL_S: + regs = read_holdings(client) + energy = decode_energy(regs) + + payload = { + "ts": utc_now(), + "asset": ASSET, + "tenant": TENANT, + "site": SITE, + "vertical": VERTICAL, + "node_type": NODE_TYPE, + "fw": FW, + "machine_id": MACHINE_ID, + "source_node": SOURCE_NODE, + "wise_host": WISE_IP, + "machine_auto_signal": auto_signal, + "machine_running": auto_signal, + "cycle_input_state": cycle_state, + "cycle_pulse_count": cycle_pulse_count, + "cycle_rate_ppm": round(cycle_rate_ppm, 3), + "last_cycle_age_s": last_cycle_age_s, + "digital_inputs": { + "DI0": auto_signal, + "DI1": cycle_state, + }, + **energy, + "comm_ok": True, + "sample_valid": True, + } + + print("TELEMETRY", json.dumps(payload, ensure_ascii=False)) + last_publish_ts = now + + except KeyboardInterrupt: + print("Parado por usuario") + break + + except Exception as exc: + print("ERROR", json.dumps({ + "ts": utc_now(), + "asset": ASSET, + "machine_id": MACHINE_ID, + "source_node": SOURCE_NODE, + "wise_host": WISE_IP, + "comm_ok": False, + "sample_valid": False, + "error": str(exc), + }, ensure_ascii=False)) + + try: + client.close() + except Exception: + pass + + time.sleep(2) + + time.sleep(POLL_INTERVAL_S) + + +if __name__ == "__main__": + main() diff --git a/ucepsa/edge-oee-demo/revpi/edge-oee/wise_reader_local_loop_v2.py b/ucepsa/edge-oee-demo/revpi/edge-oee/wise_reader_local_loop_v2.py new file mode 100755 index 0000000..7de78ad --- /dev/null +++ b/ucepsa/edge-oee-demo/revpi/edge-oee/wise_reader_local_loop_v2.py @@ -0,0 +1,303 @@ +#!/usr/bin/env python3 +import json +import os +import struct +import time +import urllib.request +from collections import deque +from datetime import datetime, timezone + +from pymodbus.client import ModbusTcpClient + + +WISE_IP = "10.43.54.249" +WISE_PORT = 502 +WISE_UNIT = 1 + +WISE_DI_URL = f"http://{WISE_IP}/di_value/slot_0" +WISE_SESSION_ID = os.getenv("WISE_SESSION_ID", "").strip() + +HOLDING_BASE = 1000 + +DI_POLL_INTERVAL_S = 0.2 +ENERGY_INTERVAL_S = 2.0 +PRINT_INTERVAL_S = 2.0 +RATE_WINDOW_S = 60.0 + +TENANT = "ucepsa" +SITE = "demo_edge_oee" +VERTICAL = "edge_oee" +ASSET = "revpi_oee_node_01" +MACHINE_ID = "CORT-01" +SOURCE_NODE = "wise-banco-01" +NODE_TYPE = "revpi_wise_oee_gateway" +FW = "0.2.0" + + +def utc_now(): + return datetime.now(timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z") + + +def f32_abcd(reg_hi, reg_lo): + raw = int(reg_hi).to_bytes(2, "big") + int(reg_lo).to_bytes(2, "big") + return struct.unpack(">f", raw)[0] + + +def read_wise_di_api(): + from urllib.error import HTTPError + + if not WISE_SESSION_ID: + raise RuntimeError( + "Falta WISE_SESSION_ID. Ejecuta primero: export WISE_SESSION_ID" + ) + + req = urllib.request.Request( + WISE_DI_URL, + headers={ + "Accept": "text/plain, */*; q=0.01", + "X-Requested-With": "XMLHttpRequest", + "Referer": f"http://{WISE_IP}/config/index.html", + "User-Agent": "Mozilla/5.0", + "Cookie": f"adamsessionid={WISE_SESSION_ID}", + }, + ) + + try: + with urllib.request.urlopen(req, timeout=2) as response: + text = response.read().decode("utf-8", errors="replace") + except HTTPError as e: + body = e.read().decode("utf-8", errors="replace") + raise RuntimeError( + f"WISE HTTP error {e.code}: {e.reason}. Body={body[:300]!r}" + ) + + data = json.loads(text) + + if isinstance(data, dict) and data.get("Err"): + raise RuntimeError( + f"WISE API error Err={data.get('Err')} Msg={data.get('Msg')!r}" + ) + + channels = {int(item["Ch"]): item for item in data.get("DIVal", [])} + + ch0 = channels.get(0, {}) + ch1 = channels.get(1, {}) + + return { + "di0_stat": bool(ch0.get("Stat", 0)), + "di1_stat": bool(ch1.get("Stat", 0)), + "di1_counter_raw": int(ch1.get("CtFq", 0)), + "di1_counting": bool(ch1.get("Cnting", 0)), + "di1_mode": int(ch1.get("Md", -1)), + "raw": data, + } + + +def read_holdings(client): + if not client.connected: + if not client.connect(): + raise RuntimeError("No conecta con WISE por Modbus/TCP") + + rr = client.read_holding_registers( + address=HOLDING_BASE, + count=20, + device_id=WISE_UNIT, + ) + + if rr.isError(): + raise RuntimeError(f"Error leyendo holding registers: {rr}") + + return rr.registers + + +def decode_energy(regs): + active_power_w = f32_abcd(regs[4], regs[5]) + + return { + "voltage_l1_v": round(f32_abcd(regs[0], regs[1]), 3), + "current_l1_a": round(f32_abcd(regs[2], regs[3]), 6), + "active_power_w": round(active_power_w, 3), + "power_total_kw": round(active_power_w / 1000.0, 6), + "apparent_power_va": round(f32_abcd(regs[6], regs[7]), 3), + "reactive_power_var": round(f32_abcd(regs[8], regs[9]), 3), + "power_factor": round(f32_abcd(regs[10], regs[11]), 6), + "frequency_hz": round(f32_abcd(regs[12], regs[13]), 3), + "import_kwh": round(f32_abcd(regs[14], regs[15]), 6), + "export_kwh": round(f32_abcd(regs[16], regs[17]), 6), + "total_active_energy_kwh": round(f32_abcd(regs[18], regs[19]), 6), + } + + +def counter_delta(previous, current): + if previous is None: + return 0, False + + if current >= previous: + return current - previous, False + + # Reset manual, reinicio del WISE o pérdida del contador. + return 0, True + + +def main(): + client = ModbusTcpClient(WISE_IP, port=WISE_PORT, timeout=3) + + previous_wise_counter = None + cycle_pulse_count = 0 + cycle_events = deque() + + last_auto_signal = None + last_energy = {} + last_energy_ts = 0.0 + last_print_ts = 0.0 + last_delta = 0 + counter_reset_detected = False + + print(f"Leyendo WISE {WISE_IP}") + print(f"DI API: {WISE_DI_URL}") + print(f"Modbus/TCP: {WISE_IP}:{WISE_PORT}, holding base {HOLDING_BASE}") + print("DI0 = marcha / automático") + print("DI1 = contador de ciclos\n") + + while True: + now = time.time() + + try: + di = read_wise_di_api() + + auto_signal = di["di0_stat"] + cycle_input_state = di["di1_stat"] + wise_counter = di["di1_counter_raw"] + counter_valid = di["di1_mode"] == 1 and di["di1_counting"] + + delta, reset = counter_delta(previous_wise_counter, wise_counter) + counter_reset_detected = reset + + if delta > 0: + cycle_pulse_count += delta + + for _ in range(min(delta, 1000)): + cycle_events.append(now) + + print( + "CYCLES", + json.dumps( + { + "ts": utc_now(), + "delta": delta, + "wise_counter": wise_counter, + "cycle_pulse_count": cycle_pulse_count, + }, + ensure_ascii=False, + ), + ) + + previous_wise_counter = wise_counter + last_delta = delta + + while cycle_events and (now - cycle_events[0]) > RATE_WINDOW_S: + cycle_events.popleft() + + cycle_rate_ppm = len(cycle_events) * 60.0 / RATE_WINDOW_S + last_cycle_age_s = None if not cycle_events else round(now - cycle_events[-1], 3) + + if last_auto_signal is not None and auto_signal != last_auto_signal: + event_name = "machine_started" if auto_signal else "machine_stopped" + + print( + "EVENT", + json.dumps( + { + "ts": utc_now(), + "tenant": TENANT, + "site": SITE, + "vertical": VERTICAL, + "asset": ASSET, + "machine_id": MACHINE_ID, + "source_node": SOURCE_NODE, + "event_type": "machine_state_changed", + "event_name": event_name, + "previous_auto_signal": last_auto_signal, + "new_auto_signal": auto_signal, + "machine_running": auto_signal, + "reason_pending": not auto_signal, + }, + ensure_ascii=False, + ), + ) + + last_auto_signal = auto_signal + + if now - last_energy_ts >= ENERGY_INTERVAL_S: + regs = read_holdings(client) + last_energy = decode_energy(regs) + last_energy_ts = now + + if now - last_print_ts >= PRINT_INTERVAL_S: + payload = { + "ts": utc_now(), + "asset": ASSET, + "tenant": TENANT, + "site": SITE, + "vertical": VERTICAL, + "node_type": NODE_TYPE, + "fw": FW, + "machine_id": MACHINE_ID, + "source_node": SOURCE_NODE, + "wise_host": WISE_IP, + "machine_auto_signal": auto_signal, + "machine_running": auto_signal, + "cycle_input_state": cycle_input_state, + "wise_cycle_counter_raw": wise_counter, + "cycle_delta_last": last_delta, + "cycle_pulse_count": cycle_pulse_count, + "cycle_rate_ppm": round(cycle_rate_ppm, 3), + "last_cycle_age_s": last_cycle_age_s, + "counter_valid": counter_valid, + "counter_reset_detected": counter_reset_detected, + "digital_inputs": { + "DI0": auto_signal, + "DI1": cycle_input_state, + }, + **last_energy, + "comm_ok": True, + "sample_valid": True, + } + + print("TELEMETRY", json.dumps(payload, ensure_ascii=False)) + last_print_ts = now + + except KeyboardInterrupt: + print("\nParado por usuario") + break + + except Exception as exc: + print( + "ERROR", + json.dumps( + { + "ts": utc_now(), + "asset": ASSET, + "machine_id": MACHINE_ID, + "source_node": SOURCE_NODE, + "wise_host": WISE_IP, + "comm_ok": False, + "sample_valid": False, + "error": str(exc), + }, + ensure_ascii=False, + ), + ) + + try: + client.close() + except Exception: + pass + + time.sleep(2) + + time.sleep(DI_POLL_INTERVAL_S) + + +if __name__ == "__main__": + main() diff --git a/ucepsa/edge-oee-demo/revpi/edge-oee/wise_reader_mqtt_loop.py b/ucepsa/edge-oee-demo/revpi/edge-oee/wise_reader_mqtt_loop.py new file mode 100755 index 0000000..ab1d809 --- /dev/null +++ b/ucepsa/edge-oee-demo/revpi/edge-oee/wise_reader_mqtt_loop.py @@ -0,0 +1,491 @@ +#!/usr/bin/env python3 +import json +import os +import signal +import struct +import time +import urllib.request +from collections import deque +from datetime import datetime, timezone +from pathlib import Path +from urllib.error import HTTPError, URLError + +import paho.mqtt.client as mqtt +from pymodbus.client import ModbusTcpClient + + +BASE_DIR = Path(__file__).resolve().parent +ENV_PATH = BASE_DIR / ".env" +NODES_PATH = BASE_DIR / "wise_nodes.json" + + +def load_env(path: Path): + if not path.exists(): + return + + for raw in path.read_text().splitlines(): + line = raw.strip() + if not line or line.startswith("#") or "=" not in line: + continue + + key, value = line.split("=", 1) + os.environ.setdefault(key.strip(), value.strip().strip('"').strip("'")) + + +def env(name, default=None, required=False, cast=str): + value = os.environ.get(name, default) + + if required and (value is None or str(value).strip() == ""): + raise RuntimeError(f"Falta variable obligatoria: {name}") + + if value is None: + return None + + return cast(value) + + +def utc_now(): + return datetime.now(timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z") + + +def dumps(data): + return json.dumps(data, ensure_ascii=False, separators=(",", ":")) + + +def f32_abcd(reg_hi, reg_lo): + raw = int(reg_hi).to_bytes(2, "big") + int(reg_lo).to_bytes(2, "big") + return struct.unpack(">f", raw)[0] + + +def counter_delta(previous, current): + if previous is None: + return 0, False + if current >= previous: + return current - previous, False + return 0, True + + +def load_nodes(): + data = json.loads(NODES_PATH.read_text()) + nodes = [n for n in data.get("nodes", []) if n.get("enabled", True)] + + if not nodes: + raise RuntimeError("No hay nodos WISE habilitados en wise_nodes.json") + + return nodes + + +def make_mqtt_client(): + host = env("MQTT_HOST", required=True) + port = env("MQTT_PORT", 1883, cast=int) + user = env("MQTT_USER", "") + password = env("MQTT_PASSWORD", "") + client_id = env("MQTT_CLIENT_ID", required=True) + + try: + client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id=client_id) + except AttributeError: + client = mqtt.Client(client_id=client_id) + + if user: + client.username_pw_set(user, password) + + client.connect(host, port, keepalive=60) + client.loop_start() + return client + + +def publish(client, topic, payload): + client.publish(topic, dumps(payload), qos=0, retain=False) + + +def read_wise_di_api(node): + wise_ip = node["wise_ip"] + session_env = node["session_env"] + session_id = os.environ.get(session_env, "").strip() + + if not session_id: + raise RuntimeError(f"Falta {session_env}. Exporta la cookie adamsessionid del WISE.") + + req = urllib.request.Request( + f"http://{wise_ip}/di_value/slot_0", + headers={ + "Accept": "text/plain, */*; q=0.01", + "X-Requested-With": "XMLHttpRequest", + "Referer": f"http://{wise_ip}/config/index.html", + "User-Agent": "Mozilla/5.0", + "Cookie": f"adamsessionid={session_id}", + }, + ) + + try: + with urllib.request.urlopen(req, timeout=2) as response: + text = response.read().decode("utf-8", errors="replace") + except HTTPError as e: + body = e.read().decode("utf-8", errors="replace") + raise RuntimeError(f"WISE HTTP {e.code}: {e.reason}. Body={body[:200]!r}") + except URLError as e: + raise RuntimeError(f"WISE HTTP unreachable: {e}") + + data = json.loads(text) + + if isinstance(data, dict) and data.get("Err"): + raise RuntimeError(f"WISE API Err={data.get('Err')} Msg={data.get('Msg')!r}") + + channels = {int(item["Ch"]): item for item in data.get("DIVal", [])} + + ch0 = channels.get(0, {}) + ch1 = channels.get(1, {}) + + return { + "di0_stat": bool(ch0.get("Stat", 0)), + "di1_stat": bool(ch1.get("Stat", 0)), + "di1_counter_raw": int(ch1.get("CtFq", 0)), + "di1_counting": bool(ch1.get("Cnting", 0)), + "di1_mode": int(ch1.get("Md", -1)), + } + + + +def u32_lo_hi(lo, hi): + return (int(lo) & 0xFFFF) + ((int(hi) & 0xFFFF) << 16) + + +def read_wise_inputs_modbus(state, node): + client = ensure_modbus_client(state, node) + unit = int(node.get("unit", 1)) + + rr_di = client.read_discrete_inputs( + address=0, + count=2, + device_id=unit, + ) + + if rr_di.isError(): + raise RuntimeError(f"Error leyendo DI status: {rr_di}") + + rr_counter = client.read_holding_registers( + address=0, + count=4, + device_id=unit, + ) + + if rr_counter.isError(): + raise RuntimeError(f"Error leyendo contadores DI: {rr_counter}") + + regs = rr_counter.registers + + di0_counter = u32_lo_hi(regs[0], regs[1]) + di1_counter = u32_lo_hi(regs[2], regs[3]) + + return { + "di0_stat": bool(rr_di.bits[0]), + "di1_stat": bool(rr_di.bits[1]), + "di0_counter_raw": di0_counter, + "di1_counter_raw": di1_counter, + "di1_counting": True, + "di1_mode": 1, + } + +def ensure_modbus_client(state, node): + client = state.get("modbus_client") + + if client is None: + client = ModbusTcpClient( + node["wise_ip"], + port=int(node.get("wise_port", 502)), + timeout=3, + ) + state["modbus_client"] = client + + if not client.connected: + if not client.connect(): + raise RuntimeError("No conecta con WISE por Modbus/TCP") + + return client + + +def read_holdings(state, node): + client = ensure_modbus_client(state, node) + + rr = client.read_holding_registers( + address=int(node.get("holding_base", 1000)), + count=int(node.get("holding_count", 20)), + device_id=int(node.get("unit", 1)), + ) + + if rr.isError(): + raise RuntimeError(f"Error leyendo holding registers: {rr}") + + return rr.registers + + +def decode_energy(regs): + if len(regs) < 34: + raise ValueError(f"Se esperaban 34 registros SDM630, recibidos {len(regs)}") + + def v(offset): + return f32_abcd(regs[offset], regs[offset + 1]) + + voltage_l1_v = v(0) + voltage_l2_v = v(2) + voltage_l3_v = v(4) + + current_l1_a = v(6) + current_l2_a = v(8) + current_l3_a = v(10) + + active_power_l1_w = v(12) + active_power_l2_w = v(14) + active_power_l3_w = v(16) + + active_power_w = v(18) + apparent_power_va = v(20) + reactive_power_var = v(22) + power_factor = v(24) + + frequency_hz = v(26) + import_kwh = v(28) + export_kwh = v(30) + total_active_energy_kwh = v(32) + + return { + "energy_meter_model": "eastron_sdm630", + "energy_map": "sdm630_3phase_v1", + + "voltage_l1_v": round(voltage_l1_v, 3), + "voltage_l2_v": round(voltage_l2_v, 3), + "voltage_l3_v": round(voltage_l3_v, 3), + + "current_l1_a": round(current_l1_a, 6), + "current_l2_a": round(current_l2_a, 6), + "current_l3_a": round(current_l3_a, 6), + + "active_power_l1_w": round(active_power_l1_w, 3), + "active_power_l2_w": round(active_power_l2_w, 3), + "active_power_l3_w": round(active_power_l3_w, 3), + + "active_power_w": round(active_power_w, 3), + "power_total_kw": round(active_power_w / 1000.0, 6), + + "apparent_power_va": round(apparent_power_va, 3), + "reactive_power_var": round(reactive_power_var, 3), + "power_factor": round(power_factor, 6), + + "frequency_hz": round(frequency_hz, 3), + "import_kwh": round(import_kwh, 6), + "export_kwh": round(export_kwh, 6), + "total_active_energy_kwh": round(total_active_energy_kwh, 6), + } + + +def base_payload(node): + return { + "tenant": env("TENANT", "ucepsa"), + "site": env("SITE", "ucepsa_test"), + "vertical": env("VERTICAL", "edge_oee"), + "asset": env("ASSET", "revpi_oee_node_01"), + "machine_id": node["machine_id"], + "source_node": node["source_node"], + "wise_host": node["wise_ip"], + "node_type": "revpi_wise_oee_gateway", + "fw": "0.3.0", + } + + +def init_state(): + return { + "previous_counter": None, + "cycle_pulse_count": 0, + "cycle_events": deque(), + "last_auto_signal": None, + "last_energy": {}, + "last_energy_ts": 0.0, + "last_publish_ts": 0.0, + "last_delta": 0, + "counter_reset_detected": False, + "modbus_client": None, + } + + +def main(): + load_env(ENV_PATH) + + nodes = load_nodes() + states = {node["source_node"]: init_state() for node in nodes} + + telemetry_topic = env("MQTT_TOPIC_TELEMETRY", required=True) + status_topic = env("MQTT_TOPIC_STATUS", required=True) + event_topic = env("MQTT_TOPIC_EVENT", required=True) + + publish_interval_s = env("PUBLISH_INTERVAL_SECONDS", 10, cast=float) + di_poll_interval_s = env("WISE_DI_POLL_INTERVAL_SECONDS", 0.5, cast=float) + energy_interval_s = env("WISE_ENERGY_INTERVAL_SECONDS", 2.0, cast=float) + rate_window_s = env("CYCLE_RATE_WINDOW_SECONDS", 60, cast=float) + + mqtt_client = make_mqtt_client() + + stop = {"value": False} + + def on_stop(signum, frame): + stop["value"] = True + + signal.signal(signal.SIGINT, on_stop) + signal.signal(signal.SIGTERM, on_stop) + + print(f"Nodos WISE habilitados: {len(nodes)}") + for node in nodes: + print(f"- {node['machine_id']} {node['source_node']} {node['wise_ip']}") + + publish(mqtt_client, status_topic, { + "ts": utc_now(), + "status": "started", + "asset": env("ASSET", "revpi_oee_node_01"), + "enabled_nodes": [ + { + "machine_id": n["machine_id"], + "source_node": n["source_node"], + "wise_ip": n["wise_ip"], + } + for n in nodes + ], + }) + + while not stop["value"]: + loop_start = time.time() + + for node in nodes: + state = states[node["source_node"]] + now = time.time() + + try: + di = read_wise_inputs_modbus(state, node) + + auto_signal = di["di0_stat"] + cycle_input_state = di["di1_stat"] + wise_counter = di["di1_counter_raw"] + counter_valid = di["di1_mode"] == 1 and di["di1_counting"] + + delta, reset = counter_delta(state["previous_counter"], wise_counter) + state["counter_reset_detected"] = reset + + if delta > 0: + state["cycle_pulse_count"] += delta + + for _ in range(min(delta, 1000)): + state["cycle_events"].append(now) + + event = { + "ts": utc_now(), + **base_payload(node), + "event_type": "cycle_count_changed", + "delta": delta, + "wise_cycle_counter_raw": wise_counter, + "cycle_pulse_count": state["cycle_pulse_count"], + } + publish(mqtt_client, event_topic, event) + print("CYCLES", dumps(event)) + + state["previous_counter"] = wise_counter + state["last_delta"] = delta + + while state["cycle_events"] and (now - state["cycle_events"][0]) > rate_window_s: + state["cycle_events"].popleft() + + cycle_rate_ppm = len(state["cycle_events"]) * 60.0 / rate_window_s + last_cycle_age_s = None + if state["cycle_events"]: + last_cycle_age_s = round(now - state["cycle_events"][-1], 3) + + if state["last_auto_signal"] is not None and auto_signal != state["last_auto_signal"]: + event_name = "machine_started" if auto_signal else "machine_stopped" + event = { + "ts": utc_now(), + **base_payload(node), + "event_type": "machine_state_changed", + "event_name": event_name, + "previous_auto_signal": state["last_auto_signal"], + "new_auto_signal": auto_signal, + "machine_running": auto_signal, + "reason_pending": not auto_signal, + } + publish(mqtt_client, event_topic, event) + print("EVENT", dumps(event)) + + state["last_auto_signal"] = auto_signal + + if now - state["last_energy_ts"] >= energy_interval_s: + regs = read_holdings(state, node) + state["last_energy"] = decode_energy(regs) + state["last_energy_ts"] = now + + if now - state["last_publish_ts"] >= publish_interval_s: + payload = { + "ts": utc_now(), + **base_payload(node), + "machine_auto_signal": auto_signal, + "machine_running": auto_signal, + "cycle_input_state": cycle_input_state, + "wise_cycle_counter_raw": wise_counter, + "cycle_delta_last": state["last_delta"], + "cycle_pulse_count": state["cycle_pulse_count"], + "cycle_rate_ppm": round(cycle_rate_ppm, 3), + "last_cycle_age_s": last_cycle_age_s, + "counter_valid": counter_valid, + "counter_reset_detected": state["counter_reset_detected"], + "digital_inputs": { + "DI0": auto_signal, + "DI1": cycle_input_state, + }, + **state["last_energy"], + "comm_ok": True, + "sample_valid": True, + } + + publish(mqtt_client, telemetry_topic, payload) + print("TELEMETRY", dumps(payload)) + state["last_publish_ts"] = now + + except Exception as exc: + error_payload = { + "ts": utc_now(), + **base_payload(node), + "comm_ok": False, + "sample_valid": False, + "error": str(exc), + } + publish(mqtt_client, status_topic, error_payload) + print("ERROR", dumps(error_payload)) + + client = state.get("modbus_client") + if client is not None: + try: + client.close() + except Exception: + pass + state["modbus_client"] = None + + elapsed = time.time() - loop_start + time.sleep(max(0.05, di_poll_interval_s - elapsed)) + + publish(mqtt_client, status_topic, { + "ts": utc_now(), + "status": "stopped", + "asset": env("ASSET", "revpi_oee_node_01"), + }) + + for state in states.values(): + client = state.get("modbus_client") + if client is not None: + try: + client.close() + except Exception: + pass + + mqtt_client.loop_stop() + mqtt_client.disconnect() + print("Parado correctamente") + + +if __name__ == "__main__": + main() diff --git a/ucepsa/edge-oee-demo/revpi/systemd/mesavault-beacon-controller.service b/ucepsa/edge-oee-demo/revpi/systemd/mesavault-beacon-controller.service new file mode 100644 index 0000000..ca2786e --- /dev/null +++ b/ucepsa/edge-oee-demo/revpi/systemd/mesavault-beacon-controller.service @@ -0,0 +1,2 @@ +[sudo] password for pi: +^C^C^C^C^Csudo: a password is required diff --git a/ucepsa/edge-oee-demo/revpi/systemd/mesavault-edge-oee-revpi.service b/ucepsa/edge-oee-demo/revpi/systemd/mesavault-edge-oee-revpi.service new file mode 100644 index 0000000..6e0f353 --- /dev/null +++ b/ucepsa/edge-oee-demo/revpi/systemd/mesavault-edge-oee-revpi.service @@ -0,0 +1,2 @@ +[sudo] password for pi: +^C^C^C^C^C^Csudo: a password is required diff --git a/ucepsa/edge-oee-demo/services/edge-oee-mqtt-pg-sink/main.py.bak.2026-05-08_09-18-47 b/ucepsa/edge-oee-demo/services/edge-oee-mqtt-pg-sink/main.py.bak.2026-05-08_09-18-47 deleted file mode 100644 index 8c826a9..0000000 --- a/ucepsa/edge-oee-demo/services/edge-oee-mqtt-pg-sink/main.py.bak.2026-05-08_09-18-47 +++ /dev/null @@ -1,278 +0,0 @@ -#!/usr/bin/env python3 -import json -import os -import signal -import sys -from datetime import datetime, timezone - -import paho.mqtt.client as mqtt -import psycopg2 -from psycopg2.extras import Json - - -RUNNING = True - - -def stop_handler(signum, frame): - global RUNNING - RUNNING = False - - -signal.signal(signal.SIGTERM, stop_handler) -signal.signal(signal.SIGINT, stop_handler) - - -def getenv(name, default=None, required=False): - value = os.getenv(name, default) - if required and not value: - print(f"[ERROR] Missing required env var: {name}", file=sys.stderr) - sys.exit(2) - return value - - -MQTT_HOST = getenv("MQTT_HOST", required=True) -MQTT_PORT = int(getenv("MQTT_PORT", "1883")) -MQTT_TOPIC = getenv("MQTT_TOPIC", required=True) - -PGHOST = getenv("PGHOST", required=True) -PGPORT = int(getenv("PGPORT", "5432")) -PGDATABASE = getenv("PGDATABASE", required=True) -PGUSER = getenv("PGUSER", required=True) -PGPASSWORD = getenv("PGPASSWORD", required=True) - -DEFAULT_TENANT = getenv("DEFAULT_TENANT", "ucepsa") -DEFAULT_SITE = getenv("DEFAULT_SITE", "demo_edge_oee") -DEFAULT_ASSET = getenv("DEFAULT_ASSET", "revpi_oee_node_01") - - -def num(payload, key): - value = payload.get(key) - if value is None or value == "": - return None - try: - return float(value) - except Exception: - return None - - -def bool_or_false(payload, key): - value = payload.get(key) - if isinstance(value, bool): - return value - if value is None: - return False - if isinstance(value, str): - return value.lower() in ("true", "1", "yes", "ok") - return bool(value) - - -def parse_ts(payload): - raw = payload.get("ts") - if not raw: - return datetime.now(timezone.utc) - - try: - return datetime.fromisoformat(raw.replace("Z", "+00:00")) - except Exception: - return datetime.now(timezone.utc) - - -def connect_pg(): - return psycopg2.connect( - host=PGHOST, - port=PGPORT, - dbname=PGDATABASE, - user=PGUSER, - password=PGPASSWORD, - connect_timeout=5, - ) - - -PG_CONN = connect_pg() -PG_CONN.autocommit = True - - -def ensure_pg(): - global PG_CONN - try: - with PG_CONN.cursor() as cur: - cur.execute("SELECT 1;") - except Exception: - try: - PG_CONN.close() - except Exception: - pass - PG_CONN = connect_pg() - PG_CONN.autocommit = True - - -def insert_payload(topic, payload): - ensure_pg() - - tenant = payload.get("tenant") or DEFAULT_TENANT - site = payload.get("site") or DEFAULT_SITE - asset = payload.get("asset") or DEFAULT_ASSET - - machine_id = payload.get("machine_id") or None - order_id = payload.get("order_id") or None - - comm_ok = bool_or_false(payload, "comm_ok") - error_text = payload.get("error_text") - - sql = """ - INSERT INTO mv_hot.edge_oee_eastron_readings ( - ts, - tenant, - site, - asset, - source, - meter_model, - meter_id, - machine_id, - order_id, - voltage_l1_v, - voltage_l2_v, - voltage_l3_v, - current_l1_a, - current_l2_a, - current_l3_a, - power_l1_kw, - power_l2_kw, - power_l3_kw, - power_total_kw, - frequency_hz, - import_kwh, - export_kwh, - comm_ok, - error_text, - payload_json - ) - VALUES ( - %(ts)s, - %(tenant)s, - %(site)s, - %(asset)s, - %(source)s, - %(meter_model)s, - %(meter_id)s, - %(machine_id)s, - %(order_id)s, - %(voltage_l1_v)s, - %(voltage_l2_v)s, - %(voltage_l3_v)s, - %(current_l1_a)s, - %(current_l2_a)s, - %(current_l3_a)s, - %(power_l1_kw)s, - %(power_l2_kw)s, - %(power_l3_kw)s, - %(power_total_kw)s, - %(frequency_hz)s, - %(import_kwh)s, - %(export_kwh)s, - %(comm_ok)s, - %(error_text)s, - %(payload_json)s - ); - """ - - power_total_kw = num(payload, "power_total_kw") - - params = { - "ts": parse_ts(payload), - "tenant": tenant, - "site": site, - "asset": asset, - "source": payload.get("source") or "mqtt_edge_oee_sink", - "meter_model": payload.get("meter_model"), - "meter_id": payload.get("meter_id"), - "machine_id": machine_id, - "order_id": order_id, - - "voltage_l1_v": num(payload, "voltage_l1_v"), - "voltage_l2_v": num(payload, "voltage_l2_v"), - "voltage_l3_v": num(payload, "voltage_l3_v"), - - "current_l1_a": num(payload, "current_l1_a"), - "current_l2_a": num(payload, "current_l2_a"), - "current_l3_a": num(payload, "current_l3_a"), - - "power_l1_kw": num(payload, "power_l1_kw") if payload.get("power_l1_kw") is not None else power_total_kw, - "power_l2_kw": num(payload, "power_l2_kw"), - "power_l3_kw": num(payload, "power_l3_kw"), - "power_total_kw": power_total_kw, - - "frequency_hz": num(payload, "frequency_hz"), - "import_kwh": num(payload, "import_kwh"), - "export_kwh": num(payload, "export_kwh"), - - "comm_ok": comm_ok, - "error_text": error_text, - "payload_json": Json({ - **payload, - "_mqtt_topic": topic, - "_ingested_by": "edge_oee_mqtt_pg_sink", - }), - } - - with PG_CONN.cursor() as cur: - cur.execute(sql, params) - - -def on_connect(client, userdata, flags, reason_code, properties=None): - print(f"[MQTT] connected reason_code={reason_code}") - client.subscribe(MQTT_TOPIC) - print(f"[MQTT] subscribed topic={MQTT_TOPIC}") - - -def on_message(client, userdata, msg): - try: - body = msg.payload.decode("utf-8") - payload = json.loads(body) - - insert_payload(msg.topic, payload) - - asset = payload.get("asset") - voltage = payload.get("voltage_l1_v") - power = payload.get("power_total_kw") - comm_ok = payload.get("comm_ok") - - print(f"[OK] topic={msg.topic} asset={asset} voltage={voltage} power_kw={power} comm_ok={comm_ok}") - - except Exception as exc: - print(f"[ERROR] processing topic={msg.topic}: {type(exc).__name__}: {exc}", file=sys.stderr) - - -def main(): - print("[START] edge_oee_mqtt_pg_sink") - print(f"[CONFIG] MQTT {MQTT_HOST}:{MQTT_PORT} topic={MQTT_TOPIC}") - print(f"[CONFIG] PostgreSQL {PGHOST}:{PGPORT}/{PGDATABASE} user={PGUSER}") - - client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2) - client.on_connect = on_connect - client.on_message = on_message - - client.connect(MQTT_HOST, MQTT_PORT, keepalive=60) - client.loop_start() - - try: - while RUNNING: - signal.pause() - except AttributeError: - import time - while RUNNING: - time.sleep(1) - finally: - client.loop_stop() - client.disconnect() - try: - PG_CONN.close() - except Exception: - pass - print("[STOP] edge_oee_mqtt_pg_sink") - - return 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/ucepsa/edge-oee-demo/services/edge-oee-mqtt-pg-sink/main.py.bak.2026-05-08_09-32-17 b/ucepsa/edge-oee-demo/services/edge-oee-mqtt-pg-sink/main.py.bak.2026-05-08_09-32-17 deleted file mode 100644 index d885a09..0000000 --- a/ucepsa/edge-oee-demo/services/edge-oee-mqtt-pg-sink/main.py.bak.2026-05-08_09-32-17 +++ /dev/null @@ -1,410 +0,0 @@ -#!/usr/bin/env python3 -import json -import os -import signal -import sys -from datetime import datetime, timezone - -import paho.mqtt.client as mqtt -import psycopg2 -from psycopg2.extras import Json - - -RUNNING = True - - -def stop_handler(signum, frame): - global RUNNING - RUNNING = False - - -signal.signal(signal.SIGTERM, stop_handler) -signal.signal(signal.SIGINT, stop_handler) - - -def getenv(name, default=None, required=False): - value = os.getenv(name, default) - if required and not value: - print(f"[ERROR] Missing required env var: {name}", file=sys.stderr) - sys.exit(2) - return value - - -MQTT_HOST = getenv("MQTT_HOST", required=True) -MQTT_PORT = int(getenv("MQTT_PORT", "1883")) -MQTT_TOPIC = getenv("MQTT_TOPIC", required=True) - -PGHOST = getenv("PGHOST", required=True) -PGPORT = int(getenv("PGPORT", "5432")) -PGDATABASE = getenv("PGDATABASE", required=True) -PGUSER = getenv("PGUSER", required=True) -PGPASSWORD = getenv("PGPASSWORD", required=True) - -DEFAULT_TENANT = getenv("DEFAULT_TENANT", "ucepsa") -DEFAULT_SITE = getenv("DEFAULT_SITE", "demo_edge_oee") -DEFAULT_ASSET = getenv("DEFAULT_ASSET", "revpi_oee_node_01") - - -def num(payload, key): - value = payload.get(key) - if value is None or value == "": - return None - try: - return float(value) - except Exception: - return None - - -def bool_or_false(payload, key): - value = payload.get(key) - if isinstance(value, bool): - return value - if value is None: - return False - if isinstance(value, str): - return value.lower() in ("true", "1", "yes", "ok") - return bool(value) - - -def parse_ts(payload): - raw = payload.get("ts") - if not raw: - return datetime.now(timezone.utc) - - try: - return datetime.fromisoformat(raw.replace("Z", "+00:00")) - except Exception: - return datetime.now(timezone.utc) - - -def connect_pg(): - return psycopg2.connect( - host=PGHOST, - port=PGPORT, - dbname=PGDATABASE, - user=PGUSER, - password=PGPASSWORD, - connect_timeout=5, - ) - - -PG_CONN = connect_pg() -PG_CONN.autocommit = True - - -def ensure_pg(): - global PG_CONN - try: - with PG_CONN.cursor() as cur: - cur.execute("SELECT 1;") - except Exception: - try: - PG_CONN.close() - except Exception: - pass - PG_CONN = connect_pg() - PG_CONN.autocommit = True - - -def insert_payload(topic, payload): - ensure_pg() - - tenant = payload.get("tenant") or DEFAULT_TENANT - site = payload.get("site") or DEFAULT_SITE - asset = payload.get("asset") or DEFAULT_ASSET - - machine_id = payload.get("machine_id") or None - order_id = payload.get("order_id") or None - - comm_ok = bool_or_false(payload, "comm_ok") - error_text = payload.get("error_text") - - sql = """ - INSERT INTO mv_hot.edge_oee_eastron_readings ( - ts, - tenant, - site, - asset, - source, - meter_model, - meter_id, - machine_id, - order_id, - voltage_l1_v, - voltage_l2_v, - voltage_l3_v, - current_l1_a, - current_l2_a, - current_l3_a, - power_l1_kw, - power_l2_kw, - power_l3_kw, - power_total_kw, - frequency_hz, - import_kwh, - export_kwh, - comm_ok, - error_text, - payload_json - ) - VALUES ( - %(ts)s, - %(tenant)s, - %(site)s, - %(asset)s, - %(source)s, - %(meter_model)s, - %(meter_id)s, - %(machine_id)s, - %(order_id)s, - %(voltage_l1_v)s, - %(voltage_l2_v)s, - %(voltage_l3_v)s, - %(current_l1_a)s, - %(current_l2_a)s, - %(current_l3_a)s, - %(power_l1_kw)s, - %(power_l2_kw)s, - %(power_l3_kw)s, - %(power_total_kw)s, - %(frequency_hz)s, - %(import_kwh)s, - %(export_kwh)s, - %(comm_ok)s, - %(error_text)s, - %(payload_json)s - ); - """ - - power_total_kw = num(payload, "power_total_kw") - - params = { - "ts": parse_ts(payload), - "tenant": tenant, - "site": site, - "asset": asset, - "source": payload.get("source") or "mqtt_edge_oee_sink", - "meter_model": payload.get("meter_model"), - "meter_id": payload.get("meter_id"), - "machine_id": machine_id, - "order_id": order_id, - - "voltage_l1_v": num(payload, "voltage_l1_v"), - "voltage_l2_v": num(payload, "voltage_l2_v"), - "voltage_l3_v": num(payload, "voltage_l3_v"), - - "current_l1_a": num(payload, "current_l1_a"), - "current_l2_a": num(payload, "current_l2_a"), - "current_l3_a": num(payload, "current_l3_a"), - - "power_l1_kw": num(payload, "power_l1_kw") if payload.get("power_l1_kw") is not None else power_total_kw, - "power_l2_kw": num(payload, "power_l2_kw"), - "power_l3_kw": num(payload, "power_l3_kw"), - "power_total_kw": power_total_kw, - - "frequency_hz": num(payload, "frequency_hz"), - "import_kwh": num(payload, "import_kwh"), - "export_kwh": num(payload, "export_kwh"), - - "comm_ok": comm_ok, - "error_text": error_text, - "payload_json": Json({ - **payload, - "_mqtt_topic": topic, - "_ingested_by": "edge_oee_mqtt_pg_sink", - }), - } - - with PG_CONN.cursor() as cur: - cur.execute(sql, params) - -def insert_event(topic, payload): - ensure_pg() - - tenant = payload.get("tenant") or DEFAULT_TENANT - site = payload.get("site") or DEFAULT_SITE - vertical = payload.get("vertical") or "edge_oee" - asset = payload.get("asset") or DEFAULT_ASSET - - raw_ts = payload.get("event_ts") or payload.get("ts") - - try: - event_ts = datetime.fromisoformat(raw_ts.replace("Z", "+00:00")) if raw_ts else datetime.now(timezone.utc) - except Exception: - event_ts = datetime.now(timezone.utc) - - def to_bool(value): - if isinstance(value, bool): - return value - if value is None: - return None - if isinstance(value, str): - return value.lower() in ("true", "1", "yes", "ok") - return bool(value) - - def to_int(value): - if value is None or value == "": - return None - try: - return int(value) - except Exception: - return None - - def to_float(value): - if value is None or value == "": - return None - try: - return float(value) - except Exception: - return None - - sql = """ - INSERT INTO mv_hot.edge_oee_node_events ( - event_ts, - topic, - tenant, - site, - vertical, - asset, - machine_id, - order_id, - event_type, - event_name, - previous_auto_signal, - new_auto_signal, - machine_running, - reason_pending, - cycle_pulse_count, - cycle_rate_ppm, - last_cycle_age_s, - payload_json - ) - VALUES ( - %(event_ts)s, - %(topic)s, - %(tenant)s, - %(site)s, - %(vertical)s, - %(asset)s, - %(machine_id)s, - %(order_id)s, - %(event_type)s, - %(event_name)s, - %(previous_auto_signal)s, - %(new_auto_signal)s, - %(machine_running)s, - %(reason_pending)s, - %(cycle_pulse_count)s, - %(cycle_rate_ppm)s, - %(last_cycle_age_s)s, - %(payload_json)s - ); - """ - - params = { - "event_ts": event_ts, - "topic": topic, - "tenant": tenant, - "site": site, - "vertical": vertical, - "asset": asset, - "machine_id": payload.get("machine_id") or None, - "order_id": payload.get("order_id") or None, - "event_type": payload.get("event_type") or "unknown", - "event_name": payload.get("event_name") or "unknown", - "previous_auto_signal": to_bool(payload.get("previous_auto_signal")), - "new_auto_signal": to_bool(payload.get("new_auto_signal")), - "machine_running": to_bool(payload.get("machine_running")), - "reason_pending": to_bool(payload.get("reason_pending")), - "cycle_pulse_count": to_int(payload.get("cycle_pulse_count")), - "cycle_rate_ppm": to_float(payload.get("cycle_rate_ppm")), - "last_cycle_age_s": to_float(payload.get("last_cycle_age_s")), - "payload_json": Json({ - **payload, - "_mqtt_topic": topic, - "_ingested_by": "edge_oee_mqtt_pg_sink", - }), - } - - with PG_CONN.cursor() as cur: - cur.execute(sql, params) - -def on_connect(client, userdata, flags, reason_code, properties=None): - print(f"[MQTT] connected reason_code={reason_code}") - client.subscribe(MQTT_TOPIC) - print(f"[MQTT] subscribed topic={MQTT_TOPIC}") - - -def on_message(client, userdata, msg): - try: - body = msg.payload.decode("utf-8") - payload = json.loads(body) - - if msg.topic.endswith("/telemetry"): - insert_payload(msg.topic, payload) - - asset = payload.get("asset") - voltage = payload.get("voltage_l1_v") - power = payload.get("power_total_kw") - comm_ok = payload.get("comm_ok") - pulses = payload.get("cycle_pulse_count") - rate = payload.get("cycle_rate_ppm") - - print( - f"[OK][telemetry] topic={msg.topic} asset={asset} " - f"voltage={voltage} power_kw={power} pulses={pulses} rate_ppm={rate} comm_ok={comm_ok}", - flush=True, - ) - - elif msg.topic.endswith("/event"): - insert_event(msg.topic, payload) - - print( - f"[OK][event] topic={msg.topic} " - f"event_name={payload.get('event_name')} " - f"asset={payload.get('asset')} " - f"previous={payload.get('previous_auto_signal')} " - f"new={payload.get('new_auto_signal')} " - f"pulses={payload.get('cycle_pulse_count')}", - flush=True, - ) - - else: - print(f"[WARN] Ignored topic={msg.topic}", flush=True) - - except Exception as exc: - print(f"[ERROR] processing topic={msg.topic}: {type(exc).__name__}: {exc}", file=sys.stderr, flush=True) - -def main(): - print("[START] edge_oee_mqtt_pg_sink") - print(f"[CONFIG] MQTT {MQTT_HOST}:{MQTT_PORT} topic={MQTT_TOPIC}") - print(f"[CONFIG] PostgreSQL {PGHOST}:{PGPORT}/{PGDATABASE} user={PGUSER}") - - client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2) - client.on_connect = on_connect - client.on_message = on_message - - client.connect(MQTT_HOST, MQTT_PORT, keepalive=60) - client.loop_start() - - try: - while RUNNING: - signal.pause() - except AttributeError: - import time - while RUNNING: - time.sleep(1) - finally: - client.loop_stop() - client.disconnect() - try: - PG_CONN.close() - except Exception: - pass - print("[STOP] edge_oee_mqtt_pg_sink") - - return 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/ucepsa/edge-oee-demo/sql/065_fix_odoo_start_delay_scope.sql b/ucepsa/edge-oee-demo/sql/065_fix_odoo_start_delay_scope.sql new file mode 100644 index 0000000..c5f688a --- /dev/null +++ b/ucepsa/edge-oee-demo/sql/065_fix_odoo_start_delay_scope.sql @@ -0,0 +1,121 @@ +DROP VIEW IF EXISTS mv_reports_ucepsa_demo.odoo_discrepancies_demo; +DROP VIEW IF EXISTS mv_reports_ucepsa_demo.odoo_vs_machine_demo; + +CREATE VIEW mv_reports_ucepsa_demo.odoo_vs_machine_demo AS +WITH odoo AS ( + SELECT * + FROM mv_reports_ucepsa_demo.odoo_current_order_demo +), +machine AS ( + SELECT * + FROM mv_reports_ucepsa_demo.latest_machine_state_fast + WHERE machine_id = 'CORT-01' + ORDER BY ingest_ts DESC + LIMIT 1 +), +stops AS ( + SELECT + o.odoo_workorder_id, + COUNT(s.*)::int AS stop_count, + COALESCE(ROUND(SUM(s.duration_min)::numeric, 2), 0) AS stop_min, + COALESCE(ROUND(SUM(s.cost_eur)::numeric, 2), 0) AS stop_cost_eur + FROM odoo o + LEFT JOIN mv_reports_ucepsa_demo.stops_recent s + ON s.machine_id = o.machine_id + AND s.started_at >= COALESCE(o.odoo_reference_start, o.odoo_start, o.snapshot_at) + AND s.started_at <= COALESCE(o.odoo_end, now()) + GROUP BY o.odoo_workorder_id +) +SELECT + o.odoo_workorder_id, + o.odoo_production_id, + o.odoo_order_ref, + o.odoo_state, + o.odoo_is_started, + o.product, + o.machine_id, + o.asset, + o.odoo_start, + o.odoo_reference_start, + o.odoo_end, + COALESCE(m.machine_running, false) AS machine_running, + m.age_s AS telemetry_age_s, + COALESCE(m.cycle_pulse_count, 0) AS cycle_pulse_count, + COALESCE(m.cycle_rate_ppm, 0) AS cycle_rate_ppm, + ma.first_machine_activity, + CASE + WHEN o.odoo_is_started = true + AND ma.first_machine_activity IS NOT NULL + AND COALESCE(o.odoo_reference_start, o.odoo_start) IS NOT NULL + THEN ROUND( + GREATEST( + EXTRACT(EPOCH FROM (ma.first_machine_activity - COALESCE(o.odoo_reference_start, o.odoo_start)))::numeric, + 0 + ) / 60.0, + 2 + ) + ELSE NULL::numeric + END AS start_delay_min, + COALESCE(s.stop_count, 0) AS stop_count, + COALESCE(s.stop_min, 0) AS stop_min, + COALESCE(s.stop_cost_eur, 0) AS stop_cost_eur, + COALESCE(m.cycle_pulse_count, 0)::numeric AS real_cycles, + COALESCE(o.planned_cycles, o.qty_planned, 0)::numeric AS planned_cycles, + CASE + WHEN COALESCE(o.planned_cycles, o.qty_planned, 0) > 0 + THEN ROUND( + ( + COALESCE(m.cycle_pulse_count, 0)::numeric + / COALESCE(o.planned_cycles, o.qty_planned, 0)::numeric + ) * 100, + 2 + ) + ELSE 0::numeric + END AS cycle_completion_pct, + CASE + WHEN m.age_s IS NULL OR m.age_s > 120 THEN 'SIN_TELEMETRIA' + WHEN o.odoo_state = 'ready' AND COALESCE(m.machine_running, false) = true THEN 'MAQUINA_EN_MARCHA_SIN_ORDEN_INICIADA' + WHEN o.odoo_state = 'ready' AND COALESCE(m.machine_running, false) = false THEN 'ORDEN_PREPARADA_MAQUINA_PARADA' + WHEN o.odoo_state IN ('progress', 'in_progress') AND COALESCE(m.machine_running, false) = false THEN 'ORDEN_EN_CURSO_MAQUINA_PARADA' + ELSE 'OK' + END AS status_check +FROM odoo o +LEFT JOIN machine m ON true +LEFT JOIN LATERAL ( + SELECT + MIN(r.ingest_ts) AS first_machine_activity + FROM mv_hot.edge_oee_eastron_readings r + WHERE r.machine_id = o.machine_id + AND (r.payload_json ->> 'machine_running')::boolean = true + AND r.ingest_ts >= COALESCE(o.odoo_reference_start, o.odoo_start, o.snapshot_at) + AND r.ingest_ts <= COALESCE(o.odoo_end, now()) +) ma ON true +LEFT JOIN stops s ON s.odoo_workorder_id = o.odoo_workorder_id; + +CREATE VIEW mv_reports_ucepsa_demo.odoo_discrepancies_demo AS +SELECT + now() AS checked_at, + odoo_order_ref, + product, + machine_id, + status_check, + CASE + WHEN status_check = 'MAQUINA_EN_MARCHA_SIN_ORDEN_INICIADA' + THEN 'La máquina está en marcha, pero la orden de Odoo está preparada o en espera, no iniciada.' + WHEN status_check = 'ORDEN_PREPARADA_MAQUINA_PARADA' + THEN 'La orden está preparada/en espera y la máquina está parada. No hay desviación productiva activa.' + WHEN status_check = 'ORDEN_EN_CURSO_MAQUINA_PARADA' + THEN 'La orden está en curso en Odoo, pero la máquina está parada.' + WHEN status_check = 'SIN_TELEMETRIA' + THEN 'No hay telemetría reciente de máquina.' + ELSE 'Sin discrepancia relevante.' + END AS descripcion, + start_delay_min, + stop_count, + stop_min, + stop_cost_eur, + cycle_completion_pct +FROM mv_reports_ucepsa_demo.odoo_vs_machine_demo; + +GRANT SELECT ON mv_reports_ucepsa_demo.odoo_vs_machine_demo TO grafana_ucepsa_ro; +GRANT SELECT ON mv_reports_ucepsa_demo.odoo_discrepancies_demo TO grafana_ucepsa_ro; diff --git a/ucepsa/edge-oee-demo/sql/080_energy_3phase_views.sql b/ucepsa/edge-oee-demo/sql/080_energy_3phase_views.sql new file mode 100644 index 0000000..4691b6d --- /dev/null +++ b/ucepsa/edge-oee-demo/sql/080_energy_3phase_views.sql @@ -0,0 +1,114 @@ +DROP VIEW IF EXISTS mv_reports_ucepsa_demo.machine_state_latest_multi; +DROP VIEW IF EXISTS mv_reports_ucepsa_demo.energy_3phase_latest; +DROP VIEW IF EXISTS mv_reports_ucepsa_demo.energy_3phase_timeseries; + +CREATE VIEW mv_reports_ucepsa_demo.energy_3phase_timeseries AS +SELECT + r.ingest_ts AS ts, + r.ingest_ts, + r.tenant, + r.site, + r.asset, + r.machine_id, + EXTRACT(EPOCH FROM (now() - r.ingest_ts))::double precision AS age_s, + r.comm_ok, + + CASE + WHEN lower(r.payload_json ->> 'sample_valid') IN ('true','t','1','yes') THEN true + WHEN lower(r.payload_json ->> 'sample_valid') IN ('false','f','0','no') THEN false + ELSE NULL + END AS sample_valid, + + r.payload_json ->> 'energy_meter_model' AS energy_meter_model, + r.payload_json ->> 'energy_map' AS energy_map, + + r.voltage_l1_v, + NULLIF(r.payload_json ->> 'voltage_l2_v','')::double precision AS voltage_l2_v, + NULLIF(r.payload_json ->> 'voltage_l3_v','')::double precision AS voltage_l3_v, + + r.current_l1_a, + NULLIF(r.payload_json ->> 'current_l2_a','')::double precision AS current_l2_a, + NULLIF(r.payload_json ->> 'current_l3_a','')::double precision AS current_l3_a, + + NULLIF(r.payload_json ->> 'active_power_l1_w','')::double precision AS active_power_l1_w, + NULLIF(r.payload_json ->> 'active_power_l2_w','')::double precision AS active_power_l2_w, + NULLIF(r.payload_json ->> 'active_power_l3_w','')::double precision AS active_power_l3_w, + + COALESCE( + NULLIF(r.payload_json ->> 'active_power_w','')::double precision, + r.power_total_kw * 1000.0 + ) AS active_power_w, + + r.power_total_kw, + + NULLIF(r.payload_json ->> 'apparent_power_va','')::double precision AS apparent_power_va, + NULLIF(r.payload_json ->> 'reactive_power_var','')::double precision AS reactive_power_var, + NULLIF(r.payload_json ->> 'power_factor','')::double precision AS power_factor, + + r.frequency_hz, + r.import_kwh, + NULLIF(r.payload_json ->> 'export_kwh','')::double precision AS export_kwh, + NULLIF(r.payload_json ->> 'total_active_energy_kwh','')::double precision AS total_active_energy_kwh, + + r.payload_json +FROM mv_hot.edge_oee_eastron_readings r; + +CREATE VIEW mv_reports_ucepsa_demo.energy_3phase_latest AS +SELECT DISTINCT ON (machine_id) + * +FROM mv_reports_ucepsa_demo.energy_3phase_timeseries +WHERE site = 'ucepsa_onpremise' +ORDER BY machine_id, ingest_ts DESC; + +CREATE VIEW mv_reports_ucepsa_demo.machine_state_latest_multi AS +SELECT + l.ingest_ts, + l.ts, + l.tenant, + l.site, + l.asset, + l.machine_id, + l.age_s, + l.comm_ok, + l.sample_valid, + l.energy_map, + + CASE + WHEN lower(l.payload_json ->> 'machine_running') IN ('true','t','1','yes') THEN true + WHEN lower(l.payload_json ->> 'machine_running') IN ('false','f','0','no') THEN false + ELSE false + END AS machine_running, + + COALESCE(NULLIF(l.payload_json ->> 'cycle_pulse_count','')::numeric::bigint, 0) AS cycle_pulse_count, + COALESCE(NULLIF(l.payload_json ->> 'cycle_rate_ppm','')::double precision, 0.0) AS cycle_rate_ppm, + NULLIF(l.payload_json ->> 'wise_cycle_counter_raw','')::numeric::bigint AS wise_cycle_counter_raw, + + CASE + WHEN lower(l.payload_json ->> 'cycle_input_state') IN ('true','t','1','yes') THEN true + WHEN lower(l.payload_json ->> 'cycle_input_state') IN ('false','f','0','no') THEN false + ELSE NULL + END AS cycle_input_state, + + l.payload_json -> 'digital_inputs' AS digital_inputs, + + l.voltage_l1_v, + l.voltage_l2_v, + l.voltage_l3_v, + + l.current_l1_a, + l.current_l2_a, + l.current_l3_a, + + l.active_power_l1_w, + l.active_power_l2_w, + l.active_power_l3_w, + + l.power_total_kw, + l.frequency_hz, + l.import_kwh, + l.payload_json +FROM mv_reports_ucepsa_demo.energy_3phase_latest l; + +GRANT SELECT ON mv_reports_ucepsa_demo.energy_3phase_timeseries TO grafana_ucepsa_ro; +GRANT SELECT ON mv_reports_ucepsa_demo.energy_3phase_latest TO grafana_ucepsa_ro; +GRANT SELECT ON mv_reports_ucepsa_demo.machine_state_latest_multi TO grafana_ucepsa_ro; diff --git a/ucepsa/edge-oee-demo/sql/082_machine_stops_grafana.sql b/ucepsa/edge-oee-demo/sql/082_machine_stops_grafana.sql new file mode 100644 index 0000000..4661c4a --- /dev/null +++ b/ucepsa/edge-oee-demo/sql/082_machine_stops_grafana.sql @@ -0,0 +1,81 @@ +CREATE OR REPLACE VIEW mv_reports_ucepsa_demo.machine_stops_grafana AS +SELECT + s.id, + s.machine_id, + + s.started_at, + s.ended_at, + COALESCE(s.ended_at, now()) AS effective_ended_at, + + s.status, + s.classification_status, + + s.start_event_name, + s.end_event_name, + + COALESCE( + s.duration_s, + EXTRACT(EPOCH FROM (COALESCE(s.ended_at, now()) - s.started_at)) + ) AS duration_s_current, + + ROUND( + ( + COALESCE( + s.duration_s, + EXTRACT(EPOCH FROM (COALESCE(s.ended_at, now()) - s.started_at)) + ) / 60.0 + )::numeric, + 2 + ) AS duration_min, + + COALESCE(s.cost_eur, 0)::numeric AS cost_eur, + + COALESCE( + to_jsonb(s) ->> 'cause_code', + to_jsonb(s) ->> 'stop_cause_code', + to_jsonb(s) ->> 'reason_code', + CASE + WHEN s.classification_status = 'PENDING' THEN 'PENDIENTE' + ELSE 'SIN_CAUSA' + END + ) AS cause_code, + + COALESCE( + to_jsonb(s) ->> 'cause_name', + to_jsonb(s) ->> 'stop_cause_name', + to_jsonb(s) ->> 'reason_name', + CASE + WHEN s.classification_status = 'PENDING' THEN 'Pendiente de clasificar' + ELSE 'Sin causa registrada' + END + ) AS cause_name, + + to_jsonb(s) ->> 'operator_note' AS operator_note, + to_jsonb(s) ->> 'classified_by' AS classified_by, + to_jsonb(s) ->> 'order_ref' AS order_ref, + to_jsonb(s) ->> 'production_order' AS production_order, + + CASE WHEN s.status = 'OPEN' THEN true ELSE false END AS is_open, + + CASE + WHEN s.classification_status = 'PENDING' THEN true + ELSE false + END AS is_pending_classification, + + CASE + WHEN s.status = 'OPEN' THEN 'ABIERTO' + WHEN s.classification_status = 'PENDING' THEN 'CERRADO SIN CLASIFICAR' + WHEN s.classification_status = 'CLASSIFIED' THEN 'CLASIFICADO' + ELSE COALESCE(s.status, 'SIN_ESTADO') + END AS stop_status_label, + + CASE + WHEN s.status = 'OPEN' THEN 3 + WHEN s.classification_status = 'PENDING' THEN 2 + WHEN s.classification_status = 'CLASSIFIED' THEN 1 + ELSE 0 + END AS stop_priority_score + +FROM mv_hot.edge_oee_machine_stops s; + +GRANT SELECT ON mv_reports_ucepsa_demo.machine_stops_grafana TO grafana_ucepsa_ro; diff --git a/ucepsa/edge-oee-demo/sql/083_machine_stops_grafana_cause_fix.sql b/ucepsa/edge-oee-demo/sql/083_machine_stops_grafana_cause_fix.sql new file mode 100644 index 0000000..e2037fa --- /dev/null +++ b/ucepsa/edge-oee-demo/sql/083_machine_stops_grafana_cause_fix.sql @@ -0,0 +1,93 @@ +CREATE OR REPLACE VIEW mv_reports_ucepsa_demo.machine_stops_grafana AS +SELECT + s.id, + s.machine_id, + + s.started_at, + s.ended_at, + COALESCE(s.ended_at, now()) AS effective_ended_at, + + s.status, + s.classification_status, + + s.start_event_name, + s.end_event_name, + + COALESCE( + s.duration_s, + EXTRACT(EPOCH FROM (COALESCE(s.ended_at, now()) - s.started_at)) + ) AS duration_s_current, + + ROUND( + ( + COALESCE( + s.duration_s, + EXTRACT(EPOCH FROM (COALESCE(s.ended_at, now()) - s.started_at)) + ) / 60.0 + )::numeric, + 2 + ) AS duration_min, + + COALESCE(s.cost_eur, 0)::numeric AS cost_eur, + + COALESCE( + to_jsonb(s) ->> 'cause_code', + to_jsonb(s) ->> 'stop_cause_code', + to_jsonb(s) ->> 'reason_code', + to_jsonb(c) ->> 'code', + to_jsonb(c) ->> 'cause_code', + CASE + WHEN s.classification_status = 'PENDING' THEN 'PENDIENTE' + ELSE 'SIN_CAUSA' + END + ) AS cause_code, + + COALESCE( + to_jsonb(s) ->> 'cause_name', + to_jsonb(s) ->> 'stop_cause_name', + to_jsonb(s) ->> 'reason_name', + to_jsonb(c) ->> 'name', + to_jsonb(c) ->> 'description', + to_jsonb(c) ->> 'label', + CASE + WHEN s.classification_status = 'PENDING' THEN 'Pendiente de clasificar' + ELSE 'Sin causa registrada' + END + ) AS cause_name, + + to_jsonb(s) ->> 'operator_note' AS operator_note, + to_jsonb(s) ->> 'classified_by' AS classified_by, + to_jsonb(s) ->> 'classified_at' AS classified_at, + to_jsonb(s) ->> 'order_ref' AS order_ref, + to_jsonb(s) ->> 'production_order' AS production_order, + + CASE WHEN s.status = 'OPEN' THEN true ELSE false END AS is_open, + + CASE + WHEN s.classification_status = 'PENDING' THEN true + ELSE false + END AS is_pending_classification, + + CASE + WHEN s.status = 'OPEN' THEN 'ABIERTO' + WHEN s.classification_status = 'PENDING' THEN 'CERRADO SIN CLASIFICAR' + WHEN s.classification_status = 'CLASSIFIED' THEN 'CLASIFICADO' + ELSE COALESCE(s.status, 'SIN_ESTADO') + END AS stop_status_label, + + CASE + WHEN s.status = 'OPEN' THEN 3 + WHEN s.classification_status = 'PENDING' THEN 2 + WHEN s.classification_status = 'CLASSIFIED' THEN 1 + ELSE 0 + END AS stop_priority_score + +FROM mv_hot.edge_oee_machine_stops s +LEFT JOIN mv_hot.edge_oee_stop_causes c + ON (to_jsonb(c) ->> 'id') = COALESCE( + to_jsonb(s) ->> 'cause_id', + to_jsonb(s) ->> 'stop_cause_id', + to_jsonb(s) ->> 'reason_id' + ); + +GRANT SELECT ON mv_reports_ucepsa_demo.machine_stops_grafana TO grafana_ucepsa_ro; diff --git a/ucepsa/edge-oee-demo/sql/084_machine_stops_grafana_cause_names_fix.sql b/ucepsa/edge-oee-demo/sql/084_machine_stops_grafana_cause_names_fix.sql new file mode 100644 index 0000000..dcf0827 --- /dev/null +++ b/ucepsa/edge-oee-demo/sql/084_machine_stops_grafana_cause_names_fix.sql @@ -0,0 +1,144 @@ +DROP VIEW IF EXISTS mv_reports_ucepsa_demo.machine_stops_grafana; + +CREATE VIEW mv_reports_ucepsa_demo.machine_stops_grafana AS +WITH base AS ( + SELECT + s.*, + to_jsonb(s) AS sj, + + COALESCE( + to_jsonb(s) ->> 'cause_code', + to_jsonb(s) ->> 'stop_cause_code', + to_jsonb(s) ->> 'reason_code' + ) AS raw_cause_code, + + COALESCE( + to_jsonb(s) ->> 'cause_id', + to_jsonb(s) ->> 'stop_cause_id', + to_jsonb(s) ->> 'reason_id' + ) AS raw_cause_id, + + COALESCE( + to_jsonb(s) ->> 'cause_name', + to_jsonb(s) ->> 'stop_cause_name', + to_jsonb(s) ->> 'reason_name' + ) AS raw_cause_name + FROM mv_hot.edge_oee_machine_stops s +), +base_with_cause AS ( + SELECT + b.*, + cause.cj AS cause_json + FROM base b + LEFT JOIN LATERAL ( + SELECT to_jsonb(sc) AS cj + FROM mv_hot.edge_oee_stop_causes sc + WHERE + ( + b.raw_cause_id IS NOT NULL + AND to_jsonb(sc) ->> 'id' = b.raw_cause_id + ) + OR + ( + b.raw_cause_code IS NOT NULL + AND ( + to_jsonb(sc) ->> 'code' = b.raw_cause_code + OR to_jsonb(sc) ->> 'cause_code' = b.raw_cause_code + OR to_jsonb(sc) ->> 'name' = b.raw_cause_code + ) + ) + LIMIT 1 + ) cause ON true +) +SELECT + b.id, + b.machine_id, + + b.started_at, + b.ended_at, + COALESCE(b.ended_at, now()) AS effective_ended_at, + + b.status, + b.classification_status, + + b.start_event_name, + b.end_event_name, + + COALESCE( + b.duration_s, + EXTRACT(EPOCH FROM (COALESCE(b.ended_at, now()) - b.started_at)) + ) AS duration_s_current, + + ROUND( + ( + COALESCE( + b.duration_s, + EXTRACT(EPOCH FROM (COALESCE(b.ended_at, now()) - b.started_at)) + ) / 60.0 + )::numeric, + 2 + ) AS duration_min, + + COALESCE(b.cost_eur, 0)::numeric AS cost_eur, + + COALESCE( + NULLIF(b.raw_cause_code, ''), + NULLIF(b.cause_json ->> 'code', ''), + NULLIF(b.cause_json ->> 'cause_code', ''), + CASE + WHEN b.classification_status = 'PENDING' THEN 'PENDIENTE' + ELSE 'SIN_CAUSA' + END + ) AS cause_code, + + COALESCE( + NULLIF(b.raw_cause_name, ''), + NULLIF(b.cause_json ->> 'name', ''), + NULLIF(b.cause_json ->> 'description', ''), + NULLIF(b.cause_json ->> 'label', ''), + + CASE COALESCE(NULLIF(b.raw_cause_code, ''), NULLIF(b.cause_json ->> 'code', '')) + WHEN 'cambio_bobina' THEN 'Cambio de bobina' + WHEN 'espera_instrucciones' THEN 'Espera de instrucciones' + WHEN 'fallo_empalme' THEN 'Fallo de empalme' + WHEN 'falta_material' THEN 'Falta de material' + WHEN 'ajuste_sellado' THEN 'Ajuste de sellado' + ELSE NULL + END, + + CASE + WHEN b.classification_status = 'PENDING' THEN 'Pendiente de clasificar' + ELSE 'Sin causa registrada' + END + ) AS cause_name, + + b.sj ->> 'operator_note' AS operator_note, + b.sj ->> 'classified_by' AS classified_by, + b.sj ->> 'classified_at' AS classified_at, + b.sj ->> 'order_ref' AS order_ref, + b.sj ->> 'production_order' AS production_order, + + CASE WHEN b.status = 'OPEN' THEN true ELSE false END AS is_open, + + CASE + WHEN b.classification_status = 'PENDING' THEN true + ELSE false + END AS is_pending_classification, + + CASE + WHEN b.status = 'OPEN' THEN 'ABIERTO' + WHEN b.classification_status = 'PENDING' THEN 'CERRADO SIN CLASIFICAR' + WHEN b.classification_status = 'CLASSIFIED' THEN 'CLASIFICADO' + ELSE COALESCE(b.status, 'SIN_ESTADO') + END AS stop_status_label, + + CASE + WHEN b.status = 'OPEN' THEN 3 + WHEN b.classification_status = 'PENDING' THEN 2 + WHEN b.classification_status = 'CLASSIFIED' THEN 1 + ELSE 0 + END AS stop_priority_score + +FROM base_with_cause b; + +GRANT SELECT ON mv_reports_ucepsa_demo.machine_stops_grafana TO grafana_ucepsa_ro; diff --git a/ucepsa/edge-oee-demo/sql/reports/070_order_execution_report_views.sql b/ucepsa/edge-oee-demo/sql/reports/070_order_execution_report_views.sql deleted file mode 100644 index 26f995a..0000000 --- a/ucepsa/edge-oee-demo/sql/reports/070_order_execution_report_views.sql +++ /dev/null @@ -1,885 +0,0 @@ --- MESAVAULT Edge-OEE UCEPSA --- Order execution report views --- Generated from validated PostgreSQL views. --- Contains no data and no secrets. - -DROP VIEW IF EXISTS mv_reports_ucepsa_demo.order_execution_report_full_v1; -DROP VIEW IF EXISTS mv_reports_ucepsa_demo.order_execution_data_quality_v1; -DROP VIEW IF EXISTS mv_reports_ucepsa_demo.order_execution_energy_timeseries_v1; -DROP VIEW IF EXISTS mv_reports_ucepsa_demo.order_execution_timeline_v1; -DROP VIEW IF EXISTS mv_reports_ucepsa_demo.order_execution_stop_causes_v1; -DROP VIEW IF EXISTS mv_reports_ucepsa_demo.order_execution_stops_v1; -DROP VIEW IF EXISTS mv_reports_ucepsa_demo.order_execution_report_v1; - --- --- PostgreSQL database dump --- - -\restrict YbA0VcRIrZ1kSDSxIf4zctoL9eu8TYoJ5cvkbSD5WkFTQ1jRyH26snWSZehz5VJ - --- Dumped from database version 16.14 --- Dumped by pg_dump version 16.14 - -SET statement_timeout = 0; -SET lock_timeout = 0; -SET idle_in_transaction_session_timeout = 0; -SET client_encoding = 'UTF8'; -SET standard_conforming_strings = on; -SELECT pg_catalog.set_config('search_path', '', false); -SET check_function_bodies = false; -SET xmloption = content; -SET client_min_messages = warning; -SET row_security = off; - --- --- Name: order_execution_report_v1; Type: VIEW; Schema: mv_reports_ucepsa_demo; Owner: - --- - -CREATE VIEW mv_reports_ucepsa_demo.order_execution_report_v1 AS - WITH orders AS ( - SELECT o_1.id, - o_1.tenant, - o_1.site, - o_1.odoo_instance, - o_1.odoo_workorder_id, - o_1.odoo_production_id, - o_1.odoo_order_ref, - o_1.odoo_state, - o_1.odoo_workcenter_code, - o_1.odoo_workcenter_name, - o_1.machine_id, - o_1.asset, - o_1.product, - o_1.qty_planned, - o_1.qty_done, - o_1.uom, - o_1.planned_cycles, - o_1.target_cycle_rate_ppm, - o_1.odoo_start, - o_1.odoo_end, - o_1.notes, - o_1.snapshot_at, - o_1.odoo_start AS start_ts, - CASE - WHEN (o_1.odoo_state = ANY (ARRAY['done'::text, 'to_close'::text, 'cancel'::text])) THEN COALESCE(o_1.odoo_end, o_1.snapshot_at) - ELSE COALESCE(( SELECT max(r.ingest_ts) AS max - FROM mv_hot.edge_oee_eastron_readings r - WHERE ((r.machine_id = o_1.machine_id) AND (r.ingest_ts >= o_1.odoo_start) AND (r.comm_ok = true) AND (r.meter_model = 'Eastron SDM120-M'::text))), o_1.snapshot_at) - END AS end_ts - FROM mv_edge_oee_ucepsa_demo.odoo_workorders_demo o_1 - WHERE ((o_1.odoo_instance = 'odoo_ucepsa_test'::text) AND (o_1.machine_id IS NOT NULL) AND (o_1.odoo_start IS NOT NULL)) - ), energy AS ( - SELECT o_1.id AS order_row_id, - count(r.id) AS energy_samples, - min(r.ingest_ts) AS energy_first_ts, - max(r.ingest_ts) AS energy_last_ts, - min(r.import_kwh) AS start_import_kwh, - max(r.import_kwh) AS end_import_kwh, - round((max(r.import_kwh) - min(r.import_kwh)), 3) AS kwh_consumed, - round(avg(r.power_total_kw), 3) AS avg_kw, - round(max(r.power_total_kw), 3) AS max_kw - FROM (orders o_1 - LEFT JOIN mv_hot.edge_oee_eastron_readings r ON (((r.machine_id = o_1.machine_id) AND (r.ingest_ts >= o_1.start_ts) AND (r.ingest_ts <= o_1.end_ts) AND (r.comm_ok = true) AND (r.meter_model = 'Eastron SDM120-M'::text)))) - GROUP BY o_1.id - ), cycles AS ( - SELECT o_1.id AS order_row_id, - count(r.id) AS telemetry_samples, - min(r.ingest_ts) AS telemetry_first_ts, - max(r.ingest_ts) AS telemetry_last_ts, - min(((r.payload_json ->> 'cycle_pulse_count'::text))::bigint) AS min_cycle_pulse_count, - max(((r.payload_json ->> 'cycle_pulse_count'::text))::bigint) AS max_cycle_pulse_count, - GREATEST((max(((r.payload_json ->> 'cycle_pulse_count'::text))::bigint) - min(((r.payload_json ->> 'cycle_pulse_count'::text))::bigint)), (0)::bigint) AS real_cycle_delta, - round(avg(((r.payload_json ->> 'cycle_rate_ppm'::text))::numeric), 2) AS avg_cycle_rate_ppm, - round(avg(NULLIF(((r.payload_json ->> 'cycle_rate_ppm'::text))::numeric, (0)::numeric)), 2) AS avg_nonzero_cycle_rate_ppm - FROM (orders o_1 - LEFT JOIN mv_hot.edge_oee_eastron_readings r ON (((r.machine_id = o_1.machine_id) AND (r.ingest_ts >= o_1.start_ts) AND (r.ingest_ts <= o_1.end_ts) AND (r.comm_ok = true) AND (r.meter_model = 'Eastron SDM120-M'::text) AND (r.payload_json ? 'cycle_pulse_count'::text)))) - GROUP BY o_1.id - ), stops AS ( - SELECT o_1.id AS order_row_id, - count(ms.id) AS stop_count, - round((COALESCE(sum(GREATEST(EXTRACT(epoch FROM (LEAST(COALESCE(ms.ended_at, o_1.end_ts), o_1.end_ts) - GREATEST(ms.started_at, o_1.start_ts))), (0)::numeric)), (0)::numeric) / 60.0), 2) AS stop_min, - count(ms.id) FILTER (WHERE (ms.classification_status = 'PENDING'::text)) AS pending_stops, - count(ms.id) FILTER (WHERE (ms.classification_status = 'CLASSIFIED'::text)) AS classified_stops, - round(COALESCE(sum(((GREATEST(EXTRACT(epoch FROM (LEAST(COALESCE(ms.ended_at, o_1.end_ts), o_1.end_ts) - GREATEST(ms.started_at, o_1.start_ts))), (0)::numeric) / 3600.0) * ms.value_hour_eur)), (0)::numeric), 2) AS stop_cost_eur - FROM (orders o_1 - LEFT JOIN mv_hot.edge_oee_machine_stops ms ON (((ms.machine_id = o_1.machine_id) AND (ms.started_at < o_1.end_ts) AND (COALESCE(ms.ended_at, now()) > o_1.start_ts)))) - GROUP BY o_1.id - ) - SELECT o.id AS order_row_id, - o.tenant, - o.site, - o.odoo_instance, - o.odoo_workorder_id, - o.odoo_production_id, - o.odoo_order_ref, - o.odoo_state, - o.odoo_workcenter_code, - o.odoo_workcenter_name, - o.machine_id, - o.asset, - o.product, - o.qty_planned, - o.qty_done, - o.uom, - o.planned_cycles, - o.target_cycle_rate_ppm, - o.start_ts, - o.end_ts, - round((EXTRACT(epoch FROM (o.end_ts - o.start_ts)) / 60.0), 2) AS window_min, - COALESCE(c.telemetry_samples, (0)::bigint) AS telemetry_samples, - c.telemetry_first_ts, - c.telemetry_last_ts, - COALESCE(c.min_cycle_pulse_count, (0)::bigint) AS min_cycle_pulse_count, - COALESCE(c.max_cycle_pulse_count, (0)::bigint) AS max_cycle_pulse_count, - COALESCE(c.real_cycle_delta, (0)::bigint) AS real_cycle_delta, - c.avg_cycle_rate_ppm, - c.avg_nonzero_cycle_rate_ppm, - COALESCE(e.energy_samples, (0)::bigint) AS energy_samples, - e.energy_first_ts, - e.energy_last_ts, - e.start_import_kwh, - e.end_import_kwh, - e.kwh_consumed, - e.avg_kw, - e.max_kw, - COALESCE(s.stop_count, (0)::bigint) AS stop_count, - COALESCE(s.stop_min, (0)::numeric) AS stop_min, - COALESCE(s.classified_stops, (0)::bigint) AS classified_stops, - COALESCE(s.pending_stops, (0)::bigint) AS pending_stops, - COALESCE(s.stop_cost_eur, (0)::numeric) AS stop_cost_eur, - CASE - WHEN (COALESCE(c.telemetry_samples, (0)::bigint) = 0) THEN 'SIN_TELEMETRIA_EN_VENTANA'::text - WHEN (COALESCE(e.energy_samples, (0)::bigint) = 0) THEN 'SIN_ENERGIA_EN_VENTANA'::text - WHEN (o.odoo_state = ANY (ARRAY['progress'::text, 'in_progress'::text, 'started'::text])) THEN 'PARCIAL_ORDEN_EN_CURSO'::text - WHEN (COALESCE(s.pending_stops, (0)::bigint) > 0) THEN 'PARCIAL_PAROS_PENDIENTES'::text - ELSE 'CERRADO_CALCULABLE'::text - END AS report_status, - CASE - WHEN (COALESCE(c.telemetry_samples, (0)::bigint) = 0) THEN 'No hay telemetría Edge-OEE dentro de la ventana de la orden.'::text - WHEN (COALESCE(e.energy_samples, (0)::bigint) = 0) THEN 'No hay muestras energéticas válidas dentro de la ventana de la orden.'::text - WHEN (o.odoo_state = ANY (ARRAY['progress'::text, 'in_progress'::text, 'started'::text])) THEN 'La orden sigue en curso. El informe es parcial.'::text - WHEN (COALESCE(s.pending_stops, (0)::bigint) > 0) THEN 'Existen paros pendientes de clasificación.'::text - ELSE 'Orden cerrada con datos suficientes para informe.'::text - END AS report_status_description - FROM (((orders o - LEFT JOIN cycles c ON ((c.order_row_id = o.id))) - LEFT JOIN energy e ON ((e.order_row_id = o.id))) - LEFT JOIN stops s ON ((s.order_row_id = o.id))); - - --- --- Name: order_execution_energy_timeseries_v1; Type: VIEW; Schema: mv_reports_ucepsa_demo; Owner: - --- - -CREATE VIEW mv_reports_ucepsa_demo.order_execution_energy_timeseries_v1 AS - WITH base AS ( - SELECT r.order_row_id, - r.tenant, - r.site, - r.odoo_instance, - r.odoo_workorder_id, - r.odoo_production_id, - r.odoo_order_ref, - r.odoo_state, - r.product, - r.machine_id, - r.asset, - r.start_ts AS order_start_ts, - r.end_ts AS order_end_ts, - r.report_status, - e.id AS reading_id, - e.ts AS meter_ts, - e.ingest_ts AS "time", - e.voltage_l1_v, - e.current_l1_a, - e.power_total_kw, - e.frequency_hz, - e.import_kwh, - e.export_kwh, - e.comm_ok, - e.error_text, - CASE - WHEN (e.payload_json ? 'machine_running'::text) THEN ((e.payload_json ->> 'machine_running'::text))::boolean - ELSE NULL::boolean - END AS machine_running, - CASE - WHEN (e.payload_json ? 'machine_auto_signal'::text) THEN ((e.payload_json ->> 'machine_auto_signal'::text))::boolean - ELSE NULL::boolean - END AS machine_auto_signal, - CASE - WHEN (e.payload_json ? 'cycle_pulse_count'::text) THEN (NULLIF((e.payload_json ->> 'cycle_pulse_count'::text), ''::text))::bigint - ELSE NULL::bigint - END AS cycle_pulse_count, - CASE - WHEN (e.payload_json ? 'cycle_rate_ppm'::text) THEN (NULLIF((e.payload_json ->> 'cycle_rate_ppm'::text), ''::text))::numeric - ELSE NULL::numeric - END AS cycle_rate_ppm, - CASE - WHEN (e.payload_json ? 'last_cycle_age_s'::text) THEN (NULLIF((e.payload_json ->> 'last_cycle_age_s'::text), ''::text))::numeric - ELSE NULL::numeric - END AS last_cycle_age_s, - e.payload_json - FROM (mv_reports_ucepsa_demo.order_execution_report_v1 r - JOIN mv_hot.edge_oee_eastron_readings e ON (((e.machine_id = r.machine_id) AND (e.ingest_ts >= r.start_ts) AND (e.ingest_ts <= r.end_ts) AND (e.meter_model = 'Eastron SDM120-M'::text)))) - ), enriched AS ( - SELECT b.order_row_id, - b.tenant, - b.site, - b.odoo_instance, - b.odoo_workorder_id, - b.odoo_production_id, - b.odoo_order_ref, - b.odoo_state, - b.product, - b.machine_id, - b.asset, - b.order_start_ts, - b.order_end_ts, - b.report_status, - b.reading_id, - b.meter_ts, - b."time", - b.voltage_l1_v, - b.current_l1_a, - b.power_total_kw, - b.frequency_hz, - b.import_kwh, - b.export_kwh, - b.comm_ok, - b.error_text, - b.machine_running, - b.machine_auto_signal, - b.cycle_pulse_count, - b.cycle_rate_ppm, - b.last_cycle_age_s, - b.payload_json, - round((EXTRACT(epoch FROM (b."time" - b.order_start_ts)) / 60.0), 2) AS minute_from_order_start, - round(EXTRACT(epoch FROM (b."time" - lag(b."time") OVER (PARTITION BY b.order_row_id ORDER BY b."time"))), 2) AS sample_gap_s, - min(b.import_kwh) FILTER (WHERE ((b.comm_ok = true) AND (b.import_kwh IS NOT NULL))) OVER (PARTITION BY b.order_row_id) AS order_start_import_kwh, - min(b.cycle_pulse_count) FILTER (WHERE (b.cycle_pulse_count IS NOT NULL)) OVER (PARTITION BY b.order_row_id) AS order_start_cycle_pulse_count - FROM base b - ) - SELECT order_row_id, - tenant, - site, - odoo_instance, - odoo_workorder_id, - odoo_production_id, - odoo_order_ref, - odoo_state, - product, - machine_id, - asset, - order_start_ts, - order_end_ts, - report_status, - reading_id, - meter_ts, - "time", - minute_from_order_start, - sample_gap_s, - voltage_l1_v, - current_l1_a, - power_total_kw, - frequency_hz, - import_kwh, - export_kwh, - round(GREATEST((import_kwh - order_start_import_kwh), (0)::numeric), 3) AS kwh_from_order_start, - comm_ok, - error_text, - machine_running, - machine_auto_signal, - cycle_pulse_count, - GREATEST((cycle_pulse_count - order_start_cycle_pulse_count), (0)::bigint) AS cycles_from_order_start, - cycle_rate_ppm, - last_cycle_age_s, - CASE - WHEN (comm_ok = false) THEN 'BAD_COMMUNICATION'::text - WHEN ((sample_gap_s IS NOT NULL) AND (sample_gap_s > (30)::numeric)) THEN 'TELEMETRY_GAP'::text - WHEN (power_total_kw IS NULL) THEN 'NO_POWER_VALUE'::text - WHEN (import_kwh IS NULL) THEN 'NO_IMPORT_KWH'::text - ELSE 'OK'::text - END AS sample_quality_status, - payload_json - FROM enriched; - - --- --- Name: order_execution_data_quality_v1; Type: VIEW; Schema: mv_reports_ucepsa_demo; Owner: - --- - -CREATE VIEW mv_reports_ucepsa_demo.order_execution_data_quality_v1 AS - WITH base AS ( - SELECT r.order_row_id, - r.tenant, - r.site, - r.odoo_instance, - r.odoo_workorder_id, - r.odoo_production_id, - r.odoo_order_ref, - r.odoo_state, - r.odoo_workcenter_code, - r.odoo_workcenter_name, - r.machine_id, - r.asset, - r.product, - r.qty_planned, - r.qty_done, - r.uom, - r.planned_cycles, - r.target_cycle_rate_ppm, - r.start_ts, - r.end_ts, - r.window_min, - r.telemetry_samples, - r.telemetry_first_ts, - r.telemetry_last_ts, - r.min_cycle_pulse_count, - r.max_cycle_pulse_count, - r.real_cycle_delta, - r.avg_cycle_rate_ppm, - r.avg_nonzero_cycle_rate_ppm, - r.energy_samples, - r.energy_first_ts, - r.energy_last_ts, - r.start_import_kwh, - r.end_import_kwh, - r.kwh_consumed, - r.avg_kw, - r.max_kw, - r.stop_count, - r.stop_min, - r.classified_stops, - r.pending_stops, - r.stop_cost_eur, - r.report_status, - r.report_status_description, - GREATEST(((floor((EXTRACT(epoch FROM (r.end_ts - r.start_ts)) / 10.0)))::integer + 1), 1) AS expected_samples_10s - FROM mv_reports_ucepsa_demo.order_execution_report_v1 r - ), gap_stats AS ( - SELECT t.order_row_id, - count(*) FILTER (WHERE ((t.sample_gap_s IS NOT NULL) AND (t.sample_gap_s > (30)::numeric))) AS telemetry_gap_count_30s, - round(max(t.sample_gap_s), 2) AS max_sample_gap_s, - round(avg(t.sample_gap_s) FILTER (WHERE (t.sample_gap_s IS NOT NULL)), 2) AS avg_sample_gap_s, - count(*) FILTER (WHERE (t.sample_quality_status <> 'OK'::text)) AS non_ok_samples - FROM mv_reports_ucepsa_demo.order_execution_energy_timeseries_v1 t - GROUP BY t.order_row_id - ), scored AS ( - SELECT b.order_row_id, - b.tenant, - b.site, - b.odoo_instance, - b.odoo_workorder_id, - b.odoo_production_id, - b.odoo_order_ref, - b.odoo_state, - b.product, - b.machine_id, - b.asset, - b.start_ts, - b.end_ts, - b.window_min, - b.qty_planned, - b.qty_done, - b.uom, - b.planned_cycles, - b.target_cycle_rate_ppm, - b.telemetry_samples, - b.energy_samples, - b.expected_samples_10s, - round((((COALESCE(b.telemetry_samples, (0)::bigint))::numeric / (NULLIF(b.expected_samples_10s, 0))::numeric) * 100.0), 2) AS telemetry_sample_coverage_pct, - b.telemetry_first_ts, - b.telemetry_last_ts, - b.energy_first_ts, - b.energy_last_ts, - b.real_cycle_delta, - b.avg_cycle_rate_ppm, - b.avg_nonzero_cycle_rate_ppm, - b.kwh_consumed, - b.avg_kw, - b.max_kw, - b.stop_count, - b.stop_min, - b.classified_stops, - b.pending_stops, - b.stop_cost_eur, - COALESCE(g.telemetry_gap_count_30s, (0)::bigint) AS telemetry_gap_count_30s, - COALESCE(g.max_sample_gap_s, (0)::numeric) AS max_sample_gap_s, - COALESCE(g.avg_sample_gap_s, (0)::numeric) AS avg_sample_gap_s, - COALESCE(g.non_ok_samples, (0)::bigint) AS non_ok_samples, - b.report_status, - b.report_status_description, - CASE - WHEN (COALESCE(b.planned_cycles, (0)::numeric) <= (0)::numeric) THEN 'SIN_OBJETIVO_CICLOS'::text - WHEN ((COALESCE(b.real_cycle_delta, (0)::bigint) = 0) AND (COALESCE(b.qty_done, (0)::numeric) > (0)::numeric)) THEN 'CANTIDAD_ODOO_SIN_CICLOS_EDGE'::text - WHEN ((COALESCE(b.real_cycle_delta, (0)::bigint) > 0) AND (COALESCE(b.planned_cycles, (0)::numeric) > (0)::numeric) AND ((((COALESCE(b.real_cycle_delta, (0)::bigint))::numeric / NULLIF(b.planned_cycles, (0)::numeric)) >= 0.5) AND (((COALESCE(b.real_cycle_delta, (0)::bigint))::numeric / NULLIF(b.planned_cycles, (0)::numeric)) <= 1.5))) THEN 'CICLOS_COHERENTES_CON_OBJETIVO'::text - ELSE 'REVISAR_CONVERSION_ODOO_CICLOS'::text - END AS production_conversion_status, - GREATEST(0, (((((((100 - - CASE - WHEN (COALESCE(b.telemetry_samples, (0)::bigint) = 0) THEN 60 - ELSE 0 - END) - - CASE - WHEN (COALESCE(b.energy_samples, (0)::bigint) = 0) THEN 50 - ELSE 0 - END) - - CASE - WHEN (b.odoo_state = ANY (ARRAY['progress'::text, 'in_progress'::text, 'started'::text])) THEN 20 - ELSE 0 - END) - - CASE - WHEN (COALESCE(b.pending_stops, (0)::bigint) > 0) THEN 20 - ELSE 0 - END) - - CASE - WHEN (COALESCE(g.telemetry_gap_count_30s, (0)::bigint) > 0) THEN LEAST(((COALESCE(g.telemetry_gap_count_30s, (0)::bigint))::integer * 2), 15) - ELSE 0 - END) - - CASE - WHEN (COALESCE(g.max_sample_gap_s, (0)::numeric) > (60)::numeric) THEN 10 - ELSE 0 - END) - - CASE - WHEN (COALESCE(g.non_ok_samples, (0)::bigint) > 0) THEN 10 - ELSE 0 - END)) AS technical_quality_score - FROM (base b - LEFT JOIN gap_stats g ON ((g.order_row_id = b.order_row_id))) - ) - SELECT order_row_id, - tenant, - site, - odoo_instance, - odoo_workorder_id, - odoo_production_id, - odoo_order_ref, - odoo_state, - product, - machine_id, - asset, - start_ts, - end_ts, - window_min, - qty_planned, - qty_done, - uom, - planned_cycles, - target_cycle_rate_ppm, - telemetry_samples, - energy_samples, - expected_samples_10s, - telemetry_sample_coverage_pct, - telemetry_first_ts, - telemetry_last_ts, - energy_first_ts, - energy_last_ts, - real_cycle_delta, - avg_cycle_rate_ppm, - avg_nonzero_cycle_rate_ppm, - kwh_consumed, - avg_kw, - max_kw, - stop_count, - stop_min, - classified_stops, - pending_stops, - stop_cost_eur, - telemetry_gap_count_30s, - max_sample_gap_s, - avg_sample_gap_s, - non_ok_samples, - report_status, - report_status_description, - production_conversion_status, - technical_quality_score, - CASE - WHEN (technical_quality_score >= 90) THEN 'FIABLE'::text - WHEN (technical_quality_score >= 70) THEN 'ACEPTABLE_CON_OBSERVACIONES'::text - WHEN (technical_quality_score >= 40) THEN 'PARCIAL'::text - ELSE 'NO_DEFENDIBLE'::text - END AS technical_quality_level, - CASE - WHEN (COALESCE(telemetry_samples, (0)::bigint) = 0) THEN 'No hay telemetría Edge-OEE dentro de la ventana de la orden. El informe solo puede apoyarse en Odoo.'::text - WHEN (COALESCE(energy_samples, (0)::bigint) = 0) THEN 'Faltan muestras energéticas válidas dentro de la ventana de la orden.'::text - WHEN (odoo_state = ANY (ARRAY['progress'::text, 'in_progress'::text, 'started'::text])) THEN 'La orden sigue abierta. El informe debe tratarse como parcial.'::text - WHEN (COALESCE(pending_stops, (0)::bigint) > 0) THEN 'Hay paros pendientes de clasificación. Conviene clasificarlos antes de cerrar el informe.'::text - WHEN (COALESCE(telemetry_gap_count_30s, (0)::bigint) > 0) THEN 'Hay gaps de telemetría superiores a 30 segundos. Revisar continuidad de señal.'::text - WHEN (production_conversion_status = 'REVISAR_CONVERSION_ODOO_CICLOS'::text) THEN 'El informe técnico es válido para paros, energía y ciclos reales; revisar la conversión entre cantidad Odoo y ciclos/golpes.'::text - ELSE 'Datos suficientes para generar informe técnico de orden.'::text - END AS recommended_action - FROM scored; - - --- --- Name: order_execution_stops_v1; Type: VIEW; Schema: mv_reports_ucepsa_demo; Owner: - --- - -CREATE VIEW mv_reports_ucepsa_demo.order_execution_stops_v1 AS - SELECT r.order_row_id, - r.tenant, - r.site, - r.odoo_instance, - r.odoo_workorder_id, - r.odoo_production_id, - r.odoo_order_ref, - r.odoo_state, - r.product, - r.machine_id, - r.asset, - r.start_ts AS order_start_ts, - r.end_ts AS order_end_ts, - r.report_status AS order_report_status, - ms.id AS stop_id, - ms.started_at AS stop_started_at, - ms.ended_at AS stop_ended_at, - GREATEST(ms.started_at, r.start_ts) AS attributed_started_at, - LEAST(COALESCE(ms.ended_at, r.end_ts), r.end_ts) AS attributed_ended_at, - round(ms.duration_s, 2) AS original_duration_s, - round((ms.duration_s / 60.0), 2) AS original_duration_min, - round(EXTRACT(epoch FROM (LEAST(COALESCE(ms.ended_at, r.end_ts), r.end_ts) - GREATEST(ms.started_at, r.start_ts))), 2) AS attributed_duration_s, - round((EXTRACT(epoch FROM (LEAST(COALESCE(ms.ended_at, r.end_ts), r.end_ts) - GREATEST(ms.started_at, r.start_ts))) / 60.0), 2) AS attributed_duration_min, - ms.status AS stop_status, - ms.classification_status, - ms.cause_code, - ms.operator_note, - ms.value_hour_eur, - round(((EXTRACT(epoch FROM (LEAST(COALESCE(ms.ended_at, r.end_ts), r.end_ts) - GREATEST(ms.started_at, r.start_ts))) / 3600.0) * ms.value_hour_eur), 2) AS attributed_cost_eur, - ms.start_event_name, - ms.end_event_name, - ms.created_at, - ms.updated_at, - CASE - WHEN (ms.status = 'OPEN'::text) THEN 'PARO_ABIERTO'::text - WHEN (ms.classification_status = 'PENDING'::text) THEN 'PENDIENTE_CLASIFICACION'::text - WHEN (ms.classification_status = 'CLASSIFIED'::text) THEN 'CLASIFICADO'::text - ELSE 'SIN_ESTADO_CLARO'::text - END AS stop_quality_status - FROM (mv_reports_ucepsa_demo.order_execution_report_v1 r - JOIN mv_hot.edge_oee_machine_stops ms ON (((ms.machine_id = r.machine_id) AND (ms.started_at < r.end_ts) AND (COALESCE(ms.ended_at, r.end_ts) > r.start_ts)))) - WHERE (LEAST(COALESCE(ms.ended_at, r.end_ts), r.end_ts) > GREATEST(ms.started_at, r.start_ts)); - - --- --- Name: order_execution_stop_causes_v1; Type: VIEW; Schema: mv_reports_ucepsa_demo; Owner: - --- - -CREATE VIEW mv_reports_ucepsa_demo.order_execution_stop_causes_v1 AS - SELECT order_row_id, - tenant, - site, - odoo_instance, - odoo_workorder_id, - odoo_production_id, - odoo_order_ref, - product, - machine_id, - cause_code, - count(*) AS stop_count, - round(sum(attributed_duration_min), 2) AS total_stop_min, - round(sum(attributed_cost_eur), 2) AS total_stop_cost_eur, - count(*) FILTER (WHERE (classification_status = 'CLASSIFIED'::text)) AS classified_stop_count, - count(*) FILTER (WHERE (classification_status = 'PENDING'::text)) AS pending_stop_count, - min(stop_started_at) AS first_stop_at, - max(stop_ended_at) AS last_stop_at - FROM mv_reports_ucepsa_demo.order_execution_stops_v1 - GROUP BY order_row_id, tenant, site, odoo_instance, odoo_workorder_id, odoo_production_id, odoo_order_ref, product, machine_id, cause_code; - - --- --- Name: order_execution_timeline_v1; Type: VIEW; Schema: mv_reports_ucepsa_demo; Owner: - --- - -CREATE VIEW mv_reports_ucepsa_demo.order_execution_timeline_v1 AS - SELECT r.order_row_id, - r.tenant, - r.site, - r.odoo_instance, - r.odoo_workorder_id, - r.odoo_production_id, - r.odoo_order_ref, - r.product, - r.machine_id, - r.asset, - r.start_ts AS event_ts, - 'ORDER_START'::text AS event_type, - 'Inicio de orden'::text AS event_label, - 'INFO'::text AS severity, - NULL::bigint AS stop_id, - NULL::bigint AS node_event_id, - NULL::text AS cause_code, - NULL::numeric AS duration_min, - NULL::numeric AS cost_eur, - 'Inicio de la ventana de ejecución de la orden según Odoo.'::text AS description, - jsonb_build_object('source', 'mv_reports_ucepsa_demo.order_execution_report_v1', 'odoo_state', r.odoo_state, 'qty_planned', r.qty_planned, 'qty_done', r.qty_done, 'uom', r.uom) AS evidence_json - FROM mv_reports_ucepsa_demo.order_execution_report_v1 r - WHERE (r.start_ts IS NOT NULL) -UNION ALL - SELECT r.order_row_id, - r.tenant, - r.site, - r.odoo_instance, - r.odoo_workorder_id, - r.odoo_production_id, - r.odoo_order_ref, - r.product, - r.machine_id, - r.asset, - r.telemetry_first_ts AS event_ts, - 'FIRST_TELEMETRY'::text AS event_type, - 'Primera telemetría Edge-OEE'::text AS event_label, - 'INFO'::text AS severity, - NULL::bigint AS stop_id, - NULL::bigint AS node_event_id, - NULL::text AS cause_code, - NULL::numeric AS duration_min, - NULL::numeric AS cost_eur, - 'Primera muestra de telemetría recibida dentro de la ventana de la orden.'::text AS description, - jsonb_build_object('source', 'mv_reports_ucepsa_demo.order_execution_report_v1', 'telemetry_samples', r.telemetry_samples, 'energy_samples', r.energy_samples) AS evidence_json - FROM mv_reports_ucepsa_demo.order_execution_report_v1 r - WHERE (r.telemetry_first_ts IS NOT NULL) -UNION ALL - SELECT s.order_row_id, - s.tenant, - s.site, - s.odoo_instance, - s.odoo_workorder_id, - s.odoo_production_id, - s.odoo_order_ref, - s.product, - s.machine_id, - s.asset, - s.attributed_started_at AS event_ts, - 'STOP_START'::text AS event_type, - 'Inicio de paro'::text AS event_label, - CASE - WHEN (s.classification_status = 'PENDING'::text) THEN 'WARNING'::text - ELSE 'LOSS'::text - END AS severity, - s.stop_id, - NULL::bigint AS node_event_id, - s.cause_code, - s.attributed_duration_min AS duration_min, - s.attributed_cost_eur AS cost_eur, - concat('Inicio de paro imputado a la orden. Causa: ', COALESCE(s.cause_code, 'sin clasificar'::text), '. Duración imputada: ', COALESCE((s.attributed_duration_min)::text, '0'::text), ' min.') AS description, - jsonb_build_object('source', 'mv_reports_ucepsa_demo.order_execution_stops_v1', 'stop_id', s.stop_id, 'classification_status', s.classification_status, 'operator_note', s.operator_note, 'original_started_at', s.stop_started_at, 'original_ended_at', s.stop_ended_at) AS evidence_json - FROM mv_reports_ucepsa_demo.order_execution_stops_v1 s -UNION ALL - SELECT s.order_row_id, - s.tenant, - s.site, - s.odoo_instance, - s.odoo_workorder_id, - s.odoo_production_id, - s.odoo_order_ref, - s.product, - s.machine_id, - s.asset, - s.attributed_ended_at AS event_ts, - 'STOP_END'::text AS event_type, - 'Fin de paro'::text AS event_label, - CASE - WHEN (s.classification_status = 'PENDING'::text) THEN 'WARNING'::text - ELSE 'INFO'::text - END AS severity, - s.stop_id, - NULL::bigint AS node_event_id, - s.cause_code, - s.attributed_duration_min AS duration_min, - s.attributed_cost_eur AS cost_eur, - concat('Fin de paro imputado a la orden. Causa: ', COALESCE(s.cause_code, 'sin clasificar'::text), '. Coste imputado: ', COALESCE((s.attributed_cost_eur)::text, '0'::text), ' €.') AS description, - jsonb_build_object('source', 'mv_reports_ucepsa_demo.order_execution_stops_v1', 'stop_id', s.stop_id, 'classification_status', s.classification_status, 'operator_note', s.operator_note, 'value_hour_eur', s.value_hour_eur) AS evidence_json - FROM mv_reports_ucepsa_demo.order_execution_stops_v1 s -UNION ALL - SELECT r.order_row_id, - r.tenant, - r.site, - r.odoo_instance, - r.odoo_workorder_id, - r.odoo_production_id, - r.odoo_order_ref, - r.product, - r.machine_id, - r.asset, - r.telemetry_last_ts AS event_ts, - 'LAST_TELEMETRY'::text AS event_type, - 'Última telemetría Edge-OEE'::text AS event_label, - CASE - WHEN (r.report_status = 'PARCIAL_ORDEN_EN_CURSO'::text) THEN 'WARNING'::text - ELSE 'INFO'::text - END AS severity, - NULL::bigint AS stop_id, - NULL::bigint AS node_event_id, - NULL::text AS cause_code, - NULL::numeric AS duration_min, - NULL::numeric AS cost_eur, - 'Última muestra de telemetría recibida dentro de la ventana analizada.'::text AS description, - jsonb_build_object('source', 'mv_reports_ucepsa_demo.order_execution_report_v1', 'real_cycle_delta', r.real_cycle_delta, 'kwh_consumed', r.kwh_consumed, 'avg_kw', r.avg_kw, 'max_kw', r.max_kw) AS evidence_json - FROM mv_reports_ucepsa_demo.order_execution_report_v1 r - WHERE (r.telemetry_last_ts IS NOT NULL) -UNION ALL - SELECT r.order_row_id, - r.tenant, - r.site, - r.odoo_instance, - r.odoo_workorder_id, - r.odoo_production_id, - r.odoo_order_ref, - r.product, - r.machine_id, - r.asset, - r.end_ts AS event_ts, - CASE - WHEN (r.odoo_state = ANY (ARRAY['progress'::text, 'in_progress'::text, 'started'::text])) THEN 'REPORT_CUTOFF'::text - ELSE 'ORDER_END'::text - END AS event_type, - CASE - WHEN (r.odoo_state = ANY (ARRAY['progress'::text, 'in_progress'::text, 'started'::text])) THEN 'Corte provisional del informe'::text - ELSE 'Fin de orden'::text - END AS event_label, - CASE - WHEN (r.report_status = 'CERRADO_CALCULABLE'::text) THEN 'INFO'::text - ELSE 'WARNING'::text - END AS severity, - NULL::bigint AS stop_id, - NULL::bigint AS node_event_id, - NULL::text AS cause_code, - NULL::numeric AS duration_min, - NULL::numeric AS cost_eur, - r.report_status_description AS description, - jsonb_build_object('source', 'mv_reports_ucepsa_demo.order_execution_report_v1', 'report_status', r.report_status, 'window_min', r.window_min, 'stop_count', r.stop_count, 'stop_min', r.stop_min, 'stop_cost_eur', r.stop_cost_eur) AS evidence_json - FROM mv_reports_ucepsa_demo.order_execution_report_v1 r - WHERE (r.end_ts IS NOT NULL); - - --- --- Name: order_execution_report_full_v1; Type: VIEW; Schema: mv_reports_ucepsa_demo; Owner: - --- - -CREATE VIEW mv_reports_ucepsa_demo.order_execution_report_full_v1 AS - WITH stop_causes_json AS ( - SELECT order_execution_stop_causes_v1.order_row_id, - jsonb_agg(jsonb_build_object('cause_code', order_execution_stop_causes_v1.cause_code, 'stop_count', order_execution_stop_causes_v1.stop_count, 'total_stop_min', order_execution_stop_causes_v1.total_stop_min, 'total_stop_cost_eur', order_execution_stop_causes_v1.total_stop_cost_eur, 'classified_stop_count', order_execution_stop_causes_v1.classified_stop_count, 'pending_stop_count', order_execution_stop_causes_v1.pending_stop_count, 'first_stop_at', order_execution_stop_causes_v1.first_stop_at, 'last_stop_at', order_execution_stop_causes_v1.last_stop_at) ORDER BY order_execution_stop_causes_v1.total_stop_min DESC) AS stop_causes_json, - string_agg(concat(COALESCE(order_execution_stop_causes_v1.cause_code, 'sin_clasificar'::text), ': ', order_execution_stop_causes_v1.stop_count, ' paros / ', order_execution_stop_causes_v1.total_stop_min, ' min / ', order_execution_stop_causes_v1.total_stop_cost_eur, ' €'), '; '::text ORDER BY order_execution_stop_causes_v1.total_stop_min DESC) AS stop_causes_summary - FROM mv_reports_ucepsa_demo.order_execution_stop_causes_v1 - GROUP BY order_execution_stop_causes_v1.order_row_id - ), stops_json AS ( - SELECT order_execution_stops_v1.order_row_id, - jsonb_agg(jsonb_build_object('stop_id', order_execution_stops_v1.stop_id, 'stop_started_at', order_execution_stops_v1.stop_started_at, 'stop_ended_at', order_execution_stops_v1.stop_ended_at, 'attributed_started_at', order_execution_stops_v1.attributed_started_at, 'attributed_ended_at', order_execution_stops_v1.attributed_ended_at, 'attributed_duration_min', order_execution_stops_v1.attributed_duration_min, 'cause_code', order_execution_stops_v1.cause_code, 'operator_note', order_execution_stops_v1.operator_note, 'classification_status', order_execution_stops_v1.classification_status, 'attributed_cost_eur', order_execution_stops_v1.attributed_cost_eur, 'stop_quality_status', order_execution_stops_v1.stop_quality_status) ORDER BY order_execution_stops_v1.attributed_started_at) AS stops_json - FROM mv_reports_ucepsa_demo.order_execution_stops_v1 - GROUP BY order_execution_stops_v1.order_row_id - ), timeline_json AS ( - SELECT order_execution_timeline_v1.order_row_id, - jsonb_agg(jsonb_build_object('event_ts', order_execution_timeline_v1.event_ts, 'event_type', order_execution_timeline_v1.event_type, 'event_label', order_execution_timeline_v1.event_label, 'severity', order_execution_timeline_v1.severity, 'stop_id', order_execution_timeline_v1.stop_id, 'cause_code', order_execution_timeline_v1.cause_code, 'duration_min', order_execution_timeline_v1.duration_min, 'cost_eur', order_execution_timeline_v1.cost_eur, 'description', order_execution_timeline_v1.description) ORDER BY order_execution_timeline_v1.event_ts, order_execution_timeline_v1.event_type) AS timeline_json - FROM mv_reports_ucepsa_demo.order_execution_timeline_v1 - GROUP BY order_execution_timeline_v1.order_row_id - ) - SELECT r.order_row_id, - r.tenant, - r.site, - r.odoo_instance, - r.odoo_workorder_id, - r.odoo_production_id, - r.odoo_order_ref, - r.odoo_state, - r.odoo_workcenter_code, - r.odoo_workcenter_name, - r.machine_id, - r.asset, - r.product, - r.qty_planned, - r.qty_done, - r.uom, - r.planned_cycles, - r.target_cycle_rate_ppm, - r.start_ts, - r.end_ts, - r.window_min, - GREATEST((r.window_min - r.stop_min), (0)::numeric) AS run_min, - CASE - WHEN (r.window_min > (0)::numeric) THEN round(((r.stop_min / r.window_min) * 100.0), 2) - ELSE NULL::numeric - END AS stop_pct, - CASE - WHEN (r.window_min > (0)::numeric) THEN round(((GREATEST((r.window_min - r.stop_min), (0)::numeric) / r.window_min) * 100.0), 2) - ELSE NULL::numeric - END AS availability_pct, - r.telemetry_samples, - r.telemetry_first_ts, - r.telemetry_last_ts, - r.energy_samples, - r.energy_first_ts, - r.energy_last_ts, - r.start_import_kwh, - r.end_import_kwh, - r.kwh_consumed, - r.avg_kw, - r.max_kw, - r.min_cycle_pulse_count, - r.max_cycle_pulse_count, - r.real_cycle_delta, - r.avg_cycle_rate_ppm, - r.avg_nonzero_cycle_rate_ppm, - CASE - WHEN (r.real_cycle_delta > 0) THEN round((r.kwh_consumed / (NULLIF(r.real_cycle_delta, 0))::numeric), 6) - ELSE NULL::numeric - END AS kwh_per_cycle, - CASE - WHEN (GREATEST((r.window_min - r.stop_min), (0)::numeric) > (0)::numeric) THEN round(((r.real_cycle_delta)::numeric / NULLIF(GREATEST((r.window_min - r.stop_min), (0)::numeric), (0)::numeric)), 2) - ELSE NULL::numeric - END AS cycles_per_run_min, - r.stop_count, - r.stop_min, - r.classified_stops, - r.pending_stops, - r.stop_cost_eur, - COALESCE(sc.stop_causes_summary, 'Sin paros imputados'::text) AS stop_causes_summary, - COALESCE(sc.stop_causes_json, '[]'::jsonb) AS stop_causes_json, - COALESCE(sj.stops_json, '[]'::jsonb) AS stops_json, - COALESCE(tj.timeline_json, '[]'::jsonb) AS timeline_json, - q.expected_samples_10s, - q.telemetry_sample_coverage_pct, - q.telemetry_gap_count_30s, - q.max_sample_gap_s, - q.avg_sample_gap_s, - q.non_ok_samples, - q.production_conversion_status, - q.technical_quality_score, - q.technical_quality_level, - r.report_status, - r.report_status_description, - q.recommended_action, - CASE - WHEN ((r.report_status = 'CERRADO_CALCULABLE'::text) AND (q.technical_quality_level = 'FIABLE'::text) AND (q.production_conversion_status = 'CICLOS_COHERENTES_CON_OBJETIVO'::text)) THEN 'LISTO_INFORME_FINAL'::text - WHEN ((r.report_status = 'CERRADO_CALCULABLE'::text) AND (q.technical_quality_level = 'FIABLE'::text) AND (q.production_conversion_status = 'REVISAR_CONVERSION_ODOO_CICLOS'::text)) THEN 'LISTO_TECNICO_REVISAR_CONVERSION'::text - WHEN ((r.report_status = 'CERRADO_CALCULABLE'::text) AND (q.technical_quality_level = ANY (ARRAY['FIABLE'::text, 'ACEPTABLE_CON_OBSERVACIONES'::text]))) THEN 'LISTO_CON_OBSERVACIONES'::text - WHEN (r.report_status = 'PARCIAL_ORDEN_EN_CURSO'::text) THEN 'INFORME_PARCIAL_ORDEN_EN_CURSO'::text - WHEN (r.report_status = 'SIN_TELEMETRIA_EN_VENTANA'::text) THEN 'BLOQUEADO_SIN_TELEMETRIA'::text - WHEN (r.report_status = 'SIN_ENERGIA_EN_VENTANA'::text) THEN 'BLOQUEADO_SIN_ENERGIA'::text - WHEN (r.pending_stops > 0) THEN 'PENDIENTE_CLASIFICAR_PAROS'::text - ELSE 'REVISAR'::text - END AS final_report_status, - concat('Orden ', r.odoo_order_ref, ' en ', r.machine_id, ': duración ', r.window_min, ' min; ciclos reales ', r.real_cycle_delta, '; energía ', COALESCE((r.kwh_consumed)::text, 'sin dato'::text), ' kWh; paros ', r.stop_count, ' (', r.stop_min, ' min); coste estimado ', r.stop_cost_eur, ' €. Estado técnico: ', q.technical_quality_level, '.') AS executive_summary - FROM ((((mv_reports_ucepsa_demo.order_execution_report_v1 r - LEFT JOIN mv_reports_ucepsa_demo.order_execution_data_quality_v1 q ON ((q.order_row_id = r.order_row_id))) - LEFT JOIN stop_causes_json sc ON ((sc.order_row_id = r.order_row_id))) - LEFT JOIN stops_json sj ON ((sj.order_row_id = r.order_row_id))) - LEFT JOIN timeline_json tj ON ((tj.order_row_id = r.order_row_id))); - - --- --- PostgreSQL database dump complete --- - -\unrestrict YbA0VcRIrZ1kSDSxIf4zctoL9eu8TYoJ5cvkbSD5WkFTQ1jRyH26snWSZehz5VJ - - -GRANT USAGE ON SCHEMA mv_reports_ucepsa_demo TO grafana_ucepsa_ro; - -GRANT SELECT ON mv_reports_ucepsa_demo.order_execution_report_v1 TO grafana_ucepsa_ro; -GRANT SELECT ON mv_reports_ucepsa_demo.order_execution_stops_v1 TO grafana_ucepsa_ro; -GRANT SELECT ON mv_reports_ucepsa_demo.order_execution_stop_causes_v1 TO grafana_ucepsa_ro; -GRANT SELECT ON mv_reports_ucepsa_demo.order_execution_timeline_v1 TO grafana_ucepsa_ro; -GRANT SELECT ON mv_reports_ucepsa_demo.order_execution_energy_timeseries_v1 TO grafana_ucepsa_ro; -GRANT SELECT ON mv_reports_ucepsa_demo.order_execution_data_quality_v1 TO grafana_ucepsa_ro; -GRANT SELECT ON mv_reports_ucepsa_demo.order_execution_report_full_v1 TO grafana_ucepsa_ro; diff --git a/ucepsa/edge-oee-demo/sql/reports/080_margin_intelligence_cost_readiness.sql b/ucepsa/edge-oee-demo/sql/reports/080_margin_intelligence_cost_readiness.sql deleted file mode 100644 index b9d6ee8..0000000 --- a/ucepsa/edge-oee-demo/sql/reports/080_margin_intelligence_cost_readiness.sql +++ /dev/null @@ -1,1186 +0,0 @@ --- MESAVAULT Edge-OEE UCEPSA --- Margin Intelligence + Cost Readiness Cockpit --- Schema-only export. No data. No secrets. - -DROP VIEW IF EXISTS mv_reports_ucepsa_demo.cost_readiness_summary_v1; -DROP VIEW IF EXISTS mv_reports_ucepsa_demo.cost_readiness_raw_materials_v1; -DROP VIEW IF EXISTS mv_reports_ucepsa_demo.cost_readiness_orders_v1; - -DROP VIEW IF EXISTS mv_reports_ucepsa_demo.order_margin_report_v1; -DROP VIEW IF EXISTS mv_reports_ucepsa_demo.order_raw_material_costs_summary_v1; -DROP VIEW IF EXISTS mv_reports_ucepsa_demo.order_raw_material_costs_v1; - -DROP TABLE IF EXISTS mv_edge_oee_ucepsa_demo.raw_material_cost_parameters; -DROP TABLE IF EXISTS mv_edge_oee_ucepsa_demo.margin_cost_parameters; -DROP TABLE IF EXISTS mv_edge_oee_ucepsa_demo.odoo_order_commercials; - --- --- PostgreSQL database dump --- - -\restrict prs5NerySlAyEA4mTL6Sg3Uqx00yOGe9h66uyBfmfApVdToFu1LKFfY74XjwGca - --- Dumped from database version 16.14 --- Dumped by pg_dump version 16.14 - -SET statement_timeout = 0; -SET lock_timeout = 0; -SET idle_in_transaction_session_timeout = 0; -SET client_encoding = 'UTF8'; -SET standard_conforming_strings = on; -SELECT pg_catalog.set_config('search_path', '', false); -SET check_function_bodies = false; -SET xmloption = content; -SET client_min_messages = warning; -SET row_security = off; - -SET default_tablespace = ''; - -SET default_table_access_method = heap; - --- --- Name: margin_cost_parameters; Type: TABLE; Schema: mv_edge_oee_ucepsa_demo; Owner: - --- - -CREATE TABLE mv_edge_oee_ucepsa_demo.margin_cost_parameters ( - id bigint NOT NULL, - tenant text DEFAULT 'ucepsa'::text NOT NULL, - site text DEFAULT 'ucepsa_test'::text NOT NULL, - scope_level text DEFAULT 'GLOBAL'::text NOT NULL, - machine_id text, - product_default_code text, - energy_cost_eur_per_kwh numeric(14,6), - material_cost_eur_per_kg numeric(14,6), - machine_run_cost_eur_per_h numeric(14,4), - labor_cost_eur_per_h numeric(14,4), - overhead_cost_eur_per_h numeric(14,4), - valid_from timestamp with time zone DEFAULT now() NOT NULL, - valid_to timestamp with time zone, - is_active boolean DEFAULT true NOT NULL, - notes text, - created_at timestamp with time zone DEFAULT now() NOT NULL -); - - --- --- Name: margin_cost_parameters_id_seq; Type: SEQUENCE; Schema: mv_edge_oee_ucepsa_demo; Owner: - --- - -CREATE SEQUENCE mv_edge_oee_ucepsa_demo.margin_cost_parameters_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: margin_cost_parameters_id_seq; Type: SEQUENCE OWNED BY; Schema: mv_edge_oee_ucepsa_demo; Owner: - --- - -ALTER SEQUENCE mv_edge_oee_ucepsa_demo.margin_cost_parameters_id_seq OWNED BY mv_edge_oee_ucepsa_demo.margin_cost_parameters.id; - - --- --- Name: odoo_order_commercials; Type: TABLE; Schema: mv_edge_oee_ucepsa_demo; Owner: - --- - -CREATE TABLE mv_edge_oee_ucepsa_demo.odoo_order_commercials ( - id bigint NOT NULL, - tenant text DEFAULT 'ucepsa'::text NOT NULL, - site text DEFAULT 'ucepsa_test'::text NOT NULL, - odoo_instance text NOT NULL, - odoo_production_id integer NOT NULL, - odoo_order_ref text NOT NULL, - odoo_origin text, - odoo_state text, - product_id integer, - product_default_code text, - product_name text, - product_uom text, - product_standard_price numeric(14,4), - product_list_price numeric(14,4), - production_qty numeric(14,3), - qty_producing numeric(14,3), - production_uom text, - sale_line_id integer, - sale_order_id integer, - sale_order_ref text, - customer_id integer, - customer_name text, - sale_state text, - sale_date_order timestamp with time zone, - currency text, - sale_line_qty numeric(14,3), - sale_line_qty_delivered numeric(14,3), - sale_price_unit numeric(14,4), - sale_discount_pct numeric(10,2), - revenue_net_eur numeric(14,4), - revenue_total_eur numeric(14,4), - raw_material_value_eur numeric(14,4), - finished_value_eur numeric(14,4), - material_cost_status text DEFAULT 'UNAVAILABLE'::text NOT NULL, - stock_raw_moves_json jsonb DEFAULT '[]'::jsonb NOT NULL, - stock_finished_moves_json jsonb DEFAULT '[]'::jsonb NOT NULL, - raw_json jsonb DEFAULT '{}'::jsonb NOT NULL, - snapshot_at timestamp with time zone DEFAULT now() NOT NULL -); - - --- --- Name: odoo_order_commercials_id_seq; Type: SEQUENCE; Schema: mv_edge_oee_ucepsa_demo; Owner: - --- - -CREATE SEQUENCE mv_edge_oee_ucepsa_demo.odoo_order_commercials_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: odoo_order_commercials_id_seq; Type: SEQUENCE OWNED BY; Schema: mv_edge_oee_ucepsa_demo; Owner: - --- - -ALTER SEQUENCE mv_edge_oee_ucepsa_demo.odoo_order_commercials_id_seq OWNED BY mv_edge_oee_ucepsa_demo.odoo_order_commercials.id; - - --- --- Name: raw_material_cost_parameters; Type: TABLE; Schema: mv_edge_oee_ucepsa_demo; Owner: - --- - -CREATE TABLE mv_edge_oee_ucepsa_demo.raw_material_cost_parameters ( - id bigint NOT NULL, - tenant text DEFAULT 'ucepsa'::text NOT NULL, - site text DEFAULT 'ucepsa_test'::text NOT NULL, - raw_product_default_code text NOT NULL, - raw_product_name text, - material_cost_eur_per_kg numeric(14,6) NOT NULL, - source text DEFAULT 'MANUAL'::text NOT NULL, - valid_from timestamp with time zone DEFAULT now() NOT NULL, - valid_to timestamp with time zone, - is_active boolean DEFAULT true NOT NULL, - notes text, - created_at timestamp with time zone DEFAULT now() NOT NULL -); - - --- --- Name: raw_material_cost_parameters_id_seq; Type: SEQUENCE; Schema: mv_edge_oee_ucepsa_demo; Owner: - --- - -CREATE SEQUENCE mv_edge_oee_ucepsa_demo.raw_material_cost_parameters_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: raw_material_cost_parameters_id_seq; Type: SEQUENCE OWNED BY; Schema: mv_edge_oee_ucepsa_demo; Owner: - --- - -ALTER SEQUENCE mv_edge_oee_ucepsa_demo.raw_material_cost_parameters_id_seq OWNED BY mv_edge_oee_ucepsa_demo.raw_material_cost_parameters.id; - - --- --- Name: order_raw_material_costs_v1; Type: VIEW; Schema: mv_reports_ucepsa_demo; Owner: - --- - -CREATE VIEW mv_reports_ucepsa_demo.order_raw_material_costs_v1 AS - WITH raw_moves AS ( - SELECT c.id AS commercial_row_id, - c.tenant, - c.site, - c.odoo_instance, - c.odoo_production_id, - c.odoo_order_ref, - c.sale_order_ref, - c.customer_name, - c.product_default_code AS finished_product_default_code, - c.product_name AS finished_product_name, - ((rm.raw_move ->> 'id'::text))::integer AS raw_move_id, - (NULLIF(((rm.raw_move -> 'product_id'::text) ->> 0), ''::text))::integer AS raw_product_id, - ((rm.raw_move -> 'product_id'::text) ->> 1) AS raw_product_name, - "substring"(((rm.raw_move -> 'product_id'::text) ->> 1), '\[([^\]]+)\]'::text) AS raw_product_default_code, - (NULLIF((rm.raw_move ->> 'product_uom_qty'::text), ''::text))::numeric AS planned_qty, - (NULLIF((rm.raw_move ->> 'quantity'::text), ''::text))::numeric AS consumed_qty, - ((rm.raw_move -> 'product_uom'::text) ->> 1) AS raw_uom, - (NULLIF((rm.raw_move ->> 'price_unit'::text), ''::text))::numeric AS odoo_price_unit, - (NULLIF((rm.raw_move ->> 'value'::text), ''::text))::numeric AS odoo_value, - (rm.raw_move ->> 'state'::text) AS move_state, - rm.raw_move AS raw_move_json - FROM (mv_edge_oee_ucepsa_demo.odoo_order_commercials c - CROSS JOIN LATERAL jsonb_array_elements(c.stock_raw_moves_json) rm(raw_move)) - ), costed AS ( - SELECT r.commercial_row_id, - r.tenant, - r.site, - r.odoo_instance, - r.odoo_production_id, - r.odoo_order_ref, - r.sale_order_ref, - r.customer_name, - r.finished_product_default_code, - r.finished_product_name, - r.raw_move_id, - r.raw_product_id, - r.raw_product_name, - r.raw_product_default_code, - r.planned_qty, - r.consumed_qty, - r.raw_uom, - r.odoo_price_unit, - r.odoo_value, - r.move_state, - r.raw_move_json, - p.id AS raw_material_cost_parameter_id, - p.material_cost_eur_per_kg, - p.source AS raw_material_cost_source, - p.notes AS raw_material_cost_notes, - CASE - WHEN (COALESCE(r.odoo_value, (0)::numeric) <> (0)::numeric) THEN abs(r.odoo_value) - WHEN ((lower(COALESCE(r.raw_uom, ''::text)) = 'kg'::text) AND (p.material_cost_eur_per_kg IS NOT NULL) AND (r.consumed_qty IS NOT NULL)) THEN (r.consumed_qty * p.material_cost_eur_per_kg) - ELSE NULL::numeric - END AS material_cost_estimated_eur, - CASE - WHEN (COALESCE(r.odoo_value, (0)::numeric) <> (0)::numeric) THEN 'FROM_ODOO_STOCK_MOVE_VALUE'::text - WHEN (r.raw_product_default_code IS NULL) THEN 'MISSING_RAW_PRODUCT_CODE'::text - WHEN (lower(COALESCE(r.raw_uom, ''::text)) <> 'kg'::text) THEN 'UNSUPPORTED_RAW_UOM'::text - WHEN (p.material_cost_eur_per_kg IS NULL) THEN 'MISSING_RAW_MATERIAL_COST_PARAMETER'::text - WHEN (r.consumed_qty IS NULL) THEN 'MISSING_CONSUMED_QTY'::text - ELSE 'FROM_RAW_MATERIAL_COST_PARAMETER'::text - END AS material_cost_status - FROM (raw_moves r - LEFT JOIN LATERAL ( SELECT p_1.id, - p_1.tenant, - p_1.site, - p_1.raw_product_default_code, - p_1.raw_product_name, - p_1.material_cost_eur_per_kg, - p_1.source, - p_1.valid_from, - p_1.valid_to, - p_1.is_active, - p_1.notes, - p_1.created_at - FROM mv_edge_oee_ucepsa_demo.raw_material_cost_parameters p_1 - WHERE ((p_1.tenant = r.tenant) AND (p_1.site = r.site) AND (p_1.is_active = true) AND (p_1.raw_product_default_code = r.raw_product_default_code) AND (p_1.valid_from <= now()) AND ((p_1.valid_to IS NULL) OR (p_1.valid_to > now()))) - ORDER BY p_1.valid_from DESC, p_1.id DESC - LIMIT 1) p ON (true)) - ) - SELECT commercial_row_id, - tenant, - site, - odoo_instance, - odoo_production_id, - odoo_order_ref, - sale_order_ref, - customer_name, - finished_product_default_code, - finished_product_name, - raw_move_id, - raw_product_id, - raw_product_name, - raw_product_default_code, - planned_qty, - consumed_qty, - raw_uom, - odoo_price_unit, - odoo_value, - move_state, - raw_move_json, - raw_material_cost_parameter_id, - material_cost_eur_per_kg, - raw_material_cost_source, - raw_material_cost_notes, - material_cost_estimated_eur, - material_cost_status, - round(material_cost_estimated_eur, 4) AS material_cost_estimated_eur_rounded - FROM costed; - - --- --- Name: order_raw_material_costs_summary_v1; Type: VIEW; Schema: mv_reports_ucepsa_demo; Owner: - --- - -CREATE VIEW mv_reports_ucepsa_demo.order_raw_material_costs_summary_v1 AS - SELECT tenant, - site, - odoo_instance, - odoo_production_id, - odoo_order_ref, - sale_order_ref, - customer_name, - finished_product_default_code, - finished_product_name, - count(*) AS raw_material_line_count, - round(sum(COALESCE(consumed_qty, (0)::numeric)), 3) AS total_consumed_qty, - string_agg(concat(COALESCE(raw_product_default_code, 'sin_codigo'::text), ' - ', COALESCE(raw_product_name, 'sin_nombre'::text), ': ', COALESCE((consumed_qty)::text, 'sin_cantidad'::text), ' ', COALESCE(raw_uom, ''::text)), '; '::text ORDER BY raw_product_default_code) AS raw_materials_summary, - round(sum(COALESCE(material_cost_estimated_eur, (0)::numeric)), 4) AS material_cost_estimated_eur, - count(*) FILTER (WHERE (material_cost_estimated_eur IS NOT NULL)) AS costed_raw_material_line_count, - count(*) FILTER (WHERE ((material_cost_status <> 'FROM_RAW_MATERIAL_COST_PARAMETER'::text) AND (material_cost_status <> 'FROM_ODOO_STOCK_MOVE_VALUE'::text))) AS missing_or_invalid_raw_material_cost_count, - string_agg(DISTINCT material_cost_status, ', '::text ORDER BY material_cost_status) AS material_cost_status_summary - FROM mv_reports_ucepsa_demo.order_raw_material_costs_v1 - GROUP BY tenant, site, odoo_instance, odoo_production_id, odoo_order_ref, sale_order_ref, customer_name, finished_product_default_code, finished_product_name; - - --- --- Name: order_margin_report_v1; Type: VIEW; Schema: mv_reports_ucepsa_demo; Owner: - --- - -CREATE VIEW mv_reports_ucepsa_demo.order_margin_report_v1 AS - WITH joined AS ( - SELECT r.order_row_id, - r.tenant, - r.site, - r.odoo_instance, - r.odoo_workorder_id, - r.odoo_production_id, - r.odoo_order_ref, - r.odoo_state, - r.machine_id, - r.asset, - r.product AS edge_product, - r.start_ts, - r.end_ts, - r.window_min, - r.run_min, - r.stop_min, - r.stop_cost_eur, - r.real_cycle_delta, - r.kwh_consumed, - r.kwh_per_cycle, - r.technical_quality_level, - r.final_report_status, - c.odoo_origin, - c.sale_order_ref, - c.customer_id, - c.customer_name, - c.sale_state, - c.sale_date_order, - c.currency, - c.product_id, - c.product_default_code, - c.product_name, - c.production_qty, - c.qty_producing, - c.production_uom, - c.sale_line_id, - c.sale_order_id, - c.sale_line_qty, - c.sale_line_qty_delivered, - c.sale_price_unit, - c.sale_discount_pct, - c.revenue_net_eur, - c.revenue_total_eur, - c.product_standard_price, - c.product_list_price, - c.raw_material_value_eur, - c.finished_value_eur, - c.material_cost_status AS odoo_material_cost_status, - c.snapshot_at AS commercial_snapshot_at, - rms.raw_material_line_count, - rms.total_consumed_qty AS raw_total_consumed_qty, - rms.raw_materials_summary, - rms.material_cost_estimated_eur AS raw_material_cost_estimated_eur, - rms.costed_raw_material_line_count, - rms.missing_or_invalid_raw_material_cost_count, - rms.material_cost_status_summary AS raw_material_cost_status_summary - FROM ((mv_reports_ucepsa_demo.order_execution_report_full_v1 r - LEFT JOIN mv_edge_oee_ucepsa_demo.odoo_order_commercials c ON (((c.odoo_instance = r.odoo_instance) AND (c.odoo_production_id = r.odoo_production_id)))) - LEFT JOIN mv_reports_ucepsa_demo.order_raw_material_costs_summary_v1 rms ON (((rms.odoo_instance = r.odoo_instance) AND (rms.odoo_production_id = r.odoo_production_id)))) - ), with_params AS ( - SELECT j.order_row_id, - j.tenant, - j.site, - j.odoo_instance, - j.odoo_workorder_id, - j.odoo_production_id, - j.odoo_order_ref, - j.odoo_state, - j.machine_id, - j.asset, - j.edge_product, - j.start_ts, - j.end_ts, - j.window_min, - j.run_min, - j.stop_min, - j.stop_cost_eur, - j.real_cycle_delta, - j.kwh_consumed, - j.kwh_per_cycle, - j.technical_quality_level, - j.final_report_status, - j.odoo_origin, - j.sale_order_ref, - j.customer_id, - j.customer_name, - j.sale_state, - j.sale_date_order, - j.currency, - j.product_id, - j.product_default_code, - j.product_name, - j.production_qty, - j.qty_producing, - j.production_uom, - j.sale_line_id, - j.sale_order_id, - j.sale_line_qty, - j.sale_line_qty_delivered, - j.sale_price_unit, - j.sale_discount_pct, - j.revenue_net_eur, - j.revenue_total_eur, - j.product_standard_price, - j.product_list_price, - j.raw_material_value_eur, - j.finished_value_eur, - j.odoo_material_cost_status, - j.commercial_snapshot_at, - j.raw_material_line_count, - j.raw_total_consumed_qty, - j.raw_materials_summary, - j.raw_material_cost_estimated_eur, - j.costed_raw_material_line_count, - j.missing_or_invalid_raw_material_cost_count, - j.raw_material_cost_status_summary, - p.id AS cost_parameter_id, - p.scope_level AS cost_parameter_scope_level, - p.energy_cost_eur_per_kwh, - p.material_cost_eur_per_kg AS fallback_material_cost_eur_per_kg, - p.machine_run_cost_eur_per_h, - p.labor_cost_eur_per_h, - p.overhead_cost_eur_per_h, - p.notes AS cost_parameter_notes - FROM (joined j - LEFT JOIN LATERAL ( SELECT p_1.id, - p_1.tenant, - p_1.site, - p_1.scope_level, - p_1.machine_id, - p_1.product_default_code, - p_1.energy_cost_eur_per_kwh, - p_1.material_cost_eur_per_kg, - p_1.machine_run_cost_eur_per_h, - p_1.labor_cost_eur_per_h, - p_1.overhead_cost_eur_per_h, - p_1.valid_from, - p_1.valid_to, - p_1.is_active, - p_1.notes, - p_1.created_at - FROM mv_edge_oee_ucepsa_demo.margin_cost_parameters p_1 - WHERE ((p_1.is_active = true) AND (p_1.tenant = j.tenant) AND (p_1.site = j.site) AND (p_1.valid_from <= now()) AND ((p_1.valid_to IS NULL) OR (p_1.valid_to > now())) AND ((p_1.machine_id IS NULL) OR (p_1.machine_id = j.machine_id)) AND ((p_1.product_default_code IS NULL) OR (p_1.product_default_code = j.product_default_code))) - ORDER BY - CASE - WHEN ((p_1.product_default_code IS NOT NULL) AND (p_1.machine_id IS NOT NULL)) THEN 1 - WHEN (p_1.product_default_code IS NOT NULL) THEN 2 - WHEN (p_1.machine_id IS NOT NULL) THEN 3 - ELSE 4 - END, p_1.valid_from DESC, p_1.id DESC - LIMIT 1) p ON (true)) - ), costs AS ( - SELECT with_params.order_row_id, - with_params.tenant, - with_params.site, - with_params.odoo_instance, - with_params.odoo_workorder_id, - with_params.odoo_production_id, - with_params.odoo_order_ref, - with_params.odoo_state, - with_params.machine_id, - with_params.asset, - with_params.edge_product, - with_params.start_ts, - with_params.end_ts, - with_params.window_min, - with_params.run_min, - with_params.stop_min, - with_params.stop_cost_eur, - with_params.real_cycle_delta, - with_params.kwh_consumed, - with_params.kwh_per_cycle, - with_params.technical_quality_level, - with_params.final_report_status, - with_params.odoo_origin, - with_params.sale_order_ref, - with_params.customer_id, - with_params.customer_name, - with_params.sale_state, - with_params.sale_date_order, - with_params.currency, - with_params.product_id, - with_params.product_default_code, - with_params.product_name, - with_params.production_qty, - with_params.qty_producing, - with_params.production_uom, - with_params.sale_line_id, - with_params.sale_order_id, - with_params.sale_line_qty, - with_params.sale_line_qty_delivered, - with_params.sale_price_unit, - with_params.sale_discount_pct, - with_params.revenue_net_eur, - with_params.revenue_total_eur, - with_params.product_standard_price, - with_params.product_list_price, - with_params.raw_material_value_eur, - with_params.finished_value_eur, - with_params.odoo_material_cost_status, - with_params.commercial_snapshot_at, - with_params.raw_material_line_count, - with_params.raw_total_consumed_qty, - with_params.raw_materials_summary, - with_params.raw_material_cost_estimated_eur, - with_params.costed_raw_material_line_count, - with_params.missing_or_invalid_raw_material_cost_count, - with_params.raw_material_cost_status_summary, - with_params.cost_parameter_id, - with_params.cost_parameter_scope_level, - with_params.energy_cost_eur_per_kwh, - with_params.fallback_material_cost_eur_per_kg, - with_params.machine_run_cost_eur_per_h, - with_params.labor_cost_eur_per_h, - with_params.overhead_cost_eur_per_h, - with_params.cost_parameter_notes, - COALESCE(NULLIF(with_params.qty_producing, (0)::numeric), NULLIF(with_params.production_qty, (0)::numeric)) AS costing_qty_kg, - CASE - WHEN ((with_params.raw_material_line_count > 0) AND (COALESCE(with_params.missing_or_invalid_raw_material_cost_count, (0)::bigint) = 0) AND (with_params.raw_material_cost_estimated_eur IS NOT NULL)) THEN with_params.raw_material_cost_estimated_eur - WHEN ((with_params.raw_material_line_count > 0) AND (COALESCE(with_params.missing_or_invalid_raw_material_cost_count, (0)::bigint) > 0)) THEN NULL::numeric - WHEN ((with_params.raw_material_value_eur IS NOT NULL) AND (with_params.raw_material_value_eur <> (0)::numeric)) THEN abs(with_params.raw_material_value_eur) - WHEN ((with_params.product_standard_price IS NOT NULL) AND (with_params.product_standard_price <> (0)::numeric) AND (COALESCE(NULLIF(with_params.qty_producing, (0)::numeric), NULLIF(with_params.production_qty, (0)::numeric)) IS NOT NULL)) THEN (with_params.product_standard_price * COALESCE(NULLIF(with_params.qty_producing, (0)::numeric), NULLIF(with_params.production_qty, (0)::numeric))) - WHEN ((with_params.fallback_material_cost_eur_per_kg IS NOT NULL) AND (COALESCE(NULLIF(with_params.qty_producing, (0)::numeric), NULLIF(with_params.production_qty, (0)::numeric)) IS NOT NULL)) THEN (with_params.fallback_material_cost_eur_per_kg * COALESCE(NULLIF(with_params.qty_producing, (0)::numeric), NULLIF(with_params.production_qty, (0)::numeric))) - ELSE NULL::numeric - END AS material_cost_estimated_eur, - CASE - WHEN ((with_params.raw_material_line_count > 0) AND (COALESCE(with_params.missing_or_invalid_raw_material_cost_count, (0)::bigint) = 0)) THEN 'FROM_RAW_MATERIAL_COSTS'::text - WHEN ((with_params.raw_material_line_count > 0) AND (COALESCE(with_params.missing_or_invalid_raw_material_cost_count, (0)::bigint) > 0)) THEN with_params.raw_material_cost_status_summary - WHEN ((with_params.raw_material_value_eur IS NOT NULL) AND (with_params.raw_material_value_eur <> (0)::numeric)) THEN 'FROM_ODOO_RAW_MATERIAL_VALUE'::text - WHEN ((with_params.product_standard_price IS NOT NULL) AND (with_params.product_standard_price <> (0)::numeric)) THEN 'FROM_PRODUCT_STANDARD_PRICE'::text - WHEN (with_params.fallback_material_cost_eur_per_kg IS NOT NULL) THEN 'FROM_FALLBACK_MARGIN_PARAMETER'::text - ELSE 'MISSING_MATERIAL_COST'::text - END AS material_cost_source_status, - CASE - WHEN ((with_params.kwh_consumed IS NOT NULL) AND (with_params.energy_cost_eur_per_kwh IS NOT NULL)) THEN (with_params.kwh_consumed * with_params.energy_cost_eur_per_kwh) - ELSE NULL::numeric - END AS energy_cost_estimated_eur, - CASE - WHEN ((with_params.run_min IS NOT NULL) AND (with_params.machine_run_cost_eur_per_h IS NOT NULL)) THEN ((with_params.run_min / 60.0) * with_params.machine_run_cost_eur_per_h) - ELSE NULL::numeric - END AS machine_run_cost_estimated_eur, - CASE - WHEN ((with_params.window_min IS NOT NULL) AND (with_params.labor_cost_eur_per_h IS NOT NULL)) THEN ((with_params.window_min / 60.0) * with_params.labor_cost_eur_per_h) - ELSE NULL::numeric - END AS labor_cost_estimated_eur, - CASE - WHEN ((with_params.window_min IS NOT NULL) AND (with_params.overhead_cost_eur_per_h IS NOT NULL)) THEN ((with_params.window_min / 60.0) * with_params.overhead_cost_eur_per_h) - ELSE NULL::numeric - END AS overhead_cost_estimated_eur - FROM with_params - ) - SELECT order_row_id, - tenant, - site, - odoo_instance, - odoo_workorder_id, - odoo_production_id, - odoo_order_ref, - odoo_state, - odoo_origin, - sale_order_ref, - customer_id, - customer_name, - sale_state, - sale_date_order, - currency, - machine_id, - asset, - product_id, - product_default_code, - COALESCE(product_name, edge_product) AS product_name, - production_qty, - qty_producing, - production_uom, - costing_qty_kg, - sale_line_id, - sale_order_id, - sale_line_qty, - sale_line_qty_delivered, - sale_price_unit, - sale_discount_pct, - revenue_net_eur, - revenue_total_eur, - start_ts, - end_ts, - window_min, - run_min, - stop_min, - stop_cost_eur, - real_cycle_delta, - kwh_consumed, - kwh_per_cycle, - product_standard_price, - product_list_price, - raw_material_value_eur, - finished_value_eur, - odoo_material_cost_status, - raw_material_line_count, - raw_total_consumed_qty, - raw_materials_summary, - raw_material_cost_status_summary, - costed_raw_material_line_count, - missing_or_invalid_raw_material_cost_count, - cost_parameter_id, - cost_parameter_scope_level, - energy_cost_eur_per_kwh, - fallback_material_cost_eur_per_kg, - machine_run_cost_eur_per_h, - labor_cost_eur_per_h, - overhead_cost_eur_per_h, - round(material_cost_estimated_eur, 4) AS material_cost_estimated_eur, - material_cost_source_status, - round(energy_cost_estimated_eur, 4) AS energy_cost_estimated_eur, - round(machine_run_cost_estimated_eur, 4) AS machine_run_cost_estimated_eur, - round(labor_cost_estimated_eur, 4) AS labor_cost_estimated_eur, - round(overhead_cost_estimated_eur, 4) AS overhead_cost_estimated_eur, - round((((((COALESCE(material_cost_estimated_eur, (0)::numeric) + COALESCE(energy_cost_estimated_eur, (0)::numeric)) + COALESCE(machine_run_cost_estimated_eur, (0)::numeric)) + COALESCE(labor_cost_estimated_eur, (0)::numeric)) + COALESCE(overhead_cost_estimated_eur, (0)::numeric)) + COALESCE(stop_cost_eur, (0)::numeric)), 4) AS known_or_estimated_cost_eur, - round( - CASE - WHEN (revenue_net_eur IS NOT NULL) THEN (revenue_net_eur - (((((COALESCE(material_cost_estimated_eur, (0)::numeric) + COALESCE(energy_cost_estimated_eur, (0)::numeric)) + COALESCE(machine_run_cost_estimated_eur, (0)::numeric)) + COALESCE(labor_cost_estimated_eur, (0)::numeric)) + COALESCE(overhead_cost_estimated_eur, (0)::numeric)) + COALESCE(stop_cost_eur, (0)::numeric))) - ELSE NULL::numeric - END, 4) AS partial_margin_after_known_costs_eur, - round( - CASE - WHEN ((revenue_net_eur IS NOT NULL) AND (material_cost_estimated_eur IS NOT NULL)) THEN (revenue_net_eur - (((((COALESCE(material_cost_estimated_eur, (0)::numeric) + COALESCE(energy_cost_estimated_eur, (0)::numeric)) + COALESCE(machine_run_cost_estimated_eur, (0)::numeric)) + COALESCE(labor_cost_estimated_eur, (0)::numeric)) + COALESCE(overhead_cost_estimated_eur, (0)::numeric)) + COALESCE(stop_cost_eur, (0)::numeric))) - ELSE NULL::numeric - END, 4) AS estimated_margin_eur, - round( - CASE - WHEN ((revenue_net_eur IS NOT NULL) AND (revenue_net_eur <> (0)::numeric) AND (material_cost_estimated_eur IS NOT NULL)) THEN (((revenue_net_eur - (((((COALESCE(material_cost_estimated_eur, (0)::numeric) + COALESCE(energy_cost_estimated_eur, (0)::numeric)) + COALESCE(machine_run_cost_estimated_eur, (0)::numeric)) + COALESCE(labor_cost_estimated_eur, (0)::numeric)) + COALESCE(overhead_cost_estimated_eur, (0)::numeric)) + COALESCE(stop_cost_eur, (0)::numeric))) / revenue_net_eur) * 100.0) - ELSE NULL::numeric - END, 2) AS estimated_margin_pct, - CASE - WHEN (revenue_net_eur IS NULL) THEN 'NO_REVENUE_LINKED'::text - ELSE 'REVENUE_LINKED'::text - END AS revenue_status, - CASE - WHEN ((raw_material_line_count > 0) AND (COALESCE(missing_or_invalid_raw_material_cost_count, (0)::bigint) > 0)) THEN 'MISSING_RAW_MATERIAL_COST_PARAMETER'::text - WHEN (material_cost_estimated_eur IS NULL) THEN 'MISSING_MATERIAL_COST'::text - WHEN (energy_cost_eur_per_kwh IS NULL) THEN 'MISSING_ENERGY_COST'::text - WHEN (machine_run_cost_eur_per_h IS NULL) THEN 'MISSING_MACHINE_RUN_COST'::text - ELSE 'COST_PARAMETERS_AVAILABLE'::text - END AS cost_status, - CASE - WHEN (revenue_net_eur IS NULL) THEN 'NO_MARGIN_WITHOUT_REVENUE'::text - WHEN ((raw_material_line_count > 0) AND (COALESCE(missing_or_invalid_raw_material_cost_count, (0)::bigint) > 0)) THEN 'PARTIAL_MARGIN_MISSING_RAW_MATERIAL_COST'::text - WHEN (material_cost_estimated_eur IS NULL) THEN 'PARTIAL_MARGIN_MISSING_MATERIAL'::text - ELSE 'ESTIMATED_MARGIN_AVAILABLE'::text - END AS margin_status, - technical_quality_level, - final_report_status, - commercial_snapshot_at - FROM costs; - - --- --- Name: cost_readiness_orders_v1; Type: VIEW; Schema: mv_reports_ucepsa_demo; Owner: - --- - -CREATE VIEW mv_reports_ucepsa_demo.cost_readiness_orders_v1 AS - WITH base AS ( - SELECT m.order_row_id, - m.tenant, - m.site, - m.odoo_instance, - m.odoo_workorder_id, - m.odoo_production_id, - m.odoo_order_ref, - m.odoo_state, - m.odoo_origin, - m.sale_order_ref, - m.customer_id, - m.customer_name, - m.sale_state, - m.sale_date_order, - m.currency, - m.machine_id, - m.asset, - m.product_id, - m.product_default_code, - m.product_name, - m.production_qty, - m.qty_producing, - m.production_uom, - m.costing_qty_kg, - m.sale_line_id, - m.sale_order_id, - m.sale_line_qty, - m.sale_line_qty_delivered, - m.sale_price_unit, - m.sale_discount_pct, - m.revenue_net_eur, - m.revenue_total_eur, - m.start_ts, - m.end_ts, - m.window_min, - m.run_min, - m.stop_min, - m.stop_cost_eur, - m.real_cycle_delta, - m.kwh_consumed, - m.kwh_per_cycle, - m.product_standard_price, - m.product_list_price, - m.raw_material_value_eur, - m.finished_value_eur, - m.odoo_material_cost_status, - m.raw_material_line_count, - m.raw_total_consumed_qty, - m.raw_materials_summary, - m.raw_material_cost_status_summary, - m.costed_raw_material_line_count, - m.missing_or_invalid_raw_material_cost_count, - m.cost_parameter_id, - m.cost_parameter_scope_level, - m.energy_cost_eur_per_kwh, - m.fallback_material_cost_eur_per_kg, - m.machine_run_cost_eur_per_h, - m.labor_cost_eur_per_h, - m.overhead_cost_eur_per_h, - m.material_cost_estimated_eur, - m.material_cost_source_status, - m.energy_cost_estimated_eur, - m.machine_run_cost_estimated_eur, - m.labor_cost_estimated_eur, - m.overhead_cost_estimated_eur, - m.known_or_estimated_cost_eur, - m.partial_margin_after_known_costs_eur, - m.estimated_margin_eur, - m.estimated_margin_pct, - m.revenue_status, - m.cost_status, - m.margin_status, - m.technical_quality_level, - m.final_report_status, - m.commercial_snapshot_at, - CASE - WHEN (m.revenue_net_eur IS NOT NULL) THEN true - ELSE false - END AS has_revenue, - CASE - WHEN (NULLIF(TRIM(BOTH FROM COALESCE(m.customer_name, ''::text)), ''::text) IS NOT NULL) THEN true - ELSE false - END AS has_customer, - CASE - WHEN (NULLIF(TRIM(BOTH FROM COALESCE(m.sale_order_ref, ''::text)), ''::text) IS NOT NULL) THEN true - ELSE false - END AS has_sale_order, - CASE - WHEN (COALESCE(m.raw_material_line_count, (0)::bigint) > 0) THEN true - ELSE false - END AS has_raw_materials, - CASE - WHEN ((COALESCE(m.raw_material_line_count, (0)::bigint) > 0) AND (COALESCE(m.missing_or_invalid_raw_material_cost_count, (0)::bigint) = 0) AND (m.material_cost_estimated_eur IS NOT NULL)) THEN true - ELSE false - END AS has_raw_material_cost, - CASE - WHEN (m.technical_quality_level = ANY (ARRAY['FIABLE'::text, 'ACEPTABLE_CON_OBSERVACIONES'::text])) THEN true - ELSE false - END AS has_usable_technical_data, - CASE - WHEN (m.final_report_status = ANY (ARRAY['LISTO_INFORME_FINAL'::text, 'LISTO_TECNICO_REVISAR_CONVERSION'::text, 'LISTO_CON_OBSERVACIONES'::text])) THEN true - ELSE false - END AS has_closed_or_usable_order_report, - CASE - WHEN ((m.kwh_consumed IS NULL) OR (m.energy_cost_eur_per_kwh IS NOT NULL)) THEN true - ELSE false - END AS has_energy_cost_parameter, - CASE - WHEN ((m.run_min IS NULL) OR (m.machine_run_cost_eur_per_h IS NOT NULL)) THEN true - ELSE false - END AS has_machine_cost_parameter - FROM mv_reports_ucepsa_demo.order_margin_report_v1 m - ), scored AS ( - SELECT b.order_row_id, - b.tenant, - b.site, - b.odoo_instance, - b.odoo_workorder_id, - b.odoo_production_id, - b.odoo_order_ref, - b.odoo_state, - b.odoo_origin, - b.sale_order_ref, - b.customer_id, - b.customer_name, - b.sale_state, - b.sale_date_order, - b.currency, - b.machine_id, - b.asset, - b.product_id, - b.product_default_code, - b.product_name, - b.production_qty, - b.qty_producing, - b.production_uom, - b.costing_qty_kg, - b.sale_line_id, - b.sale_order_id, - b.sale_line_qty, - b.sale_line_qty_delivered, - b.sale_price_unit, - b.sale_discount_pct, - b.revenue_net_eur, - b.revenue_total_eur, - b.start_ts, - b.end_ts, - b.window_min, - b.run_min, - b.stop_min, - b.stop_cost_eur, - b.real_cycle_delta, - b.kwh_consumed, - b.kwh_per_cycle, - b.product_standard_price, - b.product_list_price, - b.raw_material_value_eur, - b.finished_value_eur, - b.odoo_material_cost_status, - b.raw_material_line_count, - b.raw_total_consumed_qty, - b.raw_materials_summary, - b.raw_material_cost_status_summary, - b.costed_raw_material_line_count, - b.missing_or_invalid_raw_material_cost_count, - b.cost_parameter_id, - b.cost_parameter_scope_level, - b.energy_cost_eur_per_kwh, - b.fallback_material_cost_eur_per_kg, - b.machine_run_cost_eur_per_h, - b.labor_cost_eur_per_h, - b.overhead_cost_eur_per_h, - b.material_cost_estimated_eur, - b.material_cost_source_status, - b.energy_cost_estimated_eur, - b.machine_run_cost_estimated_eur, - b.labor_cost_estimated_eur, - b.overhead_cost_estimated_eur, - b.known_or_estimated_cost_eur, - b.partial_margin_after_known_costs_eur, - b.estimated_margin_eur, - b.estimated_margin_pct, - b.revenue_status, - b.cost_status, - b.margin_status, - b.technical_quality_level, - b.final_report_status, - b.commercial_snapshot_at, - b.has_revenue, - b.has_customer, - b.has_sale_order, - b.has_raw_materials, - b.has_raw_material_cost, - b.has_usable_technical_data, - b.has_closed_or_usable_order_report, - b.has_energy_cost_parameter, - b.has_machine_cost_parameter, - GREATEST(0, (((((((((100 - - CASE - WHEN (b.has_revenue = false) THEN 35 - ELSE 0 - END) - - CASE - WHEN (b.has_customer = false) THEN 5 - ELSE 0 - END) - - CASE - WHEN (b.has_sale_order = false) THEN 5 - ELSE 0 - END) - - CASE - WHEN (b.has_usable_technical_data = false) THEN 25 - ELSE 0 - END) - - CASE - WHEN (b.has_closed_or_usable_order_report = false) THEN 15 - ELSE 0 - END) - - CASE - WHEN (b.has_raw_materials = false) THEN 20 - ELSE 0 - END) - - CASE - WHEN ((b.has_raw_materials = true) AND (b.has_raw_material_cost = false)) THEN 35 - ELSE 0 - END) - - CASE - WHEN (b.has_energy_cost_parameter = false) THEN 5 - ELSE 0 - END) - - CASE - WHEN (b.has_machine_cost_parameter = false) THEN 5 - ELSE 0 - END)) AS cost_readiness_score, - concat_ws('; '::text, - CASE - WHEN (b.has_revenue = false) THEN 'Falta ingreso de venta vinculado'::text - ELSE NULL::text - END, - CASE - WHEN (b.has_customer = false) THEN 'Falta cliente'::text - ELSE NULL::text - END, - CASE - WHEN (b.has_sale_order = false) THEN 'Falta pedido de venta'::text - ELSE NULL::text - END, - CASE - WHEN (b.has_usable_technical_data = false) THEN 'Datos técnicos no defendibles'::text - ELSE NULL::text - END, - CASE - WHEN (b.has_closed_or_usable_order_report = false) THEN 'Informe técnico parcial o bloqueado'::text - ELSE NULL::text - END, - CASE - WHEN (b.has_raw_materials = false) THEN 'No se detecta materia prima consumida'::text - ELSE NULL::text - END, - CASE - WHEN ((b.has_raw_materials = true) AND (b.has_raw_material_cost = false)) THEN 'Falta coste de materia prima'::text - ELSE NULL::text - END, - CASE - WHEN (b.has_energy_cost_parameter = false) THEN 'Falta coste €/kWh'::text - ELSE NULL::text - END, - CASE - WHEN (b.has_machine_cost_parameter = false) THEN 'Falta coste hora máquina'::text - ELSE NULL::text - END) AS cost_readiness_blockers - FROM base b - ) - SELECT order_row_id, - tenant, - site, - odoo_instance, - odoo_order_ref, - odoo_state, - sale_order_ref, - customer_name, - product_default_code, - product_name, - machine_id, - revenue_net_eur, - revenue_total_eur, - raw_material_line_count, - raw_total_consumed_qty, - raw_materials_summary, - raw_material_cost_status_summary, - missing_or_invalid_raw_material_cost_count, - material_cost_estimated_eur, - material_cost_source_status, - stop_cost_eur, - kwh_consumed, - run_min, - window_min, - technical_quality_level, - final_report_status, - margin_status, - has_revenue, - has_customer, - has_sale_order, - has_raw_materials, - has_raw_material_cost, - has_usable_technical_data, - has_closed_or_usable_order_report, - has_energy_cost_parameter, - has_machine_cost_parameter, - cost_readiness_score, - CASE - WHEN (has_revenue = false) THEN 'BLOQUEADO_SIN_INGRESO'::text - WHEN (has_usable_technical_data = false) THEN 'BLOQUEADO_DATOS_TECNICOS'::text - WHEN (has_closed_or_usable_order_report = false) THEN 'PARCIAL_INFORME_TECNICO'::text - WHEN (has_raw_materials = false) THEN 'BLOQUEADO_SIN_MATERIA_PRIMA'::text - WHEN ((has_raw_materials = true) AND (has_raw_material_cost = false)) THEN 'BLOQUEADO_FALTA_COSTE_MATERIA_PRIMA'::text - WHEN ((has_energy_cost_parameter = false) OR (has_machine_cost_parameter = false)) THEN 'PARCIAL_FALTAN_COSTES_OPERATIVOS'::text - ELSE 'LISTO_PARA_MARGEN'::text - END AS cost_readiness_status, - CASE - WHEN (cost_readiness_blockers = ''::text) THEN 'Orden lista para cálculo de margen estimado.'::text - ELSE cost_readiness_blockers - END AS cost_readiness_description - FROM scored; - - --- --- Name: cost_readiness_raw_materials_v1; Type: VIEW; Schema: mv_reports_ucepsa_demo; Owner: - --- - -CREATE VIEW mv_reports_ucepsa_demo.cost_readiness_raw_materials_v1 AS - SELECT tenant, - site, - raw_product_default_code, - raw_product_name, - raw_uom, - count(DISTINCT odoo_order_ref) AS affected_order_count, - string_agg(DISTINCT odoo_order_ref, ', '::text ORDER BY odoo_order_ref) AS affected_orders, - string_agg(DISTINCT COALESCE(sale_order_ref, 'sin_pedido_venta'::text), ', '::text ORDER BY COALESCE(sale_order_ref, 'sin_pedido_venta'::text)) AS affected_sale_orders, - string_agg(DISTINCT COALESCE(customer_name, 'sin_cliente'::text), ', '::text ORDER BY COALESCE(customer_name, 'sin_cliente'::text)) AS affected_customers, - round(sum(COALESCE(consumed_qty, (0)::numeric)), 3) AS total_consumed_qty, - count(*) AS raw_material_line_count, - count(*) FILTER (WHERE (material_cost_status = 'MISSING_RAW_MATERIAL_COST_PARAMETER'::text)) AS missing_cost_line_count, - max(material_cost_eur_per_kg) AS current_material_cost_eur_per_kg, - CASE - WHEN (count(*) FILTER (WHERE (material_cost_status = 'MISSING_RAW_MATERIAL_COST_PARAMETER'::text)) > 0) THEN 'PENDIENTE_COSTE_MATERIA_PRIMA'::text - ELSE 'COSTE_DISPONIBLE'::text - END AS raw_material_readiness_status, - CASE - WHEN (count(*) FILTER (WHERE (material_cost_status = 'MISSING_RAW_MATERIAL_COST_PARAMETER'::text)) > 0) THEN concat('Falta coste €/kg para ', COALESCE(raw_product_default_code, 'sin_codigo'::text), ' - ', COALESCE(raw_product_name, 'sin_nombre'::text)) - ELSE 'Coste de materia prima disponible.'::text - END AS recommended_action - FROM mv_reports_ucepsa_demo.order_raw_material_costs_v1 - GROUP BY tenant, site, raw_product_default_code, raw_product_name, raw_uom; - - --- --- Name: cost_readiness_summary_v1; Type: VIEW; Schema: mv_reports_ucepsa_demo; Owner: - --- - -CREATE VIEW mv_reports_ucepsa_demo.cost_readiness_summary_v1 AS - SELECT tenant, - site, - count(*) AS total_orders, - count(*) FILTER (WHERE (has_revenue = true)) AS orders_with_revenue, - count(*) FILTER (WHERE (has_revenue = false)) AS orders_without_revenue, - count(*) FILTER (WHERE (has_raw_materials = true)) AS orders_with_raw_materials, - count(*) FILTER (WHERE (has_raw_materials = false)) AS orders_without_raw_materials, - count(*) FILTER (WHERE ((has_raw_materials = true) AND (has_raw_material_cost = false))) AS orders_missing_raw_material_cost, - count(*) FILTER (WHERE (has_usable_technical_data = false)) AS orders_with_bad_technical_data, - count(*) FILTER (WHERE (cost_readiness_status = 'LISTO_PARA_MARGEN'::text)) AS orders_ready_for_margin, - count(*) FILTER (WHERE (cost_readiness_status = 'BLOQUEADO_FALTA_COSTE_MATERIA_PRIMA'::text)) AS orders_blocked_missing_raw_material_cost, - count(*) FILTER (WHERE (cost_readiness_status = 'BLOQUEADO_SIN_INGRESO'::text)) AS orders_blocked_no_revenue, - round(avg(cost_readiness_score), 2) AS avg_cost_readiness_score, - min(cost_readiness_score) AS min_cost_readiness_score, - max(cost_readiness_score) AS max_cost_readiness_score - FROM mv_reports_ucepsa_demo.cost_readiness_orders_v1 - GROUP BY tenant, site; - - --- --- Name: margin_cost_parameters id; Type: DEFAULT; Schema: mv_edge_oee_ucepsa_demo; Owner: - --- - -ALTER TABLE ONLY mv_edge_oee_ucepsa_demo.margin_cost_parameters ALTER COLUMN id SET DEFAULT nextval('mv_edge_oee_ucepsa_demo.margin_cost_parameters_id_seq'::regclass); - - --- --- Name: odoo_order_commercials id; Type: DEFAULT; Schema: mv_edge_oee_ucepsa_demo; Owner: - --- - -ALTER TABLE ONLY mv_edge_oee_ucepsa_demo.odoo_order_commercials ALTER COLUMN id SET DEFAULT nextval('mv_edge_oee_ucepsa_demo.odoo_order_commercials_id_seq'::regclass); - - --- --- Name: raw_material_cost_parameters id; Type: DEFAULT; Schema: mv_edge_oee_ucepsa_demo; Owner: - --- - -ALTER TABLE ONLY mv_edge_oee_ucepsa_demo.raw_material_cost_parameters ALTER COLUMN id SET DEFAULT nextval('mv_edge_oee_ucepsa_demo.raw_material_cost_parameters_id_seq'::regclass); - - --- --- Name: margin_cost_parameters margin_cost_parameters_pkey; Type: CONSTRAINT; Schema: mv_edge_oee_ucepsa_demo; Owner: - --- - -ALTER TABLE ONLY mv_edge_oee_ucepsa_demo.margin_cost_parameters - ADD CONSTRAINT margin_cost_parameters_pkey PRIMARY KEY (id); - - --- --- Name: odoo_order_commercials odoo_order_commercials_pkey; Type: CONSTRAINT; Schema: mv_edge_oee_ucepsa_demo; Owner: - --- - -ALTER TABLE ONLY mv_edge_oee_ucepsa_demo.odoo_order_commercials - ADD CONSTRAINT odoo_order_commercials_pkey PRIMARY KEY (id); - - --- --- Name: raw_material_cost_parameters raw_material_cost_parameters_pkey; Type: CONSTRAINT; Schema: mv_edge_oee_ucepsa_demo; Owner: - --- - -ALTER TABLE ONLY mv_edge_oee_ucepsa_demo.raw_material_cost_parameters - ADD CONSTRAINT raw_material_cost_parameters_pkey PRIMARY KEY (id); - - --- --- Name: odoo_order_commercials ux_odoo_order_commercials_instance_production; Type: CONSTRAINT; Schema: mv_edge_oee_ucepsa_demo; Owner: - --- - -ALTER TABLE ONLY mv_edge_oee_ucepsa_demo.odoo_order_commercials - ADD CONSTRAINT ux_odoo_order_commercials_instance_production UNIQUE (odoo_instance, odoo_production_id); - - --- --- Name: idx_margin_cost_parameters_scope; Type: INDEX; Schema: mv_edge_oee_ucepsa_demo; Owner: - --- - -CREATE INDEX idx_margin_cost_parameters_scope ON mv_edge_oee_ucepsa_demo.margin_cost_parameters USING btree (tenant, site, is_active, machine_id, product_default_code); - - --- --- Name: idx_odoo_order_commercials_customer; Type: INDEX; Schema: mv_edge_oee_ucepsa_demo; Owner: - --- - -CREATE INDEX idx_odoo_order_commercials_customer ON mv_edge_oee_ucepsa_demo.odoo_order_commercials USING btree (customer_name); - - --- --- Name: idx_odoo_order_commercials_order_ref; Type: INDEX; Schema: mv_edge_oee_ucepsa_demo; Owner: - --- - -CREATE INDEX idx_odoo_order_commercials_order_ref ON mv_edge_oee_ucepsa_demo.odoo_order_commercials USING btree (odoo_order_ref); - - --- --- Name: idx_odoo_order_commercials_sale_order_ref; Type: INDEX; Schema: mv_edge_oee_ucepsa_demo; Owner: - --- - -CREATE INDEX idx_odoo_order_commercials_sale_order_ref ON mv_edge_oee_ucepsa_demo.odoo_order_commercials USING btree (sale_order_ref); - - --- --- Name: idx_raw_material_cost_parameters_lookup; Type: INDEX; Schema: mv_edge_oee_ucepsa_demo; Owner: - --- - -CREATE INDEX idx_raw_material_cost_parameters_lookup ON mv_edge_oee_ucepsa_demo.raw_material_cost_parameters USING btree (tenant, site, raw_product_default_code, is_active, valid_from DESC); - - --- --- PostgreSQL database dump complete --- - -\unrestrict prs5NerySlAyEA4mTL6Sg3Uqx00yOGe9h66uyBfmfApVdToFu1LKFfY74XjwGca - - --- Read-only grants for Grafana / reporting API user -GRANT USAGE ON SCHEMA mv_edge_oee_ucepsa_demo TO grafana_ucepsa_ro; -GRANT USAGE ON SCHEMA mv_reports_ucepsa_demo TO grafana_ucepsa_ro; - -GRANT SELECT ON mv_edge_oee_ucepsa_demo.odoo_order_commercials TO grafana_ucepsa_ro; -GRANT SELECT ON mv_edge_oee_ucepsa_demo.margin_cost_parameters TO grafana_ucepsa_ro; -GRANT SELECT ON mv_edge_oee_ucepsa_demo.raw_material_cost_parameters TO grafana_ucepsa_ro; - -GRANT SELECT ON mv_reports_ucepsa_demo.order_raw_material_costs_v1 TO grafana_ucepsa_ro; -GRANT SELECT ON mv_reports_ucepsa_demo.order_raw_material_costs_summary_v1 TO grafana_ucepsa_ro; -GRANT SELECT ON mv_reports_ucepsa_demo.order_margin_report_v1 TO grafana_ucepsa_ro; - -GRANT SELECT ON mv_reports_ucepsa_demo.cost_readiness_orders_v1 TO grafana_ucepsa_ro; -GRANT SELECT ON mv_reports_ucepsa_demo.cost_readiness_raw_materials_v1 TO grafana_ucepsa_ro; -GRANT SELECT ON mv_reports_ucepsa_demo.cost_readiness_summary_v1 TO grafana_ucepsa_ro; diff --git a/ucepsa/edge-oee-demo/sql/reports/090_data_quality_contract.sql b/ucepsa/edge-oee-demo/sql/reports/090_data_quality_contract.sql deleted file mode 100644 index e58bd57..0000000 --- a/ucepsa/edge-oee-demo/sql/reports/090_data_quality_contract.sql +++ /dev/null @@ -1,1166 +0,0 @@ --- MESAVAULT Edge-OEE UCEPSA --- Data Quality Contract Odoo-MESAVAULT --- Schema + seed rules. --- No secrets. - -DROP VIEW IF EXISTS mv_reports_ucepsa_demo.data_quality_contract_owner_summary_v1; -DROP VIEW IF EXISTS mv_reports_ucepsa_demo.data_quality_contract_order_summary_v1; -DROP VIEW IF EXISTS mv_reports_ucepsa_demo.data_quality_contract_order_checks_v1; - -DROP TABLE IF EXISTS mv_edge_oee_ucepsa_demo.data_quality_contract_rules; - --- --- PostgreSQL database dump --- - -\restrict Jszr4ruwRiCXF4itba8oT9UnTRsL1bJGSsYnj9UgpfOpaiQUSblgKdozAITLZdr - --- Dumped from database version 16.14 --- Dumped by pg_dump version 16.14 - -SET statement_timeout = 0; -SET lock_timeout = 0; -SET idle_in_transaction_session_timeout = 0; -SET client_encoding = 'UTF8'; -SET standard_conforming_strings = on; -SELECT pg_catalog.set_config('search_path', '', false); -SET check_function_bodies = false; -SET xmloption = content; -SET client_min_messages = warning; -SET row_security = off; - -SET default_tablespace = ''; - -SET default_table_access_method = heap; - --- --- Name: data_quality_contract_rules; Type: TABLE; Schema: mv_edge_oee_ucepsa_demo; Owner: - --- - -CREATE TABLE mv_edge_oee_ucepsa_demo.data_quality_contract_rules ( - id bigint NOT NULL, - tenant text DEFAULT 'ucepsa'::text NOT NULL, - site text DEFAULT 'ucepsa_test'::text NOT NULL, - rule_code text NOT NULL, - rule_name text NOT NULL, - domain text NOT NULL, - dimension text NOT NULL, - severity text NOT NULL, - owner_area text NOT NULL, - applies_to text NOT NULL, - description text NOT NULL, - expected_condition text NOT NULL, - blocking_for text, - is_active boolean DEFAULT true NOT NULL, - created_at timestamp with time zone DEFAULT now() NOT NULL -); - - --- --- Name: data_quality_contract_rules_id_seq; Type: SEQUENCE; Schema: mv_edge_oee_ucepsa_demo; Owner: - --- - -CREATE SEQUENCE mv_edge_oee_ucepsa_demo.data_quality_contract_rules_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: data_quality_contract_rules_id_seq; Type: SEQUENCE OWNED BY; Schema: mv_edge_oee_ucepsa_demo; Owner: - --- - -ALTER SEQUENCE mv_edge_oee_ucepsa_demo.data_quality_contract_rules_id_seq OWNED BY mv_edge_oee_ucepsa_demo.data_quality_contract_rules.id; - - --- --- Name: data_quality_contract_order_checks_v1; Type: VIEW; Schema: mv_reports_ucepsa_demo; Owner: - --- - -CREATE VIEW mv_reports_ucepsa_demo.data_quality_contract_order_checks_v1 AS - WITH raw_stats AS ( - SELECT order_raw_material_costs_v1.odoo_instance, - order_raw_material_costs_v1.odoo_production_id, - count(*) AS raw_line_count, - count(*) FILTER (WHERE ((order_raw_material_costs_v1.raw_product_default_code IS NULL) OR (TRIM(BOTH FROM order_raw_material_costs_v1.raw_product_default_code) = ''::text))) AS raw_missing_code_count, - string_agg(DISTINCT concat(COALESCE(order_raw_material_costs_v1.raw_product_default_code, 'sin_codigo'::text), ' - ', COALESCE(order_raw_material_costs_v1.raw_product_name, 'sin_nombre'::text)), '; '::text ORDER BY (concat(COALESCE(order_raw_material_costs_v1.raw_product_default_code, 'sin_codigo'::text), ' - ', COALESCE(order_raw_material_costs_v1.raw_product_name, 'sin_nombre'::text)))) AS raw_materials_detected - FROM mv_reports_ucepsa_demo.order_raw_material_costs_v1 - GROUP BY order_raw_material_costs_v1.odoo_instance, order_raw_material_costs_v1.odoo_production_id - ), base AS ( - SELECT m.order_row_id, - m.tenant, - m.site, - m.odoo_instance, - m.odoo_workorder_id, - m.odoo_production_id, - m.odoo_order_ref, - m.odoo_state, - m.sale_order_ref, - m.customer_name, - m.revenue_net_eur, - m.machine_id, - m.asset, - m.product_default_code, - m.product_name, - m.raw_material_line_count, - m.raw_total_consumed_qty, - m.raw_materials_summary, - m.raw_material_cost_status_summary, - m.missing_or_invalid_raw_material_cost_count, - m.material_cost_estimated_eur, - m.material_cost_source_status, - m.energy_cost_eur_per_kwh, - m.machine_run_cost_eur_per_h, - m.kwh_consumed, - m.run_min, - m.stop_cost_eur, - m.technical_quality_level, - m.final_report_status, - m.margin_status, - r_1.pending_stops, - r_1.production_conversion_status, - COALESCE(rs.raw_line_count, (0)::bigint) AS dqc_raw_line_count, - COALESCE(rs.raw_missing_code_count, (0)::bigint) AS dqc_raw_missing_code_count, - rs.raw_materials_detected - FROM ((mv_reports_ucepsa_demo.order_margin_report_v1 m - LEFT JOIN mv_reports_ucepsa_demo.order_execution_report_full_v1 r_1 ON ((r_1.order_row_id = m.order_row_id))) - LEFT JOIN raw_stats rs ON (((rs.odoo_instance = m.odoo_instance) AND (rs.odoo_production_id = m.odoo_production_id)))) - ), evaluations AS ( - SELECT b.order_row_id, - b.tenant, - b.site, - b.odoo_instance, - b.odoo_workorder_id, - b.odoo_production_id, - b.odoo_order_ref, - b.odoo_state, - b.sale_order_ref, - b.customer_name, - b.revenue_net_eur, - b.machine_id, - b.asset, - b.product_default_code, - b.product_name, - b.raw_material_line_count, - b.raw_total_consumed_qty, - b.raw_materials_summary, - b.raw_material_cost_status_summary, - b.missing_or_invalid_raw_material_cost_count, - b.material_cost_estimated_eur, - b.material_cost_source_status, - b.energy_cost_eur_per_kwh, - b.machine_run_cost_eur_per_h, - b.kwh_consumed, - b.run_min, - b.stop_cost_eur, - b.technical_quality_level, - b.final_report_status, - b.margin_status, - b.pending_stops, - b.production_conversion_status, - b.dqc_raw_line_count, - b.dqc_raw_missing_code_count, - b.raw_materials_detected, - 'DQC-ODOO-001'::text AS rule_code, - CASE - WHEN ((b.odoo_order_ref IS NOT NULL) AND (b.odoo_production_id IS NOT NULL)) THEN 'PASS'::text - ELSE 'FAIL'::text - END AS check_status, - CASE - WHEN ((b.odoo_order_ref IS NOT NULL) AND (b.odoo_production_id IS NOT NULL)) THEN 'Orden de fabricación correctamente identificada.'::text - ELSE 'Falta referencia de orden o identificador de producción Odoo.'::text - END AS finding, - CASE - WHEN ((b.odoo_order_ref IS NOT NULL) AND (b.odoo_production_id IS NOT NULL)) THEN 'Sin acción.'::text - ELSE 'Revisar sincronización de mrp.production / mrp.workorder desde Odoo.'::text - END AS recommended_action, - jsonb_build_object('odoo_order_ref', b.odoo_order_ref, 'odoo_production_id', b.odoo_production_id) AS evidence_json - FROM base b - UNION ALL - SELECT b.order_row_id, - b.tenant, - b.site, - b.odoo_instance, - b.odoo_workorder_id, - b.odoo_production_id, - b.odoo_order_ref, - b.odoo_state, - b.sale_order_ref, - b.customer_name, - b.revenue_net_eur, - b.machine_id, - b.asset, - b.product_default_code, - b.product_name, - b.raw_material_line_count, - b.raw_total_consumed_qty, - b.raw_materials_summary, - b.raw_material_cost_status_summary, - b.missing_or_invalid_raw_material_cost_count, - b.material_cost_estimated_eur, - b.material_cost_source_status, - b.energy_cost_eur_per_kwh, - b.machine_run_cost_eur_per_h, - b.kwh_consumed, - b.run_min, - b.stop_cost_eur, - b.technical_quality_level, - b.final_report_status, - b.margin_status, - b.pending_stops, - b.production_conversion_status, - b.dqc_raw_line_count, - b.dqc_raw_missing_code_count, - b.raw_materials_detected, - 'DQC-ODOO-002'::text AS rule_code, - CASE - WHEN ((b.machine_id IS NOT NULL) AND (TRIM(BOTH FROM b.machine_id) <> ''::text)) THEN 'PASS'::text - ELSE 'FAIL'::text - END AS check_status, - CASE - WHEN ((b.machine_id IS NOT NULL) AND (TRIM(BOTH FROM b.machine_id) <> ''::text)) THEN 'Centro de trabajo mapeado a máquina MESAVAULT.'::text - ELSE 'Falta machine_id o mapeo de centro de trabajo.'::text - END AS finding, - CASE - WHEN ((b.machine_id IS NOT NULL) AND (TRIM(BOTH FROM b.machine_id) <> ''::text)) THEN 'Sin acción.'::text - ELSE 'Revisar tabla de mapeo Odoo workcenter → machine_id.'::text - END AS recommended_action, - jsonb_build_object('machine_id', b.machine_id, 'asset', b.asset) AS evidence_json - FROM base b - UNION ALL - SELECT b.order_row_id, - b.tenant, - b.site, - b.odoo_instance, - b.odoo_workorder_id, - b.odoo_production_id, - b.odoo_order_ref, - b.odoo_state, - b.sale_order_ref, - b.customer_name, - b.revenue_net_eur, - b.machine_id, - b.asset, - b.product_default_code, - b.product_name, - b.raw_material_line_count, - b.raw_total_consumed_qty, - b.raw_materials_summary, - b.raw_material_cost_status_summary, - b.missing_or_invalid_raw_material_cost_count, - b.material_cost_estimated_eur, - b.material_cost_source_status, - b.energy_cost_eur_per_kwh, - b.machine_run_cost_eur_per_h, - b.kwh_consumed, - b.run_min, - b.stop_cost_eur, - b.technical_quality_level, - b.final_report_status, - b.margin_status, - b.pending_stops, - b.production_conversion_status, - b.dqc_raw_line_count, - b.dqc_raw_missing_code_count, - b.raw_materials_detected, - 'DQC-ODOO-003'::text AS rule_code, - CASE - WHEN (COALESCE(NULLIF(TRIM(BOTH FROM b.product_name), ''::text), NULLIF(TRIM(BOTH FROM b.product_default_code), ''::text)) IS NULL) THEN 'FAIL'::text - WHEN ((b.product_default_code IS NULL) OR (TRIM(BOTH FROM b.product_default_code) = ''::text)) THEN 'WARN'::text - ELSE 'PASS'::text - END AS check_status, - CASE - WHEN (COALESCE(NULLIF(TRIM(BOTH FROM b.product_name), ''::text), NULLIF(TRIM(BOTH FROM b.product_default_code), ''::text)) IS NULL) THEN 'Falta identificación del producto.'::text - WHEN ((b.product_default_code IS NULL) OR (TRIM(BOTH FROM b.product_default_code) = ''::text)) THEN 'Producto identificado por nombre, pero sin código interno.'::text - ELSE 'Producto identificado con nombre y código interno.'::text - END AS finding, - CASE - WHEN (COALESCE(NULLIF(TRIM(BOTH FROM b.product_name), ''::text), NULLIF(TRIM(BOTH FROM b.product_default_code), ''::text)) IS NULL) THEN 'Revisar maestro de producto en Odoo.'::text - WHEN ((b.product_default_code IS NULL) OR (TRIM(BOTH FROM b.product_default_code) = ''::text)) THEN 'Completar código interno/default_code del producto en Odoo.'::text - ELSE 'Sin acción.'::text - END AS recommended_action, - jsonb_build_object('product_default_code', b.product_default_code, 'product_name', b.product_name) AS evidence_json - FROM base b - UNION ALL - SELECT b.order_row_id, - b.tenant, - b.site, - b.odoo_instance, - b.odoo_workorder_id, - b.odoo_production_id, - b.odoo_order_ref, - b.odoo_state, - b.sale_order_ref, - b.customer_name, - b.revenue_net_eur, - b.machine_id, - b.asset, - b.product_default_code, - b.product_name, - b.raw_material_line_count, - b.raw_total_consumed_qty, - b.raw_materials_summary, - b.raw_material_cost_status_summary, - b.missing_or_invalid_raw_material_cost_count, - b.material_cost_estimated_eur, - b.material_cost_source_status, - b.energy_cost_eur_per_kwh, - b.machine_run_cost_eur_per_h, - b.kwh_consumed, - b.run_min, - b.stop_cost_eur, - b.technical_quality_level, - b.final_report_status, - b.margin_status, - b.pending_stops, - b.production_conversion_status, - b.dqc_raw_line_count, - b.dqc_raw_missing_code_count, - b.raw_materials_detected, - 'DQC-SALES-001'::text AS rule_code, - CASE - WHEN ((b.sale_order_ref IS NOT NULL) AND (b.revenue_net_eur IS NOT NULL)) THEN 'PASS'::text - ELSE 'FAIL'::text - END AS check_status, - CASE - WHEN ((b.sale_order_ref IS NOT NULL) AND (b.revenue_net_eur IS NOT NULL)) THEN 'Ingreso de venta vinculado a la orden.'::text - ELSE 'La orden no tiene ingreso de venta vinculado.'::text - END AS finding, - CASE - WHEN ((b.sale_order_ref IS NOT NULL) AND (b.revenue_net_eur IS NOT NULL)) THEN 'Sin acción.'::text - ELSE 'Revisar sale_line_id, pedido de venta u origen de la orden de fabricación.'::text - END AS recommended_action, - jsonb_build_object('sale_order_ref', b.sale_order_ref, 'revenue_net_eur', b.revenue_net_eur) AS evidence_json - FROM base b - UNION ALL - SELECT b.order_row_id, - b.tenant, - b.site, - b.odoo_instance, - b.odoo_workorder_id, - b.odoo_production_id, - b.odoo_order_ref, - b.odoo_state, - b.sale_order_ref, - b.customer_name, - b.revenue_net_eur, - b.machine_id, - b.asset, - b.product_default_code, - b.product_name, - b.raw_material_line_count, - b.raw_total_consumed_qty, - b.raw_materials_summary, - b.raw_material_cost_status_summary, - b.missing_or_invalid_raw_material_cost_count, - b.material_cost_estimated_eur, - b.material_cost_source_status, - b.energy_cost_eur_per_kwh, - b.machine_run_cost_eur_per_h, - b.kwh_consumed, - b.run_min, - b.stop_cost_eur, - b.technical_quality_level, - b.final_report_status, - b.margin_status, - b.pending_stops, - b.production_conversion_status, - b.dqc_raw_line_count, - b.dqc_raw_missing_code_count, - b.raw_materials_detected, - 'DQC-SALES-002'::text AS rule_code, - CASE - WHEN (b.sale_order_ref IS NULL) THEN 'NOT_APPLICABLE'::text - WHEN ((b.customer_name IS NOT NULL) AND (TRIM(BOTH FROM b.customer_name) <> ''::text)) THEN 'PASS'::text - ELSE 'FAIL'::text - END AS check_status, - CASE - WHEN (b.sale_order_ref IS NULL) THEN 'No aplica porque no hay pedido de venta vinculado.'::text - WHEN ((b.customer_name IS NOT NULL) AND (TRIM(BOTH FROM b.customer_name) <> ''::text)) THEN 'Cliente vinculado correctamente.'::text - ELSE 'Hay pedido de venta, pero falta cliente.'::text - END AS finding, - CASE - WHEN (b.sale_order_ref IS NULL) THEN 'Resolver primero el vínculo de venta.'::text - WHEN ((b.customer_name IS NOT NULL) AND (TRIM(BOTH FROM b.customer_name) <> ''::text)) THEN 'Sin acción.'::text - ELSE 'Revisar partner_id del pedido de venta en Odoo.'::text - END AS recommended_action, - jsonb_build_object('sale_order_ref', b.sale_order_ref, 'customer_name', b.customer_name) AS evidence_json - FROM base b - UNION ALL - SELECT b.order_row_id, - b.tenant, - b.site, - b.odoo_instance, - b.odoo_workorder_id, - b.odoo_production_id, - b.odoo_order_ref, - b.odoo_state, - b.sale_order_ref, - b.customer_name, - b.revenue_net_eur, - b.machine_id, - b.asset, - b.product_default_code, - b.product_name, - b.raw_material_line_count, - b.raw_total_consumed_qty, - b.raw_materials_summary, - b.raw_material_cost_status_summary, - b.missing_or_invalid_raw_material_cost_count, - b.material_cost_estimated_eur, - b.material_cost_source_status, - b.energy_cost_eur_per_kwh, - b.machine_run_cost_eur_per_h, - b.kwh_consumed, - b.run_min, - b.stop_cost_eur, - b.technical_quality_level, - b.final_report_status, - b.margin_status, - b.pending_stops, - b.production_conversion_status, - b.dqc_raw_line_count, - b.dqc_raw_missing_code_count, - b.raw_materials_detected, - 'DQC-RAW-001'::text AS rule_code, - CASE - WHEN (COALESCE(b.raw_material_line_count, (0)::bigint) > 0) THEN 'PASS'::text - ELSE 'FAIL'::text - END AS check_status, - CASE - WHEN (COALESCE(b.raw_material_line_count, (0)::bigint) > 0) THEN 'Materia prima consumida detectada.'::text - ELSE 'No se detecta materia prima consumida para la orden.'::text - END AS finding, - CASE - WHEN (COALESCE(b.raw_material_line_count, (0)::bigint) > 0) THEN 'Sin acción.'::text - ELSE 'Revisar movimientos de stock/MRP asociados a la orden.'::text - END AS recommended_action, - jsonb_build_object('raw_material_line_count', b.raw_material_line_count, 'raw_materials_summary', b.raw_materials_summary) AS evidence_json - FROM base b - UNION ALL - SELECT b.order_row_id, - b.tenant, - b.site, - b.odoo_instance, - b.odoo_workorder_id, - b.odoo_production_id, - b.odoo_order_ref, - b.odoo_state, - b.sale_order_ref, - b.customer_name, - b.revenue_net_eur, - b.machine_id, - b.asset, - b.product_default_code, - b.product_name, - b.raw_material_line_count, - b.raw_total_consumed_qty, - b.raw_materials_summary, - b.raw_material_cost_status_summary, - b.missing_or_invalid_raw_material_cost_count, - b.material_cost_estimated_eur, - b.material_cost_source_status, - b.energy_cost_eur_per_kwh, - b.machine_run_cost_eur_per_h, - b.kwh_consumed, - b.run_min, - b.stop_cost_eur, - b.technical_quality_level, - b.final_report_status, - b.margin_status, - b.pending_stops, - b.production_conversion_status, - b.dqc_raw_line_count, - b.dqc_raw_missing_code_count, - b.raw_materials_detected, - 'DQC-RAW-002'::text AS rule_code, - CASE - WHEN (COALESCE(b.dqc_raw_line_count, (0)::bigint) = 0) THEN 'NOT_APPLICABLE'::text - WHEN (COALESCE(b.dqc_raw_missing_code_count, (0)::bigint) = 0) THEN 'PASS'::text - ELSE 'FAIL'::text - END AS check_status, - CASE - WHEN (COALESCE(b.dqc_raw_line_count, (0)::bigint) = 0) THEN 'No aplica porque no hay materia prima detectada.'::text - WHEN (COALESCE(b.dqc_raw_missing_code_count, (0)::bigint) = 0) THEN 'Todas las materias primas detectadas tienen código identificable.'::text - ELSE 'Hay materias primas sin código identificable.'::text - END AS finding, - CASE - WHEN (COALESCE(b.dqc_raw_line_count, (0)::bigint) = 0) THEN 'Resolver primero la detección de materia prima.'::text - WHEN (COALESCE(b.dqc_raw_missing_code_count, (0)::bigint) = 0) THEN 'Sin acción.'::text - ELSE 'Completar código interno/default_code de materias primas en Odoo.'::text - END AS recommended_action, - jsonb_build_object('raw_line_count', b.dqc_raw_line_count, 'raw_missing_code_count', b.dqc_raw_missing_code_count, 'raw_materials_detected', b.raw_materials_detected) AS evidence_json - FROM base b - UNION ALL - SELECT b.order_row_id, - b.tenant, - b.site, - b.odoo_instance, - b.odoo_workorder_id, - b.odoo_production_id, - b.odoo_order_ref, - b.odoo_state, - b.sale_order_ref, - b.customer_name, - b.revenue_net_eur, - b.machine_id, - b.asset, - b.product_default_code, - b.product_name, - b.raw_material_line_count, - b.raw_total_consumed_qty, - b.raw_materials_summary, - b.raw_material_cost_status_summary, - b.missing_or_invalid_raw_material_cost_count, - b.material_cost_estimated_eur, - b.material_cost_source_status, - b.energy_cost_eur_per_kwh, - b.machine_run_cost_eur_per_h, - b.kwh_consumed, - b.run_min, - b.stop_cost_eur, - b.technical_quality_level, - b.final_report_status, - b.margin_status, - b.pending_stops, - b.production_conversion_status, - b.dqc_raw_line_count, - b.dqc_raw_missing_code_count, - b.raw_materials_detected, - 'DQC-COST-001'::text AS rule_code, - CASE - WHEN (b.material_cost_estimated_eur IS NOT NULL) THEN 'PASS'::text - ELSE 'FAIL'::text - END AS check_status, - CASE - WHEN (b.material_cost_estimated_eur IS NOT NULL) THEN 'Coste de materia prima disponible.'::text - ELSE 'Falta coste de materia prima.'::text - END AS finding, - CASE - WHEN (b.material_cost_estimated_eur IS NOT NULL) THEN 'Sin acción.'::text - ELSE COALESCE(concat('Cargar coste €/kg para materia prima: ', b.raw_materials_summary), 'Cargar coste de materia prima o revisar valoración en Odoo.'::text) - END AS recommended_action, - jsonb_build_object('material_cost_estimated_eur', b.material_cost_estimated_eur, 'material_cost_source_status', b.material_cost_source_status, 'raw_materials_summary', b.raw_materials_summary) AS evidence_json - FROM base b - UNION ALL - SELECT b.order_row_id, - b.tenant, - b.site, - b.odoo_instance, - b.odoo_workorder_id, - b.odoo_production_id, - b.odoo_order_ref, - b.odoo_state, - b.sale_order_ref, - b.customer_name, - b.revenue_net_eur, - b.machine_id, - b.asset, - b.product_default_code, - b.product_name, - b.raw_material_line_count, - b.raw_total_consumed_qty, - b.raw_materials_summary, - b.raw_material_cost_status_summary, - b.missing_or_invalid_raw_material_cost_count, - b.material_cost_estimated_eur, - b.material_cost_source_status, - b.energy_cost_eur_per_kwh, - b.machine_run_cost_eur_per_h, - b.kwh_consumed, - b.run_min, - b.stop_cost_eur, - b.technical_quality_level, - b.final_report_status, - b.margin_status, - b.pending_stops, - b.production_conversion_status, - b.dqc_raw_line_count, - b.dqc_raw_missing_code_count, - b.raw_materials_detected, - 'DQC-COST-002'::text AS rule_code, - CASE - WHEN (b.kwh_consumed IS NULL) THEN 'NOT_APPLICABLE'::text - WHEN (b.energy_cost_eur_per_kwh IS NOT NULL) THEN 'PASS'::text - ELSE 'FAIL'::text - END AS check_status, - CASE - WHEN (b.kwh_consumed IS NULL) THEN 'No aplica porque no hay consumo energético medido.'::text - WHEN (b.energy_cost_eur_per_kwh IS NOT NULL) THEN 'Coste energético parametrizado.'::text - ELSE 'Hay consumo energético, pero falta coste €/kWh.'::text - END AS finding, - CASE - WHEN (b.kwh_consumed IS NULL) THEN 'Sin acción.'::text - WHEN (b.energy_cost_eur_per_kwh IS NOT NULL) THEN 'Sin acción.'::text - ELSE 'Cargar coste €/kWh en margin_cost_parameters.'::text - END AS recommended_action, - jsonb_build_object('kwh_consumed', b.kwh_consumed, 'energy_cost_eur_per_kwh', b.energy_cost_eur_per_kwh) AS evidence_json - FROM base b - UNION ALL - SELECT b.order_row_id, - b.tenant, - b.site, - b.odoo_instance, - b.odoo_workorder_id, - b.odoo_production_id, - b.odoo_order_ref, - b.odoo_state, - b.sale_order_ref, - b.customer_name, - b.revenue_net_eur, - b.machine_id, - b.asset, - b.product_default_code, - b.product_name, - b.raw_material_line_count, - b.raw_total_consumed_qty, - b.raw_materials_summary, - b.raw_material_cost_status_summary, - b.missing_or_invalid_raw_material_cost_count, - b.material_cost_estimated_eur, - b.material_cost_source_status, - b.energy_cost_eur_per_kwh, - b.machine_run_cost_eur_per_h, - b.kwh_consumed, - b.run_min, - b.stop_cost_eur, - b.technical_quality_level, - b.final_report_status, - b.margin_status, - b.pending_stops, - b.production_conversion_status, - b.dqc_raw_line_count, - b.dqc_raw_missing_code_count, - b.raw_materials_detected, - 'DQC-COST-003'::text AS rule_code, - CASE - WHEN (b.run_min IS NULL) THEN 'NOT_APPLICABLE'::text - WHEN (b.machine_run_cost_eur_per_h IS NOT NULL) THEN 'PASS'::text - ELSE 'FAIL'::text - END AS check_status, - CASE - WHEN (b.run_min IS NULL) THEN 'No aplica porque no hay tiempo de máquina utilizable.'::text - WHEN (b.machine_run_cost_eur_per_h IS NOT NULL) THEN 'Coste hora máquina parametrizado.'::text - ELSE 'Hay tiempo de máquina, pero falta coste hora máquina.'::text - END AS finding, - CASE - WHEN (b.run_min IS NULL) THEN 'Sin acción.'::text - WHEN (b.machine_run_cost_eur_per_h IS NOT NULL) THEN 'Sin acción.'::text - ELSE 'Cargar coste hora máquina para la máquina correspondiente.'::text - END AS recommended_action, - jsonb_build_object('machine_id', b.machine_id, 'run_min', b.run_min, 'machine_run_cost_eur_per_h', b.machine_run_cost_eur_per_h) AS evidence_json - FROM base b - UNION ALL - SELECT b.order_row_id, - b.tenant, - b.site, - b.odoo_instance, - b.odoo_workorder_id, - b.odoo_production_id, - b.odoo_order_ref, - b.odoo_state, - b.sale_order_ref, - b.customer_name, - b.revenue_net_eur, - b.machine_id, - b.asset, - b.product_default_code, - b.product_name, - b.raw_material_line_count, - b.raw_total_consumed_qty, - b.raw_materials_summary, - b.raw_material_cost_status_summary, - b.missing_or_invalid_raw_material_cost_count, - b.material_cost_estimated_eur, - b.material_cost_source_status, - b.energy_cost_eur_per_kwh, - b.machine_run_cost_eur_per_h, - b.kwh_consumed, - b.run_min, - b.stop_cost_eur, - b.technical_quality_level, - b.final_report_status, - b.margin_status, - b.pending_stops, - b.production_conversion_status, - b.dqc_raw_line_count, - b.dqc_raw_missing_code_count, - b.raw_materials_detected, - 'DQC-EDGE-001'::text AS rule_code, - CASE - WHEN (b.technical_quality_level = ANY (ARRAY['FIABLE'::text, 'ACEPTABLE_CON_OBSERVACIONES'::text])) THEN 'PASS'::text - ELSE 'FAIL'::text - END AS check_status, - CASE - WHEN (b.technical_quality_level = ANY (ARRAY['FIABLE'::text, 'ACEPTABLE_CON_OBSERVACIONES'::text])) THEN 'Datos técnicos defendibles.'::text - ELSE 'Datos técnicos no defendibles para análisis.'::text - END AS finding, - CASE - WHEN (b.technical_quality_level = ANY (ARRAY['FIABLE'::text, 'ACEPTABLE_CON_OBSERVACIONES'::text])) THEN 'Sin acción.'::text - ELSE 'Revisar telemetría, cobertura Edge-OEE y calidad del informe técnico.'::text - END AS recommended_action, - jsonb_build_object('technical_quality_level', b.technical_quality_level) AS evidence_json - FROM base b - UNION ALL - SELECT b.order_row_id, - b.tenant, - b.site, - b.odoo_instance, - b.odoo_workorder_id, - b.odoo_production_id, - b.odoo_order_ref, - b.odoo_state, - b.sale_order_ref, - b.customer_name, - b.revenue_net_eur, - b.machine_id, - b.asset, - b.product_default_code, - b.product_name, - b.raw_material_line_count, - b.raw_total_consumed_qty, - b.raw_materials_summary, - b.raw_material_cost_status_summary, - b.missing_or_invalid_raw_material_cost_count, - b.material_cost_estimated_eur, - b.material_cost_source_status, - b.energy_cost_eur_per_kwh, - b.machine_run_cost_eur_per_h, - b.kwh_consumed, - b.run_min, - b.stop_cost_eur, - b.technical_quality_level, - b.final_report_status, - b.margin_status, - b.pending_stops, - b.production_conversion_status, - b.dqc_raw_line_count, - b.dqc_raw_missing_code_count, - b.raw_materials_detected, - 'DQC-EDGE-002'::text AS rule_code, - CASE - WHEN (b.final_report_status = ANY (ARRAY['LISTO_INFORME_FINAL'::text, 'LISTO_TECNICO_REVISAR_CONVERSION'::text, 'LISTO_CON_OBSERVACIONES'::text])) THEN 'PASS'::text - ELSE 'FAIL'::text - END AS check_status, - CASE - WHEN (b.final_report_status = ANY (ARRAY['LISTO_INFORME_FINAL'::text, 'LISTO_TECNICO_REVISAR_CONVERSION'::text, 'LISTO_CON_OBSERVACIONES'::text])) THEN 'Informe técnico cerrado o utilizable.'::text - ELSE 'Informe técnico parcial o bloqueado.'::text - END AS finding, - CASE - WHEN (b.final_report_status = ANY (ARRAY['LISTO_INFORME_FINAL'::text, 'LISTO_TECNICO_REVISAR_CONVERSION'::text, 'LISTO_CON_OBSERVACIONES'::text])) THEN 'Sin acción.'::text - ELSE 'Cerrar orden, revisar telemetría o completar clasificación antes de análisis económico final.'::text - END AS recommended_action, - jsonb_build_object('final_report_status', b.final_report_status) AS evidence_json - FROM base b - UNION ALL - SELECT b.order_row_id, - b.tenant, - b.site, - b.odoo_instance, - b.odoo_workorder_id, - b.odoo_production_id, - b.odoo_order_ref, - b.odoo_state, - b.sale_order_ref, - b.customer_name, - b.revenue_net_eur, - b.machine_id, - b.asset, - b.product_default_code, - b.product_name, - b.raw_material_line_count, - b.raw_total_consumed_qty, - b.raw_materials_summary, - b.raw_material_cost_status_summary, - b.missing_or_invalid_raw_material_cost_count, - b.material_cost_estimated_eur, - b.material_cost_source_status, - b.energy_cost_eur_per_kwh, - b.machine_run_cost_eur_per_h, - b.kwh_consumed, - b.run_min, - b.stop_cost_eur, - b.technical_quality_level, - b.final_report_status, - b.margin_status, - b.pending_stops, - b.production_conversion_status, - b.dqc_raw_line_count, - b.dqc_raw_missing_code_count, - b.raw_materials_detected, - 'DQC-STOPS-001'::text AS rule_code, - CASE - WHEN (COALESCE(b.pending_stops, (0)::bigint) = 0) THEN 'PASS'::text - ELSE 'FAIL'::text - END AS check_status, - CASE - WHEN (COALESCE(b.pending_stops, (0)::bigint) = 0) THEN 'No hay paros pendientes de clasificación.'::text - ELSE 'Hay paros pendientes de clasificación.'::text - END AS finding, - CASE - WHEN (COALESCE(b.pending_stops, (0)::bigint) = 0) THEN 'Sin acción.'::text - ELSE 'Clasificar paros pendientes desde la consola de paros.'::text - END AS recommended_action, - jsonb_build_object('pending_stops', b.pending_stops) AS evidence_json - FROM base b - UNION ALL - SELECT b.order_row_id, - b.tenant, - b.site, - b.odoo_instance, - b.odoo_workorder_id, - b.odoo_production_id, - b.odoo_order_ref, - b.odoo_state, - b.sale_order_ref, - b.customer_name, - b.revenue_net_eur, - b.machine_id, - b.asset, - b.product_default_code, - b.product_name, - b.raw_material_line_count, - b.raw_total_consumed_qty, - b.raw_materials_summary, - b.raw_material_cost_status_summary, - b.missing_or_invalid_raw_material_cost_count, - b.material_cost_estimated_eur, - b.material_cost_source_status, - b.energy_cost_eur_per_kwh, - b.machine_run_cost_eur_per_h, - b.kwh_consumed, - b.run_min, - b.stop_cost_eur, - b.technical_quality_level, - b.final_report_status, - b.margin_status, - b.pending_stops, - b.production_conversion_status, - b.dqc_raw_line_count, - b.dqc_raw_missing_code_count, - b.raw_materials_detected, - 'DQC-CONV-001'::text AS rule_code, - CASE - WHEN (b.production_conversion_status = 'CICLOS_COHERENTES_CON_OBJETIVO'::text) THEN 'PASS'::text - WHEN (b.production_conversion_status = 'CANTIDAD_ODOO_SIN_CICLOS_EDGE'::text) THEN 'FAIL'::text - ELSE 'WARN'::text - END AS check_status, - CASE - WHEN (b.production_conversion_status = 'CICLOS_COHERENTES_CON_OBJETIVO'::text) THEN 'Conversión producción-ciclos coherente.'::text - WHEN (b.production_conversion_status = 'CANTIDAD_ODOO_SIN_CICLOS_EDGE'::text) THEN 'Odoo muestra cantidad producida, pero Edge no detecta ciclos.'::text - ELSE 'Conversión producción-ciclos pendiente de revisión o no concluyente.'::text - END AS finding, - CASE - WHEN (b.production_conversion_status = 'CICLOS_COHERENTES_CON_OBJETIVO'::text) THEN 'Sin acción.'::text - ELSE 'Validar relación entre kg, bolsas, unidad comercial y ciclos/golpes.'::text - END AS recommended_action, - jsonb_build_object('production_conversion_status', b.production_conversion_status) AS evidence_json - FROM base b - ) - SELECT e.order_row_id, - e.tenant, - e.site, - e.odoo_instance, - e.odoo_workorder_id, - e.odoo_production_id, - e.odoo_order_ref, - e.odoo_state, - e.sale_order_ref, - e.customer_name, - e.product_default_code, - e.product_name, - e.machine_id, - r.rule_code, - r.rule_name, - r.domain, - r.dimension, - r.severity, - r.owner_area, - r.applies_to, - r.blocking_for, - e.check_status, - CASE - WHEN (e.check_status = 'PASS'::text) THEN true - WHEN (e.check_status = 'NOT_APPLICABLE'::text) THEN NULL::boolean - ELSE false - END AS check_passed, - e.finding, - e.recommended_action, - e.evidence_json, - CASE - WHEN ((e.check_status = 'FAIL'::text) AND (r.severity = 'CRITICAL'::text)) THEN 100 - WHEN ((e.check_status = 'FAIL'::text) AND (r.severity = 'HIGH'::text)) THEN 75 - WHEN ((e.check_status = 'FAIL'::text) AND (r.severity = 'MEDIUM'::text)) THEN 50 - WHEN ((e.check_status = 'WARN'::text) AND (r.severity = 'CRITICAL'::text)) THEN 60 - WHEN ((e.check_status = 'WARN'::text) AND (r.severity = 'HIGH'::text)) THEN 45 - WHEN ((e.check_status = 'WARN'::text) AND (r.severity = 'MEDIUM'::text)) THEN 30 - ELSE 0 - END AS issue_score - FROM (evaluations e - JOIN mv_edge_oee_ucepsa_demo.data_quality_contract_rules r ON (((r.tenant = e.tenant) AND (r.site = e.site) AND (r.rule_code = e.rule_code) AND (r.is_active = true)))); - - --- --- Name: data_quality_contract_order_summary_v1; Type: VIEW; Schema: mv_reports_ucepsa_demo; Owner: - --- - -CREATE VIEW mv_reports_ucepsa_demo.data_quality_contract_order_summary_v1 AS - WITH checks AS ( - SELECT data_quality_contract_order_checks_v1.order_row_id, - data_quality_contract_order_checks_v1.tenant, - data_quality_contract_order_checks_v1.site, - data_quality_contract_order_checks_v1.odoo_instance, - data_quality_contract_order_checks_v1.odoo_workorder_id, - data_quality_contract_order_checks_v1.odoo_production_id, - data_quality_contract_order_checks_v1.odoo_order_ref, - data_quality_contract_order_checks_v1.odoo_state, - data_quality_contract_order_checks_v1.sale_order_ref, - data_quality_contract_order_checks_v1.customer_name, - data_quality_contract_order_checks_v1.product_default_code, - data_quality_contract_order_checks_v1.product_name, - data_quality_contract_order_checks_v1.machine_id, - data_quality_contract_order_checks_v1.rule_code, - data_quality_contract_order_checks_v1.rule_name, - data_quality_contract_order_checks_v1.domain, - data_quality_contract_order_checks_v1.dimension, - data_quality_contract_order_checks_v1.severity, - data_quality_contract_order_checks_v1.owner_area, - data_quality_contract_order_checks_v1.applies_to, - data_quality_contract_order_checks_v1.blocking_for, - data_quality_contract_order_checks_v1.check_status, - data_quality_contract_order_checks_v1.check_passed, - data_quality_contract_order_checks_v1.finding, - data_quality_contract_order_checks_v1.recommended_action, - data_quality_contract_order_checks_v1.evidence_json, - data_quality_contract_order_checks_v1.issue_score - FROM mv_reports_ucepsa_demo.data_quality_contract_order_checks_v1 - ), agg AS ( - SELECT checks.order_row_id, - checks.tenant, - checks.site, - checks.odoo_instance, - checks.odoo_workorder_id, - checks.odoo_production_id, - checks.odoo_order_ref, - checks.odoo_state, - checks.sale_order_ref, - checks.customer_name, - checks.product_default_code, - checks.product_name, - checks.machine_id, - count(*) AS total_rules, - count(*) FILTER (WHERE (checks.check_status = 'PASS'::text)) AS passed_rules, - count(*) FILTER (WHERE (checks.check_status = 'FAIL'::text)) AS failed_rules, - count(*) FILTER (WHERE (checks.check_status = 'WARN'::text)) AS warn_rules, - count(*) FILTER (WHERE (checks.check_status = 'NOT_APPLICABLE'::text)) AS not_applicable_rules, - count(*) FILTER (WHERE ((checks.check_status = 'FAIL'::text) AND (checks.severity = 'CRITICAL'::text))) AS critical_failures, - count(*) FILTER (WHERE ((checks.check_status = 'FAIL'::text) AND (checks.severity = 'HIGH'::text))) AS high_failures, - count(*) FILTER (WHERE ((checks.check_status = 'FAIL'::text) AND (checks.severity = 'MEDIUM'::text))) AS medium_failures, - count(*) FILTER (WHERE ((checks.check_status = 'WARN'::text) AND (checks.severity = 'CRITICAL'::text))) AS critical_warnings, - count(*) FILTER (WHERE ((checks.check_status = 'WARN'::text) AND (checks.severity = 'HIGH'::text))) AS high_warnings, - count(*) FILTER (WHERE ((checks.check_status = 'WARN'::text) AND (checks.severity = 'MEDIUM'::text))) AS medium_warnings, - COALESCE(sum(checks.issue_score), (0)::bigint) AS total_issue_score, - max(checks.issue_score) AS max_issue_score - FROM checks - GROUP BY checks.order_row_id, checks.tenant, checks.site, checks.odoo_instance, checks.odoo_workorder_id, checks.odoo_production_id, checks.odoo_order_ref, checks.odoo_state, checks.sale_order_ref, checks.customer_name, checks.product_default_code, checks.product_name, checks.machine_id - ), main_issue AS ( - SELECT DISTINCT ON (checks.order_row_id) checks.order_row_id, - checks.rule_code AS main_blocker_rule_code, - checks.rule_name AS main_blocker_rule_name, - checks.domain AS main_blocker_domain, - checks.severity AS main_blocker_severity, - checks.owner_area AS owner_area_to_act, - checks.blocking_for, - checks.check_status AS main_blocker_status, - checks.finding AS main_blocker_finding, - checks.recommended_action AS main_recommended_action, - checks.issue_score AS main_issue_score - FROM checks - WHERE (checks.check_status = ANY (ARRAY['FAIL'::text, 'WARN'::text])) - ORDER BY checks.order_row_id, checks.issue_score DESC, - CASE checks.severity - WHEN 'CRITICAL'::text THEN 1 - WHEN 'HIGH'::text THEN 2 - WHEN 'MEDIUM'::text THEN 3 - ELSE 4 - END, checks.rule_code - ) - SELECT a.order_row_id, - a.tenant, - a.site, - a.odoo_instance, - a.odoo_workorder_id, - a.odoo_production_id, - a.odoo_order_ref, - a.odoo_state, - a.sale_order_ref, - a.customer_name, - a.product_default_code, - a.product_name, - a.machine_id, - a.total_rules, - a.passed_rules, - a.failed_rules, - a.warn_rules, - a.not_applicable_rules, - a.critical_failures, - a.high_failures, - a.medium_failures, - a.critical_warnings, - a.high_warnings, - a.medium_warnings, - a.total_issue_score, - a.max_issue_score, - GREATEST((0)::bigint, (100 - LEAST(a.total_issue_score, (100)::bigint))) AS contract_score, - CASE - WHEN (a.critical_failures > 0) THEN 'BLOQUEADO_CRITICO'::text - WHEN (a.high_failures > 0) THEN 'BLOQUEADO_ALTO'::text - WHEN (a.medium_failures > 0) THEN 'BLOQUEADO_MEDIO'::text - WHEN (a.warn_rules > 0) THEN 'VALIDO_CON_OBSERVACIONES'::text - WHEN ((a.failed_rules = 0) AND (a.warn_rules = 0)) THEN 'CONTRATO_CUMPLIDO'::text - ELSE 'REVISAR'::text - END AS contract_status, - mi.main_blocker_rule_code, - mi.main_blocker_rule_name, - mi.main_blocker_domain, - mi.main_blocker_severity, - mi.owner_area_to_act, - mi.blocking_for, - mi.main_blocker_status, - mi.main_blocker_finding, - mi.main_recommended_action, - mi.main_issue_score, - CASE - WHEN (a.critical_failures > 0) THEN concat('Contrato bloqueado por fallo crítico: ', COALESCE(mi.main_blocker_finding, 'sin detalle'::text)) - WHEN (a.high_failures > 0) THEN concat('Contrato bloqueado por fallo alto: ', COALESCE(mi.main_blocker_finding, 'sin detalle'::text)) - WHEN (a.medium_failures > 0) THEN concat('Contrato bloqueado por fallo medio: ', COALESCE(mi.main_blocker_finding, 'sin detalle'::text)) - WHEN (a.warn_rules > 0) THEN concat('Contrato válido con observaciones: ', COALESCE(mi.main_blocker_finding, 'sin detalle'::text)) - ELSE 'Contrato de datos cumplido para esta orden.'::text - END AS contract_summary - FROM (agg a - LEFT JOIN main_issue mi ON ((mi.order_row_id = a.order_row_id))); - - --- --- Name: data_quality_contract_owner_summary_v1; Type: VIEW; Schema: mv_reports_ucepsa_demo; Owner: - --- - -CREATE VIEW mv_reports_ucepsa_demo.data_quality_contract_owner_summary_v1 AS - SELECT tenant, - site, - owner_area, - count(*) FILTER (WHERE (check_status = 'FAIL'::text)) AS failed_checks, - count(*) FILTER (WHERE (check_status = 'WARN'::text)) AS warn_checks, - count(*) FILTER (WHERE ((check_status = 'FAIL'::text) AND (severity = 'CRITICAL'::text))) AS critical_failed_checks, - count(*) FILTER (WHERE ((check_status = 'FAIL'::text) AND (severity = 'HIGH'::text))) AS high_failed_checks, - count(DISTINCT odoo_order_ref) FILTER (WHERE (check_status = ANY (ARRAY['FAIL'::text, 'WARN'::text]))) AS affected_orders, - sum(issue_score) FILTER (WHERE (check_status = ANY (ARRAY['FAIL'::text, 'WARN'::text]))) AS total_issue_score, - string_agg(DISTINCT rule_code, ', '::text ORDER BY rule_code) FILTER (WHERE (check_status = ANY (ARRAY['FAIL'::text, 'WARN'::text]))) AS affected_rules, - string_agg(DISTINCT odoo_order_ref, ', '::text ORDER BY odoo_order_ref) FILTER (WHERE (check_status = ANY (ARRAY['FAIL'::text, 'WARN'::text]))) AS affected_order_refs - FROM mv_reports_ucepsa_demo.data_quality_contract_order_checks_v1 - GROUP BY tenant, site, owner_area; - - --- --- Name: data_quality_contract_rules id; Type: DEFAULT; Schema: mv_edge_oee_ucepsa_demo; Owner: - --- - -ALTER TABLE ONLY mv_edge_oee_ucepsa_demo.data_quality_contract_rules ALTER COLUMN id SET DEFAULT nextval('mv_edge_oee_ucepsa_demo.data_quality_contract_rules_id_seq'::regclass); - - --- --- Name: data_quality_contract_rules data_quality_contract_rules_pkey; Type: CONSTRAINT; Schema: mv_edge_oee_ucepsa_demo; Owner: - --- - -ALTER TABLE ONLY mv_edge_oee_ucepsa_demo.data_quality_contract_rules - ADD CONSTRAINT data_quality_contract_rules_pkey PRIMARY KEY (id); - - --- --- Name: data_quality_contract_rules ux_data_quality_contract_rules_code; Type: CONSTRAINT; Schema: mv_edge_oee_ucepsa_demo; Owner: - --- - -ALTER TABLE ONLY mv_edge_oee_ucepsa_demo.data_quality_contract_rules - ADD CONSTRAINT ux_data_quality_contract_rules_code UNIQUE (tenant, site, rule_code); - - --- --- Name: idx_data_quality_contract_rules_active; Type: INDEX; Schema: mv_edge_oee_ucepsa_demo; Owner: - --- - -CREATE INDEX idx_data_quality_contract_rules_active ON mv_edge_oee_ucepsa_demo.data_quality_contract_rules USING btree (tenant, site, is_active, domain, severity); - - --- --- PostgreSQL database dump complete --- - -\unrestrict Jszr4ruwRiCXF4itba8oT9UnTRsL1bJGSsYnj9UgpfOpaiQUSblgKdozAITLZdr - - --- Seed rules --- --- PostgreSQL database dump --- - -\restrict 0GULSAN6PrEX9eK1oI9gTcg4MdETx9P93tGMaONyYc1oC6UFbi5FCS1yjEVkVNx - --- Dumped from database version 16.14 --- Dumped by pg_dump version 16.14 - -SET statement_timeout = 0; -SET lock_timeout = 0; -SET idle_in_transaction_session_timeout = 0; -SET client_encoding = 'UTF8'; -SET standard_conforming_strings = on; -SELECT pg_catalog.set_config('search_path', '', false); -SET check_function_bodies = false; -SET xmloption = content; -SET client_min_messages = warning; -SET row_security = off; - --- --- Data for Name: data_quality_contract_rules; Type: TABLE DATA; Schema: mv_edge_oee_ucepsa_demo; Owner: - --- - -INSERT INTO mv_edge_oee_ucepsa_demo.data_quality_contract_rules (id, tenant, site, rule_code, rule_name, domain, dimension, severity, owner_area, applies_to, description, expected_condition, blocking_for, is_active, created_at) VALUES (1, 'ucepsa', 'ucepsa_test', 'DQC-ODOO-001', 'Orden de fabricación identificada', 'ODOO', 'COMPLETENESS', 'CRITICAL', 'Odoo / Producción', 'Manufacturing Order', 'La orden debe tener referencia de fabricación e identificador de producción Odoo.', 'odoo_order_ref y odoo_production_id deben existir.', 'order_report', true, '2026-05-30 20:18:18.616748+02'); -INSERT INTO mv_edge_oee_ucepsa_demo.data_quality_contract_rules (id, tenant, site, rule_code, rule_name, domain, dimension, severity, owner_area, applies_to, description, expected_condition, blocking_for, is_active, created_at) VALUES (2, 'ucepsa', 'ucepsa_test', 'DQC-ODOO-002', 'Centro de trabajo mapeado a máquina MESAVAULT', 'ODOO', 'TRACEABILITY', 'CRITICAL', 'Odoo / MESAVAULT', 'Workcenter / Machine', 'El centro de trabajo de Odoo debe estar asociado a un machine_id MESAVAULT.', 'machine_id debe existir y estar mapeado.', 'edge_oee_report', true, '2026-05-30 20:18:18.616748+02'); -INSERT INTO mv_edge_oee_ucepsa_demo.data_quality_contract_rules (id, tenant, site, rule_code, rule_name, domain, dimension, severity, owner_area, applies_to, description, expected_condition, blocking_for, is_active, created_at) VALUES (3, 'ucepsa', 'ucepsa_test', 'DQC-ODOO-003', 'Producto identificado', 'ODOO', 'COMPLETENESS', 'HIGH', 'Odoo / Maestro de producto', 'Product', 'El producto debe estar identificado con nombre y, cuando aplique, código interno.', 'product_name o product deben existir; product_default_code recomendable.', 'product_margin', true, '2026-05-30 20:18:18.616748+02'); -INSERT INTO mv_edge_oee_ucepsa_demo.data_quality_contract_rules (id, tenant, site, rule_code, rule_name, domain, dimension, severity, owner_area, applies_to, description, expected_condition, blocking_for, is_active, created_at) VALUES (4, 'ucepsa', 'ucepsa_test', 'DQC-SALES-001', 'Ingreso de venta vinculado', 'SALES', 'TRACEABILITY', 'CRITICAL', 'Ventas / Odoo', 'Sale Order', 'La orden debe estar vinculada a una línea de venta para poder calcular margen.', 'sale_order_ref y revenue_net_eur deben existir.', 'margin', true, '2026-05-30 20:18:18.616748+02'); -INSERT INTO mv_edge_oee_ucepsa_demo.data_quality_contract_rules (id, tenant, site, rule_code, rule_name, domain, dimension, severity, owner_area, applies_to, description, expected_condition, blocking_for, is_active, created_at) VALUES (5, 'ucepsa', 'ucepsa_test', 'DQC-SALES-002', 'Cliente vinculado', 'SALES', 'COMPLETENESS', 'HIGH', 'Ventas / Odoo', 'Customer', 'La orden debe tener cliente asociado cuando exista pedido de venta.', 'customer_name debe existir si hay sale_order_ref.', 'customer_margin', true, '2026-05-30 20:18:18.616748+02'); -INSERT INTO mv_edge_oee_ucepsa_demo.data_quality_contract_rules (id, tenant, site, rule_code, rule_name, domain, dimension, severity, owner_area, applies_to, description, expected_condition, blocking_for, is_active, created_at) VALUES (6, 'ucepsa', 'ucepsa_test', 'DQC-RAW-001', 'Materia prima consumida detectada', 'MATERIAL', 'TRACEABILITY', 'CRITICAL', 'Producción / Almacén / Odoo', 'Raw Material', 'La orden debe tener materia prima consumida o movimiento de material asociado.', 'raw_material_line_count debe ser mayor que cero.', 'material_cost', true, '2026-05-30 20:18:18.616748+02'); -INSERT INTO mv_edge_oee_ucepsa_demo.data_quality_contract_rules (id, tenant, site, rule_code, rule_name, domain, dimension, severity, owner_area, applies_to, description, expected_condition, blocking_for, is_active, created_at) VALUES (7, 'ucepsa', 'ucepsa_test', 'DQC-RAW-002', 'Materia prima con código identificable', 'MATERIAL', 'COMPLETENESS', 'HIGH', 'Maestro de producto / Odoo', 'Raw Material', 'La materia prima debe tener código interno para poder asignar costes y analizar histórico.', 'raw_product_default_code debe existir.', 'material_cost', true, '2026-05-30 20:18:18.616748+02'); -INSERT INTO mv_edge_oee_ucepsa_demo.data_quality_contract_rules (id, tenant, site, rule_code, rule_name, domain, dimension, severity, owner_area, applies_to, description, expected_condition, blocking_for, is_active, created_at) VALUES (8, 'ucepsa', 'ucepsa_test', 'DQC-COST-001', 'Coste de materia prima disponible', 'COST', 'COMPLETENESS', 'CRITICAL', 'Compras / Administración / Odoo', 'Raw Material Cost', 'La materia prima debe tener coste €/kg o valoración Odoo para calcular margen.', 'material_cost_estimated_eur debe existir o la materia prima debe tener coste parametrizado.', 'margin', true, '2026-05-30 20:18:18.616748+02'); -INSERT INTO mv_edge_oee_ucepsa_demo.data_quality_contract_rules (id, tenant, site, rule_code, rule_name, domain, dimension, severity, owner_area, applies_to, description, expected_condition, blocking_for, is_active, created_at) VALUES (9, 'ucepsa', 'ucepsa_test', 'DQC-COST-002', 'Coste energético parametrizado', 'COST', 'COMPLETENESS', 'MEDIUM', 'Administración / MESAVAULT', 'Energy Cost', 'Si se usa energía en el margen, debe existir coste €/kWh.', 'energy_cost_eur_per_kwh debe existir cuando kwh_consumed exista.', 'full_margin', true, '2026-05-30 20:18:18.616748+02'); -INSERT INTO mv_edge_oee_ucepsa_demo.data_quality_contract_rules (id, tenant, site, rule_code, rule_name, domain, dimension, severity, owner_area, applies_to, description, expected_condition, blocking_for, is_active, created_at) VALUES (10, 'ucepsa', 'ucepsa_test', 'DQC-COST-003', 'Coste hora máquina parametrizado', 'COST', 'COMPLETENESS', 'MEDIUM', 'Producción / Administración / MESAVAULT', 'Machine Cost', 'Si se imputa coste de máquina, debe existir coste horario para la máquina.', 'machine_run_cost_eur_per_h debe existir cuando run_min exista.', 'full_margin', true, '2026-05-30 20:18:18.616748+02'); -INSERT INTO mv_edge_oee_ucepsa_demo.data_quality_contract_rules (id, tenant, site, rule_code, rule_name, domain, dimension, severity, owner_area, applies_to, description, expected_condition, blocking_for, is_active, created_at) VALUES (11, 'ucepsa', 'ucepsa_test', 'DQC-EDGE-001', 'Datos técnicos defendibles', 'EDGE', 'QUALITY', 'CRITICAL', 'MESAVAULT / Producción', 'Telemetry', 'La orden debe tener datos técnicos suficientes para un informe defendible.', 'technical_quality_level debe ser FIABLE o ACEPTABLE_CON_OBSERVACIONES.', 'technical_report', true, '2026-05-30 20:18:18.616748+02'); -INSERT INTO mv_edge_oee_ucepsa_demo.data_quality_contract_rules (id, tenant, site, rule_code, rule_name, domain, dimension, severity, owner_area, applies_to, description, expected_condition, blocking_for, is_active, created_at) VALUES (12, 'ucepsa', 'ucepsa_test', 'DQC-EDGE-002', 'Informe técnico cerrado o utilizable', 'EDGE', 'TIMELINESS', 'HIGH', 'MESAVAULT / Producción', 'Order Execution Report', 'La orden debe tener informe técnico cerrado o utilizable antes de análisis económico final.', 'final_report_status debe estar en estado listo o con observaciones.', 'margin', true, '2026-05-30 20:18:18.616748+02'); -INSERT INTO mv_edge_oee_ucepsa_demo.data_quality_contract_rules (id, tenant, site, rule_code, rule_name, domain, dimension, severity, owner_area, applies_to, description, expected_condition, blocking_for, is_active, created_at) VALUES (13, 'ucepsa', 'ucepsa_test', 'DQC-STOPS-001', 'Paros clasificados', 'LOSSES', 'COMPLETENESS', 'HIGH', 'Producción', 'Stops', 'Los paros imputados a una orden deben estar clasificados para explicar pérdidas.', 'pending_stops debe ser cero.', 'loss_analysis', true, '2026-05-30 20:18:18.616748+02'); -INSERT INTO mv_edge_oee_ucepsa_demo.data_quality_contract_rules (id, tenant, site, rule_code, rule_name, domain, dimension, severity, owner_area, applies_to, description, expected_condition, blocking_for, is_active, created_at) VALUES (14, 'ucepsa', 'ucepsa_test', 'DQC-CONV-001', 'Conversión producción-ciclos revisada', 'PRODUCTION', 'CONSISTENCY', 'MEDIUM', 'Odoo / Producción / MESAVAULT', 'Production Conversion', 'La relación entre cantidad Odoo, kg, bolsas y ciclos debe estar validada antes de usar métricas unitarias.', 'production_conversion_status debe ser coherente o marcado como pendiente.', 'unit_margin', true, '2026-05-30 20:18:18.616748+02'); - - --- --- Name: data_quality_contract_rules_id_seq; Type: SEQUENCE SET; Schema: mv_edge_oee_ucepsa_demo; Owner: - --- - -SELECT pg_catalog.setval('mv_edge_oee_ucepsa_demo.data_quality_contract_rules_id_seq', 14, true); - - --- --- PostgreSQL database dump complete --- - -\unrestrict 0GULSAN6PrEX9eK1oI9gTcg4MdETx9P93tGMaONyYc1oC6UFbi5FCS1yjEVkVNx - - --- Read-only grants -GRANT USAGE ON SCHEMA mv_edge_oee_ucepsa_demo TO grafana_ucepsa_ro; -GRANT USAGE ON SCHEMA mv_reports_ucepsa_demo TO grafana_ucepsa_ro; - -GRANT SELECT ON mv_edge_oee_ucepsa_demo.data_quality_contract_rules TO grafana_ucepsa_ro; - -GRANT SELECT ON mv_reports_ucepsa_demo.data_quality_contract_order_checks_v1 TO grafana_ucepsa_ro; -GRANT SELECT ON mv_reports_ucepsa_demo.data_quality_contract_order_summary_v1 TO grafana_ucepsa_ro; -GRANT SELECT ON mv_reports_ucepsa_demo.data_quality_contract_owner_summary_v1 TO grafana_ucepsa_ro; diff --git a/ucepsa/edge-oee-demo/sql/versions/090_margin_prod_foundation.sql b/ucepsa/edge-oee-demo/sql/versions/090_margin_prod_foundation.sql new file mode 100644 index 0000000..7d218a2 --- /dev/null +++ b/ucepsa/edge-oee-demo/sql/versions/090_margin_prod_foundation.sql @@ -0,0 +1,100 @@ +BEGIN; + +CREATE SCHEMA IF NOT EXISTS mv_edge_oee_ucepsa_prod; +CREATE SCHEMA IF NOT EXISTS mv_reports_ucepsa_prod; + +-- Cache comercial/productiva de Odoo PROD. +-- Se clona estructura desde la tabla demo existente, pero se corrige la secuencia del id. +CREATE TABLE IF NOT EXISTS mv_edge_oee_ucepsa_prod.odoo_order_commercials +( + LIKE mv_edge_oee_ucepsa_demo.odoo_order_commercials + INCLUDING ALL +); + +CREATE SEQUENCE IF NOT EXISTS mv_edge_oee_ucepsa_prod.odoo_order_commercials_id_seq + OWNED BY mv_edge_oee_ucepsa_prod.odoo_order_commercials.id; + +ALTER TABLE mv_edge_oee_ucepsa_prod.odoo_order_commercials + ALTER COLUMN id SET DEFAULT nextval('mv_edge_oee_ucepsa_prod.odoo_order_commercials_id_seq'::regclass); + +-- Parámetros económicos productivos. +CREATE TABLE IF NOT EXISTS mv_edge_oee_ucepsa_prod.margin_cost_parameters +( + LIKE mv_edge_oee_ucepsa_demo.margin_cost_parameters + INCLUDING ALL +); + +CREATE TABLE IF NOT EXISTS mv_edge_oee_ucepsa_prod.raw_material_cost_parameters +( + LIKE mv_edge_oee_ucepsa_demo.raw_material_cost_parameters + INCLUDING ALL +); + +CREATE TABLE IF NOT EXISTS mv_edge_oee_ucepsa_prod.data_quality_contract_rules +( + LIKE mv_edge_oee_ucepsa_demo.data_quality_contract_rules + INCLUDING ALL +); + +-- Copia de reglas DQC demo a contexto productivo. +-- Mantiene la lógica, cambia site a ucepsa_onpremise. +INSERT INTO mv_edge_oee_ucepsa_prod.data_quality_contract_rules ( + tenant, + site, + rule_code, + rule_name, + domain, + dimension, + severity, + owner_area, + applies_to, + description, + expected_condition, + blocking_for, + is_active, + created_at +) +SELECT + tenant, + 'ucepsa_onpremise' AS site, + rule_code, + rule_name, + domain, + dimension, + severity, + owner_area, + applies_to, + description, + expected_condition, + blocking_for, + is_active, + now() +FROM mv_edge_oee_ucepsa_demo.data_quality_contract_rules d +WHERE d.tenant = 'ucepsa' + AND d.site = 'ucepsa_test' + AND NOT EXISTS ( + SELECT 1 + FROM mv_edge_oee_ucepsa_prod.data_quality_contract_rules p + WHERE p.tenant = d.tenant + AND p.site = 'ucepsa_onpremise' + AND p.rule_code = d.rule_code + ); + +-- Permisos defensivos si los roles existen. +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'grafana_ucepsa_ro') THEN + GRANT USAGE ON SCHEMA mv_edge_oee_ucepsa_prod TO grafana_ucepsa_ro; + GRANT USAGE ON SCHEMA mv_reports_ucepsa_prod TO grafana_ucepsa_ro; + GRANT SELECT ON ALL TABLES IN SCHEMA mv_edge_oee_ucepsa_prod TO grafana_ucepsa_ro; + GRANT SELECT ON ALL TABLES IN SCHEMA mv_reports_ucepsa_prod TO grafana_ucepsa_ro; + END IF; + + IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'edge_oee_odoo_sync_writer') THEN + GRANT USAGE ON SCHEMA mv_edge_oee_ucepsa_prod TO edge_oee_odoo_sync_writer; + GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA mv_edge_oee_ucepsa_prod TO edge_oee_odoo_sync_writer; + GRANT USAGE, SELECT, UPDATE ON ALL SEQUENCES IN SCHEMA mv_edge_oee_ucepsa_prod TO edge_oee_odoo_sync_writer; + END IF; +END $$; + +COMMIT; diff --git a/ucepsa/edge-oee-demo/sql/versions/091_margin_prod_current_order_readiness.sql b/ucepsa/edge-oee-demo/sql/versions/091_margin_prod_current_order_readiness.sql new file mode 100644 index 0000000..c86c734 --- /dev/null +++ b/ucepsa/edge-oee-demo/sql/versions/091_margin_prod_current_order_readiness.sql @@ -0,0 +1,136 @@ +BEGIN; + +CREATE OR REPLACE VIEW mv_reports_ucepsa_prod.current_order_margin_readiness_v1 AS +WITH current_orders AS ( + SELECT + tenant, + site, + machine_id, + workcenter_id, + workcenter_name, + order_present, + order_coherent, + odoo_state AS order_state_from_shopfloor, + workorder_id AS odoo_workorder_id, + production_order AS odoo_order_ref, + product AS order_state_product, + waiting_count, + ready_count, + progress_count, + mqtt_ts, + received_at, + payload_json, + NULLIF(payload_json ->> 'production_id', '')::integer AS odoo_production_id, + NULLIF(payload_json ->> 'product_id', '')::integer AS order_state_product_id, + NULLIF(payload_json ->> 'qty_production', '')::numeric AS order_state_qty_production, + payload_json ->> 'date_start' AS order_state_date_start, + payload_json ->> 'date_finished' AS order_state_date_finished + FROM mv_hot.odoo_order_state_current + WHERE site = 'ucepsa_onpremise' +), +commercial AS ( + SELECT + * + FROM mv_edge_oee_ucepsa_prod.odoo_order_commercials + WHERE site = 'ucepsa_onpremise' + AND odoo_instance = 'odoo_ucepsa_prod' +), +joined AS ( + SELECT + co.tenant, + co.site, + co.machine_id, + co.workcenter_id, + co.workcenter_name, + co.order_present, + co.order_coherent, + co.order_state_from_shopfloor, + co.odoo_workorder_id, + co.odoo_production_id, + co.odoo_order_ref, + co.order_state_product, + co.order_state_product_id, + co.order_state_qty_production, + co.waiting_count, + co.ready_count, + co.progress_count, + co.mqtt_ts, + co.received_at, + + c.odoo_instance, + c.odoo_state AS production_state, + c.odoo_origin, + c.product_id, + c.product_default_code, + COALESCE(c.product_name, co.order_state_product) AS product_name, + c.product_uom, + c.production_qty, + c.qty_producing, + c.production_uom, + c.sale_line_id, + c.sale_order_id, + c.sale_order_ref, + c.customer_id, + c.customer_name, + c.sale_state, + c.sale_date_order, + c.currency, + c.sale_line_qty, + c.sale_line_qty_delivered, + c.sale_price_unit, + c.sale_discount_pct, + c.revenue_net_eur, + c.revenue_total_eur, + c.raw_material_value_eur, + c.finished_value_eur, + c.material_cost_status, + jsonb_array_length(COALESCE(c.stock_raw_moves_json, '[]'::jsonb)) AS raw_material_line_count, + jsonb_array_length(COALESCE(c.stock_finished_moves_json, '[]'::jsonb)) AS finished_move_line_count, + c.stock_raw_moves_json, + c.stock_finished_moves_json, + c.snapshot_at AS commercial_snapshot_at + FROM current_orders co + LEFT JOIN commercial c + ON c.odoo_production_id = co.odoo_production_id +) +SELECT + *, + CASE + WHEN order_present IS FALSE THEN 'NO_ORDER_LOADED' + WHEN order_coherent IS FALSE THEN 'ODOO_ORDER_INCOHERENT' + WHEN odoo_production_id IS NULL THEN 'MISSING_PRODUCTION_ID_FROM_ORDER_STATE' + WHEN odoo_instance IS NULL THEN 'MISSING_COMMERCIAL_SNAPSHOT' + WHEN sale_order_ref IS NULL OR revenue_net_eur IS NULL THEN 'MISSING_REVENUE_LINK' + WHEN customer_name IS NULL THEN 'MISSING_CUSTOMER' + WHEN raw_material_line_count = 0 THEN 'MISSING_RAW_MATERIALS' + WHEN material_cost_status = 'UNAVAILABLE_OR_ZERO' THEN 'MISSING_MATERIAL_COST' + ELSE 'READY_FOR_MARGIN_BASE' + END AS margin_readiness_status, + + CASE + WHEN order_present IS FALSE THEN 'No hay orden cargada en Odoo Shop Floor para esta máquina.' + WHEN order_coherent IS FALSE THEN 'Hay estado Odoo incoherente para esta máquina.' + WHEN odoo_production_id IS NULL THEN 'El order_state no trae production_id; revisar publicador Odoo.' + WHEN odoo_instance IS NULL THEN 'La orden existe en order_state, pero aún no tiene snapshot comercial en Edge-OEE.' + WHEN sale_order_ref IS NULL OR revenue_net_eur IS NULL THEN 'Falta vínculo económico con pedido/línea de venta.' + WHEN customer_name IS NULL THEN 'Falta cliente asociado al pedido de venta.' + WHEN raw_material_line_count = 0 THEN 'No se detectan movimientos de materia prima para la orden.' + WHEN material_cost_status = 'UNAVAILABLE_OR_ZERO' THEN 'Hay materia prima, pero Odoo no aporta valoración económica útil. Cargar coste €/kg o corregir valoración.' + ELSE 'Datos comerciales mínimos disponibles para calcular margen base.' + END AS margin_readiness_description, + + CASE + WHEN order_present IS TRUE THEN 20 ELSE 0 END + + CASE WHEN order_coherent IS TRUE THEN 15 ELSE 0 END + + CASE WHEN odoo_instance IS NOT NULL THEN 15 ELSE 0 END + + CASE WHEN sale_order_ref IS NOT NULL AND revenue_net_eur IS NOT NULL THEN 20 ELSE 0 END + + CASE WHEN customer_name IS NOT NULL THEN 10 ELSE 0 END + + CASE WHEN raw_material_line_count > 0 THEN 10 ELSE 0 END + + CASE WHEN material_cost_status <> 'UNAVAILABLE_OR_ZERO' THEN 10 ELSE 0 END + AS margin_readiness_score + +FROM joined; + +GRANT SELECT ON mv_reports_ucepsa_prod.current_order_margin_readiness_v1 TO grafana_ucepsa_ro; + +COMMIT; diff --git a/ucepsa/edge-oee-demo/sql/versions/092_margin_prod_raw_material_costs.sql b/ucepsa/edge-oee-demo/sql/versions/092_margin_prod_raw_material_costs.sql new file mode 100644 index 0000000..8024367 --- /dev/null +++ b/ucepsa/edge-oee-demo/sql/versions/092_margin_prod_raw_material_costs.sql @@ -0,0 +1,271 @@ +BEGIN; + +-- ============================================================ +-- 1) Higiene de secuencias productivas +-- ============================================================ + +CREATE SEQUENCE IF NOT EXISTS mv_edge_oee_ucepsa_prod.margin_cost_parameters_id_seq + OWNED BY mv_edge_oee_ucepsa_prod.margin_cost_parameters.id; + +ALTER TABLE mv_edge_oee_ucepsa_prod.margin_cost_parameters + ALTER COLUMN id SET DEFAULT nextval('mv_edge_oee_ucepsa_prod.margin_cost_parameters_id_seq'::regclass); + +SELECT setval( + 'mv_edge_oee_ucepsa_prod.margin_cost_parameters_id_seq', + COALESCE((SELECT max(id) FROM mv_edge_oee_ucepsa_prod.margin_cost_parameters), 0) + 1, + false +); + +CREATE SEQUENCE IF NOT EXISTS mv_edge_oee_ucepsa_prod.raw_material_cost_parameters_id_seq + OWNED BY mv_edge_oee_ucepsa_prod.raw_material_cost_parameters.id; + +ALTER TABLE mv_edge_oee_ucepsa_prod.raw_material_cost_parameters + ALTER COLUMN id SET DEFAULT nextval('mv_edge_oee_ucepsa_prod.raw_material_cost_parameters_id_seq'::regclass); + +SELECT setval( + 'mv_edge_oee_ucepsa_prod.raw_material_cost_parameters_id_seq', + COALESCE((SELECT max(id) FROM mv_edge_oee_ucepsa_prod.raw_material_cost_parameters), 0) + 1, + false +); + +CREATE SEQUENCE IF NOT EXISTS mv_edge_oee_ucepsa_prod.data_quality_contract_rules_id_seq + OWNED BY mv_edge_oee_ucepsa_prod.data_quality_contract_rules.id; + +ALTER TABLE mv_edge_oee_ucepsa_prod.data_quality_contract_rules + ALTER COLUMN id SET DEFAULT nextval('mv_edge_oee_ucepsa_prod.data_quality_contract_rules_id_seq'::regclass); + +SELECT setval( + 'mv_edge_oee_ucepsa_prod.data_quality_contract_rules_id_seq', + COALESCE((SELECT max(id) FROM mv_edge_oee_ucepsa_prod.data_quality_contract_rules), 0) + 1, + false +); + +-- ============================================================ +-- 2) Ampliar parámetros de coste para permitir match por ID Odoo +-- ============================================================ + +ALTER TABLE mv_edge_oee_ucepsa_prod.raw_material_cost_parameters + ADD COLUMN IF NOT EXISTS raw_product_id integer; + +ALTER TABLE mv_edge_oee_ucepsa_prod.raw_material_cost_parameters + ADD COLUMN IF NOT EXISTS raw_uom text; + +CREATE INDEX IF NOT EXISTS idx_raw_material_cost_params_prod_product_id + ON mv_edge_oee_ucepsa_prod.raw_material_cost_parameters(raw_product_id); + +CREATE INDEX IF NOT EXISTS idx_raw_material_cost_params_prod_code + ON mv_edge_oee_ucepsa_prod.raw_material_cost_parameters(raw_product_default_code); + +CREATE INDEX IF NOT EXISTS idx_raw_material_cost_params_prod_name + ON mv_edge_oee_ucepsa_prod.raw_material_cost_parameters(raw_product_name); + +-- ============================================================ +-- 3) Vista productiva de líneas de materia prima +-- ============================================================ + +CREATE OR REPLACE VIEW mv_reports_ucepsa_prod.order_raw_material_costs_v1 AS +WITH raw_moves AS ( + SELECT + c.id AS commercial_row_id, + c.tenant, + c.site, + c.odoo_instance, + c.odoo_production_id, + c.odoo_order_ref, + c.sale_order_ref, + c.customer_name, + c.product_default_code AS finished_product_default_code, + c.product_name AS finished_product_name, + + NULLIF(rm.raw_move ->> 'id', '')::integer AS raw_move_id, + NULLIF((rm.raw_move -> 'product_id') ->> 0, '')::integer AS raw_product_id, + (rm.raw_move -> 'product_id') ->> 1 AS raw_product_name, + substring((rm.raw_move -> 'product_id') ->> 1, '\[([^\]]+)\]') AS raw_product_default_code, + + NULLIF(rm.raw_move ->> 'product_uom_qty', '')::numeric AS planned_qty, + NULLIF(rm.raw_move ->> 'quantity', '')::numeric AS consumed_qty, + (rm.raw_move -> 'product_uom') ->> 1 AS raw_uom, + NULLIF(rm.raw_move ->> 'price_unit', '')::numeric AS odoo_price_unit, + NULLIF(rm.raw_move ->> 'value', '')::numeric AS odoo_value, + rm.raw_move ->> 'state' AS move_state, + rm.raw_move AS raw_move_json + FROM mv_edge_oee_ucepsa_prod.odoo_order_commercials c + CROSS JOIN LATERAL jsonb_array_elements(c.stock_raw_moves_json) rm(raw_move) +), +costed AS ( + SELECT + r.*, + p.id AS raw_material_cost_parameter_id, + p.material_cost_eur_per_kg, + p.source AS raw_material_cost_source, + p.notes AS raw_material_cost_notes, + + CASE + WHEN COALESCE(r.odoo_value, 0::numeric) <> 0::numeric + THEN abs(r.odoo_value) + WHEN lower(COALESCE(r.raw_uom, '')) = 'kg' + AND p.material_cost_eur_per_kg IS NOT NULL + AND r.consumed_qty IS NOT NULL + THEN r.consumed_qty * p.material_cost_eur_per_kg + ELSE NULL::numeric + END AS material_cost_estimated_eur, + + CASE + WHEN COALESCE(r.odoo_value, 0::numeric) <> 0::numeric + THEN 'FROM_ODOO_STOCK_MOVE_VALUE' + WHEN r.raw_product_id IS NULL + THEN 'MISSING_RAW_PRODUCT_ID' + WHEN lower(COALESCE(r.raw_uom, '')) <> 'kg' + THEN 'UNSUPPORTED_RAW_UOM' + WHEN p.material_cost_eur_per_kg IS NULL + THEN 'MISSING_RAW_MATERIAL_COST_PARAMETER' + WHEN r.consumed_qty IS NULL + THEN 'MISSING_CONSUMED_QTY' + ELSE 'FROM_RAW_MATERIAL_COST_PARAMETER' + END AS material_cost_status + + FROM raw_moves r + LEFT JOIN LATERAL ( + SELECT p1.* + FROM mv_edge_oee_ucepsa_prod.raw_material_cost_parameters p1 + WHERE p1.tenant = r.tenant + AND p1.site = r.site + AND p1.is_active = true + AND p1.valid_from <= now() + AND (p1.valid_to IS NULL OR p1.valid_to > now()) + AND ( + (p1.raw_product_id IS NOT NULL AND p1.raw_product_id = r.raw_product_id) + OR (p1.raw_product_id IS NULL AND p1.raw_product_default_code IS NOT NULL AND p1.raw_product_default_code = r.raw_product_default_code) + OR (p1.raw_product_id IS NULL AND p1.raw_product_default_code IS NULL AND p1.raw_product_name IS NOT NULL AND p1.raw_product_name = r.raw_product_name) + ) + ORDER BY + CASE + WHEN p1.raw_product_id IS NOT NULL AND p1.raw_product_id = r.raw_product_id THEN 1 + WHEN p1.raw_product_default_code IS NOT NULL AND p1.raw_product_default_code = r.raw_product_default_code THEN 2 + WHEN p1.raw_product_name IS NOT NULL AND p1.raw_product_name = r.raw_product_name THEN 3 + ELSE 9 + END, + p1.valid_from DESC, + p1.id DESC + LIMIT 1 + ) p ON true +) +SELECT + commercial_row_id, + tenant, + site, + odoo_instance, + odoo_production_id, + odoo_order_ref, + sale_order_ref, + customer_name, + finished_product_default_code, + finished_product_name, + raw_move_id, + raw_product_id, + raw_product_name, + raw_product_default_code, + planned_qty, + consumed_qty, + raw_uom, + odoo_price_unit, + odoo_value, + move_state, + raw_material_cost_parameter_id, + material_cost_eur_per_kg, + raw_material_cost_source, + raw_material_cost_notes, + material_cost_estimated_eur, + round(material_cost_estimated_eur, 4) AS material_cost_estimated_eur_rounded, + material_cost_status, + raw_move_json +FROM costed; + +-- ============================================================ +-- 4) Resumen de coste de materia prima por orden +-- ============================================================ + +CREATE OR REPLACE VIEW mv_reports_ucepsa_prod.order_raw_material_costs_summary_v1 AS +SELECT + tenant, + site, + odoo_instance, + odoo_production_id, + odoo_order_ref, + sale_order_ref, + customer_name, + finished_product_default_code, + finished_product_name, + count(*) AS raw_material_line_count, + round(sum(COALESCE(consumed_qty, 0::numeric)), 3) AS total_consumed_qty, + string_agg( + concat( + COALESCE(raw_product_default_code, raw_product_id::text, 'sin_codigo'), + ' - ', + COALESCE(raw_product_name, 'sin_nombre'), + ': ', + COALESCE(consumed_qty::text, 'sin_cantidad'), + ' ', + COALESCE(raw_uom, '') + ), + '; ' + ORDER BY raw_product_name + ) AS raw_materials_summary, + round(sum(COALESCE(material_cost_estimated_eur, 0::numeric)), 4) AS material_cost_estimated_eur, + count(*) FILTER (WHERE material_cost_estimated_eur IS NOT NULL) AS costed_raw_material_line_count, + count(*) FILTER ( + WHERE material_cost_status <> 'FROM_RAW_MATERIAL_COST_PARAMETER' + AND material_cost_status <> 'FROM_ODOO_STOCK_MOVE_VALUE' + ) AS missing_or_invalid_raw_material_cost_count, + string_agg(DISTINCT material_cost_status, ', ' ORDER BY material_cost_status) AS material_cost_status_summary +FROM mv_reports_ucepsa_prod.order_raw_material_costs_v1 +GROUP BY + tenant, + site, + odoo_instance, + odoo_production_id, + odoo_order_ref, + sale_order_ref, + customer_name, + finished_product_default_code, + finished_product_name; + +-- ============================================================ +-- 5) Vista de costes pendientes de parametrizar +-- ============================================================ + +CREATE OR REPLACE VIEW mv_reports_ucepsa_prod.raw_material_cost_requirements_v1 AS +SELECT + tenant, + site, + raw_product_id, + raw_product_default_code, + raw_product_name, + raw_uom, + count(*) AS affected_order_lines, + count(DISTINCT odoo_order_ref) AS affected_orders, + string_agg(DISTINCT odoo_order_ref, ', ' ORDER BY odoo_order_ref) AS affected_order_refs, + round(sum(COALESCE(consumed_qty, 0::numeric)), 3) AS total_consumed_qty, + string_agg(DISTINCT customer_name, ', ' ORDER BY customer_name) AS affected_customers, + max(material_cost_status) AS current_status, + 'Cargar coste €/kg en mv_edge_oee_ucepsa_prod.raw_material_cost_parameters' AS recommended_action +FROM mv_reports_ucepsa_prod.order_raw_material_costs_v1 +WHERE material_cost_status IN ( + 'MISSING_RAW_MATERIAL_COST_PARAMETER', + 'MISSING_RAW_PRODUCT_ID', + 'MISSING_CONSUMED_QTY', + 'UNSUPPORTED_RAW_UOM' +) +GROUP BY + tenant, + site, + raw_product_id, + raw_product_default_code, + raw_product_name, + raw_uom; + +GRANT SELECT ON mv_reports_ucepsa_prod.order_raw_material_costs_v1 TO grafana_ucepsa_ro; +GRANT SELECT ON mv_reports_ucepsa_prod.order_raw_material_costs_summary_v1 TO grafana_ucepsa_ro; +GRANT SELECT ON mv_reports_ucepsa_prod.raw_material_cost_requirements_v1 TO grafana_ucepsa_ro; + +COMMIT; diff --git a/ucepsa/edge-oee-demo/tools/mqtt_auth_probe.py b/ucepsa/edge-oee-demo/tools/mqtt_auth_probe.py new file mode 100755 index 0000000..b506479 --- /dev/null +++ b/ucepsa/edge-oee-demo/tools/mqtt_auth_probe.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 +import os +import time +from pathlib import Path + +import paho.mqtt.client as mqtt + + +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 + k, v = line.split("=", 1) + env[k.strip()] = v.strip().strip('"').strip("'") + return env + + +env = read_env() + +host = os.getenv("MQTT_HOST_OVERRIDE") or env.get("MQTT_HOST", "127.0.0.1") +port = int(os.getenv("MQTT_PORT_OVERRIDE") or env.get("MQTT_PORT", "1883")) + +user = ( + os.getenv("MQTT_USER_OVERRIDE") + or os.getenv("MQTT_USERNAME_OVERRIDE") + or os.getenv("MQTT_USER") + or os.getenv("MQTT_USERNAME") + or env.get("MQTT_USER") + or env.get("MQTT_USERNAME") + or env.get("MOSQUITTO_USER") + or env.get("MOSQUITTO_USERNAME") +) + +password = ( + os.getenv("MQTT_PASSWORD_OVERRIDE") + or os.getenv("MQTT_PASS_OVERRIDE") + or os.getenv("MQTT_PASSWORD") + or os.getenv("MQTT_PASS") + or env.get("MQTT_PASSWORD") + or env.get("MQTT_PASS") + or env.get("MOSQUITTO_PASSWORD") + or env.get("MOSQUITTO_PASS") +) + +print("host =", host) +print("port =", port) +print("user_present =", bool(user)) +print("password_present =", bool(password)) + +result = { + "connect_rc": None, + "disconnect_rc": None, +} + + +def on_connect(client, userdata, flags, rc, properties=None): + result["connect_rc"] = rc + print("on_connect rc =", rc) + + +def on_disconnect(client, userdata, rc, properties=None): + result["disconnect_rc"] = rc + print("on_disconnect rc =", rc) + + +client = mqtt.Client(client_id="mqtt_auth_probe") +if user: + client.username_pw_set(user, password) + +client.on_connect = on_connect +client.on_disconnect = on_disconnect + +client.connect(host, port, 60) +client.loop_start() + +deadline = time.time() + 5 +while time.time() < deadline and result["connect_rc"] is None: + time.sleep(0.1) + +client.loop_stop() +client.disconnect() + +if result["connect_rc"] != 0: + raise SystemExit(f"MQTT auth fallida. connect_rc={result['connect_rc']}") + +print("OK: MQTT conectado correctamente") diff --git a/ucepsa/edge-oee-demo/tools/odoo_margin_commercial_sync.py b/ucepsa/edge-oee-demo/tools/odoo_margin_commercial_sync.py new file mode 100755 index 0000000..1450c1a --- /dev/null +++ b/ucepsa/edge-oee-demo/tools/odoo_margin_commercial_sync.py @@ -0,0 +1,528 @@ +#!/usr/bin/env python3 +import argparse +import json +import os +import sys +import xmlrpc.client +from datetime import datetime, timezone + + +try: + import psycopg2 + import psycopg2.extras +except Exception as exc: + print(f"ERROR: psycopg2 no disponible en esta imagen Python: {exc}", file=sys.stderr) + sys.exit(2) + + +TENANT = os.getenv("TENANT_OVERRIDE", "ucepsa") +SITE = os.getenv("SITE_OVERRIDE", "ucepsa_onpremise") +ODOO_INSTANCE = os.getenv("ODOO_INSTANCE_OVERRIDE", "odoo_ucepsa_prod") +ODOO_DB = os.getenv("ODOO_DB_OVERRIDE") or os.getenv("ODOO_DB") +ODOO_URL = (os.getenv("ODOO_URL") or "").rstrip("/") +ODOO_USER = os.getenv("ODOO_USER") +ODOO_PASSWORD = os.getenv("ODOO_PASSWORD") + +PGHOST = os.getenv("POSTGRES_HOST") or os.getenv("PGHOST") or "mv_ucepsa_postgres_hot" +PGPORT = int(os.getenv("POSTGRES_PORT") or os.getenv("PGPORT") or "5432") +PGDATABASE = os.getenv("POSTGRES_DB") or os.getenv("PGDATABASE") +PGUSER = os.getenv("POSTGRES_USER") or os.getenv("PGUSER") +PGPASSWORD = os.getenv("POSTGRES_PASSWORD") or os.getenv("PGPASSWORD") + + +def require_env(): + missing = [] + for name, value in { + "ODOO_URL": ODOO_URL, + "ODOO_DB/ODOO_DB_OVERRIDE": ODOO_DB, + "ODOO_USER": ODOO_USER, + "ODOO_PASSWORD": ODOO_PASSWORD, + "POSTGRES_DB": PGDATABASE, + "POSTGRES_USER": PGUSER, + "POSTGRES_PASSWORD": PGPASSWORD, + }.items(): + if not value: + missing.append(name) + + if missing: + raise RuntimeError("Faltan variables de entorno: " + ", ".join(missing)) + + +def odoo_connect(): + common = xmlrpc.client.ServerProxy(f"{ODOO_URL}/xmlrpc/2/common", allow_none=True) + uid = common.authenticate(ODOO_DB, ODOO_USER, ODOO_PASSWORD, {}) + if not uid: + raise RuntimeError("No se pudo autenticar contra Odoo") + models = xmlrpc.client.ServerProxy(f"{ODOO_URL}/xmlrpc/2/object", allow_none=True) + return uid, models + + +def odoo_call(models, uid, model, method, *args, **kwargs): + return models.execute_kw(ODOO_DB, uid, ODOO_PASSWORD, model, method, list(args), kwargs or {}) + + +def pg_connect(): + return psycopg2.connect( + host=PGHOST, + port=PGPORT, + dbname=PGDATABASE, + user=PGUSER, + password=PGPASSWORD, + ) + + +def as_id(value): + if isinstance(value, list) and value: + return value[0] + if isinstance(value, tuple) and value: + return value[0] + if isinstance(value, int): + return value + return None + + +def as_name(value): + if isinstance(value, list) and len(value) > 1: + return value[1] + if isinstance(value, tuple) and len(value) > 1: + return value[1] + return None + + +def read_map_by_ids(models, uid, model, ids, fields): + ids = [int(x) for x in ids if x] + if not ids: + return {} + rows = odoo_call(models, uid, model, "read", ids, fields=fields) + return {r["id"]: r for r in rows} + + +def extract_default_code(display_name): + if not display_name: + return None + if display_name.startswith("[") and "]" in display_name: + return display_name[1:display_name.index("]")] + return None + + +def build_row(prod, product, sale_line, sale_order, raw_moves, finished_moves): + product_name = as_name(prod.get("product_id")) or (product or {}).get("display_name") + product_default_code = (product or {}).get("default_code") or extract_default_code(product_name) + + product_standard_price = product.get("standard_price") if product else None + product_list_price = product.get("list_price") if product else None + product_uom = as_name((product or {}).get("uom_id")) or as_name(prod.get("product_uom_id")) + + sale_order_ref = None + sale_order_id = None + customer_id = None + customer_name = None + sale_state = None + sale_date_order = None + currency = None + + sale_line_qty = None + sale_line_qty_delivered = None + sale_price_unit = None + sale_discount_pct = None + revenue_net_eur = None + revenue_total_eur = None + sale_line_id = None + + if sale_line: + sale_line_id = sale_line.get("id") + sale_line_qty = sale_line.get("product_uom_qty") + sale_line_qty_delivered = sale_line.get("qty_delivered") + sale_price_unit = sale_line.get("price_unit") + sale_discount_pct = sale_line.get("discount") + revenue_net_eur = sale_line.get("price_subtotal") + revenue_total_eur = sale_line.get("price_total") + + if sale_order: + sale_order_id = sale_order.get("id") + sale_order_ref = sale_order.get("name") + sale_state = sale_order.get("state") + sale_date_order = sale_order.get("date_order") + customer_id = as_id(sale_order.get("partner_id")) + customer_name = as_name(sale_order.get("partner_id")) + currency = as_name(sale_order.get("currency_id")) + + raw_material_value_eur = sum([float(m.get("value") or 0.0) for m in raw_moves]) + finished_value_eur = sum([float(m.get("value") or 0.0) for m in finished_moves]) + + if raw_material_value_eur and raw_material_value_eur != 0: + material_cost_status = "FROM_ODOO_STOCK_MOVE_VALUE" + else: + material_cost_status = "UNAVAILABLE_OR_ZERO" + + raw_json = { + "production": prod, + "product": product, + "sale_line": sale_line, + "sale_order": sale_order, + } + + return { + "tenant": TENANT, + "site": SITE, + "odoo_instance": ODOO_INSTANCE, + "odoo_production_id": prod.get("id"), + "odoo_order_ref": prod.get("name"), + "odoo_origin": prod.get("origin"), + "odoo_state": prod.get("state"), + "product_id": as_id(prod.get("product_id")), + "product_default_code": product_default_code, + "product_name": product_name, + "product_uom": product_uom, + "product_standard_price": product_standard_price, + "product_list_price": product_list_price, + "production_qty": prod.get("product_qty"), + "qty_producing": prod.get("qty_producing"), + "production_uom": as_name(prod.get("product_uom_id")), + "sale_line_id": sale_line_id, + "sale_order_id": sale_order_id, + "sale_order_ref": sale_order_ref, + "customer_id": customer_id, + "customer_name": customer_name, + "sale_state": sale_state, + "sale_date_order": sale_date_order, + "currency": currency, + "sale_line_qty": sale_line_qty, + "sale_line_qty_delivered": sale_line_qty_delivered, + "sale_price_unit": sale_price_unit, + "sale_discount_pct": sale_discount_pct, + "revenue_net_eur": revenue_net_eur, + "revenue_total_eur": revenue_total_eur, + "raw_material_value_eur": raw_material_value_eur, + "finished_value_eur": finished_value_eur, + "material_cost_status": material_cost_status, + "stock_raw_moves_json": raw_moves, + "stock_finished_moves_json": finished_moves, + "raw_json": raw_json, + } + + +def upsert_rows(conn, rows): + if not rows: + return 0 + + sql = """ + INSERT INTO mv_edge_oee_ucepsa_prod.odoo_order_commercials ( + tenant, + site, + odoo_instance, + odoo_production_id, + odoo_order_ref, + odoo_origin, + odoo_state, + product_id, + product_default_code, + product_name, + product_uom, + product_standard_price, + product_list_price, + production_qty, + qty_producing, + production_uom, + sale_line_id, + sale_order_id, + sale_order_ref, + customer_id, + customer_name, + sale_state, + sale_date_order, + currency, + sale_line_qty, + sale_line_qty_delivered, + sale_price_unit, + sale_discount_pct, + revenue_net_eur, + revenue_total_eur, + raw_material_value_eur, + finished_value_eur, + material_cost_status, + stock_raw_moves_json, + stock_finished_moves_json, + raw_json, + snapshot_at + ) + VALUES ( + %(tenant)s, + %(site)s, + %(odoo_instance)s, + %(odoo_production_id)s, + %(odoo_order_ref)s, + %(odoo_origin)s, + %(odoo_state)s, + %(product_id)s, + %(product_default_code)s, + %(product_name)s, + %(product_uom)s, + %(product_standard_price)s, + %(product_list_price)s, + %(production_qty)s, + %(qty_producing)s, + %(production_uom)s, + %(sale_line_id)s, + %(sale_order_id)s, + %(sale_order_ref)s, + %(customer_id)s, + %(customer_name)s, + %(sale_state)s, + %(sale_date_order)s, + %(currency)s, + %(sale_line_qty)s, + %(sale_line_qty_delivered)s, + %(sale_price_unit)s, + %(sale_discount_pct)s, + %(revenue_net_eur)s, + %(revenue_total_eur)s, + %(raw_material_value_eur)s, + %(finished_value_eur)s, + %(material_cost_status)s, + %(stock_raw_moves_json)s, + %(stock_finished_moves_json)s, + %(raw_json)s, + now() + ) + ON CONFLICT (odoo_instance, odoo_production_id) + DO UPDATE SET + tenant = EXCLUDED.tenant, + site = EXCLUDED.site, + odoo_order_ref = EXCLUDED.odoo_order_ref, + odoo_origin = EXCLUDED.odoo_origin, + odoo_state = EXCLUDED.odoo_state, + product_id = EXCLUDED.product_id, + product_default_code = EXCLUDED.product_default_code, + product_name = EXCLUDED.product_name, + product_uom = EXCLUDED.product_uom, + product_standard_price = EXCLUDED.product_standard_price, + product_list_price = EXCLUDED.product_list_price, + production_qty = EXCLUDED.production_qty, + qty_producing = EXCLUDED.qty_producing, + production_uom = EXCLUDED.production_uom, + sale_line_id = EXCLUDED.sale_line_id, + sale_order_id = EXCLUDED.sale_order_id, + sale_order_ref = EXCLUDED.sale_order_ref, + customer_id = EXCLUDED.customer_id, + customer_name = EXCLUDED.customer_name, + sale_state = EXCLUDED.sale_state, + sale_date_order = EXCLUDED.sale_date_order, + currency = EXCLUDED.currency, + sale_line_qty = EXCLUDED.sale_line_qty, + sale_line_qty_delivered = EXCLUDED.sale_line_qty_delivered, + sale_price_unit = EXCLUDED.sale_price_unit, + sale_discount_pct = EXCLUDED.sale_discount_pct, + revenue_net_eur = EXCLUDED.revenue_net_eur, + revenue_total_eur = EXCLUDED.revenue_total_eur, + raw_material_value_eur = EXCLUDED.raw_material_value_eur, + finished_value_eur = EXCLUDED.finished_value_eur, + material_cost_status = EXCLUDED.material_cost_status, + stock_raw_moves_json = EXCLUDED.stock_raw_moves_json, + stock_finished_moves_json = EXCLUDED.stock_finished_moves_json, + raw_json = EXCLUDED.raw_json, + snapshot_at = now() + """ + + prepared = [] + for row in rows: + r = dict(row) + r["stock_raw_moves_json"] = psycopg2.extras.Json(r["stock_raw_moves_json"]) + r["stock_finished_moves_json"] = psycopg2.extras.Json(r["stock_finished_moves_json"]) + r["raw_json"] = psycopg2.extras.Json(r["raw_json"]) + prepared.append(r) + + with conn.cursor() as cur: + psycopg2.extras.execute_batch(cur, sql, prepared, page_size=50) + + return len(rows) + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--production-id", type=int, action="append", help="ID Odoo mrp.production concreto") + parser.add_argument("--order-ref", action="append", help="Referencia WH/MO/xxxxx concreta") + parser.add_argument("--limit", type=int, default=50) + parser.add_argument("--states", default="confirmed,progress,to_close,done", help="Estados mrp.production separados por coma") + parser.add_argument("--dry-run", action="store_true") + args = parser.parse_args() + + require_env() + + uid, models = odoo_connect() + print(f"Conectado a Odoo: url={ODOO_URL} db={ODOO_DB} user={ODOO_USER} uid={uid}", flush=True) + + domain = [] + if args.production_id: + domain.append(("id", "in", args.production_id)) + elif args.order_ref: + domain.append(("name", "in", args.order_ref)) + else: + states = [s.strip() for s in args.states.split(",") if s.strip()] + domain.append(("state", "in", states)) + + prod_fields = [ + "id", + "name", + "state", + "origin", + "product_id", + "product_qty", + "qty_producing", + "product_uom_id", + "bom_id", + "move_raw_ids", + "move_finished_ids", + "sale_line_id", + "date_start", + "date_finished", + "company_id", + ] + + production_ids = odoo_call( + models, + uid, + "mrp.production", + "search", + domain, + limit=args.limit, + order="id desc", + ) + + print(f"Producciones encontradas: {len(production_ids)} -> {production_ids}", flush=True) + + if not production_ids: + return 0 + + productions = odoo_call(models, uid, "mrp.production", "read", production_ids, fields=prod_fields) + + product_ids = [as_id(p.get("product_id")) for p in productions if as_id(p.get("product_id"))] + sale_line_ids = [as_id(p.get("sale_line_id")) for p in productions if as_id(p.get("sale_line_id"))] + raw_move_ids = [] + finished_move_ids = [] + + for p in productions: + raw_move_ids.extend(p.get("move_raw_ids") or []) + finished_move_ids.extend(p.get("move_finished_ids") or []) + + products = read_map_by_ids( + models, + uid, + "product.product", + product_ids, + [ + "id", + "display_name", + "default_code", + "standard_price", + "list_price", + "uom_id", + "categ_id", + "barcode", + "product_tmpl_id", + ], + ) + + sale_lines = read_map_by_ids( + models, + uid, + "sale.order.line", + sale_line_ids, + [ + "id", + "name", + "order_id", + "product_id", + "product_uom_qty", + "qty_delivered", + "price_unit", + "discount", + "price_subtotal", + "price_total", + ], + ) + + sale_order_ids = [as_id(sl.get("order_id")) for sl in sale_lines.values() if as_id(sl.get("order_id"))] + + sale_orders = read_map_by_ids( + models, + uid, + "sale.order", + sale_order_ids, + [ + "id", + "name", + "state", + "origin", + "date_order", + "partner_id", + "currency_id", + "amount_total", + "amount_untaxed", + "client_order_ref", + "order_line", + ], + ) + + move_fields = [ + "id", + "state", + "value", + "quantity", + "price_unit", + "product_id", + "product_uom", + "production_id", + "product_uom_qty", + "raw_material_production_id", + ] + + raw_moves_by_id = read_map_by_ids(models, uid, "stock.move", raw_move_ids, move_fields) + finished_moves_by_id = read_map_by_ids(models, uid, "stock.move", finished_move_ids, move_fields) + + rows = [] + for p in productions: + product = products.get(as_id(p.get("product_id"))) + sale_line = sale_lines.get(as_id(p.get("sale_line_id"))) + sale_order = sale_orders.get(as_id(sale_line.get("order_id"))) if sale_line else None + + raw_moves = [raw_moves_by_id[mid] for mid in (p.get("move_raw_ids") or []) if mid in raw_moves_by_id] + finished_moves = [finished_moves_by_id[mid] for mid in (p.get("move_finished_ids") or []) if mid in finished_moves_by_id] + + row = build_row(p, product, sale_line, sale_order, raw_moves, finished_moves) + rows.append(row) + + print(json.dumps( + [ + { + "odoo_order_ref": r["odoo_order_ref"], + "odoo_production_id": r["odoo_production_id"], + "product": r["product_name"], + "sale_order_ref": r["sale_order_ref"], + "customer_name": r["customer_name"], + "revenue_net_eur": r["revenue_net_eur"], + "raw_moves": len(r["stock_raw_moves_json"]), + "finished_moves": len(r["stock_finished_moves_json"]), + "material_cost_status": r["material_cost_status"], + } + for r in rows + ], + indent=2, + ensure_ascii=False, + default=str, + ), flush=True) + + if args.dry_run: + print("DRY RUN: no se escribe en PostgreSQL", flush=True) + return 0 + + with pg_connect() as conn: + count = upsert_rows(conn, rows) + conn.commit() + + print(f"Filas insertadas/actualizadas en mv_edge_oee_ucepsa_prod.odoo_order_commercials: {count}", flush=True) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/ucepsa/edge-oee-demo/tools/odoo_order_state_publisher.py b/ucepsa/edge-oee-demo/tools/odoo_order_state_publisher.py new file mode 100755 index 0000000..b7c1d0f --- /dev/null +++ b/ucepsa/edge-oee-demo/tools/odoo_order_state_publisher.py @@ -0,0 +1,388 @@ +#!/usr/bin/env python3 +import argparse +import json +import os +import sys +import time +import xmlrpc.client +from datetime import datetime, timezone +from pathlib import Path + + +MACHINE_MAP = { + "CORT-00": "Cortadora 0", + "CORT-01": "Cortadora 01", + "CORT-02": "Cortadora 02", +} + +VALID_LOADED_STATES = ["progress"] +DIAGNOSTIC_STATES = ["waiting", "ready", "progress"] + + +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 + k, v = line.split("=", 1) + env[k.strip()] = v.strip().strip('"').strip("'") + + return env + + +def utc_now(): + return datetime.now(timezone.utc).isoformat(timespec="seconds").replace("+00:00", "Z") + + +def m2o_id(value): + if isinstance(value, list) and len(value) >= 1: + return value[0] + return None + + +def m2o_name(value): + if isinstance(value, list) and len(value) >= 2: + return value[1] + return None + + +def connect_odoo(env): + url = env["ODOO_URL"].rstrip("/") + db = os.getenv("ODOO_DB_OVERRIDE") or env["ODOO_DB"] + user = env["ODOO_USER"] + password = env["ODOO_PASSWORD"] + + common = xmlrpc.client.ServerProxy(f"{url}/xmlrpc/2/common") + uid = common.authenticate(db, user, password, {}) + + if not uid: + raise RuntimeError("Autenticación Odoo fallida") + + models = xmlrpc.client.ServerProxy(f"{url}/xmlrpc/2/object") + + return { + "url": url, + "db": db, + "user": user, + "password": password, + "uid": uid, + "models": models, + } + + +def existing_fields(odoo, model, wanted): + fields = odoo["models"].execute_kw( + odoo["db"], + odoo["uid"], + odoo["password"], + model, + "fields_get", + [], + {"attributes": ["string", "type"]}, + ) + return [f for f in wanted if f in fields] + + +def search_read(odoo, model, domain, wanted_fields, limit=50, order="id desc"): + fields = existing_fields(odoo, model, wanted_fields) + + return odoo["models"].execute_kw( + odoo["db"], + odoo["uid"], + odoo["password"], + model, + "search_read", + [domain], + { + "fields": fields, + "limit": limit, + "order": order, + }, + ) + + +def get_workcenter(odoo, wc_name): + rows = search_read( + odoo, + "mrp.workcenter", + [("name", "=", wc_name)], + ["id", "name", "code", "active", "display_name"], + limit=5, + order="id asc", + ) + + if not rows: + return None + + if len(rows) > 1: + # Esto no debería pasar con nombres exactos, pero lo devolvemos como incoherencia. + return { + "ambiguous": True, + "rows": rows, + } + + row = rows[0] + row["ambiguous"] = False + return row + + +def get_workorders_for_wc(odoo, wc_id, states): + return search_read( + odoo, + "mrp.workorder", + [ + ("workcenter_id", "=", wc_id), + ("state", "in", states), + ], + [ + "id", + "name", + "state", + "workcenter_id", + "production_id", + "product_id", + "date_start", + "date_finished", + "duration_expected", + "duration", + "qty_production", + ], + limit=100, + order="id desc", + ) + + +def build_order_state(machine_id, wc_name, wc, loaded_workorders, diagnostic_workorders): + ts = utc_now() + + if wc is None: + return { + "machine_id": machine_id, + "ts": ts, + "source": "odoo_order_state_publisher", + "order_present": False, + "order_coherent": False, + "odoo_state": "workcenter_not_found", + "reason": f"No existe centro de trabajo exacto: {wc_name}", + "workcenter_name": wc_name, + } + + if wc.get("ambiguous"): + return { + "machine_id": machine_id, + "ts": ts, + "source": "odoo_order_state_publisher", + "order_present": True, + "order_coherent": False, + "odoo_state": "ambiguous_workcenter", + "reason": f"Más de un centro de trabajo exacto para {wc_name}", + "workcenter_name": wc_name, + "workcenters": wc.get("rows", []), + } + + ready_count = sum(1 for wo in diagnostic_workorders if wo.get("state") == "ready") + waiting_count = sum(1 for wo in diagnostic_workorders if wo.get("state") == "waiting") + progress_count = len(loaded_workorders) + + base = { + "machine_id": machine_id, + "ts": ts, + "source": "odoo_order_state_publisher", + "workcenter_id": wc.get("id"), + "workcenter_name": wc.get("name"), + "workcenter_display_name": wc.get("display_name"), + "loaded_state_rule": VALID_LOADED_STATES, + "diagnostics": { + "waiting_count": waiting_count, + "ready_count": ready_count, + "progress_count": progress_count, + }, + } + + if progress_count == 0: + base.update({ + "order_present": False, + "order_coherent": False, + "odoo_state": "no_loaded_order", + "reason": "No hay orden en progress para esta cortadora", + "workorder_id": None, + "production_order": None, + "product": None, + }) + return base + + if progress_count > 1: + base.update({ + "order_present": True, + "order_coherent": False, + "odoo_state": "multiple_loaded_orders", + "reason": "Hay más de una orden en progress para esta cortadora", + "workorders": [ + { + "workorder_id": wo.get("id"), + "state": wo.get("state"), + "production_order": m2o_name(wo.get("production_id")), + "product": m2o_name(wo.get("product_id")), + "date_start": wo.get("date_start"), + "qty_production": wo.get("qty_production"), + } + for wo in loaded_workorders + ], + }) + return base + + wo = loaded_workorders[0] + + base.update({ + "order_present": True, + "order_coherent": True, + "odoo_state": "loaded", + "reason": "Existe exactamente una orden en progress para esta cortadora", + "workorder_id": wo.get("id"), + "workorder_name": wo.get("name"), + "workorder_state": wo.get("state"), + "production_id": m2o_id(wo.get("production_id")), + "production_order": m2o_name(wo.get("production_id")), + "product_id": m2o_id(wo.get("product_id")), + "product": m2o_name(wo.get("product_id")), + "date_start": wo.get("date_start"), + "date_finished": wo.get("date_finished"), + "qty_production": wo.get("qty_production"), + }) + + return base + + +def build_all_states(odoo): + result = {} + + for machine_id, wc_name in MACHINE_MAP.items(): + wc = get_workcenter(odoo, wc_name) + + if wc is None or wc.get("ambiguous"): + result[machine_id] = build_order_state(machine_id, wc_name, wc, [], []) + continue + + loaded_workorders = get_workorders_for_wc(odoo, wc["id"], VALID_LOADED_STATES) + diagnostic_workorders = get_workorders_for_wc(odoo, wc["id"], DIAGNOSTIC_STATES) + + result[machine_id] = build_order_state( + machine_id, + wc_name, + wc, + loaded_workorders, + diagnostic_workorders, + ) + + return result + + +def publish_mqtt(env, states): + try: + import paho.mqtt.client as mqtt + except ImportError: + raise RuntimeError("Falta paho-mqtt. Instala dependencias o ejecuta dentro de contenedor.") + + mqtt_host = os.getenv("MQTT_HOST_OVERRIDE") or env.get("MQTT_HOST", "127.0.0.1") + mqtt_port = int(os.getenv("MQTT_PORT_OVERRIDE") or env.get("MQTT_PORT", "1883")) + mqtt_user = ( + os.getenv("MQTT_USER_OVERRIDE") + or os.getenv("MQTT_USERNAME_OVERRIDE") + or env.get("MQTT_USERNAME") + or env.get("MQTT_USER") + or env.get("MQTT_USER_REVPI") + or env.get("MOSQUITTO_USERNAME") + or env.get("MOSQUITTO_USER") + ) + mqtt_password = ( + os.getenv("MQTT_PASSWORD_OVERRIDE") + or os.getenv("MQTT_PASS_OVERRIDE") + or os.getenv("MQTT_PASSWORD") + or os.getenv("MQTT_PASS") + or env.get("MQTT_PASSWORD") + or env.get("MQTT_PASS") + or env.get("MQTT_PASSWORD_REVPI") + or env.get("MOSQUITTO_PASSWORD") + or env.get("MOSQUITTO_PASS") + ) + + tenant = env.get("TENANT", "ucepsa") + site = os.getenv("SITE_OVERRIDE") or env.get("SITE", "ucepsa_onpremise") + vertical = env.get("VERTICAL", "edge_oee") + + client = mqtt.Client(client_id="odoo_order_state_publisher") + + if mqtt_user: + client.username_pw_set(mqtt_user, mqtt_password) + + client.connect(mqtt_host, mqtt_port, 60) + client.loop_start() + + for machine_id, payload in states.items(): + topic = f"vertical/{tenant}/{site}/{vertical}/order_state/{machine_id}" + info = client.publish( + topic, + json.dumps(payload, separators=(",", ":"), ensure_ascii=False), + qos=0, + retain=True, + ) + info.wait_for_publish(timeout=5) + print(f"MQTT published retained: {topic}") + + client.loop_stop() + client.disconnect() + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--publish", action="store_true", help="Publica en MQTT retained") + parser.add_argument("--loop", action="store_true", help="Ejecuta en bucle") + parser.add_argument("--interval", type=float, default=5.0) + parser.add_argument("--json", action="store_true", help="Salida JSON compacta") + args = parser.parse_args() + + env = read_env() + odoo = connect_odoo(env) + + print(f"ODOO_URL={odoo['url']}", flush=True) + print(f"ODOO_DB={odoo['db']}", flush=True) + print(f"ODOO_USER={odoo['user']}", flush=True) + print(f"publish={args.publish}", flush=True) + + while True: + states = build_all_states(odoo) + + if args.json: + print(json.dumps(states, indent=2, ensure_ascii=False), flush=True) + else: + print() + print("=" * 100) + print(utc_now()) + print("=" * 100) + for machine_id, payload in states.items(): + print( + f"{machine_id}: " + f"present={payload.get('order_present')} " + f"coherent={payload.get('order_coherent')} " + f"state={payload.get('odoo_state')} " + f"mo={payload.get('production_order')} " + f"product={payload.get('product')} " + f"diag={payload.get('diagnostics')}" + ) + + if args.publish: + publish_mqtt(env, states) + + if not args.loop: + break + + time.sleep(args.interval) + + +if __name__ == "__main__": + main() diff --git a/ucepsa/edge-oee-demo/tools/probe_odoo_cutters.py b/ucepsa/edge-oee-demo/tools/probe_odoo_cutters.py new file mode 100755 index 0000000..fb43505 --- /dev/null +++ b/ucepsa/edge-oee-demo/tools/probe_odoo_cutters.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python3 +import os +import sys +import xmlrpc.client +from pathlib import Path +from datetime import datetime + +MACHINE_MAP = { + "CORT-00": "Cortadora 0", + "CORT-01": "Cortadora 01", + "CORT-02": "Cortadora 02", +} + +WORKORDER_STATES = ["waiting", "ready", "progress", "done"] + + +def read_env(path=".env"): + env = {} + p = Path(path) + for raw in p.read_text().splitlines(): + line = raw.strip() + if not line or line.startswith("#") or "=" not in line: + continue + k, v = line.split("=", 1) + env[k.strip()] = v.strip().strip('"').strip("'") + return env + + +def existing_fields(models, db, uid, password, model, wanted): + fields = models.execute_kw( + db, uid, password, + model, "fields_get", + [], + {"attributes": ["string", "type"]}, + ) + return [f for f in wanted if f in fields] + + +def search_read(models, db, uid, password, model, domain, fields, limit=20, order="id asc"): + fields = existing_fields(models, db, uid, password, model, fields) + return models.execute_kw( + db, uid, password, + model, "search_read", + [domain], + {"fields": fields, "limit": limit, "order": order}, + ) + + +def main(): + env = read_env() + + url = env["ODOO_URL"].rstrip("/") + db = os.getenv("ODOO_DB_OVERRIDE") or env["ODOO_DB"] + user = env["ODOO_USER"] + password = env["ODOO_PASSWORD"] + + print("ODOO_URL =", url) + print("ODOO_DB =", db) + print("ODOO_USER=", user) + print() + + common = xmlrpc.client.ServerProxy(f"{url}/xmlrpc/2/common") + uid = common.authenticate(db, user, password, {}) + if not uid: + raise SystemExit("ERROR: autenticación Odoo fallida") + + print("uid =", uid) + print() + + models = xmlrpc.client.ServerProxy(f"{url}/xmlrpc/2/object") + + wc_fields = existing_fields( + models, db, uid, password, + "mrp.workcenter", + ["id", "name", "code", "active", "display_name"], + ) + + wo_fields = existing_fields( + models, db, uid, password, + "mrp.workorder", + [ + "id", + "name", + "state", + "workcenter_id", + "production_id", + "product_id", + "date_start", + "date_finished", + "duration_expected", + "duration", + "qty_production", + ], + ) + + print("Campos workcenter disponibles:", wc_fields) + print("Campos workorder disponibles :", wo_fields) + print() + + for machine_id, wc_name in MACHINE_MAP.items(): + print("=" * 80) + print(f"{machine_id} -> {wc_name}") + + workcenters = search_read( + models, db, uid, password, + "mrp.workcenter", + [("name", "=", wc_name)], + ["id", "name", "code", "active", "display_name"], + limit=5, + order="id asc", + ) + + if not workcenters: + print("ERROR: no se encuentra centro de trabajo exacto") + continue + + if len(workcenters) > 1: + print("AVISO: hay más de un centro exacto") + + wc = workcenters[0] + wc_id = wc["id"] + print("Workcenter:", wc) + + workorders = search_read( + models, db, uid, password, + "mrp.workorder", + [ + ("workcenter_id", "=", wc_id), + ("state", "in", WORKORDER_STATES), + ], + [ + "id", + "name", + "state", + "workcenter_id", + "production_id", + "product_id", + "date_start", + "date_finished", + "duration_expected", + "duration", + "qty_production", + ], + limit=20, + order="id desc", + ) + + if not workorders: + print("Sin workorders candidatas") + continue + + print(f"Workorders candidatas: {len(workorders)}") + for wo in workorders: + print( + " -", + { + "id": wo.get("id"), + "name": wo.get("name"), + "state": wo.get("state"), + "production_id": wo.get("production_id"), + "product_id": wo.get("product_id"), + "date_start": wo.get("date_start"), + "date_finished": wo.get("date_finished"), + "qty_production": wo.get("qty_production"), + }, + ) + + +if __name__ == "__main__": + main() diff --git a/ucepsa/edge-oee-demo/tools/read_order_state_retained.py b/ucepsa/edge-oee-demo/tools/read_order_state_retained.py new file mode 100755 index 0000000..22bdbb5 --- /dev/null +++ b/ucepsa/edge-oee-demo/tools/read_order_state_retained.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 +import json +import os +import time +from pathlib import Path + +import paho.mqtt.client as mqtt + + +def read_env(path=".env"): + env = {} + p = Path(path) + for raw in p.read_text().splitlines(): + line = raw.strip() + if not line or line.startswith("#") or "=" not in line: + continue + k, v = line.split("=", 1) + env[k.strip()] = v.strip().strip('"').strip("'") + return env + + +env = read_env() + +mqtt_host = os.getenv("MQTT_HOST_OVERRIDE") or env.get("MQTT_HOST", "127.0.0.1") +mqtt_port = int(os.getenv("MQTT_PORT_OVERRIDE") or env.get("MQTT_PORT", "1883")) +mqtt_user = ( + os.getenv("MQTT_USER_OVERRIDE") + or os.getenv("MQTT_USERNAME_OVERRIDE") + or env.get("MQTT_USERNAME") + or env.get("MQTT_USER") + or env.get("MQTT_USER_REVPI") + or env.get("MOSQUITTO_USERNAME") + or env.get("MOSQUITTO_USER") + ) +mqtt_password = ( + os.getenv("MQTT_PASSWORD_OVERRIDE") + or os.getenv("MQTT_PASS_OVERRIDE") + or os.getenv("MQTT_PASSWORD") + or os.getenv("MQTT_PASS") + or env.get("MQTT_PASSWORD") + or env.get("MQTT_PASS") + or env.get("MQTT_PASSWORD_REVPI") + or env.get("MOSQUITTO_PASSWORD") + or env.get("MOSQUITTO_PASS") + ) + +tenant = env.get("TENANT", "ucepsa") +site = os.getenv("SITE_OVERRIDE") or env.get("SITE", "ucepsa_onpremise") +vertical = env.get("VERTICAL", "edge_oee") + +topic = f"vertical/{tenant}/{site}/{vertical}/order_state/+" + +messages = {} + + +def on_connect(client, userdata, flags, rc, properties=None): + print(f"MQTT conectado rc={rc}") + print(f"SUB {topic}") + client.subscribe(topic) + + +def on_message(client, userdata, msg): + machine_id = msg.topic.rsplit("/", 1)[-1] + try: + payload = json.loads(msg.payload.decode("utf-8")) + except Exception: + payload = msg.payload.decode("utf-8", errors="replace") + + messages[machine_id] = { + "topic": msg.topic, + "retain": bool(msg.retain), + "payload": payload, + } + + +client = mqtt.Client(client_id="read_order_state_retained") + +if mqtt_user: + client.username_pw_set(mqtt_user, mqtt_password) + +client.on_connect = on_connect +client.on_message = on_message + +client.connect(mqtt_host, mqtt_port, 60) +client.loop_start() + +deadline = time.time() + 5 +while time.time() < deadline and len(messages) < 3: + time.sleep(0.2) + +client.loop_stop() +client.disconnect() + +print(json.dumps(messages, indent=2, ensure_ascii=False)) diff --git a/ucepsa/edge-oee-demo/tools/watch_odoo_active_workorders.py b/ucepsa/edge-oee-demo/tools/watch_odoo_active_workorders.py new file mode 100755 index 0000000..8d78e32 --- /dev/null +++ b/ucepsa/edge-oee-demo/tools/watch_odoo_active_workorders.py @@ -0,0 +1,188 @@ +#!/usr/bin/env python3 +import os +import time +import sys +import xmlrpc.client +from pathlib import Path +from datetime import datetime + +MACHINE_MAP = { + "CORT-00": "Cortadora 0", + "CORT-01": "Cortadora 01", + "CORT-02": "Cortadora 02", +} + +WATCH_STATES = ["waiting", "ready", "progress"] + + +def read_env(path=".env"): + env = {} + p = Path(path) + for raw in p.read_text().splitlines(): + line = raw.strip() + if not line or line.startswith("#") or "=" not in line: + continue + k, v = line.split("=", 1) + env[k.strip()] = v.strip().strip('"').strip("'") + return env + + +def existing_fields(models, db, uid, password, model, wanted): + fields = models.execute_kw( + db, uid, password, + model, "fields_get", + [], + {"attributes": ["string", "type"]}, + ) + return [f for f in wanted if f in fields] + + +def search_read(models, db, uid, password, model, domain, fields, limit=50, order="id desc"): + valid_fields = existing_fields(models, db, uid, password, model, fields) + return models.execute_kw( + db, uid, password, + model, "search_read", + [domain], + { + "fields": valid_fields, + "limit": limit, + "order": order, + }, + ) + + +def m2o_name(value): + if isinstance(value, list) and len(value) >= 2: + return value[1] + return None + + +def connect(): + env = read_env() + + url = env["ODOO_URL"].rstrip("/") + db = os.getenv("ODOO_DB_OVERRIDE") or env["ODOO_DB"] + user = env["ODOO_USER"] + password = env["ODOO_PASSWORD"] + + common = xmlrpc.client.ServerProxy(f"{url}/xmlrpc/2/common") + uid = common.authenticate(db, user, password, {}) + if not uid: + raise RuntimeError("Autenticación Odoo fallida") + + models = xmlrpc.client.ServerProxy(f"{url}/xmlrpc/2/object") + return url, db, user, password, uid, models + + +def print_state(models, db, uid, password): + print() + print("=" * 100) + print(datetime.now().strftime("%Y-%m-%d %H:%M:%S")) + print("=" * 100) + + for machine_id, wc_name in MACHINE_MAP.items(): + workcenters = search_read( + models, db, uid, password, + "mrp.workcenter", + [("name", "=", wc_name)], + ["id", "name", "active", "display_name"], + limit=5, + order="id asc", + ) + + if not workcenters: + print(f"{machine_id} | {wc_name} | ERROR: no existe centro de trabajo") + continue + + wc = workcenters[0] + wc_id = wc["id"] + + workorders = search_read( + models, db, uid, password, + "mrp.workorder", + [ + ("workcenter_id", "=", wc_id), + ("state", "in", WATCH_STATES), + ], + [ + "id", + "name", + "state", + "workcenter_id", + "production_id", + "product_id", + "date_start", + "date_finished", + "qty_production", + ], + limit=100, + order="id desc", + ) + + by_state = {s: [] for s in WATCH_STATES} + for wo in workorders: + by_state.setdefault(wo.get("state"), []).append(wo) + + progress = by_state.get("progress", []) + ready = by_state.get("ready", []) + waiting = by_state.get("waiting", []) + + if len(progress) == 1: + beacon_state = "AZUL_FIJO_CANDIDATO" + elif len(progress) > 1: + beacon_state = "INCOHERENTE_MULTIPLES_PROGRESS" + else: + beacon_state = "SIN_ORDEN_CARGADA" + + print( + f"{machine_id} | {wc_name} | " + f"waiting={len(waiting)} ready={len(ready)} progress={len(progress)} | " + f"{beacon_state}" + ) + + for wo in progress: + print( + f" ACTIVE progress: " + f"wo_id={wo.get('id')} " + f"mo={m2o_name(wo.get('production_id'))} " + f"product={m2o_name(wo.get('product_id'))} " + f"date_start={wo.get('date_start')}" + ) + + if not progress and ready: + first = ready[0] + print( + f" primera ready: " + f"wo_id={first.get('id')} " + f"mo={m2o_name(first.get('production_id'))} " + f"product={m2o_name(first.get('product_id'))}" + ) + + +def main(): + watch = False + interval = 2.0 + + if "--watch" in sys.argv: + watch = True + idx = sys.argv.index("--watch") + if len(sys.argv) > idx + 1: + interval = float(sys.argv[idx + 1]) + + url, db, user, password, uid, models = connect() + + print(f"ODOO_URL={url}") + print(f"ODOO_DB={db}") + print(f"ODOO_USER={user}") + print(f"uid={uid}") + + if watch: + while True: + print_state(models, db, uid, password) + time.sleep(interval) + else: + print_state(models, db, uid, password) + + +if __name__ == "__main__": + main()