diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index ad0330b56a0..34a396758cb 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -1,23 +1,9 @@ """Support for Modbus.""" -import asyncio import logging +import threading -from async_timeout import timeout -from pymodbus.client.asynchronous.asyncio import ( - AsyncioModbusSerialClient, - ModbusClientProtocol, - init_tcp_client, - init_udp_client, -) -from pymodbus.exceptions import ModbusException -from pymodbus.factory import ClientDecoder -from pymodbus.pdu import ExceptionResponse -from pymodbus.transaction import ( - ModbusAsciiFramer, - ModbusBinaryFramer, - ModbusRtuFramer, - ModbusSocketFramer, -) +from pymodbus.client.sync import ModbusSerialClient, ModbusTcpClient, ModbusUdpClient +from pymodbus.transaction import ModbusRtuFramer import voluptuous as vol from homeassistant.const import ( @@ -50,6 +36,7 @@ from .const import ( _LOGGER = logging.getLogger(__name__) + BASE_SCHEMA = vol.Schema({vol.Optional(CONF_NAME, default=DEFAULT_HUB): cv.string}) SERIAL_SCHEMA = BASE_SCHEMA.extend( @@ -101,57 +88,55 @@ SERVICE_WRITE_COIL_SCHEMA = vol.Schema( ) -async def async_setup(hass, config): +def setup(hass, config): """Set up Modbus component.""" hass.data[DOMAIN] = hub_collect = {} for client_config in config[DOMAIN]: - hub_collect[client_config[CONF_NAME]] = ModbusHub(client_config, hass.loop) + hub_collect[client_config[CONF_NAME]] = ModbusHub(client_config) def stop_modbus(event): """Stop Modbus service.""" for client in hub_collect.values(): - del client + client.close() - async def write_register(service): + def write_register(service): """Write Modbus registers.""" unit = int(float(service.data[ATTR_UNIT])) address = int(float(service.data[ATTR_ADDRESS])) value = service.data[ATTR_VALUE] client_name = service.data[ATTR_HUB] if isinstance(value, list): - await hub_collect[client_name].write_registers( + hub_collect[client_name].write_registers( unit, address, [int(float(i)) for i in value] ) else: - await hub_collect[client_name].write_register( - unit, address, int(float(value)) - ) + hub_collect[client_name].write_register(unit, address, int(float(value))) - async def write_coil(service): + def write_coil(service): """Write Modbus coil.""" unit = service.data[ATTR_UNIT] address = service.data[ATTR_ADDRESS] state = service.data[ATTR_STATE] client_name = service.data[ATTR_HUB] - await hub_collect[client_name].write_coil(unit, address, state) + hub_collect[client_name].write_coil(unit, address, state) # do not wait for EVENT_HOMEASSISTANT_START, activate pymodbus now for client in hub_collect.values(): - await client.setup(hass) + client.setup() # register function to gracefully stop modbus hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_modbus) # Register services for modbus - hass.services.async_register( + hass.services.register( DOMAIN, SERVICE_WRITE_REGISTER, write_register, schema=SERVICE_WRITE_REGISTER_SCHEMA, ) - hass.services.async_register( - DOMAIN, SERVICE_WRITE_COIL, write_coil, schema=SERVICE_WRITE_COIL_SCHEMA, + hass.services.register( + DOMAIN, SERVICE_WRITE_COIL, write_coil, schema=SERVICE_WRITE_COIL_SCHEMA ) return True @@ -159,13 +144,12 @@ async def async_setup(hass, config): class ModbusHub: """Thread safe wrapper class for pymodbus.""" - def __init__(self, client_config, main_loop): + def __init__(self, client_config): """Initialize the Modbus hub.""" # generic configuration - self._loop = main_loop self._client = None - self._lock = asyncio.Lock() + 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] @@ -183,144 +167,101 @@ class ModbusHub: # 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 - async def _connect_delay(self): - if self._config_delay > 0: - await asyncio.sleep(self._config_delay) - self._config_delay = 0 - - @staticmethod - def _framer(method): - if method == "ascii": - framer = ModbusAsciiFramer(ClientDecoder()) - elif method == "rtu": - framer = ModbusRtuFramer(ClientDecoder()) - elif method == "binary": - framer = ModbusBinaryFramer(ClientDecoder()) - elif method == "socket": - framer = ModbusSocketFramer(ClientDecoder()) - else: - framer = None - return framer - - async def setup(self, hass): + def setup(self): """Set up pymodbus client.""" if self._config_type == "serial": - # reconnect ?? - framer = self._framer(self._config_method) - - # just a class creation no IO or other slow items - self._client = AsyncioModbusSerialClient( - self._config_port, - protocol_class=ModbusClientProtocol, - framer=framer, - loop=self._loop, + 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, - stopbits=self._config_stopbits, + timeout=self._config_timeout, ) - await self._client.connect() elif self._config_type == "rtuovertcp": - # framer ModbusRtuFramer ?? - # timeout ?? - self._client = await init_tcp_client( - None, self._loop, self._config_host, self._config_port + self._client = ModbusTcpClient( + host=self._config_host, + port=self._config_port, + framer=ModbusRtuFramer, + timeout=self._config_timeout, ) elif self._config_type == "tcp": - # framer ?? - # timeout ?? - self._client = await init_tcp_client( - None, self._loop, self._config_host, self._config_port + self._client = ModbusTcpClient( + host=self._config_host, + port=self._config_port, + timeout=self._config_timeout, ) elif self._config_type == "udp": - # framer ?? - # timeout ?? - self._client = await init_udp_client( - None, self._loop, self._config_host, self._config_port + self._client = ModbusUdpClient( + host=self._config_host, + port=self._config_port, + timeout=self._config_timeout, ) else: assert False - async def _read(self, unit, address, count, func): - """Read generic with error handling.""" - await self._connect_delay() - async with self._lock: - kwargs = {"unit": unit} if unit else {} - try: - async with timeout(self._config_timeout): - result = await func(address, count, **kwargs) - except asyncio.TimeoutError: - result = None + # Connect device + self.connect() - if isinstance(result, (ModbusException, ExceptionResponse)): - _LOGGER.error("Hub %s Exception (%s)", self._config_name, result) - return result + def close(self): + """Disconnect client.""" + with self._lock: + self._client.close() - async def _write(self, unit, address, value, func): - """Read generic with error handling.""" - await self._connect_delay() - async with self._lock: - kwargs = {"unit": unit} if unit else {} - try: - async with timeout(self._config_timeout): - func(address, value, **kwargs) - except asyncio.TimeoutError: - return + def connect(self): + """Connect client.""" + with self._lock: + self._client.connect() - async def read_coils(self, unit, address, count): + def read_coils(self, unit, address, count): """Read coils.""" - if self._client.protocol is None: - return None - return await self._read(unit, address, count, self._client.protocol.read_coils) + with self._lock: + kwargs = {"unit": unit} if unit else {} + return self._client.read_coils(address, count, **kwargs) - async def read_discrete_inputs(self, unit, address, count): + def read_discrete_inputs(self, unit, address, count): """Read discrete inputs.""" - if self._client.protocol is None: - return None - return await self._read( - unit, address, count, self._client.protocol.read_discrete_inputs - ) + with self._lock: + kwargs = {"unit": unit} if unit else {} + return self._client.read_discrete_inputs(address, count, **kwargs) - async def read_input_registers(self, unit, address, count): + def read_input_registers(self, unit, address, count): """Read input registers.""" - if self._client.protocol is None: - return None - return await self._read( - unit, address, count, self._client.protocol.read_input_registers - ) + with self._lock: + kwargs = {"unit": unit} if unit else {} + return self._client.read_input_registers(address, count, **kwargs) - async def read_holding_registers(self, unit, address, count): + def read_holding_registers(self, unit, address, count): """Read holding registers.""" - if self._client.protocol is None: - return None - return await self._read( - unit, address, count, self._client.protocol.read_holding_registers - ) + with self._lock: + kwargs = {"unit": unit} if unit else {} + return self._client.read_holding_registers(address, count, **kwargs) - async def write_coil(self, unit, address, value): + def write_coil(self, unit, address, value): """Write coil.""" - if self._client.protocol is None: - return None - return await self._write(unit, address, value, self._client.protocol.write_coil) + with self._lock: + kwargs = {"unit": unit} if unit else {} + self._client.write_coil(address, value, **kwargs) - async def write_register(self, unit, address, value): + def write_register(self, unit, address, value): """Write register.""" - if self._client.protocol is None: - return None - return await self._write( - unit, address, value, self._client.protocol.write_register - ) + with self._lock: + kwargs = {"unit": unit} if unit else {} + self._client.write_register(address, value, **kwargs) - async def write_registers(self, unit, address, values): + def write_registers(self, unit, address, values): """Write registers.""" - if self._client.protocol is None: - return None - return await self._write( - unit, address, values, self._client.protocol.write_registers - ) + with self._lock: + kwargs = {"unit": unit} if unit else {} + self._client.write_registers(address, values, **kwargs) diff --git a/homeassistant/components/modbus/binary_sensor.py b/homeassistant/components/modbus/binary_sensor.py index 9989b9d530a..5f80813d108 100644 --- a/homeassistant/components/modbus/binary_sensor.py +++ b/homeassistant/components/modbus/binary_sensor.py @@ -2,7 +2,7 @@ import logging from typing import Optional -from pymodbus.exceptions import ModbusException +from pymodbus.exceptions import ConnectionException, ModbusException from pymodbus.pdu import ExceptionResponse import voluptuous as vol @@ -54,7 +54,7 @@ PLATFORM_SCHEMA = vol.All( ) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Modbus binary sensors.""" sensors = [] for entry in config[CONF_INPUTS]: @@ -70,7 +70,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) ) - async_add_entities(sensors) + add_entities(sensors) class ModbusBinarySensor(BinarySensorDevice): @@ -107,15 +107,17 @@ class ModbusBinarySensor(BinarySensorDevice): """Return True if entity is available.""" return self._available - async def async_update(self): + def update(self): """Update the state of the sensor.""" - if self._input_type == CALL_TYPE_COIL: - result = await self._hub.read_coils(self._slave, self._address, 1) - else: - result = await self._hub.read_discrete_inputs(self._slave, self._address, 1) - if result is None: + try: + if self._input_type == CALL_TYPE_COIL: + result = self._hub.read_coils(self._slave, self._address, 1) + else: + result = self._hub.read_discrete_inputs(self._slave, self._address, 1) + except ConnectionException: self._available = False return + if isinstance(result, (ModbusException, ExceptionResponse)): self._available = False return diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py index e5fbcf4d421..5cfd9c36967 100644 --- a/homeassistant/components/modbus/climate.py +++ b/homeassistant/components/modbus/climate.py @@ -3,7 +3,7 @@ import logging import struct from typing import Optional -from pymodbus.exceptions import ModbusException +from pymodbus.exceptions import ConnectionException, ModbusException from pymodbus.pdu import ExceptionResponse import voluptuous as vol @@ -72,7 +72,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Modbus Thermostat Platform.""" name = config[CONF_NAME] modbus_slave = config[CONF_SLAVE] @@ -91,7 +91,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= hub_name = config[CONF_HUB] hub = hass.data[MODBUS_DOMAIN][hub_name] - async_add_entities( + add_entities( [ ModbusThermostat( hub, @@ -170,12 +170,12 @@ class ModbusThermostat(ClimateDevice): """Return the list of supported features.""" return SUPPORT_TARGET_TEMPERATURE - async def async_update(self): + def update(self): """Update Target & Current Temperature.""" - self._target_temperature = await self._read_register( + self._target_temperature = self._read_register( CALL_TYPE_REGISTER_HOLDING, self._target_temperature_register ) - self._current_temperature = await self._read_register( + self._current_temperature = self._read_register( self._current_temperature_register_type, self._current_temperature_register ) @@ -224,7 +224,7 @@ class ModbusThermostat(ClimateDevice): """Return the supported step of target temperature.""" return self._temp_step - async def set_temperature(self, **kwargs): + def set_temperature(self, **kwargs): """Set new target temperature.""" target_temperature = int( (kwargs.get(ATTR_TEMPERATURE) - self._offset) / self._scale @@ -233,26 +233,28 @@ class ModbusThermostat(ClimateDevice): return byte_string = struct.pack(self._structure, target_temperature) register_value = struct.unpack(">h", byte_string[0:2])[0] - await self._write_register(self._target_temperature_register, register_value) + self._write_register(self._target_temperature_register, register_value) @property def available(self) -> bool: """Return True if entity is available.""" return self._available - async def _read_register(self, register_type, register) -> Optional[float]: + def _read_register(self, register_type, register) -> Optional[float]: """Read register using the Modbus hub slave.""" - if register_type == CALL_TYPE_REGISTER_INPUT: - result = await self._hub.read_input_registers( - self._slave, register, self._count - ) - else: - result = await self._hub.read_holding_registers( - self._slave, register, self._count - ) - if result is None: + try: + if register_type == CALL_TYPE_REGISTER_INPUT: + result = self._hub.read_input_registers( + self._slave, register, self._count + ) + else: + result = self._hub.read_holding_registers( + self._slave, register, self._count + ) + except ConnectionException: self._available = False return + if isinstance(result, (ModbusException, ExceptionResponse)): self._available = False return @@ -269,7 +271,12 @@ class ModbusThermostat(ClimateDevice): return register_value - async def _write_register(self, register, value): + def _write_register(self, register, value): """Write holding register using the Modbus hub slave.""" - await self._hub.write_registers(self._slave, register, [value, 0]) + try: + self._hub.write_registers(self._slave, register, [value, 0]) + except ConnectionException: + self._available = False + return + self._available = True diff --git a/homeassistant/components/modbus/manifest.json b/homeassistant/components/modbus/manifest.json index 2cdc8fe027c..a9155c7b628 100644 --- a/homeassistant/components/modbus/manifest.json +++ b/homeassistant/components/modbus/manifest.json @@ -2,7 +2,6 @@ "domain": "modbus", "name": "Modbus", "documentation": "https://www.home-assistant.io/integrations/modbus", - "requirements": ["pymodbus==2.3.0", - "pyserial-asyncio==0.4"], + "requirements": ["pymodbus==2.3.0"], "codeowners": ["@adamchengtkc", "@janiversen"] } diff --git a/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py index be3bc4c52c6..8c475a114eb 100644 --- a/homeassistant/components/modbus/sensor.py +++ b/homeassistant/components/modbus/sensor.py @@ -3,7 +3,7 @@ import logging import struct from typing import Any, Optional, Union -from pymodbus.exceptions import ModbusException +from pymodbus.exceptions import ConnectionException, ModbusException from pymodbus.pdu import ExceptionResponse import voluptuous as vol @@ -89,7 +89,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Modbus sensors.""" sensors = [] data_types = {DATA_TYPE_INT: {1: "h", 2: "i", 4: "q"}} @@ -148,7 +148,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= if not sensors: return False - async_add_entities(sensors) + add_entities(sensors) class ModbusRegisterSensor(RestoreEntity): @@ -219,19 +219,21 @@ class ModbusRegisterSensor(RestoreEntity): """Return True if entity is available.""" return self._available - async def async_update(self): + def update(self): """Update the state of the sensor.""" - if self._register_type == CALL_TYPE_REGISTER_INPUT: - result = await self._hub.read_input_registers( - self._slave, self._register, self._count - ) - else: - result = await self._hub.read_holding_registers( - self._slave, self._register, self._count - ) - if result is None: + try: + if self._register_type == CALL_TYPE_REGISTER_INPUT: + result = self._hub.read_input_registers( + self._slave, self._register, self._count + ) + else: + result = self._hub.read_holding_registers( + self._slave, self._register, self._count + ) + except ConnectionException: self._available = False return + if isinstance(result, (ModbusException, ExceptionResponse)): self._available = False return @@ -246,7 +248,7 @@ class ModbusRegisterSensor(RestoreEntity): if isinstance(val, int): self._value = str(val) if self._precision > 0: - self._value += f".{'0' * self._precision}" + self._value += "." + "0" * self._precision else: self._value = f"{val:.{self._precision}f}" diff --git a/homeassistant/components/modbus/switch.py b/homeassistant/components/modbus/switch.py index e4ec6a004fb..97a5d00a30f 100644 --- a/homeassistant/components/modbus/switch.py +++ b/homeassistant/components/modbus/switch.py @@ -2,7 +2,7 @@ import logging from typing import Optional -from pymodbus.exceptions import ModbusException +from pymodbus.exceptions import ConnectionException, ModbusException from pymodbus.pdu import ExceptionResponse import voluptuous as vol @@ -76,7 +76,7 @@ PLATFORM_SCHEMA = vol.All( ) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +def setup_platform(hass, config, add_entities, discovery_info=None): """Read configuration and create Modbus devices.""" switches = [] if CONF_COILS in config: @@ -109,7 +109,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) ) - async_add_entities(switches) + add_entities(switches) class ModbusCoilSwitch(ToggleEntity, RestoreEntity): @@ -146,24 +146,26 @@ class ModbusCoilSwitch(ToggleEntity, RestoreEntity): """Return True if entity is available.""" return self._available - async def turn_on(self, **kwargs): + def turn_on(self, **kwargs): """Set switch on.""" - await self._write_coil(self._coil, True) + self._write_coil(self._coil, True) - async def turn_off(self, **kwargs): + def turn_off(self, **kwargs): """Set switch off.""" - await self._write_coil(self._coil, False) + self._write_coil(self._coil, False) - async def async_update(self): + def update(self): """Update the state of the switch.""" - self._is_on = await self._read_coil(self._coil) + self._is_on = self._read_coil(self._coil) - async def _read_coil(self, coil) -> Optional[bool]: + def _read_coil(self, coil) -> Optional[bool]: """Read coil using the Modbus hub slave.""" - result = await self._hub.read_coils(self._slave, coil, 1) - if result is None: + try: + result = self._hub.read_coils(self._slave, coil, 1) + except ConnectionException: self._available = False return + if isinstance(result, (ModbusException, ExceptionResponse)): self._available = False return @@ -173,9 +175,14 @@ class ModbusCoilSwitch(ToggleEntity, RestoreEntity): return value - async def _write_coil(self, coil, value): + def _write_coil(self, coil, value): """Write coil using the Modbus hub slave.""" - await self._hub.write_coil(self._slave, coil, value) + try: + self._hub.write_coil(self._slave, coil, value) + except ConnectionException: + self._available = False + return + self._available = True @@ -221,21 +228,21 @@ class ModbusRegisterSwitch(ModbusCoilSwitch): self._is_on = None - async def turn_on(self, **kwargs): + def turn_on(self, **kwargs): """Set switch on.""" # Only holding register is writable if self._register_type == CALL_TYPE_REGISTER_HOLDING: - await self._write_register(self._command_on) + self._write_register(self._command_on) if not self._verify_state: self._is_on = True - async def turn_off(self, **kwargs): + def turn_off(self, **kwargs): """Set switch off.""" # Only holding register is writable if self._register_type == CALL_TYPE_REGISTER_HOLDING: - await self._write_register(self._command_off) + self._write_register(self._command_off) if not self._verify_state: self._is_on = False @@ -244,12 +251,12 @@ class ModbusRegisterSwitch(ModbusCoilSwitch): """Return True if entity is available.""" return self._available - async def async_update(self): + def update(self): """Update the state of the switch.""" if not self._verify_state: return - value = await self._read_register() + value = self._read_register() if value == self._state_on: self._is_on = True elif value == self._state_off: @@ -263,18 +270,18 @@ class ModbusRegisterSwitch(ModbusCoilSwitch): value, ) - async def _read_register(self) -> Optional[int]: - if self._register_type == CALL_TYPE_REGISTER_INPUT: - result = await self._hub.read_input_registers( - self._slave, self._register, 1 - ) - else: - result = await self._hub.read_holding_registers( - self._slave, self._register, 1 - ) - if result is None: + def _read_register(self) -> Optional[int]: + try: + if self._register_type == CALL_TYPE_REGISTER_INPUT: + result = self._hub.read_input_registers(self._slave, self._register, 1) + else: + result = self._hub.read_holding_registers( + self._slave, self._register, 1 + ) + except ConnectionException: self._available = False return + if isinstance(result, (ModbusException, ExceptionResponse)): self._available = False return @@ -284,7 +291,12 @@ class ModbusRegisterSwitch(ModbusCoilSwitch): return value - async def _write_register(self, value): + def _write_register(self, value): """Write holding register using the Modbus hub slave.""" - await self._hub.write_register(self._slave, self._register, value) + try: + self._hub.write_register(self._slave, self._register, value) + except ConnectionException: + self._available = False + return + self._available = True diff --git a/requirements_all.txt b/requirements_all.txt index ccf443d0860..f25a7b25505 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1531,7 +1531,6 @@ pysdcp==1 # homeassistant.components.sensibo pysensibo==1.0.3 -# homeassistant.components.modbus # homeassistant.components.serial pyserial-asyncio==0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8f07c7bf6a4..aa49dc935e7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -600,10 +600,6 @@ pyps4-2ndscreen==1.0.7 # homeassistant.components.qwikswitch pyqwikswitch==0.93 -# homeassistant.components.modbus -# homeassistant.components.serial -pyserial-asyncio==0.4 - # homeassistant.components.signal_messenger pysignalclirestapi==0.2.4 diff --git a/tests/components/modbus/conftest.py b/tests/components/modbus/conftest.py index d9cd62313b4..814e59e5571 100644 --- a/tests/components/modbus/conftest.py +++ b/tests/components/modbus/conftest.py @@ -40,19 +40,11 @@ class ReadResult: self.registers = register_words -read_result = None - - async def run_test( hass, use_mock_hub, register_config, entity_domain, register_words, expected ): """Run test for given config and check that sensor outputs expected result.""" - async def simulate_read_registers(unit, address, count): - """Simulate modbus register read.""" - del unit, address, count # not used in simulation, but in real connection - return read_result - # Full sensor configuration sensor_name = "modbus_test_sensor" scan_interval = 5 @@ -69,9 +61,9 @@ async def run_test( # Setup inputs for the sensor read_result = ReadResult(register_words) if register_config.get(CONF_REGISTER_TYPE) == CALL_TYPE_REGISTER_INPUT: - use_mock_hub.read_input_registers = simulate_read_registers + use_mock_hub.read_input_registers.return_value = read_result else: - use_mock_hub.read_holding_registers = simulate_read_registers + use_mock_hub.read_holding_registers.return_value = read_result # Initialize sensor now = dt_util.utcnow()