diff --git a/.gitignore b/.gitignore index 77746aa..586ea38 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,9 @@ variable_mask.txt variable_screen.txt .~lock.* protocols/*custom* +protocols/*/*custom* +protocols/*override* +protocols/*/*override* classes/transports/*custom* input_registry.json diff --git a/Dockerfile b/Dockerfile index 595092f..1dcb260 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,5 +11,7 @@ COPY protocol_settings.py /app/ COPY protocol_gateway.py /app/ COPY inverter.py /app/ COPY config.cfg /app/ +COPY defs/ /app/defs/ +COPY classes /app/classes/ WORKDIR /app -CMD ["python3", "protocol_gateway.py"] \ No newline at end of file +CMD ["python3", "protocol_gateway.py"] diff --git a/README.md b/README.md index 57b40db..c25a622 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,10 @@ When connected, the device will show up as a serial port. Alternatively, connect a usb adapter to your rs485 / can port with appropriate wiring. +### install as homeassistant add-on +checkout: +https://github.com/felipecrs/python-protocol-gateway-hass-addon/tree/master + ### install requirements ``` apt install pip python3 -y @@ -47,11 +51,11 @@ protocol_version = {{version}} v0.14 = growatt inverters 2020+ sigineer_v0.11 = sigineer inverters growatt_2020_v1.24 = alt protocol for large growatt inverters - currently untested -srne_v3.9 = SRNE inverters - Untested +srne_v3.9 = SRNE inverters - confirmed working-ish victron_gx_3.3 = Victron GX Devices - Untested solark_v1.1 = SolarArk 8/12K Inverters - Untested hdhk_16ch_ac_module = some chinese current monitoring device :P -srne_2021_v1.96 = SRNE inverters 2021+ (tested at ASF48100S200-H) +srne_2021_v1.96 = SRNE inverters 2021+ (tested at ASF48100S200-H, ok-ish for HF2430U60-100 ) eg4_v58 = eg4 inverters ( EG4-6000XP ) - confirmed working eg4_3000ehv_v1 = eg4 inverters ( EG4_3000EHV ) diff --git a/classes/protocol_settings.py b/classes/protocol_settings.py index c668059..f95eda3 100644 --- a/classes/protocol_settings.py +++ b/classes/protocol_settings.py @@ -136,6 +136,10 @@ class WriteMode(Enum): WRITE = 0x02 ''' READ AND WRITE ''' + #todo, write only + WRITEONLY = 0x03 + ''' WRITE ONLY''' + @classmethod def fromString(cls, name : str): name = name.strip().upper() @@ -153,7 +157,8 @@ def fromString(cls, name : str): "R/W" : "WRITE", "RW" : "WRITE", "W" : "WRITE", - "YES" : "WRITE" + "YES" : "WRITE", + "WO" : "WRITEONLY" } if name in alias: @@ -332,6 +337,21 @@ def load__json(self, file : str = '', settings_dir : str = ''): if not key.endswith("_codes"): self.settings[key] = value + def load_registry_overrides(self, override_path, keys : list[str]): + """Load overrides into a multidimensional dictionary keyed by each specified key.""" + overrides = {key: {} for key in keys} + + with open(override_path, newline='', encoding='latin-1') as csvfile: + reader = csv.DictReader(csvfile) + for row in reader: + for key in keys: + if key in row: + row[key] = row[key].strip().lower().replace(' ', '_') + key_value = row[key] + if key_value: + overrides[key][key_value] = row + return overrides + def load__registry(self, path, registry_type : Registry_Type = Registry_Type.INPUT) -> list[registry_map_entry]: registry_map : list[registry_map_entry] = [] @@ -347,219 +367,293 @@ 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 + + overrides : dict[str, dict] = None + override_keys = ['documented name', 'register'] + overrided_keys = set() + ''' list / set of keys that were used for overriding. to track unique entries''' + + #assuming path ends with .csv + override_path = path[:-4] + '.override.csv' + + if os.path.exists(override_path): + self._log.info("loading override file: " + override_path) + + overrides = self.load_registry_overrides(override_path, override_keys) + def determine_delimiter(first_row) -> str: if first_row.count(';') > first_row.count(','): return ';' else: - return ',' - + return ',' + + def process_row(row): + # Initialize variables to hold numeric and character parts + unit_multiplier : float = 1 + unit_symbol : str = '' + + #clean up doc name, for extra parsing + row['documented name'] = row['documented name'].strip().lower().replace(' ', '_') + + + #region overrides + if overrides != None: + #apply overrides using documented name or register + override_row = None + # Check each key in order until a match is found + for key in override_keys: + key_value = row.get(key) + if key_value and key_value in overrides[key]: + override_row = overrides[key][key_value] + overrided_keys.add(key_value) + break - with open(path, newline='', encoding='latin-1') as csvfile: + # Apply non-empty override values if an override row is found + if override_row: + for field, override_value in override_row.items(): + if override_value: # Only replace if override value is non-empty + row[field] = override_value - #clean column names before passing to csv dict reader + #endregion overrides - delimeter = ';' - first_row = next(csvfile).lower().replace('_', ' ') - if first_row.count(';') < first_row.count(','): - delimeter = ',' + #region unit - first_row = re.sub(r"\s+" + re.escape(delimeter) +"|" + re.escape(delimeter) +r"\s+", delimeter, first_row) #trim values + #if or is in the unit; ignore unit + if "or" in row['unit'].lower() or ":" in row['unit'].lower(): + unit_multiplier = 1 + unit_symbol = row['unit'] + else: + # Use regular expressions to extract numeric and character parts + matches = re.findall(r'(\-?[0-9.]+)|(.*?)$', row['unit']) - csvfile = itertools.chain([first_row], csvfile) #add clean header to begining of iterator + # Iterate over the matches and assign them to appropriate variables + for match in matches: + if match[0]: # If it matches a numeric part + unit_multiplier = float(match[0]) + elif match[1]: # If it matches a character part + unit_symbol = match[1].strip() - # Create a CSV reader object - reader = csv.DictReader(csvfile, delimiter=delimeter) + #convert to float + try: + unit_multiplier = float(unit_multiplier) + except: + unit_multiplier = float(1) - # Iterate over each row in the CSV file - for row in reader: + if unit_multiplier == 0: + unit_multiplier = float(1) - # Initialize variables to hold numeric and character parts - numeric_part = 1 - character_part = '' + #endregion unit - #if or is in the unit; ignore unit - if "or" in row['unit'].lower() or ":" in row['unit'].lower(): - numeric_part = 1 - character_part = row['unit'] - else: - # Use regular expressions to extract numeric and character parts - matches = re.findall(r'(\-?[0-9.]+)|(.*?)$', row['unit']) - # Iterate over the matches and assign them to appropriate variables - for match in matches: - if match[0]: # If it matches a numeric part - numeric_part = float(match[0]) - elif match[1]: # If it matches a character part - character_part = match[1].strip() - #print(str(row['documented name']) + " Unit: " + str(character_part) ) + variable_name = row['variable name'] if row['variable name'] else row['documented name'] + variable_name = variable_name.strip().lower().replace(' ', '_').replace('__', '_') #clean name + + if re.search(r"[^a-zA-Z0-9\_]", variable_name) : + self._log.warning("Invalid Name : " + str(variable_name) + " reg: " + str(row['register']) + " doc name: " + str(row['documented name']) + " path: " + str(path)) - #clean up doc name, for extra parsing - 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 = variable_name.strip().lower().replace(' ', '_').replace('__', '_') #clean name - - if re.search(r"[^a-zA-Z0-9\_]", variable_name) : - self._log.warning("Invalid Name : " + str(variable_name) + " reg: " + str(row['register']) + " doc name: " + str(row['documented name']) + " path: " + str(path)) + if not variable_name and not row['documented name']: #skip empty entry / no name. todo add more invalidator checks. + return + + #region data type + data_type = Data_Type.USHORT - #convert to float + data_type_len : int = -1 + #optional row, only needed for non-default data types + if 'data type' in row and row['data type']: + matches = data_type_regex.search(row['data type']) + if matches: + data_type_len = int(matches.group('length')) + data_type = Data_Type.fromString(matches.group('datatype')) + else: + data_type = Data_Type.fromString(row['data type']) + + + if 'values' not in row: + row['values'] = "" + self._log.warning("No Value Column : path: " + str(path)) + + #endregion data type + + #region values + #get value range for protocol analyzer + values : list = [] + value_min : int = 0 + value_max : int = 65535 #default - max value for ushort + value_regex : str = "" + value_is_json : bool = False + + #test if value is json. + if "{" in row['values']: #to try and stop non-json values from parsing. the json parser is buggy for validation try: - numeric_part = float(numeric_part) - except: - numeric_part = float(1) - - if numeric_part == 0: - numeric_part = float(1) - - data_type = Data_Type.USHORT - - - if 'values' not in row: - row['values'] = "" - self._log.warning("No Value Column : path: " + str(path)) - - data_type_len : int = -1 - #optional row, only needed for non-default data types - if 'data type' in row and row['data type']: - matches = data_type_regex.search(row['data type']) - if matches: - data_type_len = int(matches.group('length')) - data_type = Data_Type.fromString(matches.group('datatype')) - else: - data_type = Data_Type.fromString(row['data type']) - - - #get value range for protocol analyzer - values : list = [] - value_min : int = 0 - value_max : int = 65535 #default - max value for ushort - value_regex : str = "" - value_is_json : bool = False - - #test if value is json. - if "{" in row['values']: #to try and stop non-json values from parsing. the json parser is buggy for validation - try: - codes_json = json.loads(row['values']) - value_is_json = True - - name = row['documented name']+'_codes' - if name not in self.codes: - self.codes[name] = codes_json - - except ValueError: - value_is_json = False - - if not value_is_json: - if ',' in row['values']: - matches = list_regex.finditer(row['values']) - - for match in matches: - groups = match.groupdict() - if groups['range_start'] and groups['range_end']: - start = strtoint(groups['range_start']) - end = strtoint(groups['range_end']) - values.extend(range(start, end + 1)) - else: - values.append(groups['element']) - else: - matched : bool = False - val_match = range_regex.search(row['values']) + codes_json = json.loads(row['values']) + value_is_json = True + + name = row['documented name']+'_codes' + if name not in self.codes: + self.codes[name] = codes_json + + except ValueError: + value_is_json = False + + if not value_is_json: + if ',' in row['values']: + matches = list_regex.finditer(row['values']) + + for match in matches: + groups = match.groupdict() + if groups['range_start'] and groups['range_end']: + start = strtoint(groups['range_start']) + end = strtoint(groups['range_end']) + values.extend(range(start, end + 1)) + else: + values.append(groups['element']) + else: + matched : bool = False + val_match = range_regex.search(row['values']) + if val_match: + value_min = strtoint(val_match.group('start')) + value_max = strtoint(val_match.group('end')) + matched = True + + if data_type == Data_Type.ASCII: + #value_regex + val_match = ascii_value_regex.search(row['values']) if val_match: - value_min = strtoint(val_match.group('start')) - value_max = strtoint(val_match.group('end')) + value_regex = val_match.group('regex') matched = True - if data_type == Data_Type.ASCII: - #value_regex - val_match = ascii_value_regex.search(row['values']) - if val_match: - value_regex = val_match.group('regex') - matched = True - - if not matched: #single value - values.append(row['values']) - - concatenate : bool = False - concatenate_registers : list[int] = [] - - 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')) - - register_bit = match.group('bit') - if register_bit: - register_bit = strtoint(register_bit) - else: - register_bit = 0 + if not matched: #single value + values.append(row['values']) + #endregion values + + #region register + concatenate : bool = False + concatenate_registers : list[int] = [] + + 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')) + + register_bit = match.group('bit') + if register_bit: + register_bit = strtoint(register_bit) + else: + register_bit = 0 - register_byte = match.group('byte') - if register_byte: - register_byte = strtoint(register_byte) - else: - register_byte = 0 + register_byte = match.group('byte') + if register_byte: + register_byte = strtoint(register_byte) + else: + register_byte = 0 - #print("register: " + str(register) + " bit : " + str(register_bit)) + #print("register: " + str(register) + " bit : " + str(register_bit)) + else: + range_match = range_regex.search(row['register']) + if not range_match: + register = strtoint(row['register']) else: - range_match = range_regex.search(row['register']) - if not range_match: - register = strtoint(row['register']) - else: - reverse = range_match.group('reverse') - start = strtoint(range_match.group('start')) - end = strtoint(range_match.group('end')) - register = start - if end > start: - concatenate = True - if reverse: - for i in range(end, start-1, -1): - concatenate_registers.append(i) - else: - for i in range(start, end+1): - concatenate_registers.append(i) - - if concatenate_registers: - r = range(len(concatenate_registers)) + reverse = range_match.group('reverse') + start = strtoint(range_match.group('start')) + end = strtoint(range_match.group('end')) + register = start + if end > start: + concatenate = True + if reverse: + for i in range(end, start-1, -1): + concatenate_registers.append(i) + else: + for i in range(start, end+1): + concatenate_registers.append(i) + + if concatenate_registers: + r = range(len(concatenate_registers)) + else: + r = range(1) + + #endregion register + + read_command = None + if "read command" in row and row['read command']: + if row['read command'][0] == 'x': + read_command = bytes.fromhex(row['read command'][1:]) else: - r = range(1) + read_command = row['read command'].encode('utf-8') - read_command = None - if "read command" in row and row['read command']: - if row['read command'][0] == 'x': - read_command = bytes.fromhex(row['read command'][1:]) - else: - read_command = row['read command'].encode('utf-8') + writeMode : WriteMode = WriteMode.READ + if "writable" in row: + writeMode = WriteMode.fromString(row['writable']) + + for i in r: + item = registry_map_entry( + registry_type = registry_type, + register= register, + register_bit=register_bit, + register_byte= register_byte, + variable_name= variable_name, + documented_name = row['documented name'], + unit= str(unit_symbol), + unit_mod= unit_multiplier, + data_type= data_type, + data_type_size = data_type_len, + concatenate = concatenate, + concatenate_registers = concatenate_registers, + values=values, + value_min=value_min, + value_max=value_max, + value_regex=value_regex, + read_command = read_command, + write_mode=writeMode + ) + registry_map.append(item) + + register = register + 1 - writeMode : WriteMode = WriteMode.READ - if "writable" in row: - writeMode = WriteMode.fromString(row['writable']) - for i in r: - item = registry_map_entry( - registry_type = registry_type, - register= register, - register_bit=register_bit, - register_byte= register_byte, - variable_name= variable_name, - documented_name = row['documented name'], - unit= str(character_part), - unit_mod= numeric_part, - data_type= data_type, - data_type_size = data_type_len, - concatenate = concatenate, - concatenate_registers = concatenate_registers, - values=values, - value_min=value_min, - value_max=value_max, - value_regex=value_regex, - read_command = read_command, - write_mode=writeMode - ) - registry_map.append(item) - register = register + 1 + with open(path, newline='', encoding='latin-1') as csvfile: + + #clean column names before passing to csv dict reader + + delimeter = ';' + first_row = next(csvfile).lower().replace('_', ' ') + if first_row.count(';') < first_row.count(','): + delimeter = ',' + + first_row = re.sub(r"\s+" + re.escape(delimeter) +"|" + re.escape(delimeter) +r"\s+", delimeter, first_row) #trim values + + csvfile = itertools.chain([first_row], csvfile) #add clean header to begining of iterator + + # Create a CSV reader object + reader = csv.DictReader(csvfile, delimiter=delimeter) + + # Iterate over each row in the CSV file + for row in reader: + process_row(row) + + if overrides != None: + # Add any unmatched overrides as new entries... probably need to add some better error handling to ensure entry isnt empty ect... + for key in override_keys: + applied = False + for key_value, override_row in overrides[key].items(): + # Check if both keys are unique before applying + if all(override_row.get(k) for k in override_keys): + if all(override_row.get(k) not in overrided_keys for k in override_keys): + self._log.info("Loading unique entry from overrides for both unique keys") + process_row(override_row) + + # Mark both keys as applied + for k in override_keys: + overrided_keys.add(override_row.get(k)) + + applied = True + break # Exit inner loop after applying unique entry + + if applied: + continue for index in reversed(range(len(registry_map))): item = registry_map[index] @@ -633,6 +727,9 @@ def calculate_registry_ranges(self, map : list[registry_map_entry], max_register if register.register >= start and register.register < end: if register.write_mode == WriteMode.READDISABLED: ##register is disabled; skip continue + if register.write_mode == WriteMode.WRITEONLY: ##Write Only; skip + continue + registers.append(register.register) if registers: #not empty diff --git a/classes/transports/canbus.py b/classes/transports/canbus.py index b01c38b..767b93d 100644 --- a/classes/transports/canbus.py +++ b/classes/transports/canbus.py @@ -220,7 +220,7 @@ 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: + def write_data(self, data : dict[str, str], from_transport : transport_base) -> None: if not self.write_enabled: return diff --git a/classes/transports/modbus_base.py b/classes/transports/modbus_base.py index 130ff9d..0a1d4c9 100644 --- a/classes/transports/modbus_base.py +++ b/classes/transports/modbus_base.py @@ -68,6 +68,7 @@ def init_after_connect(self): #if sn is empty, attempt to autoread it if not self.device_serial_number: self.device_serial_number = self.read_serial_number() + self.update_identifier() def connect(self): if self.connected and self.first_connect: @@ -104,7 +105,7 @@ def read_serial_number(self) -> str: print(sn2) print(sn3) - if not re.search("[^a-zA-Z0-9\_]", sn2) : + if not re.search("[^a-zA-Z0-9_]", sn2) : serial_number = sn2 return serial_number @@ -117,7 +118,7 @@ def enable_write(self): self.write_enabled = True self._log.warning("enable write - validation passed") - def write_data(self, data : dict[str, str]) -> None: + def write_data(self, data : dict[str, str], from_transport : transport_base) -> None: if not self.write_enabled: return @@ -261,7 +262,7 @@ def analyze_protocol(self, settings_dir : str = 'protocols'): def evaluate_score(entry : registry_map_entry, val): score = 0 if entry.data_type == Data_Type.ASCII: - if val and not re.match('[^a-zA-Z0-9\_\-]', val): #validate ascii + if val and not re.match('[^a-zA-Z0-9_-]', val): #validate ascii mod = 1 if entry.concatenate: mod = len(entry.concatenate_registers) @@ -458,10 +459,14 @@ def read_modbus_registers(self, ranges : list[tuple] = None, start : int = 0, en if e.error_code == 4: #if no response; probably time out. retry with increased delay isError = True else: - raise + isError = True #other erorrs. ie Failed to connect[ModbusSerialClient(rtu baud[9600])] - if register.isError() or isError: - self._log.error(register.__str__) + + if isinstance(register, bytes) or register.isError() or isError: #sometimes weird errors are handled incorrectly and response is a ascii error string + if isinstance(register, bytes): + self._log.error(register.decode('utf-8')) + else: + self._log.error(register.__str__) 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 @@ -482,7 +487,6 @@ def read_modbus_registers(self, ranges : list[tuple] = None, start : int = 0, en self.modbus_delay = self.modbus_delay_setting - retry -= 1 if retry < 0: retry = 0 diff --git a/classes/transports/modbus_rtu.py b/classes/transports/modbus_rtu.py index 8e94e72..056551e 100644 --- a/classes/transports/modbus_rtu.py +++ b/classes/transports/modbus_rtu.py @@ -33,6 +33,9 @@ def __init__(self, settings : SectionProxy, protocolSettings : protocol_settings raise ValueError("Port is not set") self.port = find_usb_serial_port(self.port) + if not self.port: + raise ValueError("Port is not valid / not found") + print("Serial Port : " + self.port + " = ", get_usb_serial_port_info(self.port)) #print for config convience if "baud" in self.protocolSettings.settings: @@ -42,7 +45,7 @@ def __init__(self, settings : SectionProxy, protocolSettings : protocol_settings address : int = settings.getint("address", 0) self.addresses = [address] - + # pymodbus compatability; unit was renamed to address if 'slave' in inspect.signature(ModbusSerialClient.read_holding_registers).parameters: self.pymodbus_slave_arg = 'slave' diff --git a/classes/transports/mqtt.py b/classes/transports/mqtt.py index f184d03..2e9dbdd 100644 --- a/classes/transports/mqtt.py +++ b/classes/transports/mqtt.py @@ -153,14 +153,14 @@ def on_connect(self, client, userdata, flags, rc): __write_topics : dict[str, registry_map_entry] = {} - def write_data(self, data : dict[str, str]): + def write_data(self, data : dict[str, str], from_transport : transport_base): if not self.write_enabled: return if self.connected: self.connected = self.client.is_connected() - self._log.info("write data to mqtt transport") + self._log.info(f"write data from [{from_transport.transport_name}] to mqtt transport") self._log.info(data) #have to send this every loop, because mqtt doesnt disconnect when HA restarts. HA bug. info = self.client.publish(self.base_topic + "/availability","online", qos=0,retain=True) @@ -170,13 +170,13 @@ def write_data(self, data : dict[str, str]): if(self.json): # Serializing json json_object = json.dumps(data, indent=4) - self.client.publish(self.base_topic, json_object, 0, properties=self.mqtt_properties) + self.client.publish(self.base_topic+'/'+from_transport.device_identifier, json_object, 0, properties=self.mqtt_properties) else: for entry, val in data.items(): if isinstance(val, float) and self.max_precision >= 0: #apply max_precision on mqtt transport val = round(val, self.max_precision) - self.client.publish(str(self.base_topic+'/'+entry).lower(), str(val)) + self.client.publish(str(self.base_topic+'/'+from_transport.device_identifier+'/'+entry).lower(), str(val)) def client_on_message(self, client, userdata, msg): """ The callback for when a PUBLISH message is received from the server. """ @@ -194,7 +194,7 @@ def init_bridge(self, from_transport : transport_base): self.__write_topics = {} #subscribe to write topics for entry in from_transport.protocolSettings.get_registry_map(Registry_Type.HOLDING): - if entry.write_mode == WriteMode.WRITE: + if entry.write_mode == WriteMode.WRITE or entry.write_mode == WriteMode.WRITEONLY: #__write_topics topic : str = self.base_topic + "/write/" + entry.variable_name.lower().replace(' ', '_') self.__write_topics[topic] = entry @@ -230,7 +230,10 @@ def mqtt_discovery(self, from_transport : transport_base): if item.write_mode == WriteMode.READDISABLED: #disabled continue - clean_name = item.variable_name.lower().replace(' ', '_') + + clean_name = item.variable_name.lower().replace(' ', '_').strip() + if not clean_name: #if name is empty, skip + continue if False: if self.__input_register_prefix and item.registry_type == Registry_Type.INPUT: @@ -250,10 +253,10 @@ def mqtt_discovery(self, from_transport : transport_base): disc_payload['unique_id'] = "hotnoob_" + from_transport.device_serial_number + "_"+clean_name writePrefix = "" - if from_transport.write_enabled and item.write_mode == WriteMode.WRITE: - writePrefix = "" #home assistant doesnt like write prefix + if from_transport.write_enabled and ( item.write_mode == WriteMode.WRITE or item.write_mode == WriteMode.WRITEONLY ): + writePrefix = "" #home assistant doesnt like write prefix - disc_payload['state_topic'] = self.base_topic +writePrefix+ "/"+clean_name + disc_payload['state_topic'] = self.base_topic + '/' +from_transport.device_identifier + writePrefix+ "/"+clean_name if item.unit: disc_payload['unit_of_measurement'] = item.unit @@ -264,6 +267,10 @@ def mqtt_discovery(self, from_transport : transport_base): self.client.publish(discovery_topic, json.dumps(disc_payload),qos=1, retain=True) + #send WO message to indicate topic is write only + if item.write_mode == WriteMode.WRITEONLY: + self.client.publish(disc_payload['state_topic'], "WRITEONLY") + time.sleep(0.07) #slow down for better reliability self.client.publish(disc_payload['availability_topic'],"online",qos=0, retain=True) diff --git a/classes/transports/transport_base.py b/classes/transports/transport_base.py index d6363ae..89913f0 100644 --- a/classes/transports/transport_base.py +++ b/classes/transports/transport_base.py @@ -17,6 +17,7 @@ class transport_base: device_serial_number : str = '' device_manufacturer : str = 'hotnoob' device_model : str = 'hotnoob' + device_identifier : str = 'hotnoob' bridge : str = '' write_enabled : bool = False max_precision : int = 2 @@ -33,15 +34,16 @@ class transport_base: def __init__(self, settings : 'SectionProxy', protocolSettings : 'protocol_settings' = None) -> None: + self.transport_name = settings.name #section name + #apply log level to logger self._log_level = getattr(logging, settings.get('log_level', fallback='INFO'), logging.INFO) - self._log : logging.Logger = logging.getLogger(__name__) + short_name : str = __name__[__name__.rfind('.'): ] if '.' in __name__ else None + self._log : logging.Logger = logging.getLogger(short_name + f"[{self.transport_name}]") self._log.setLevel(self._log_level) - - self.transport_name = settings.name #section name - self.type = self.__class__.__name__ + self.type = self.__class__.__name__ self.protocolSettings = protocolSettings if not self.protocolSettings: #if not, attempt to load. lazy i know @@ -65,8 +67,12 @@ def __init__(self, settings : 'SectionProxy', protocolSettings : 'protocol_setti self.write_enabled = settings.getboolean(["write_enabled", "enable_write"], self.write_enabled) else: self.write_enabled = settings.getboolean("write", self.write_enabled) - + + self.update_identifier() + + def update_identifier(self): + self.device_identifier = self.device_serial_number.strip().lower() def init_bridge(self, from_transport : 'transport_base'): pass @@ -81,7 +87,7 @@ def _get_top_class_name(cls, cls_obj): def connect(self): pass - def write_data(self, data : dict[str, registry_map_entry]): + def write_data(self, data : dict[str, registry_map_entry], from_transport : 'transport_base'): ''' general purpose write function for between transports''' pass diff --git a/documentation/3rdparty/protocols/SRNE.Solar.Charge.Inverter.MODBUS.Protocol1.96.pdf b/documentation/3rdparty/protocols/SRNE.Solar.Charge.Inverter.MODBUS.Protocol1.96.pdf new file mode 100644 index 0000000..f5425f6 Binary files /dev/null and b/documentation/3rdparty/protocols/SRNE.Solar.Charge.Inverter.MODBUS.Protocol1.96.pdf differ diff --git a/documentation/usage/protocols.md b/documentation/usage/protocols.md index 9068598..ea76190 100644 --- a/documentation/usage/protocols.md +++ b/documentation/usage/protocols.md @@ -1,3 +1,20 @@ +## Overriding protocols +Protocols CSVs can be overriden, so that specific entries can be modified. +Protocols CSV overrides can be created naming them with name.override.csv + +For example, creating a file called: v0.14.input_registry_map.override.csv +will allow the v0.14 protocol to be modified while preseving the main csv. + +"documented name" is used as the primary key. the "register" is a secondary key. + +only non-empty values will overwrite; not all columns need to be specified. + +| documented name | data type | +| ------------- | ------------- | +| product id | ASCII | + +if both the "documented name" and "register" are unqiue, the row will be treated as a new entry. + ## Custom / Editing Protocols Custom protocols can be created by naming them with name.custom. this will ensure that they do not get overwritten when updating. @@ -45,4 +62,4 @@ protocol_version = sigineer_v0.11 ``` protocol_version = pace_bms_v1.3 ``` -[Devices\SOK to MQTT](https://github.com/HotNoob/PythonProtocolGateway/wiki/Devices%5CSOK-to-MQTT) \ No newline at end of file +[Devices\SOK to MQTT](https://github.com/HotNoob/PythonProtocolGateway/wiki/Devices%5CSOK-to-MQTT) diff --git a/protocol_gateway.py b/protocol_gateway.py index cfa1765..6ef43f8 100644 --- a/protocol_gateway.py +++ b/protocol_gateway.py @@ -171,7 +171,7 @@ def on_message(self, transport : transport_base, entry : registry_map_entry, dat for to_transport in self.__transports: if to_transport.transport_name != transport.transport_name: if to_transport.transport_name == transport.bridge or transport.transport_name == to_transport.bridge: - to_transport.write_data({entry.variable_name : data}) + to_transport.write_data({entry.variable_name : data}, transport) break def run(self): @@ -204,7 +204,7 @@ def run(self): if transport.bridge: for to_transport in self.__transports: if to_transport.transport_name == transport.bridge: - to_transport.write_data(info) + to_transport.write_data(info, transport) break except Exception as err: diff --git a/protocols/growatt/v0.14.input_registry_map.override.csv b/protocols/growatt/v0.14.input_registry_map.override.csv new file mode 100644 index 0000000..d19d04b --- /dev/null +++ b/protocols/growatt/v0.14.input_registry_map.override.csv @@ -0,0 +1,5 @@ +variable name,data type,register,documented name,description,values,unit,,,,,,,,,,, +,,0,System Status,System run state,{{system_status_codes}},1,,3: Fault,4: Flash,5: PV charge,6: AC charge,7: Combine charge,8: Combine charge and Bypass,9: PV charge and Bypass,10: AC charge and Bypass, 11: Bypass,12: PV charge andDischarge +OVERRIDE PV VOLTAGE,,,Vpv1,PV1 voltage,,0.1V,,,,,,,,,,, +extra entry,,777,unique extra entry,PV1 voltage,,0.1V,,,,,,,,,,, +extra entry broken,,,unique broken entry,,,-1,,,,,,,,,,, diff --git a/protocols/srne/srne_2021_v1.96.holding_registry_map.csv b/protocols/srne/srne_2021_v1.96.holding_registry_map.csv index ce9db32..461428a 100644 --- a/protocols/srne/srne_2021_v1.96.holding_registry_map.csv +++ b/protocols/srne/srne_2021_v1.96.holding_registry_map.csv @@ -19,32 +19,32 @@ variable name,data type,register,documented name,description,writable,values,uni ,,0x010E,Device Total charging power,,R,,W, ,BYTE,0x0210,Device state,,R,"{""0"": ""Initialization"", ""1"": ""Standby"", ""2"": ""AC power operation"", ""3"": ""Inverter operation""}",, ,,0x0212,Device Bus Voltage Sum,,R,,0.1V, -,,0x0213,Grid phase-A voltage,,R,,0.1V, -,,0x0214,Grid phase-A current,,R,,0.1A, -,,0x022A,Grid phase-B voltage,,R,,0.1V, -,,0x0238,Grid phase-B current,,R,,0.1A, -,,0x022B,Grid phase-C voltage,,R,,0.1V, -,,0x0239,Grid phase-C current,,R,,0.1A, +,,0x0213,Grid phase_A voltage,,R,,0.1V, +,,0x0214,Grid phase_A current,,R,,0.1A, +,,0x022A,Grid phase_B voltage,,R,,0.1V, +,,0x0238,Grid phase_B current,,R,,0.1A, +,,0x022B,Grid phase_C voltage,,R,,0.1V, +,,0x0239,Grid phase_C current,,R,,0.1A, ,,0x0215,Grid frequency,,R,,0.01Hz, -,,0x0216,Inverter phase-A output voltage,,R,,0.1V, -,,0x0217,Inverter phase-A inductive current,,R,,0.1A, -,,0x022C,Inverter phase-B output voltage,,R,,0.1V, -,,0x022E,Inverter phase-B inductive current,,R,,0.1A, -,,0x022D,Inverter phase-C output voltage,,R,,0.1V, -,,0x022F,Inverter phase-C inductive current,,R,,0.1A, +,,0x0216,Inverter phase_A output voltage,,R,,0.1V, +,,0x0217,Inverter phase_A inductive current,,R,,0.1A, +,,0x022C,Inverter phase_B output voltage,,R,,0.1V, +,,0x022E,Inverter phase_B inductive current,,R,,0.1A, +,,0x022D,Inverter phase_C output voltage,,R,,0.1V, +,,0x022F,Inverter phase_C inductive current,,R,,0.1A, ,,0x0218,Inverter frequency,,R,,0.01Hz, -,,0x0219,Load Phase-A current,,R,,0.1A, -,,0x021B,Load Phase-A active power,,R,,W, -,,0x021C,Load Phase-A apparent power,,R,,VA, -,,0x021F,Load Phase-A ratio,,R,0~100,%, -,,0x0230,Load Phase-B current,,R,,0.1A, -,,0x0232,Load Phase-B active power,,R,,W, -,,0x0234,Load Phase-B apparent power,,R,,VA, -,,0x0236,Load Phase-B ratio,,R,0~100,%, -,,0x0231,Load Phase-C current,,R,,0.1A, -,,0x0233,Load Phase-C active power,,R,,W, -,,0x0235,Load Phase-C apparent power,,R,,VA, -,,0x0237,Load Phase-C ratio,,R,0~100,%, +,,0x0219,Load Phase_A current,,R,,0.1A, +,,0x021B,Load Phase_A active power,,R,,W, +,,0x021C,Load Phase_A apparent power,,R,,VA, +,,0x021F,Load Phase_A ratio,,R,0~100,%, +,,0x0230,Load Phase_B current,,R,,0.1A, +,,0x0232,Load Phase_B active power,,R,,W, +,,0x0234,Load Phase_B apparent power,,R,,VA, +,,0x0236,Load Phase_B ratio,,R,0~100,%, +,,0x0231,Load Phase_C current,,R,,0.1A, +,,0x0233,Load Phase_C active power,,R,,W, +,,0x0235,Load Phase_C apparent power,,R,,VA, +,,0x0237,Load Phase_C ratio,,R,0~100,%, ,BYTE,0xF02C,Stats GenerateEnergyToGridTday,,R,,0.1kWh, ,BYTE,0xF02D,Stats BatChgTday,,R,,1AH, ,BYTE,0xF02E,Stats BatDischgTday,,R,,1AH, diff --git a/protocols/srne/srne_v1.7.holding_registry_map.csv b/protocols/srne/srne_v1.7.holding_registry_map.csv new file mode 100644 index 0000000..f087180 --- /dev/null +++ b/protocols/srne/srne_v1.7.holding_registry_map.csv @@ -0,0 +1,262 @@ +variable name,data type,register,documented name,description,writable,values,unit,note +,BYTE,0x000B,Product type,,R,"{""0"": ""domestic controller"", ""1"": ""controller for street light"", ""3"": ""grid-connected inverter"", ""4"": ""all-in-one solar charger inverter"", ""5"": ""power frequency off-grid""}",, +,,x14,Software version 1,,R,,0.01, +,,x15,Software version 2,,R,,0.01, +,,x16,Hardware version 1,,R,,0.01, +,,x17,Hardware version 2,,R,,0.01, +,,0x001A,RS485 address,,R,1~247,, +,,x1B,Model Code,,R,,, +,,0x001C,RS485 version,,R,,0.01, +,ASCII,x21~x34,software compilation time ,,R,,, +,ASCII,0x0035~0x0048,Product SN,,R,,, +,,0x0100,Battery capacity SOC,,R,0~100,%, +,,0x0101,Battery voltage,,R,,0.1V, +,SHORT,0x0102,Battery current,,R,,0.1A, +,BYTE,x103,Controller temperature,,R,,C, +,BYTE,x103.b8,Battery temperature,,R,,C, +,,x104,Load DC Voltage,,R,,0.1V, +,,x105,Load DC Current,,R,,0.01A, +,,x106,Load DC Power,,R,,W, +,,0x0107,PV1 voltage,,R,,0.1V, +,,0x0108,PV1 current,,R,,0.1A, +,,0x0109,PV1 power,,R,,W, +,,0x010F,PV2 voltage,,R,,0.1V, +,,0x0110,PV2 current,,R,,0.1A, +,,0x0111,PV2 power,,R,,W, +,,0x010B,Device Charge state,,R,"{""0"": ""Charge off"", ""1"": ""Quick charge"", ""2"": ""Const voltage charge"", ""4"": ""Float charge"", ""6"": ""Li battery activate"", ""8"": ""Full""}",, +,,0x010E,Device Total charging power,,R,,W, +,16BIT_FLAGS,x200,Fault Bits 1,,R,,, +,16BIT_FLAGS,x201,Fault Bits 2,,R,,, +,16BIT_FLAGS,x202,Fault Bits 3,,R,,, +,16BIT_FLAGS,x203,Fault Bits 4,,R,,, +,,x204,Fault Code 1,,R,,, +,,x205,Fault Code 2,,R,,, +,,x206,Fault Code 3,,R,,, +,,x207,Fault Code 4,,R,,, +,BYTE,x20C,Current Time Year,,RW,,, +,BYTE,x20C.b8,Current Time Month,,RW,,, +,BYTE,x21D,Current Time Day,,RW,,, +,BYTE,x21D.b8,Current Time Hour,,RW,,, +,BYTE,x21E,Current Time Minute,,RW,,, +,BYTE,x21E.b8,Current Time Second,,RW,,, +,,0x0210,Device state,,R,"{""0"":""Power-up delay"",""1"":""Waiting state"",""2"":""Initialization"",""3"":""Soft start"",""4"":""Mains powered operation"",""5"":""Inverter powered operation"",""6"":""Inverter to mains"",""7"":""Mains to inverter"",""8"":""Battery activate"",""9"":""Shutdown by user"",""10"":""Fault""}",, +,,x211,Password protection status mark ,,R,"{""0"":""No password entered by the user"",""1"":""User password has been entered"",""4"":""Manufacturer password has been entered""}",, +,,0x0212,Device Bus Voltage Sum,,R,,0.1V, +,,0x0213,Grid phase_A voltage,,R,,0.1V, +,,0x0214,Grid phase_A current,,R,,0.1A, +,,0x0215,Grid frequency,,R,,0.01Hz, +,,0x0216,Inverter phase_A output voltage,,R,,0.1V, +,,0x0217,Inverter phase_A inductive current,,R,,0.1A, +,,0x0218,Inverter frequency,,R,,0.01Hz, +,,0x0219,Load Phase_A current,,R,,0.1A, +,,x21A,Load PF,,R,,0.01, +,,0x021B,Load Phase_A active power,,R,,W, +,,0x021C,Load Phase_A apparent power,,R,,VA, +,,x21D,Inverter DC Component,,R,,1mV, +,,x21E,Mains charge current ,,R,,0.1A, +,,0x021F,Load Phase_A ratio,,R,0~100,%, +,,x220,DC_DC heat sink temperature ,,R,,0.1C, +,,x221,DC_AC heat sink temperature ,,R,,0.1C, +,,x222,Translator heat sink temperature ,,R,,0.1C, +,,x223,Heat sink D temperature ,,R,,0.1C, +,,x224,PV Charge Current,,R,,0.1A, +,,x225,Ibuck2 Current,,R,,0.1A, +,,x226,Inverter fault state ,,R,,, +,,x227,Charge status ,,R,,, +,,x228,PBusVolt ,,R,,0.1V, +,,x229,NBusVolt ,,R,,0.1V, +,,0x022A,Grid phase_B voltage,,R,,0.1V, +,,0x022B,Grid phase_C voltage,,R,,0.1V, +,,0x022C,Inverter phase_B output voltage,,R,,0.1V, +,,0x022E,Inverter phase_B inductive current,,R,,0.1A, +,,0x022D,Inverter phase_C output voltage,,R,,0.1V, +,,0x022F,Inverter phase_C inductive current,,R,,0.1A, +,,0x0230,Load Phase_B current,,R,,0.1A, +,,0x0231,Load Phase_C current,,R,,0.1A, +,,0x0232,Load Phase_B active power,,R,,W, +,,0x0233,Load Phase_C active power,,R,,W, +,,0x0234,Load Phase_B apparent power,,R,,VA, +,,0x0235,Load Phase_C apparent power,,R,,VA, +,,0x0236,Load Phase_B ratio,,R,0~100,%, +,,0x0237,Load Phase_C ratio,,R,0~100,%, +,,,,,,,, +,,xE001,PV charge current limit ,,RW,0~100,0.1A, +,,xE002,Nominal battery capacity ,,RW,0~400,1AH, +,,xE003,System Voltage,,R,12~255,V, +,,xE004,Battery Type,,RW,"{""0"":""User define"",""1"":""SLD"",""2"":""FLD""}",, +,,xE005,Battery Over Voltage,,RW,9~15.5,0.1V, +,,xE006,Limited Charge Voltage,,RW,9~15.5,0.1V, +,,xE007,Equalizing charge voltage ,,RW,9~15.5,0.1V, +,,xE008,Boost Charge Voltage Or Lithium Over Voltage,,RW,9~15.5,0.1V, +,,xE009,Float Charge Voltage Or Lithium Over Charge Return Voltage,,RW,9~15.5,0.1V, +,,xE00A,Boost charge return voltage ,,RW,9~15.5,0.1V, +,,xE00B,Over discharge return voltage ,,RW,9~15.5,0.1V, +,,xE00C,Under_voltage warning voltage ,,RW,9~15.5,0.1V, +,,xE00D,Over discharge voltage ,,RW,9~15.5,0.1V, +,,xE00E,Limited discharge voltage ,,RW,9~15.5,0.1V, +,BYTE,xE00F,Charge Cutoff SOC,,RW,0~100,%, +,BYTE,xE00F.b8,Discharge Cuttoff SOC,,RW,0~100,%, +,,xE010,Over discharge delay time ,,RW,0~120,S, +,,xE011,Equalizing charge time ,,RW,0~600,Min, +,,xE012,Boost charge time ,,RW,0~600,Min, +,,xE013,Equalizing charge interval ,,RW,0~255,Day, +,,xE014,Temperature compensation coefficient ,,RW,0~10,mV/C/2V , +,SHORT,xE015,Charge upper limit temperature ,,RW,Err:502,C, +,SHORT,xE016,Charge lower limit temperature ,,RW,Err:502,C, +,SHORT,xE017,Discharge upper limit temperature ,,RW,Err:502,C, +,SHORT,xE018,DisChgMinTemperature,,RW,Err:502,C, +,SHORT,xE019,HeatBatStartTemperature,,RW,Err:502,C, +,SHORT,xE01A,HeatBatStopTemperature ,,RW,Err:502,C, +,,xE01B,Mains switching voltage ,,RW,9~15.5,0.1V, +,,xE01C,Stop charging current ,,RW,0~40,0.1A, +,,xE01D,DC load working mode ,,RW,,, +,,xE01E,Light control delay time,,RW,0~60,Min, +,,xE01F,Light control voltage ,,RW,1~45,V, +,,xE020,1 Number of batteries connected in series ,,RW,1~200,, +,,xE021,Special power control ,,RW,,, +,,xE022,Inverter switching voltage ,,RW,9~15.5,0.1V, +,,xE023,Equalizing charge timeout time ,,RW,5~900,Min, +,,xE024,Lithium battery activation current ,,RW,0~10,A, +,,,,,,,, +,,xE026,1_section start charging time,hour and minute:23*256+59==5947 ,RW,0~ 5947 ,Min, +,,xE027,1_section stop charging time,,RW,0~ 5947 ,Min, +,,xE028,2_section start charging time,,RW,0~ 5947 ,Min, +,,xE029,2_section stop charging time,,RW,0~ 5947 ,Min, +,,xE02A,3_section start charging time,,RW,0~ 5947 ,Min, +,,xE02B,3_section stop charging time,,RW,0~ 5947 ,Min, +,,xE02C,Sectional charging function enable,,RW,"{""0"":""Disable"",""1"":""Enable""}",, +,,xE02D,1_section start discharging time,,RW,0~ 5947 ,Min, +,,xE02E,1_section stop discharging time,,RW,0~ 5947 ,Min, +,,xE02F,2_section start discharging time,,RW,0~ 5947 ,Min, +,,xE030,2_section stop discharging time,,RW,0~ 5947 ,Min, +,,xE031,3_section start discharging time,,RW,0~ 5947 ,Min, +,,xE032,3_section stop discharging time,,RW,0~ 5947 ,Min, +,,xE033,Sectional discharging function enable,,RW,"{""0"":""Disable"",""1"":""Enable""}",, +,,xE034,current time setup year month,E034 - year and month:99*256+12==25356 ,RW,0~ 25356 ,Yr/Mo, +,,xE035,current time setup day hour,E035 -day and hour:31*256+23==7959 ,RW,0~7959,Day/Hr, +,,xE036,current time setup min second,E036 -minute and second:59*256+59==15163 ,RW,0~ 15163,Min/Sec, +,,xE037,PV grid_connected power generation enable,,,"{""0"":""Disable"",""1"":""Enable""}",, +,,xE038,GFCI Enable,,,"{""0"":""Disable"",""1"":""Enable""}",, +,,,,,,,, +,,xE200,Inverter 485 address setup,,RW,1~254,, +,,xE201,Inverter parallel mode setup,,RW,0~7,, +,,xE202,User password set value,,W,0~65535,, +,,xE203,Password input,,W,0~65535,, +,,,,,,,, +,,xE204,Output priority,,RW,"{""0"":""solar"",""1"":""line"",""2"":""sbu""}",, +,,xE205,Mains charge current limit,,RW,0~100,0.1A, +,,xE206,Equalizing charge enable,,RW,,, +,,xE207,Power save level,,RW,0~1000,W, +,,xE208,Output voltage,,RW,100~264,0.1V, +,,xE209,Output frequency,,RW,45~65,0.01Hz, +,,xE20A,Maximum charge current,,RW,0~150,0.1A, +,,xE20B,AC input range,,RW,"{""0"":""wide range (APL)"",""1"":""narrow range (UPS)""}",, +,,xE20C,Eco mode,,RW,"{""0"":""Disable"",""1"":""Enable""}",, +,,xE20D,Overload auto restart,,RW,"{""0"":""Disable"",""1"":""Enable""}",, +,,xE20E,Over temperature auto restart,,RW,"{""0"":""Disable"",""1"":""Enable""}",, +,,xE20F,Charge priority,,RW,"{""0"":""PV preferred, only start mains charging when PV is not available"",""1"":""Mains preferred, only start PV charging when mains is not available"",""2"":""Hybrid mode, mains and PV charging at the same time, PV is preferred"",""3"":""PV only, mains does not charge""}",, +,,xE210,Alarm control,,RW,"{""0"":""Disable"",""1"":""Enable""}",, +,,xE211,Alarm enable when input source is interrupted,,RW,"{""0"":""Disable"",""1"":""Enable""}",, +,,xE212,Overload bypass enable,,RW,"{""0"":""Disable"",""1"":""Enable""}",, +,,xE213,Record fault code,,RW,"{""0"":""Disable"",""1"":""Enable""}",, +,,xE214,Split_phase transformer,,RW,"{""0"":""Disable"",""1"":""Enable""}",, +,,xE215,BMS communication enable,,RW,"{""0"":""Disable"",""1"":""Enable""}",, +,,xE216,Start charge time setup,,RW,0~23,, +,,xE217,Start discharge time setup,,RW,0~23,, +,,,,,,,, +,UINT,xE219,UniqueIDcode,,R,,, +,,xE21B,BMS protocol,,RW,0~30,, +,,,,,,,, +,,xF000,History PV power generation Today Minus 1,,R,,AH, +,,xF001,History PV power generation Today Minus 2,,R,,AH, +,,xF002,history PV power generation Today Minus 3,,R,,AH, +,,xF003,history PV power generation Today Minus 4,,R,,AH, +,,xF004,history PV power generation Today Minus 5,,R,,AH, +,,xF005,history PV power generation Today Minus 6,,R,,AH, +,,xF006,history PV power generation Today Minus 7,,R,,AH, +,,,,,,,, +,,xF007,history battery charge level Today Minus 1,,R,,AH, +,,xF008,history battery charge level Today Minus 2,,R,,AH, +,,xF009,history battery charge level Today Minus 3,,R,,AH, +,,xF00A,history battery charge level Today Minus 4,,R,,AH, +,,xF00B,history battery charge level Today Minus 5,,R,,AH, +,,xF00C,history battery charge level Today Minus 6,,R,,AH, +,,xF00D,history battery charge level Today Minus 7,,R,,AH, +,,,,,,,, +,,xF00E,History battery discharge level Today Minus 1,,R,,AH, +,,xF00F,History battery discharge level Today Minus 2,,R,,AH, +,,xF010,History battery discharge level Today Minus 3,,R,,AH, +,,xF011,History battery discharge level Today Minus 4,,R,,AH, +,,xF012,History battery discharge level Today Minus 5,,R,,AH, +,,xF013,History battery discharge level Today Minus 6,,R,,AH, +,,xF014,History battery discharge level Today Minus 7,,R,,AH, +,,,,,,,, +,,xF015,history mains charge level Today Minus 1,,R,,AH, +,,xF016,history mains charge level Today Minus 2,,R,,AH, +,,xF017,history mains charge level Today Minus 3,,R,,AH, +,,xF018,history mains charge level Today Minus 4,,R,,AH, +,,xF019,history mains charge level Today Minus 5,,R,,AH, +,,xF01A,history mains charge level Today Minus 6,,R,,AH, +,,xF01B,history mains charge level Today Minus 7,,R,,AH, +,,,,,,,, +,,xF01C,history data of power consumption by load today minus 1,,R,,0.1kwh, +,,xF01D,history power consumption by load today minus 2,,R,,0.1kwh, +,,xF01E,history power consumption by load today minus 3,,R,,0.1kwh, +,,xF01F,history power consumption by load today minus 4,,R,,0.1kwh, +,,xF020,history power consumption by load today minus 5,,R,,0.1kwh, +,,xF021,history power consumption by load today minus 6,,R,,0.1kwh, +,,xF022,history power consumption by load today minus 7,,R,,0.1kwh, +,,,,,,,, +,,xF023,History power consumption by load from mains today minus 1,,R,,0.1kwh, +,,xF024,History power consumption by load from mains today minus 2,,R,,0.1kwh, +,,xF025,History power consumption by load from mains today minus 3,,R,,0.1kwh, +,,xF026,History power consumption by load from mains today minus 4,,R,,0.1kwh, +,,xF027,History power consumption by load from mains today minus 5,,R,,0.1kwh, +,,xF028,History power consumption by load from mains today minus 6,,R,,0.1kwh, +,,xF029,History power consumption by load from mains today minus 7,,R,,0.1kwh, +,,,,,,,, +,,,,,,,, +,,xF02D,Battery charge AH of the day,,R,,AH, +,,xF02E,Battery discharge AH of the day,,R,,AH, +,,xF02F,PV power generation of the day,,R,,0.1kwh, +,,xF030,Load power consumption of the day,,R,,0.1kwh, +,,xF031,Total running days,,R,,day, +,,xF032,Total number of battery overdischarge,,R,,, +,,,,,,,, +,,xF033,Total number of battery full charge,,R,,, +,UINT,xF034~xF035,Accumulated battery charge AH,,R,,AH, +,UINT,xF036~xF037,Accumulated battery discharge AH,,R,,AH, +,UINT,xF038~xF039,Accumulated PV power generation,,R,,0.1kWh, +,,xF03A,Accumulated power consumption of load,,R,,0.1kWh, +,,xF03C,Mains charge level of today,,R,,AH, +,,xF03D,Power consumption by load from mains of today,,R,,0.1kWh, +,,xF03E,Inverter working hours of today,,R,,min, +,,xF03F,Bypass working hours of today,,R,,min, +,UINT,xF040~xF041,Power on time,UINT48 – TODO,R,,, +,UINT,xF043~xF044,Last equalizing charge completion time,UINT48 – TODO,R,,, +,UINT,xF046~xF047,Accumulated charge level by mains,,R,,0.1kWh, +,UINT,xF048~xF048,Accumulated power consumption by load from mains,,R,,0.1kWh, +,,xF04A,Accumulated working hours of inverter,,R,,h, +,,xF04B,Accumulated working hours of bypass,,R,,h, +,,,,,,,, +,,,,,,,, +,ASCII,xF800~xF80F,FaultHistoryRecord00,,,,, +,ASCII,xF810~xF81F,FaultHistoryRecord01,,,,, +,ASCII,xF820~xF82F,FaultHistoryRecord02,,,,, +,ASCII,xF830~xF83F,FaultHistoryRecord03,,,,, +,ASCII,xF840~xF84F,FaultHistoryRecord04,,,,, +,ASCII,xF850~xF85F,FaultHistoryRecord05,,,,, +,ASCII,xF860~xF86F,FaultHistoryRecord06,,,,, +,ASCII,xF870~xF87F,FaultHistoryRecord07,,,,, +,ASCII,xF880~xF88F,FaultHistoryRecord08,,,,, +,ASCII,xF890~xF89F,FaultHistoryRecord09,,,,, +,ASCII,xF8A0~xF8AF,FaultHistoryRecord10,,,,, +,ASCII,xF8B0~xF8BF,FaultHistoryRecord11,,,,, +,ASCII,xF8C0~xF8CF,FaultHistoryRecord12,,,,, +,ASCII,xF8D0~xF8DF,FaultHistoryRecord13,,,,, +,ASCII,xF8E0~xF8EF,FaultHistoryRecord14,,,,, +,ASCII,xF8F0~xF8FF,FaultHistoryRecord15,,,,, +,,,,,,,, +,,0x0238,MGrid phase_B current,,R,,0.1A, +,,0x0239,MGrid phase_C current,,R,,0.1A, +,BYTE,0xF02C,MStats GenerateEnergyToGridTday,,R,,0.1kWh, diff --git a/protocols/srne/srne_v1.7.json b/protocols/srne/srne_v1.7.json new file mode 100644 index 0000000..ee874b1 --- /dev/null +++ b/protocols/srne/srne_v1.7.json @@ -0,0 +1,5 @@ +{ + "transport" : "modbus_rtu", + "send_holding_register": true, + "send_input_register" : false +} \ No newline at end of file