Cliente Python para a API do Wialon que funciona

Siarhei Havarunou – CEO

·

Um cliente Python moderno para a Remote API do Wialon: login, busca, eventos ao vivo com avl_evts e quais bibliotecas seguem vivas e quais morreram em 2013.

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

A cada poucos meses alguém pergunta qual biblioteca Python usar com o Wialon. A resposta honesta é desconfortável: as duas bibliotecas com os nomes mais conhecidos — python-wialon e php-wialon, na organização wialon do GitHub — não são mexidas desde cerca de 2013. Elas ainda funcionam para chamadas básicas, mas são anteriores a metade da superfície da API e a todo o conhecimento operacional. Este guia entrega um cliente mínimo que você controla, em cerca de setenta linhas; depois, os cinco comportamentos que decidem se ele sobrevive ao contato com uma conta real; e por fim um mapa honesto do cenário de bibliotecas.

O protocolo, para o cliente fazer sentido

O Wialon usa POST com formulário codificado, não REST com JSON: toda chamada vai para wialon/ajax.html com um campo svc que nomeia o serviço, um campo params com JSON e um campo sid assim que você tem uma sessão. Não há caminhos, verbos nem códigos de status para ler — um HTTP 200 com {"error":7} no corpo é a forma como a negação de acesso chega. (A mecânica completa está na página de configuração do Postman.) Esse é o protocolo inteiro, e é por isso que um wrapper é um objeto de sessão em volta do requests e nada mais.

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 linhas é uma qualidade, não uma limitação: não há mágica para depurar às 3 da manhã quando uma sincronização trava. Gere o token na interface do Wialon com as flags de acesso mais estreitas que cubram suas leituras — a página do token tem a tabela de flags — e mantenha-o fora do código, em uma variável de ambiente.

Com qual host você está de fato falando

hst-api.wialon.com é o endpoint do Hosting, e só é o padrão correto se a sua conta vive no Wialon Hosting. Uma instalação do Wialon Local responde no domínio do próprio parceiro, e um token gerado em um host não significa nada no outro — a sessão é emitida pelo servidor que guarda a conta. Por isso host é um argumento do construtor acima e não uma constante do módulo: no dia em que você adicionar um segundo cliente em outro deployment, a única coisa que deve mudar é o valor que você passa.

Vale nomear desde já dois erros relacionados. O primeiro é deixar hst-api.wialon.com fixo em um cliente que você pretende vender a parceiros, o que limita silenciosamente o seu produto a um único deployment. O segundo é tratar o host da interface e o host da API como a mesma string; eles são relacionados, mas não intercambiáveis, e copiar uma URL do navegador para a configuração do cliente produz um 404 que parece uma falha de rede.

Primeiro buscar, depois mensagens

Duas chamadas carregam a maior parte das integrações. core/search_items encontra coisas; messages/load_interval lê o histórico delas:

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,
})

Quatro detalhes dessa primeira chamada decidem o que você recebe. itemsType escolhe o catálogo: avl_unit para unidades, avl_resource para recursos, avl_unit_group para grupos, user para usuários. propName e propValueMask são o filtro, e sys_name com * é o idioma para “tudo o que tenho permissão de ver”. sortType dá uma ordem estável, o que importa mais do que parece: sem ele, ao paginar uma conta grande você pode ver a mesma unidade duas vezes e perder outra. E flags não é um booleano, e sim uma máscara de bits de data-flags: 1 retorna o conjunto base (id, nome, classe, unidades de medida), enquanto sensores, contadores e última posição são bits separados, cada um com custo no tamanho da resposta. A referência de unidades traz a tabela completa; a regra a internalizar é pedir a máscara mais estreita que responda à sua pergunta, porque, em uma conta de três mil unidades, a diferença entre a flag 1 e uma máscara gulosa se mede em megabytes por consulta.

A paginação é a segunda coisa a acertar. from e to são índices de linha, não timestamps, e a resposta diz onde você está: totalItemsCount junto com indexFrom e indexTo. Pedir de 0 a 0 retorna uma única linha, o que serve para o exemplo acima e está errado para produção; percorra o índice em blocos de algumas centenas e pare quando indexTo chegar a totalItemsCount.

