diff --git a/.gitignore b/.gitignore index af7ee80c..77746aad 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ growatt2mqtt.cfg growatt2mqtt.service config.cfg *.cfg +*.old invertermodbustomqtt.service log.txt variable_mask.txt diff --git a/README.md b/README.md index 4753df7f..57b40db8 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,8 @@ [![CodeQL](https://github.com/HotNoob/PythonProtocolGateway/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/HotNoob/PythonProtocolGateway/actions/workflows/github-code-scanning/codeql) -For advanced configuration help, please checkout the wiki :) +For advanced configuration help, please checkout the documentation :) +https://github.com/HotNoob/PythonProtocolGateway/tree/main/documentation # Python Protocol Gateway @@ -14,9 +15,9 @@ Python Protocol Gateway is a python-based service that reads data via Modbus RTU Configuration is handled via a small config files. In the long run, Python Protocol Gateway will become a general purpose protocol gateway to translate between more than just modbus and mqtt. -For specific device installation instructions please checkout the wiki: +For specific device installation instructions please checkout the documentation: Growatt, EG4, Sigineer, SOK, PACE-BMS -https://github.com/HotNoob/PythonProtocolGateway/wiki +https://github.com/HotNoob/PythonProtocolGateway/tree/main/documentation # General Installation Connect the USB port on the inverter into your computer / device. This port is essentially modbus usb adapter. @@ -56,7 +57,8 @@ 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 +more details on these protocols can be found in the documentation: +https://github.com/HotNoob/PythonProtocolGateway/tree/main/documentation ### run as script ``` diff --git a/classes/protocol_settings.py b/classes/protocol_settings.py index f1801e0d..c6680591 100644 --- a/classes/protocol_settings.py +++ b/classes/protocol_settings.py @@ -2,6 +2,7 @@ from dataclasses import dataclass from enum import Enum import glob +import logging from typing import Union from defs.common import strtoint import itertools @@ -233,7 +234,18 @@ class protocol_settings: settings : dict[str, str] ''' default settings provided by protocol json ''' + byteorder : str = "big" + + _log : logging.Logger = None + + def __init__(self, protocol : str, settings_dir : str = 'protocols'): + + #apply log level to logger + self._log_level = getattr(logging, logging.getLevelName(logging.getLogger().getEffectiveLevel()), logging.INFO) + self._log : logging.Logger = logging.getLogger(__name__) + self._log.setLevel(self._log_level) + self.protocol = protocol self.settings_dir = settings_dir @@ -266,6 +278,8 @@ def __init__(self, protocol : str, settings_dir : str = 'protocols'): else: self.transport = "modbus_rtu" + if "byteorder" in self.settings: #handle byte order for ints n stuff + self.byteorder = self.settings["byteorder"] for registry_type in Registry_Type: self.load_registry_map(registry_type) @@ -305,7 +319,7 @@ def load__json(self, file : str = '', settings_dir : str = ''): #if path does not exist; nothing to load. skip. if not path: - print("ERROR: '"+file+"' not found") + self._log.error("ERROR: '"+file+"' not found") return with open(path) as f: @@ -321,13 +335,13 @@ def load__json(self, file : str = '', settings_dir : str = ''): def load__registry(self, path, registry_type : Registry_Type = Registry_Type.INPUT) -> list[registry_map_entry]: registry_map : list[registry_map_entry] = [] - register_regex = re.compile(r'(?P(?:0?x[\dA-Z]+|[\d]+))\.(b(?Px?\d{1,2})|(?Px?\d{1,2}))') + register_regex = re.compile(r'(?P(?:0?x[\da-z]+|[\d]+))\.(b(?Px?\d{1,2})|(?Px?\d{1,2}))') data_type_regex = re.compile(r'(?P\w+)\.(?P\d+)') - range_regex = re.compile(r'(?Pr|)(?P(?:0?x[\dA-Z]+|[\d]+))[\-~](?P(?:0?x[\dA-Z]+|[\d]+))') + range_regex = re.compile(r'(?Pr|)(?P(?:0?x[\da-z]+|[\d]+))[\-~](?P(?:0?x[\da-z]+|[\d]+))') ascii_value_regex = re.compile(r'(?P^\[.+\]$)') - list_regex = re.compile(r'\s*(?:(?P(?:0?x[\dA-Z]+|[\d]+))-(?P(?:0?x[\dA-Z]+|[\d]+))|(?P[^,\s][^,]*?))\s*(?:,|$)') + list_regex = re.compile(r'\s*(?:(?P(?:0?x[\da-z]+|[\d]+))-(?P(?:0?x[\da-z]+|[\d]+))|(?P[^,\s][^,]*?))\s*(?:,|$)') if not os.path.exists(path): #return empty is file doesnt exist. @@ -386,7 +400,7 @@ def determine_delimiter(first_row) -> str: variable_name = variable_name = variable_name.strip().lower().replace(' ', '_').replace('__', '_') #clean 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)) + self._log.warning("Invalid Name : " + str(variable_name) + " reg: " + str(row['register']) + " doc name: " + str(row['documented name']) + " path: " + str(path)) #convert to float try: @@ -402,7 +416,7 @@ def determine_delimiter(first_row) -> str: if 'values' not in row: row['values'] = "" - print("WARNING No Value Column : path: " + str(path)) + self._log.warning("No Value Column : path: " + str(path)) data_type_len : int = -1 #optional row, only needed for non-default data types @@ -471,6 +485,7 @@ def determine_delimiter(first_row) -> str: register : int = -1 register_bit : int = 0 register_byte : int = -1 + row['register'] = row['register'].lower() #ensure is all lower case match = register_regex.search(row['register']) if match: register = strtoint(match.group('register')) @@ -673,7 +688,11 @@ def load_registry_map(self, registry_type : Registry_Type, file : str = '', sett def process_register_bytes(self, registry : dict[int,bytes], entry : registry_map_entry): ''' process bytes into data''' - register = registry[entry.register] + if isinstance(registry[entry.register], tuple): + register = registry[entry.register][0] #can bus uses tuple for timestamp + else: + register = registry[entry.register] + if entry.register_byte > 0: register = register[entry.register_byte:] @@ -681,13 +700,13 @@ def process_register_bytes(self, registry : dict[int,bytes], entry : registry_ma register = register[:entry.data_type_size] if entry.data_type == Data_Type.UINT: - value = int.from_bytes(register[:4], byteorder='big', signed=False) + value = int.from_bytes(register[:4], byteorder=self.byteorder, signed=False) elif entry.data_type == Data_Type.INT: - value = int.from_bytes(register[:4], byteorder='big', signed=True) + value = int.from_bytes(register[:4], byteorder=self.byteorder, signed=True) elif entry.data_type == Data_Type.USHORT: - value = int.from_bytes(register[:2], byteorder='big', signed=False) + value = int.from_bytes(register[:2], byteorder=self.byteorder, signed=False) elif entry.data_type == Data_Type.SHORT: - value = int.from_bytes(register[:2], byteorder='big', signed=True) + value = int.from_bytes(register[:2], byteorder=self.byteorder, signed=True) 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 @@ -700,8 +719,10 @@ def process_register_bytes(self, registry : dict[int,bytes], entry : registry_ma #handle custom sizes, less than 1 register end_bit = flag_size + start_bit - if entry.documented_name+'_codes' in self.protocolSettings.codes: + if entry.documented_name+'_codes' in self.codes: + code_key : str = entry.documented_name+'_codes' flags : list[str] = [] + flag_indexes : list[str] = [] for i in range(start_bit, end_bit): # Iterate over each bit position (0 to 15) byte = i // 8 bit = i % 8 @@ -709,9 +730,20 @@ def process_register_bytes(self, registry : dict[int,bytes], entry : registry_ma # Check if the i-th bit is set if (val >> bit) & 1: flag_index = "b"+str(i) - if flag_index in self.protocolSettings.codes[entry.documented_name+'_codes']: - flags.append(self.protocolSettings.codes[entry.documented_name+'_codes'][flag_index]) - + flag_indexes.append(flag_index) + if flag_index in self.codes[code_key]: + flags.append(self.codes[code_key][flag_index]) + + #check multibit flags + multibit_flags = [key for key in self.codes if '&' in key] + + if multibit_flags: #if multibit flags are found + flag_indexes_set : set[str] = set(flag_indexes) + for multibit_flag in multibit_flags: + bits = multibit_flag.split('&') # Split key into 'bits' + if all(bit in flag_indexes_set for bit in bits): # Check if all bits are present in the flag_indexes_set + flags.append(self.codes[code_key][multibit_flag]) + value = ",".join(flags) else: flags : list[str] = [] @@ -760,7 +792,23 @@ def process_register_bytes(self, registry : dict[int,bytes], entry : registry_ma try: value = register.decode("utf-8") #convert bytes to ascii except UnicodeDecodeError as e: - print("UnicodeDecodeError:", e) + self._log.error("UnicodeDecodeError:", e) + + #apply unit mod + if entry.unit_mod != float(1): + value = value * entry.unit_mod + + #apply codes + if (entry.data_type != Data_Type._16BIT_FLAGS and + entry.documented_name+'_codes' in self.codes): + try: + cleanval = str(int(value)) + + if cleanval in self.codes[entry.documented_name+'_codes']: + value = self.codes[entry.documented_name+'_codes'][cleanval] + except: + #do nothing; try is for intval + value = value return value @@ -855,11 +903,11 @@ def process_register_ushort(self, registry : dict[int, int], entry : registry_ma bit_index = entry.register_bit value = (registry[entry.register] >> bit_index) & bit_mask elif entry.data_type == Data_Type.ASCII: - value = registry[entry.register].to_bytes((16 + 7) // 8, byteorder='big') #convert to ushort to bytes + value = registry[entry.register].to_bytes((16 + 7) // 8, byteorder=self.byteorder) #convert to ushort to bytes try: value = value.decode("utf-8") #convert bytes to ascii except UnicodeDecodeError as e: - print("UnicodeDecodeError:", e) + self._log.error("UnicodeDecodeError:", e) else: #default, Data_Type.USHORT value = float(registry[entry.register]) diff --git a/classes/transports/canbus.py b/classes/transports/canbus.py new file mode 100644 index 00000000..b01c38b7 --- /dev/null +++ b/classes/transports/canbus.py @@ -0,0 +1,276 @@ +import re +import time +import can +import asyncio +import threading +import platform +import os + + + +from .transport_base import transport_base +from ..protocol_settings import Data_Type, Registry_Type, registry_map_entry, protocol_settings +from defs.common import strtobool, strtoint +from collections import OrderedDict + +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from configparser import SectionProxy + +class canbus(transport_base): + ''' canbus is a more passive protocol; todo to include active commands to trigger canbus responses ''' + + interface : str = 'socketcan' + ''' bustype / interface for canbus device ''' + + port : str = '' + ''' 'can0' ''' + + baudrate : int = 500000 + + bus : can.BusABC = None + ''' holds canbus interface''' + + reader = can.AsyncBufferedReader() + + thread : threading.Thread + ''' main thread for async loop''' + + #lock : threading.Lock = threading.Lock() + lock : threading.Lock = None + loop : asyncio.AbstractEventLoop = None + + cache : OrderedDict [int,(bytes, float)] = None + ''' cache, key is id, value is touple (data, timestamp)''' + + cacheTimeout : int = 120 + ''' seconds to keep message in cache ''' + + emptyTime : float = None + ''' the last time values were read for watchdog''' + + watchDogTime : float = 120 + ''' number of seconds of empty cache before restarting''' + + linux : bool = True + + + def __init__(self, settings : 'SectionProxy', protocolSettings : 'protocol_settings' = None): + super().__init__(settings, protocolSettings=protocolSettings) + + #check if running on windows or linux + self.linux = platform.system() != 'Windows' + + + self.port = settings.get(["port", "channel"], "").lower() + if not self.port: + raise ValueError("Port/Channel is not set") + + #get default baud from protocol settings + if "baud" in self.protocolSettings.settings: + self.baudrate = strtoint(self.protocolSettings.settings["baud"]) + + self.baudrate = settings.getint(["baudrate", "bitrate"], self.baudrate) + self.interface = settings.get(["interface", "bustype"], self.interface).lower() + self.cacheTimeout = settings.getint(["cacheTimeout", "cache_timeout"], self.cacheTimeout) + + #setup / configure socketcan + if self.interface == "socketcan": + self.setup_socketcan() + + self.bus = can.interface.Bus(interface=self.interface, channel=self.port, bitrate=self.baudrate) + self.reader = can.AsyncBufferedReader() + self.lock = threading.Lock() + with self.lock: + self.cache = OrderedDict() + + + # Set up an event loop and run the async function + #self.loop = asyncio.get_event_loop() + + #notifier = can.Notifier(self.bus, [self.reader], loop=self.loop) + + + thread = threading.Thread(target=self.start_loop) + thread.daemon = True + thread.start() + + self.connected = True + self.emptyTime =time.time() + + self.init_after_connect() + + def setup_socketcan(self): + ''' ensures socketcan interface is up and applies some common hotfixes ''' + if not self.linux: + print("socketcan setup not implemented for windows") + return + + self._log.info("restart and configure socketcan") + os.system("ip link set can0 down") + os.system("ip link set can0 type can restart-ms 100") + os.system("ip link set can0 up type can bitrate " + str(self.baudrate)) + + def is_socketcan_up(self) -> bool: + if not self.linux: + self._log.error("socketcan status not implemented for windows") + return True + + try: + with open(f'/sys/class/net/{self.port}/operstate', 'r') as f: + state = f.read().strip() + return state == 'up' + except FileNotFoundError: + return False + + def start_loop(self): + self.read_bus(self.bus) + + def read_bus(self, bus : can.BusABC): + ''' read canbus asynco and store results in cache''' + msg = None #fix scope bug + + while True: + try: + msg = self.bus.recv() # This will be non-blocking with asyncio + + except can.CanError as e: + # Handle specific CAN errors + self._log.error(f"CAN error: {e}") + except asyncio.CancelledError: + # Handle the case where the task is cancelled + self._log.error("Read bus task was cancelled.") + break + except Exception as e: + # Handle unexpected errors + self._log.error(f"An unexpected error occurred: {e}") + + + if msg: + self._log.info(f"Received message: {msg.arbitration_id:X}, data: {msg.data}") + + with self.lock: + #convert bytearray to bytes; were working with bytes. + self.cache[msg.arbitration_id] = (bytes(msg.data), time.time()) + + #time.sleep(1) no need for sleep because recv is blocking + + + def clean_cache(self): + current_time = time.time() + + with self.lock: + # Create a list of keys to remove (don't remove while iterating) + keys_to_delete = [msg_id for msg_id, (_, timestamp) in self.cache.items() if current_time - timestamp > self.cacheTimeout] + + # Remove old messages from the dictionary + for key in keys_to_delete: + del self.cache[key] + + def init_after_connect(self): + return True + + ''' todo, a startup phase to get serial number''' + #from transport_base settings + if self.write_enabled: + self.enable_write() + + #if sn is empty, attempt to autoread it + if not self.device_serial_number: + self.device_serial_number = self.read_serial_number() + + def read_serial_number(self) -> str: + ''' not so simple in canbus''' + return '' + serial_number = str(self.read_variable("Serial Number", Registry_Type.HOLDING)) + print("read SN: " +serial_number) + if serial_number: + return serial_number + + sn2 = "" + sn3 = "" + fields = ['Serial No 1', 'Serial No 2', 'Serial No 3', 'Serial No 4', 'Serial No 5'] + for field in fields: + self._log.info("Reading " + field) + 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.read_modbus_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() + + serial_number = serial_number + str(data.registers[0]) + + data_bytes = data.registers[0].to_bytes((data.registers[0].bit_length() + 7) // 8, byteorder='big') + sn2 = sn2 + str(data_bytes.decode('utf-8')) + sn3 = str(data_bytes.decode('utf-8')) + sn3 + + time.sleep(self.modbus_delay*2) #sleep inbetween requests so modbus can rest + + print(sn2) + print(sn3) + + if not re.search("[^a-zA-Z0-9\_]", sn2) : + serial_number = sn2 + + return serial_number + + def enable_write(self): + self.write_enabled = True + self._log.warning("enable write - validation on the todo") + + def write_data(self, data : dict[str, str]) -> None: + if not self.write_enabled: + return + + def read_data(self) -> dict[str, str]: + ''' because canbus is passive / broadcast, were just going to read from the cache ''' + + info = {} + + #remove timestamp for processing + with self.lock: + registry = {key: value[0] for key, value in self.cache.items()} + + new_info = self.protocolSettings.process_registery(registry, self.protocolSettings.get_registry_map(Registry_Type.ZERO)) + + info.update(new_info) + + currentTime = time.time() + + if not info: + self._log.info("Register/Cache is Empty; no new information reported.") + if currentTime - self.emptyTime > self.watchDogTime: + self._log.error("Register/Cache has been empty for over " + str(self.watchDogTime) + "seconds. watchdog qutting application. ") + quit() #quit application, service should be configured to restart + + else: + self.emptyTime = currentTime + + self.clean_cache() #clean cache of old data + + return info + + def read_variable(self, variable_name : str, registry_type : Registry_Type, entry : registry_map_entry = None): + ''' read's variable from cache''' + ##clean for convinecne + if variable_name: + variable_name = variable_name.strip().lower().replace(' ', '_') + + registry_map = self.protocolSettings.get_registry_map(registry_type) + + if entry == None: + for e in registry_map: + if e.variable_name == variable_name: + entry = e + break + + if entry: + #no concat for canbus or concat on todo + with self.lock: + if entry.register in self.cache: + results = self.protocolSettings.process_register_bytes(self.cache, entry) + return results[entry.variable_name] + else: + return None #empty \ No newline at end of file diff --git a/classes/transports/modbus_base.py b/classes/transports/modbus_base.py index 2d215db4..130ff9d7 100644 --- a/classes/transports/modbus_base.py +++ b/classes/transports/modbus_base.py @@ -14,6 +14,13 @@ from configparser import SectionProxy class modbus_base(transport_base): + + modbus_delay_increament : float = 0.05 + ''' delay adjustment every error. todo: add a setting for this ''' + + modbus_delay_setting : float = 0.85 + '''time inbetween requests, unmodified''' + modbus_delay : float = 0.85 '''time inbetween requests''' @@ -38,9 +45,14 @@ def __init__(self, settings : 'SectionProxy', protocolSettings : 'protocol_setti if 'send_holding_register' in self.protocolSettings.settings: self.send_holding_register = strtobool(self.protocolSettings.settings['send_holding_register']) + if 'batch_delay' in self.protocolSettings.settings: + self.modbus_delay = float(self.protocolSettings.settings['batch_delay']) + #allow enable/disable of which registers to send self.send_holding_register = settings.getboolean('send_holding_register', fallback=self.send_holding_register) self.send_input_register = settings.getboolean('send_input_register', fallback=self.send_input_register) + self.modbus_delay = settings.getfloat(['batch_delay', 'modbus_delay'], fallback=self.modbus_delay) + self.modbus_delay_setting = self.modbus_delay if self.analyze_protocol_enabled: @@ -64,7 +76,7 @@ def connect(self): def read_serial_number(self) -> str: serial_number = str(self.read_variable("Serial Number", Registry_Type.HOLDING)) - print("read SN: " +serial_number) + self._log.info("read SN: " +serial_number) if serial_number: return serial_number @@ -98,12 +110,12 @@ def read_serial_number(self) -> str: return serial_number def enable_write(self): - print("Validating Protocol for Writing") + self._log.info("Validating Protocol for Writing") self.write_enabled = False score_percent = self.validate_protocol(Registry_Type.HOLDING) if(score_percent > 90): self.write_enabled = True - print("enable write - validation passed") + self._log.warning("enable write - validation passed") def write_data(self, data : dict[str, str]) -> None: if not self.write_enabled: @@ -167,7 +179,7 @@ def validate_registry(self, registry_type : Registry_Type = Registry_Type.INPUT) maxScore = len(registry_map) percent = score*100/maxScore - print("validation score: " + str(score) + " of " + str(maxScore) + " : " + str(round(percent)) + "%") + self._log.info("validation score: " + str(score) + " of " + str(maxScore) + " : " + str(round(percent)) + "%") return percent def analyze_protocol(self, settings_dir : str = 'protocols'): @@ -434,7 +446,7 @@ def read_modbus_registers(self, ranges : list[tuple] = None, start : int = 0, en while (index := index + 1) < len(ranges) : range = ranges[index] - print("get registers("+str(index)+"): " + str(range[0]) + " to " + str(range[0]+range[1]-1) + " ("+str(range[1])+")") + self._log.info("get registers ("+str(index)+"): " +str(registry_type)+ " - " + str(range[0]) + " to " + str(range[0]+range[1]-1) + " ("+str(range[1])+")") time.sleep(self.modbus_delay) #sleep for 1ms to give bus a rest #manual recommends 1s between commands isError = False @@ -442,7 +454,7 @@ def read_modbus_registers(self, ranges : list[tuple] = None, start : int = 0, en register = self.read_registers(range[0], range[1], registry_type=registry_type) except ModbusIOException as e: - print("ModbusIOException : ", e.error_code) + self._log.error("ModbusIOException : ", e.error_code) if e.error_code == 4: #if no response; probably time out. retry with increased delay isError = True else: @@ -450,7 +462,7 @@ def read_modbus_registers(self, ranges : list[tuple] = None, start : int = 0, en if register.isError() or isError: self._log.error(register.__str__) - self.modbus_delay = self.modbus_delay + 0.050 #increase delay, error is likely due to modbus being busy + self.modbus_delay += self.modbus_delay_increament #increase delay, error is likely due to modbus being busy if self.modbus_delay > 60: #max delay. 60 seconds between requests should be way over kill if it happens self.modbus_delay = 60 @@ -461,9 +473,14 @@ def read_modbus_registers(self, ranges : list[tuple] = None, start : int = 0, en #undo step in loop and retry read retry = retry + 1 total_retries = total_retries + 1 - print("Retry("+str(retry)+" - ("+str(total_retries)+")) range("+str(index)+")") + self._log.warning("Retry("+str(retry)+" - ("+str(total_retries)+")) range("+str(index)+")") index = index - 1 continue + elif self.modbus_delay > self.modbus_delay_setting: #no error, decrease delay + self.modbus_delay -= self.modbus_delay_increament + if self.modbus_delay < self.modbus_delay_setting: + self.modbus_delay = self.modbus_delay_setting + retry -= 1 diff --git a/classes/transports/mqtt.py b/classes/transports/mqtt.py index 20392f86..f184d031 100644 --- a/classes/transports/mqtt.py +++ b/classes/transports/mqtt.py @@ -98,7 +98,7 @@ def __init__(self, settings : SectionProxy): def connect(self): - print("mqtt connect") + self._log.info("mqtt connect") if self.__first_connection: self.__first_connection = False self.client.connect(str(self.host), int(self.port), 60) @@ -109,7 +109,7 @@ def connect(self): def exit_handler(self): '''on exit handler''' - print("MQTT Exiting...") + self._log.warning("MQTT Exiting...") self.client.publish( self.base_topic + "/availability","offline") return @@ -204,7 +204,7 @@ def init_bridge(self, from_transport : transport_base): self.mqtt_discovery(from_transport) def mqtt_discovery(self, from_transport : transport_base): - print("Publishing HA Discovery Topics...") + self._log.info("Publishing HA Discovery Topics...") disc_payload = {} disc_payload['availability_topic'] = self.base_topic + "/availability" @@ -240,7 +240,7 @@ def mqtt_discovery(self, from_transport : transport_base): clean_name = self.__holding_register_prefix + clean_name - print('Publishing Topic '+str(count)+' of ' + str(length) + ' "'+str(clean_name)+'"', end='\r', flush=True) + print(('#Publishing Topic '+str(count)+' of ' + str(length) + ' "'+str(clean_name)+'"').ljust(100)+"#", end='\r', flush=True) #device['sw_version'] = bms_version disc_payload = {} diff --git a/classes/transports/serial_pylon.py b/classes/transports/serial_pylon.py index e592c6fe..b4f6ee0c 100644 --- a/classes/transports/serial_pylon.py +++ b/classes/transports/serial_pylon.py @@ -94,11 +94,11 @@ def connect(self): version = self.read_variable('version', attribute='ver') if version: self.connected = True - print("pylon protocol version is "+str(version)) + self._log.info("pylon protocol version is "+str(version)) self.VER = version name = self.read_variable('battery_name') - print(name) + self._log.info(name) pass def read_data(self): diff --git a/classes/transports/transport_base.py b/classes/transports/transport_base.py index 048c62e8..d6363aed 100644 --- a/classes/transports/transport_base.py +++ b/classes/transports/transport_base.py @@ -34,11 +34,10 @@ 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_level = getattr(logging, settings.get('log_level', fallback='INFO'), logging.INFO) self._log : logging.Logger = logging.getLogger(__name__) self._log.setLevel(self._log_level) - logging.basicConfig(level=self._log_level) self.transport_name = settings.name #section name @@ -79,7 +78,7 @@ def _get_top_class_name(cls, cls_obj): else: return cls._get_top_class_name(cls_obj.__bases__[0]) - def connect(self, transports : 'transport_base'): + def connect(self): pass def write_data(self, data : dict[str, registry_map_entry]): diff --git a/config.cfg.example b/config.cfg.example index 74474c74..c2600e13 100644 --- a/config.cfg.example +++ b/config.cfg.example @@ -32,7 +32,6 @@ model = HDHK 16CH AC serial_number = HDHK777 read_interval = 10 -error_interval = 60 [transport.1] diff --git a/defs/common.py b/defs/common.py index f3e2c852..a66e63b2 100644 --- a/defs/common.py +++ b/defs/common.py @@ -19,12 +19,24 @@ def strtoint(val : str) -> int: if isinstance(val, int): #is already int. return val + + val = val.lower() if val and val[0] == 'x': - return int.from_bytes(bytes.fromhex(val[1:]), byteorder='big') + val = val[1:] + # Pad the string with a leading zero + if len(val) % 2 != 0: + val = '0' + val + + return int.from_bytes(bytes.fromhex(val), byteorder='big') if val and val.startswith("0x"): - return int.from_bytes(bytes.fromhex(val[2:]), byteorder='big') + val = val[2:] + # Pad the string with a leading zero + if len(val) % 2 != 0: + val = '0' + val + + return int.from_bytes(bytes.fromhex(val), byteorder='big') if not val: #empty return 0 @@ -42,6 +54,8 @@ def find_usb_serial_port(port : str = '', vendor_id : str = '', product_id : st if not port.startswith('['): return port + port = port.replace('None', '') + match = re.match(r"\[(?P[\da-zA-Z]+|):?(?P[\da-zA-Z]+|):?(?P[\da-zA-Z]+|):?(?P[\d\-]+|)\]", port) if match: vendor_id = int(match.group("vendor"), 16) if match.group("vendor") else '' diff --git a/documentation/3rdparty/README.md b/documentation/3rdparty/README.md index f38d8524..bfd5c9ce 100644 --- a/documentation/3rdparty/README.md +++ b/documentation/3rdparty/README.md @@ -26,6 +26,7 @@ This README file contains an index of all files in the documentation directory. - [PYLON LFP Battery communication protocol - RS485 V2.8 20161216.pdf](protocols/PYLON%20LFP%20Battery%20communication%20protocol%20-%20RS485%20V2.8%2020161216.pdf) - [RS485-protocol-pylon-low-voltage-V3.3-20180821.pdf](protocols/RS485-protocol-pylon-low-voltage-V3.3-20180821.pdf) - [Sigineer-Solar-Inverter-RS485-Port-Modbus-RTU-Protocol-v0.11-20200302.pdf](protocols/Sigineer-Solar-Inverter-RS485-Port-Modbus-RTU-Protocol-v0.11-20200302.pdf) +- [SMA CAN protocol.pdf](protocols/SMA%20CAN%20protocol.pdf) - [Sol-Ark ModBus V1.1.pdf](protocols/Sol-Ark%20ModBus%20V1.1.pdf) - [SRNE_MODBUS_v3.9.pdf](protocols/SRNE_MODBUS_v3.9.pdf) - [Victron VE-Bus-products-MK2-Protocol-3-14.pdf](protocols/Victron%20VE-Bus-products-MK2-Protocol-3-14.pdf) diff --git a/documentation/3rdparty/protocols/Growatt-BMS-CAN-Bus-protocol-low-voltage-V1.04-1.pdf b/documentation/3rdparty/protocols/Growatt-BMS-CAN-Bus-protocol-low-voltage-V1.04-1.pdf new file mode 100644 index 00000000..6da1e5ef Binary files /dev/null and b/documentation/3rdparty/protocols/Growatt-BMS-CAN-Bus-protocol-low-voltage-V1.04-1.pdf differ diff --git a/documentation/3rdparty/protocols/SMA CAN protocol.pdf b/documentation/3rdparty/protocols/SMA CAN protocol.pdf new file mode 100644 index 00000000..ed6e5d13 Binary files /dev/null and b/documentation/3rdparty/protocols/SMA CAN protocol.pdf differ diff --git a/documentation/3rdparty/protocols/Voltronic Inverter and BMS 485 communication protocol 20200325.pdf b/documentation/3rdparty/protocols/Voltronic Inverter and BMS 485 communication protocol 20200325.pdf new file mode 100644 index 00000000..b5da6030 Binary files /dev/null and b/documentation/3rdparty/protocols/Voltronic Inverter and BMS 485 communication protocol 20200325.pdf differ diff --git a/documentation/3rdparty/protocols/voltronic BMS Modbus Protocol for RS485 V1.1(2018-11-15).pdf b/documentation/3rdparty/protocols/voltronic BMS Modbus Protocol for RS485 V1.1(2018-11-15).pdf new file mode 100644 index 00000000..c8202157 Binary files /dev/null and b/documentation/3rdparty/protocols/voltronic BMS Modbus Protocol for RS485 V1.1(2018-11-15).pdf differ diff --git a/documentation/README.md b/documentation/README.md index 9b6944e8..f756603d 100644 --- a/documentation/README.md +++ b/documentation/README.md @@ -59,6 +59,7 @@ This README file contains an index of all files in the documentation directory. - [PYLON LFP Battery communication protocol - RS485 V2.8 20161216.pdf](3rdparty/protocols/PYLON%20LFP%20Battery%20communication%20protocol%20-%20RS485%20V2.8%2020161216.pdf) - [RS485-protocol-pylon-low-voltage-V3.3-20180821.pdf](3rdparty/protocols/RS485-protocol-pylon-low-voltage-V3.3-20180821.pdf) - [Sigineer-Solar-Inverter-RS485-Port-Modbus-RTU-Protocol-v0.11-20200302.pdf](3rdparty/protocols/Sigineer-Solar-Inverter-RS485-Port-Modbus-RTU-Protocol-v0.11-20200302.pdf) +- [SMA CAN protocol.pdf](3rdparty/protocols/SMA%20CAN%20protocol.pdf) - [Sol-Ark ModBus V1.1.pdf](3rdparty/protocols/Sol-Ark%20ModBus%20V1.1.pdf) - [SRNE_MODBUS_v3.9.pdf](3rdparty/protocols/SRNE_MODBUS_v3.9.pdf) - [Victron VE-Bus-products-MK2-Protocol-3-14.pdf](3rdparty/protocols/Victron%20VE-Bus-products-MK2-Protocol-3-14.pdf) diff --git a/documentation/dashboards/nodered.md b/documentation/dashboards/nodered.md index 32ae6c43..47df5dab 100644 --- a/documentation/dashboards/nodered.md +++ b/documentation/dashboards/nodered.md @@ -52,6 +52,8 @@ If you want to do some historical logging, InfluxDB is a better solution than th ### Import example Go to the hamburger, down to `Import`, click `select a file to import`, find `nodered-example-flow.json` in the repo. You should be left with something like this. ![example flow](https://github.com/user-attachments/assets/c2c284f8-e40f-4e05-bcb7-e054e32dad4c) +![example dashboard](https://github.com/user-attachments/assets/d5d283f2-694d-4fd7-ab6a-773d701a5226) + ### Create your own flow 1. Drag a `debug` node out - we will be using this throughout to see how the data flows. You can see the debug output by dragging a wire from an output to the debug's input and turning it on. @@ -79,4 +81,4 @@ Use sudo and your favorite editor to edit `/etc/nginx/sites-enabled/default`. ju You should now be able to browse to [Solar.local/Home](http://solar.local/Home/) --- -source: https://github.com/yNosGR/PythonProtocolGateway/blob/NodeRed_howto/NodeRed.MD \ No newline at end of file +source: https://github.com/yNosGR/PythonProtocolGateway/blob/NodeRed_howto/NodeRed.MD diff --git a/documentation/devices/AOLithium.md b/documentation/devices/AOLithium.md new file mode 100644 index 00000000..d62b1922 --- /dev/null +++ b/documentation/devices/AOLithium.md @@ -0,0 +1,26 @@ +# AOLithium To MQTT + +## Battery Protocols + +AOLithium has multiple protocols, which are set via the dip switches. + + +## Voltronic RS485 +``` +protocol_version = voltronic_bms_2020_03_25 +``` + +## SMA / Victron CanBus - dip5 off, dip 6 off +``` +protocol_version = victron_gx_generic_canbus +``` + +sma_sunny_island_v1 can also be used, but provides less information + +### CanBus pinout +see manual + +### hardware +1. USB Canbus adapter -- see canbus transport +2. rj45 ethernet cable cut in half + diff --git a/documentation/usage/configuration_examples/canbus_to_mqtt.md b/documentation/usage/configuration_examples/canbus_to_mqtt.md new file mode 100644 index 00000000..55a283c0 --- /dev/null +++ b/documentation/usage/configuration_examples/canbus_to_mqtt.md @@ -0,0 +1,41 @@ +### CanBus to MQTT +``` +[general] +log_level = DEBUG + +[transport.0] #name must be unique, ie: transport.canbus +#canbus device +#protocol config files are located in protocols/ +protocol_version = victron_gx_generic_canbus + +#canbus port or interface; varies based on usb adapter +port = can0 +bustype = socketcan +baudrate = 500000 + +#the 'transport' that we want to share this with +bridge = transport.1 + +manufacturer = {{Your device's manufacturer here}} +model = {{Your device's model number here}} +#optional; leave blank to autofetch serial from device. on the todo to autofetch serial for canbus +serial_number = {{random numbers here}} + +# canbus is a passive protocol, this is how often information is sent to mqtt. actual data interval is dependant on device +read_interval = 10 + + +[transport.1] +#connect mqtt +transport=mqtt +host = {{mqtt ip / host}} +port = 1883 +user = {{mqtt username here}} +pass = {{mqtt password}} +base_topic = home/inverter/ +error_topic = /error +json = false +discovery_enabled = true +discovery_topic = homeassistant +``` + diff --git a/documentation/usage/configuration_examples/modbus_rtu_to_modbus_tcp.md b/documentation/usage/configuration_examples/modbus_rtu_to_modbus_tcp.md index 0f564cea..42c72f8c 100644 --- a/documentation/usage/configuration_examples/modbus_rtu_to_modbus_tcp.md +++ b/documentation/usage/configuration_examples/modbus_rtu_to_modbus_tcp.md @@ -31,7 +31,6 @@ model = {{Your device's model number here}} serial_number = read_interval = 10 -error_interval = 60 [transport.1] diff --git a/documentation/usage/configuration_examples/modbus_rtu_to_mqtt.md b/documentation/usage/configuration_examples/modbus_rtu_to_mqtt.md index 1dc4fba1..da93887d 100644 --- a/documentation/usage/configuration_examples/modbus_rtu_to_mqtt.md +++ b/documentation/usage/configuration_examples/modbus_rtu_to_mqtt.md @@ -30,8 +30,6 @@ model = {{Your device's model number here}} serial_number = read_interval = 10 -error_interval = 60 - [transport.1] #connect mqtt diff --git a/documentation/usage/creating_and_editing_protocols.md b/documentation/usage/creating_and_editing_protocols.md index 670aaf78..efd3e26f 100644 --- a/documentation/usage/creating_and_editing_protocols.md +++ b/documentation/usage/creating_and_editing_protocols.md @@ -69,6 +69,32 @@ R = Read Only RD = Read Disabled W = Write ``` +#### values / codes +there are two main purposes for this column. +1. defines possible values / ranges of values for protocol validation / safety +for example: +```0~100``` + +2. defines the values for flag data types, such as 16BIT_FLAGS; any data type can be used. +the format for these flags is json. these flags / codes can also be defined via the .json file by naming them as such: +"{{document_name}}_codes" + +##### Bit Flag Example +``` +{"b0" : "StandBy", "b1" : "On"} +``` + +##### MultiBit Flag Example +Some protocols utilize combinations of bits as flags. +``` +{"b0" : "StandBy", "b0&b1" : "Operational", "b1" : "Error" } +``` + + +##### uint / bits / other +``` +{"75" : "Error", "50": "Warning"} +```` diff --git a/documentation/usage/transports.md b/documentation/usage/transports.md index 195aa75a..8e516c5c 100644 --- a/documentation/usage/transports.md +++ b/documentation/usage/transports.md @@ -67,7 +67,6 @@ serial_number = bridge = write_enabled = False Interval = 10 -Error_Interval = 60 ``` ### transport Transport is the type or method of reading and writing data @@ -217,3 +216,43 @@ analyze_protocol_save_load = true ``` When enabled, the analyzer will save dump files containing the raw data found while scanning +# CanBus + +``` +transport = canbus +protocol_version = + +port = +bustype = socketcan +buadrate = + +``` + +## Linux / Windows +usb can adapters are a pain with windows, so primary focus is linux. + +## CanBus USB Adapters + +there is a bit of a learning curve with canbus usb adapters. + +the main adapter being used to test python protocol gateway: https://github.com/FYSETC/UCAN +these are dime a dozen on aliexpress / ebay / wherever. most of them will be based on the CANable adapters. + +fysetc sells them directly here: https://www.fysetc.com/products/fysetc-ucan-board +this adapter comes with the candlelight ( socketcan ) by default. the board is CANable v1.0 compatible. slcan firmware can be flashed here: https://canable.io/updater/canable1.html + +candlelight utilizes socketcan, slcan is can over serial. + +here is some extra reading material to help on using a usb canbus adapter: https://canable.io/getting-started.html + +### candlelight / socketcan +This adapter will appear as a gs_usb device, and the can inteface will appear as an ip interface + +``` ip link show ``` + +### slcan +This adapter will appear as a serial device. + +``` +bustype = slcan +``` diff --git a/protocol_gateway.py b/protocol_gateway.py index b992d485..cfa17653 100644 --- a/protocol_gateway.py +++ b/protocol_gateway.py @@ -118,7 +118,10 @@ def __init__(self, config_file : str): ##[general] self.__log_level = self.__settings.get('general','log_level', fallback='INFO') - self.__log.setLevel(logging.getLevelName(self.__log_level)) + + log_level = getattr(logging, self.__log_level, logging.INFO) + self.__log.setLevel(log_level) + logging.basicConfig(level=log_level) for section in self.__settings.sections(): if section.startswith('transport'): @@ -230,8 +233,15 @@ def main(): parser = argparse.ArgumentParser(description='Python Protocol Gateway') # Add arguments - parser.add_argument('--config', '-c', type=str, help='Specify Config File', default='config.cfg') + parser.add_argument('--config', '-c', type=str, help='Specify Config File') + + # Add a positional argument with default + parser.add_argument('positional_config', type=str, help='Specify Config File', nargs='?', default='config.cfg') + # Parse arguments args = parser.parse_args() + # If '--config' is provided, use it; otherwise, fall back to the positional or default. + args.config = args.config if args.config else args.positional_config + main() diff --git a/protocols/eg4/eg4_v58.holding_registry_map.csv b/protocols/eg4/eg4_v58.holding_registry_map.csv index a0e477cd..504f1cd5 100644 --- a/protocols/eg4/eg4_v58.holding_registry_map.csv +++ b/protocols/eg4/eg4_v58.holding_registry_map.csv @@ -4,11 +4,11 @@ variable name,data type,register,documented name,unit,values,writable,note,,,, ,8bit,9.b8,Com Ver,,0-255,W,For Communication CPU software version number,,,, ,8bit,10,Cntl Ver,,0-255,W,For Control CPU software version number,,,, ,8bit,10.b8,FWVer,,0-255,W,For external software version,,,, -,1bit,11,ResetSetting_EnergyRecordClr,Bit0,0-1,W,Resetting energy and running time,,,, -,1bit,11.b1,ResetSetting_AlltoDefault,Bit1,0-1,W,Reset all settings,,,, -,1bit,11.b2,ResetSetting_AdjRatioClr,Bit2,0-1,W,Reset all adjust data,,,, -,1bit,11.b3,ResetSetting_FaultRecordClr,Bit3,0-1,W,Clear the failure record,,,, -,1bit,11.b5,ResetSetting_ InvReboot,Bit5,0-1,W,0-null 1- restart inverter,,,, +,1bit,11,ResetSetting_EnergyRecordClr,,0-1,W,Resetting energy and running time,,,, +,1bit,11.b1,ResetSetting_AlltoDefault,,0-1,W,Reset all settings,,,, +,1bit,11.b2,ResetSetting_AdjRatioClr,,0-1,W,Reset all adjust data,,,, +,1bit,11.b3,ResetSetting_FaultRecordClr,,0-1,W,Clear the failure record,,,, +,1bit,11.b5,ResetSetting_ InvReboot,,0-1,W,0-null 1- restart inverter,,,, ,8bit,12,Time_Year,,17-255,W,inverter time-year,,,, ,8bit,12.b8,Time_Month,,1-12,W,inverter time-month,,,, ,8bit,13,Time_Date,,1-31,W,inverter time-day,,,, @@ -18,22 +18,22 @@ variable name,data type,register,documented name,unit,values,writable,note,,,, ,,15,Com Addr,,0-150,W,MODBUS address,,,, ,,16,Language,,"{""0"":""English"",""1"":""German""}",W,0-English 1-German Language 0-English 1-German,,,, ,,20,PVInputModel,,0-7,W,0: No PV plug in 1: PV1 plug in 2: PV2 plug in 3: two PVs in parallel 4: two separate PVs,,,, -,1bit,21,FuncEn_EPSEn,0,0-1,W,Off-grid mode enable,,,, -,1bit,21.b1,FuncEn_OVFLoadDerateEn,1,0-1,W,Overfrequency load reduction enable,,,, -,1bit,21.b2,FuncEn_DRMSEn,2,0-1,W,DRMS enable,,,, -,1bit,21.b3,FuncEn_LVRTEn,3,0-1,W,Low voltage ride-through enable,,,, -,1bit,21.b4,FuncEn_AntiIslandEn,4,0-1,W,Anti-islanding enablement,,,, -,1bit,21.b5,FuncEn_NeutralDetectEn,5,0-1,W,Ground neutral detection enable,,,, -,1bit,21.b6,FuncEn_GridOnPowerSSEn,6,0-1,W,On-grid power soft start enable,,,, -,1bit,21.b7,FuncEn_ACChargeEn,7,0-1,W,AC charging enable,,,, -,1bit,21.b8,FuncEn_SWSeamlesslyEn,8,0-1,W,seamless off-grid mode switching enable,,,, -,1bit,21.b9,FuncEn_SetToStandby,9,0-1,W,0: Standby 1: Power on,,,, -,1bit,21.b10,FuncEn_ForcedDischgEn,10,0-1,W,Forced discharge enable,,,, -,1bit,21.b11,FuncEn_ForcedChgEn,11,0-1,W,Force charge enable,,,, -,1bit,21.b12,FuncEn_ISOEn,12,0-1,W,ISO enable,,,, -,1bit,21.b13,FuncEn_GFCIEn,13,0-1,W,GFCI enable,,,, -,1bit,21.b14,FuncEn_DCIEn,14,0-1,W,DCI enable,,,, -,1bit,21.b15,FuncEn_FeedInGridEn,15,0-1,W,0-disable 1-enable,,,, +,1bit,21,FuncEn_EPSEn,,0-1,W,Off-grid mode enable,,,, +,1bit,21.b1,FuncEn_OVFLoadDerateEn,,0-1,W,Overfrequency load reduction enable,,,, +,1bit,21.b2,FuncEn_DRMSEn,,0-1,W,DRMS enable,,,, +,1bit,21.b3,FuncEn_LVRTEn,,0-1,W,Low voltage ride-through enable,,,, +,1bit,21.b4,FuncEn_AntiIslandEn,,0-1,W,Anti-islanding enablement,,,, +,1bit,21.b5,FuncEn_NeutralDetectEn,,0-1,W,Ground neutral detection enable,,,, +,1bit,21.b6,FuncEn_GridOnPowerSSEn,,0-1,W,On-grid power soft start enable,,,, +,1bit,21.b7,FuncEn_ACChargeEn,,0-1,W,AC charging enable,,,, +,1bit,21.b8,FuncEn_SWSeamlesslyEn,,0-1,W,seamless off-grid mode switching enable,,,, +,1bit,21.b9,FuncEn_SetToStandby,,0-1,W,0: Standby 1: Power on,,,, +,1bit,21.b10,FuncEn_ForcedDischgEn,,0-1,W,Forced discharge enable,,,, +,1bit,21.b11,FuncEn_ForcedChgEn,,0-1,W,Force charge enable,,,, +,1bit,21.b12,FuncEn_ISOEn,,0-1,W,ISO enable,,,, +,1bit,21.b13,FuncEn_GFCIEn,,0-1,W,GFCI enable,,,, +,1bit,21.b14,FuncEn_DCIEn,,0-1,W,DCI enable,,,, +,1bit,21.b15,FuncEn_FeedInGridEn,,0-1,W,0-disable 1-enable,,,, ,,22,StartPVVolt,0.1V,900-5000,W,PV start-up voltage,,,, ,,23,ConnectTime,s,30-600,W,Waiting time of on-grid,,,, ,,24,ReconnectTime,s,0-900,W,Waiting time of Reconnect on- gird,,,, @@ -166,54 +166,54 @@ variable name,data type,register,documented name,unit,values,writable,note,,,, ,1bit,120.b7,stSysEnable_bit_ GenChargeType,,"{""0"":""According to Battery voltage"",""1"":""According to Battery SOC""}",W,0-According to Battery voltage1-According to Battery SOC,,,, ,,124,OVFDerateEndPoint,0.01Hz,5000-5200,W,Overfrequency load reduction ends at the frequency point,,,, ,,125,SOCLowLimitForEPSDischg,%,0-EOD,W,SOC low limit for EPS discharge,,,, -,2bit,126,OptimalChg_DisChg_Time0,Bit0~1,0~2,W,"0:00~0:30 Mark of time period charging and discharging. Default: 0; 0 - does not operate, 1-AC charge, 2-PV charge, 3 - discharge",,,, -,2bit,126.b2,OptimalChg_DisChg_Time1,Bit2~3,0~2,W,0:30~1:00 Mark of time period charging and discharging.,,,, -,2bit,126.b4,OptimalChg_DisChg_Time2,Bit4~5,0~2,W,1:00~1:30 Mark of time period charging and discharging.,,,, +,2bit,126,OptimalChg_DisChg_Time0,,0~2,W,"0:00~0:30 Mark of time period charging and discharging. Default: 0; 0 - does not operate, 1-AC charge, 2-PV charge, 3 - discharge",,,, +,2bit,126.b2,OptimalChg_DisChg_Time1,,0~2,W,0:30~1:00 Mark of time period charging and discharging.,,,, +,2bit,126.b4,OptimalChg_DisChg_Time2,,0~2,W,1:00~1:30 Mark of time period charging and discharging.,,,, ,2bit,126.b6,OptimalChg_DisChg_Time3,,0~2,W,,,,, ,2bit,126.b8,OptimalChg_DisChg_Time4,,0~2,W,,,,, ,2bit,126.b10,OptimalChg_DisChg_Time5,,0~2,W,,,,, ,2bit,126.b12,OptimalChg_DisChg_Time6,,0~2,W,,,,, -,2bit,126.b14,OptimalChg_DisChg_Time7,Bit14~15,0~2,W,3:30~4:00 Mark of time period charging and discharging.,,,, -,2bit,127,OptimalChg_DisChg_Time8,Bit0~1,0~2,W,"4:00~4:30 Mark of time period charging and discharging. Default: 0; 0 - does not operate, 1-AC charge, 2-PV charge, 3 - discharge",,,, -,2bit,127.b2,OptimalChg_DisChg_Time9,Bit2~3,0~2,W,4:30~5:00 Mark of time period charging and discharging.,,,, -,2bit,127.b4,OptimalChg_DisChg_Time10,Bit4~5,0~2,W,5:00~5:30 Mark of time period charging and discharging.,,,, +,2bit,126.b14,OptimalChg_DisChg_Time7,,0~2,W,3:30~4:00 Mark of time period charging and discharging.,,,, +,2bit,127,OptimalChg_DisChg_Time8,,0~2,W,"4:00~4:30 Mark of time period charging and discharging. Default: 0; 0 - does not operate, 1-AC charge, 2-PV charge, 3 - discharge",,,, +,2bit,127.b2,OptimalChg_DisChg_Time9,,0~2,W,4:30~5:00 Mark of time period charging and discharging.,,,, +,2bit,127.b4,OptimalChg_DisChg_Time10,,0~2,W,5:00~5:30 Mark of time period charging and discharging.,,,, ,2bit,127.b6,OptimalChg_DisChg_Time11,,0~2,W,,,,, ,2bit,127.b8,OptimalChg_DisChg_Time12,,0~2,W,,,,, ,2bit,127.b10,OptimalChg_DisChg_Time13,,0~2,W,,,,, ,2bit,127.b12,OptimalChg_DisChg_Time14,,0~2,W,,,,, -,2bit,127.b14,OptimalChg_DisChg_Time15,Bit14~15,0~2,W,7:30~8:00 Mark of time period charging and discharging.,,,, -,2bit,128,OptimalChg_DisChg_Time16,Bit0~1,0~2,W,"8:00~8:30 Mark of time period charging and discharging. Default: 0; 0 - does not operate, 1-AC charge, 2-PV charge, 3 - discharge",,,, -,2bit,128.b2,OptimalChg_DisChg_Time17,Bit2~3,0~2,W,8:30~9:00 Mark of time period charging and discharging.,,,, -,2bit,128.b4,OptimalChg_DisChg_Time18,Bit4~5,0~2,W,9:00~9:30 Mark of time period charging and discharging.,,,, +,2bit,127.b14,OptimalChg_DisChg_Time15,,0~2,W,7:30~8:00 Mark of time period charging and discharging.,,,, +,2bit,128,OptimalChg_DisChg_Time16,,0~2,W,"8:00~8:30 Mark of time period charging and discharging. Default: 0; 0 - does not operate, 1-AC charge, 2-PV charge, 3 - discharge",,,, +,2bit,128.b2,OptimalChg_DisChg_Time17,,0~2,W,8:30~9:00 Mark of time period charging and discharging.,,,, +,2bit,128.b4,OptimalChg_DisChg_Time18,,0~2,W,9:00~9:30 Mark of time period charging and discharging.,,,, ,2bit,128.b6,OptimalChg_DisChg_Time19,,0~2,W,,,,, ,2bit,128.b8,OptimalChg_DisChg_Time20,,0~2,W,,,,, ,2bit,128.b10,OptimalChg_DisChg_Time21,,0~2,W,,,,, ,2bit,128.b12,OptimalChg_DisChg_Time22,,0~2,W,,,,, -,2bit,128.b14,OptimalChg_DisChg_Time23,Bit14~15,0~2,W,11:30~12:00 Mark of time period charging and discharging.,,,, -,2bit,129,OptimalChg_DisChg_Time24,Bit0~1,0~2,W,"12:00~12:30 Mark of time period charging and discharging. Default: 0; 0 - does not operate, 1-AC charge, 2-PV charge, 3 - discharge;",,,, -,2bit,129.b2,OptimalChg_DisChg_Time25,Bit2~3,0~2,W,12:30~13:00 Mark of time period charging and discharging.,,,, -,2bit,129.b4,OptimalChg_DisChg_Time26,Bit4~5,0~2,W,13:00~13:30 Mark of time period charging and discharging.,,,, +,2bit,128.b14,OptimalChg_DisChg_Time23,,0~2,W,11:30~12:00 Mark of time period charging and discharging.,,,, +,2bit,129,OptimalChg_DisChg_Time24,,0~2,W,"12:00~12:30 Mark of time period charging and discharging. Default: 0; 0 - does not operate, 1-AC charge, 2-PV charge, 3 - discharge;",,,, +,2bit,129.b2,OptimalChg_DisChg_Time25,,0~2,W,12:30~13:00 Mark of time period charging and discharging.,,,, +,2bit,129.b4,OptimalChg_DisChg_Time26,,0~2,W,13:00~13:30 Mark of time period charging and discharging.,,,, ,2bit,129.b6,OptimalChg_DisChg_Time27,,0~2,W,,,,, ,2bit,129.b8,OptimalChg_DisChg_Time28,,0~2,W,,,,, ,2bit,129.b10,OptimalChg_DisChg_Time29,,0~2,W,,,,, ,2bit,129.b12,OptimalChg_DisChg_Time30,,0~2,W,,,,, -,2bit,129.b14,OptimalChg_DisChg_Time31,Bit14~15,0~2,W,17:00~17:30 Mark of time period charging and discharging.,,,, -,2bit,130,OptimalChg_DisChg_Time32,Bit0~1,0~2,W,"16:00~16:30 Mark of time period charging and discharging. 0 - does not operate, 1-AC charge, 2-PV charge, 3 - discharge;",,,, -,2bit,130.b2,OptimalChg_DisChg_Time33,Bit2~3,0~2,W,16:30~17:00 Mark of time period charging and discharging.,,,, +,2bit,129.b14,OptimalChg_DisChg_Time31,,0~2,W,17:00~17:30 Mark of time period charging and discharging.,,,, +,2bit,130,OptimalChg_DisChg_Time32,,0~2,W,"16:00~16:30 Mark of time period charging and discharging. 0 - does not operate, 1-AC charge, 2-PV charge, 3 - discharge;",,,, +,2bit,130.b2,OptimalChg_DisChg_Time33,,0~2,W,16:30~17:00 Mark of time period charging and discharging.,,,, ,2bit,130.b4,OptimalChg_DisChg_Time34,,0~2,W,,,,, ,2bit,130.b6,OptimalChg_DisChg_Time35,,0~2,W,,,,, ,2bit,130.b8,OptimalChg_DisChg_Time36,,0~2,W,,,,, ,2bit,130.b10,OptimalChg_DisChg_Time37,,0~2,W,,,,, ,2bit,130.b12,OptimalChg_DisChg_Time38,,0~2,W,,,,, -,2bit,130.b14,OptimalChg_DisChg_Time39,Bit14~15,0~2,W,19:30~20:00 Mark of time period charging and discharging.,,,, -,2bit,131,OptimalChg_DisChg_Time40,Bit0~1,0~2,W,20:00~20:30 Mark of time period charging and discharging. 0-does not operate,,,, -,2bit,131.b2,OptimalChg_DisChg_Time41,Bit2~3,0~2,W,20:30~21:00 Mark of time period charging and discharging.,,,, -,2bit,131.b4,OptimalChg_DisChg_Time42,Bit4~5,0~2,W,21:00~21:30 Mark of time period charging and discharging.,,,, +,2bit,130.b14,OptimalChg_DisChg_Time39,,0~2,W,19:30~20:00 Mark of time period charging and discharging.,,,, +,2bit,131,OptimalChg_DisChg_Time40,,0~2,W,20:00~20:30 Mark of time period charging and discharging. 0-does not operate,,,, +,2bit,131.b2,OptimalChg_DisChg_Time41,,0~2,W,20:30~21:00 Mark of time period charging and discharging.,,,, +,2bit,131.b4,OptimalChg_DisChg_Time42,,0~2,W,21:00~21:30 Mark of time period charging and discharging.,,,, ,2bit,131.b6,OptimalChg_DisChg_Time43,,0~2,W,,,,, ,2bit,131.b8,OptimalChg_DisChg_Time44,,0~2,W,,,,, ,2bit,131.b10,OptimalChg_DisChg_Time45,,0~2,W,,,,, ,2bit,131.b12,OptimalChg_DisChg_Time46,,0~2,W,,,,, -,2bit,131.b14,OptimalChg_DisChg_Time47,Bit14~15,0~2,W,23:30~0:00 Mark of time period charging and discharging.,,,, +,2bit,131.b14,OptimalChg_DisChg_Time47,,0~2,W,23:30~0:00 Mark of time period charging and discharging.,,,, ,8bit,132,BatCellVoltLow,0.1V,0-200,W,Battery cell voltage lower limit.,,,, ,8bit,132.b8,BatCellVoltHigh,0.1V,0-200,W,Battery cell voltage upper limit,,,, ,8bit,133,BatCellSerialNum,1,0-200,W,Number of battery cells in series,,,, @@ -266,22 +266,22 @@ variable name,data type,register,documented name,unit,values,writable,note,,,, ,,175,SOCCurve_InnerResista nce,m?,0-100,W,,,,, ,,176,MaxGridInputPower,W,0-65535,W,,,,, ,,177,GenRatePower,W,0-65535,W,,,,, -,1bit,179,uFunctionEn2_ACCTDirection,Bit0,0~1,W,1,,,, -,1bit,179.b1,uFunctionEn2_PVCTDirection,Bit1,0~1,W,1,,,, -,1bit,179.b2,uFunctionEn2_AFCIAlarmClr,Bit2,0~1,W,1,,,, -,1bit,179.b3,uFunctionEn2_BatWakeupEn PVSellFirst,Bit3,0~1,W,1,,,, -,1bit,179.b4,uFunctionEn2_VoltWattEn,Bit4,0~1,W,1,,,, -,1bit,179.b5,uFunctionEn2_TriptimeUnit,Bit5,0~1,W,1,,,, -,1bit,179.b6,uFunctionEn2_ActPowerCMDEn,Bit6,0~1,W,1,,,, -,1bit,179.b7,uFunctionEn2_ubGridPeakShaving,Bit7,0~1,W,1,,,, -,1bit,179.b8,uFunctionEn2_ubGenPeakShaving,Bit8,0~1,W,1,,,, -,1bit,179.b9,uFunctionEn2_ubBatChgcontrol,Bit9,0~1,W,1,,,, -,1bit,179.b10,uFunctionEn2_ubBatDischgControl,Bit10,0~1,W,1,,,, -,1bit,179.b11,uFunctionEn2_ubACcou pling,Bit11,0~1,W,1,,,, -,1bit,179.b12,uFunctionEn2_ubPVArcEn,Bit12,0~1,W,1,,,, -,1bit,179.b13,uFunctionEn2_ ubSmartLoadEn,Bit13,0~1,W,1,,,, -,1bit,179.b14,uFunctionEn2_ubRSDDisable,Bit14,0~1,W,1,,,, -,1bit,179.b15,uFunctionEn2_OnGridAlwaysOn,Bit15,0~1,W,1,,,, +,1bit,179,uFunctionEn2_ACCTDirection,,0~1,W,1,,,, +,1bit,179.b1,uFunctionEn2_PVCTDirection,,0~1,W,1,,,, +,1bit,179.b2,uFunctionEn2_AFCIAlarmClr,,0~1,W,1,,,, +,1bit,179.b3,uFunctionEn2_BatWakeupEn PVSellFirst,,0~1,W,1,,,, +,1bit,179.b4,uFunctionEn2_VoltWattEn,,0~1,W,1,,,, +,1bit,179.b5,uFunctionEn2_TriptimeUnit,,0~1,W,1,,,, +,1bit,179.b6,uFunctionEn2_ActPowerCMDEn,,0~1,W,1,,,, +,1bit,179.b7,uFunctionEn2_ubGridPeakShaving,,0~1,W,1,,,, +,1bit,179.b8,uFunctionEn2_ubGenPeakShaving,,0~1,W,1,,,, +,1bit,179.b9,uFunctionEn2_ubBatChgcontrol,,0~1,W,1,,,, +,1bit,179.b10,uFunctionEn2_ubBatDischgControl,,0~1,W,1,,,, +,1bit,179.b11,uFunctionEn2_ubACcou pling,,0~1,W,1,,,, +,1bit,179.b12,uFunctionEn2_ubPVArcEn,,0~1,W,1,,,, +,1bit,179.b13,uFunctionEn2_ ubSmartLoadEn,,0~1,W,1,,,, +,1bit,179.b14,uFunctionEn2_ubRSDDisable,,0~1,W,1,,,, +,1bit,179.b15,uFunctionEn2_OnGridAlwaysOn,,0~1,W,1,,,, ,,180,AFCIArcThreshold,,,W,,,,, ,,181,VoltWatt_V1,0.1V,,W,1.05Vn-1.09Vn,,,, ,,182,VoltWatt_V2,0.1V,,W,(V1+0.01Vn)-1.10Vn,,,, @@ -329,10 +329,10 @@ variable name,data type,register,documented name,unit,values,writable,note,,,, ,,221,ACCoupleEndSOC,%,0-100,W,,,,, ,,222,ACCoupleStartVolt,0.1V,400-520,W,,,,, ,,223,ACCoupleEndVolt,0.1V,400-560,W,,,,, -,8bit,224,LCDVersion,But0~7,0-255,W,,,,, -,2bit,224.b8,LCDScreenType,Bit8~10,"{""0"": ""big screen"", ""1"": ""small screen"", ""2"": ""unknown"", ""3"": ""unknown"", ""4"": ""unknown"", ""5"": ""unknown"", ""6"": ""unknown"", ""7"": ""unknown""}",W,"0~7, 0 - big screen 1 - small screen",,,, -,6bit,224.b10,LCDMachineModelCode,Bit11~15,"{""0"": ""12K"", ""01"": ""All-in-one"", ""02"": ""Three phase inverter""}",W,"0~31, 00:12K 01: All-in-one 02: Three phase inverter",,,, -,2bit,226,Resvd,Bit0~1,,WD,,,,, -,1bit,226.b2,Function3_ExCtEn,Bit2,0~1,W,1,,,, -,1bit,226.b3,Function3_RunwithoutGrid,Bit3,0~1,W,1,,,, -,1bit,226.b4,Function3_NPeRlyEn,Bit4,0~1,W,1,,,, +,8bit,224,LCDVersion,,0-255,W,,,,, +,2bit,224.b8,LCDScreenType,,"{""0"": ""big screen"", ""1"": ""small screen"", ""2"": ""unknown"", ""3"": ""unknown"", ""4"": ""unknown"", ""5"": ""unknown"", ""6"": ""unknown"", ""7"": ""unknown""}",W,"0~7, 0 - big screen 1 - small screen",,,, +,6bit,224.b10,LCDMachineModelCode,,"{""0"": ""12K"", ""01"": ""All-in-one"", ""02"": ""Three phase inverter""}",W,"0~31, 00:12K 01: All-in-one 02: Three phase inverter",,,, +,2bit,226,Resvd,,0,WD,,,,, +,1bit,226.b2,Function3_ExCtEn,,0~1,W,1,,,, +,1bit,226.b3,Function3_RunwithoutGrid,,0~1,W,1,,,, +,1bit,226.b4,Function3_NPeRlyEn,,0~1,W,1,,,, diff --git a/protocols/growatt/growatt_bms_canbus_v1.04.json b/protocols/growatt/growatt_bms_canbus_v1.04.json new file mode 100644 index 00000000..f58e3aa1 --- /dev/null +++ b/protocols/growatt/growatt_bms_canbus_v1.04.json @@ -0,0 +1,4 @@ +{ + "transport" : "canbus", + "baud" : 500000 +} \ No newline at end of file diff --git a/protocols/growatt/growatt_bms_canbus_v1.04.registry_map.csv b/protocols/growatt/growatt_bms_canbus_v1.04.registry_map.csv new file mode 100644 index 00000000..fbad44ea --- /dev/null +++ b/protocols/growatt/growatt_bms_canbus_v1.04.registry_map.csv @@ -0,0 +1,60 @@ +variable name,data type,register,documented name,description,writable,values,unit,note +,USHORT,x311,Battery charge voltage ,,,41~63,0.1V, +,USHORT,X311.2,Charge current limit,,,,0.1A, +,USHORT,X311.4,Discharge current limit,,,,0.1A, +,2BIT,X311.6,Status,todo bit flags; they are a bit weird,,"{""b0"": ""charging"", ""b1"" : ""stand by"", ""b0&b1"" : ""discharging""}",, +,,,,,,,, +,,,,,,,, +,,,,,,,, +,,,,,,,, +,,,,,,,, +,16BIT_FLAGS,x312,Protection Flags,,,"{""b15"": ""OTD (Over Temperature Discharge) protection"", ""b14"": ""OTC (Over Temperature Charge) protection"", ""b13"": ""UTD (Under Temperature Discharge) protection"", ""b12"": ""UTC (Under Temperature Charge) protection"", ""b11"": ""System error"", ""b10"": ""Delta V Fail"", ""b7"": ""DisCharge over current"", ""b6"": ""Charge over current"", ""b5"": ""SCD (Short Circuit Discharge) protection"", ""b4"": ""Cell over voltage"", ""b3"": ""Cell under voltage"", ""b2"": ""Module over voltage"", ""b1"": ""Module under voltage"", ""b0"": ""Soft start fail""}",, +,16BIT_FLAGS,X312.2,Alarm Flags,,,"{""b7"": ""DisCharge over current"", ""b6"": ""Charge over current"", ""b5"": ""Cell over voltage"", ""b4"": ""Cell under voltage"", ""b3"": ""Module over voltage"", ""b2"": ""Module under voltage"", ""b1"": """", ""b0"": """", ""b15"": ""OTD (Over Temperature Discharge) protection"", ""b14"": ""OTC (Over Temperature Charge) protection"", ""b13"": ""UTD (Under Temperature Discharge) protection"", ""b12"": ""UTC (Under Temperature Charge) protection"", ""b11"": ""Delta V Fail"", ""b10"": ""Pack before turn off"", ""b9"": ""Internal communication fail"", ""b8"": """"}",, +,,,,,,,, +,SHORT,x313,Average module voltage of system ,,,,0.01V, +,SHORT,X313.2,Total current of system,,,,0.1A, +,SHORT,X313.4,Maximum cell temperature,,,,0.1A, +,BYTE,X313.6,Average State of Charge of System,,,,%, +,8BIT_FLAGS,X313.7,State of Health Flags,,,,, +,,,,,,,, +Remaining Capacity,USHORT,x314,Guage RM,,,,0.01AH, +Full Charge Capacity,USHORT,314.2,Gauge FCC,,,,0.01AH, +Cell Voltage Difference,USHORT,314.4,Delta V,,,,1mV, +,,314.6,Cycle Count,,,,, +,,,,,,,, +,8BIT_FLAGS,x319,Request And Battery Type,,,"{""b7"": ""Charge enable"", ""b6"": ""Discharge enable"", ""b5"": ""Request force charge I* (Strong charge mark 1)"", ""b4"": ""Request force charge II* (Strong charge mark 2)"", ""b3"": ""00: Lithium Iron Phosphate (LiFePO4) battery"", ""b2"": ""01: Nickel Cobalt Manganese (NCM) battery"", ""b1"": ""10: Lithium Titanate (LTO) battery"", ""b0"": ""11: Reserved""}",, +,USHORT,X319.1,Maximum cell voltage ,,,,1mV, +,USHORT,X319.3,Minimum cell voltage ,,,,1mV, +,BYTE,X319.5,Maximum cell voltage number ,,,,, +,BYTE,X319.6,Minimum cell voltage number ,,,,, +,BYTE,X319.7,Protect pack ID ,,,,, +,,,,,,,, +,ASCII.2,x320,Manufacturer Name ,,,,, +,ASCII.1,X320.2,Hardware version ,,,,, +,ASCII.1,X320.3,Software version,,,,, +,,X320.4,Date and Time 1,"todo bit flags, another weird setup",,,, +,,X320.5,Date and Time 2,,,,, +,,X320.6,Date and Time 3,,,,, +,,X320.7,Date and Time 4,,,,, +,,,,,,,, +,8BIT_FLAGS,x321,Update status ,,,,, +,BYTE,X321.1,Update schedule of single pack ,,,0~100,, +,BYTE,X321.2,programming ID of pack ,,,,, +,BYTE,X321.3,Update Successful count ,,,,, +,,,,,,,, +,USHORT,x315,Cell Voltage 1,,,,1mV, +,USHORT,X315.2,Cell Voltage 2,,,,1mV, +,USHORT,X315.4,Cell Voltage 3,,,,1mV, +,USHORT,X315.6,Cell Voltage 4,,,,1mV, +,USHORT,x316,Cell Voltage 5,,,,1mV, +,USHORT,x316.2,Cell Voltage 6,,,,1mV, +,USHORT,x316.4,Cell Voltage 7,,,,1mV, +,USHORT,x316.6,Cell Voltage 8,,,,1mV, +,USHORT,x317,Cell Voltage 9,,,,1mV, +,USHORT,x317.2,Cell Voltage 10,,,,1mV, +,USHORT,x317.4,Cell Voltage 11,,,,1mV, +,USHORT,x317.6,Cell Voltage 12,,,,1mV, +,USHORT,x318,Cell Voltage 13,,,,1mV, +,USHORT,x318.2,Cell Voltage 14,,,,1mV, +,USHORT,x318.4,Cell Voltage 15,,,,1mV, +,USHORT,x318.6,Cell Voltage 16,,,,1mV, diff --git a/protocols/sma/sma_sunny_island_v1.json b/protocols/sma/sma_sunny_island_v1.json new file mode 100644 index 00000000..d4db3cdb --- /dev/null +++ b/protocols/sma/sma_sunny_island_v1.json @@ -0,0 +1,5 @@ +{ + "transport" : "canbus", + "baud" : 500000, + "byteorder" : "little" +} \ No newline at end of file diff --git a/protocols/sma/sma_sunny_island_v1.registry_map.csv b/protocols/sma/sma_sunny_island_v1.registry_map.csv new file mode 100644 index 00000000..e4d0578b --- /dev/null +++ b/protocols/sma/sma_sunny_island_v1.registry_map.csv @@ -0,0 +1,32 @@ +variable name,data type,register,documented name,description,writable,values,unit,note +,USHORT,x351,Battery charge voltage ,Set point for battery charge voltage ,,41~63,0.1V, +,SHORT,x351.2,DC charge current limitation ,DC charge current limitation ,,0~1200,0.1A, +,SHORT,x351.4,DC discharge current limitation ,DC discharge current limitation ,,0~1200,0.1A, +,USHORT,x351.6,Battery discharge voltage ,Voltage discharge limit ,,41~48,0.1V, +,,,,,,,, +,USHORT,x355,State of Charge,State of Charge (SOC) value from an external BMS ,,0~100,%, +,USHORT,x355.2,State of Health,State of Health (SOH) value from external Battery Management ,,0~100,%, +,USHORT,x355.4,High Res State of Charge,High resolution SOC value: It allows more sophisticated protection of the battery ,,0~100,0.01%, +,,,,,,,, +,SHORT,x356,Battery Voltage,Measured actual Battery Voltage value from external BMS ,,,0.01V, +,SHORT,x356.2,Battery Current,Measured actual Battery Current value from external BMS ,,,0.1A, +,SHORT,X356.4,Battery Temperature,Measured actual Battery Temperature value from external BMS ,,,0.1C, +,,,,,,,, +,8BIT_FLAGS ,x35A,External Alarm 1 ,,,"{""b0"": ""General arrive"", ""b1"": ""General leave"", ""b2"": ""Battery High Voltage arrive"", ""b3"": ""Battery High Voltage leave"", ""b4"": ""Battery Low Voltage arrive"", ""b5"": ""Battery Low Voltage leave"", ""b6"": ""Battery High Temp arrive"", ""b7"": ""Battery High Temp leave""}",, +,8BIT_FLAGS ,X35A.1,External Alarm 2,,,"{""b0"": ""Battery High Current Charge arrive"", ""b1"": ""Battery High Current Charge leave"", ""b2"": ""Contactor arrive"", ""b3"": ""Contactor leave "", ""b4"": ""Short circuit arrive"", ""b5"": ""Short circuit leave"", ""b6"": ""BMS internal arrive"", ""b7"": ""BMS internal leave""}",, +,8BIT_FLAGS ,X35A.2,External Alarm 3,,,"{""b0"": ""Cell Imbalance arrive"", ""b1"": ""Cell Imbalance leave"", ""b2"": ""Arrives Reserved"", ""b3"": ""Leaves Reserved"", ""b4"": ""Arrives Reserved"", ""b5"": ""Leaves Reserved"", ""b6"": ""Arrives Reserved"", ""b7"": ""Leaves Reserved""}",, +,8BIT_FLAGS ,X35A.3,External Warning 1 ,,,"{""b0"": ""General arrive"", ""b1"": ""General leave"", ""b2"": ""Battery High Voltage arrive"", ""b3"": ""Battery High Voltage leave"", ""b4"": ""Battery Low Voltage arrive"", ""b5"": ""Battery Low Voltage leave"", ""b6"": ""Battery High Temp arrive"", ""b7"": ""Battery High Temp leave""}",, +,8BIT_FLAGS ,X35A.4,External Warning 2,,,"{""b0"": ""Battery Low Temp arrive"", ""b1"": ""Battery Low Temp leaves"", ""b2"": ""Battery High Temp Charge arrive"", ""b3"": ""Battery High Temp Charge leave"", ""b4"": ""Battery Low Temp Charge arrive"", ""b5"": ""Battery Low Temp Charge leave"", ""b6"": ""Battery High Current arrive"", ""b7"": ""Battery High Current leaves""}",, +,8BIT_FLAGS ,X35A.5,External Warning 3,,,"{""b0"": ""Battery High Current Charge arrive"", ""b1"": ""Battery High Current Charge leave"", ""b2"": ""Contactor arrive"", ""b3"": ""Contactor leave"", ""b4"": ""Short circuit arrive"", ""b5"": ""Short circuit leaves"", ""b6"": ""BMS internal arrive"", ""b7"": ""BMS internal leave""}",, +,8BIT_FLAGS ,X35A.6,External Warning 4,,,"{""b0"": ""Cell Imbalance arrive"", ""b1"": ""Cell Imbalance leave"", ""b2"": ""Reserved arrive"", ""b3"": ""Reserved leave"", ""b4"": ""Reserved arrive"", ""b5"": ""Reserved leave"", ""b6"": ""Reserved arrive"", ""b7"": ""Reserved leave""}",, +,,,,,,,, +,USHORT,x305,Battery Voltage Inverter,Battery voltage measured by Sunny Island ,,,0.1V, +,SHORT,X305.2,Battery Current Inverter,"Battery current measured by Sunny Island, (negative while charging) ",,,0.1A, +,SHORT,X305.4,Battery Temperature Inverter,Battery temperature measured by Battery/ Sunny Island ,,,0.1C, +,SHORT,X305.6,SOC Battery,State of charge of the battery received from the external BMS ,,,0.10%, +,USHORT,x306,SOH Battery,State of health of the battery received from the external BMS ,,,1.00%, +,BYTE,X306.2,Charging procedure ,Charging mode of the SunnyIsland internal Battery Man-agement. If external BMS is selected displayed value is 10 ,,,, +,BYTE,X306.3,Operating state,"Operating state of the inverter --- (0),Operating (1), Warning (3), Failure (4) Valid only for SI6.0H-11, FW Release 2.1 ",,"{""0"" : """", ""1"" : ""Operating"", ""3"" : ""Warning"", ""4"" : ""Failure""}",, +,USHORT,X306.4,Error Messages,Number of the error message ,,,, +,USHORT,X306.6,Battery charging voltage,Current set point of charging voltage ,,,0.1V, +,16BIT_FLAGS,x307,Relay state,state of the relay bitcoded ,,,, diff --git a/protocols/srne_2021_v1.96.holding_registry_map.csv b/protocols/srne/srne_2021_v1.96.holding_registry_map.csv similarity index 100% rename from protocols/srne_2021_v1.96.holding_registry_map.csv rename to protocols/srne/srne_2021_v1.96.holding_registry_map.csv diff --git a/protocols/srne_2021_v1.96.json b/protocols/srne/srne_2021_v1.96.json similarity index 100% rename from protocols/srne_2021_v1.96.json rename to protocols/srne/srne_2021_v1.96.json diff --git a/protocols/srne_v3.9.holding_registry_map.csv b/protocols/srne/srne_v3.9.holding_registry_map.csv similarity index 100% rename from protocols/srne_v3.9.holding_registry_map.csv rename to protocols/srne/srne_v3.9.holding_registry_map.csv diff --git a/protocols/srne_v3.9.json b/protocols/srne/srne_v3.9.json similarity index 100% rename from protocols/srne_v3.9.json rename to protocols/srne/srne_v3.9.json diff --git a/protocols/victron/victron_gx_generic_canbus.json b/protocols/victron/victron_gx_generic_canbus.json new file mode 100644 index 00000000..1df968c7 --- /dev/null +++ b/protocols/victron/victron_gx_generic_canbus.json @@ -0,0 +1,7 @@ +{ + "transport" : "canbus", + "baud" : 500000, + "byteorder" : "little", + "note" : "this appears to be a common 'generic' victron canbus implementation used by various batteries, built upon the sma sunny island protocol. some documentation leads me to believe this is for victron gx devices" + +} \ No newline at end of file diff --git a/protocols/victron/victron_gx_generic_canbus.registry_map.csv b/protocols/victron/victron_gx_generic_canbus.registry_map.csv new file mode 100644 index 00000000..96526168 --- /dev/null +++ b/protocols/victron/victron_gx_generic_canbus.registry_map.csv @@ -0,0 +1,64 @@ +variable name,data type,register,documented name,description,writable,values,unit,note +,USHORT,x351,Battery charge voltage ,Set point for battery charge voltage ,,41~63,0.1V, +,SHORT,x351.2,DC charge current limitation ,DC charge current limitation ,,0~1200,0.1A, +,SHORT,x351.4,DC discharge current limitation ,DC discharge current limitation ,,0~1200,0.1A, +,USHORT,x351.6,Battery discharge voltage ,Voltage discharge limit ,,41~48,0.1V, +,,,,,,,, +,USHORT,x355,State of Charge,State of Charge (SOC) value from an external BMS ,,0~100,%, +,USHORT,x355.2,State of Health,State of Health (SOH) value from external Battery Management ,,0~100,%, +,USHORT,x355.4,High Res State of Charge,High resolution SOC value: It allows more sophisticated protection of the battery ,,0~100,0.01%, +,,,,,,,, +,SHORT,x356,Battery Voltage,Measured actual Battery Voltage value from external BMS ,,,0.01V, +,SHORT,x356.2,Battery Current,Measured actual Battery Current value from external BMS ,,,0.1A, +,SHORT,X356.4,Battery Temperature,Measured actual Battery Temperature value from external BMS ,,,0.1C, +,,,,,,,, +,8BIT_FLAGS ,x35A,External Alarm 1 ,,,"{""b0"": ""General arrive"", ""b1"": ""General leave"", ""b2"": ""Battery High Voltage arrive"", ""b3"": ""Battery High Voltage leave"", ""b4"": ""Battery Low Voltage arrive"", ""b5"": ""Battery Low Voltage leave"", ""b6"": ""Battery High Temp arrive"", ""b7"": ""Battery High Temp leave""}",, +,8BIT_FLAGS ,X35A.1,External Alarm 2,,,"{""b0"": ""Battery High Current Charge arrive"", ""b1"": ""Battery High Current Charge leave"", ""b2"": ""Contactor arrive"", ""b3"": ""Contactor leave "", ""b4"": ""Short circuit arrive"", ""b5"": ""Short circuit leave"", ""b6"": ""BMS internal arrive"", ""b7"": ""BMS internal leave""}",, +,8BIT_FLAGS ,X35A.2,External Alarm 3,,,"{""b0"": ""Cell Imbalance arrive"", ""b1"": ""Cell Imbalance leave"", ""b2"": ""Arrives Reserved"", ""b3"": ""Leaves Reserved"", ""b4"": ""Arrives Reserved"", ""b5"": ""Leaves Reserved"", ""b6"": ""Arrives Reserved"", ""b7"": ""Leaves Reserved""}",, +,8BIT_FLAGS ,X35A.3,External Warning 1 ,,,"{""b0"": ""General arrive"", ""b1"": ""General leave"", ""b2"": ""Battery High Voltage arrive"", ""b3"": ""Battery High Voltage leave"", ""b4"": ""Battery Low Voltage arrive"", ""b5"": ""Battery Low Voltage leave"", ""b6"": ""Battery High Temp arrive"", ""b7"": ""Battery High Temp leave""}",, +,8BIT_FLAGS ,X35A.4,External Warning 2,,,"{""b0"": ""Battery Low Temp arrive"", ""b1"": ""Battery Low Temp leaves"", ""b2"": ""Battery High Temp Charge arrive"", ""b3"": ""Battery High Temp Charge leave"", ""b4"": ""Battery Low Temp Charge arrive"", ""b5"": ""Battery Low Temp Charge leave"", ""b6"": ""Battery High Current arrive"", ""b7"": ""Battery High Current leaves""}",, +,8BIT_FLAGS ,X35A.5,External Warning 3,,,"{""b0"": ""Battery High Current Charge arrive"", ""b1"": ""Battery High Current Charge leave"", ""b2"": ""Contactor arrive"", ""b3"": ""Contactor leave"", ""b4"": ""Short circuit arrive"", ""b5"": ""Short circuit leaves"", ""b6"": ""BMS internal arrive"", ""b7"": ""BMS internal leave""}",, +,8BIT_FLAGS ,X35A.6,External Warning 4,,,"{""b0"": ""Cell Imbalance arrive"", ""b1"": ""Cell Imbalance leave"", ""b2"": ""Reserved arrive"", ""b3"": ""Reserved leave"", ""b4"": ""Reserved arrive"", ""b5"": ""Reserved leave"", ""b6"": ""Reserved arrive"", ""b7"": ""Reserved leave""}",, +,,,,,,,, +,USHORT,x305,Battery Voltage Inverter,Battery voltage measured by Sunny Island ,,,0.1V, +,SHORT,X305.2,Battery Current Inverter,"Battery current measured by Sunny Island, (negative while charging) ",,,0.1A, +,SHORT,X305.4,Battery Temperature Inverter,Battery temperature measured by Battery/ Sunny Island ,,,0.1C, +,SHORT,X305.6,SOC Battery,State of charge of the battery received from the external BMS ,,,0.10%, +,USHORT,x306,SOH Battery,State of health of the battery received from the external BMS ,,,1.00%, +,BYTE,X306.2,Charging procedure ,Charging mode of the SunnyIsland internal Battery Man-agement. If external BMS is selected displayed value is 10 ,,,, +,BYTE,X306.3,Operating state,"Operating state of the inverter --- (0),Operating (1), Warning (3), Failure (4) Valid only for SI6.0H-11, FW Release 2.1 ",,"{""0"" : """", ""1"" : ""Operating"", ""3"" : ""Warning"", ""4"" : ""Failure""}",, +,USHORT,X306.4,Error Messages,Number of the error message ,,,, +,USHORT,X306.6,Battery charging voltage,Current set point of charging voltage ,,,0.1V, +,16BIT_FLAGS,x307,Relay state,state of the relay bitcoded ,,,, +,,,,,,,, +,ASCII,x370,device name part 1,,,,, +,ASCII,x371,device name part 2,,,,, +,,,,,,,, +,USHORT,x372,Number of modules ok,,,,, +,USHORT,X372.2,Number of modules blocking charge,,,,, +,USHORT,X372.4,Number of modules blocking discharge,,,,, +,USHORT,X372.6,Number of modules offline,,,,, +,,,,,,,, +,USHORT,x373,min cell voltage,,,,mv, +,USHORT,X373.2,max cell voltage,,,,mv, +,USHORT,X373.4,lowest cell temperature,,,,0.1C, +,USHORT,X373.6,highest cell temperature,,,,0.1C, +,,,,,,,, +,ASCII,x374,battery with lowest cell voltage,,,,, +,ASCII,x375,battery with highest cell voltage,,,,, +,ASCII,x376,battery with lowest cell temperature,,,,, +,ASCII,x377,battery with lowest cell highest,,,,, +,,,,,,,, +,UINT,x378,Historic Energy Charged,,,,KWH, +,UINT,X378.4,Historic Energy Discharged,,,,KWH, +,,,,,,,, +,USHORT,x379,installed battery capacity,,,,AH, +,,,,,,,, +,ASCII,x380,serial number part 1,,,,, +,ASCII,x381,serial number part 2,,,,, +,,,,,,,, +,ASCII,x35E,device name,,,,, +,,,,,,,, +,USHORT,x35F,Battery Model,,,,, +,USHORT,X35F.2,Firmware Version,,,,, +,USHORT,X35F.4,available battery capacity,,,,AH, diff --git a/protocols/voltronic/voltronic_bms_2020_03_25.holding_registry_map.csv b/protocols/voltronic/voltronic_bms_2020_03_25.holding_registry_map.csv new file mode 100644 index 00000000..e37c41aa --- /dev/null +++ b/protocols/voltronic/voltronic_bms_2020_03_25.holding_registry_map.csv @@ -0,0 +1,91 @@ +variable name,data type,register,documented_name,description,writable,values,unit,note +,,1,Protocol_Type,,R,,,Positive: charging Negative: discharging +,,2,Protocol_Version,,R,,, +,ASCII,3~4,BMS_Firmware_Version,,,,, +,ASCII,4~5,BMS_Hardware_Version,,,,, +,,,,,,,, +,,x10,Number_of_Cells,,,,, +,,x11,Cell_Voltage_1,,,,0.1V, +,,x12,Cell_Voltage_2,,,,0.1V, +,,x13,Cell_Voltage_3,,,,0.1V, +,,x14,Cell_Voltage_4,,,,0.1V, +,,x15,Cell_Voltage_5,,,,0.1V, +,,x16,Cell_Voltage_6,,,,0.1V, +,,x17,Cell_Voltage_7,,,,0.1V, +,,x18,Cell_Voltage_8,,,,0.1V, +,,x19,Cell_Voltage_9,,,,0.1V, +,,x1A,Cell_Voltage_10,,,,0.1V, +,,x1B,Cell_Voltage_11,,,,0.1V, +,,x1C,Cell_Voltage_12,,,,0.1V, +,,x1D,Cell_Voltage_13,,,,0.1V, +,,x1E,Cell_Voltage_14,,,,0.1V, +,,x1F,Cell_Voltage_15,,,,0.1V, +,,x20,Cell_Voltage_16,,,,0.1V, +,,x21,Cell_Voltage_17,,,,0.1V, +,,x22,Cell_Voltage_18,,,,0.1V, +,,x23,Cell_Voltage_19,,,,0.1V, +,,x24,Cell_Voltage_20,,,,0.1V, +,,,,,,,, +,,x25,Number_of_Temperature_Sensors,,,,, +,,x26,Temperature_Sensor_1,,,,0.1K, +,,x27,Temperature_Sensor_2,,,,0.1K, +,,x28,Temperature_Sensor_3,,,,0.1K, +,,x29,Temperature_Sensor_4,,,,0.1K, +,,x2A,Temperature_Sensor_5,,,,0.1K, +,,x2B,Temperature_Sensor_6,,,,0.1K, +,,x2C,Temperature_Sensor_7,,,,0.1K, +,,x2D,Temperature_Sensor_8,,,,0.1K, +,,x2E,Temperature_Sensor_9,,,,0.1K, +,,x2F,Temperature_Sensor_10,,,,0.1K, +,,,,,,,, +,,x30,Module_charge_current,,,,0.1A, +,,x31,Module_discharge_current,,,,0.1A, +,,x32,Module_voltage,,,,0.1V, +,,x33,SOC,,,,%, +,,x34,Module_total_capacity,,,,mAH, +,,,,,,,, +,,x36,Pack_parallel_number,,,,, +,16BIT_FLAGS,x37,Charge_Alarm,,,"{""b15"":""Reserved"",""b14"":""Reserved"",""b13"":""Reserved"",""b12"":""Reserved"",""b11"":""Reserved"",""b10"":""Reserved"",""b9"":""Reserved"",""b8"":""Reserved"",""b7"":""Reserved"",""b6"":""Reserved"",""b5"":""Reserved"",""b4"":""Reserved"",""b3"":""Charge over current alarm"",""b2"":""Charge low temperature alarm"",""b1"":""Cell over voltage alarm"",""b0"":""Charge over temperature alarm""}",, +,16BIT_FLAGS,x38,Discharge_Alarm,,,"{""b15"":""Reserved"",""b14"":""Reserved"",""b13"":""Reserved"",""b12"":""Reserved"",""b11"":""Reserved"",""b10"":""Reserved"",""b9"":""Reserved"",""b8"":""Reserved"",""b7"":""Reserved"",""b6"":""Reserved"",""b5"":""Reserved"",""b4"":""Reserved"",""b3"":""Cell voltage low alarm"",""b2"":""Mosfet over temperature alarm"",""b1"":""Discharge low temperature alarm"",""b0"":""Discharge over temperature alarm""}",, +,16BIT_FLAGS,x39,Charge_Protect,,,"{""b15"":""OCC2-2nd level charge over current"",""b14"":""Reserved-"",""b13"":""CANID-CAN ID distribution not complete"",""b12"":""2NDOVP-2nd level cell over voltage"",""b11"":""OCC-Charge over current"",""b10"":""OCV-Cell over voltage"",""b9"":""CLT-Charge low temperature"",""b8"":""CHT-Charge over temperature"",""b7"":""SUV-Safety under voltage"",""b6"":""FETHT-Mosfet over temperature"",""b5"":""AFESCD-AFE detect discharge short circuit"",""b4"":""AFEOCD-AFE detect discharge over current"",""b3"":""AFEOCC-AFE detect charge over current"",""b2"":""AFEComm-AFE communication fail"",""b1"":""BoostNRDY-Mosfet driver status"",""b0"":""PRES-'In System' signal""}",, +,16BIT_FLAGS,x3A,Charge_Protect_2,,,"{""b15"":""Reserved-"",""b14"":""Reserved-"",""b13"":""Reserved-"",""b12"":""Reserved-"",""b11"":""Reserved-"",""b10"":""Reserved-"",""b9"":""Reserved-"",""b8"":""Reserved-"",""b7"":""Reserved-"",""b6"":""Reserved-"",""b5"":""Reserved-"",""b4"":""Reserved-"",""b3"":""IDError-CAN ID error"",""b2"":""OCCHw-Detect charge short circuit"",""b1"":""Shutdown-Low voltage shutdown"",""b0"":""ShutdownByCmd-Receive shutdown command""}",, +,16BIT_FLAGS,x3B,Discharge_Protect,,,"{""b15"":""Shutdown-Low voltage shutdown"",""b14"":""DHT-Discharge over temperature"",""b13"":""DLT-Discharge low temperature"",""b12"":""OCD-Discharge over current"",""b11"":""CUV-Cell under voltage"",""b10"":""FETHT-Mosfet over temperature"",""b9"":""Reserved-"",""b8"":""Reserved-"",""b7"":""Reserved-"",""b6"":""IDError-CAN ID error"",""b5"":""Reserved-"",""b4"":""AFESCD-AFE detect discharge short circuit"",""b3"":""AFEOCD-AFE detect discharge over current"",""b2"":""AFEOCC-AFE detect charge over current"",""b1"":""BoostNRDY-Mosfet driver status"",""b0"":""PRES-'In System' signal""}",, +,16BIT_FLAGS,x3C,Discharge_Protect_2,,,"{""b15"":""Reserved-"",""b14"":""Reserved-"",""b13"":""CANID-CAN ID distribution not complete"",""b12"":""Reserved-"",""b11"":""Reserved-"",""b10"":""Reserved-"",""b9"":""Reserved-"",""b8"":""Reserved-"",""b7"":""Reserved-"",""b6"":""ShutdownByCmd-Receive shutdown command"",""b5"":""DHT2-2nd level discharge over temperature"",""b4"":""2NDOVP-2nd level cell over voltage"",""b3"":""Reserved-"",""b2"":""OCD2-2nd level discharge over current"",""b1"":""Short-Detect discharge short circuit"",""b0"":""AFEComm-AFE communication fail""}",, +,,x3D,BMS_State,,,,, +,,x3E,Design_capacity,,,,mAH, +,,,,,,,, +,,x40,Number_of_cells_M,,,,, +,,x41,Cell_1_2_voltage_state,,,{{cell_voltage_state_codes}},,"not sure if these registers should be split in BYTES, to get codes for individual cells" +,,x42,Cell_3_4_voltage_state,,,{{cell_voltage_state_codes}},, +,,x43,Cell_5_6_voltage_state,,,{{cell_voltage_state_codes}},, +,,x44,Cell_7_8_voltage_state,,,{{cell_voltage_state_codes}},, +,,x45,Cell_9_10_voltage_state,,,{{cell_voltage_state_codes}},, +,,x46,Cell_11_12_voltage_state,,,{{cell_voltage_state_codes}},, +,,x47,Cell_13_14_voltage_state,,,{{cell_voltage_state_codes}},, +,,x48,Cell_15_16_voltage_state,,,{{cell_voltage_state_codes}},, +,,x49,Cell_17_18_voltage_state,,,{{cell_voltage_state_codes}},, +,,x4A,Cell_19_20_voltage_state,,,{{cell_voltage_state_codes}},, +,,x50,Number_of_temperature_sensor_N,,,,, +,,x51,BMS_Temperature1_2_state,,,{{bms_temperature_state_codes}},, +,,x52,BMS_Temperature3_4_state,,,{{bms_temperature_state_codes}},, +,,x53,BMS_Temperature5_6_state,,,{{bms_temperature_state_codes}},, +,,x54,BMS_Temperature7_8_state,,,{{bms_temperature_state_codes}},, +,,x55,BMS_Temperature9_10_state,,,{{bms_temperature_state_codes}},, +,,x60,Module_charge_voltage_state,,,,, +,,x61,Module_discharge_voltage_state,,,,, +,,x62,Cell_charge_voltage_state,,,,, +,,x63,Cell_discharge_voltage_state,,,,, +,,x64,Module_charge_current_state,,,,, +,,x65,Module_discharge_current_state,,,,, +,,x66,Module_charge_temperature_state,,,,, +,,x67,Module_discharge_temperature_state,,,,, +,,x68,Cell_charge_temperature_state,,,,, +,,x69,Cell_discharge_temperature_state,,,,, +,,,,,,,, +,,x70,Charge_voltage_limit,,,,0.1V, +,,x71,Discharge_voltage_limit,,,,0.1V, +,,x72,Charge_current_limit,,,,0.1A, +,,x73,Discharge_current_limit,,,,0.1A, +,16BIT_FLAGS,x74,Charge_discharge_status,,,"{""b7"":""Charge enable-"",""b6"":""Discharge enable-"",""b5"":""Charge immediately-"",""b4"":""Charge immediately2-"",""b3"":""Full charge request-"",""b2"":""Small current charge request-"",""b1"":""-"",""b0"":""-""}",, +,,x75,Run_Time_To_Empty,,,,min, +,UINT,x76,Module_remain_capacity,,,,mAh, diff --git a/protocols/voltronic/voltronic_bms_2020_03_25.json b/protocols/voltronic/voltronic_bms_2020_03_25.json new file mode 100644 index 00000000..3fd749d9 --- /dev/null +++ b/protocols/voltronic/voltronic_bms_2020_03_25.json @@ -0,0 +1,22 @@ +{ + "transport" : "modbus_rtu", + "baud" : "9600", + "send_holding_register" : "true", + "send_input_register" : "false", + "batch_size" : 21, + "batch_delay" : 1.4, + "cell_voltage_state_codes" : + { + "0": "normal", + "1": "below lower limit", + "2": "above higher limit", + "240": "other error" + }, + "bms_temperature_state_codes" : + { + "0": "normal", + "1": "below lower limit", + "2": "above higher limit", + "240": "other error" + } +} \ No newline at end of file diff --git a/protocols/voltronic/voltronic_bms_v1.1.holding_registry_map.csv b/protocols/voltronic/voltronic_bms_v1.1.holding_registry_map.csv new file mode 100644 index 00000000..910bf324 --- /dev/null +++ b/protocols/voltronic/voltronic_bms_v1.1.holding_registry_map.csv @@ -0,0 +1,162 @@ +variable name,data type,register,documented_name,description,writable,values,unit,note +,,0,Current,,R,,0.01A,Positive: charging Negative: discharging +,,1,Voltage_of_pack,,R,,0.01V, +,BYTE,2,SOC,,R,0~100,,0~100 +,BYTE,3,SOH,,R,0~100,,0~100 +,,4,Remain_capacity,,R,,0.01AH, +,,5,Full_capacity,,R,,0.01AH, +,,6,Design_capacity,,R,,0.01AH, +,,7,Battery_cycle_counts,,R,,Cyc., +,16BIT_FLAGS,9,Warning_flag,,R,"{""b0"": ""battery cell overvoltage alarm"", ""b1"": ""battery cell low voltage alarm"", ""b2"": ""battery pack overvoltage alarm"", ""b3"": ""battery pack low voltage alarm"", ""b4"": ""charging over current alarm"", ""b5"": ""discharging over current alarm"", ""b8"": ""charging high temperature alarm"", ""b9"": ""discharging high temperature alarm"", ""b10"": ""charging low temperature alarm"", ""b11"": ""discharging low temperature alarm"", ""b12"": ""environment high temperature alarm"", ""b13"": ""environment low temperature alarm"", ""b14"": ""MOSFET high temperature alarm"", ""b15"": ""SOC Low alarm""}",,See description-1 +,16BIT_FLAGS,10,Protection_flag,,R,"{""b0"": ""battery cell over voltage protection"", ""b1"": ""battery cell low voltage protection"", ""b2"": ""battery pack over voltage protection"", ""b3"": ""battery pack low voltage protection"", ""b4"": ""charging over current protection"", ""b5"": ""discharging over current protection"", ""b6"": ""short circuit protection"", ""b7"": ""charger overvoltage protection"", ""b8"": ""charging high temperature protection"", ""b9"": ""discharging high temperature protection"", ""b10"": ""charging low temperature protection"", ""b11"": ""discharging low temperature protection"", ""b12"": ""MOSFET high temperature protection"", ""b13"": ""environment high temperature protection"", ""b14"": ""environment low temperature protection"", ""b15"": ""reserve""}",,See description-2 +,16BIT_FLAGS,11,Status_Fault_flag,,R,"{""b0"": ""charging MOSFET fault"", ""b1"": ""discharging MOSFET fault"", ""b2"": ""temperature sensor fault"", ""b3"": ""reserve"", ""b4"": ""battery cell fault"", ""b5"": ""front end sampling communication fault"", ""b6"": ""reserve"", ""b7"": ""reserve"", ""b8"": ""state of charge"", ""b9"": ""state of discharge"", ""b10"": ""charging MOSFET is ON"", ""b11"": ""discharging MOSFET is ON"", ""b12"": ""charging Limiter is ON"", ""b13"": ""reserve"", ""b14"": ""charger inversed"", ""b15"": ""heater is ON""}",,See description-3 +,,12,Balance_status,,R,,, +,,15,Cell_1_Voltage,,R,,1mv,"Voltage of 16 cells, 2 byte for each cell" +,,16,Cell_2_Voltage,,R,,1mv, +,,17,Cell_3_Voltage,,R,,1mv, +,,18,Cell_4_Voltage,,R,,1mv, +,,19,Cell_5_Voltage,,R,,1mv, +,,20,Cell_6_Voltage,,R,,1mv, +,,21,Cell_7_Voltage,,R,,1mv, +,,22,Cell_8_Voltage,,R,,1mv, +,,23,Cell_9_Voltage,,R,,1mv, +,,24,Cell_10_Voltage,,R,,1mv, +,,25,Cell_11_Voltage,,R,,1mv, +,,26,Cell_12_Voltage,,R,,1mv, +,,27,Cell_13_Voltage,,R,,1mv, +,,28,Cell_14_Voltage,,R,,1mv, +,,29,Cell_15_Voltage,,R,,1mv, +,,30,Cell_16_Voltage,,R,,1mv, +,4BIT,31,Cell_1_Temperature,,R,,0.1C, +,4BIT,31.b4,Cell_2_Temperature,,R,,0.1C, +,4BIT,31.b8,Cell_3_Temperature,,R,,0.1C, +,4BIT,31.b12,Cell_4_Temperature,,R,,0.1C, +,4BIT,32,Cell_5_Temperature,,R,,0.1C, +,4BIT,32.b4,Cell_6_Temperature,,R,,0.1C, +,4BIT,32.b8,Cell_7_Temperature,,R,,0.1C, +,4BIT,32.b12,Cell_8_Temperature,,R,,0.1C, +,4BIT,33,Cell_9_Temperature,,R,,0.1C, +,4BIT,33.b4,Cell_10_Temperature,,R,,0.1C, +,4BIT,33.b8,Cell_11_Temperature,,R,,0.1C, +,4BIT,33.b12,Cell_12_Temperature,,R,,0.1C, +,4BIT,34,Cell_13_Temperature,,R,,0.1C, +,4BIT,34.b4,Cell_14_Temperature,,R,,0.1C, +,4BIT,34.b8,Cell_15_Temperature,,R,,0.1C, +,4BIT,34.b12,Cell_16_Temperature,,R,,0.1C, +,,35,MOSFET_temperature,,R,,0.1C, +,,36,Environment_temperature,,R,,0.1C, +,UINT,40~41,Cumulative_discharging_AH,,R,,0.01AH, +,UINT,42~43,Cumulative_discharging_KWH,,R,,WH, +,BYTE,56,Year,,RW,,, +,BYTE,56.b8,Month,,RW,,, +,BYTE,57,Day,,RW,,, +,BYTE,57.b8,Hour,,RW,,, +,BYTE,58,Minute,,RW,,, +,BYTE,58.b8,Second,,RW,,, +,,60,Pack_OV_alarm,,RW,,mV, +,,61,Pack_OV_protection,,RW,,mV, +,,62,Pack_OV_release_protection,,RW,,mV, +,,63,Pack_OV_protection_delay_time,,RW,1~255,0.1S, +,,64,Cell_OV_alarm,,RW,,mV, +,,65,Cell_OV_protection,,RW,,mV, +,,66,Cell_OV_release_protection,,RW,,mV, +,,67,Cell_OV_protection_delay_time,,RW,1~255,0.1S, +,,68,Pack_UV_alarm,,RW,,mV, +,,69,Pack_UV_protection,,RW,,mV, +,,70,Pack_UV_release_protection,,RW,,mV, +,,71,Pack_UV_protection_delay_time,,RW,1~255,0.1S, +,,72,Cell_UV_alarm,,RW,,mV, +,,73,Cell_UV_protection,,RW,,mV, +,,74,Cell_UV_release_protection,,RW,,mV, +,,75,Cell_UV_protection_delay_time,,RW,1~255,0.1S, +,,76,Charging_OC_alarm,,RW,,A, +,,77,Charging_OC_protection,,RW,,A, +,,78,Charging_OC_protection_delay_time,,RW,1~255,0.1S, +,,79,Discharging_OC_alarm,,RW,,A, +,,80,Discharging_OC_protection,,RW,,A, +,,81,Discharging_OC_protection_delay_time,,RW,1~255,0.1S, +,,82,Discharging_OC-2_protection,,RW,,A, +,,83,Discharging_OC-2_protection_delay_time,,RW,1~255,0.025S, +,SHORT,84,Charging_OT_alarm,,RW,,0.1?, +,SHORT,85,Charging_OT_protection,,RW,,0.1?, +,SHORT,86,Charging_OT_release_protection,,RW,,0.1?, +,SHORT,87,Discharging_OT_alarm,,RW,,0.1?, +,SHORT,88,Discharging_OT_protection,,RW,,0.1?, +,SHORT,89,Discharging_OT_release_protection,,RW,,0.1?, +,SHORT,90,Charging_UT_alarm,,RW,,0.1?, +,SHORT,91,Charging_UT_protection,,RW,,0.1?, +,SHORT,92,Charging_UT_release_protection,,RW,,0.1?, +,SHORT,93,Discharging_UT_alarm,,RW,,0.1?, +,SHORT,94,Discharging_UT_protection,,RW,,0.1?, +,SHORT,95,Discharging_UT_release_protection,,RW,,0.1?, +,SHORT,96,MOSFET_OT_alarm,,RW,,0.1?,Or invalid parameters in BMS-4820 +,SHORT,97,MOSFET_OT_protection,,RW,,0.1?, +,SHORT,98,MOSFET_OT_release_protection,,RW,,0.1?, +,SHORT,99,Environment_OT_alarm,,RW,,0.1?, +,SHORT,100,Environment_OT_protection,,RW,,0.1?, +,SHORT,101,Environment_OT_release_protection,,RW,,0.1?, +,SHORT,102,Environment_UT_alarm,,RW,,0.1?, +,SHORT,103,Environment_UT_protection,,RW,,0.1?, +,SHORT,104,Environment_UT_release_protection,,RW,,0.1?, +,,105,Balance_start_cell_voltage,,RW,,mV, +,,106,Balance_start_delta_voltage,,RW,,mV, +,,107,Pack_full-charge_voltage,,RW,,mV, +,,108,Pack_full-charge_current,,RW,,mA, +,,109,Cell_sleep_voltage,,RW,,mV, +,,110,Cell_sleep_delay_time,,RW,,min, +,,111,Short_circuit_protect_delay_time,,RW,,25uS,Max 500uS +,,112,SOC_alarm_threshold,,RW,0~100,, +,,113,Charging_OC-2_protection,,RW,,A, +,,114,Charging_OC-2_protection_delay_time,,RW,1~255,0.025S, +,ASCII,0150~0159,Version_information,,RW,,, +,ASCII,0160~0169,Model_SN,,RW,,,BMS Manufacturer +,ASCII,0170~0179,PACK_SN,,RW,,,PACK Manufacturer +,,200,LOG_read_control,,D,,, +,BYTE,201,Year,,D,,,2018=18(Year)+2000 +,BYTE,201.b8,Month,,D,,, +,BYTE,202,Day,,D,,, +,BYTE,202.b8,Hour,,D,,, +,BYTE,203,Minute,,D,,, +,BYTE,203.b8,Second,,D,,, +,SHORT,204,Current,,RD,10mA,,Positive: charging Negative: discharging +,,205,Voltage_of_pack,,RD,10mV,, +,,206,Remain_capacity,,RD,10mAH,, +,,207,Full_capacity,,RD,10mAH,, +,,208,Cell_voltage_1,,RD,mV,,"Voltage of 16 cells, 2 byte for each cell" +,,209,Cell_voltage_2,,RD,mV,, +,,210,Cell_voltage_3,,RD,mV,, +,,211,Cell_voltage_4,,RD,mV,, +,,212,Cell_voltage_5,,RD,mV,, +,,213,Cell_voltage_6,,RD,mV,, +,,214,Cell_voltage_7,,RD,mV,, +,,215,Cell_voltage_8,,RD,mV,, +,,216,Cell_voltage_9,,RD,mV,, +,,217,Cell_voltage_10,,RD,mV,, +,,218,Cell_voltage_11,,RD,mV,, +,,219,Cell_voltage_12,,RD,mV,, +,,220,Cell_voltage_13,,RD,mV,, +,,221,Cell_voltage_14,,RD,mV,, +,,222,Cell_voltage_15,,RD,mV,, +,,223,Cell_voltage_16,,RD,mV,, +,4BIT,224,Cell_temperature_1,,RD,0.1C,,"4 cell temperature, 2 byte for each cell" +,4BIT,224.b4,Cell_temperature_2,,RD,0.1C,, +,4BIT,224.b8,Cell_temperature_3,,RD,0.1C,, +,4BIT,224.b12,Cell_temperature_4,,RD,0.1C,, +,4BIT,225,Cell_temperature_5,,RD,0.1C,, +,4BIT,225.b4,Cell_temperature_6,,RD,0.1C,, +,4BIT,225.b8,Cell_temperature_7,,RD,0.1C,, +,4BIT,225.b12,Cell_temperature_8,,RD,0.1C,, +,4BIT,226,Cell_temperature_9,,RD,0.1C,, +,4BIT,226.b4,Cell_temperature_10,,RD,0.1C,, +,4BIT,226.b8,Cell_temperature_11,,RD,0.1C,, +,4BIT,226.b12,Cell_temperature_12,,RD,0.1C,, +,4BIT,227,Cell_temperature_13,,RD,0.1C,, +,4BIT,227.b4,Cell_temperature_14,,RD,0.1C,, +,4BIT,227.b8,Cell_temperature_15,,RD,0.1C,, +,4BIT,227.b12,Cell_temperature_16,,RD,0.1C,, +,,228,MOSFET_temperature,,R,0.1C,, +,,229,Environment_temperature,,R,0.1C,, +,,230,Battery_cycle_counts,,R,Cyc.,, +,,231,Warning_flag,,R,Hex,,See description-1 +,,232,Protection_flag,,R,Hex,,See description-2 +,,233,Status/Fault_flag,,R,Hex,,See description-3 diff --git a/protocols/voltronic/voltronic_bms_v1.1.json b/protocols/voltronic/voltronic_bms_v1.1.json new file mode 100644 index 00000000..7359add6 --- /dev/null +++ b/protocols/voltronic/voltronic_bms_v1.1.json @@ -0,0 +1,8 @@ +{ + "transport" : "modbus_rtu", + "baud" : "9600", + "send_holding_register" : "true", + "send_input_register" : "false", + "batch_size" : 21, + "batch_delay" : 1.4 +} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index b6495693..40e2ac21 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ pymodbus==3.7.0 paho-mqtt -pyserial \ No newline at end of file +pyserial +python-can \ No newline at end of file diff --git a/test.py b/test.py index f7ee1f71..1ed177fe 100644 --- a/test.py +++ b/test.py @@ -1,3 +1,66 @@ + +#pip install "python-can[gs_usb]" + +import can #v4.2.0+ + +if False: + import usb #pyusb - requires https://github.com/mcuee/libusb-win32 + + + +# Candlelight firmware on Linux +bus = can.interface.Bus(interface='socketcan', channel='can0', bitrate=500000) + +# Stock slcan firmware on Linux +#bus = can.interface.Bus(bustype='slcan', channel='/dev/ttyACM0', bitrate=500000) + + +# Stock slcan firmware on Windows +#bus = can.interface.Bus(bustype='slcan', channel='COM0', bitrate=500000) + +# Candlelight firmware on windows +#USB\VID_1D50&PID_606F&REV_0000&MI_00 +if False: + dev = usb.core.find(idVendor=0x1D50, idProduct=0x606F) + bus = can.Bus(interface="gs_usb", channel=dev.product, index=0, bitrate=250000) + + + + + +# Listen for messages +try: + while True: + msg = bus.recv() # Block until a message is received + + print(str(msg.arbitration_id) + "- "+ hex(msg.arbitration_id)) + + # Check if it's the State of Charge (SoC) message (ID: 0x0FFF) + if msg.arbitration_id == 0x0FFF: + # The data is a 2-byte value (un16) + soc_bytes = msg.data[:2] + soc = int.from_bytes(soc_bytes, byteorder='big', signed=False) / 100.0 + + print(f"State of Charge: {soc:.2f}%") + + if msg.arbitration_id == 0x0355: + # Extract and print SOC value (U16, 0.01%) + soc_value = int.from_bytes(msg.data[0:0 + 2], byteorder='little') + print(f"State of Charge (SOC) Value: {soc_value / 100:.2f}%") + + # Extract and print SOH value (U16, 1%) + soh_value = int.from_bytes(msg.data[2:2 + 2], byteorder='little') + print(f"State of Health (SOH) Value: {soh_value:.2f}%") + + # Extract and print HiRes SOC value (U16, 0.01%) + hires_soc_value = int.from_bytes(msg.data[4:4 + 2], byteorder='little') + print(f"High Resolution SOC Value: {hires_soc_value / 100:.2f}%") + +except KeyboardInterrupt: + print("Listening stopped.") + +quit() + import re import ast