2021-02-12 15:33:18 +00:00
|
|
|
"""Support for Modbus."""
|
|
|
|
import logging
|
|
|
|
import threading
|
|
|
|
|
|
|
|
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,
|
|
|
|
)
|
|
|
|
from homeassistant.helpers.discovery import load_platform
|
2021-05-09 17:50:23 +00:00
|
|
|
from homeassistant.helpers.event import 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,
|
|
|
|
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,
|
|
|
|
)
|
|
|
|
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
def modbus_setup(
|
|
|
|
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]:
|
|
|
|
hub_collect[conf_hub[CONF_NAME]] = ModbusHub(conf_hub)
|
|
|
|
|
|
|
|
# modbus needs to be activated before components are loaded
|
|
|
|
# to avoid a racing problem
|
2021-05-08 11:28:35 +00:00
|
|
|
hub_collect[conf_hub[CONF_NAME]].setup(hass)
|
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:
|
|
|
|
load_platform(hass, component, DOMAIN, conf_hub, config)
|
|
|
|
|
|
|
|
def stop_modbus(event):
|
|
|
|
"""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():
|
|
|
|
client.close()
|
2021-04-19 15:18:15 +00:00
|
|
|
del client
|
2021-02-12 15:33:18 +00:00
|
|
|
|
|
|
|
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]
|
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):
|
|
|
|
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]
|
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):
|
|
|
|
hub_collect[client_name].write_coils(unit, address, state)
|
|
|
|
else:
|
|
|
|
hub_collect[client_name].write_coil(unit, address, state)
|
2021-02-12 15:33:18 +00:00
|
|
|
|
|
|
|
# register function to gracefully stop modbus
|
2021-05-09 17:50:23 +00:00
|
|
|
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_modbus)
|
2021-02-12 15:33:18 +00:00
|
|
|
|
|
|
|
# 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
|
2021-05-08 11:28:35 +00:00
|
|
|
self._cancel_listener = None
|
2021-04-19 15:18:15 +00:00
|
|
|
self._in_error = False
|
2021-02-12 15:33:18 +00:00
|
|
|
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]
|
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]
|
|
|
|
|
|
|
|
@property
|
|
|
|
def name(self):
|
|
|
|
"""Return the name of this hub."""
|
|
|
|
return self._config_name
|
|
|
|
|
2021-04-19 15:18:15 +00:00
|
|
|
def _log_error(self, exception_error: ModbusException, error_state=True):
|
2021-04-29 13:59:17 +00:00
|
|
|
log_text = "Pymodbus: " + str(exception_error)
|
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-08 11:28:35 +00:00
|
|
|
def setup(self, hass):
|
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:
|
|
|
|
self._log_error(exception_error, error_state=False)
|
|
|
|
return
|
2021-02-12 15:33:18 +00:00
|
|
|
|
|
|
|
# Connect device
|
|
|
|
self.connect()
|
|
|
|
|
2021-05-08 11:28:35 +00:00
|
|
|
# Start counting down to allow modbus requests.
|
|
|
|
if self._config_delay:
|
2021-05-09 17:50:23 +00:00
|
|
|
self._cancel_listener = call_later(hass, self._config_delay, self.end_delay)
|
2021-05-08 11:28:35 +00:00
|
|
|
|
|
|
|
def end_delay(self, args):
|
|
|
|
"""End startup delay."""
|
|
|
|
self._cancel_listener = None
|
|
|
|
self._config_delay = 0
|
|
|
|
|
2021-02-12 15:33:18 +00:00
|
|
|
def close(self):
|
|
|
|
"""Disconnect client."""
|
2021-05-08 11:28:35 +00:00
|
|
|
if self._cancel_listener:
|
|
|
|
self._cancel_listener()
|
|
|
|
self._cancel_listener = None
|
2021-02-12 15:33:18 +00:00
|
|
|
with self._lock:
|
2021-04-19 15:18:15 +00:00
|
|
|
try:
|
2021-04-21 09:46:40 +00:00
|
|
|
if self._client:
|
|
|
|
self._client.close()
|
|
|
|
self._client = None
|
2021-04-19 15:18:15 +00:00
|
|
|
except ModbusException as exception_error:
|
2021-04-21 09:46:40 +00:00
|
|
|
self._log_error(exception_error)
|
2021-04-19 15:18:15 +00:00
|
|
|
return
|
2021-02-12 15:33:18 +00:00
|
|
|
|
|
|
|
def connect(self):
|
|
|
|
"""Connect client."""
|
|
|
|
with self._lock:
|
2021-04-19 15:18:15 +00:00
|
|
|
try:
|
|
|
|
self._client.connect()
|
|
|
|
except ModbusException as exception_error:
|
|
|
|
self._log_error(exception_error, error_state=False)
|
|
|
|
return
|
2021-02-12 15:33:18 +00:00
|
|
|
|
|
|
|
def read_coils(self, unit, address, count):
|
|
|
|
"""Read coils."""
|
2021-05-08 11:28:35 +00:00
|
|
|
if self._config_delay:
|
|
|
|
return None
|
2021-02-12 15:33:18 +00:00
|
|
|
with self._lock:
|
|
|
|
kwargs = {"unit": unit} if unit else {}
|
2021-04-19 15:18:15 +00:00
|
|
|
try:
|
|
|
|
result = self._client.read_coils(address, count, **kwargs)
|
|
|
|
except ModbusException as exception_error:
|
|
|
|
self._log_error(exception_error)
|
2021-05-01 22:03:52 +00:00
|
|
|
result = exception_error
|
2021-05-08 11:26:31 +00:00
|
|
|
if not hasattr(result, "bits"):
|
2021-04-29 13:59:17 +00:00
|
|
|
self._log_error(result)
|
|
|
|
return None
|
2021-04-19 15:18:15 +00:00
|
|
|
self._in_error = False
|
|
|
|
return result
|
2021-02-12 15:33:18 +00:00
|
|
|
|
|
|
|
def read_discrete_inputs(self, unit, address, count):
|
|
|
|
"""Read discrete inputs."""
|
2021-05-08 11:28:35 +00:00
|
|
|
if self._config_delay:
|
|
|
|
return None
|
2021-02-12 15:33:18 +00:00
|
|
|
with self._lock:
|
|
|
|
kwargs = {"unit": unit} if unit else {}
|
2021-04-19 15:18:15 +00:00
|
|
|
try:
|
|
|
|
result = self._client.read_discrete_inputs(address, count, **kwargs)
|
|
|
|
except ModbusException as exception_error:
|
2021-05-01 22:03:52 +00:00
|
|
|
result = exception_error
|
2021-05-08 11:26:31 +00:00
|
|
|
if not hasattr(result, "bits"):
|
2021-04-29 13:59:17 +00:00
|
|
|
self._log_error(result)
|
|
|
|
return None
|
2021-04-19 15:18:15 +00:00
|
|
|
self._in_error = False
|
|
|
|
return result
|
2021-02-12 15:33:18 +00:00
|
|
|
|
|
|
|
def read_input_registers(self, unit, address, count):
|
|
|
|
"""Read input registers."""
|
2021-05-08 11:28:35 +00:00
|
|
|
if self._config_delay:
|
|
|
|
return None
|
2021-02-12 15:33:18 +00:00
|
|
|
with self._lock:
|
|
|
|
kwargs = {"unit": unit} if unit else {}
|
2021-04-19 15:18:15 +00:00
|
|
|
try:
|
|
|
|
result = self._client.read_input_registers(address, count, **kwargs)
|
|
|
|
except ModbusException as exception_error:
|
2021-05-01 22:03:52 +00:00
|
|
|
result = exception_error
|
|
|
|
if not hasattr(result, "registers"):
|
2021-04-29 13:59:17 +00:00
|
|
|
self._log_error(result)
|
|
|
|
return None
|
2021-04-19 15:18:15 +00:00
|
|
|
self._in_error = False
|
|
|
|
return result
|
2021-02-12 15:33:18 +00:00
|
|
|
|
|
|
|
def read_holding_registers(self, unit, address, count):
|
|
|
|
"""Read holding registers."""
|
2021-05-08 11:28:35 +00:00
|
|
|
if self._config_delay:
|
|
|
|
return None
|
2021-02-12 15:33:18 +00:00
|
|
|
with self._lock:
|
|
|
|
kwargs = {"unit": unit} if unit else {}
|
2021-04-19 15:18:15 +00:00
|
|
|
try:
|
|
|
|
result = self._client.read_holding_registers(address, count, **kwargs)
|
|
|
|
except ModbusException as exception_error:
|
2021-05-01 22:03:52 +00:00
|
|
|
result = exception_error
|
|
|
|
if not hasattr(result, "registers"):
|
2021-04-29 13:59:17 +00:00
|
|
|
self._log_error(result)
|
|
|
|
return None
|
2021-04-19 15:18:15 +00:00
|
|
|
self._in_error = False
|
|
|
|
return result
|
|
|
|
|
|
|
|
def write_coil(self, unit, address, value) -> bool:
|
2021-02-12 15:33:18 +00:00
|
|
|
"""Write coil."""
|
2021-05-08 11:28:35 +00:00
|
|
|
if self._config_delay:
|
|
|
|
return False
|
2021-02-12 15:33:18 +00:00
|
|
|
with self._lock:
|
|
|
|
kwargs = {"unit": unit} if unit else {}
|
2021-04-19 15:18:15 +00:00
|
|
|
try:
|
2021-04-29 13:59:17 +00:00
|
|
|
result = self._client.write_coil(address, value, **kwargs)
|
2021-04-19 15:18:15 +00:00
|
|
|
except ModbusException as exception_error:
|
2021-05-01 22:03:52 +00:00
|
|
|
result = exception_error
|
2021-05-08 11:26:31 +00:00
|
|
|
if not hasattr(result, "value"):
|
2021-04-29 13:59:17 +00:00
|
|
|
self._log_error(result)
|
|
|
|
return False
|
2021-04-19 15:18:15 +00:00
|
|
|
self._in_error = False
|
|
|
|
return True
|
|
|
|
|
|
|
|
def write_coils(self, unit, address, values) -> bool:
|
2021-04-04 19:53:52 +00:00
|
|
|
"""Write coil."""
|
2021-05-08 11:28:35 +00:00
|
|
|
if self._config_delay:
|
|
|
|
return False
|
2021-04-04 19:53:52 +00:00
|
|
|
with self._lock:
|
|
|
|
kwargs = {"unit": unit} if unit else {}
|
2021-04-19 15:18:15 +00:00
|
|
|
try:
|
2021-04-29 13:59:17 +00:00
|
|
|
result = self._client.write_coils(address, values, **kwargs)
|
2021-04-19 15:18:15 +00:00
|
|
|
except ModbusException as exception_error:
|
2021-05-01 22:03:52 +00:00
|
|
|
result = exception_error
|
2021-05-08 11:26:31 +00:00
|
|
|
if not hasattr(result, "count"):
|
2021-04-29 13:59:17 +00:00
|
|
|
self._log_error(result)
|
|
|
|
return False
|
2021-04-19 15:18:15 +00:00
|
|
|
self._in_error = False
|
|
|
|
return True
|
|
|
|
|
|
|
|
def write_register(self, unit, address, value) -> bool:
|
2021-02-12 15:33:18 +00:00
|
|
|
"""Write register."""
|
2021-05-08 11:28:35 +00:00
|
|
|
if self._config_delay:
|
|
|
|
return False
|
2021-02-12 15:33:18 +00:00
|
|
|
with self._lock:
|
|
|
|
kwargs = {"unit": unit} if unit else {}
|
2021-04-19 15:18:15 +00:00
|
|
|
try:
|
2021-04-29 13:59:17 +00:00
|
|
|
result = self._client.write_register(address, value, **kwargs)
|
2021-04-19 15:18:15 +00:00
|
|
|
except ModbusException as exception_error:
|
2021-05-01 22:03:52 +00:00
|
|
|
result = exception_error
|
2021-05-08 11:26:31 +00:00
|
|
|
if not hasattr(result, "value"):
|
2021-04-29 13:59:17 +00:00
|
|
|
self._log_error(result)
|
|
|
|
return False
|
2021-04-19 15:18:15 +00:00
|
|
|
self._in_error = False
|
|
|
|
return True
|
|
|
|
|
|
|
|
def write_registers(self, unit, address, values) -> bool:
|
2021-02-12 15:33:18 +00:00
|
|
|
"""Write registers."""
|
2021-05-08 11:28:35 +00:00
|
|
|
if self._config_delay:
|
|
|
|
return False
|
2021-02-12 15:33:18 +00:00
|
|
|
with self._lock:
|
|
|
|
kwargs = {"unit": unit} if unit else {}
|
2021-04-19 15:18:15 +00:00
|
|
|
try:
|
2021-04-29 13:59:17 +00:00
|
|
|
result = self._client.write_registers(address, values, **kwargs)
|
2021-04-19 15:18:15 +00:00
|
|
|
except ModbusException as exception_error:
|
2021-05-01 22:03:52 +00:00
|
|
|
result = exception_error
|
2021-05-08 11:26:31 +00:00
|
|
|
if not hasattr(result, "count"):
|
2021-04-29 13:59:17 +00:00
|
|
|
self._log_error(result)
|
|
|
|
return False
|
2021-04-19 15:18:15 +00:00
|
|
|
self._in_error = False
|
|
|
|
return True
|