Duas armadilhas moram na segunda chamada. Primeiro, flagsMask seleciona quais tipos de mensagem voltam e flags diz quais deles você quer: o par documentado para mensagens de dados é a máscara 0xFF00 com flags 0x0000, e pedir tudo em uma unidade movimentada devolve megabytes que você não queria. Segundo, loadCount limita o lote: itere avançando timeFrom em vez de aumentá-lo, ou uma única unidade de longa data estourará o orçamento de cinco minutos dos relatórios e devolverá zero linhas que parecem uma semana tranquila.

Eventos ao vivo sem se matar de polling

Para quase tempo real, não consulte load_interval num temporizador. Inscreva a sessão com core/update_data_flags e depois faça polling de avl_evts, o canal de eventos do próprio Wialon. Atenção ao endereço: avl_evts não é um svc contra wialon/ajax.html, e sim um caminho próprio, {host}/avl_evts?sid=<sid>, e é por isso que o cliente precisa do segundo método acima em vez de outro 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)

Vale ler com atenção a entrada de spec. type diz como você seleciona os itens: "type" inscreve em uma classe inteira, e a alternativa seleciona uma coleção específica de ids, que é o que você vai querer quando a conta for grande o bastante para “todas as unidades” ser mais do que você precisa. flags é a mesma máscara de data-flags da busca, então os eventos que você recebe trazem apenas os campos que pediu. mode 0 substitui o que a sessão tinha inscrito, o que transforma a reinscrição após uma reconexão em uma única chamada idempotente, e não em uma diferença que você precise calcular.

Cada evento traz um id de item, um tipo e uma carga: uma mensagem nova, uma atualização do item, uma exclusão. A resposta também traz o relógio do próprio servidor, e usá-lo em vez do relógio do seu processo elimina toda uma classe de bugs do tipo “o evento chegou antes de acontecer” em máquinas com a hora desajustada.

Dois limites, ambos publicados, dão forma a esse laço: no máximo 10 requisições a avl_evts a cada 10 segundos por sessão, e dez requisições simultâneas à API por sessão no total. Um laço de um segundo já gasta todo o orçamento de eventos, então essa sessão não deve fazer mais nada — rode as extrações em uma segunda sessão. E o laço acima é enxuto de propósito: ele move os eventos para uma fila e volta ao polling. Processar em linha é como as integrações ficam para trás nos horários de pico e depois martelam a API para se recuperar, que é exatamente o modo de falha de que trata o artigo sobre design de sync resiliente. Se você precisa de push de verdade em vez de polling, leia por que o Wialon não tem webhooks antes de desenhar a arquitetura em torno dessa ausência.

A sessão morre em silêncio, com relógio

Uma sessão do Wialon não é um bearer token de vida longa. Ela é encerrada após cinco minutos sem requisições, o que significa que uma integração que roda um job por hora mantém uma sessão morta por cinquenta e cinco minutos de cada sessenta e só descobre isso no pior momento. O laço de eventos acima nunca percebe, porque faz polling a cada segundo; um lote noturno percebe toda noite.

Há três defesas e você quer as três. Mantenha a sessão aquecida se o processo for de longa duração — qualquer chamada barata dentro da janela serve, e avl_evts é a convencional porque também é o que você já estava consultando. Trate o erro 1 como uma instrução para fazer login de novo exatamente uma vez e repetir a chamada, nunca como algo para tentar em laço. E renove o próprio token bem dentro da regra de cem dias de inatividade descrita na página do token, porque uma sessão expirada e um token excluído aparecem ambos como erro 1, mas exigem correções completamente diferentes.

O padrão que falha em produção é a sessão compartilhada de longa duração: um login no início do processo, um sid entregue a todos os workers e nenhum caminho de reautenticação. Funciona até a primeira oscilação de rede, e então todos os workers falham ao mesmo tempo com o mesmo código, sem que nada na sua aplicação tenha mudado para explicar.

Erros, classificados antes de repetir

O Wialon devolve erros como um HTTP 200 com um código no corpo, então o seu cliente precisa procurá-los explicitamente — raise_for_status() nunca vai disparar. Os códigos caem em quatro grupos, e o grupo decide o comportamento:

