Skip to main content

README - jmq_olt_huawei

Paquete de integración con OLTs Huawei MA56XXT (como MA5603T) vía Telnet y SNMP asíncrono, diseñado para automatizar la recolección de información GPON desde Python.

🛰️ ¿Qué hace este paquete?

Permite conectarse a una OLT Huawei MA56XXT y ejecutar operaciones como:

  • Listar slots activos (display board 0)
  • Consultar puertos GPON por slot
  • Obtener ONTs conectadas a cada puerto, incluyendo:
    • Potencia TX (ptx) y RX (prx) vía SNMP asíncrono
  • Realizar un escaneo completo (slots → puertos → ONTs)
  • Manejar paginación, prompts dinámicos y errores comunes de sesión (bloqueo de usuario)

📦 Instalación

Requiere Python ≥ 3.7 y PySNMP ≥ 6.2 (para usar el HLAPI asíncrono).

pip install jmq_olt_huawei

o bien:

pip install git+https://github.com/juaquicar/jmq_olt_huawei.git

Para desarrollo en modo editable:

pip install -e .

📁 Estructura del paquete

jmq_olt_huawei/
│
├── ma56xxt.py          # Lógica principal de conexión Telnet y SNMP asíncrono
├── __init__.py         # Archivo de inicialización del paquete
├── Examples/           # Scripts de ejemplo (opcional)
├── tests/              # Pruebas automatizadas (pytest)
├── requirements.txt    # Dependencias de desarrollo / CI
├── pyproject.toml      # Configuración de build
├── LICENSE             # Licencia MIT
└── README.md           # Este archivo

🧪 Ejemplo de uso

A continuación se muestra un ejemplo mínimo de cómo usar APIMA56XXT con llamadas asíncronas para obtener la lista completa de ONTs y sus potencias SNMP.

import asyncio
from jmq_olt_huawei.ma56xxt import APIMA56XXT, UserBusyError
from pprint import pprint

async def main():
    api = APIMA56XXT(
        host='192.168.88.25',
        user='root',
        password='admin',
        prompt='MA5603T',
        snmp_ip='192.168.88.25',
        snmp_port=161,
        snmp_community='public',
        debug=True
    )

    try:
        # Conexión Telnet (síncrona)
        api.connect()
        # Escaneo completo (async)
        full = await api.scan_all()
        pprint(full)
    except UserBusyError as e:
        print(f"ERROR: {e}")
    finally:
        api.close()  # Cierre de Telnet

if __name__ == '__main__':
    asyncio.run(main())

Flujo de ejemplo

  1. api.connect() abre una sesión Telnet, hace login, entra en modo enable y config.

  2. await api.scan_all() recorre:

    • get_slots() (síncrono, Telnet)

    • Para cada slot tipo GPBD: get_ports(slot) (síncrono)

    • Para cada puerto con optical_state == 'Online': await get_onts(slot, port_id) (asíncrono)

      • Cada ONT “online” dispara consultas SNMP asíncronas a await _snmp_potencia_tx(...) y await _snmp_potencia_rx(...).
  3. Devuelve una lista jerárquica de slots → puertos → ONTs con campos SNMP (ptx, prx).


🔍 Manual de uso de los métodos de APIMA56XXT

1. __init__(host, user, password, prompt, snmp_ip, snmp_port, snmp_community, timeout=2.0, debug=False)

Inicializa la clase:

  • host (str): IP o hostname de la OLT.
  • user (str): Usuario Telnet.
  • password (str): Contraseña Telnet.
  • prompt (str): Prefijo del prompt (p.ej. "MA5603T").
  • snmp_ip (str): IP de la OLT para SNMP.
  • snmp_port (int): Puerto SNMP (por defecto 161).
  • snmp_community (str): Comunidad SNMP (p.ej. "public").
  • timeout (float): Segundos de espera en lecturas Telnet (por defecto 2.0).
  • debug (bool): Si True, imprime logs de depuración.
api = APIMA56XXT(
    host='192.168.88.25',
    user='root',
    password='admin',
    prompt='MA5603T',
    snmp_ip='192.168.88.25',
    snmp_port=161,
    snmp_community='public',
    timeout=2.0,
    debug=True
)

2. connect()

Abre sesión Telnet, hace login y entra en modo operativo:

api.connect()
# Si debug=True:
# [DEBUG] Conectando a 192.168.88.25
# Conectado en modo config
  • No devuelve valor.
  • Lanza excepción (EOFError, socket.error) si la conexión falla.
  • Después de llamar, se puede invocar get_slots(), get_ports(), etc.

3. disconnect()

Sale de los modos CLI (config, enable) y cierra la sesión Telnet:

