Skip to content

Commit f75cee3

Browse files
authored
Merge pull request #99 from andrei-lazarov/tcp_proxy
Added Modbus TCP proxy
2 parents 5a3cbb4 + 87e7680 commit f75cee3

2 files changed

Lines changed: 135 additions & 0 deletions

File tree

‎docs/utilities.rst‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,22 @@
22
Utilities
33
===================
44

5+
Solarman TCP Proxy
6+
^^^^^^^^^^^^^^^^^^
7+
8+
.. code-block:: console
9+
10+
user@host:~ $ solarman-tcp-proxy -h
11+
usage: solarman tcp proxy [-h] [-b BIND] [-p PORT] -l LOGGER -s SERIAL
12+
13+
A Modbus TCP Proxy for Solarman loggers
14+
15+
options:
16+
-h, --help show this help message and exit
17+
-b, --bind BIND The address to listen on
18+
-p, --port PORT The TCP port to listen on
19+
-l, --logger LOGGER The IP address of the logger
20+
-s, --serial SERIAL The serial number of the logger
521
622
Solarman RTU Proxy
723
^^^^^^^^^^^^^^^^^^

‎utils/solarman_tcp_proxy.py‎

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
"""Modbus TCP to Solarman proxy
2+
3+
Can be used with Home Assistant's native Modbus integration using config below:
4+
5+
- name: "solarman-modbus-proxy"
6+
type: tcp
7+
host: 192.168.1.20
8+
port: 1502
9+
delay: 3
10+
retry_on_empty: true
11+
sensors:
12+
[...]
13+
14+
"""
15+
16+
import argparse
17+
import asyncio
18+
import struct
19+
from functools import partial
20+
from umodbus.client.serial.redundancy_check import get_crc
21+
from pysolarmanv5 import PySolarmanV5Async
22+
23+
24+
async def handle_client(
25+
reader: asyncio.StreamReader,
26+
writer: asyncio.StreamWriter,
27+
logger_address: str,
28+
logger_serial: int,
29+
):
30+
solarmanv5 = PySolarmanV5Async(
31+
logger_address, logger_serial, verbose=True, auto_reconnect=True
32+
)
33+
await solarmanv5.connect()
34+
35+
addr = writer.get_extra_info("peername")
36+
37+
print(f"{addr}: New connection")
38+
39+
try:
40+
while True:
41+
# Convert TCP to RTU
42+
header = await reader.readexactly(6)
43+
if not header:
44+
break
45+
46+
trans_id, proto_id, length = struct.unpack(">HHH", header)
47+
unit_id = await reader.readexactly(1)
48+
pdu = await reader.readexactly(length - 1) # length includes unit_id
49+
50+
slave_id = b"\x01"
51+
modbus_rtu = slave_id + pdu + get_crc(slave_id + pdu)
52+
53+
try:
54+
# Convert RTU back to TCP
55+
reply_rtu = await solarmanv5.send_raw_modbus_frame(modbus_rtu)
56+
57+
slave_id_reply = reply_rtu[0:1]
58+
pdu_reply = reply_rtu[1:-2]
59+
crc_reply = reply_rtu[-2:]
60+
61+
mbap = struct.pack(">HHH", trans_id, 0, len(pdu_reply) + 1)
62+
reply_tcp = mbap + unit_id + pdu_reply
63+
64+
writer.write(reply_tcp)
65+
except:
66+
pass
67+
68+
await writer.drain()
69+
except OSError:
70+
# https://github.com/python/cpython/issues/83037
71+
pass
72+
73+
print(f"{addr}: Connection closed")
74+
await solarmanv5.disconnect()
75+
76+
77+
async def run_proxy(
78+
bind_address: str, port: int, logger_address: str, logger_serial: int
79+
):
80+
server = await asyncio.start_server(
81+
partial(
82+
handle_client, logger_address=logger_address, logger_serial=logger_serial
83+
),
84+
bind_address,
85+
port,
86+
)
87+
async with server:
88+
print(f"Listening on {bind_address}:{port}")
89+
await server.serve_forever()
90+
91+
92+
def main():
93+
parser = argparse.ArgumentParser(
94+
prog="solarman rtu proxy",
95+
description="A Modbus RTU over TCP Proxy for Solarman loggers",
96+
)
97+
parser.add_argument(
98+
"-b", "--bind", default="0.0.0.0", help="The address to listen on"
99+
)
100+
parser.add_argument(
101+
"-p", "--port", default=1502, type=int, help="The TCP port to listen on"
102+
)
103+
parser.add_argument(
104+
"-l", "--logger", required=True, help="The IP address of the logger"
105+
)
106+
parser.add_argument(
107+
"-s",
108+
"--serial",
109+
required=True,
110+
type=int,
111+
help="The serial number of the logger",
112+
)
113+
args = parser.parse_args()
114+
115+
asyncio.run(run_proxy(args.bind, args.port, args.logger, args.serial))
116+
117+
118+
if __name__ == "__main__":
119+
main()

0 commit comments

Comments
 (0)