Files
sensgw/sensgw/protocols/snmp.py
2025-12-16 12:13:43 +06:00

153 lines
4.7 KiB
Python

# sensgw/protocols/snmp.py
from __future__ import annotations
import datetime as dt
from dataclasses import dataclass
from ..models import Endpoint, Device, Channel
from ..writer import Writer
from .polling import poll_forever
@dataclass(frozen=True)
class SnmpBinding:
endpoint: Endpoint
device: Device
channel: Channel
oid: str
datatype: str # "float" | "int" | ...
def _parse_numeric(datatype: str, raw: str) -> float:
kind = (datatype or "float").strip().lower()
if kind == "int":
return float(int(raw))
return float(raw)
def _parse_version(conn: dict) -> int:
"""
Return mpModel:
SNMPv1 -> 0
SNMPv2c -> 1
"""
v = str(conn.get("version", "2c")).lower()
if v in {"1", "v1", "snmpv1"}:
return 0
return 1
class SnmpEndpointCollector:
def __init__(self, writer: Writer, default_poll_s: int):
self.writer = writer
self.default_poll_s = default_poll_s
async def _get_many(
self,
*,
host: str,
port: int,
community: str,
mp_model: int,
timeout_s: int,
oids: list[str],
) -> dict[str, str]:
from pysnmp.hlapi.v3arch.asyncio import ( # type: ignore
SnmpEngine,
CommunityData,
UdpTransportTarget,
ContextData,
ObjectType,
ObjectIdentity,
get_cmd,
)
snmp_engine = SnmpEngine()
try:
var_binds = [ObjectType(ObjectIdentity(oid)) for oid in oids]
# In pysnmp 7.x, target creation is async:
target = await UdpTransportTarget.create((host, port), timeout=timeout_s, retries=0)
iterator = get_cmd(
snmp_engine,
CommunityData(community, mpModel=mp_model),
target,
ContextData(),
*var_binds,
)
error_indication, error_status, error_index, out_binds = await iterator
if error_indication:
raise RuntimeError(str(error_indication))
if error_status:
raise RuntimeError(
f"{error_status.prettyPrint()} at "
f"{out_binds[int(error_index) - 1][0] if error_index else '?'}"
)
return {str(name): str(val) for name, val in out_binds}
finally:
snmp_engine.close_dispatcher()
async def run_endpoint(self, endpoint: Endpoint, bindings: list[SnmpBinding]) -> None:
host = endpoint.conn["host"]
port = int(endpoint.conn.get("port", 161))
community = endpoint.conn.get("community", "public")
timeout_s = int(endpoint.conn.get("timeout_s", 2))
mp_model = _parse_version(endpoint.conn)
intervals = [
int(b.channel.poll_interval_s)
for b in bindings
if b.channel.poll_interval_s is not None
]
interval_s = min(intervals) if intervals else self.default_poll_s
oid_to_binding: dict[str, SnmpBinding] = {b.oid.strip(): b for b in bindings}
oids = list(oid_to_binding.keys())
async def read_once() -> None:
ts = dt.datetime.now(dt.timezone.utc)
try:
values = await self._get_many(
host=host,
port=port,
community=community,
mp_model=mp_model,
timeout_s=timeout_s,
oids=oids,
)
for oid_str, raw in values.items():
b = oid_to_binding.get(oid_str)
if b is None:
continue
try:
v = _parse_numeric(b.datatype, raw)
v = v * b.channel.scale_value + b.channel.offset_value
await self.writer.write_metric(
ts=ts,
device_id=b.device.device_id,
location_id=b.device.location_id,
metric=b.channel.metric,
value=v,
)
except Exception as e:
await self.writer.write_error(
device_id=b.device.device_id,
error=f"snmp parse/write: {e}",
)
except Exception as e:
# Endpoint-level failure: mark all devices as error
for b in bindings:
await self.writer.write_error(
device_id=b.device.device_id,
error=f"snmp endpoint: {e}",
)
await poll_forever(interval_s=interval_s, read_once=read_once)