Home Assistant Unofficial Reference 2024.12.1
__init__.py
Go to the documentation of this file.
1 """Support for Modbus."""
2 
3 from __future__ import annotations
4 
5 import logging
6 from typing import cast
7 
8 import voluptuous as vol
9 
11  DEVICE_CLASSES_SCHEMA as BINARY_SENSOR_DEVICE_CLASSES_SCHEMA,
12 )
14  DEVICE_CLASSES_SCHEMA as COVER_DEVICE_CLASSES_SCHEMA,
15 )
17  CONF_STATE_CLASS,
18  DEVICE_CLASSES_SCHEMA as SENSOR_DEVICE_CLASSES_SCHEMA,
19  STATE_CLASSES_SCHEMA as SENSOR_STATE_CLASSES_SCHEMA,
20 )
22  DEVICE_CLASSES_SCHEMA as SWITCH_DEVICE_CLASSES_SCHEMA,
23 )
24 from homeassistant.const import (
25  CONF_ADDRESS,
26  CONF_BINARY_SENSORS,
27  CONF_COMMAND_OFF,
28  CONF_COMMAND_ON,
29  CONF_COUNT,
30  CONF_COVERS,
31  CONF_DELAY,
32  CONF_DEVICE_CLASS,
33  CONF_HOST,
34  CONF_LIGHTS,
35  CONF_METHOD,
36  CONF_NAME,
37  CONF_OFFSET,
38  CONF_PORT,
39  CONF_SCAN_INTERVAL,
40  CONF_SENSORS,
41  CONF_SLAVE,
42  CONF_STRUCTURE,
43  CONF_SWITCHES,
44  CONF_TEMPERATURE_UNIT,
45  CONF_TIMEOUT,
46  CONF_TYPE,
47  CONF_UNIQUE_ID,
48  CONF_UNIT_OF_MEASUREMENT,
49 )
50 from homeassistant.core import HomeAssistant
52 from homeassistant.helpers.typing import ConfigType
53 
54 from .const import (
55  CALL_TYPE_COIL,
56  CALL_TYPE_DISCRETE,
57  CALL_TYPE_REGISTER_HOLDING,
58  CALL_TYPE_REGISTER_INPUT,
59  CALL_TYPE_X_COILS,
60  CALL_TYPE_X_REGISTER_HOLDINGS,
61  CONF_BAUDRATE,
62  CONF_BYTESIZE,
63  CONF_CLIMATES,
64  CONF_DATA_TYPE,
65  CONF_DEVICE_ADDRESS,
66  CONF_FAN_MODE_AUTO,
67  CONF_FAN_MODE_DIFFUSE,
68  CONF_FAN_MODE_FOCUS,
69  CONF_FAN_MODE_HIGH,
70  CONF_FAN_MODE_LOW,
71  CONF_FAN_MODE_MEDIUM,
72  CONF_FAN_MODE_MIDDLE,
73  CONF_FAN_MODE_OFF,
74  CONF_FAN_MODE_ON,
75  CONF_FAN_MODE_REGISTER,
76  CONF_FAN_MODE_TOP,
77  CONF_FAN_MODE_VALUES,
78  CONF_FANS,
79  CONF_HVAC_MODE_AUTO,
80  CONF_HVAC_MODE_COOL,
81  CONF_HVAC_MODE_DRY,
82  CONF_HVAC_MODE_FAN_ONLY,
83  CONF_HVAC_MODE_HEAT,
84  CONF_HVAC_MODE_HEAT_COOL,
85  CONF_HVAC_MODE_OFF,
86  CONF_HVAC_MODE_REGISTER,
87  CONF_HVAC_MODE_VALUES,
88  CONF_HVAC_ONOFF_REGISTER,
89  CONF_INPUT_TYPE,
90  CONF_MAX_TEMP,
91  CONF_MAX_VALUE,
92  CONF_MIN_TEMP,
93  CONF_MIN_VALUE,
94  CONF_MSG_WAIT,
95  CONF_NAN_VALUE,
96  CONF_PARITY,
97  CONF_PRECISION,
98  CONF_SCALE,
99  CONF_SLAVE_COUNT,
100  CONF_STATE_CLOSED,
101  CONF_STATE_CLOSING,
102  CONF_STATE_OFF,
103  CONF_STATE_ON,
104  CONF_STATE_OPEN,
105  CONF_STATE_OPENING,
106  CONF_STATUS_REGISTER,
107  CONF_STATUS_REGISTER_TYPE,
108  CONF_STEP,
109  CONF_STOPBITS,
110  CONF_SWAP,
111  CONF_SWAP_BYTE,
112  CONF_SWAP_WORD,
113  CONF_SWAP_WORD_BYTE,
114  CONF_SWING_MODE_REGISTER,
115  CONF_SWING_MODE_SWING_BOTH,
116  CONF_SWING_MODE_SWING_HORIZ,
117  CONF_SWING_MODE_SWING_OFF,
118  CONF_SWING_MODE_SWING_ON,
119  CONF_SWING_MODE_SWING_VERT,
120  CONF_SWING_MODE_VALUES,
121  CONF_TARGET_TEMP,
122  CONF_TARGET_TEMP_WRITE_REGISTERS,
123  CONF_VERIFY,
124  CONF_VIRTUAL_COUNT,
125  CONF_WRITE_REGISTERS,
126  CONF_WRITE_TYPE,
127  CONF_ZERO_SUPPRESS,
128  DEFAULT_HUB,
129  DEFAULT_SCAN_INTERVAL,
130  DEFAULT_TEMP_UNIT,
131  MODBUS_DOMAIN as DOMAIN,
132  RTUOVERTCP,
133  SERIAL,
134  TCP,
135  UDP,
136  DataType,
137 )
138 from .modbus import ModbusHub, async_modbus_setup
139 from .validators import (
140  duplicate_fan_mode_validator,
141  duplicate_swing_mode_validator,
142  hvac_fixedsize_reglist_validator,
143  nan_validator,
144  register_int_list_validator,
145  struct_validator,
146 )
147 
148 _LOGGER = logging.getLogger(__name__)
149 
150 
151 BASE_SCHEMA = vol.Schema({vol.Optional(CONF_NAME, default=DEFAULT_HUB): cv.string})
152 
153 
154 BASE_COMPONENT_SCHEMA = vol.Schema(
155  {
156  vol.Required(CONF_NAME): cv.string,
157  vol.Required(CONF_ADDRESS): cv.positive_int,
158  vol.Exclusive(CONF_DEVICE_ADDRESS, "slave_addr"): cv.positive_int,
159  vol.Exclusive(CONF_SLAVE, "slave_addr"): cv.positive_int,
160  vol.Optional(
161  CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL
162  ): cv.positive_int,
163  vol.Optional(CONF_UNIQUE_ID): cv.string,
164  }
165 )
166 
167 
168 BASE_STRUCT_SCHEMA = BASE_COMPONENT_SCHEMA.extend(
169  {
170  vol.Optional(CONF_INPUT_TYPE, default=CALL_TYPE_REGISTER_HOLDING): vol.In(
171  [
172  CALL_TYPE_REGISTER_HOLDING,
173  CALL_TYPE_REGISTER_INPUT,
174  ]
175  ),
176  vol.Optional(CONF_COUNT): cv.positive_int,
177  vol.Optional(CONF_DATA_TYPE, default=DataType.INT16): vol.In(
178  [
179  DataType.INT16,
180  DataType.INT32,
181  DataType.INT64,
182  DataType.UINT16,
183  DataType.UINT32,
184  DataType.UINT64,
185  DataType.FLOAT16,
186  DataType.FLOAT32,
187  DataType.FLOAT64,
188  DataType.STRING,
189  DataType.CUSTOM,
190  ]
191  ),
192  vol.Optional(CONF_STRUCTURE): cv.string,
193  vol.Optional(CONF_SCALE, default=1): vol.Coerce(float),
194  vol.Optional(CONF_OFFSET, default=0): vol.Coerce(float),
195  vol.Optional(CONF_PRECISION): cv.positive_int,
196  vol.Optional(
197  CONF_SWAP,
198  ): vol.In(
199  [
200  CONF_SWAP_BYTE,
201  CONF_SWAP_WORD,
202  CONF_SWAP_WORD_BYTE,
203  ]
204  ),
205  }
206 )
207 
208 
209 BASE_SWITCH_SCHEMA = BASE_COMPONENT_SCHEMA.extend(
210  {
211  vol.Optional(CONF_WRITE_TYPE, default=CALL_TYPE_REGISTER_HOLDING): vol.In(
212  [
213  CALL_TYPE_REGISTER_HOLDING,
214  CALL_TYPE_COIL,
215  CALL_TYPE_X_COILS,
216  CALL_TYPE_X_REGISTER_HOLDINGS,
217  ]
218  ),
219  vol.Optional(CONF_COMMAND_OFF, default=0x00): cv.positive_int,
220  vol.Optional(CONF_COMMAND_ON, default=0x01): cv.positive_int,
221  vol.Optional(CONF_VERIFY): vol.Maybe(
222  {
223  vol.Optional(CONF_ADDRESS): cv.positive_int,
224  vol.Optional(CONF_INPUT_TYPE): vol.In(
225  [
226  CALL_TYPE_REGISTER_HOLDING,
227  CALL_TYPE_DISCRETE,
228  CALL_TYPE_REGISTER_INPUT,
229  CALL_TYPE_COIL,
230  CALL_TYPE_X_COILS,
231  CALL_TYPE_X_REGISTER_HOLDINGS,
232  ]
233  ),
234  vol.Optional(CONF_STATE_OFF): vol.All(
235  cv.ensure_list, [cv.positive_int]
236  ),
237  vol.Optional(CONF_STATE_ON): vol.All(cv.ensure_list, [cv.positive_int]),
238  vol.Optional(CONF_DELAY, default=0): cv.positive_int,
239  }
240  ),
241  }
242 )
243 
244 
245 CLIMATE_SCHEMA = vol.All(
246  BASE_STRUCT_SCHEMA.extend(
247  {
248  vol.Required(CONF_TARGET_TEMP): hvac_fixedsize_reglist_validator,
249  vol.Optional(CONF_TARGET_TEMP_WRITE_REGISTERS, default=False): cv.boolean,
250  vol.Optional(CONF_MAX_TEMP, default=35): vol.Coerce(float),
251  vol.Optional(CONF_MIN_TEMP, default=5): vol.Coerce(float),
252  vol.Optional(CONF_STEP, default=0.5): vol.Coerce(float),
253  vol.Optional(CONF_TEMPERATURE_UNIT, default=DEFAULT_TEMP_UNIT): cv.string,
254  vol.Optional(CONF_HVAC_ONOFF_REGISTER): cv.positive_int,
255  vol.Optional(CONF_WRITE_REGISTERS, default=False): cv.boolean,
256  vol.Optional(CONF_HVAC_MODE_REGISTER): vol.Maybe(
257  {
258  CONF_ADDRESS: cv.positive_int,
259  CONF_HVAC_MODE_VALUES: {
260  vol.Optional(CONF_HVAC_MODE_OFF): vol.Any(
261  cv.positive_int, [cv.positive_int]
262  ),
263  vol.Optional(CONF_HVAC_MODE_HEAT): vol.Any(
264  cv.positive_int, [cv.positive_int]
265  ),
266  vol.Optional(CONF_HVAC_MODE_COOL): vol.Any(
267  cv.positive_int, [cv.positive_int]
268  ),
269  vol.Optional(CONF_HVAC_MODE_HEAT_COOL): vol.Any(
270  cv.positive_int, [cv.positive_int]
271  ),
272  vol.Optional(CONF_HVAC_MODE_AUTO): vol.Any(
273  cv.positive_int, [cv.positive_int]
274  ),
275  vol.Optional(CONF_HVAC_MODE_DRY): vol.Any(
276  cv.positive_int, [cv.positive_int]
277  ),
278  vol.Optional(CONF_HVAC_MODE_FAN_ONLY): vol.Any(
279  cv.positive_int, [cv.positive_int]
280  ),
281  },
282  vol.Optional(CONF_WRITE_REGISTERS, default=False): cv.boolean,
283  }
284  ),
285  vol.Optional(CONF_FAN_MODE_REGISTER): vol.Maybe(
286  vol.All(
287  {
288  vol.Required(CONF_ADDRESS): register_int_list_validator,
289  CONF_FAN_MODE_VALUES: {
290  vol.Optional(CONF_FAN_MODE_ON): cv.positive_int,
291  vol.Optional(CONF_FAN_MODE_OFF): cv.positive_int,
292  vol.Optional(CONF_FAN_MODE_AUTO): cv.positive_int,
293  vol.Optional(CONF_FAN_MODE_LOW): cv.positive_int,
294  vol.Optional(CONF_FAN_MODE_MEDIUM): cv.positive_int,
295  vol.Optional(CONF_FAN_MODE_HIGH): cv.positive_int,
296  vol.Optional(CONF_FAN_MODE_TOP): cv.positive_int,
297  vol.Optional(CONF_FAN_MODE_MIDDLE): cv.positive_int,
298  vol.Optional(CONF_FAN_MODE_FOCUS): cv.positive_int,
299  vol.Optional(CONF_FAN_MODE_DIFFUSE): cv.positive_int,
300  },
301  },
302  duplicate_fan_mode_validator,
303  ),
304  ),
305  vol.Optional(CONF_SWING_MODE_REGISTER): vol.Maybe(
306  vol.All(
307  {
308  vol.Required(CONF_ADDRESS): register_int_list_validator,
309  CONF_SWING_MODE_VALUES: {
310  vol.Optional(CONF_SWING_MODE_SWING_ON): cv.positive_int,
311  vol.Optional(CONF_SWING_MODE_SWING_OFF): cv.positive_int,
312  vol.Optional(CONF_SWING_MODE_SWING_HORIZ): cv.positive_int,
313  vol.Optional(CONF_SWING_MODE_SWING_VERT): cv.positive_int,
314  vol.Optional(CONF_SWING_MODE_SWING_BOTH): cv.positive_int,
315  },
316  },
317  duplicate_swing_mode_validator,
318  )
319  ),
320  },
321  ),
322 )
323 
324 COVERS_SCHEMA = BASE_COMPONENT_SCHEMA.extend(
325  {
326  vol.Optional(
327  CONF_INPUT_TYPE,
328  default=CALL_TYPE_REGISTER_HOLDING,
329  ): vol.In(
330  [
331  CALL_TYPE_REGISTER_HOLDING,
332  CALL_TYPE_COIL,
333  ]
334  ),
335  vol.Optional(CONF_DEVICE_CLASS): COVER_DEVICE_CLASSES_SCHEMA,
336  vol.Optional(CONF_STATE_CLOSED, default=0): cv.positive_int,
337  vol.Optional(CONF_STATE_CLOSING, default=3): cv.positive_int,
338  vol.Optional(CONF_STATE_OPEN, default=1): cv.positive_int,
339  vol.Optional(CONF_STATE_OPENING, default=2): cv.positive_int,
340  vol.Optional(CONF_STATUS_REGISTER): cv.positive_int,
341  vol.Optional(
342  CONF_STATUS_REGISTER_TYPE,
343  default=CALL_TYPE_REGISTER_HOLDING,
344  ): vol.In([CALL_TYPE_REGISTER_HOLDING, CALL_TYPE_REGISTER_INPUT]),
345  }
346 )
347 
348 SWITCH_SCHEMA = BASE_SWITCH_SCHEMA.extend(
349  {
350  vol.Optional(CONF_DEVICE_CLASS): SWITCH_DEVICE_CLASSES_SCHEMA,
351  }
352 )
353 
354 LIGHT_SCHEMA = BASE_SWITCH_SCHEMA.extend({})
355 
356 FAN_SCHEMA = BASE_SWITCH_SCHEMA.extend({})
357 
358 SENSOR_SCHEMA = vol.All(
359  BASE_STRUCT_SCHEMA.extend(
360  {
361  vol.Optional(CONF_DEVICE_CLASS): SENSOR_DEVICE_CLASSES_SCHEMA,
362  vol.Optional(CONF_STATE_CLASS): SENSOR_STATE_CLASSES_SCHEMA,
363  vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
364  vol.Exclusive(CONF_VIRTUAL_COUNT, "vir_sen_count"): cv.positive_int,
365  vol.Exclusive(CONF_SLAVE_COUNT, "vir_sen_count"): cv.positive_int,
366  vol.Optional(CONF_MIN_VALUE): vol.Coerce(float),
367  vol.Optional(CONF_MAX_VALUE): vol.Coerce(float),
368  vol.Optional(CONF_NAN_VALUE): nan_validator,
369  vol.Optional(CONF_ZERO_SUPPRESS): cv.positive_float,
370  }
371  ),
372 )
373 
374 BINARY_SENSOR_SCHEMA = BASE_COMPONENT_SCHEMA.extend(
375  {
376  vol.Optional(CONF_DEVICE_CLASS): BINARY_SENSOR_DEVICE_CLASSES_SCHEMA,
377  vol.Optional(CONF_INPUT_TYPE, default=CALL_TYPE_COIL): vol.In(
378  [
379  CALL_TYPE_COIL,
380  CALL_TYPE_DISCRETE,
381  CALL_TYPE_REGISTER_HOLDING,
382  CALL_TYPE_REGISTER_INPUT,
383  ]
384  ),
385  vol.Exclusive(CONF_VIRTUAL_COUNT, "vir_bin_count"): cv.positive_int,
386  vol.Exclusive(CONF_SLAVE_COUNT, "vir_bin_count"): cv.positive_int,
387  }
388 )
389 
390 MODBUS_SCHEMA = vol.Schema(
391  {
392  vol.Optional(CONF_NAME, default=DEFAULT_HUB): cv.string,
393  vol.Optional(CONF_TIMEOUT, default=3): cv.socket_timeout,
394  vol.Optional(CONF_DELAY, default=0): cv.positive_int,
395  vol.Optional(CONF_MSG_WAIT): cv.positive_int,
396  vol.Optional(CONF_BINARY_SENSORS): vol.All(
397  cv.ensure_list, [BINARY_SENSOR_SCHEMA]
398  ),
399  vol.Optional(CONF_CLIMATES): vol.All(
400  cv.ensure_list, [vol.All(CLIMATE_SCHEMA, struct_validator)]
401  ),
402  vol.Optional(CONF_COVERS): vol.All(cv.ensure_list, [COVERS_SCHEMA]),
403  vol.Optional(CONF_LIGHTS): vol.All(cv.ensure_list, [LIGHT_SCHEMA]),
404  vol.Optional(CONF_SENSORS): vol.All(
405  cv.ensure_list, [vol.All(SENSOR_SCHEMA, struct_validator)]
406  ),
407  vol.Optional(CONF_SWITCHES): vol.All(cv.ensure_list, [SWITCH_SCHEMA]),
408  vol.Optional(CONF_FANS): vol.All(cv.ensure_list, [FAN_SCHEMA]),
409  }
410 )
411 
412 SERIAL_SCHEMA = MODBUS_SCHEMA.extend(
413  {
414  vol.Required(CONF_TYPE): SERIAL,
415  vol.Required(CONF_BAUDRATE): cv.positive_int,
416  vol.Required(CONF_BYTESIZE): vol.Any(5, 6, 7, 8),
417  vol.Required(CONF_METHOD): vol.Any("rtu", "ascii"),
418  vol.Required(CONF_PORT): cv.string,
419  vol.Required(CONF_PARITY): vol.Any("E", "O", "N"),
420  vol.Required(CONF_STOPBITS): vol.Any(1, 2),
421  }
422 )
423 
424 ETHERNET_SCHEMA = MODBUS_SCHEMA.extend(
425  {
426  vol.Required(CONF_HOST): cv.string,
427  vol.Required(CONF_PORT): cv.port,
428  vol.Required(CONF_TYPE): vol.Any(TCP, UDP, RTUOVERTCP),
429  }
430 )
431 
432 CONFIG_SCHEMA = vol.Schema(
433  {
434  DOMAIN: vol.All(
435  cv.ensure_list,
436  [
437  vol.Any(SERIAL_SCHEMA, ETHERNET_SCHEMA),
438  ],
439  ),
440  },
441  extra=vol.ALLOW_EXTRA,
442 )
443 
444 
445 def get_hub(hass: HomeAssistant, name: str) -> ModbusHub:
446  """Return modbus hub with name."""
447  return cast(ModbusHub, hass.data[DOMAIN][name])
448 
449 
450 async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
451  """Set up Modbus component."""
452  if DOMAIN not in config:
453  return True
454  return await async_modbus_setup(
455  hass,
456  config,
457  )
458 
459 
460 async def async_reset_platform(hass: HomeAssistant, integration_name: str) -> None:
461  """Release modbus resources."""
462  if DOMAIN not in hass.data:
463  _LOGGER.error("Modbus cannot reload, because it was never loaded")
464  return
465  _LOGGER.debug("Modbus reloading")
466  hubs = hass.data[DOMAIN]
467  for name in hubs:
468  await hubs[name].async_close()
bool async_modbus_setup(HomeAssistant hass, ConfigType config)
Definition: modbus.py:125
ModbusHub get_hub(HomeAssistant hass, str name)
Definition: __init__.py:445
None async_reset_platform(HomeAssistant hass, str integration_name)
Definition: __init__.py:460
bool async_setup(HomeAssistant hass, ConfigType config)
Definition: __init__.py:450