class ModbusHub: """Thread safe wrapper class for pymodbus.""" def __init__(self, client_config): """Initialize the Modbus hub.""" # generic configuration self._client = None self._lock = threading.Lock() self._config_name = client_config[CONF_NAME] self._config_type = client_config[CONF_TYPE] self._config_port = client_config[CONF_PORT] self._config_timeout = client_config[CONF_TIMEOUT] self._config_delay = 0 if self._config_type == "serial": # serial configuration self._config_method = client_config[CONF_METHOD] self._config_baudrate = client_config[CONF_BAUDRATE] self._config_stopbits = client_config[CONF_STOPBITS] self._config_bytesize = client_config[CONF_BYTESIZE] self._config_parity = client_config[CONF_PARITY] else: # network configuration self._config_host = client_config[CONF_HOST] self._config_delay = client_config[CONF_DELAY] if self._config_delay > 0: _LOGGER.warning( "Parameter delay is accepted but not used in this version") @property def name(self): """Return the name of this hub.""" return self._config_name def setup(self): """Set up pymodbus client.""" if self._config_type == "serial": self._client = ModbusSerialClient( method=self._config_method, port=self._config_port, baudrate=self._config_baudrate, stopbits=self._config_stopbits, bytesize=self._config_bytesize, parity=self._config_parity, timeout=self._config_timeout, ) elif self._config_type == "rtuovertcp": self._client = ModbusTcpClient( host=self._config_host, port=self._config_port, framer=ModbusRtuFramer, timeout=self._config_timeout, ) elif self._config_type == "tcp": self._client = ModbusTcpClient( host=self._config_host, port=self._config_port, timeout=self._config_timeout, ) elif self._config_type == "udp": self._client = ModbusUdpClient( host=self._config_host, port=self._config_port, timeout=self._config_timeout, ) else: assert False # Connect device self.connect() def close(self): """Disconnect client.""" with self._lock: self._client.close() def connect(self): """Connect client.""" with self._lock: self._client.connect() def read_coils(self, unit, address, count): """Read coils.""" with self._lock: kwargs = {"unit": unit} if unit else {} return self._client.read_coils(address, count, **kwargs) def read_discrete_inputs(self, unit, address, count): """Read discrete inputs.""" with self._lock: kwargs = {"unit": unit} if unit else {} return self._client.read_discrete_inputs(address, count, **kwargs) def read_input_registers(self, unit, address, count): """Read input registers.""" with self._lock: kwargs = {"unit": unit} if unit else {} return self._client.read_input_registers(address, count, **kwargs) def read_holding_registers(self, unit, address, count): """Read holding registers.""" with self._lock: kwargs = {"unit": unit} if unit else {} return self._client.read_holding_registers(address, count, **kwargs) def write_coil(self, unit, address, value): """Write coil.""" with self._lock: kwargs = {"unit": unit} if unit else {} self._client.write_coil(address, value, **kwargs) def write_register(self, unit, address, value): """Write register.""" with self._lock: kwargs = {"unit": unit} if unit else {} self._client.write_register(address, value, **kwargs) def write_registers(self, unit, address, values): """Write registers.""" with self._lock: kwargs = {"unit": unit} if unit else {} self._client.write_registers(address, values, **kwargs)
class ModbusHub: def __init__(self, protocol, host, port, slave): self._lock = threading.Lock() self._slave = slave if (protocol == "rtuovertcp"): self._client = ModbusTcpClient(host=host, port=port, framer=ModbusRtuFramer, timeout=2, retry_on_empty=True, retry_on_invalid=False) elif (protocol == "rtuoverudp"): self._client = ModbusUdpClient(host=host, port=port, framer=ModbusRtuFramer, timeout=2, retry_on_empty=False, retry_on_invalid=False) def connect(self): with self._lock: self._client.connect() def close(self): with self._lock: self._client.close() def read_holding_register(self): pass def read_input_registers(self, address, count): with self._lock: kwargs = {"unit": self._slave} return self._client.read_input_registers(address, count, **kwargs) def reset_energy(self): with self._lock: kwargs = {"unit": self._slave} request = ModbusResetEnergyRequest(**kwargs) self._client.execute(request) def info_gather(self): data = {} try: result = self.read_input_registers(0, 9) if result is not None and type(result) is not ModbusIOException \ and result.registers is not None and len(result.registers) == 9: data[DEVICE_CLASS_VOLTAGE] = result.registers[0] / 10 data[DEVICE_CLASS_CURRENT] = ( (result.registers[2] << 16) + result.registers[1]) / 1000 data[DEVICE_CLASS_POWER] = ( (result.registers[4] << 16) + result.registers[3]) / 10 data[DEVICE_CLASS_ENERGY] = ( (result.registers[6] << 16) + result.registers[5]) / 1000 data[DEVICE_CLASS_FREQUENCY] = result.registers[7] / 10 data[DEVICE_CLASS_POWER_FACTOR] = result.registers[8] / 100 else: _LOGGER.debug(f"Error in gathering, timed out") except Exception as e: _LOGGER.error(f"Error in gathering, {e}") return data
class ModbusHub: """Thread safe wrapper class for pymodbus.""" def __init__(self, client_config): """Initialize the modbus hub.""" # generic configuration self._client = None self._kwargs = {'unit': client_config[CONF_MASTER_UNIT_ID]} self._lock = threading.Lock() self._config_type = client_config[CONF_TYPE] self._config_port = client_config[CONF_PORT] self._config_timeout = client_config[CONF_TIMEOUT] self._config_delay = 0 if self._config_type == "serial": # serial configuration self._config_method = client_config[CONF_METHOD] self._config_baudrate = client_config[CONF_BAUDRATE] self._config_stopbits = client_config[CONF_STOPBITS] self._config_bytesize = client_config[CONF_BYTESIZE] self._config_parity = client_config[CONF_PARITY] else: # network configuration self._config_host = client_config[CONF_HOST] def setup(self): """Set up pymodbus client.""" if self._config_type == "serial": from pymodbus.client.sync import ModbusSerialClient self._client = ModbusSerialClient( method=self._config_method, port=self._config_port, baudrate=self._config_baudrate, stopbits=self._config_stopbits, bytesize=self._config_bytesize, parity=self._config_parity, timeout=self._config_timeout, retry_on_empty=True, ) elif self._config_type == "rtuovertcp": from pymodbus.client.sync import ModbusTcpClient from pymodbus.transaction import ModbusRtuFramer self._client = ModbusTcpClient( host=self._config_host, port=self._config_port, framer=ModbusRtuFramer, timeout=self._config_timeout, ) elif self._config_type == "tcp": from pymodbus.client.sync import ModbusTcpClient self._client = ModbusTcpClient( host=self._config_host, port=self._config_port, timeout=self._config_timeout, ) elif self._config_type == "udp": from pymodbus.client.sync import ModbusUdpClient self._client = ModbusUdpClient( host=self._config_host, port=self._config_port, timeout=self._config_timeout, ) else: raise ValueError(("Unsupported config_type, must be serial, " + "tcp, udp, rtuovertcp")) # Connect device self.connect() def close(self): """Disconnect client.""" with self._lock: self._client.close() def connect(self): """Connect client.""" with self._lock: self._client.connect() def read_coils(self, address, count=1): """Read coils.""" with self._lock: return self._client.read_coils(address, count, **self._kwargs) def read_input_registers(self, address, count=1): """Read input registers.""" with self._lock: return self._client.read_input_registers(address, count, **self._kwargs) def read_holding_registers(self, address, count=1): """Read holding registers.""" with self._lock: return self._client.read_holding_registers(address, count, **self._kwargs) def write_coil(self, address, value): """Write coil.""" with self._lock: self._client.write_coil(address, value, **self._kwargs) def write_register(self, address, value): """Write register.""" with self._lock: self._client.write_register(address, value, **self._kwargs) def write_registers(self, address, values): """Write registers.""" with self._lock: self._client.write_registers(address, values, **self._kwargs)
class CtModbus(Ctui): """Commands that users may use at the application prompt.""" name = 'ctmodbus' version = '0.5' description = 'a security professional\'s swiss army knife for interacting with Modbus devices' prompt = 'ctmodbus> ' session = None unit_id = 1 statusbar = 'Session:{}'.format(session) def do_debug(self, args, output_text): message_dialog(title='Debug', text=str(eval(args))) def validate_serial_device(self, device): devices = [x.device for x in serial.tools.list_ports.comports()] devices_pretty = '\n'.join([x for x in devices]) assert (device in devices), '{} is not in: \n{}'.format( device, devices_pretty) return device def do_connect_ascii(self, args, output_text): """Connect to a Modbus ASCII serial device""" assert ( self.session == None ), 'Session already open. Close first.' # ToDo assert session type device = self.validate_serial_device(args) self.session = ModbusSerialClient(method='ascii', port=device, timeout=1) message_dialog(title='Success', text='Session opened with {}'.format(device)) def do_connect_rtu(self, args, output_text): """Connect to a Modbus RTU serial device""" assert ( self.session == None ), 'Session already open. Close first.' # ToDo assert session type device = self.validate_serial_device(args) self.session = ModbusSerialClient(method='rtu', port=device, timeout=1) message_dialog(title='Success', text='Session opened with {}'.format(device)) def parse_ip_port(self, ip_port): parts = ip_port.split(':') assert (0 < len(parts) < 3), 'Must be in format ip or host or ip:port or host:port' host = parts[0] port = 502 if len(parts) == 2: port = int(parts[1]) return host, port def do_connect_tcp(self, args, output_text): """Connect to a Modbus TCP device""" assert ( self.session == None ), 'Session already open. Close first.' # ToDo assert session type host, port = self.parse_ip_port(args) self.session = ModbusTcpClient(host, port) message_dialog(title='Success', text='Session opened with {}:{}'.format(host, port)) def do_connect_udp(self, args, output_text): """Connect to a Modbus UDP device""" assert ( self.session == None ), 'Session already open. Close first.' # ToDo assert session type host, port = self.parse_ip_port(args) self.session = ModbusUdpClient(host, port) message_dialog(title='Success', text='Session opened with {}:{}'.format(host, port)) def do_close(self, args, output_text): """Close a session.""" assert ( self.session ), 'There is not an open session. Connect to one first' # ToDo assert session type self.session.close() message_dialog(title='Success', text='Session closed with {}'.format(self.session)) self.session = None return None def do_read_id(self, args, output_text): """Read device identification data.""" assert ( self.session ), 'There is not an open session. Connect to one first' # ToDo assert session type request = ReadDeviceInformationRequest(unit=1) response = self.session.execute(request) message_dialog(title="Response", text=str(reponse)) return None def log_and_output_bits(self, desc, start, stop, results): """Log in project database and output to screen""" date, time = str(datetime.today()).split() output_text = '{} {} - {}'.format(date, time, desc) i = 0 for address in range(start, stop): # Finish line after 32 bits of output if i % 32 == 0: output_text += '\n{:>5}: '.format(address) i += 1 # Print next bit output_text += str(results[address]) # Print spaces ever 4 bits if i % 4 == 0: output_text += ' ' # TODO: Add to self.storage output_text += '\n' return output_text def log_and_output_words(self, desc, start, stop, results): """Log in project database and output to screen""" date, time = str(datetime.today()).split() output_text = '{} {} - {}'.format(date, time, desc) i = 0 for address in range(start, stop): # Finish line after 8 words of output if i % 8 == 0: output_text += '\n{:>5}: '.format(address) i += 1 # Print next word output_text += '{:04x}'.format(results[address]) + ' ' # TODO: Add to self.storage output_text += '\n' return output_text def response_message_dialog(self, message, results): la, lr, fa = None, None, None #last_addres, last_result, first_address table = [['Addr', 'Int', 'HEX', 'ASCII']] for address, result in results.items(): if address - 1 == la and result == lr: if fa == None: fa = la elif la != None: if fa == None: table.append([la, lr, '{:04x}'.format(lr), str(chr(lr))]) fa = None else: s = '{0}-{1}'.format(fa, la) table.append([s, lr, '{:04x}'.format(lr), str(chr(lr))]) fa = None la, lr = address, result # Print final output from for loop if fa == None: table.append([la, lr, '{:04x}'.format(lr), str(chr(lr))]) else: s = '{0}-{1}'.format(fa, la) table.append([s, lr, '{:04x}'.format(lr), str(chr(lr))]) message += tabulate(table, headers='firstrow', tablefmt='simple') message_dialog(title='Success', text=message) def read_common(self, args): assert (self.session ), 'There is not an open session. Connect to one first' # TODO assert session type assert (len(args) > 0), 'No address specified' parts = args.split(maxsplit=1) csr = parts[0] max_count = 100 if len(parts) == 2: assert (parts[1].isdigit()), 'max_count must be a digit' max_count = int(parts[1]) loops = Loops(csr, minimum=0, maximum=65535) for loop in loops.max_count(max_count): yield loop.values() def do_read_discrete_inputs(self, args, output_text): """Read digital inputs in format: 30,50,70-99,105.""" desc = 'Modbus Function 2, Read Discrete Inputs' results = {} for start, stop, count in self.read_common(args): response = self.session.read_discrete_inputs(start, count, unit=self.unit_id) for address, result in zip(range(start, stop), response.bits): results[address] = int(result) output_text += self.log_and_output_bits(desc, start, stop, results) csr = args.split()[0] message = '{}: {}\n\n'.format(desc, csr) self.response_message_dialog(message, results) return output_text def do_read_coils(self, args, output_text): """Read digital outputs in format: 30,50,70-99,105.""" desc = 'Modbus Function 1, Read Coils' results = {} for start, stop, count in self.read_common(args): response = self.session.read_coils(start, count, unit=self.unit_id) for address, result in zip(range(start, stop), response.bits): results[address] = int(result) output_text += self.log_and_output_bits(desc, start, stop, results) csr = args.split()[0] message = '{}: {}\n\n'.format(desc, csr) self.response_message_dialog(message, results) return output_text def do_read_input_registers(self, args, output_text): """Read analog inputs or internal registers in format: 30,50,70-99,105.""" desc = 'Modbus Function 4, Read Input Registers' results = {} for start, stop, count in self.read_common(args): response = self.session.read_input_registers(start, count, unit=self.unit_id) for address, result in zip(range(start, stop), response.registers): results[address] = result output_text += self.log_and_output_words(desc, start, stop, results) csr = args.split()[0] message = '{}: {}\n\n'.format(desc, csr) self.response_message_dialog(message, results) return output_text def do_read_holding_registers(self, args, output_text): """Read digital inputs in format: 30,50,70-99,105.""" desc = 'Modbus Function 3, Read Holding Registers' results = {} for start, stop, count in self.read_common(args): response = self.session.read_holding_registers(start, count, unit=self.unit_id) for address, result in zip(range(start, stop), response.registers): results[address] = result output_text += self.log_and_output_words(desc, start, stop, results) csr = args.split()[0] message = '{}: {}\n\n'.format(desc, csr) self.response_message_dialog(message, results) return output_text def do_write_registers(self, args, output_text): """Write to registers in format: <address> <int>...""" start, values = args.split(maxsplit=1) assert (start.isdigit()), 'start must be an integer' int_start = int(start) try: list_int_values = [int(x) for x in values.split()] except: raise AssertionError('List of values to write must be decimals') if len(list_int_values) == 1: self.session.write_register(int_start, list_int_values[0], unit=self.unit_id) desc = 'Modbus Function 5, Write Single Register' else: self.session.write_registers(int_start, list_int_values, unit=self.unit_id) desc = 'Modbus Function 5, Write Multiple Registers' message_dialog(title='Success', text=f'Wrote {list_int_values} starting at {int_start}') results = {} stop = int_start + len(list_int_values) for i in range(len(list_int_values)): results[int_start + i] = list_int_values[i] output_text += self.log_and_output_words(desc, int_start, stop, results) return output_text def do_write_coils(self, args, output_text): """Write to registers in format: <address> <int>...""" desc = 'Modbus Function 3, Read Holding Registers' start, values = args.split(maxsplit=1) assert (start.isdigit()), 'start must be an integer' int_start = int(start) try: list_bool_values = [bool(int(x)) for x in values.replace(' ', '')] except: raise AssertionError('List of values to write must be decimals') if len(list_bool_values) == 1: self.session.write_coil(int_start, list_bool_values[0], unit=self.unit_id) desc = 'Modbus Function 5, Write Single Coil' else: self.session.write_coils(int_start, list_bool_values, unit=self.unit_id) desc = 'Modbus Function 15, Write Multiple Coils' message_dialog( title='Success', text=f'Wrote {list_bool_values} starting at {int_start}') results = {} stop = int_start + len(list_bool_values) for i in range(len(list_bool_values)): results[int_start + i] = int(list_bool_values[i]) output_text += self.log_and_output_bits(desc, int_start, stop, results) return output_text
class ModbusHub: """Thread safe wrapper class for pymodbus.""" def __init__(self, client_config): """Initialize the Modbus hub.""" # generic configuration self._client = None self._in_error = False self._lock = threading.Lock() self._config_name = client_config[CONF_NAME] self._config_type = client_config[CONF_TYPE] self._config_port = client_config[CONF_PORT] self._config_timeout = client_config[CONF_TIMEOUT] self._config_delay = 0 Defaults.Timeout = 10 if self._config_type == "serial": # serial configuration self._config_method = client_config[CONF_METHOD] self._config_baudrate = client_config[CONF_BAUDRATE] self._config_stopbits = client_config[CONF_STOPBITS] self._config_bytesize = client_config[CONF_BYTESIZE] self._config_parity = client_config[CONF_PARITY] else: # network configuration self._config_host = client_config[CONF_HOST] self._config_delay = client_config[CONF_DELAY] if self._config_delay > 0: _LOGGER.warning("Parameter delay is accepted but not used in this version") @property def name(self): """Return the name of this hub.""" return self._config_name def _log_error(self, exception_error: ModbusException, error_state=True): log_text = "Pymodbus: " + str(exception_error) if self._in_error: _LOGGER.debug(log_text) else: _LOGGER.error(log_text) self._in_error = error_state def setup(self): """Set up pymodbus client.""" try: if self._config_type == "serial": self._client = ModbusSerialClient( method=self._config_method, port=self._config_port, baudrate=self._config_baudrate, stopbits=self._config_stopbits, bytesize=self._config_bytesize, parity=self._config_parity, timeout=self._config_timeout, retry_on_empty=True, ) elif self._config_type == "rtuovertcp": self._client = ModbusTcpClient( host=self._config_host, port=self._config_port, framer=ModbusRtuFramer, timeout=self._config_timeout, ) elif self._config_type == "tcp": self._client = ModbusTcpClient( host=self._config_host, port=self._config_port, timeout=self._config_timeout, ) elif self._config_type == "udp": self._client = ModbusUdpClient( host=self._config_host, port=self._config_port, timeout=self._config_timeout, ) except ModbusException as exception_error: self._log_error(exception_error, error_state=False) return # Connect device self.connect() def close(self): """Disconnect client.""" with self._lock: try: if self._client: self._client.close() self._client = None except ModbusException as exception_error: self._log_error(exception_error) return def connect(self): """Connect client.""" with self._lock: try: self._client.connect() except ModbusException as exception_error: self._log_error(exception_error, error_state=False) return def read_coils(self, unit, address, count): """Read coils.""" with self._lock: kwargs = {"unit": unit} if unit else {} try: result = self._client.read_coils(address, count, **kwargs) except ModbusException as exception_error: self._log_error(exception_error) return None if isinstance(result, (ExceptionResponse, IllegalFunctionRequest)): self._log_error(result) return None self._in_error = False return result def read_discrete_inputs(self, unit, address, count): """Read discrete inputs.""" with self._lock: kwargs = {"unit": unit} if unit else {} try: result = self._client.read_discrete_inputs(address, count, **kwargs) except ModbusException as exception_error: self._log_error(exception_error) return None if isinstance(result, (ExceptionResponse, IllegalFunctionRequest)): self._log_error(result) return None self._in_error = False return result def read_input_registers(self, unit, address, count): """Read input registers.""" with self._lock: kwargs = {"unit": unit} if unit else {} try: result = self._client.read_input_registers(address, count, **kwargs) except ModbusException as exception_error: self._log_error(exception_error) return None if isinstance(result, (ExceptionResponse, IllegalFunctionRequest)): self._log_error(result) return None self._in_error = False return result def read_holding_registers(self, unit, address, count): """Read holding registers.""" with self._lock: kwargs = {"unit": unit} if unit else {} try: result = self._client.read_holding_registers(address, count, **kwargs) except ModbusException as exception_error: self._log_error(exception_error) return None if isinstance(result, (ExceptionResponse, IllegalFunctionRequest)): self._log_error(result) return None self._in_error = False return result def write_coil(self, unit, address, value) -> bool: """Write coil.""" with self._lock: kwargs = {"unit": unit} if unit else {} try: result = self._client.write_coil(address, value, **kwargs) except ModbusException as exception_error: self._log_error(exception_error) return False if isinstance(result, (ExceptionResponse, IllegalFunctionRequest)): self._log_error(result) return False self._in_error = False return True def write_coils(self, unit, address, values) -> bool: """Write coil.""" with self._lock: kwargs = {"unit": unit} if unit else {} try: result = self._client.write_coils(address, values, **kwargs) except ModbusException as exception_error: self._log_error(exception_error) return False if isinstance(result, (ExceptionResponse, IllegalFunctionRequest)): self._log_error(result) return False self._in_error = False return True def write_register(self, unit, address, value) -> bool: """Write register.""" with self._lock: kwargs = {"unit": unit} if unit else {} try: result = self._client.write_register(address, value, **kwargs) except ModbusException as exception_error: self._log_error(exception_error) return False if isinstance(result, (ExceptionResponse, IllegalFunctionRequest)): self._log_error(result) return False self._in_error = False return True def write_registers(self, unit, address, values) -> bool: """Write registers.""" with self._lock: kwargs = {"unit": unit} if unit else {} try: result = self._client.write_registers(address, values, **kwargs) except ModbusException as exception_error: self._log_error(exception_error) return False if isinstance(result, (ExceptionResponse, IllegalFunctionRequest)): self._log_error(result) return False self._in_error = False return True