diff --git a/.coveragerc b/.coveragerc index c17f3d1057d..6043d3d45f0 100644 --- a/.coveragerc +++ b/.coveragerc @@ -563,6 +563,7 @@ omit = homeassistant/components/mochad/* homeassistant/components/modbus/climate.py homeassistant/components/modbus/cover.py + homeassistant/components/modbus/modbus.py homeassistant/components/modbus/switch.py homeassistant/components/modbus/sensor.py homeassistant/components/modem_callerid/sensor.py diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index c2a1a76840c..fe6811a35d9 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -1,9 +1,6 @@ """Support for Modbus.""" import logging -import threading -from pymodbus.client.sync import ModbusSerialClient, ModbusTcpClient, ModbusUdpClient -from pymodbus.transaction import ModbusRtuFramer import voluptuous as vol from homeassistant.components.cover import ( @@ -24,10 +21,8 @@ from homeassistant.const import ( CONF_STRUCTURE, CONF_TIMEOUT, CONF_TYPE, - EVENT_HOMEASSISTANT_STOP, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.discovery import load_platform from .const import ( ATTR_ADDRESS, @@ -71,9 +66,8 @@ from .const import ( DEFAULT_STRUCTURE_PREFIX, DEFAULT_TEMP_UNIT, MODBUS_DOMAIN as DOMAIN, - SERVICE_WRITE_COIL, - SERVICE_WRITE_REGISTER, ) +from .modbus import modbus_setup _LOGGER = logging.getLogger(__name__) @@ -193,186 +187,6 @@ CONFIG_SCHEMA = vol.Schema( def setup(hass, config): """Set up Modbus component.""" - hass.data[DOMAIN] = hub_collect = {} - - for conf_hub in config[DOMAIN]: - hub_collect[conf_hub[CONF_NAME]] = ModbusHub(conf_hub) - - # load platforms - for component, conf_key in ( - ("climate", CONF_CLIMATES), - ("cover", CONF_COVERS), - ): - if conf_key in conf_hub: - load_platform(hass, component, DOMAIN, conf_hub, config) - - def stop_modbus(event): - """Stop Modbus service.""" - for client in hub_collect.values(): - client.close() - - 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): - hub_collect[client_name].write_registers( - unit, address, [int(float(i)) for i in value] - ) - else: - hub_collect[client_name].write_register(unit, address, int(float(value))) - - 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] - 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(): - client.setup() - - # register function to gracefully stop modbus - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_modbus) - - # Register services for modbus - hass.services.register( - DOMAIN, - SERVICE_WRITE_REGISTER, - write_register, - schema=SERVICE_WRITE_REGISTER_SCHEMA, + return modbus_setup( + hass, config, SERVICE_WRITE_REGISTER_SCHEMA, SERVICE_WRITE_COIL_SCHEMA ) - hass.services.register( - DOMAIN, SERVICE_WRITE_COIL, write_coil, schema=SERVICE_WRITE_COIL_SCHEMA - ) - return True - - -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, - 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, - ) - 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) diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py index c1b7b2e6bf4..45cfbf5eb57 100644 --- a/homeassistant/components/modbus/climate.py +++ b/homeassistant/components/modbus/climate.py @@ -29,7 +29,6 @@ from homeassistant.helpers.typing import ( HomeAssistantType, ) -from . import ModbusHub from .const import ( CALL_TYPE_REGISTER_HOLDING, CALL_TYPE_REGISTER_INPUT, @@ -49,6 +48,7 @@ from .const import ( DEFAULT_STRUCT_FORMAT, MODBUS_DOMAIN, ) +from .modbus import ModbusHub _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/modbus/const.py b/homeassistant/components/modbus/const.py index 5e304165e42..d3193cc004c 100644 --- a/homeassistant/components/modbus/const.py +++ b/homeassistant/components/modbus/const.py @@ -16,7 +16,7 @@ CONF_PRECISION = "precision" CONF_COILS = "coils" # integration names -DEFAULT_HUB = "default" +DEFAULT_HUB = "modbus_hub" MODBUS_DOMAIN = "modbus" # data types @@ -67,6 +67,7 @@ CONF_VERIFY_STATE = "verify_state" # climate.py CONF_CLIMATES = "climates" +CONF_CLIMATE = "climate" CONF_TARGET_TEMP = "target_temp_register" CONF_CURRENT_TEMP = "current_temp_register" CONF_CURRENT_TEMP_REGISTER_TYPE = "current_temp_register_type" @@ -80,6 +81,7 @@ DEFAULT_STRUCTURE_PREFIX = ">f" DEFAULT_TEMP_UNIT = "C" # cover.py +CONF_COVER = "cover" CONF_STATE_OPEN = "state_open" CONF_STATE_CLOSED = "state_closed" CONF_STATE_OPENING = "state_opening" diff --git a/homeassistant/components/modbus/cover.py b/homeassistant/components/modbus/cover.py index 709c772564a..09a465a2cdd 100644 --- a/homeassistant/components/modbus/cover.py +++ b/homeassistant/components/modbus/cover.py @@ -21,7 +21,6 @@ from homeassistant.helpers.typing import ( HomeAssistantType, ) -from . import ModbusHub from .const import ( CALL_TYPE_COIL, CALL_TYPE_REGISTER_HOLDING, @@ -35,6 +34,7 @@ from .const import ( CONF_STATUS_REGISTER_TYPE, MODBUS_DOMAIN, ) +from .modbus import ModbusHub async def async_setup_platform( diff --git a/homeassistant/components/modbus/modbus.py b/homeassistant/components/modbus/modbus.py new file mode 100644 index 00000000000..21c6caa6fcc --- /dev/null +++ b/homeassistant/components/modbus/modbus.py @@ -0,0 +1,229 @@ +"""Support for Modbus.""" +import logging +import threading + +from pymodbus.client.sync import ModbusSerialClient, ModbusTcpClient, ModbusUdpClient +from pymodbus.transaction import ModbusRtuFramer + +from homeassistant.const import ( + ATTR_STATE, + CONF_COVERS, + CONF_DELAY, + CONF_HOST, + CONF_METHOD, + CONF_NAME, + CONF_PORT, + CONF_TIMEOUT, + CONF_TYPE, + EVENT_HOMEASSISTANT_STOP, +) +from homeassistant.helpers.discovery import load_platform + +from .const import ( + ATTR_ADDRESS, + ATTR_HUB, + ATTR_UNIT, + ATTR_VALUE, + CONF_BAUDRATE, + CONF_BYTESIZE, + CONF_CLIMATE, + CONF_CLIMATES, + CONF_COVER, + CONF_PARITY, + CONF_STOPBITS, + MODBUS_DOMAIN as DOMAIN, + SERVICE_WRITE_COIL, + SERVICE_WRITE_REGISTER, +) + +_LOGGER = logging.getLogger(__name__) + + +def modbus_setup( + hass, config, service_write_register_schema, service_write_coil_schema +): + """Set up Modbus component.""" + hass.data[DOMAIN] = hub_collect = {} + + for conf_hub in config[DOMAIN]: + hub_collect[conf_hub[CONF_NAME]] = ModbusHub(conf_hub) + + # modbus needs to be activated before components are loaded + # to avoid a racing problem + hub_collect[conf_hub[CONF_NAME]].setup() + + # load platforms + for component, conf_key in ( + (CONF_CLIMATE, CONF_CLIMATES), + (CONF_COVER, CONF_COVERS), + ): + if conf_key in conf_hub: + load_platform(hass, component, DOMAIN, conf_hub, config) + + def stop_modbus(event): + """Stop Modbus service.""" + for client in hub_collect.values(): + client.close() + + 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): + hub_collect[client_name].write_registers( + unit, address, [int(float(i)) for i in value] + ) + else: + hub_collect[client_name].write_register(unit, address, int(float(value))) + + 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] + hub_collect[client_name].write_coil(unit, address, state) + + # register function to gracefully stop modbus + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_modbus) + + # Register services for modbus + hass.services.register( + DOMAIN, + SERVICE_WRITE_REGISTER, + write_register, + schema=service_write_register_schema, + ) + hass.services.register( + DOMAIN, SERVICE_WRITE_COIL, write_coil, schema=service_write_coil_schema + ) + return True + + +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, + 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, + ) + 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) diff --git a/homeassistant/components/modbus/switch.py b/homeassistant/components/modbus/switch.py index b1b07fb5a55..36fbef08428 100644 --- a/homeassistant/components/modbus/switch.py +++ b/homeassistant/components/modbus/switch.py @@ -20,7 +20,6 @@ from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, HomeAssistantType -from . import ModbusHub from .const import ( CALL_TYPE_COIL, CALL_TYPE_REGISTER_HOLDING, @@ -37,6 +36,7 @@ from .const import ( DEFAULT_HUB, MODBUS_DOMAIN, ) +from .modbus import ModbusHub _LOGGER = logging.getLogger(__name__)