40-clients/ucepsa/edge-oee-demo/tools/odoo_order_state_publisher.py
2026-06-25 18:51:33 +02:00

389 lines
11 KiB
Python
Executable File

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