Home Assistant Unofficial Reference 2024.12.1
climate.py
Go to the documentation of this file.
1 """Support for Generic Modbus Thermostats."""
2 
3 from __future__ import annotations
4 
5 from datetime import datetime
6 import logging
7 import struct
8 from typing import Any, cast
9 
11  FAN_AUTO,
12  FAN_DIFFUSE,
13  FAN_FOCUS,
14  FAN_HIGH,
15  FAN_LOW,
16  FAN_MEDIUM,
17  FAN_MIDDLE,
18  FAN_OFF,
19  FAN_ON,
20  FAN_TOP,
21  SWING_BOTH,
22  SWING_HORIZONTAL,
23  SWING_OFF,
24  SWING_ON,
25  SWING_VERTICAL,
26  ClimateEntity,
27  ClimateEntityFeature,
28  HVACMode,
29 )
30 from homeassistant.const import (
31  ATTR_TEMPERATURE,
32  CONF_ADDRESS,
33  CONF_NAME,
34  CONF_TEMPERATURE_UNIT,
35  PRECISION_TENTHS,
36  PRECISION_WHOLE,
37  STATE_UNKNOWN,
38  UnitOfTemperature,
39 )
40 from homeassistant.core import HomeAssistant
41 from homeassistant.helpers.entity_platform import AddEntitiesCallback
42 from homeassistant.helpers.restore_state import RestoreEntity
43 from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
44 
45 from . import get_hub
46 from .const import (
47  CALL_TYPE_REGISTER_HOLDING,
48  CALL_TYPE_WRITE_REGISTER,
49  CALL_TYPE_WRITE_REGISTERS,
50  CONF_CLIMATES,
51  CONF_FAN_MODE_AUTO,
52  CONF_FAN_MODE_DIFFUSE,
53  CONF_FAN_MODE_FOCUS,
54  CONF_FAN_MODE_HIGH,
55  CONF_FAN_MODE_LOW,
56  CONF_FAN_MODE_MEDIUM,
57  CONF_FAN_MODE_MIDDLE,
58  CONF_FAN_MODE_OFF,
59  CONF_FAN_MODE_ON,
60  CONF_FAN_MODE_REGISTER,
61  CONF_FAN_MODE_TOP,
62  CONF_FAN_MODE_VALUES,
63  CONF_HVAC_MODE_AUTO,
64  CONF_HVAC_MODE_COOL,
65  CONF_HVAC_MODE_DRY,
66  CONF_HVAC_MODE_FAN_ONLY,
67  CONF_HVAC_MODE_HEAT,
68  CONF_HVAC_MODE_HEAT_COOL,
69  CONF_HVAC_MODE_OFF,
70  CONF_HVAC_MODE_REGISTER,
71  CONF_HVAC_MODE_VALUES,
72  CONF_HVAC_ONOFF_REGISTER,
73  CONF_MAX_TEMP,
74  CONF_MIN_TEMP,
75  CONF_STEP,
76  CONF_SWING_MODE_REGISTER,
77  CONF_SWING_MODE_SWING_BOTH,
78  CONF_SWING_MODE_SWING_HORIZ,
79  CONF_SWING_MODE_SWING_OFF,
80  CONF_SWING_MODE_SWING_ON,
81  CONF_SWING_MODE_SWING_VERT,
82  CONF_SWING_MODE_VALUES,
83  CONF_TARGET_TEMP,
84  CONF_TARGET_TEMP_WRITE_REGISTERS,
85  CONF_WRITE_REGISTERS,
86  DataType,
87 )
88 from .entity import BaseStructPlatform
89 from .modbus import ModbusHub
90 
91 _LOGGER = logging.getLogger(__name__)
92 
93 PARALLEL_UPDATES = 1
94 
95 HVACMODE_TO_TARG_TEMP_REG_INDEX_ARRAY = {
96  HVACMode.AUTO: 0,
97  HVACMode.COOL: 1,
98  HVACMode.DRY: 2,
99  HVACMode.FAN_ONLY: 3,
100  HVACMode.HEAT: 4,
101  HVACMode.HEAT_COOL: 5,
102  HVACMode.OFF: 6,
103  None: 0,
104 }
105 
106 
108  hass: HomeAssistant,
109  config: ConfigType,
110  async_add_entities: AddEntitiesCallback,
111  discovery_info: DiscoveryInfoType | None = None,
112 ) -> None:
113  """Read configuration and create Modbus climate."""
114  if discovery_info is None:
115  return
116 
117  entities = []
118  for entity in discovery_info[CONF_CLIMATES]:
119  hub: ModbusHub = get_hub(hass, discovery_info[CONF_NAME])
120  entities.append(ModbusThermostat(hass, hub, entity))
121 
122  async_add_entities(entities)
123 
124 
126  """Representation of a Modbus Thermostat."""
127 
128  _attr_supported_features = (
129  ClimateEntityFeature.TARGET_TEMPERATURE
130  | ClimateEntityFeature.TURN_OFF
131  | ClimateEntityFeature.TURN_ON
132  )
133  _enable_turn_on_off_backwards_compatibility = False
134 
135  def __init__(
136  self,
137  hass: HomeAssistant,
138  hub: ModbusHub,
139  config: dict[str, Any],
140  ) -> None:
141  """Initialize the modbus thermostat."""
142  super().__init__(hass, hub, config)
143  self._target_temperature_register_target_temperature_register = config[CONF_TARGET_TEMP]
144  self._target_temperature_write_registers_target_temperature_write_registers = config[
145  CONF_TARGET_TEMP_WRITE_REGISTERS
146  ]
147  self._unit_unit = config[CONF_TEMPERATURE_UNIT]
148  self._attr_current_temperature_attr_current_temperature = None
149  self._attr_target_temperature_attr_target_temperature = None
150  self._attr_temperature_unit_attr_temperature_unit = (
151  UnitOfTemperature.FAHRENHEIT
152  if self._unit_unit == "F"
153  else UnitOfTemperature.CELSIUS
154  )
155  self._attr_precision_attr_precision = (
156  PRECISION_TENTHS if self._precision_precision >= 1 else PRECISION_WHOLE
157  )
158  self._attr_min_temp_attr_min_temp = config[CONF_MIN_TEMP]
159  self._attr_max_temp_attr_max_temp = config[CONF_MAX_TEMP]
160  self._attr_target_temperature_step_attr_target_temperature_step = config[CONF_STEP]
161 
162  if CONF_HVAC_MODE_REGISTER in config:
163  mode_config = config[CONF_HVAC_MODE_REGISTER]
164  self._hvac_mode_register_hvac_mode_register = mode_config[CONF_ADDRESS]
165  self._attr_hvac_modes_attr_hvac_modes = cast(list[HVACMode], [])
166  self._attr_hvac_mode_attr_hvac_mode = None
167  self._hvac_mode_mapping: list[tuple[int, HVACMode]] = []
168  self._hvac_mode_write_registers_hvac_mode_write_registers = mode_config[CONF_WRITE_REGISTERS]
169  mode_value_config = mode_config[CONF_HVAC_MODE_VALUES]
170 
171  for hvac_mode_kw, hvac_mode in (
172  (CONF_HVAC_MODE_OFF, HVACMode.OFF),
173  (CONF_HVAC_MODE_HEAT, HVACMode.HEAT),
174  (CONF_HVAC_MODE_COOL, HVACMode.COOL),
175  (CONF_HVAC_MODE_HEAT_COOL, HVACMode.HEAT_COOL),
176  (CONF_HVAC_MODE_AUTO, HVACMode.AUTO),
177  (CONF_HVAC_MODE_DRY, HVACMode.DRY),
178  (CONF_HVAC_MODE_FAN_ONLY, HVACMode.FAN_ONLY),
179  ):
180  if hvac_mode_kw in mode_value_config:
181  values = mode_value_config[hvac_mode_kw]
182  if not isinstance(values, list):
183  values = [values]
184  for value in values:
185  self._hvac_mode_mapping.append((value, hvac_mode))
186  self._attr_hvac_modes_attr_hvac_modes.append(hvac_mode)
187  else:
188  # No HVAC modes defined
189  self._hvac_mode_register_hvac_mode_register = None
190  self._attr_hvac_mode_attr_hvac_mode = HVACMode.AUTO
191  self._attr_hvac_modes_attr_hvac_modes = [HVACMode.AUTO]
192 
193  if CONF_FAN_MODE_REGISTER in config:
194  self._attr_supported_features_attr_supported_features_attr_supported_features = (
195  self._attr_supported_features_attr_supported_features_attr_supported_features | ClimateEntityFeature.FAN_MODE
196  )
197  mode_config = config[CONF_FAN_MODE_REGISTER]
198  self._fan_mode_register_fan_mode_register = mode_config[CONF_ADDRESS]
199  self._attr_fan_modes_attr_fan_modes = cast(list[str], [])
200  self._attr_fan_mode_attr_fan_mode = None
201  self._fan_mode_mapping_to_modbus: dict[str, int] = {}
202  self._fan_mode_mapping_from_modbus: dict[int, str] = {}
203  mode_value_config = mode_config[CONF_FAN_MODE_VALUES]
204  for fan_mode_kw, fan_mode in (
205  (CONF_FAN_MODE_ON, FAN_ON),
206  (CONF_FAN_MODE_OFF, FAN_OFF),
207  (CONF_FAN_MODE_AUTO, FAN_AUTO),
208  (CONF_FAN_MODE_LOW, FAN_LOW),
209  (CONF_FAN_MODE_MEDIUM, FAN_MEDIUM),
210  (CONF_FAN_MODE_HIGH, FAN_HIGH),
211  (CONF_FAN_MODE_TOP, FAN_TOP),
212  (CONF_FAN_MODE_MIDDLE, FAN_MIDDLE),
213  (CONF_FAN_MODE_FOCUS, FAN_FOCUS),
214  (CONF_FAN_MODE_DIFFUSE, FAN_DIFFUSE),
215  ):
216  if fan_mode_kw in mode_value_config:
217  value = mode_value_config[fan_mode_kw]
218  self._fan_mode_mapping_from_modbus[value] = fan_mode
219  self._fan_mode_mapping_to_modbus[fan_mode] = value
220  self._attr_fan_modes_attr_fan_modes.append(fan_mode)
221 
222  else:
223  # No FAN modes defined
224  self._fan_mode_register_fan_mode_register = None
225  self._attr_fan_mode_attr_fan_mode = FAN_AUTO
226  self._attr_fan_modes_attr_fan_modes = [FAN_AUTO]
227 
228  # No SWING modes defined
229  self._swing_mode_register_swing_mode_register = None
230  if CONF_SWING_MODE_REGISTER in config:
231  self._attr_supported_features_attr_supported_features_attr_supported_features = (
232  self._attr_supported_features_attr_supported_features_attr_supported_features | ClimateEntityFeature.SWING_MODE
233  )
234  mode_config = config[CONF_SWING_MODE_REGISTER]
235  self._swing_mode_register_swing_mode_register = mode_config[CONF_ADDRESS]
236  self._attr_swing_modes_attr_swing_modes = cast(list[str], [])
237  self._attr_swing_mode_attr_swing_mode = None
238  self._swing_mode_modbus_mapping: list[tuple[int, str]] = []
239  mode_value_config = mode_config[CONF_SWING_MODE_VALUES]
240  for swing_mode_kw, swing_mode in (
241  (CONF_SWING_MODE_SWING_ON, SWING_ON),
242  (CONF_SWING_MODE_SWING_OFF, SWING_OFF),
243  (CONF_SWING_MODE_SWING_HORIZ, SWING_HORIZONTAL),
244  (CONF_SWING_MODE_SWING_VERT, SWING_VERTICAL),
245  (CONF_SWING_MODE_SWING_BOTH, SWING_BOTH),
246  ):
247  if swing_mode_kw in mode_value_config:
248  value = mode_value_config[swing_mode_kw]
249  self._swing_mode_modbus_mapping.append((value, swing_mode))
250  self._attr_swing_modes_attr_swing_modes.append(swing_mode)
251 
252  if CONF_HVAC_ONOFF_REGISTER in config:
253  self._hvac_onoff_register_hvac_onoff_register = config[CONF_HVAC_ONOFF_REGISTER]
254  self._hvac_onoff_write_registers_hvac_onoff_write_registers = config[CONF_WRITE_REGISTERS]
255  if HVACMode.OFF not in self._attr_hvac_modes_attr_hvac_modes:
256  self._attr_hvac_modes_attr_hvac_modes.append(HVACMode.OFF)
257  else:
258  self._hvac_onoff_register_hvac_onoff_register = None
259 
260  async def async_added_to_hass(self) -> None:
261  """Handle entity which will be added."""
262  await self.async_base_added_to_hassasync_base_added_to_hass()
263  state = await self.async_get_last_stateasync_get_last_state()
264  if state and state.attributes.get(ATTR_TEMPERATURE):
265  self._attr_target_temperature_attr_target_temperature = float(state.attributes[ATTR_TEMPERATURE])
266 
267  async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
268  """Set new target hvac mode."""
269  if self._hvac_onoff_register_hvac_onoff_register is not None:
270  # Turn HVAC Off by writing 0 to the On/Off register, or 1 otherwise.
271  if self._hvac_onoff_write_registers_hvac_onoff_write_registers:
272  await self._hub_hub.async_pb_call(
273  self._slave_slave,
274  self._hvac_onoff_register_hvac_onoff_register,
275  [0 if hvac_mode == HVACMode.OFF else 1],
276  CALL_TYPE_WRITE_REGISTERS,
277  )
278  else:
279  await self._hub_hub.async_pb_call(
280  self._slave_slave,
281  self._hvac_onoff_register_hvac_onoff_register,
282  0 if hvac_mode == HVACMode.OFF else 1,
283  CALL_TYPE_WRITE_REGISTER,
284  )
285 
286  if self._hvac_mode_register_hvac_mode_register is not None:
287  # Write a value to the mode register for the desired mode.
288  for value, mode in self._hvac_mode_mapping:
289  if mode == hvac_mode:
290  if self._hvac_mode_write_registers_hvac_mode_write_registers:
291  await self._hub_hub.async_pb_call(
292  self._slave_slave,
293  self._hvac_mode_register_hvac_mode_register,
294  [value],
295  CALL_TYPE_WRITE_REGISTERS,
296  )
297  else:
298  await self._hub_hub.async_pb_call(
299  self._slave_slave,
300  self._hvac_mode_register_hvac_mode_register,
301  value,
302  CALL_TYPE_WRITE_REGISTER,
303  )
304  break
305 
306  await self.async_updateasync_updateasync_update()
307 
308  async def async_set_fan_mode(self, fan_mode: str) -> None:
309  """Set new target fan mode."""
310  if self._fan_mode_register_fan_mode_register is not None:
311  # Write a value to the mode register for the desired mode.
312  value = self._fan_mode_mapping_to_modbus[fan_mode]
313  if isinstance(self._fan_mode_register_fan_mode_register, list):
314  await self._hub_hub.async_pb_call(
315  self._slave_slave,
316  self._fan_mode_register_fan_mode_register[0],
317  [value],
318  CALL_TYPE_WRITE_REGISTERS,
319  )
320  else:
321  await self._hub_hub.async_pb_call(
322  self._slave_slave,
323  self._fan_mode_register_fan_mode_register,
324  value,
325  CALL_TYPE_WRITE_REGISTER,
326  )
327 
328  await self.async_updateasync_updateasync_update()
329 
330  async def async_set_swing_mode(self, swing_mode: str) -> None:
331  """Set new target swing mode."""
332  if self._swing_mode_register_swing_mode_register:
333  # Write a value to the mode register for the desired mode.
334  for value, smode in self._swing_mode_modbus_mapping:
335  if swing_mode == smode:
336  if isinstance(self._swing_mode_register_swing_mode_register, list):
337  await self._hub_hub.async_pb_call(
338  self._slave_slave,
339  self._swing_mode_register_swing_mode_register[0],
340  [value],
341  CALL_TYPE_WRITE_REGISTERS,
342  )
343  else:
344  await self._hub_hub.async_pb_call(
345  self._slave_slave,
346  self._swing_mode_register_swing_mode_register,
347  value,
348  CALL_TYPE_WRITE_REGISTER,
349  )
350  break
351  await self.async_updateasync_updateasync_update()
352 
353  async def async_set_temperature(self, **kwargs: Any) -> None:
354  """Set new target temperature."""
355  target_temperature = (
356  float(kwargs[ATTR_TEMPERATURE]) - self._offset_offset
357  ) / self._scale_scale
358  if self._data_type_data_type in (
359  DataType.INT16,
360  DataType.INT32,
361  DataType.INT64,
362  DataType.UINT16,
363  DataType.UINT32,
364  DataType.UINT64,
365  ):
366  target_temperature = int(target_temperature)
367  as_bytes = struct.pack(self._structure, target_temperature)
368  raw_regs = [
369  int.from_bytes(as_bytes[i : i + 2], "big")
370  for i in range(0, len(as_bytes), 2)
371  ]
372  registers = self._swap_registers_swap_registers(raw_regs, 0)
373 
374  if self._data_type_data_type in (
375  DataType.INT16,
376  DataType.UINT16,
377  ):
378  if self._target_temperature_write_registers_target_temperature_write_registers:
379  result = await self._hub_hub.async_pb_call(
380  self._slave_slave,
381  self._target_temperature_register_target_temperature_register[
382  HVACMODE_TO_TARG_TEMP_REG_INDEX_ARRAY[self._attr_hvac_mode_attr_hvac_mode]
383  ],
384  [int(float(registers[0]))],
385  CALL_TYPE_WRITE_REGISTERS,
386  )
387  else:
388  result = await self._hub_hub.async_pb_call(
389  self._slave_slave,
390  self._target_temperature_register_target_temperature_register[
391  HVACMODE_TO_TARG_TEMP_REG_INDEX_ARRAY[self._attr_hvac_mode_attr_hvac_mode]
392  ],
393  int(float(registers[0])),
394  CALL_TYPE_WRITE_REGISTER,
395  )
396  else:
397  result = await self._hub_hub.async_pb_call(
398  self._slave_slave,
399  self._target_temperature_register_target_temperature_register[
400  HVACMODE_TO_TARG_TEMP_REG_INDEX_ARRAY[self._attr_hvac_mode_attr_hvac_mode]
401  ],
402  [int(float(i)) for i in registers],
403  CALL_TYPE_WRITE_REGISTERS,
404  )
405  self._attr_available_attr_available_attr_available = result is not None
406  await self.async_updateasync_updateasync_update()
407 
408  async def async_update(self, now: datetime | None = None) -> None:
409  """Update Target & Current Temperature."""
410  # remark "now" is a dummy parameter to avoid problems with
411  # async_track_time_interval
412 
413  self._attr_target_temperature_attr_target_temperature = await self._async_read_register_async_read_register(
414  CALL_TYPE_REGISTER_HOLDING,
415  self._target_temperature_register_target_temperature_register[
416  HVACMODE_TO_TARG_TEMP_REG_INDEX_ARRAY[self._attr_hvac_mode_attr_hvac_mode]
417  ],
418  )
419 
420  self._attr_current_temperature_attr_current_temperature = await self._async_read_register_async_read_register(
421  self._input_type_input_type, self._address_address
422  )
423  # Read the HVAC mode register if defined
424  if self._hvac_mode_register_hvac_mode_register is not None:
425  hvac_mode = await self._async_read_register_async_read_register(
426  CALL_TYPE_REGISTER_HOLDING, self._hvac_mode_register_hvac_mode_register, raw=True
427  )
428 
429  # Translate the value received
430  if hvac_mode is not None:
431  self._attr_hvac_mode_attr_hvac_mode = None
432  for value, mode in self._hvac_mode_mapping:
433  if hvac_mode == value:
434  self._attr_hvac_mode_attr_hvac_mode = mode
435  break
436 
437  # Read the Fan mode register if defined
438  if self._fan_mode_register_fan_mode_register is not None:
439  fan_mode = await self._async_read_register_async_read_register(
440  CALL_TYPE_REGISTER_HOLDING,
441  self._fan_mode_register_fan_mode_register
442  if isinstance(self._fan_mode_register_fan_mode_register, int)
443  else self._fan_mode_register_fan_mode_register[0],
444  raw=True,
445  )
446 
447  # Translate the value received
448  if fan_mode is not None:
449  self._attr_fan_mode_attr_fan_mode = self._fan_mode_mapping_from_modbus.get(
450  int(fan_mode), self._attr_fan_mode_attr_fan_mode
451  )
452 
453  # Read the Swing mode register if defined
454  if self._swing_mode_register_swing_mode_register:
455  swing_mode = await self._async_read_register_async_read_register(
456  CALL_TYPE_REGISTER_HOLDING,
457  self._swing_mode_register_swing_mode_register
458  if isinstance(self._swing_mode_register_swing_mode_register, int)
459  else self._swing_mode_register_swing_mode_register[0],
460  raw=True,
461  )
462 
463  self._attr_swing_mode_attr_swing_mode = STATE_UNKNOWN
464  for value, smode in self._swing_mode_modbus_mapping:
465  if swing_mode == value:
466  self._attr_swing_mode_attr_swing_mode = smode
467  break
468 
469  if self._attr_swing_mode_attr_swing_mode is STATE_UNKNOWN:
470  _err = f"{self.name}: No answer received from Swing mode register. State is Unknown"
471  _LOGGER.error(_err)
472 
473  # Read the on/off register if defined. If the value in this
474  # register is "OFF", it will take precedence over the value
475  # in the mode register.
476  if self._hvac_onoff_register_hvac_onoff_register is not None:
477  onoff = await self._async_read_register_async_read_register(
478  CALL_TYPE_REGISTER_HOLDING, self._hvac_onoff_register_hvac_onoff_register, raw=True
479  )
480  if onoff == 0:
481  self._attr_hvac_mode_attr_hvac_mode = HVACMode.OFF
482 
483  self.async_write_ha_stateasync_write_ha_state()
484 
486  self, register_type: str, register: int, raw: bool | None = False
487  ) -> float | None:
488  """Read register using the Modbus hub slave."""
489  result = await self._hub_hub.async_pb_call(
490  self._slave_slave, register, self._count_count, register_type
491  )
492  if result is None:
493  self._attr_available_attr_available_attr_available = False
494  return -1
495 
496  if raw:
497  # Return the raw value read from the register, do not change
498  # the object's state
499  self._attr_available_attr_available_attr_available = True
500  return int(result.registers[0])
501 
502  # The regular handling of the value
503  self._value_value = self.unpack_structure_resultunpack_structure_result(result.registers)
504  if not self._value_value:
505  self._attr_available_attr_available_attr_available = False
506  return None
507  self._attr_available_attr_available_attr_available = True
508  return float(self._value_value)
None __init__(self, HomeAssistant hass, ModbusHub hub, dict[str, Any] config)
Definition: climate.py:140
None async_update(self, datetime|None now=None)
Definition: climate.py:408
float|None _async_read_register(self, str register_type, int register, bool|None raw=False)
Definition: climate.py:487
None async_set_hvac_mode(self, HVACMode hvac_mode)
Definition: climate.py:267
None async_update(self, datetime|None now=None)
Definition: entity.py:112
str|None unpack_structure_result(self, list[int] registers)
Definition: entity.py:225
list[int] _swap_registers(self, list[int] registers, int slave_count)
Definition: entity.py:183
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
None async_setup_platform(HomeAssistant hass, ConfigType config, AddEntitiesCallback async_add_entities, DiscoveryInfoType|None discovery_info=None)
Definition: climate.py:112
ModbusHub get_hub(HomeAssistant hass, str name)
Definition: __init__.py:445