Skip to content

Commit

Permalink
Merge pull request #17 from HotNoob/v1.0.7
Browse files Browse the repository at this point in the history
V1.0.7
  • Loading branch information
HotNoob authored Mar 24, 2024
2 parents c704fa3 + 77f2fd2 commit bc84dd5
Show file tree
Hide file tree
Showing 15 changed files with 487 additions and 326 deletions.
89 changes: 27 additions & 62 deletions inverter.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@
import logging
import re
import time
import struct
import importlib

from pymodbus.exceptions import ModbusIOException

from typing import TYPE_CHECKING
if TYPE_CHECKING:
from pymodbus.client.sync import ModbusSerialClient as ModbusClient

from readers.reader_base import reader_base
from protocol_settings import Data_Type, registry_map_entry, protocol_settings, Registry_Type

class Inverter:
Expand All @@ -20,15 +22,18 @@ class Inverter:
max_precision : int
modbus_delay : float = 0.85
modbus_version = ""
reader : reader_base
settings : dict[str, str]

'''time inbetween requests'''

def __init__(self, client, name, unit, protocol_version, max_precision : int = -1, log = None):
self.client : ModbusClient = client
def __init__(self, name, unit, protocol_version, settings : dict[str,str], max_precision : int = -1, log = None):
self.name = name
self.unit = unit
self.protocol_version = protocol_version
self.max_precision = max_precision
self.settings = settings

