core/homeassistant/components/modbus/__init__.py

269 lines
8.5 KiB
Python

"""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.const import (
ATTR_STATE,
CONF_DELAY,
CONF_HOST,
CONF_METHOD,
CONF_NAME,
CONF_PORT,
CONF_TIMEOUT,
CONF_TYPE,
EVENT_HOMEASSISTANT_STOP,
)
import homeassistant.helpers.config_validation as cv
from .const import (
ATTR_ADDRESS,
ATTR_HUB,
ATTR_UNIT,
ATTR_VALUE,
CONF_BAUDRATE,
CONF_BYTESIZE,
CONF_PARITY,
CONF_STOPBITS,
DEFAULT_HUB,
MODBUS_DOMAIN as DOMAIN,
SERVICE_WRITE_COIL,
SERVICE_WRITE_REGISTER,
)
_LOGGER = logging.getLogger(__name__)
BASE_SCHEMA = vol.Schema({vol.Optional(CONF_NAME, default=DEFAULT_HUB): cv.string})
SERIAL_SCHEMA = BASE_SCHEMA.extend(
{
vol.Required(CONF_BAUDRATE): cv.positive_int,
vol.Required(CONF_BYTESIZE): vol.Any(5, 6, 7, 8),
vol.Required(CONF_METHOD): vol.Any("rtu", "ascii"),
vol.Required(CONF_PORT): cv.string,
vol.Required(CONF_PARITY): vol.Any("E", "O", "N"),
vol.Required(CONF_STOPBITS): vol.Any(1, 2),
vol.Required(CONF_TYPE): "serial",
vol.Optional(CONF_TIMEOUT, default=3): cv.socket_timeout,
}
)
ETHERNET_SCHEMA = BASE_SCHEMA.extend(
{
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_PORT): cv.port,
vol.Required(CONF_TYPE): vol.Any("tcp", "udp", "rtuovertcp"),
vol.Optional(CONF_TIMEOUT, default=3): cv.socket_timeout,
vol.Optional(CONF_DELAY, default=0): cv.positive_int,
}
)
CONFIG_SCHEMA = vol.Schema(
{DOMAIN: vol.All(cv.ensure_list, [vol.Any(SERIAL_SCHEMA, ETHERNET_SCHEMA)])},
extra=vol.ALLOW_EXTRA,
)
SERVICE_WRITE_REGISTER_SCHEMA = vol.Schema(
{
vol.Optional(ATTR_HUB, default=DEFAULT_HUB): cv.string,
vol.Required(ATTR_UNIT): cv.positive_int,
vol.Required(ATTR_ADDRESS): cv.positive_int,
vol.Required(ATTR_VALUE): vol.Any(
cv.positive_int, vol.All(cv.ensure_list, [cv.positive_int])
),
}
)
SERVICE_WRITE_COIL_SCHEMA = vol.Schema(
{
vol.Optional(ATTR_HUB, default=DEFAULT_HUB): cv.string,
vol.Required(ATTR_UNIT): cv.positive_int,
vol.Required(ATTR_ADDRESS): cv.positive_int,
vol.Required(ATTR_STATE): cv.boolean,
}
)
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)
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,
)
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)