Cliente Python para la API de Wialon que funciona

Siarhei Havarunou – CEO

·

Un cliente Python moderno para la Remote API de Wialon: login, búsqueda, eventos en vivo con avl_evts, y qué librerías siguen vivas y cuáles murieron.

A minimal Wialon Remote API client in Python, with live events over avl_evts

Cada pocos meses alguien pregunta qué librería de Python usar con Wialon. La respuesta honesta es incómoda: las dos librerías con los nombres más reconocibles — python-wialon y php-wialon, en la organización wialon de GitHub — no se tocan desde aproximadamente 2013. Todavía funcionan para llamadas básicas, pero son anteriores a la mitad de la superficie de la API y a todo el conocimiento operativo. Esta guía le da un cliente mínimo que usted controla, en unas setenta líneas; después, los cinco comportamientos que deciden si sobrevive al contacto con una cuenta real; y por último un mapa honesto del panorama de librerías.

El protocolo, para que el cliente tenga sentido

Wialon usa POST con formularios codificados, no REST con JSON: cada llamada va a wialon/ajax.html con un campo svc que nombra el servicio, un campo params con JSON y un campo sid una vez que tiene sesión. No hay rutas, ni verbos, ni códigos de estado que leer — un HTTP 200 con {"error":7} en el cuerpo es la forma en que llega la denegación de acceso. (La mecánica completa está en la página de configuración de Postman.) Ese es todo el protocolo, y por eso un wrapper es un objeto de sesión alrededor de requests y nada más.

import requests

class Wialon:
    def __init__(self, host="https://hst-api.wialon.com", token=None):
        self.host = host
        self.base = host + "/wialon/ajax.html"
        self.sid = None
        self.token = token

    def call(self, svc, params=None, sid=True):
        data = {"svc": svc, "params": params or {}}
        if sid:
            data["sid"] = self.sid
        r = requests.post(self.base, data=data, timeout=30)
        r.raise_for_status()
        return r.json()

    def login(self):
        res = self.call("token/login", {"token": self.token}, sid=False)
        if "error" in res:
            raise RuntimeError(f"login failed: {res}")
        self.sid = res["eid"]
        return self.sid

    def events(self):
        # avl_evts is its own path, not an svc against ajax.html.
        r = requests.post(f"{self.host}/avl_evts", data={"sid": self.sid}, timeout=30)
        r.raise_for_status()
        return r.json()

Setenta líneas es una ventaja, no una limitación: no hay magia que depurar a las 3 a. m. cuando una sincronización se traba. Genere el token en la interfaz de Wialon con los flags de acceso más estrechos que cubran sus lecturas — la página del token tiene la tabla de flags — y manténgalo fuera del código, en una variable de entorno.

Con qué host está hablando realmente

hst-api.wialon.com es el endpoint de Hosting, y es el valor por defecto correcto solo si su cuenta vive en Wialon Hosting. Una instalación de Wialon Local responde en el dominio propio del partner, y un token generado contra un host no significa nada contra el otro — la sesión la emite el servidor que tiene la cuenta. Por eso host es un argumento del constructor más arriba y no una constante del módulo: el día que sume un segundo cliente en otro despliegue, lo único que debería cambiar es el valor que pasa.

Vale la pena nombrar desde ya dos errores relacionados. El primero es dejar hst-api.wialon.com fijo en un cliente que piensa vender a partners, lo que limita sin avisar su producto a un solo despliegue. El segundo es tratar el host de la interfaz y el host de la API como la misma cadena; están relacionados pero no son intercambiables, y copiar una URL del navegador a la configuración del cliente produce un 404 que parece una falla de red.

Primero buscar, después mensajes

Dos llamadas cargan con la mayoría de las integraciones. core/search_items encuentra cosas; messages/load_interval lee su historial:

w = Wialon(token="...")
w.login()

units = w.call("core/search_items", {
    "spec": {
        "itemsType": "avl_unit",
        "propName": "sys_name",
        "propValueMask": "*",
        "sortType": "sys_name",
    },
    "force": 1,
    "flags": 1,
    "from": 0,
    "to": 0,
})
unit_id = units["items"][0]["id"]

msgs = w.call("messages/load_interval", {
    "itemId": unit_id,
    "timeFrom": 1726000000,
    "timeTo": 1726086400,
    "flags": 0,
    "flagsMask": 0xFF00,
    "loadCount": 10000,
})

Cuatro detalles de esa primera llamada deciden lo que obtiene. itemsType elige el catálogo: avl_unit para unidades, avl_resource para recursos, avl_unit_group para grupos, user para usuarios. propName y propValueMask son el filtro, y sys_name con * es la expresión idiomática para “todo lo que tengo permiso de ver”. sortType le da un orden estable, que importa más de lo que parece: sin él, al paginar una cuenta grande puede ver la misma unidad dos veces y perderse otra. Y flags no es un booleano sino una máscara de bits de data-flags: 1 devuelve el conjunto base (id, nombre, clase, unidades de medida), mientras que sensores, contadores y última posición son bits aparte, cada uno con costo en el tamaño de la respuesta. La referencia de unidades trae la tabla completa; la regla a interiorizar es pedir la máscara más estrecha que responda su pregunta, porque en una cuenta de tres mil unidades la diferencia entre el flag 1 y una máscara codiciosa se mide en megabytes por consulta.

