Skip to content

Commit 715e581

Browse files
authored
Add handler for MUST PV/PH solar system inverters (#2)
1 parent 2d2d561 commit 715e581

File tree

5 files changed

+170
-1
lines changed

5 files changed

+170
-1
lines changed

dependencies.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
python3-eventlet
22
python3-flask
33
python3-flask-socketio
4+
python3-minimalmodbus
45
python3-pony
56
python3-requests
67
python3-serial

modules/handlers/__init__.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
11
from modules.handlers.serial_handler import SerialHandler
22
from modules.handlers.http_handler import HttpHandler
33
from modules.handlers.bms_serial_handler import BmsSerialHandler
4+
from modules.handlers.must_pv_ph_inverter_modbus_handler import (
5+
MustPVPHInverterModbusHandler,
6+
)
47

5-
loaded_handlers = [SerialHandler, HttpHandler, BmsSerialHandler]
8+
loaded_handlers = [
9+
SerialHandler,
10+
HttpHandler,
11+
BmsSerialHandler,
12+
MustPVPHInverterModbusHandler,
13+
]
614

715

816
def get_handler_class(handler_type):
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
from minimalmodbus import Instrument, NoResponseError, InvalidResponseError
2+
from os import path
3+
from serial import SerialException
4+
from threading import Thread
5+
from time import sleep
6+
7+
from modules.logging.logger import logger
8+
from .abstract_handler import AbstractHandler
9+
10+
11+
class MustPVPHInverterModbusHandler(AbstractHandler):
12+
"""Class for handling MUST PV/PH solar system inverters."""
13+
14+
type = "must_pv_ph_modbus"
15+
icon = "inverter"
16+
name = "MUST PV/PH solar inverter"
17+
config_fields = {
18+
"port": ["string", "Device port (e.g., /dev/ttyUSB0)"],
19+
"slave-address": ["int", "Device slave address", 4],
20+
"interval": ["int", "Fetching interval in seconds", 10],
21+
"timeout": ["float", "Timeout in seconds", 0.1],
22+
"auto-reconnect": ["bool", "Auto reconnect", True],
23+
}
24+
25+
registers = {
26+
"charger": {
27+
"pv-voltage": [15205, 1],
28+
"battery-voltage": [15206, 1],
29+
"current": [15207, 1],
30+
"power": [15208, 0],
31+
},
32+
"inverter": {
33+
"battery-voltage": [25205, 1],
34+
"power": [25213, 0],
35+
"power-grid": [25214, 0],
36+
"power-load": [25215, 0],
37+
},
38+
}
39+
40+
def _read_message(self):
41+
if not self.first_tick():
42+
self.wait_for_interval(self.config("interval"))
43+
44+
result = {"charger": {}, "inverter": {}}
45+
46+
for section_type in self.registers.keys():
47+
for key, data in self.registers[section_type].items():
48+
result[section_type][key] = self.connection.read_register(
49+
data[0], data[1]
50+
)
51+
sleep(0.05)
52+
53+
return result
54+
55+
def _message_watcher(self):
56+
self.log.debug("Starting message watcher")
57+
while self.active:
58+
if path.exists(self.connection.serial.port):
59+
try:
60+
message = self._read_message()
61+
if message:
62+
self.add_message(message)
63+
except SerialException as error:
64+
self._handle_error(error, "Failed to read from device")
65+
break
66+
except UnicodeDecodeError as error:
67+
self.log.warning(error)
68+
sleep(0.1)
69+
except NoResponseError as error:
70+
self._handle_error(error, "Communication error")
71+
break
72+
except InvalidResponseError as error:
73+
self._handle_error(error, "Invalid response error")
74+
break
75+
else:
76+
self.log.info("Lost connection with device")
77+
self.connection.serial.close()
78+
self.add_changed("handlers")
79+
if self.config("auto-reconnect"):
80+
Thread(target=self._reconnect_watcher).start()
81+
else:
82+
self.suspended = True
83+
break
84+
self.log.debug("Stopping message watcher")
85+
86+
def _handle_error(self, error, message):
87+
# print(error)
88+
self.log.warning(message)
89+
self.log.error(error)
90+
self.success = False
91+
self.add_changed("handlers")
92+
Thread(target=self._reconnect_watcher).start()
93+
94+
def _reconnect_watcher(self):
95+
self.log.debug("Starting reconnect watcher")
96+
while self.active:
97+
if path.exists(self.connection.serial.port):
98+
if self._reconnect():
99+
Thread(target=self._message_watcher).start()
100+
break
101+
sleep(1)
102+
self.log.debug("Stopping reconnect watcher")
103+
104+
def _reconnect(self):
105+
try:
106+
self.connection.serial.open()
107+
self.log.info("Established connection with device")
108+
self.add_changed("handlers")
109+
return True
110+
except SerialException:
111+
if path.exists(self.connection.serial.port):
112+
self.log.warning("Failed to establish connection - Permission denied")
113+
else:
114+
self.log.warning(
115+
"Failed to establish connection - Device does not exist"
116+
)
117+
self.connection.serial.close()
118+
return False
119+
except NoResponseError:
120+
return False
121+
except InvalidResponseError:
122+
return False
123+
124+
def __init__(self, settings):
125+
super().__init__(settings)
126+
self.log = logger(
127+
f"SerialDevice {self.config('port')}:{self.config('slave-address')}"
128+
)
129+
130+
self.connection = Instrument(self.config("port"), self.config("slave-address"))
131+
self.connection.serial.timeout = self.config("timeout")
132+
133+
self.active = True
134+
self.suspended = False
135+
self.add_changed("handlers")
136+
137+
Thread(target=self._reconnect_watcher).start()
138+
139+
def update_config(self, new_config):
140+
super().update_config(new_config)
141+
142+
# TODO: Semaphore may be required
143+
144+
self.connection.serial.close()
145+
self.connection.serial.port = self.config("port")
146+
self.connection.serial.timeout = self.config("timeout")
147+
self.connection.address = self.config("slave-address")
148+
self.connection.serial.close()
149+
150+
if self.suspended:
151+
Thread(target=self._reconnect_watcher).start()
152+
153+
self.add_changed("handlers")
154+
155+
def get_description(self):
156+
return self.connection.serial.port
157+
158+
def is_connected(self):
159+
return self.connection.serial.is_open
883 Bytes
Loading

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
eventlet
22
flask
33
flask-socketio
4+
minimalmodbus
45
pony
56
pyserial
67
requests

0 commit comments

Comments
 (0)