Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

V1.1.8 #77

Merged
merged 26 commits into from
Feb 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 8 additions & 1 deletion classes/protocol_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ class Data_Type(Enum):

ASCII = 84
''' 2 characters '''
HEX = 85
''' HEXADECIMAL STRING '''

_1BIT = 201
_2BIT = 202
Expand Down Expand Up @@ -516,7 +518,7 @@ def process_row(row):
value_max = strtoint(val_match.group('end'))
matched = True

if data_type == Data_Type.ASCII:
if data_type == Data_Type.ASCII: #might need to apply too hex values as well? or min-max works for hex?
#value_regex
val_match = ascii_value_regex.search(row['values'])
if val_match:
Expand Down Expand Up @@ -885,6 +887,8 @@ def process_register_bytes(self, registry : dict[int,bytes], entry : registry_ma
bit_mask = (1 << bit_size) - 1 # Create a mask for extracting X bits
bit_index = entry.register_bit
value = (register >> bit_index) & bit_mask
elif entry.data_type == Data_Type.HEX:
value = register.hex() #convert bytes to hex
elif entry.data_type == Data_Type.ASCII:
try:
value = register.decode("utf-8") #convert bytes to ascii
Expand Down Expand Up @@ -999,6 +1003,9 @@ def process_register_ushort(self, registry : dict[int, int], entry : registry_ma
bit_mask = (1 << bit_size) - 1 # Create a mask for extracting X bits
bit_index = entry.register_bit
value = (registry[entry.register] >> bit_index) & bit_mask
elif entry.data_type == Data_Type.HEX:
value = registry[entry.register].to_bytes((16 + 7) // 8, byteorder=self.byteorder) #convert to ushort to bytes
value = value.hex() #convert bytes to hex
elif entry.data_type == Data_Type.ASCII:
value = registry[entry.register].to_bytes((16 + 7) // 8, byteorder=self.byteorder) #convert to ushort to bytes
try:
Expand Down
3 changes: 2 additions & 1 deletion classes/transports/canbus.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ def __init__(self, settings : 'SectionProxy', protocolSettings : 'protocol_setti
self.linux = platform.system() != 'Windows'


self.port = settings.get(["port", "channel"], "").lower()
self.port = settings.get(["port", "channel"], "")
if not self.port:
raise ValueError("Port/Channel is not set")

Expand All @@ -77,6 +77,7 @@ def __init__(self, settings : 'SectionProxy', protocolSettings : 'protocol_setti
#setup / configure socketcan
if self.interface == "socketcan":
self.setup_socketcan()
self.port = self.port.lower()

self.bus = can.interface.Bus(interface=self.interface, channel=self.port, bitrate=self.baudrate)
self.reader = can.AsyncBufferedReader()
Expand Down
12 changes: 12 additions & 0 deletions classes/transports/modbus_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,20 @@
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from configparser import SectionProxy
try:
from pymodbus.client.sync import BaseModbusClient
except ImportError:
from pymodbus.client import BaseModbusClient


class modbus_base(transport_base):


#this is specifically static
clients : dict[str, 'BaseModbusClient'] = {}
''' str is identifier, dict of clients when multiple transports use the same ports '''

#non-static here for reference, type hinting, python bs ect...
modbus_delay_increament : float = 0.05
''' delay adjustment every error. todo: add a setting for this '''

Expand Down Expand Up @@ -491,6 +502,7 @@ def read_modbus_registers(self, ranges : list[tuple] = None, start : int = 0, en
if retry < 0:
retry = 0


#combine registers into "registry"
i = -1
while(i := i + 1 ) < range[1]:
Expand Down
17 changes: 14 additions & 3 deletions classes/transports/modbus_rtu.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
except ImportError:
from pymodbus.client 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, strtoint
Expand Down Expand Up @@ -54,16 +55,26 @@ def __init__(self, settings : SectionProxy, protocolSettings : protocol_settings
# Get the signature of the __init__ method
init_signature = inspect.signature(ModbusSerialClient.__init__)

client_str = self.port+"("+str(self.baudrate)+")"

if client_str in modbus_base.clients:
self.client = modbus_base.clients[client_str]
return

if 'method' in init_signature.parameters:
self.client = ModbusSerialClient(method='rtu', port=self.port,
baudrate=int(self.baudrate),
stopbits=1, parity='N', bytesize=8, timeout=2
)
else:
self.client = ModbusSerialClient(port=self.port,
self.client = ModbusSerialClient(
port=self.port,
baudrate=int(self.baudrate),
stopbits=1, parity='N', bytesize=8, timeout=2
)

#add to clients
modbus_base.clients[client_str] = self.client

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

Expand All @@ -75,9 +86,9 @@ def read_registers(self, start, count=1, registry_type : Registry_Type = Registr
kwargs['slave'] = kwargs.pop('unit')

if registry_type == Registry_Type.INPUT:
return self.client.read_input_registers(start, count, **kwargs)
return self.client.read_input_registers(address=start, count=count, **kwargs)
elif registry_type == Registry_Type.HOLDING:
return self.client.read_holding_registers(start, count, **kwargs)
return self.client.read_holding_registers(address=start, count=count, **kwargs)

def write_register(self, register : int, value : int, **kwargs):
if not self.write_enabled:
Expand Down
39 changes: 35 additions & 4 deletions classes/transports/modbus_tcp.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
import logging
import inspect

from classes.protocol_settings import Registry_Type, protocol_settings
from pymodbus.client.sync import ModbusTcpClient
from .transport_base import transport_base

#compatability
try:
from pymodbus.client.sync import ModbusTcpClient
except ImportError:
from pymodbus.client import ModbusTcpClient

from .modbus_base import modbus_base
from configparser import SectionProxy


class modbus_tcp(transport_base):
class modbus_tcp(modbus_base):
port : str = 502
host : str = ""
client : ModbusTcpClient
pymodbus_slave_arg = 'unit'

def __init__(self, settings : SectionProxy, protocolSettings : protocol_settings = None):
#logger = logging.getLogger(__name__)
Expand All @@ -20,12 +29,34 @@ def __init__(self, settings : SectionProxy, protocolSettings : protocol_settings

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

# pymodbus compatability; unit was renamed to address
if 'slave' in inspect.signature(ModbusTcpClient.read_holding_registers).parameters:
self.pymodbus_slave_arg = 'slave'

client_str = self.host+"("+str(self.port)+")"
#check if client is already initialied
if client_str in modbus_base.clients:
self.client = modbus_base.clients[client_str]
return

self.client = ModbusTcpClient(host=self.host, port=self.port, timeout=7, retries=3)

#add to clients
modbus_base.clients[client_str] = self.client

super().__init__(settings, protocolSettings=protocolSettings)

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

if 'unit' not in kwargs:
kwargs = {'unit': 1, **kwargs}

#compatability
if self.pymodbus_slave_arg != 'unit':
kwargs['slave'] = kwargs.pop('unit')

if registry_type == Registry_Type.INPUT:
return self.client.read_input_registers(start, count, **kwargs)
return self.client.read_input_registers(start, count, **kwargs )
elif registry_type == Registry_Type.HOLDING:
return self.client.read_holding_registers(start, count, **kwargs)

Expand Down
10 changes: 5 additions & 5 deletions classes/transports/mqtt.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ def connect(self):
def exit_handler(self):
'''on exit handler'''
self._log.warning("MQTT Exiting...")
self.client.publish( self.base_topic + "/availability","offline")
self.client.publish( self.base_topic + '/' + self.device_identifier + "/availability","offline")
return

def mqtt_reconnect(self):
Expand Down Expand Up @@ -163,7 +163,7 @@ def write_data(self, data : dict[str, str], from_transport : transport_base):
self._log.info(f"write data from [{from_transport.transport_name}] to mqtt transport")
self._log.info(data)
#have to send this every loop, because mqtt doesnt disconnect when HA restarts. HA bug.
info = self.client.publish(self.base_topic + "/availability","online", qos=0,retain=True)
info = self.client.publish(self.base_topic + '/' + from_transport.device_identifier + "/availability","online", qos=0,retain=True)
if info.rc == MQTT_ERR_NO_CONN:
self.connected = False

Expand Down Expand Up @@ -196,7 +196,7 @@ def init_bridge(self, from_transport : transport_base):
for entry in from_transport.protocolSettings.get_registry_map(Registry_Type.HOLDING):
if entry.write_mode == WriteMode.WRITE or entry.write_mode == WriteMode.WRITEONLY:
#__write_topics
topic : str = self.base_topic + "/write/" + entry.variable_name.lower().replace(' ', '_')
topic : str = self.base_topic + '/'+ from_transport.device_identifier + "/write/" + entry.variable_name.lower().replace(' ', '_')
self.__write_topics[topic] = entry
self.client.subscribe(topic)

Expand All @@ -207,7 +207,7 @@ def mqtt_discovery(self, from_transport : transport_base):
self._log.info("Publishing HA Discovery Topics...")

disc_payload = {}
disc_payload['availability_topic'] = self.base_topic + "/availability"
disc_payload['availability_topic'] = self.base_topic + '/' + from_transport.device_identifier + "/availability"

device = {}
device['manufacturer'] = from_transport.device_manufacturer
Expand Down Expand Up @@ -247,7 +247,7 @@ def mqtt_discovery(self, from_transport : transport_base):

#device['sw_version'] = bms_version
disc_payload = {}
disc_payload['availability_topic'] = self.base_topic + "/availability"
disc_payload['availability_topic'] = self.base_topic + '/' + from_transport.device_identifier + "/availability"
disc_payload['device'] = device
disc_payload['name'] = clean_name
disc_payload['unique_id'] = "hotnoob_" + from_transport.device_serial_number + "_"+clean_name
Expand Down
2 changes: 1 addition & 1 deletion defs/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def strtoint(val : str) -> int:
if isinstance(val, int): #is already int.
return val

val = val.lower()
val = val.lower().strip()

if val and val[0] == 'x':
val = val[1:]
Expand Down
Binary file not shown.
Binary file not shown.
10 changes: 10 additions & 0 deletions documentation/devices/EG4.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,16 @@

3. Connect appropriate wires to USB RS485 Adapter

## Raspberry Pi Can Hat
If using a Raspberry Pi Can Hat, The expected pinout RS485 A/B configuration maybe be reversed.

If you get the following error while using a Raspberry Pi Can Hat swap your A/B wires:

```
ERROR:.transport_base[transport.0]:<bound method ModbusException.__str__ of ModbusIOException()>
```


## Configuration
Follow configuration example for ModBus RTU to MQTT
https://github.com/HotNoob/PythonProtocolGateway/wiki/Configuration-Examples#modbus-rtu-to-mqtt
Expand Down
12 changes: 12 additions & 0 deletions documentation/devices/Growatt.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@
2. for models with a DB9 port a DB9 RS232 adapter is required. "Before use RS232 communication, you should make sure the follow PIN1 and PIN2 are OFF"
3. Connect cable to wifi dongle port; if a alternative usb port exists, try connecting to that one first.

Optional hardware:
ADUM3160 chipset / USB isolator. The growatt inverter's usb port has a power issue that can effect reliablilty; using an isolator will help mitigate this problem.
https://www.aliexpress.com/item/1005002959825296.html?spm=a2g0o.order_list.order_list_main.5.51c618021n8SGf

For best long term reliability, use a rs485 adapter \w rj45 ethernet cable.

## Configuration
Follow configuration example for ModBus RTU to MQTT
https://github.com/HotNoob/PythonProtocolGateway/wiki/Configuration-Examples#modbus-rtu-to-mqtt
Expand All @@ -13,6 +19,12 @@ https://github.com/HotNoob/PythonProtocolGateway/wiki/Configuration-Examples#mod
protocol_version = v0.14
```

## Inverter Protocol Select
```
spf 5000 = v0.14
spf 12000t dvm-us mpv = v0.14
```

## HomeAssistant Cards
Here are some example cards. If you want to use them, you will have to change the variable names and others to reflect your configs.

Expand Down
Loading