Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
773e84b
adding EG4-3000-EHV and ignoring the python venv dirs
Jul 19, 2024
9ab4476
Merge pull request #42 from yNosGR/main
HotNoob Jul 19, 2024
9014ad7
ignore python venvs
HotNoob Jul 19, 2024
d638922
add eg4_3000ehv protocol and implement 32bit flag
HotNoob Jul 19, 2024
52bc28c
implement 32bit flags for ushort registers / modbus
HotNoob Jul 20, 2024
4347fa4
Update protocol_settings.py
HotNoob Jul 20, 2024
d0b3572
Update common.py
HotNoob Jul 20, 2024
021e400
Update protocol_settings.py
HotNoob Jul 20, 2024
8687a4f
Update protocol_settings.py
HotNoob Jul 20, 2024
2ec2782
Update README.md
HotNoob Jul 20, 2024
54213b6
Update README.md
HotNoob Jul 20, 2024
5280c4c
update flake8 tests
HotNoob Jul 20, 2024
887ca25
Merge branch 'v1.1.4' of https://github.com/HotNoob/PythonProtocolGat…
HotNoob Jul 20, 2024
3428566
add pytests for loading protocol_settings
HotNoob Jul 20, 2024
cf3a05f
Revert "add pytests for loading protocol_settings"
HotNoob Jul 20, 2024
a7bc8fb
add pytests for loading protocol_settings
HotNoob Jul 20, 2024
110835a
Merge branch 'v1.1.4' of github.com:HotNoob/PythonProtocolGateway int…
HotNoob Jul 20, 2024
e5f6001
add pytest to perform basic check to example cfg
HotNoob Jul 20, 2024
4b5ce2d
readme for folder
HotNoob Jul 20, 2024
9afe386
fix typo
HotNoob Jul 23, 2024
30769b5
Update eg4_3000ehv_v1.holding_registry_map.csv
HotNoob Jul 23, 2024
0ffbe71
allow negative units
HotNoob Jul 23, 2024
05f70dd
clean ascii strings
HotNoob Jul 23, 2024
b3ae239
fix log level handling
HotNoob Jul 23, 2024
8b1da89
log level example, per transport level
HotNoob Jul 23, 2024
88ff1a7
python compatibility fix
HotNoob Jul 31, 2024
3bcf70a
fix pytest deprecation warnings
HotNoob Jul 31, 2024
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
4 changes: 3 additions & 1 deletion .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,10 @@ jobs:
run: |
# stop the build if there are Python syntax errors or undefined names
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics

# don't care about coding standards
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
# flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Test with pytest
run: |
pytest
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@
*.pyc
.vscode
settings.json

