40-clients/ucepsa/edge-oee-demo/tools/order_state_pg_sink.py
2026-06-22 12:54:08 +02:00

248 lines
8.6 KiB
Python
Executable File

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