#!/usr/bin/env python3 import hashlib 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 env_first(*names, default=None): for name in names: value = os.getenv(name) if value not in (None, ""): return value return default def to_bool(value): if isinstance(value, bool): return value if value is None: return None return str(value).strip().lower() in ("true", "t", "1", "yes", "y", "si", "sí") def to_int(value): if value in (None, ""): return None try: return int(value) except Exception: return None def parse_ts(value): if not value: return None try: return datetime.fromisoformat(str(value).replace("Z", "+00:00")) except Exception: return None def state_hash(payload): diagnostics = payload.get("diagnostics") or {} relevant = { "machine_id": payload.get("machine_id"), "workcenter_id": payload.get("workcenter_id"), "order_present": payload.get("order_present"), "order_coherent": payload.get("order_coherent"), "odoo_state": payload.get("odoo_state"), "reason": payload.get("reason"), "workorder_id": payload.get("workorder_id"), "production_order": payload.get("production_order"), "product": payload.get("product"), "waiting_count": diagnostics.get("waiting_count"), "ready_count": diagnostics.get("ready_count"), "progress_count": diagnostics.get("progress_count"), } raw = json.dumps(relevant, sort_keys=True, ensure_ascii=False) return hashlib.sha256(raw.encode("utf-8")).hexdigest() TENANT = env_first("TENANT", "TENANT_OVERRIDE", default="ucepsa") SITE = env_first("SITE", "SITE_OVERRIDE", default="ucepsa_onpremise") MQTT_HOST = env_first("MQTT_HOST", "MQTT_HOST_OVERRIDE", default="mv_ucepsa_mosquitto") MQTT_PORT = int(env_first("MQTT_PORT", default="1883")) MQTT_TOPIC = env_first( "MQTT_TOPIC", "ORDER_STATE_TOPIC", default=f"vertical/{TENANT}/{SITE}/edge_oee/order_state/+", ) MQTT_USER = env_first("MQTT_USER", "MQTT_USER_REVPI") MQTT_PASSWORD = env_first("MQTT_PASSWORD", "MQTT_PASSWORD_REVPI") PGHOST = env_first("PGHOST", default="mv_ucepsa_postgres_hot") PGPORT = int(env_first("PGPORT", default="5432")) PGDATABASE = env_first("PGDATABASE", "POSTGRES_DB") PGUSER = env_first("PGUSER", "POSTGRES_USER") PGPASSWORD = env_first("PGPASSWORD", "POSTGRES_PASSWORD") if not PGDATABASE or not PGUSER or not PGPASSWORD: print("ERROR: faltan credenciales PostgreSQL", file=sys.stderr) sys.exit(1) conn = psycopg2.connect( host=PGHOST, port=PGPORT, dbname=PGDATABASE, user=PGUSER, password=PGPASSWORD, ) conn.autocommit = True def upsert_order_state(topic, payload): machine_id = payload.get("machine_id") or topic.rstrip("/").split("/")[-1] diagnostics = payload.get("diagnostics") or {} mqtt_ts = parse_ts(payload.get("ts")) h = state_hash(payload) row = { "tenant": TENANT, "site": SITE, "machine_id": machine_id, "topic": topic, "mqtt_ts": mqtt_ts, "source": payload.get("source"), "workcenter_id": to_int(payload.get("workcenter_id")), "workcenter_name": payload.get("workcenter_name"), "workcenter_display_name": payload.get("workcenter_display_name"), "order_present": to_bool(payload.get("order_present")), "order_coherent": to_bool(payload.get("order_coherent")), "odoo_state": payload.get("odoo_state"), "reason": payload.get("reason"), "workorder_id": str(payload.get("workorder_id")) if payload.get("workorder_id") is not None else None, "production_order": payload.get("production_order"), "product": payload.get("product"), "waiting_count": to_int(diagnostics.get("waiting_count")), "ready_count": to_int(diagnostics.get("ready_count")), "progress_count": to_int(diagnostics.get("progress_count")), "payload_hash": h, "payload_json": payload, } with conn.cursor() as cur: cur.execute( """ INSERT INTO mv_hot.odoo_order_state_current ( tenant, site, machine_id, topic, mqtt_ts, received_at, source, workcenter_id, workcenter_name, workcenter_display_name, order_present, order_coherent, odoo_state, reason, workorder_id, production_order, product, waiting_count, ready_count, progress_count, payload_hash, payload_json ) VALUES ( %(tenant)s, %(site)s, %(machine_id)s, %(topic)s, %(mqtt_ts)s, now(), %(source)s, %(workcenter_id)s, %(workcenter_name)s, %(workcenter_display_name)s, %(order_present)s, %(order_coherent)s, %(odoo_state)s, %(reason)s, %(workorder_id)s, %(production_order)s, %(product)s, %(waiting_count)s, %(ready_count)s, %(progress_count)s, %(payload_hash)s, %(payload_json)s ) ON CONFLICT (tenant, site, machine_id) DO UPDATE SET topic = EXCLUDED.topic, mqtt_ts = EXCLUDED.mqtt_ts, received_at = now(), source = EXCLUDED.source, workcenter_id = EXCLUDED.workcenter_id, workcenter_name = EXCLUDED.workcenter_name, workcenter_display_name = EXCLUDED.workcenter_display_name, order_present = EXCLUDED.order_present, order_coherent = EXCLUDED.order_coherent, odoo_state = EXCLUDED.odoo_state, reason = EXCLUDED.reason, workorder_id = EXCLUDED.workorder_id, production_order = EXCLUDED.production_order, product = EXCLUDED.product, waiting_count = EXCLUDED.waiting_count, ready_count = EXCLUDED.ready_count, progress_count = EXCLUDED.progress_count, payload_hash = EXCLUDED.payload_hash, payload_json = EXCLUDED.payload_json """, {**row, "payload_json": Json(payload)}, ) cur.execute( """ INSERT INTO mv_hot.odoo_order_state_events ( tenant, site, machine_id, topic, mqtt_ts, received_at, odoo_state, order_present, order_coherent, production_order, product, reason, waiting_count, ready_count, progress_count, payload_hash, payload_json ) VALUES ( %(tenant)s, %(site)s, %(machine_id)s, %(topic)s, %(mqtt_ts)s, now(), %(odoo_state)s, %(order_present)s, %(order_coherent)s, %(production_order)s, %(product)s, %(reason)s, %(waiting_count)s, %(ready_count)s, %(progress_count)s, %(payload_hash)s, %(payload_json)s ) ON CONFLICT (tenant, site, machine_id, payload_hash) DO NOTHING """, {**row, "payload_json": Json(payload)}, ) def on_connect(client, userdata, flags, rc, properties=None): if rc != 0: print(f"MQTT connect failed rc={rc}", flush=True) return print(f"MQTT connected. Subscribing to {MQTT_TOPIC}", flush=True) client.subscribe(MQTT_TOPIC, qos=1) def on_message(client, userdata, msg): try: payload = json.loads(msg.payload.decode("utf-8")) upsert_order_state(msg.topic, payload) machine_id = payload.get("machine_id") or msg.topic.rstrip("/").split("/")[-1] print( f"ORDER_STATE {machine_id} " f"state={payload.get('odoo_state')} " f"present={payload.get('order_present')} " f"coherent={payload.get('order_coherent')} " f"mo={payload.get('production_order')}", flush=True, ) except Exception as exc: print(f"ERROR processing message topic={msg.topic}: {exc}", file=sys.stderr, flush=True) client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2) if MQTT_USER: client.username_pw_set(MQTT_USER, MQTT_PASSWORD) client.on_connect = on_connect client.on_message = on_message print(f"Starting order_state_pg_sink MQTT={MQTT_HOST}:{MQTT_PORT} topic={MQTT_TOPIC}", flush=True) client.connect(MQTT_HOST, MQTT_PORT, keepalive=30) client.loop_forever()