Home Assistant Unofficial Reference 2024.12.1
hitachi_air_to_air_heat_pump_ovp.py
Go to the documentation of this file.
1 """Support for HitachiAirToAirHeatPump."""
2 
3 from __future__ import annotations
4 
5 from typing import Any
6 
7 from pyoverkiz.enums import OverkizCommand, OverkizCommandParam, OverkizState
8 
10  FAN_AUTO,
11  FAN_HIGH,
12  FAN_LOW,
13  FAN_MEDIUM,
14  PRESET_NONE,
15  SWING_BOTH,
16  SWING_HORIZONTAL,
17  SWING_OFF,
18  SWING_VERTICAL,
19  ClimateEntity,
20  ClimateEntityFeature,
21  HVACMode,
22 )
23 from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
24 
25 from ..const import DOMAIN
26 from ..coordinator import OverkizDataUpdateCoordinator
27 from ..entity import OverkizEntity
28 
29 PRESET_HOLIDAY_MODE = "holiday_mode"
30 FAN_SILENT = "silent"
31 TEMP_MIN = 16
32 TEMP_MAX = 32
33 TEMP_AUTO_MIN = 22
34 TEMP_AUTO_MAX = 28
35 AUTO_PIVOT_TEMPERATURE = 25
36 AUTO_TEMPERATURE_CHANGE_MIN = TEMP_AUTO_MIN - AUTO_PIVOT_TEMPERATURE
37 AUTO_TEMPERATURE_CHANGE_MAX = TEMP_AUTO_MAX - AUTO_PIVOT_TEMPERATURE
38 
39 OVERKIZ_TO_HVAC_MODES: dict[str, HVACMode] = {
40  OverkizCommandParam.AUTOHEATING: HVACMode.AUTO,
41  OverkizCommandParam.AUTOCOOLING: HVACMode.AUTO,
42  OverkizCommandParam.ON: HVACMode.HEAT,
43  OverkizCommandParam.OFF: HVACMode.OFF,
44  OverkizCommandParam.HEATING: HVACMode.HEAT,
45  OverkizCommandParam.FAN: HVACMode.FAN_ONLY,
46  OverkizCommandParam.DEHUMIDIFY: HVACMode.DRY,
47  OverkizCommandParam.COOLING: HVACMode.COOL,
48 }
49 
50 HVAC_MODES_TO_OVERKIZ: dict[HVACMode, str] = {
51  HVACMode.AUTO: OverkizCommandParam.AUTO,
52  HVACMode.HEAT: OverkizCommandParam.HEATING,
53  HVACMode.OFF: OverkizCommandParam.HEATING,
54  HVACMode.FAN_ONLY: OverkizCommandParam.FAN,
55  HVACMode.DRY: OverkizCommandParam.DEHUMIDIFY,
56  HVACMode.COOL: OverkizCommandParam.COOLING,
57 }
58 
59 OVERKIZ_TO_SWING_MODES: dict[str, str] = {
60  OverkizCommandParam.BOTH: SWING_BOTH,
61  OverkizCommandParam.HORIZONTAL: SWING_HORIZONTAL,
62  OverkizCommandParam.STOP: SWING_OFF,
63  OverkizCommandParam.VERTICAL: SWING_VERTICAL,
64 }
65 
66 SWING_MODES_TO_OVERKIZ = {v: k for k, v in OVERKIZ_TO_SWING_MODES.items()}
67 
68 OVERKIZ_TO_FAN_MODES: dict[str, str] = {
69  OverkizCommandParam.AUTO: FAN_AUTO,
70  OverkizCommandParam.HIGH: FAN_HIGH, # fallback, state can be exposed as HIGH, new state = hi
71  OverkizCommandParam.HI: FAN_HIGH,
72  OverkizCommandParam.LOW: FAN_LOW,
73  OverkizCommandParam.LO: FAN_LOW,
74  OverkizCommandParam.MEDIUM: FAN_MEDIUM, # fallback, state can be exposed as MEDIUM, new state = med
75  OverkizCommandParam.MED: FAN_MEDIUM,
76  OverkizCommandParam.SILENT: OverkizCommandParam.SILENT,
77 }
78 
79 FAN_MODES_TO_OVERKIZ: dict[str, str] = {
80  FAN_AUTO: OverkizCommandParam.AUTO,
81  FAN_HIGH: OverkizCommandParam.HI,
82  FAN_LOW: OverkizCommandParam.LO,
83  FAN_MEDIUM: OverkizCommandParam.MED,
84  FAN_SILENT: OverkizCommandParam.SILENT,
85 }
86 
87 
89  """Representation of Hitachi Air To Air HeatPump."""
90 
91  _attr_hvac_modes = [*HVAC_MODES_TO_OVERKIZ]
92  _attr_fan_modes = [*FAN_MODES_TO_OVERKIZ]
93  _attr_preset_modes = [PRESET_NONE, PRESET_HOLIDAY_MODE]
94  _attr_swing_modes = [*SWING_MODES_TO_OVERKIZ]
95  _attr_target_temperature_step = 1.0
96  _attr_temperature_unit = UnitOfTemperature.CELSIUS
97  _attr_translation_key = DOMAIN
98  _enable_turn_on_off_backwards_compatibility = False
99 
100  def __init__(
101  self, device_url: str, coordinator: OverkizDataUpdateCoordinator
102  ) -> None:
103  """Init method."""
104  super().__init__(device_url, coordinator)
105 
106  self._attr_supported_features_attr_supported_features = (
107  ClimateEntityFeature.TARGET_TEMPERATURE
108  | ClimateEntityFeature.FAN_MODE
109  | ClimateEntityFeature.PRESET_MODE
110  | ClimateEntityFeature.TURN_OFF
111  | ClimateEntityFeature.TURN_ON
112  )
113 
114  if self.devicedevice.states.get(OverkizState.OVP_SWING):
115  self._attr_supported_features_attr_supported_features |= ClimateEntityFeature.SWING_MODE
116 
117  if self._attr_device_info_attr_device_info:
118  self._attr_device_info_attr_device_info["manufacturer"] = "Hitachi"
119 
120  @property
121  def hvac_mode(self) -> HVACMode:
122  """Return hvac operation ie. heat, cool mode."""
123  if (
124  main_op_state := self.devicedevice.states[OverkizState.OVP_MAIN_OPERATION]
125  ) and main_op_state.value_as_str:
126  if main_op_state.value_as_str.lower() == OverkizCommandParam.OFF:
127  return HVACMode.OFF
128 
129  if (
130  mode_change_state := self.devicedevice.states[OverkizState.OVP_MODE_CHANGE]
131  ) and mode_change_state.value_as_str:
132  # The OVP protocol has 'auto cooling' and 'auto heating' values
133  # that are equivalent to the HLRRWIFI protocol without spaces
134  sanitized_value = mode_change_state.value_as_str.replace(" ", "").lower()
135  return OVERKIZ_TO_HVAC_MODES[sanitized_value]
136 
137  return HVACMode.OFF
138 
139  async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
140  """Set new target hvac mode."""
141  if hvac_mode == HVACMode.OFF:
142  await self._global_control_global_control(main_operation=OverkizCommandParam.OFF)
143  else:
144  await self._global_control_global_control(
145  main_operation=OverkizCommandParam.ON,
146  hvac_mode=HVAC_MODES_TO_OVERKIZ[hvac_mode],
147  )
148 
149  @property
150  def fan_mode(self) -> str | None:
151  """Return the fan setting."""
152  if (
153  state := self.devicedevice.states[OverkizState.OVP_FAN_SPEED]
154  ) and state.value_as_str:
155  return OVERKIZ_TO_FAN_MODES[state.value_as_str]
156 
157  return None
158 
159  async def async_set_fan_mode(self, fan_mode: str) -> None:
160  """Set new target fan mode."""
161  await self._global_control_global_control(fan_mode=FAN_MODES_TO_OVERKIZ[fan_mode])
162 
163  @property
164  def swing_mode(self) -> str | None:
165  """Return the swing setting."""
166  if (state := self.devicedevice.states[OverkizState.OVP_SWING]) and state.value_as_str:
167  return OVERKIZ_TO_SWING_MODES[state.value_as_str]
168 
169  return None
170 
171  async def async_set_swing_mode(self, swing_mode: str) -> None:
172  """Set new target swing operation."""
173  await self._global_control_global_control(swing_mode=SWING_MODES_TO_OVERKIZ[swing_mode])
174 
175  @property
176  def target_temperature(self) -> int | None:
177  """Return the target temperature."""
178  if (
179  temperature := self.devicedevice.states[OverkizState.CORE_TARGET_TEMPERATURE]
180  ) and temperature.value_as_int:
181  return temperature.value_as_int
182 
183  return None
184 
185  @property
186  def current_temperature(self) -> int | None:
187  """Return current temperature."""
188  if (
189  state := self.devicedevice.states[OverkizState.OVP_ROOM_TEMPERATURE]
190  ) and state.value_as_int:
191  return state.value_as_int
192 
193  return None
194 
195  async def async_set_temperature(self, **kwargs: Any) -> None:
196  """Set new temperature."""
197  await self._global_control_global_control(target_temperature=int(kwargs[ATTR_TEMPERATURE]))
198 
199  @property
200  def preset_mode(self) -> str | None:
201  """Return the current preset mode, e.g., home, away, temp."""
202  if (
203  state := self.devicedevice.states[OverkizState.CORE_HOLIDAYS_MODE]
204  ) and state.value_as_str:
205  if state.value_as_str == OverkizCommandParam.ON:
206  return PRESET_HOLIDAY_MODE
207 
208  if state.value_as_str == OverkizCommandParam.OFF:
209  return PRESET_NONE
210 
211  return None
212 
213  async def async_set_preset_mode(self, preset_mode: str) -> None:
214  """Set new preset mode."""
215  if preset_mode == PRESET_HOLIDAY_MODE:
216  await self.executorexecutor.async_execute_command(
217  OverkizCommand.SET_HOLIDAYS,
218  OverkizCommandParam.ON,
219  )
220  if preset_mode == PRESET_NONE:
221  await self.executorexecutor.async_execute_command(
222  OverkizCommand.SET_HOLIDAYS,
223  OverkizCommandParam.OFF,
224  )
225 
226  # OVP has this property to control the unit's timer mode
227  @property
228  def auto_manu_mode(self) -> str | None:
229  """Return auto/manu mode."""
230  if (
231  state := self.devicedevice.states[OverkizState.CORE_AUTO_MANU_MODE]
232  ) and state.value_as_str:
233  return state.value_as_str
234  return None
235 
236  # OVP has this property to control the target temperature delta in auto mode
237  @property
238  def temperature_change(self) -> int | None:
239  """Return temperature change state."""
240  if (
241  state := self.devicedevice.states[OverkizState.OVP_TEMPERATURE_CHANGE]
242  ) and state.value_as_int:
243  return state.value_as_int
244 
245  return None
246 
247  @property
248  def min_temp(self) -> float:
249  """Return the minimum temperature."""
250  if self.hvac_modehvac_modehvac_modehvac_modehvac_mode == HVACMode.AUTO:
251  return TEMP_AUTO_MIN
252  return TEMP_MIN
253 
254  @property
255  def max_temp(self) -> float:
256  """Return the maximum temperature."""
257  if self.hvac_modehvac_modehvac_modehvac_modehvac_mode == HVACMode.AUTO:
258  return TEMP_AUTO_MAX
259  return TEMP_MAX
260 
262  self, value: str | None, state_name: str, fallback_value: str
263  ) -> str:
264  """Return a parameter value which will be accepted in a command by Overkiz.
265 
266  Overkiz doesn't accept commands with undefined parameters. This function
267  is guaranteed to return a `str` which is the provided `value` if set, or
268  the current device state if set, or the provided `fallback_value` otherwise.
269  """
270  if value:
271  return value
272  if (state := self.devicedevice.states[state_name]) is not None and (
273  value := state.value_as_str
274  ) is not None:
275  return value
276  return fallback_value
277 
278  async def _global_control(
279  self,
280  main_operation: str | None = None,
281  target_temperature: int | None = None,
282  fan_mode: str | None = None,
283  hvac_mode: str | None = None,
284  swing_mode: str | None = None,
285  leave_home: str | None = None,
286  ) -> None:
287  """Execute globalControl command with all parameters.
288 
289  There is no option to only set a single parameter, without passing
290  all other values.
291  """
292 
293  main_operation = self._control_backfill_control_backfill(
294  main_operation, OverkizState.OVP_MAIN_OPERATION, OverkizCommandParam.ON
295  )
296  fan_mode = self._control_backfill_control_backfill(
297  fan_mode,
298  OverkizState.OVP_FAN_SPEED,
299  OverkizCommandParam.AUTO,
300  )
301  # Sanitize fan mode: Overkiz is sometimes providing a state that
302  # cannot be used as a command. Convert it to HA space and back to Overkiz
303  if fan_mode not in FAN_MODES_TO_OVERKIZ.values():
304  fan_mode = FAN_MODES_TO_OVERKIZ[OVERKIZ_TO_FAN_MODES[fan_mode]]
305 
306  hvac_mode = self._control_backfill_control_backfill(
307  hvac_mode,
308  OverkizState.OVP_MODE_CHANGE,
309  OverkizCommandParam.AUTO,
310  ).lower() # Overkiz returns uppercase states that are not acceptable commands
311  if hvac_mode.replace(" ", "") in [
312  # Overkiz returns compound states like 'auto cooling' or 'autoHeating'
313  # that are not valid commands and need to be mapped to 'auto'
314  OverkizCommandParam.AUTOCOOLING,
315  OverkizCommandParam.AUTOHEATING,
316  ]:
317  hvac_mode = OverkizCommandParam.AUTO
318 
319  swing_mode = self._control_backfill_control_backfill(
320  swing_mode,
321  OverkizState.OVP_SWING,
322  OverkizCommandParam.STOP,
323  )
324 
325  # AUTO_MANU parameter is not controlled by HA and is turned "off" when the device is on Holiday mode
326  auto_manu_mode = self._control_backfill_control_backfill(
327  None, OverkizState.CORE_AUTO_MANU_MODE, OverkizCommandParam.MANU
328  )
329  if self.preset_modepreset_modepreset_modepreset_mode == PRESET_HOLIDAY_MODE:
330  auto_manu_mode = OverkizCommandParam.OFF
331 
332  # In all the hvac modes except AUTO, the temperature command parameter is the target temperature
333  temperature_command = None
334  target_temperature = target_temperature or self.target_temperaturetarget_temperaturetarget_temperature
335  if hvac_mode == OverkizCommandParam.AUTO:
336  # In hvac mode AUTO, the temperature command parameter is a temperature_change
337  # which is the delta between a pivot temperature (25) and the target temperature
338  temperature_change = 0
339 
340  if target_temperature:
341  temperature_change = target_temperature - AUTO_PIVOT_TEMPERATURE
342  elif self.temperature_changetemperature_change:
343  temperature_change = self.temperature_changetemperature_change
344 
345  # Keep temperature_change in the API accepted range
346  temperature_change = min(
347  max(temperature_change, AUTO_TEMPERATURE_CHANGE_MIN),
348  AUTO_TEMPERATURE_CHANGE_MAX,
349  )
350 
351  temperature_command = temperature_change
352  else:
353  # In other modes, the temperature command is the target temperature
354  temperature_command = target_temperature
355 
356  command_data = [
357  main_operation, # Main Operation
358  temperature_command, # Temperature Command
359  fan_mode, # Fan Mode
360  hvac_mode, # Mode
361  auto_manu_mode, # Auto Manu Mode
362  ]
363 
364  await self.executorexecutor.async_execute_command(
365  OverkizCommand.GLOBAL_CONTROL, *command_data
366  )
None _global_control(self, str|None main_operation=None, int|None target_temperature=None, str|None fan_mode=None, str|None hvac_mode=None, str|None swing_mode=None, str|None leave_home=None)