2021-02-12 15:33:18 +00:00
|
|
|
"""Support for Modbus."""
|
2021-05-15 17:54:17 +00:00
|
|
|
import asyncio
|
2021-02-12 15:33:18 +00:00
|
|
|
import logging
|
|
|
|
|
|
|
|
from pymodbus.client.sync import ModbusSerialClient, ModbusTcpClient, ModbusUdpClient
|
2021-04-25 21:11:01 +00:00
|
|
|
from pymodbus.constants import Defaults
|
2021-04-19 15:18:15 +00:00
|
|
|
from pymodbus.exceptions import ModbusException
|
2021-02-12 15:33:18 +00:00
|
|
|
from pymodbus.transaction import ModbusRtuFramer
|
|
|
|
|
|
|
|
from homeassistant.const import (
|
|
|
|
CONF_DELAY,
|
|
|
|
CONF_HOST,
|
|
|
|
CONF_METHOD,
|
|
|
|
CONF_NAME,
|
|
|
|
CONF_PORT,
|
|
|
|
CONF_TIMEOUT,
|
|
|
|
CONF_TYPE,
|
|
|
|
EVENT_HOMEASSISTANT_STOP,
|
|
|
|
)
|
2021-05-15 17:54:17 +00:00
|
|
|
from homeassistant.core import callback
|
|
|
|
from homeassistant.helpers.discovery import async_load_platform
|
|
|
|
from homeassistant.helpers.event import async_call_later
|
2021-02-12 15:33:18 +00:00
|
|
|
|
|
|
|
from .const import (
|
|
|
|
ATTR_ADDRESS,
|
|
|
|
ATTR_HUB,
|
2021-04-04 12:02:47 +00:00
|
|
|
ATTR_STATE,
|
2021-02-12 15:33:18 +00:00
|
|
|
ATTR_UNIT,
|
|
|
|
ATTR_VALUE,
|
2021-05-17 09:20:12 +00:00
|
|
|
CALL_TYPE_COIL,
|
|
|
|
CALL_TYPE_DISCRETE,
|
|
|
|
CALL_TYPE_REGISTER_HOLDING,
|
|
|
|
CALL_TYPE_REGISTER_INPUT,
|
|
|
|
CALL_TYPE_WRITE_COIL,
|
|
|
|
CALL_TYPE_WRITE_COILS,
|
|
|
|
CALL_TYPE_WRITE_REGISTER,
|
|
|
|
CALL_TYPE_WRITE_REGISTERS,
|
2021-02-12 15:33:18 +00:00
|
|
|
CONF_BAUDRATE,
|
|
|
|
CONF_BYTESIZE,
|
2021-05-14 08:54:23 +00:00
|
|
|
CONF_CLOSE_COMM_ON_ERROR,
|
2021-02-12 15:33:18 +00:00
|
|
|
CONF_PARITY,
|
|
|
|
CONF_STOPBITS,
|
2021-04-19 15:18:15 +00:00
|
|
|
DEFAULT_HUB,
|
2021-02-12 15:33:18 +00:00
|
|
|
MODBUS_DOMAIN as DOMAIN,
|
2021-05-10 17:28:38 +00:00
|
|
|
PLATFORMS,
|
2021-02-12 15:33:18 +00:00
|
|
|
SERVICE_WRITE_COIL,
|
|
|
|
SERVICE_WRITE_REGISTER,
|
|
|
|
)
|
|
|
|
|
2021-05-17 09:20:12 +00:00
|
|
|
ENTRY_FUNC = "func"
|
|
|
|
ENTRY_ATTR = "attr"
|
|
|
|
|
2021-02-12 15:33:18 +00:00
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
2021-05-15 17:54:17 +00:00
|
|
|
async def async_modbus_setup(
|
2021-02-12 15:33:18 +00:00
|
|
|
hass, config, service_write_register_schema, service_write_coil_schema
|
|
|
|
):
|
|
|
|
"""Set up Modbus component."""
|
|
|
|
|
2021-04-19 15:18:15 +00:00
|
|
|
hass.data[DOMAIN] = hub_collect = {}
|
2021-02-12 15:33:18 +00:00
|
|
|
for conf_hub in config[DOMAIN]:
|
2021-05-15 17:54:17 +00:00
|
|
|
my_hub = ModbusHub(hass, conf_hub)
|
|
|
|
hub_collect[conf_hub[CONF_NAME]] = my_hub
|
2021-02-12 15:33:18 +00:00
|
|
|
|
|
|
|
# modbus needs to be activated before components are loaded
|
|
|
|
# to avoid a racing problem
|
2021-06-05 12:39:09 +00:00
|
|
|
if not await my_hub.async_setup():
|
|
|
|
return False
|
2021-02-12 15:33:18 +00:00
|
|
|
|
|
|
|
# load platforms
|
2021-05-10 17:28:38 +00:00
|
|
|
for component, conf_key in PLATFORMS:
|
2021-02-12 15:33:18 +00:00
|
|
|
if conf_key in conf_hub:
|
2021-05-15 17:54:17 +00:00
|
|
|
hass.async_create_task(
|
|
|
|
async_load_platform(hass, component, DOMAIN, conf_hub, config)
|
|
|
|
)
|
2021-02-12 15:33:18 +00:00
|
|
|
|
2021-05-15 17:54:17 +00:00
|
|
|
async def async_stop_modbus(event):
|
2021-02-12 15:33:18 +00:00
|
|
|
"""Stop Modbus service."""
|
2021-04-19 15:18:15 +00:00
|
|
|
|
2021-02-12 15:33:18 +00:00
|
|
|
for client in hub_collect.values():
|
2021-05-15 17:54:17 +00:00
|
|
|
await client.async_close()
|
2021-04-19 15:18:15 +00:00
|
|
|
del client
|
2021-02-12 15:33:18 +00:00
|
|
|
|
2021-05-15 17:54:17 +00:00
|
|
|
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop_modbus)
|
|
|
|
|
|
|
|
async def async_write_register(service):
|
2021-02-12 15:33:18 +00:00
|
|
|
"""Write Modbus registers."""
|
|
|
|
unit = int(float(service.data[ATTR_UNIT]))
|
|
|
|
address = int(float(service.data[ATTR_ADDRESS]))
|
|
|
|
value = service.data[ATTR_VALUE]
|
2021-04-19 15:18:15 +00:00
|
|
|
client_name = (
|
|
|
|
service.data[ATTR_HUB] if ATTR_HUB in service.data else DEFAULT_HUB
|
|
|
|
)
|
2021-02-12 15:33:18 +00:00
|
|
|
if isinstance(value, list):
|
2021-05-19 09:39:53 +00:00
|
|
|
await hub_collect[client_name].async_pymodbus_call(
|
|
|
|
unit, address, [int(float(i)) for i in value], CALL_TYPE_WRITE_REGISTERS
|
2021-02-12 15:33:18 +00:00
|
|
|
)
|
|
|
|
else:
|
2021-05-19 09:39:53 +00:00
|
|
|
await hub_collect[client_name].async_pymodbus_call(
|
|
|
|
unit, address, int(float(value)), CALL_TYPE_WRITE_REGISTER
|
2021-05-15 17:54:17 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
hass.services.async_register(
|
|
|
|
DOMAIN,
|
|
|
|
SERVICE_WRITE_REGISTER,
|
|
|
|
async_write_register,
|
|
|
|
schema=service_write_register_schema,
|
|
|
|
)
|
2021-02-12 15:33:18 +00:00
|
|
|
|
2021-05-15 17:54:17 +00:00
|
|
|
async def async_write_coil(service):
|
2021-02-12 15:33:18 +00:00
|
|
|
"""Write Modbus coil."""
|
|
|
|
unit = service.data[ATTR_UNIT]
|
|
|
|
address = service.data[ATTR_ADDRESS]
|
|
|
|
state = service.data[ATTR_STATE]
|
2021-04-19 15:18:15 +00:00
|
|
|
client_name = (
|
|
|
|
service.data[ATTR_HUB] if ATTR_HUB in service.data else DEFAULT_HUB
|
|
|
|
)
|
2021-04-03 11:15:01 +00:00
|
|
|
if isinstance(state, list):
|
2021-05-19 09:39:53 +00:00
|
|
|
await hub_collect[client_name].async_pymodbus_call(
|
|
|
|
unit, address, state, CALL_TYPE_WRITE_COILS
|
|
|
|
)
|
2021-04-03 11:15:01 +00:00
|
|
|
else:
|
2021-05-19 09:39:53 +00:00
|
|
|
await hub_collect[client_name].async_pymodbus_call(
|
|
|
|
unit, address, state, CALL_TYPE_WRITE_COIL
|
|
|
|
)
|
2021-02-12 15:33:18 +00:00
|
|
|
|
2021-05-15 17:54:17 +00:00
|
|
|
hass.services.async_register(
|
|
|
|
DOMAIN, SERVICE_WRITE_COIL, async_write_coil, schema=service_write_coil_schema
|
2021-02-12 15:33:18 +00:00
|
|
|
)
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
class ModbusHub:
|
|
|
|
"""Thread safe wrapper class for pymodbus."""
|
|
|
|
|
2021-05-15 17:54:17 +00:00
|
|
|
def __init__(self, hass, client_config):
|
2021-02-12 15:33:18 +00:00
|
|
|
"""Initialize the Modbus hub."""
|
|
|
|
|
|
|
|
# generic configuration
|
|
|
|
self._client = None
|
2021-05-15 17:54:17 +00:00
|
|
|
self._async_cancel_listener = None
|
2021-04-19 15:18:15 +00:00
|
|
|
self._in_error = False
|
2021-05-15 17:54:17 +00:00
|
|
|
self._lock = asyncio.Lock()
|
|
|
|
self.hass = hass
|
2021-02-12 15:33:18 +00:00
|
|
|
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]
|
2021-05-08 11:28:35 +00:00
|
|
|
self._config_delay = client_config[CONF_DELAY]
|
2021-05-14 08:54:23 +00:00
|
|
|
self._config_reset_socket = client_config[CONF_CLOSE_COMM_ON_ERROR]
|
2021-05-10 17:28:38 +00:00
|
|
|
Defaults.Timeout = client_config[CONF_TIMEOUT]
|
2021-02-12 15:33:18 +00:00
|
|
|
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]
|
|
|
|
|
2021-05-17 09:20:12 +00:00
|
|
|
self._call_type = {
|
|
|
|
CALL_TYPE_COIL: {
|
|
|
|
ENTRY_ATTR: "bits",
|
|
|
|
ENTRY_FUNC: None,
|
|
|
|
},
|
|
|
|
CALL_TYPE_DISCRETE: {
|
|
|
|
ENTRY_ATTR: "bits",
|
|
|
|
ENTRY_FUNC: None,
|
|
|
|
},
|
|
|
|
CALL_TYPE_REGISTER_HOLDING: {
|
|
|
|
ENTRY_ATTR: "registers",
|
|
|
|
ENTRY_FUNC: None,
|
|
|
|
},
|
|
|
|
CALL_TYPE_REGISTER_INPUT: {
|
|
|
|
ENTRY_ATTR: "registers",
|
|
|
|
ENTRY_FUNC: None,
|
|
|
|
},
|
|
|
|
CALL_TYPE_WRITE_COIL: {
|
|
|
|
ENTRY_ATTR: "value",
|
|
|
|
ENTRY_FUNC: None,
|
|
|
|
},
|
|
|
|
CALL_TYPE_WRITE_COILS: {
|
|
|
|
ENTRY_ATTR: "count",
|
|
|
|
ENTRY_FUNC: None,
|
|
|
|
},
|
|
|
|
CALL_TYPE_WRITE_REGISTER: {
|
|
|
|
ENTRY_ATTR: "value",
|
|
|
|
ENTRY_FUNC: None,
|
|
|
|
},
|
|
|
|
CALL_TYPE_WRITE_REGISTERS: {
|
|
|
|
ENTRY_ATTR: "count",
|
|
|
|
ENTRY_FUNC: None,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
2021-06-05 12:39:09 +00:00
|
|
|
def _log_error(self, text: str, error_state=True):
|
|
|
|
log_text = f"Pymodbus: {text}"
|
2021-04-19 15:18:15 +00:00
|
|
|
if self._in_error:
|
2021-04-29 13:59:17 +00:00
|
|
|
_LOGGER.debug(log_text)
|
2021-04-19 15:18:15 +00:00
|
|
|
else:
|
2021-04-29 13:59:17 +00:00
|
|
|
_LOGGER.error(log_text)
|
2021-04-19 15:18:15 +00:00
|
|
|
self._in_error = error_state
|
|
|
|
|
2021-05-15 17:54:17 +00:00
|
|
|
async def async_setup(self):
|
2021-02-12 15:33:18 +00:00
|
|
|
"""Set up pymodbus client."""
|
2021-04-19 15:18:15 +00:00
|
|
|
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,
|
2021-05-14 08:54:23 +00:00
|
|
|
reset_socket=self._config_reset_socket,
|
2021-04-19 15:18:15 +00:00
|
|
|
)
|
|
|
|
elif self._config_type == "rtuovertcp":
|
|
|
|
self._client = ModbusTcpClient(
|
|
|
|
host=self._config_host,
|
|
|
|
port=self._config_port,
|
|
|
|
framer=ModbusRtuFramer,
|
|
|
|
timeout=self._config_timeout,
|
2021-05-14 08:54:23 +00:00
|
|
|
reset_socket=self._config_reset_socket,
|
2021-04-19 15:18:15 +00:00
|
|
|
)
|
|
|
|
elif self._config_type == "tcp":
|
|
|
|
self._client = ModbusTcpClient(
|
|
|
|
host=self._config_host,
|
|
|
|
port=self._config_port,
|
|
|
|
timeout=self._config_timeout,
|
2021-05-14 08:54:23 +00:00
|
|
|
reset_socket=self._config_reset_socket,
|
2021-04-19 15:18:15 +00:00
|
|
|
)
|
|
|
|
elif self._config_type == "udp":
|
|
|
|
self._client = ModbusUdpClient(
|
|
|
|
host=self._config_host,
|
|
|
|
port=self._config_port,
|
|
|
|
timeout=self._config_timeout,
|
2021-05-14 08:54:23 +00:00
|
|
|
reset_socket=self._config_reset_socket,
|
2021-04-19 15:18:15 +00:00
|
|
|
)
|
|
|
|
except ModbusException as exception_error:
|
2021-06-05 12:39:09 +00:00
|
|
|
self._log_error(str(exception_error), error_state=False)
|
|
|
|
return False
|
2021-02-12 15:33:18 +00:00
|
|
|
|
2021-05-15 17:54:17 +00:00
|
|
|
async with self._lock:
|
2021-06-05 12:39:09 +00:00
|
|
|
if not await self.hass.async_add_executor_job(self._pymodbus_connect):
|
|
|
|
self._log_error("initial connect failed, no retry", error_state=False)
|
|
|
|
return False
|
2021-02-12 15:33:18 +00:00
|
|
|
|
2021-05-17 09:20:12 +00:00
|
|
|
self._call_type[CALL_TYPE_COIL][ENTRY_FUNC] = self._client.read_coils
|
|
|
|
self._call_type[CALL_TYPE_DISCRETE][
|
|
|
|
ENTRY_FUNC
|
|
|
|
] = self._client.read_discrete_inputs
|
|
|
|
self._call_type[CALL_TYPE_REGISTER_HOLDING][
|
|
|
|
ENTRY_FUNC
|
|
|
|
] = self._client.read_holding_registers
|
|
|
|
self._call_type[CALL_TYPE_REGISTER_INPUT][
|
|
|
|
ENTRY_FUNC
|
|
|
|
] = self._client.read_input_registers
|
|
|
|
self._call_type[CALL_TYPE_WRITE_COIL][ENTRY_FUNC] = self._client.write_coil
|
|
|
|
self._call_type[CALL_TYPE_WRITE_COILS][ENTRY_FUNC] = self._client.write_coils
|
|
|
|
self._call_type[CALL_TYPE_WRITE_REGISTER][
|
|
|
|
ENTRY_FUNC
|
|
|
|
] = self._client.write_register
|
|
|
|
self._call_type[CALL_TYPE_WRITE_REGISTERS][
|
|
|
|
ENTRY_FUNC
|
|
|
|
] = self._client.write_registers
|
|
|
|
|
2021-05-08 11:28:35 +00:00
|
|
|
# Start counting down to allow modbus requests.
|
|
|
|
if self._config_delay:
|
2021-05-15 17:54:17 +00:00
|
|
|
self._async_cancel_listener = async_call_later(
|
|
|
|
self.hass, self._config_delay, self.async_end_delay
|
|
|
|
)
|
2021-06-05 12:39:09 +00:00
|
|
|
return True
|
2021-05-08 11:28:35 +00:00
|
|
|
|
2021-05-15 17:54:17 +00:00
|
|
|
@callback
|
|
|
|
def async_end_delay(self, args):
|
2021-05-08 11:28:35 +00:00
|
|
|
"""End startup delay."""
|
2021-05-15 17:54:17 +00:00
|
|
|
self._async_cancel_listener = None
|
2021-05-08 11:28:35 +00:00
|
|
|
self._config_delay = 0
|
|
|
|
|
2021-05-15 17:54:17 +00:00
|
|
|
def _pymodbus_close(self):
|
|
|
|
"""Close sync. pymodbus."""
|
|
|
|
if self._client:
|
2021-04-19 15:18:15 +00:00
|
|
|
try:
|
2021-05-15 17:54:17 +00:00
|
|
|
self._client.close()
|
2021-04-19 15:18:15 +00:00
|
|
|
except ModbusException as exception_error:
|
2021-06-05 12:39:09 +00:00
|
|
|
self._log_error(str(exception_error))
|
2021-05-15 17:54:17 +00:00
|
|
|
self._client = None
|
|
|
|
|
|
|
|
async def async_close(self):
|
|
|
|
"""Disconnect client."""
|
|
|
|
if self._async_cancel_listener:
|
|
|
|
self._async_cancel_listener()
|
|
|
|
self._async_cancel_listener = None
|
|
|
|
|
|
|
|
async with self._lock:
|
|
|
|
return await self.hass.async_add_executor_job(self._pymodbus_close)
|
2021-02-12 15:33:18 +00:00
|
|
|
|
2021-05-15 17:54:17 +00:00
|
|
|
def _pymodbus_connect(self):
|
2021-02-12 15:33:18 +00:00
|
|
|
"""Connect client."""
|
2021-05-15 17:54:17 +00:00
|
|
|
try:
|
2021-06-05 12:39:09 +00:00
|
|
|
return self._client.connect()
|
2021-05-15 17:54:17 +00:00
|
|
|
except ModbusException as exception_error:
|
2021-06-05 12:39:09 +00:00
|
|
|
self._log_error(str(exception_error), error_state=False)
|
|
|
|
return False
|
2021-02-12 15:33:18 +00:00
|
|
|
|
2021-05-17 09:20:12 +00:00
|
|
|
def _pymodbus_call(self, unit, address, value, use_call):
|
2021-05-15 17:54:17 +00:00
|
|
|
"""Call sync. pymodbus."""
|
|
|
|
kwargs = {"unit": unit} if unit else {}
|
|
|
|
try:
|
2021-05-17 09:20:12 +00:00
|
|
|
result = self._call_type[use_call][ENTRY_FUNC](address, value, **kwargs)
|
2021-05-15 17:54:17 +00:00
|
|
|
except ModbusException as exception_error:
|
2021-06-05 12:39:09 +00:00
|
|
|
self._log_error(str(exception_error))
|
2021-05-15 17:54:17 +00:00
|
|
|
result = exception_error
|
2021-05-17 09:20:12 +00:00
|
|
|
if not hasattr(result, self._call_type[use_call][ENTRY_ATTR]):
|
2021-06-05 12:39:09 +00:00
|
|
|
self._log_error(str(result))
|
2021-05-08 11:28:35 +00:00
|
|
|
return None
|
2021-05-15 17:54:17 +00:00
|
|
|
self._in_error = False
|
|
|
|
return result
|
|
|
|
|
2021-05-17 09:20:12 +00:00
|
|
|
async def async_pymodbus_call(self, unit, address, value, use_call):
|
2021-05-15 17:54:17 +00:00
|
|
|
"""Convert async to sync pymodbus call."""
|
2021-05-08 11:28:35 +00:00
|
|
|
if self._config_delay:
|
|
|
|
return None
|
2021-06-05 12:39:09 +00:00
|
|
|
if not self._client.is_socket_open():
|
|
|
|
return None
|
2021-05-15 17:54:17 +00:00
|
|
|
async with self._lock:
|
|
|
|
return await self.hass.async_add_executor_job(
|
2021-05-17 09:20:12 +00:00
|
|
|
self._pymodbus_call, unit, address, value, use_call
|
2021-05-15 17:54:17 +00:00
|
|
|
)
|