Skip to content

Commit 6cf6cee

Browse files
authored
Merge pull request #43 from HotNoob/v1.1.4
V1.1.4
2 parents f3f5679 + 3bcf70a commit 6cf6cee

14 files changed

+194
-31
lines changed

.github/workflows/python-package.yml

+3-1
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,10 @@ jobs:
3333
run: |
3434
# stop the build if there are Python syntax errors or undefined names
3535
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
36+
37+
# don't care about coding standards
3638
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
37-
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
39+
# flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
3840
- name: Test with pytest
3941
run: |
4042
pytest

.gitignore

+6
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@
22
*.pyc
33
.vscode
44
settings.json
5+
6+
# Ignore Python virtual environment directories
57
virtualenv/*
8+
venv/
9+
env/
10+
.venv/
11+
.env/
612

713
growatt2mqtt.cfg
814
growatt2mqtt.service

README.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,13 @@ protocol_version = {{version}}
4141
v0.14 = growatt inverters 2020+
4242
sigineer_v0.11 = sigineer inverters
4343
growatt_2020_v1.24 = alt protocol for large growatt inverters - currently untested
44-
eg4_v58 = eg4 inverters ( EG4-6000XP ) - confirmed working
4544
srne_v3.9 = SRNE inverters - Untested
4645
victron_gx_3.3 = Victron GX Devices - Untested
4746
solark_v1.1 = SolarArk 8/12K Inverters - Untested
4847
hdhk_16ch_ac_module = some chinese current monitoring device :P
48+
49+
eg4_v58 = eg4 inverters ( EG4-6000XP ) - confirmed working
50+
eg4_3000ehv_v1 = eg4 inverters ( EG4_3000EHV )
4951
```
5052

5153
more details on these protocols can be found in the wiki

classes/protocol_settings.py

+69-27
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import csv
22
from dataclasses import dataclass
33
from enum import Enum
4+
from typing import Union
45
from defs.common import strtoint
56
import itertools
67
import json
@@ -22,6 +23,8 @@ class Data_Type(Enum):
2223
'''32 bit signed int'''
2324
_16BIT_FLAGS = 7
2425
_8BIT_FLAGS = 8
26+
_32BIT_FLAGS = 9
27+
2528

2629
ASCII = 84
2730
''' 2 characters '''
@@ -104,7 +107,9 @@ def getSize(cls, data_type : 'Data_Type'):
104107
Data_Type.UINT : 32,
105108
Data_Type.SHORT : 16,
106109
Data_Type.INT : 32,
107-
Data_Type._16BIT_FLAGS : 16
110+
Data_Type._8BIT_FLAGS : 8,
111+
Data_Type._16BIT_FLAGS : 16,
112+
Data_Type._32BIT_FLAGS : 32
108113
}
109114

110115
if data_type in sizes:
@@ -143,6 +148,7 @@ def fromString(cls, name : str):
143148
"READDISABLED" : "READDISABLED",
144149
"DISABLED" : "READDISABLED",
145150
"D" : "READDISABLED",
151+
"R/W" : "WRITE",
146152
"RW" : "WRITE",
147153
"W" : "WRITE",
148154
"YES" : "WRITE"
@@ -337,7 +343,7 @@ def determine_delimiter(first_row) -> str:
337343
if first_row.count(';') < first_row.count(','):
338344
delimeter = ','
339345

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

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

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

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

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

379385
#convert to float
@@ -659,15 +665,21 @@ def process_register_bytes(self, registry : dict[int,bytes], entry : registry_ma
659665
value = int.from_bytes(register[:2], byteorder='big', signed=False)
660666
elif entry.data_type == Data_Type.SHORT:
661667
value = int.from_bytes(register[:2], byteorder='big', signed=True)
662-
elif entry.data_type == Data_Type._16BIT_FLAGS or entry.data_type == Data_Type._8BIT_FLAGS:
668+
elif entry.data_type == Data_Type._16BIT_FLAGS or entry.data_type == Data_Type._8BIT_FLAGS or entry.data_type == Data_Type._32BIT_FLAGS:
663669
#16 bit flags
664670
start_bit : int = 0
665-
if entry.data_type == Data_Type._8BIT_FLAGS:
666-
start_bit = 8
671+
end_bit : int = 16 #default 16 bit
672+
flag_size : int = Data_Type.getSize(entry.data_type)
673+
674+
if entry.register_bit > 0: #handle custom bit offset
675+
start_bit = entry.register_bit
676+
677+
#handle custom sizes, less than 1 register
678+
end_bit = flag_size + start_bit
667679

668680
if entry.documented_name+'_codes' in self.protocolSettings.codes:
669681
flags : list[str] = []
670-
for i in range(start_bit, 16): # Iterate over each bit position (0 to 15)
682+
for i in range(start_bit, end_bit): # Iterate over each bit position (0 to 15)
671683
byte = i // 8
672684
bit = i % 8
673685
val = register[byte]
@@ -680,7 +692,7 @@ def process_register_bytes(self, registry : dict[int,bytes], entry : registry_ma
680692
value = ",".join(flags)
681693
else:
682694
flags : list[str] = []
683-
for i in range(start_bit, 16): # Iterate over each bit position (0 to 15)
695+
for i in range(start_bit, end_bit): # Iterate over each bit position (0 to 15)
684696
# Check if the i-th bit is set
685697
if (val >> i) & 1:
686698
flags.append("1")
@@ -762,32 +774,58 @@ def process_register_ushort(self, registry : dict[int, int], entry : registry_ma
762774
value = -value
763775
#value = struct.unpack('<h', bytes([min(max(registry[item.register], 0), 255), min(max(registry[item.register+1], 0), 255)]))[0]
764776
#value = int.from_bytes(bytes([registry[item.register], registry[item.register + 1]]), byteorder='little', signed=True)
765-
elif entry.data_type == Data_Type._16BIT_FLAGS or entry.data_type == Data_Type._8BIT_FLAGS:
766-
val = registry[entry.register]
777+
elif entry.data_type == Data_Type._16BIT_FLAGS or entry.data_type == Data_Type._8BIT_FLAGS or entry.data_type == Data_Type._32BIT_FLAGS:
778+
767779
#16 bit flags
768780
start_bit : int = 0
769-
if entry.data_type == Data_Type._8BIT_FLAGS:
770-
start_bit = 8
781+
end_bit : int = 16 #default 16 bit
782+
flag_size : int = Data_Type.getSize(entry.data_type)
783+
784+
if entry.register_bit > 0: #handle custom bit offset
785+
start_bit = entry.register_bit
786+
787+
#handle custom sizes, less than 1 register
788+
end_bit = flag_size + start_bit
789+
790+
offset : int = 0
791+
#calculate current offset for mutliregiter values, were assuming concatenate registers is in order, 0 being the first / lowest
792+
#offset should always be >= 0
793+
if entry.concatenate:
794+
offset : int = entry.register - entry.concatenate_registers[0]
795+
796+
#compensate for current offset
797+
end_bit = end_bit - (offset * 16)
798+
799+
val = registry[entry.register]
771800

772801
if entry.documented_name+'_codes' in self.codes:
773802
flags : list[str] = []
774-
for i in range(start_bit, 16): # Iterate over each bit position (0 to 15)
775-
# Check if the i-th bit is set
776-
if (val >> i) & 1:
777-
flag_index = "b"+str(i)
778-
if flag_index in self.codes[entry.documented_name+'_codes']:
779-
flags.append(self.codes[entry.documented_name+'_codes'][flag_index])
803+
offset : int = 0
804+
805+
if end_bit > 0:
806+
end : int = 16 if end_bit >= 16 else end_bit
807+
for i in range(start_bit, end): # Iterate over each bit position (0 to 15)
808+
# Check if the i-th bit is set
809+
if (val >> i) & 1:
810+
flag_index = "b"+str(i+offset)
811+
if flag_index in self.codes[entry.documented_name+'_codes']:
812+
flags.append(self.codes[entry.documented_name+'_codes'][flag_index])
813+
780814

781815
value = ",".join(flags)
782816
else:
783817
flags : list[str] = []
784-
for i in range(start_bit, 16): # Iterate over each bit position (0 to 15)
785-
# Check if the i-th bit is set
786-
if (val >> i) & 1:
787-
flags.append("1")
788-
else:
789-
flags.append("0")
818+
if end_bit > 0:
819+
end : int = 16 if end_bit >= 16 else end_bit
820+
for i in range(start_bit, end): # Iterate over each bit position (0 to 15)
821+
# Check if the i-th bit is set
822+
if (val >> i) & 1:
823+
flags.append("1")
824+
else:
825+
flags.append("0")
826+
790827
value = ''.join(flags)
828+
791829
elif entry.data_type.value > 200 or entry.data_type == Data_Type.BYTE: #bit types
792830
bit_size = Data_Type.getSize(entry.data_type)
793831
bit_mask = (1 << bit_size) - 1 # Create a mask for extracting X bits
@@ -823,7 +861,7 @@ def process_register_ushort(self, registry : dict[int, int], entry : registry_ma
823861

824862
return value
825863

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

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

897+
#replace null characters with spaces and trim
898+
if entry.data_type == Data_Type.ASCII:
899+
concatenated_value = concatenated_value.replace("\x00", " ").strip()
900+
859901
info[entry.variable_name] = concatenated_value
860902
else:
861903
info[entry.variable_name] = value
@@ -871,7 +913,7 @@ def validate_registry_entry(self, entry : registry_map_entry, val) -> int:
871913
return 0
872914

873915
if entry.data_type == Data_Type.ASCII:
874-
if val and not re.match('[^a-zA-Z0-9\_\-]', val): #validate ascii
916+
if val and not re.match(r'[^a-zA-Z0-9\_\-]', val): #validate ascii
875917
if entry.value_regex: #regex validation
876918
if re.match(entry.value_regex, val):
877919
if entry.concatenate:

classes/transports/transport_base.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,12 @@ class transport_base:
3333

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

36+
#apply log level to logger
37+
self._log_level = logging.getLevelName(settings.get('log_level', fallback='INFO'))
3638
self._log : logging.Logger = logging.getLogger(__name__)
37-
logging.basicConfig(level=logging.DEBUG)
39+
40+
self._log.setLevel(self._log_level)
41+
logging.basicConfig(level=self._log_level)
3842

3943
self.transport_name = settings.name #section name
4044

config.cfg.example

+3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
log_level = DEBUG
33

44
[transport.0] #name must be unique, ie: transport.modbus
5+
#logging level for transport
6+
log_level = DEBUG
7+
58
#rs485 / modbus device
69
#protocol config files are located in protocols/
710
protocol_version = v0.14

defs/common.py

+4
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ def strtobool (val):
1616

1717
def strtoint(val : str) -> int:
1818
''' converts str to int, but allows for hex string input, identified by x prefix'''
19+
20+
if isinstance(val, int): #is already int.
21+
return val
22+
1923
if val and val[0] == 'x':
2024
return int.from_bytes(bytes.fromhex(val[1:]), byteorder='big')
2125

Binary file not shown.

protocol_gateway.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ class Protocol_Gateway:
9696
def __init__(self, config_file : str):
9797
self.__log = logging.getLogger('invertermodbustomqqt_log')
9898
handler = logging.StreamHandler(sys.stdout)
99-
self.__log.setLevel(logging.DEBUG)
99+
#self.__log.setLevel(logging.DEBUG)
100100
formatter = logging.Formatter('[%(asctime)s] {%(filename)s:%(lineno)d} %(levelname)s - %(message)s')
101101
handler.setFormatter(formatter)
102102
self.__log.addHandler(handler)

0 commit comments

Comments
 (0)