La paginación es lo segundo que hay que hacer bien. from y to son índices de fila, no marcas de tiempo, y la respuesta le dice dónde está: totalItemsCount junto con indexFrom e indexTo. Pedir de 0 a 0 devuelve una sola fila, lo cual sirve para el ejemplo de arriba y es incorrecto para producción; recorra el índice en bloques de unos cientos y deténgase cuando indexTo llegue a totalItemsCount.

Dos trampas viven en la segunda llamada. Primero, flagsMask selecciona qué tipos de mensaje vuelven y flags indica cuáles de ellos quiere: el par documentado para mensajes de datos es la máscara 0xFF00 con flags 0x0000, y pedir todo en una unidad activa devuelve megabytes que no quería. Segundo, loadCount limita el lote: itere avanzando timeFrom en lugar de subirlo, o una sola unidad longeva superará el presupuesto de cinco minutos de los reportes y le devolverá cero filas que parecen una semana tranquila.

Eventos en vivo sin matarse a polling

Para casi tiempo real, no consulte load_interval con un temporizador. Suscriba la sesión con core/update_data_flags y luego haga polling de avl_evts, el canal de eventos propio de Wialon. Atención a la dirección: avl_evts no es un svc contra wialon/ajax.html sino una ruta propia, {host}/avl_evts?sid=<sid>, y por eso el cliente necesita el segundo método de arriba en lugar de otro call():

import time

w.call("core/update_data_flags", {
    "spec": [{"type": "type", "data": "avl_unit", "flags": 1, "mode": 0}],
})

while True:
    for e in w.events().get("events", []):
        handle(e)  # your code: queue it, do not process inline
    time.sleep(1)

Conviene leer con atención la entrada de spec. type dice cómo selecciona los elementos: "type" suscribe a una clase entera, y la alternativa selecciona una colección específica de ids, que es lo que querrá cuando la cuenta sea tan grande que “todas las unidades” sea más de lo que necesita. flags es la misma máscara de data-flags de la búsqueda, así que los eventos que recibe traen solo los campos que pidió. mode 0 reemplaza lo que tuviera suscrito la sesión, lo que convierte la resuscripción tras una reconexión en una única llamada idempotente y no en una diferencia que usted deba calcular.

Cada evento trae un id de elemento, un tipo y una carga útil: un mensaje nuevo, una actualización del elemento, una eliminación. La respuesta incluye además el reloj del propio servidor, y usarlo en lugar del reloj de su proceso elimina toda una clase de bugs de “el evento llegó antes de ocurrir” en máquinas con la hora desfasada.

Dos límites, ambos publicados, dan forma a este bucle: no más de 10 solicitudes a avl_evts cada 10 segundos por sesión, y diez solicitudes simultáneas a la API por sesión en total. Un bucle de un segundo ya gasta todo el presupuesto de eventos, así que esta sesión no debería hacer nada más — ejecute las extracciones en una segunda sesión. Y el bucle de arriba es delgado a propósito: mueve los eventos a una cola y vuelve al polling. Procesar en línea es la manera en que las integraciones se atrasan en las horas pico y luego golpean la API al ponerse al día, que es justamente el modo de falla del que trata el artículo sobre diseño de sync resiliente. Si necesita push de verdad en lugar de polling, lea por qué Wialon no tiene webhooks antes de diseñar la arquitectura alrededor de esa ausencia.

La sesión muere en silencio, con reloj

Una sesión de Wialon no es un bearer token de larga vida. Se cierra tras cinco minutos sin solicitudes, lo que significa que una integración que ejecuta un trabajo por hora tiene una sesión muerta cincuenta y cinco minutos de cada sesenta y lo descubre en el peor momento. El bucle de eventos de arriba nunca lo nota, porque hace polling cada segundo; un lote nocturno lo nota todas las noches.

Hay tres defensas y conviene tener las tres. Mantenga la sesión activa si el proceso es longevo: cualquier llamada barata dentro de la ventana sirve, y avl_evts es la convencional porque además es lo que ya estaba consultando. Trate el error 1 como una instrucción de volver a iniciar sesión exactamente una vez y repetir la llamada, nunca como algo para reintentar en un bucle. Y renueve el token mismo bien dentro de la regla de cien días de inactividad descrita en la página del token, porque una sesión expirada y un token eliminado aparecen ambos como error 1 pero requieren arreglos completamente distintos.

El patrón que falla en producción es la sesión compartida de larga vida: un login al iniciar el proceso, un sid entregado a todos los workers y ningún camino de reautenticación. Funciona hasta el primer corte de red, y entonces todos los workers fallan a la vez con el mismo código sin que nada en su aplicación haya cambiado para explicarlo.

Errores, clasificados antes de reintentar

Wialon devuelve los errores como un HTTP 200 con un código en el cuerpo, así que su cliente debe buscarlos explícitamente — raise_for_status() nunca se disparará. Los códigos caen en cuatro grupos, y el grupo decide el comportamiento:

CódigosClaseQué debe hacer el cliente
1, 1011Sesión perdidaVolver a iniciar sesión una vez, repetir la llamada una vez, y luego rendirse
4Entrada inválidaNo reintentar: es un bug en sus parámetros
7Acceso denegadoNo reintentar: al token le falta un flag, o el elemento no es suyo
10Límite de concurrenciaEsperar y reintentar; superó el tope por sesión
1001Sin mensajes en el intervaloNo es un error: es un resultado vacío para esa ventana
1005Tiempo de ejecución excedidoDividir el intervalo y probar las mitades

La distinción que más tiempo ahorra es la que separa el 4 del 7. El error 4 significa que la solicitud estaba mal formada para el servicio: un campo faltante, una cadena donde va un número, un id que no se puede interpretar. El error 7 significa que la solicitud estaba bien formada y usted no tiene permiso para hacerla, lo que casi siempre se debe a los flags de acceso del token y no a algo del elemento. Reintentar cualquiera de los dos es puro desperdicio, y reintentar el error 1 en un bucle es la forma de que bloqueen una dirección de salida. La referencia de errores trae la tabla completa y las cadenas de motivo que califican los códigos.

Una solicitud en lugar de cuarenta

Cuando necesita la misma llamada para muchos elementos — leer un contador en cada unidad, actualizar un campo en toda una flota — core/batch las envía juntas:

res = w.call("core/batch", {
    "params": [
        {"svc": "core/search_item", "params": {"id": uid, "flags": 1}}
        for uid in unit_ids
    ],
    "flags": 0,
})

flags 0 ejecuta todos los comandos sin importar las fallas y devuelve un resultado por comando; flags 1 se detiene en el primer error, tras lo cual los comandos restantes devuelven el error 10. Prefiera 0 para lecturas, de modo que un id malo no le cueste los otros treinta y nueve resultados, y 1 para escrituras que no deben aplicarse a medias.

Agrupar también es el arreglo más barato para el tope de concurrencia que sigue: cuarenta llamadas en una solicitud ocupan un lugar, no cuarenta.

La concurrencia pertenece a la sesión, no a su pool

Tres límites publicados gobiernan cuánto puede empujar, y los tres son por sesión, no por cuenta ni por IP: diez solicitudes simultáneas a la API, tres solicitudes pesadas simultáneas (la carga de mensajes y la ejecución de reportes son los ejemplos que da la documentación) y los diez avl_evts cada diez segundos ya mencionados. Un pool de veinte hilos que comparten una sesión no va el doble de rápido que uno de diez: empieza a recolectar el error 10.

La forma que funciona es una sesión por worker, con un token por worker y no un token por ejecución. Los tokens tienen un tope por usuario, así que generar uno nuevo en cada ejecución termina agotando el cupo y el trabajo empieza a fallar incluso al autenticarse. Y mantenga el bucle de eventos en una sesión propia: es barato, es constante, y compartirlo con una extracción es la manera de que un backfill nocturno deje sin recursos su feed en tiempo real.

Qué registrar, desde el primer día

Cuatro campos hacen tratable cualquier incidente posterior: el nombre del servicio, los milisegundos transcurridos, el código de error si lo hay y los límites del intervalo para cualquier cosa que lea historial. Agregue una huella corta del id de sesión — nunca el id en sí — y podrá distinguir “la sesión murió” de “la llamada estaba mal” con una búsqueda en los logs y no con una sesión de depuración. Lo único que no debe registrar es el token.

El panorama de librerías, mapeado con honestidad

  • python-wialon / php-wialon (organización wialon, ~2013). Nombres reconocibles, proyectos muertos. Sirven como referencia de la forma de las llamadas, no como dependencias.
  • py-aiowialon, wialon, wialon-sdk (PyPI, de terceros). Wrappers asíncronos o de conveniencia, de vigencia variable. Bien para scripts; audite el manejo de sesión antes de confiarle a uno la sincronización de producción: la renovación del token y el re-login ante el error 1/4 es donde suelen recortar.
  • Sus propias setenta líneas (arriba). Lo que nosotros corremos en producción detrás de una cola. Usted es dueño de la política de reintentos, el backoff y el registro, que son las partes que de verdad se rompen.

El patrón en las tres: el HTTP es trivial, la operación no. Las sesiones expiran, los tokens se revocan tras 100 días de inactividad, las llamadas pesadas se limitan a tres a la vez por sesión. Quien sea dueño de su código de cliente es dueño de esos comportamientos: asegúrese de que sea usted, no un paquete abandonado.

Qué sigue

Con un cliente y una cola en mano, las siguientes preguntas son de arquitectura: una sincronización que sobreviva a los límites de tasa, llevar los datos a PostgreSQL y empujarlos después hacia un ERP. Si esa cola empieza a parecer un producto y no un script, eso es lo que construye nuestra práctica de integración.

Más de Asset Track

Hablemos

  • “Our client needed a data pipeline. It came back working, plus a few Wialon fixes we had not asked for. That client trusts us more now.”
    Faiz K. Customer Manager · Trakpro Limited