api.disconnect()
# Si debug=True imprime:
# [DEBUG] Desconectado
  • Protege contra llamadas dobles (si self.tn is None, no hace nada).

4. get_slots() → List[Tuple[str, str]]

Obtiene los slots en la OLT con display board 0:

slots = api.get_slots()
# Ejemplo:
# [('0', 'GPBD'), ('6', 'SCUN'), ('7', 'SCUN'), ('9', 'GICF')]
  • Ejecuta internamente:

    1. _send('display board 0')
    2. _read_until_prompt()
    3. Parsea cada línea para extraer (slot_id, tipo).
  • Es método síncrono (no necesita await).


5. get_ports(slot: str) → List[dict]

Obtiene la lista de puertos GPON de un slot específico:

ports = api.get_ports('0')
# Ejemplo de un diccionario en la lista:
# {
#   'id': 0,
#   'schema_fsp': '0/0/0',
#   'optical_state': 'Online',
#   'port_state': 'Offline',
#   'laser_state': 'Normal',
#   'bw': '1239040',
#   'temperature': '35',
#   'tx_bias': '12',
#   'voltage': '3.22',
#   'tx_power': '3.72',
#   'illegal_rogue_ont': 'Inexistent',
#   'max_distance': '40',
#   'wave_length': '1490',
#   'fiber_type': 'Single Mode',
#   'length': '-'
# }
  • Paso a paso:

    1. _send(f'interface gpon 0/{slot}')
    2. _read_until_prompt()
    3. _send('display port state all')
    4. _read_until_prompt()
    5. Parsea bloques con _parse_port_block.
    6. _send('quit') + _read_until_prompt()
  • Retorna lista de diccionarios, uno por puerto.

  • Método síncrono (no usa await).


6. async get_onts(slot: str, port_id: int) → List[dict]

Devuelve la lista de ONTs conectadas a un puerto GPON:

onts = await api.get_onts('0', 0)
# Ejemplo de un diccionario ONT:
# {
#   'id': 2,
#   'schema_fsp': '0/0/0',
#   'sn': '48575443E010C4AC',
#   'control_flag': 'active',
#   'run_state': 'online',
#   'config_state': 'normal',
#   'match_state': 'match',
#   'protect_side': 'no',
#   'description': 'Cliente01',
#   'ptx': 2.34,      # Potencia TX (mW), float o "" si no está disponible
#   'prx': -21.57     # Potencia RX (dBm), float o "" si no está disponible
# }
  • Internamente:

    1. _send(f'interface gpon 0/{slot}') (Telnet) + _read_until_prompt()

    2. _send(f'display ont info {port_id} all') + _read_until_prompt()

    3. _send('quit') + _read_until_prompt()

    4. Parsea sección principal y descripción con _parse_onts.

    5. Para cada ONT con run_state.lower() == 'online':

      • Llama a await _snmp_potencia_tx(slot, port_id, ont_id)
      • Llama a await _snmp_potencia_rx(slot, port_id, ont_id)
    6. Si SNMP falla, asigna "" a ptx y prx.

  • Debe invocarse con await.

  • Retorna lista de diccionarios ONT, incluyendo campos SNMP (ptx, prx).


7. async scan_all() → List[dict]

Realiza un escaneo completo de la OLT:

  1. Obtiene todos los slots vía get_slots() (síncrono).

  2. Para cada slot de tipo GPBD:

    • Llama a get_ports(slot) (síncrono).

    • Para cada puerto donde optical_state.lower() == 'online':

      • Llama a await get_onts(slot, port_id) (asíncrono).
    • Agrega un campo 'onts' a cada puerto (lista vacía si está offline).

  3. Devuelve lista de diccionarios, cada uno así:

{
  "id": "0",
  "tipo": "GPBD",
  "ports": [
    {
      "id": 0,
      "schema_fsp": "0/0/0",
      "optical_state": "Online",
      "port_state": "Online",
      "laser_state": "Normal",
      "bw": "1239040",
      "temperature": "35",
      "tx_bias": "12",
      "voltage": "3.22",
      "tx_power": "3.72",
      "illegal_rogue_ont": "Inexistent",
      "max_distance": "40",
      "wave_length": "1490",
      "fiber_type": "Single Mode",
      "length": "-",
      "onts": [
        {
          "id": 2,
          "schema_fsp": "0/0/0",
          "sn": "48575443E010C4AC",
          "control_flag": "active",
          "run_state": "online",
          "config_state": "normal",
          "match_state": "match",
          "protect_side": "no",
          "description": "Cliente01",
          "ptx": 2.34,
          "prx": -21.57
        }
      ]
    },
    {
      "id": 1,
      "schema_fsp": "0/0/1",
      "optical_state": "Offline",
      "port_state": "Offline",
      // …otros campos
      "onts": []
    }
    // … más puertos
  ]
}
  • Debe invocarse con await api.scan_all().
  • Incluye SNMP asíncrono para ONTs “online”.