print("max_precision: " + str(self.max_precision))
if (log is None):
self.__log = log
Expand All @@ -39,6 +44,16 @@ def __init__(self, client, name, unit, protocol_version, max_precision : int = -
#load protocol settings
self.protocolSettings = protocol_settings(self.protocol_version)

#load reader
# Import the module
module = importlib.import_module('readers.'+self.protocolSettings.reader)

# Get the class from the module
cls = getattr(module, self.protocolSettings.reader)

self.reader : reader_base = cls(self.settings)
self.reader.connect()

self.read_info()

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

if not ranges: #ranges is empty, use min max
ranges = []
start = -batch_size
start = start - batch_size
while( start := start + batch_size ) < end:
ranges.append((start, batch_size)) ##APPEND TUPLE
count = batch_size
if start + batch_size > end:
count = end - start + 1
ranges.append((start, count)) ##APPEND TUPLE

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

isError = False
try:
if registry_type == Registry_Type.INPUT:
register = self.client.read_input_registers(range[0], range[1], unit=self.unit)
else:
print("get holding")
register = self.client.read_holding_registers(range[0], range[1], unit=self.unit)
#register.addCallback
register = self.reader.read_registers(range[0], range[1], registry_type=registry_type, unit=self.unit)

except ModbusIOException as e:
print("ModbusIOException : ", e.error_code)
Expand Down Expand Up @@ -181,14 +194,13 @@ def read_registers(self, ranges : list[tuple] = None, start : int = 0, end : int
retry -= 1
if retry < 0:
retry = 0

#combine registers into "registry"
print("combine results, " + str(len(register.registers)))
i = -1
while(i := i + 1 ) < range[1]:
#print(str(i) + " => " + str(i+range[0]))
registry[i+range[0]] = register.registers[i]

print("registry len: " + str(len(registry)))
return registry

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

registry = self.read_registers(self.protocolSettings.holding_registry_ranges, registry_type=Registry_Type.HOLDING)
info = self.process_registery(registry, self.protocolSettings.holding_registry_map)
return info


# def read_fault_table(self, name, base_index, count):
# fault_table = {}
# for i in range(0, count):
# fault_table[name + '_' + str(i)] = self.read_fault_record(base_index + i * 5)
# return fault_table
#
# def read_fault_record(self, index):
# row = self.client.read_input_registers(index, 5, unit=self.unit)
# # TODO: Figure out how to read the date for these records?
# print(row.registers[0],
# ErrorCodes[row.registers[0]],
# '\n',
# row.registers[1],
# row.registers[2],
# row.registers[3],
# '\n',
# 2000 + (row.registers[1] >> 8),
# row.registers[1] & 0xFF,
# row.registers[2] >> 8,
# row.registers[2] & 0xFF,
# row.registers[3] >> 8,
# row.registers[3] & 0xFF,
# row.registers[4],
# '\n',
# 2000 + (row.registers[1] >> 4),
# row.registers[1] & 0xF,
# row.registers[2] >> 4,
# row.registers[2] & 0xF,
# row.registers[3] >> 4,
# row.registers[3] & 0xF,
# row.registers[4]
# )
# return {
# 'FaultCode': row.registers[0],
# 'Fault': ErrorCodes[row.registers[0]],
# #'Time': int(datetime.datetime(
# # 2000 + (row.registers[1] >> 8),
# # row.registers[1] & 0xFF,
# # row.registers[2] >> 8,
# # row.registers[2] & 0xFF,
# # row.registers[3] >> 8,
# # row.registers[3] & 0xFF
# #).timestamp()),
# 'Value': row.registers[4]
# }
return info
42 changes: 21 additions & 21 deletions invertermodbustomqtt.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,25 @@
"""
Main module for Growatt / Inverters ModBus RTU data to MQTT
"""


import sys
import time

# Check if Python version is greater than 3.9
if sys.version_info < (3, 9):
print("==================================================")
print("WARNING: python version 3.9 or higher is recommended")
print("Current version: " + sys.version)
print("Please upgrade your python version to 3.9")
print("==================================================")
time.sleep(4)

import atexit
import glob
import random
import re
import time

import os
import json
import logging
Expand All @@ -16,13 +30,11 @@
import paho.mqtt.client as mqtt
from paho.mqtt.properties import Properties
from paho.mqtt.packettypes import PacketTypes
from pymodbus.client.sync import ModbusSerialClient as ModbusClient

from inverter import Inverter

from protocol_settings import protocol_settings,Data_Type,registry_map_entry,Registry_Type


__logo = """
____ _ _ ____ __ __ ___ _____ _____
/ ___|_ __ _____ ____ _| |_| |_|___ \| \/ |/ _ \_ _|_ _|
Expand Down Expand Up @@ -50,8 +62,6 @@ class InverterModBusToMQTT:
__port = None
# baudrate to access modbus connection
__baudrate = -1
# modbus client handle
__client = None
# mqtt server host address
__mqtt_host = None
# mqtt client handle
Expand Down Expand Up @@ -157,21 +167,6 @@ def init_invertermodbustomqtt(self):
self.__log.setLevel(logging.getLevelName(self.__log_level))



self.__log.info('Setup Serial Connection... ')
self.__port = self.__settings.get(
'serial', 'port', fallback='/dev/ttyUSB0')
self.__baudrate = self.__settings.get(
'serial', 'baudrate', fallback=9600)


self.__client = ModbusClient(method='rtu', port=self.__port,
baudrate=int(self.__baudrate),
stopbits=1, parity='N', bytesize=8, timeout=2
)
self.__client.connect()
self.__log.info('Serial connection established...')

self.__log.info("start connection mqtt ...")
self.__mqtt_host = self.__settings.get(
'mqtt', 'host', fallback='mqtt.eclipseprojects.io')
Expand Down Expand Up @@ -209,7 +204,12 @@ def init_invertermodbustomqtt(self):
self.__send_holding_register = self.__settings.getboolean(section, 'send_holding_register', fallback=False)
self.__send_input_register = self.__settings.getboolean(section, 'send_input_register', fallback=True)
self.measurement = self.__settings.get(section, 'measurement', fallback="")
self.inverter = Inverter(self.__client, name, unit, protocol_version, self.__max_precision, self.__log)

reader_settings : dict[str, object] = {}
reader_settings["port"] = self.__settings.get('serial', 'port', fallback='/dev/ttyUSB0')
reader_settings["baudrate"] = self.__settings.getint('serial', 'baudrate', fallback=9600)

self.inverter = Inverter(name, unit, protocol_version, settings=reader_settings, max_precision=self.__max_precision, log=self.__log)
self.inverter.print_info()


Expand Down
32 changes: 25 additions & 7 deletions 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
import itertools
import json
import re
import os
Expand Down Expand Up @@ -105,6 +106,7 @@ class registry_map_entry:

class protocol_settings:
protocol : str
reader : str
settings_dir : str
variable_mask : list[str]
input_registry_map : list[registry_map_entry]
Expand All @@ -130,6 +132,11 @@ def __init__(self, protocol : str, settings_dir : str = 'protocols'):
self.variable_mask.append(line.strip().lower())

self.load__codes() #load first, so priority to json codes
if "reader" in self.codes:
self.reader = self.codes["reader"]
else:
self.reader = "modbus_rtu"

self.load__input_registry_map()
self.load__holding_registry_map()

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

def clean_header(iterator):
# Lowercase and strip whitespace from each item in the first row
first_row = next(iterator).lower().replace('_', ' ')
first_row = re.sub(r"\s+;|;\s+", ";", first_row) #trim values
return itertools.chain([first_row], iterator)


with open(path, newline='', encoding='latin-1') as csvfile:

#clean column names before passing to csv dict reader
csvfile = clean_header(csvfile)

# Create a CSV reader object
reader = csv.DictReader(csvfile, delimiter=';') #compensate for openoffice
reader = csv.DictReader(clean_header(csvfile), delimiter=';') #compensate for openoffice

# Iterate over each row in the CSV file
for row in reader:

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

variable_name = row['variable name'] if row['variable name'] else row['documented name']
variable_name = variable_name.lower().replace(' ', '_').replace('__', '_') #clean name

variable_name = variable_name = variable_name.strip().lower().replace(' ', '_').replace('__', '_') #clean name
if re.search("[^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))

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

name = item.documented_name+'_codes'
name = row['documented name']+'_codes'
if name not in self.codes:
self.codes[name] = codes_json

Expand Down Expand Up @@ -280,8 +298,8 @@ def load__registry(self, path, registry_type : Registry_Type = Registry_Type.INP
if end > start:
concatenate = True
if reverse:
for i in range(end, start, -1):
concatenate_registers.append(i-1)
for i in range(end, start-1, -1):
concatenate_registers.append(i)
else:
for i in range(start, end+1):
concatenate_registers.append(i)
Expand Down
Loading

0 comments on commit bc84dd5

Please sign in to comment.