Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,23 @@ sigineer_v0.11 = sigineer inverters
growatt_2020_v1.24 = alt protocol for large growatt inverters - currently untested
eg4_v58 = eg4 inverters ( EG4-6000XP ) - confirmed working
srne_v3.9 = SRNE inverters - Untested
victron_gx_3.3 = Victron GX Devices - Untested
solark_v1.1 = SolarArk 8/12K Inverters - Untested
hdhk_16ch_ac_module = some chinese current monitoring device :P
```

more details on these protocols can be found in the wiki

### run as script
```python3 -u protocol_gateway.py```
```
python3 -u protocol_gateway.py
```

or

```
python3 -u protocol_gateway.py config.cfg
```

### install as service
ppg can be used as a shorter service name ;)
Expand Down
15 changes: 11 additions & 4 deletions classes/protocol_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,9 @@ def fromString(cls, name : str):
alias : dict[str,str] = {
"UINT8" : "BYTE",
"INT16" : "SHORT",
"UINT16" : "USHORT"
"UINT16" : "USHORT",
"UINT32" : "UINT",
"INT32" : "INT"
}

if name in alias:
Expand Down Expand Up @@ -134,6 +136,7 @@ def fromString(cls, name : str):
#common alternative names
alias : dict[str,WriteMode] = {
"R" : "READ",
"NO" : "READ",
"READ" : "READ",
"WD" : "READ",
"RD" : "READDISABLED",
Expand All @@ -142,7 +145,7 @@ def fromString(cls, name : str):
"D" : "READDISABLED",
"RW" : "WRITE",
"W" : "WRITE",
"WRITE" : "WRITE"
"YES" : "WRITE"
}

if name in alias:
Expand Down Expand Up @@ -436,8 +439,8 @@ def determine_delimiter(first_row) -> str:
matched : bool = False
val_match = range_regex.search(row['values'])
if val_match:
value_min = int(val_match.group('start'))
value_max = int(val_match.group('end'))
value_min = strtoint(val_match.group('start'))
value_max = strtoint(val_match.group('end'))
matched = True

if data_type == Data_Type.ASCII:
Expand Down Expand Up @@ -622,6 +625,10 @@ def load_registry_map(self, registry_type : Registry_Type, file : str = '', sett

path = settings_dir + '/' + file

#if path does not exist; nothing to load. skip.
if not os.path.exists(path):
return

self.registry_map[registry_type] = self.load__registry(path, registry_type)

size : int = 0
Expand Down
31 changes: 28 additions & 3 deletions classes/transports/modbus_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from .transport_base import transport_base
from ..protocol_settings import Data_Type, Registry_Type, registry_map_entry, protocol_settings
from defs.common import strtobool

from typing import TYPE_CHECKING
if TYPE_CHECKING:
Expand All @@ -20,13 +21,28 @@ class modbus_base(transport_base):
analyze_protocol_save_load : bool = False
first_connect : bool = True

send_holding_register : bool = True
send_input_register : bool = True

def __init__(self, settings : 'SectionProxy', protocolSettings : 'protocol_settings' = None):
super().__init__(settings, protocolSettings=protocolSettings)

self.analyze_protocol_enabled = settings.getboolean('analyze_protocol', fallback=self.analyze_protocol)
self.analyze_protocol_enabled = settings.getboolean('analyze_protocol', fallback=self.analyze_protocol_enabled)
self.analyze_protocol_save_load = settings.getboolean('analyze_protocol_save_load', fallback=self.analyze_protocol_save_load)


#get defaults from protocol settings
if 'send_input_register' in self.protocolSettings.settings:
self.send_input_register = strtobool(self.protocolSettings.settings['send_input_register'])

if 'send_holding_register' in self.protocolSettings.settings:
self.send_holding_register = strtobool(self.protocolSettings.settings['send_holding_register'])

#allow enable/disable of which registers to send
self.send_holding_register = settings.getboolean('send_holding_register', fallback=self.send_holding_register)
self.send_input_register = settings.getboolean('send_input_register', fallback=self.send_input_register)


if self.analyze_protocol_enabled:
self.connect()
self.analyze_protocol()
Expand Down Expand Up @@ -71,7 +87,7 @@ def read_serial_number(self) -> str:
sn2 = sn2 + str(data_bytes.decode('utf-8'))
sn3 = str(data_bytes.decode('utf-8')) + sn3

time.sleep(self.modbus_delay) #sleep inbetween requests so modbus can rest
time.sleep(self.modbus_delay*2) #sleep inbetween requests so modbus can rest

print(sn2)
print(sn3)
Expand Down Expand Up @@ -105,7 +121,16 @@ def write_data(self, data : dict[str, str]) -> None:

def read_data(self) -> dict[str, str]:
info = {}
for registry_type in Registry_Type:
#modbus - only read input/holding registries
for registry_type in (Registry_Type.INPUT, Registry_Type.HOLDING):

#enable / disable input/holding register
if registry_type == Registry_Type.INPUT and not self.send_input_register:
continue

if registry_type == Registry_Type.HOLDING and not self.send_holding_register:
continue

registry = self.read_modbus_registers(ranges=self.protocolSettings.get_registry_ranges(registry_type=registry_type), registry_type=registry_type)
new_info = self.protocolSettings.process_registery(registry, self.protocolSettings.get_registry_map(registry_type))

Expand Down
13 changes: 8 additions & 5 deletions classes/transports/modbus_rtu.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from pymodbus.client.sync import ModbusSerialClient
from .modbus_base import modbus_base
from configparser import SectionProxy
from defs.common import find_usb_serial_port, get_usb_serial_port_info
from defs.common import find_usb_serial_port, get_usb_serial_port_info, strtoint

class modbus_rtu(modbus_base):
port : str = "/dev/ttyUSB0"
Expand All @@ -15,16 +15,20 @@ def __init__(self, settings : SectionProxy, protocolSettings : protocol_settings
#logger = logging.getLogger(__name__)
#logging.basicConfig(level=logging.DEBUG)

#todo: implement send holding/input option? here?
super().__init__(settings, protocolSettings=protocolSettings)


self.port = settings.get("port", "")
if not self.port:
raise ValueError("Port is not set")

self.port = find_usb_serial_port(self.port)
print("Serial Port : " + self.port + " = "+get_usb_serial_port_info(self.port)) #print for config convience
print("Serial Port : " + self.port + " = ", get_usb_serial_port_info(self.port)) #print for config convience

self.baudrate = settings.getint("baudrate", 9600)
if "baud" in self.protocolSettings.settings:
self.baudrate = strtoint(self.protocolSettings.settings["baud"])

self.baudrate = settings.getint("baudrate", self.baudrate)

address : int = settings.getint("address", 0)
self.addresses = [address]
Expand All @@ -33,7 +37,6 @@ def __init__(self, settings : SectionProxy, protocolSettings : protocol_settings
baudrate=int(self.baudrate),
stopbits=1, parity='N', bytesize=8, timeout=2
)
super().__init__(settings, protocolSettings=protocolSettings)

def read_registers(self, start, count=1, registry_type : Registry_Type = Registry_Type.INPUT, **kwargs):

Expand Down
4 changes: 2 additions & 2 deletions classes/transports/mqtt.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@ def __init__(self, settings : SectionProxy):
self.holding_register_prefix = settings.get("holding_register_prefix", fallback="")
self.input_register_prefix = settings.get("input_register_prefix", fallback="")

username = settings.get('user')
password = settings.get('pass')
username = settings.get('user', fallback="")
password = settings.get('pass', fallback="")

if not username:
raise ValueError("User is not set")
Expand Down
2 changes: 1 addition & 1 deletion classes/transports/transport_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ def __init__(self, settings : 'SectionProxy', protocolSettings : 'protocol_setti
self.read_interval = settings.getfloat("read_interval", self.read_interval)
self.max_precision = settings.getint(["max_precision", "precision"], self.max_precision)
if "write_enabled" in settings:
self.write_enabled = settings.getboolean("write_enabled", self.write_enabled)
self.write_enabled = settings.getboolean(["write_enabled", "enable_write"], self.write_enabled)
else:
self.write_enabled = settings.getboolean("write", self.write_enabled)

Expand Down
23 changes: 16 additions & 7 deletions defs/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ def strtobool (val):
"""Convert a string representation of truth to true (1) or false (0).
True values are 'y', 'yes', 't', 'true', 'on', and '1'
"""
if isinstance(val, bool):
return val

val = val.lower()
if val in ('y', 'yes', 't', 'true', 'on', '1'):
return 1
Expand All @@ -28,22 +31,28 @@ def get_usb_serial_port_info(port : str = '') -> str:
for p in serial.tools.list_ports.comports():
if str(p.device).upper() == port.upper():
return "["+hex(p.vid)+":"+hex(p.pid)+":"+str(p.serial_number)+":"+str(p.location)+"]"

return ""

def find_usb_serial_port(port : str = '', vendor_id : str = '', product_id : str = '', serial_number : str = '', location : str = '') -> str:
if not port.startswith('['):
return port

match = re.match(r"\[(?P<vendor>[x\d]+|):?(?P<product>[x\d]+|):?(?P<serial>\d+|):?(?P<location>[\d\-]+|)\]", port)
match = re.match(r"\[(?P<vendor>[\da-zA-Z]+|):?(?P<product>[\da-zA-Z]+|):?(?P<serial>[\da-zA-Z]+|):?(?P<location>[\d\-]+|)\]", port)
if match:
vendor_id = int(match.group("vendor"), 16) if match.group("vendor") else ''
product_id = int(match.group("product"), 16) if match.group("product") else ''
serial_number = match.group("serial") if match.group("serial") else ''
location = match.group("location") if match.group("location") else ''

for port in serial.tools.list_ports.comports():
if ((not vendor_id or port.vid == vendor_id) and
( not product_id or port.pid == product_id) and
( not serial_number or port.serial_number == serial_number) and
( not location or port.location == location)):
return port.device
for port in serial.tools.list_ports.comports():
if ((not vendor_id or port.vid == vendor_id) and
( not product_id or port.pid == product_id) and
( not serial_number or port.serial_number == serial_number) and
( not location or port.location == location)):
return port.device
else:
print("Bad Port Pattern", port)
return None

return None
Binary file added docs/Sol-Ark ModBus V1.1.pdf
Binary file not shown.
Binary file not shown.
Loading