🛠️ Métodos y privados útiles

  • _read_until_prompt() Lee líneas desde Telnet hasta detectar el prompt dinámico. Atiende paginación (More, ---- More) y errores de login acumulado (UserBusyError).

  • _parse_port_block(slot, bloque) → dict | None Dado un bloque de texto de display port state all, extrae campos como id, optical_state, laser_state, bw, etc.

  • _parse_onts(raw, slot, port_id) → List[dict] Dada la salida de display ont info {port_id} all, extrae la tabla de ONTs y sus descripciones. Devuelve lista de dicts con id, sn, run_state, etc.

  • _calcular_codigo(slot, pon) → str Calcula la parte numérica base del OID SNMP:

    base = 4194304000
    código = base + pon_int * 256 + slot_int * 8192
    devuelve "<código>."
    
  • “Helpers” SNMP asíncronos (async def)

    • _consultagetsnmp(codigo_oid: str) → int | None Crea un UdpTransportTarget con await UdpTransportTarget.create((ip, port), timeout=1, retries=0), luego hace await get_cmd(...).
    • _snmp_potencia_tx(slot, port, ont) → float | "" Llama a await _consultagetsnmp(...) con OID para potencia TX y procesa la respuesta (mW).
    • _snmp_potencia_rx(slot, port, ont) → float | "" Similar para potencia RX (dBm).

✅ Tests

Para ejecutar pruebas unitarias:

pytest -s tests/test_ma56xxt.py
  • Asegúrate de tener un entorno de testing adecuado o mocks para Telnet/SNMP.
  • Los tests deben validar parsing de puertos, ONTs y comportamiento SNMP asíncrono.

🧩 Funcionalidades destacadas

  • Prompt dinámico configurable Detecta automáticamente prompts como MA5603T>, MA5603T(config)#, etc.
  • Manejo de paginación Envía \n si detecta líneas con More, ---- More o Press 'Q'.
  • Bloqueo de usuario Si la OLT informa “Reenter times have reached the upper limit”, lanza UserBusyError.
  • SNMP asíncrono (PySNMP ≥ 6.2) Obtiene potencias TX/RX de cada ONT “online” de forma concurrente sin bloquear la lectura Telnet.
  • Escaneo jerárquico (slots → puertos → ONTs) Permite obtener toda la información en una sola llamada a await scan_all().

📢 Contribuyendo

  1. Haz un fork del repositorio.

  2. Crea una rama:

    git checkout -b feature/nombre-de-funcionalidad
    
  3. Realiza tus cambios, añade tests y actualiza este README si es necesario.

  4. Haz commit y push a tu rama.

  5. Abre un Pull Request describiendo los cambios propuestos.


⚖️ Licencia

MIT © Juanma Quijada


🔗 Enlaces

Metadata

Release files for jmq-olt-huawei 1.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for jmq-olt-huawei 1.0.0
File Size Uploaded
jmq_olt_huawei-1.0.0.tar.gz 15.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for jmq-olt-huawei 1.0.0
File Interpreter ABI Platform
jmq_olt_huawei-1.0.0-py3-none-any.whl Python 3 none any Details

Total release size: 26.7 kB

Release files / jmq_olt_huawei-1.0.0.tar.gz

Download URL jmq_olt_huawei-1.0.0.tar.gz
Size 15.9 kB
Tags Source
SHA-256 checksum
How to use checksums
56ba27b67c81b643392b783c51d052e841c3d9a34bef92ab12c0734debe42994
BLAKE2b-256 checksum
How to use checksums
e668ff6d62305b247e7cdaf278d93002db1d9af8e72da7ebb57fbddd490bda22
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.12.9

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jun 30, 2025.

Transparency log

Release files / jmq_olt_huawei-1.0.0-py3-none-any.whl

Download URL jmq_olt_huawei-1.0.0-py3-none-any.whl
Size 10.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
6ee00686b5e847e06303b73138c6f4a20df9b81f69dea207525b5c4f4286aeff
BLAKE2b-256 checksum
How to use checksums
fce3a4b77e9190f3fe5e52d28c7e3eee08d09860ee765588192e9259598706b4
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.12.9

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jun 30, 2025.

Transparency log

Release history Release notifications | RSS feed

This release

1.0.0 This release

2 release files

0.2.0

2 release files

0.1.2

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page