Squashed 'core/' content from commit 92ec910
git-subtree-dir: core git-subtree-split: 92ec910a132e379a3a6e442a75bcb07cac0f0010
This commit is contained in:
87
backend/app/collectors/modbus_tcp.py
Normal file
87
backend/app/collectors/modbus_tcp.py
Normal file
@@ -0,0 +1,87 @@
|
||||
"""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])
|
||||
Reference in New Issue
Block a user