88 lines
3.0 KiB
Python
88 lines
3.0 KiB
Python
|
|
"""Modbus TCP protocol collector."""
|
||
|
|
import struct
|
||
|
|
from typing import Optional
|
||
|
|
|
||
|
|
from pymodbus.client import AsyncModbusTcpClient
|
||
|
|
|
||
|
|
from app.collectors.base import BaseCollector
|
||
|
|
|
||
|
|
|
||
|
|
class ModbusTcpCollector(BaseCollector):
|
||
|
|
"""Collect data from devices via Modbus TCP.
|
||
|
|
|
||
|
|
connection_params example:
|
||
|
|
{
|
||
|
|
"host": "192.168.1.100",
|
||
|
|
"port": 502,
|
||
|
|
"slave_id": 1,
|
||
|
|
"registers": [
|
||
|
|
{"address": 0, "count": 2, "data_type": "active_power", "scale": 0.1, "unit": "kW"},
|
||
|
|
{"address": 2, "count": 2, "data_type": "voltage", "scale": 0.1, "unit": "V"}
|
||
|
|
]
|
||
|
|
}
|
||
|
|
"""
|
||
|
|
|
||
|
|
def __init__(self, device_id, device_code, connection_params, collect_interval=15):
|
||
|
|
super().__init__(device_id, device_code, connection_params, collect_interval)
|
||
|
|
self._client: Optional[AsyncModbusTcpClient] = None
|
||
|
|
self._host = connection_params.get("host", "127.0.0.1")
|
||
|
|
self._port = connection_params.get("port", 502)
|
||
|
|
self._slave_id = connection_params.get("slave_id", 1)
|
||
|
|
self._registers = connection_params.get("registers", [])
|
||
|
|
|
||
|
|
async def connect(self):
|
||
|
|
self._client = AsyncModbusTcpClient(
|
||
|
|
self._host,
|
||
|
|
port=self._port,
|
||
|
|
timeout=5,
|
||
|
|
)
|
||
|
|
connected = await self._client.connect()
|
||
|
|
if not connected:
|
||
|
|
raise ConnectionError(f"Cannot connect to Modbus TCP {self._host}:{self._port}")
|
||
|
|
|
||
|
|
async def disconnect(self):
|
||
|
|
if self._client:
|
||
|
|
self._client.close()
|
||
|
|
self._client = None
|
||
|
|
|
||
|
|
async def collect(self) -> dict:
|
||
|
|
if not self._client or not self._client.connected:
|
||
|
|
raise ConnectionError("Modbus client not connected")
|
||
|
|
|
||
|
|
data = {}
|
||
|
|
for reg in self._registers:
|
||
|
|
address = reg["address"]
|
||
|
|
count = reg.get("count", 1)
|
||
|
|
data_type = reg["data_type"]
|
||
|
|
scale = reg.get("scale", 1.0)
|
||
|
|
unit = reg.get("unit", "")
|
||
|
|
|
||
|
|
result = await self._client.read_holding_registers(
|
||
|
|
address, count=count, slave=self._slave_id
|
||
|
|
)
|
||
|
|
if result.isError():
|
||
|
|
self.logger.warning(
|
||
|
|
"Modbus read error at address %d for %s: %s",
|
||
|
|
address, self.device_code, result,
|
||
|
|
)
|
||
|
|
continue
|
||
|
|
|
||
|
|
raw_value = self._decode_registers(result.registers, count)
|
||
|
|
value = round(raw_value * scale, 4)
|
||
|
|
data[data_type] = (value, unit)
|
||
|
|
|
||
|
|
return data
|
||
|
|
|
||
|
|
@staticmethod
|
||
|
|
def _decode_registers(registers: list, count: int) -> float:
|
||
|
|
"""Decode register values to a numeric value."""
|
||
|
|
if count == 1:
|
||
|
|
return float(registers[0])
|
||
|
|
elif count == 2:
|
||
|
|
# Two 16-bit registers -> 32-bit float (big-endian)
|
||
|
|
raw = struct.pack(">HH", registers[0], registers[1])
|
||
|
|
return struct.unpack(">f", raw)[0]
|
||
|
|
else:
|
||
|
|
# Fallback: treat as concatenated 16-bit values
|
||
|
|
return float(registers[0])
|