1 """Support for Modbus."""
3 from __future__
import annotations
6 from collections
import namedtuple
7 from collections.abc
import Callable
11 from pymodbus.client
import (
12 AsyncModbusSerialClient,
16 from pymodbus.exceptions
import ModbusException
17 from pymodbus.pdu
import ModbusResponse
18 from pymodbus.transaction
import ModbusAsciiFramer, ModbusRtuFramer, ModbusSocketFramer
19 import voluptuous
as vol
30 EVENT_HOMEASSISTANT_STOP,
48 CALL_TYPE_REGISTER_HOLDING,
49 CALL_TYPE_REGISTER_INPUT,
51 CALL_TYPE_WRITE_COILS,
52 CALL_TYPE_WRITE_REGISTER,
53 CALL_TYPE_WRITE_REGISTERS,
60 MODBUS_DOMAIN
as DOMAIN,
66 SERVICE_WRITE_REGISTER,
71 from .validators
import check_config
73 _LOGGER = logging.getLogger(__name__)
76 ConfEntry = namedtuple(
"ConfEntry",
"call_type attr func_name")
77 RunEntry = namedtuple(
"RunEntry",
"attr func")
87 "read_discrete_inputs",
90 CALL_TYPE_REGISTER_HOLDING,
92 "read_holding_registers",
95 CALL_TYPE_REGISTER_INPUT,
97 "read_input_registers",
100 CALL_TYPE_WRITE_COIL,
105 CALL_TYPE_WRITE_COILS,
110 CALL_TYPE_WRITE_REGISTER,
115 CALL_TYPE_WRITE_REGISTERS,
126 """Set up Modbus component."""
132 if not config[DOMAIN]:
134 if DOMAIN
in hass.data
and config[DOMAIN] == []:
135 hubs = hass.data[DOMAIN]
139 hub_collect = hass.data[DOMAIN]
141 hass.data[DOMAIN] = hub_collect = {}
143 for conf_hub
in config[DOMAIN]:
145 hub_collect[conf_hub[CONF_NAME]] = my_hub
149 if not await my_hub.async_setup():
153 for component, conf_key
in PLATFORMS:
154 if conf_key
in conf_hub:
155 hass.async_create_task(
159 async
def async_stop_modbus(event: Event) ->
None:
160 """Stop Modbus service."""
161 for client
in hub_collect.values():
162 await client.async_close()
164 hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop_modbus)
166 async
def async_write_register(service: ServiceCall) ->
None:
167 """Write Modbus registers."""
169 if ATTR_UNIT
in service.data:
170 slave =
int(
float(service.data[ATTR_UNIT]))
172 if ATTR_SLAVE
in service.data:
173 slave =
int(
float(service.data[ATTR_SLAVE]))
174 address =
int(
float(service.data[ATTR_ADDRESS]))
175 value = service.data[ATTR_VALUE]
176 hub = hub_collect[service.data.get(ATTR_HUB, DEFAULT_HUB)]
177 if isinstance(value, list):
178 await hub.async_pb_call(
182 CALL_TYPE_WRITE_REGISTERS,
185 await hub.async_pb_call(
186 slave, address,
int(
float(value)), CALL_TYPE_WRITE_REGISTER
189 async
def async_write_coil(service: ServiceCall) ->
None:
190 """Write Modbus coil."""
192 if ATTR_UNIT
in service.data:
193 slave =
int(
float(service.data[ATTR_UNIT]))
194 if ATTR_SLAVE
in service.data:
195 slave =
int(
float(service.data[ATTR_SLAVE]))
196 address = service.data[ATTR_ADDRESS]
197 state = service.data[ATTR_STATE]
198 hub = hub_collect[service.data.get(ATTR_HUB, DEFAULT_HUB)]
199 if isinstance(state, list):
200 await hub.async_pb_call(slave, address, state, CALL_TYPE_WRITE_COILS)
202 await hub.async_pb_call(slave, address, state, CALL_TYPE_WRITE_COIL)
205 (SERVICE_WRITE_REGISTER, async_write_register, ATTR_VALUE, cv.positive_int),
206 (SERVICE_WRITE_COIL, async_write_coil, ATTR_STATE, cv.boolean),
208 hass.services.async_register(
214 vol.Optional(ATTR_HUB, default=DEFAULT_HUB): cv.string,
215 vol.Exclusive(ATTR_SLAVE,
"unit"): cv.positive_int,
216 vol.Exclusive(ATTR_UNIT,
"unit"): cv.positive_int,
217 vol.Required(ATTR_ADDRESS): cv.positive_int,
218 vol.Required(x_write[2]): vol.Any(
219 cv.positive_int, vol.All(cv.ensure_list, [x_write[3]])
225 async
def async_stop_hub(service: ServiceCall) ->
None:
226 """Stop Modbus hub."""
228 hub = hub_collect[service.data[ATTR_HUB]]
229 await hub.async_close()
231 hass.services.async_register(
235 schema=vol.Schema({vol.Required(ATTR_HUB): cv.string}),
241 """Thread safe wrapper class for pymodbus."""
243 def __init__(self, hass: HomeAssistant, client_config: dict[str, Any]) ->
None:
244 """Initialize the Modbus hub."""
248 AsyncModbusSerialClient | AsyncModbusTcpClient | AsyncModbusUdpClient |
None
254 self.
namename = client_config[CONF_NAME]
257 self._pb_request: dict[str, RunEntry] = {}
259 SERIAL: AsyncModbusSerialClient,
260 TCP: AsyncModbusTcpClient,
261 UDP: AsyncModbusUdpClient,
262 RTUOVERTCP: AsyncModbusTcpClient,
265 "port": client_config[CONF_PORT],
266 "timeout": client_config[CONF_TIMEOUT],
268 "retry_on_empty":
True,
272 if client_config[CONF_METHOD] ==
"ascii":
273 self.
_pb_params_pb_params[
"framer"] = ModbusAsciiFramer
275 self.
_pb_params_pb_params[
"framer"] = ModbusRtuFramer
278 "baudrate": client_config[CONF_BAUDRATE],
279 "stopbits": client_config[CONF_STOPBITS],
280 "bytesize": client_config[CONF_BYTESIZE],
281 "parity": client_config[CONF_PARITY],
286 self.
_pb_params_pb_params[
"host"] = client_config[CONF_HOST]
288 self.
_pb_params_pb_params[
"framer"] = ModbusRtuFramer
290 self.
_pb_params_pb_params[
"framer"] = ModbusSocketFramer
292 if CONF_MSG_WAIT
in client_config:
293 self.
_msg_wait_msg_wait = client_config[CONF_MSG_WAIT] / 1000
299 def _log_error(self, text: str, error_state: bool =
True) ->
None:
300 log_text = f
"Pymodbus: {self.name}: {text}"
302 _LOGGER.debug(log_text)
304 _LOGGER.error(log_text)
308 """Connect to device, async."""
309 async
with self.
_lock_lock:
311 await self.
_client_client.connect()
312 except ModbusException
as exception_error:
313 err = f
"{self.name} connect failed, retry in pymodbus ({exception_error!s})"
314 self.
_log_error_log_error(err, error_state=
False)
316 message = f
"modbus {self.name} communication open"
317 _LOGGER.info(message)
320 """Set up pymodbus client."""
323 except ModbusException
as exception_error:
324 self.
_log_error_log_error(
str(exception_error), error_state=
False)
327 for entry
in PB_CALL:
328 func = getattr(self.
_client_client, entry.func_name)
329 self._pb_request[entry.call_type] =
RunEntry(entry.attr, func)
331 self.
hasshass.async_create_background_task(
344 """End startup delay."""
349 """Reconnect client."""
356 """Disconnect client."""
360 async
with self.
_lock_lock:
364 except ModbusException
as exception_error:
368 message = f
"modbus {self.name} communication closed"
369 _LOGGER.info(message)
372 self, slave: int |
None, address: int, value: int | list[int], use_call: str
373 ) -> ModbusResponse |
None:
374 """Call sync. pymodbus."""
375 kwargs = {
"slave": slave}
if slave
else {}
376 entry = self._pb_request[use_call]
378 result: ModbusResponse = await entry.func(address, value, **kwargs)
379 except ModbusException
as exception_error:
380 error = f
"Error: device: {slave} address: {address} -> {exception_error!s}"
385 f
"Error: device: {slave} address: {address} -> pymodbus returned None"
389 if not hasattr(result, entry.attr):
390 error = f
"Error: device: {slave} address: {address} -> {result!s}"
394 error = f
"Error: device: {slave} address: {address} -> pymodbus returned isError True"
404 value: int | list[int],
406 ) -> ModbusResponse |
None:
407 """Convert async to sync pymodbus call."""
410 async
with self.
_lock_lock:
413 result = await self.
low_level_pb_calllow_level_pb_call(unit, address, value, use_call)
416 await asyncio.sleep(self.
_msg_wait_msg_wait)
ModbusResponse|None low_level_pb_call(self, int|None slave, int address, int|list[int] value, str use_call)
None _log_error(self, str text, bool error_state=True)
ModbusResponse|None async_pb_call(self, int|None unit, int address, int|list[int] value, str use_call)
None async_end_delay(self, Any args)
None async_pb_connect(self)
None __init__(self, HomeAssistant hass, dict[str, Any] client_config)
IssData update(pyiss.ISS iss)
bool async_modbus_setup(HomeAssistant hass, ConfigType config)
dict check_config(HomeAssistant hass, dict config)
bool async_setup(HomeAssistant hass, ConfigType config)
None async_load_platform(core.HomeAssistant hass, Platform|str component, str platform, DiscoveryInfoType|None discovered, ConfigType hass_config)
None async_dispatcher_send(HomeAssistant hass, str signal, *Any args)
CALLBACK_TYPE async_call_later(HomeAssistant hass, float|timedelta delay, HassJob[[datetime], Coroutine[Any, Any, None]|None]|Callable[[datetime], Coroutine[Any, Any, None]|None] action)
None async_setup_reload_service(HomeAssistant hass, str domain, Iterable[str] platforms)