CódigosClasseO que o cliente deve fazer
1, 1011Sessão perdidaFazer login de novo uma vez, repetir a chamada uma vez e então desistir
4Entrada inválidaNão repetir: é um bug nos seus parâmetros
7Acesso negadoNão repetir: falta uma flag no token, ou o item não é seu
10Limite de concorrênciaAguardar e repetir; você excedeu o teto por sessão
1001Sem mensagens no intervaloNão é um erro: é um resultado vazio para aquela janela
1005Tempo de execução excedidoDividir o intervalo e tentar as metades

A distinção que mais poupa tempo é a entre 4 e 7. O erro 4 significa que a requisição estava malformada para o serviço: um campo ausente, uma string onde deveria haver um número, um id que não pode ser interpretado. O erro 7 significa que a requisição estava bem formada e você não tem permissão para fazê-la, o que quase sempre se deve às flags de acesso do token e não a algo do item. Repetir qualquer um dos dois é puro desperdício, e repetir o erro 1 em laço é como um endereço de saída acaba bloqueado. A referência de erros traz a tabela completa e as strings de motivo que qualificam os códigos.

Uma requisição em vez de quarenta

Quando você precisa da mesma chamada para muitos itens — ler um contador em cada unidade, atualizar um campo em uma frota inteira — core/batch as envia 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 executa todos os comandos independentemente de falhas e devolve um resultado por comando; flags 1 para no primeiro erro, e depois disso os comandos restantes devolvem o erro 10. Prefira 0 para leituras, para que um id ruim não custe os outros trinta e nove resultados, e 1 para escritas que não podem ser aplicadas pela metade.

Agrupar também é a correção mais barata para o teto de concorrência a seguir: quarenta chamadas em uma requisição ocupam uma vaga, não quarenta.

A concorrência pertence à sessão, não ao seu pool

Três limites publicados governam o quanto você pode empurrar, e todos os três são por sessão, não por conta nem por IP: dez requisições simultâneas à API, três requisições pesadas simultâneas (o carregamento de mensagens e a execução de relatórios são os exemplos dados na documentação) e os dez avl_evts a cada dez segundos já mencionados. Um pool de vinte threads compartilhando uma sessão não vai duas vezes mais rápido que um de dez: ele começa a colecionar o erro 10.

O formato que funciona é uma sessão por worker, com um token por worker e não um token por execução. Os tokens têm um teto por usuário, então gerar um novo a cada execução acaba esgotando a cota e o job passa a falhar até para se autenticar. E mantenha o laço de eventos em uma sessão própria: ele é barato, é constante, e compartilhá-lo com uma extração é como um backfill noturno deixa o seu feed em tempo real sem recursos.

O que registrar em log, desde o primeiro dia

Quatro campos tornam qualquer incidente posterior tratável: o nome do serviço, os milissegundos decorridos, o código de erro, se houver, e os limites do intervalo para qualquer coisa que leia histórico. Adicione uma impressão digital curta do id da sessão — nunca o id em si — e você consegue distinguir “a sessão morreu” de “a chamada estava errada” com uma busca nos logs, e não com uma sessão de depuração. A única coisa que não se deve registrar é o token.

O cenário de bibliotecas, mapeado com honestidade

  • python-wialon / php-wialon (organização wialon, ~2013). Nomes conhecidos, projetos mortos. Servem como referência do formato das chamadas, não como dependências.
  • py-aiowialon, wialon, wialon-sdk (PyPI, de terceiros). Wrappers assíncronos ou de conveniência, de atualização variável. Bons para scripts; audite o tratamento de sessão antes de confiar um deles à sincronização de produção: a renovação do token e o novo login nos erros 1/4 é onde costumam economizar.
  • Suas próprias setenta linhas (acima). O que rodamos em produção atrás de uma fila. Você é dono da política de novas tentativas, do backoff e do log, que são as partes que de fato quebram.

O padrão nas três: o HTTP é trivial, a operação não. Sessões expiram, tokens são revogados após 100 dias de inatividade, chamadas pesadas são limitadas a três por vez por sessão. Quem é dono do seu código de cliente é dono desses comportamentos — garanta que seja você, não um pacote abandonado.

Próximos passos

Com um cliente e uma fila em mãos, as próximas perguntas são de arquitetura: um sync que sobrevive aos limites de taxa, levar os dados para o PostgreSQL e empurrá-los adiante para um ERP. Se essa fila está começando a parecer um produto e não um script, é isso que a nossa prática de integração constrói.

Mais da Asset Track

Vamos conversar

  • “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