# Ignore Python virtual environment directories
virtualenv/*
venv/
env/
.venv/
.env/

growatt2mqtt.cfg
growatt2mqtt.service
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,13 @@ protocol_version = {{version}}
v0.14 = growatt inverters 2020+
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

eg4_v58 = eg4 inverters ( EG4-6000XP ) - confirmed working
eg4_3000ehv_v1 = eg4 inverters ( EG4_3000EHV )
```

more details on these protocols can be found in the wiki
Expand Down
96 changes: 69 additions & 27 deletions classes/protocol_settings.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import csv
from dataclasses import dataclass
from enum import Enum
from typing import Union
from defs.common import strtoint
import itertools
import json
Expand All @@ -22,6 +23,8 @@ class Data_Type(Enum):
'''32 bit signed int'''
_16BIT_FLAGS = 7
_8BIT_FLAGS = 8
_32BIT_FLAGS = 9


ASCII = 84
''' 2 characters '''
Expand Down Expand Up @@ -104,7 +107,9 @@ def getSize(cls, data_type : 'Data_Type'):
Data_Type.UINT : 32,
Data_Type.SHORT : 16,
Data_Type.INT : 32,
Data_Type._16BIT_FLAGS : 16
Data_Type._8BIT_FLAGS : 8,
Data_Type._16BIT_FLAGS : 16,
Data_Type._32BIT_FLAGS : 32
}

if data_type in sizes:
Expand Down Expand Up @@ -143,6 +148,7 @@ def fromString(cls, name : str):
"READDISABLED" : "READDISABLED",
"DISABLED" : "READDISABLED",
"D" : "READDISABLED",
"R/W" : "WRITE",
"RW" : "WRITE",
"W" : "WRITE",
"YES" : "WRITE"
Expand Down Expand Up @@ -337,7 +343,7 @@ def determine_delimiter(first_row) -> str:
if first_row.count(';') < first_row.count(','):
delimeter = ','

first_row = re.sub(r"\s+" + re.escape(delimeter) +"|" + re.escape(delimeter) +"\s+", delimeter, first_row) #trim values
first_row = re.sub(r"\s+" + re.escape(delimeter) +"|" + re.escape(delimeter) +r"\s+", delimeter, first_row) #trim values

csvfile = itertools.chain([first_row], csvfile) #add clean header to begining of iterator

Expand All @@ -357,7 +363,7 @@ def determine_delimiter(first_row) -> str:
character_part = row['unit']
else:
# Use regular expressions to extract numeric and character parts
matches = re.findall(r'([0-9.]+)|(.*?)$', row['unit'])
matches = re.findall(r'(\-?[0-9.]+)|(.*?)$', row['unit'])

# Iterate over the matches and assign them to appropriate variables
for match in matches:
Expand All @@ -373,7 +379,7 @@ def determine_delimiter(first_row) -> str:
variable_name = row['variable name'] if row['variable name'] else row['documented name']
variable_name = variable_name = variable_name.strip().lower().replace(' ', '_').replace('__', '_') #clean name

if re.search("[^a-zA-Z0-9\_]", variable_name) :
if re.search(r"[^a-zA-Z0-9\_]", variable_name) :
print("WARNING Invalid Name : " + str(variable_name) + " reg: " + str(row['register']) + " doc name: " + str(row['documented name']) + " path: " + str(path))

#convert to float
Expand Down Expand Up @@ -659,15 +665,21 @@ def process_register_bytes(self, registry : dict[int,bytes], entry : registry_ma
value = int.from_bytes(register[:2], byteorder='big', signed=False)
elif entry.data_type == Data_Type.SHORT:
value = int.from_bytes(register[:2], byteorder='big', signed=True)
elif entry.data_type == Data_Type._16BIT_FLAGS or entry.data_type == Data_Type._8BIT_FLAGS:
elif entry.data_type == Data_Type._16BIT_FLAGS or entry.data_type == Data_Type._8BIT_FLAGS or entry.data_type == Data_Type._32BIT_FLAGS:
#16 bit flags
start_bit : int = 0
if entry.data_type == Data_Type._8BIT_FLAGS:
start_bit = 8
end_bit : int = 16 #default 16 bit
flag_size : int = Data_Type.getSize(entry.data_type)

if entry.register_bit > 0: #handle custom bit offset
start_bit = entry.register_bit

#handle custom sizes, less than 1 register
end_bit = flag_size + start_bit

if entry.documented_name+'_codes' in self.protocolSettings.codes:
flags : list[str] = []
for i in range(start_bit, 16): # Iterate over each bit position (0 to 15)
for i in range(start_bit, end_bit): # Iterate over each bit position (0 to 15)
byte = i // 8
bit = i % 8
val = register[byte]
Expand All @@ -680,7 +692,7 @@ def process_register_bytes(self, registry : dict[int,bytes], entry : registry_ma
value = ",".join(flags)
else:
flags : list[str] = []
for i in range(start_bit, 16): # Iterate over each bit position (0 to 15)
for i in range(start_bit, end_bit): # Iterate over each bit position (0 to 15)
# Check if the i-th bit is set
if (val >> i) & 1:
flags.append("1")
Expand Down Expand Up @@ -762,32 +774,58 @@ def process_register_ushort(self, registry : dict[int, int], entry : registry_ma
value = -value
#value = struct.unpack('<h', bytes([min(max(registry[item.register], 0), 255), min(max(registry[item.register+1], 0), 255)]))[0]
#value = int.from_bytes(bytes([registry[item.register], registry[item.register + 1]]), byteorder='little', signed=True)
elif entry.data_type == Data_Type._16BIT_FLAGS or entry.data_type == Data_Type._8BIT_FLAGS:
val = registry[entry.register]
elif entry.data_type == Data_Type._16BIT_FLAGS or entry.data_type == Data_Type._8BIT_FLAGS or entry.data_type == Data_Type._32BIT_FLAGS:

#16 bit flags
start_bit : int = 0
if entry.data_type == Data_Type._8BIT_FLAGS:
start_bit = 8
end_bit : int = 16 #default 16 bit
flag_size : int = Data_Type.getSize(entry.data_type)

if entry.register_bit > 0: #handle custom bit offset
start_bit = entry.register_bit

#handle custom sizes, less than 1 register
end_bit = flag_size + start_bit

offset : int = 0
#calculate current offset for mutliregiter values, were assuming concatenate registers is in order, 0 being the first / lowest
#offset should always be >= 0
if entry.concatenate:
offset : int = entry.register - entry.concatenate_registers[0]

#compensate for current offset
end_bit = end_bit - (offset * 16)

val = registry[entry.register]

if entry.documented_name+'_codes' in self.codes:
flags : list[str] = []
for i in range(start_bit, 16): # Iterate over each bit position (0 to 15)
# Check if the i-th bit is set
if (val >> i) & 1:
flag_index = "b"+str(i)
if flag_index in self.codes[entry.documented_name+'_codes']:
flags.append(self.codes[entry.documented_name+'_codes'][flag_index])
offset : int = 0

if end_bit > 0:
end : int = 16 if end_bit >= 16 else end_bit
for i in range(start_bit, end): # Iterate over each bit position (0 to 15)
# Check if the i-th bit is set
if (val >> i) & 1:
flag_index = "b"+str(i+offset)
if flag_index in self.codes[entry.documented_name+'_codes']:
flags.append(self.codes[entry.documented_name+'_codes'][flag_index])


value = ",".join(flags)
else:
flags : list[str] = []
for i in range(start_bit, 16): # Iterate over each bit position (0 to 15)
# Check if the i-th bit is set
if (val >> i) & 1:
flags.append("1")
else:
flags.append("0")
if end_bit > 0:
end : int = 16 if end_bit >= 16 else end_bit
for i in range(start_bit, end): # Iterate over each bit position (0 to 15)
# Check if the i-th bit is set
if (val >> i) & 1:
flags.append("1")
else:
flags.append("0")

value = ''.join(flags)

elif entry.data_type.value > 200 or entry.data_type == Data_Type.BYTE: #bit types
bit_size = Data_Type.getSize(entry.data_type)
bit_mask = (1 << bit_size) - 1 # Create a mask for extracting X bits
Expand Down Expand Up @@ -823,7 +861,7 @@ def process_register_ushort(self, registry : dict[int, int], entry : registry_ma

return value

def process_registery(self, registry : dict[int,int] | dict[int,bytes] , map : list[registry_map_entry]) -> dict[str,str]:
def process_registery(self, registry : Union[dict[int, int], dict[int, bytes]] , map : list[registry_map_entry]) -> dict[str,str]:
'''process registry into appropriate datatypes and names -- maybe add func for single entry later?'''

concatenate_registry : dict = {}
Expand Down Expand Up @@ -856,6 +894,10 @@ def process_registery(self, registry : dict[int,int] | dict[int,bytes] , map : l
concatenated_value = concatenated_value + str(concatenate_registry[key])
del concatenate_registry[key]

#replace null characters with spaces and trim
if entry.data_type == Data_Type.ASCII:
concatenated_value = concatenated_value.replace("\x00", " ").strip()

info[entry.variable_name] = concatenated_value
else:
info[entry.variable_name] = value
Expand All @@ -871,7 +913,7 @@ def validate_registry_entry(self, entry : registry_map_entry, val) -> int:
return 0

if entry.data_type == Data_Type.ASCII:
if val and not re.match('[^a-zA-Z0-9\_\-]', val): #validate ascii
if val and not re.match(r'[^a-zA-Z0-9\_\-]', val): #validate ascii
if entry.value_regex: #regex validation
if re.match(entry.value_regex, val):
if entry.concatenate:
Expand Down
6 changes: 5 additions & 1 deletion classes/transports/transport_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,12 @@ class transport_base:

def __init__(self, settings : 'SectionProxy', protocolSettings : 'protocol_settings' = None) -> None:

#apply log level to logger
self._log_level = logging.getLevelName(settings.get('log_level', fallback='INFO'))
self._log : logging.Logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.DEBUG)

self._log.setLevel(self._log_level)
logging.basicConfig(level=self._log_level)

self.transport_name = settings.name #section name

Expand Down
3 changes: 3 additions & 0 deletions config.cfg.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
log_level = DEBUG

[transport.0] #name must be unique, ie: transport.modbus
#logging level for transport
log_level = DEBUG

#rs485 / modbus device
#protocol config files are located in protocols/
protocol_version = v0.14
Expand Down
4 changes: 4 additions & 0 deletions defs/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ def strtobool (val):

def strtoint(val : str) -> int:
''' converts str to int, but allows for hex string input, identified by x prefix'''

if isinstance(val, int): #is already int.
return val

if val and val[0] == 'x':
return int.from_bytes(bytes.fromhex(val[1:]), byteorder='big')

Expand Down
Binary file not shown.
2 changes: 1 addition & 1 deletion protocol_gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ class Protocol_Gateway:
def __init__(self, config_file : str):
self.__log = logging.getLogger('invertermodbustomqqt_log')
handler = logging.StreamHandler(sys.stdout)
self.__log.setLevel(logging.DEBUG)
#self.__log.setLevel(logging.DEBUG)
formatter = logging.Formatter('[%(asctime)s] {%(filename)s:%(lineno)d} %(levelname)s - %(message)s')
handler.setFormatter(formatter)
self.__log.addHandler(handler)
Expand Down
Loading
Loading