Skip to content

Commit bc84dd5

Browse files
authored
Merge pull request #17 from HotNoob/v1.0.7
V1.0.7
2 parents c704fa3 + 77f2fd2 commit bc84dd5

15 files changed

+487
-326
lines changed

inverter.py

Lines changed: 27 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@
55
import logging
66
import re
77
import time
8-
import struct
8+
import importlib
9+
910
from pymodbus.exceptions import ModbusIOException
1011

1112
from typing import TYPE_CHECKING
1213
if TYPE_CHECKING:
1314
from pymodbus.client.sync import ModbusSerialClient as ModbusClient
1415

16+
from readers.reader_base import reader_base
1517
from protocol_settings import Data_Type, registry_map_entry, protocol_settings, Registry_Type
1618

1719
class Inverter:
@@ -20,15 +22,18 @@ class Inverter:
2022
max_precision : int
2123
modbus_delay : float = 0.85
2224
modbus_version = ""
25+
reader : reader_base
26+
settings : dict[str, str]
2327

2428
'''time inbetween requests'''
2529

26-
def __init__(self, client, name, unit, protocol_version, max_precision : int = -1, log = None):
27-
self.client : ModbusClient = client
30+
def __init__(self, name, unit, protocol_version, settings : dict[str,str], max_precision : int = -1, log = None):
2831
self.name = name
2932
self.unit = unit
3033
self.protocol_version = protocol_version
3134
self.max_precision = max_precision
35+
self.settings = settings
36+
3237
print("max_precision: " + str(self.max_precision))
3338
if (log is None):
3439
self.__log = log
@@ -39,6 +44,16 @@ def __init__(self, client, name, unit, protocol_version, max_precision : int = -
3944
#load protocol settings
4045
self.protocolSettings = protocol_settings(self.protocol_version)
4146

47+
#load reader
48+
# Import the module
49+
module = importlib.import_module('readers.'+self.protocolSettings.reader)
50+
51+
# Get the class from the module
52+
cls = getattr(module, self.protocolSettings.reader)
53+
54+
self.reader : reader_base = cls(self.settings)
55+
self.reader.connect()
56+
4257
self.read_info()
4358

4459
def read_serial_number(self) -> str:
@@ -57,7 +72,7 @@ def read_serial_number(self) -> str:
5772
registry_entry = self.protocolSettings.get_holding_registry_entry(field)
5873
if registry_entry is not None:
5974
self.__log.info("Reading " + field + "("+str(registry_entry.register)+")")
60-
data = self.client.read_holding_registers(registry_entry.register)
75+
data = self.reader.read_registers(registry_entry.register, registry_type=Registry_Type.HOLDING)
6176
if not hasattr(data, 'registers') or data.registers is None:
6277
self.__log.critical("Failed to get serial number register ("+field+") ; exiting")
6378
exit()
@@ -128,9 +143,12 @@ def read_registers(self, ranges : list[tuple] = None, start : int = 0, end : int
128143

129144
if not ranges: #ranges is empty, use min max
130145
ranges = []
131-
start = -batch_size
146+
start = start - batch_size
132147
while( start := start + batch_size ) < end:
133-
ranges.append((start, batch_size)) ##APPEND TUPLE
148+
count = batch_size
149+
if start + batch_size > end:
150+
count = end - start + 1
151+
ranges.append((start, count)) ##APPEND TUPLE
134152

135153
registry : dict[int,] = {}
136154
retries = 7
@@ -146,12 +164,7 @@ def read_registers(self, ranges : list[tuple] = None, start : int = 0, end : int
146164

147165
isError = False
148166
try:
149-
if registry_type == Registry_Type.INPUT:
150-
register = self.client.read_input_registers(range[0], range[1], unit=self.unit)
151-
else:
152-
print("get holding")
153-
register = self.client.read_holding_registers(range[0], range[1], unit=self.unit)
154-
#register.addCallback
167+
register = self.reader.read_registers(range[0], range[1], registry_type=registry_type, unit=self.unit)
155168

156169
except ModbusIOException as e:
157170
print("ModbusIOException : ", e.error_code)
@@ -181,14 +194,13 @@ def read_registers(self, ranges : list[tuple] = None, start : int = 0, end : int
181194
retry -= 1
182195
if retry < 0:
183196
retry = 0
197+
184198
#combine registers into "registry"
185-
print("combine results, " + str(len(register.registers)))
186199
i = -1
187200
while(i := i + 1 ) < range[1]:
188201
#print(str(i) + " => " + str(i+range[0]))
189202
registry[i+range[0]] = register.registers[i]
190203

191-
print("registry len: " + str(len(registry)))
192204
return registry
193205

194206
def process_registery(self, registry : dict, map : list[registry_map_entry]) -> dict[str,str]:
@@ -316,51 +328,4 @@ def read_holding_registry(self) -> dict[str,str]:
316328

317329
registry = self.read_registers(self.protocolSettings.holding_registry_ranges, registry_type=Registry_Type.HOLDING)
318330
info = self.process_registery(registry, self.protocolSettings.holding_registry_map)
319-
return info
320-
321-
322-
# def read_fault_table(self, name, base_index, count):
323-
# fault_table = {}
324-
# for i in range(0, count):
325-
# fault_table[name + '_' + str(i)] = self.read_fault_record(base_index + i * 5)
326-
# return fault_table
327-
#
328-
# def read_fault_record(self, index):
329-
# row = self.client.read_input_registers(index, 5, unit=self.unit)
330-
# # TODO: Figure out how to read the date for these records?
331-
# print(row.registers[0],
332-
# ErrorCodes[row.registers[0]],
333-
# '\n',
334-
# row.registers[1],
335-
# row.registers[2],
336-
# row.registers[3],
337-
# '\n',
338-
# 2000 + (row.registers[1] >> 8),
339-
# row.registers[1] & 0xFF,
340-
# row.registers[2] >> 8,
341-
# row.registers[2] & 0xFF,
342-
# row.registers[3] >> 8,
343-
# row.registers[3] & 0xFF,
344-
# row.registers[4],
345-
# '\n',
346-
# 2000 + (row.registers[1] >> 4),
347-
# row.registers[1] & 0xF,
348-
# row.registers[2] >> 4,
349-
# row.registers[2] & 0xF,
350-
# row.registers[3] >> 4,
351-
# row.registers[3] & 0xF,
352-
# row.registers[4]
353-
# )
354-
# return {
355-
# 'FaultCode': row.registers[0],
356-
# 'Fault': ErrorCodes[row.registers[0]],
357-
# #'Time': int(datetime.datetime(
358-
# # 2000 + (row.registers[1] >> 8),
359-
# # row.registers[1] & 0xFF,
360-
# # row.registers[2] >> 8,
361-
# # row.registers[2] & 0xFF,
362-
# # row.registers[3] >> 8,
363-
# # row.registers[3] & 0xFF
364-
# #).timestamp()),
365-
# 'Value': row.registers[4]
366-
# }
331+
return info

invertermodbustomqtt.py

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,25 @@
22
"""
33
Main module for Growatt / Inverters ModBus RTU data to MQTT
44
"""
5+
6+
7+
import sys
8+
import time
9+
10+
# Check if Python version is greater than 3.9
11+
if sys.version_info < (3, 9):
12+
print("==================================================")
13+
print("WARNING: python version 3.9 or higher is recommended")
14+
print("Current version: " + sys.version)
15+
print("Please upgrade your python version to 3.9")
16+
print("==================================================")
17+
time.sleep(4)
18+
519
import atexit
620
import glob
721
import random
822
import re
9-
import time
23+
1024
import os
1125
import json
1226
import logging
@@ -16,13 +30,11 @@
1630
import paho.mqtt.client as mqtt
1731
from paho.mqtt.properties import Properties
1832
from paho.mqtt.packettypes import PacketTypes
19-
from pymodbus.client.sync import ModbusSerialClient as ModbusClient
2033

2134
from inverter import Inverter
2235

2336
from protocol_settings import protocol_settings,Data_Type,registry_map_entry,Registry_Type
2437

25-
2638
__logo = """
2739
____ _ _ ____ __ __ ___ _____ _____
2840
/ ___|_ __ _____ ____ _| |_| |_|___ \| \/ |/ _ \_ _|_ _|
@@ -50,8 +62,6 @@ class InverterModBusToMQTT:
5062
__port = None
5163
# baudrate to access modbus connection
5264
__baudrate = -1
53-
# modbus client handle
54-
__client = None
5565
# mqtt server host address
5666
__mqtt_host = None
5767
# mqtt client handle
@@ -157,21 +167,6 @@ def init_invertermodbustomqtt(self):
157167
self.__log.setLevel(logging.getLevelName(self.__log_level))
158168

159169

160-
161-
self.__log.info('Setup Serial Connection... ')
162-
self.__port = self.__settings.get(
163-
'serial', 'port', fallback='/dev/ttyUSB0')
164-
self.__baudrate = self.__settings.get(
165-
'serial', 'baudrate', fallback=9600)
166-
167-
168-
self.__client = ModbusClient(method='rtu', port=self.__port,
169-
baudrate=int(self.__baudrate),
170-
stopbits=1, parity='N', bytesize=8, timeout=2
171-
)
172-
self.__client.connect()
173-
self.__log.info('Serial connection established...')
174-
175170
self.__log.info("start connection mqtt ...")
176171
self.__mqtt_host = self.__settings.get(
177172
'mqtt', 'host', fallback='mqtt.eclipseprojects.io')
@@ -209,7 +204,12 @@ def init_invertermodbustomqtt(self):
209204
self.__send_holding_register = self.__settings.getboolean(section, 'send_holding_register', fallback=False)
210205
self.__send_input_register = self.__settings.getboolean(section, 'send_input_register', fallback=True)
211206
self.measurement = self.__settings.get(section, 'measurement', fallback="")
212-
self.inverter = Inverter(self.__client, name, unit, protocol_version, self.__max_precision, self.__log)
207+
208+
reader_settings : dict[str, object] = {}
209+
reader_settings["port"] = self.__settings.get('serial', 'port', fallback='/dev/ttyUSB0')
210+
reader_settings["baudrate"] = self.__settings.getint('serial', 'baudrate', fallback=9600)
211+
212+
self.inverter = Inverter(name, unit, protocol_version, settings=reader_settings, max_precision=self.__max_precision, log=self.__log)
213213
self.inverter.print_info()
214214

215215

protocol_settings.py

Lines changed: 25 additions & 7 deletions
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+
import itertools
45
import json
56
import re
67
import os
@@ -105,6 +106,7 @@ class registry_map_entry:
105106

106107
class protocol_settings:
107108
protocol : str
109+
reader : str
108110
settings_dir : str
109111
variable_mask : list[str]
110112
input_registry_map : list[registry_map_entry]
@@ -130,6 +132,11 @@ def __init__(self, protocol : str, settings_dir : str = 'protocols'):
130132
self.variable_mask.append(line.strip().lower())
131133

132134
self.load__codes() #load first, so priority to json codes
135+
if "reader" in self.codes:
136+
self.reader = self.codes["reader"]
137+
else:
138+
self.reader = "modbus_rtu"
139+
133140
self.load__input_registry_map()
134141
self.load__holding_registry_map()
135142

@@ -171,10 +178,21 @@ def load__registry(self, path, registry_type : Registry_Type = Registry_Type.INP
171178
if not os.path.exists(path): #return empty is file doesnt exist.
172179
return registry_map
173180

181+
def clean_header(iterator):
182+
# Lowercase and strip whitespace from each item in the first row
183+
first_row = next(iterator).lower().replace('_', ' ')
184+
first_row = re.sub(r"\s+;|;\s+", ";", first_row) #trim values
185+
return itertools.chain([first_row], iterator)
186+
187+
174188
with open(path, newline='', encoding='latin-1') as csvfile:
189+
190+
#clean column names before passing to csv dict reader
191+
csvfile = clean_header(csvfile)
192+
175193
# Create a CSV reader object
176-
reader = csv.DictReader(csvfile, delimiter=';') #compensate for openoffice
177-
194+
reader = csv.DictReader(clean_header(csvfile), delimiter=';') #compensate for openoffice
195+
178196
# Iterate over each row in the CSV file
179197
for row in reader:
180198

@@ -202,8 +220,8 @@ def load__registry(self, path, registry_type : Registry_Type = Registry_Type.INP
202220
row['documented name'] = row['documented name'].strip().lower().replace(' ', '_')
203221

204222
variable_name = row['variable name'] if row['variable name'] else row['documented name']
205-
variable_name = variable_name.lower().replace(' ', '_').replace('__', '_') #clean name
206-
223+
variable_name = variable_name = variable_name.strip().lower().replace(' ', '_').replace('__', '_') #clean name
224+
207225
if re.search("[^a-zA-Z0-9\_]", variable_name) :
208226
print("WARNING Invalid Name : " + str(variable_name) + " reg: " + str(row['register']) + " doc name: " + str(row['documented name']) + " path: " + str(path))
209227

@@ -239,7 +257,7 @@ def load__registry(self, path, registry_type : Registry_Type = Registry_Type.INP
239257
codes_json = json.loads(row['values'])
240258
value_is_json = True
241259

242-
name = item.documented_name+'_codes'
260+
name = row['documented name']+'_codes'
243261
if name not in self.codes:
244262
self.codes[name] = codes_json
245263

@@ -280,8 +298,8 @@ def load__registry(self, path, registry_type : Registry_Type = Registry_Type.INP
280298
if end > start:
281299
concatenate = True
282300
if reverse:
283-
for i in range(end, start, -1):
284-
concatenate_registers.append(i-1)
301+
for i in range(end, start-1, -1):
302+
concatenate_registers.append(i)
285303
else:
286304
for i in range(start, end+1):
287305
concatenate_registers.append(i)

0 commit comments

Comments
 (0)