From 669ac3f1f5448e521cdd2223a6226bb06b319045 Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Wed, 14 Jul 2021 15:33:29 -0700 Subject: [PATCH] Add basic support for HitachiAirToAirHeatPump (#364) --- custom_components/tahoma/climate.py | 2 + .../hitachi_air_to_air_heat_pump.py | 268 ++++++++++++++++++ custom_components/tahoma/const.py | 1 + custom_components/tahoma/tahoma_entity.py | 2 +- 4 files changed, 272 insertions(+), 1 deletion(-) create mode 100644 custom_components/tahoma/climate_devices/hitachi_air_to_air_heat_pump.py diff --git a/custom_components/tahoma/climate.py b/custom_components/tahoma/climate.py index 53a942def..7bfae52c6 100644 --- a/custom_components/tahoma/climate.py +++ b/custom_components/tahoma/climate.py @@ -16,6 +16,7 @@ from .climate_devices.dimmer_exterior_heating import DimmerExteriorHeating from .climate_devices.evo_home_controller import EvoHomeController from .climate_devices.heating_set_point import HeatingSetPoint +from .climate_devices.hitachi_air_to_air_heat_pump import HitachiAirToAirHeatPump from .climate_devices.hitachi_air_to_water_heating_zone import ( HitachiAirToWaterHeatingZone, ) @@ -34,6 +35,7 @@ "DimmerExteriorHeating": DimmerExteriorHeating, "EvoHomeController": EvoHomeController, "HeatingSetPoint": HeatingSetPoint, + "HitachiAirToAirHeatPump": HitachiAirToAirHeatPump, "HitachiAirToWaterHeatingZone": HitachiAirToWaterHeatingZone, "SomfyThermostat": SomfyThermostat, "StatelessExteriorHeating": StatelessExteriorHeating, diff --git a/custom_components/tahoma/climate_devices/hitachi_air_to_air_heat_pump.py b/custom_components/tahoma/climate_devices/hitachi_air_to_air_heat_pump.py new file mode 100644 index 000000000..6fd66aca6 --- /dev/null +++ b/custom_components/tahoma/climate_devices/hitachi_air_to_air_heat_pump.py @@ -0,0 +1,268 @@ +"""Support for HitachiAirToAirHeatPump.""" +import logging +from typing import Any, Dict, List, Optional + +from homeassistant.components.climate import ClimateEntity +from homeassistant.components.climate.const import ( + FAN_AUTO, + FAN_HIGH, + FAN_LOW, + FAN_MEDIUM, + HVAC_MODE_AUTO, + HVAC_MODE_COOL, + HVAC_MODE_DRY, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_HEAT, + HVAC_MODE_OFF, + PRESET_NONE, + SUPPORT_FAN_MODE, + SUPPORT_PRESET_MODE, + SUPPORT_SWING_MODE, + SUPPORT_TARGET_TEMPERATURE, + SWING_BOTH, + SWING_HORIZONTAL, + SWING_OFF, + SWING_VERTICAL, +) +from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS + +from ..tahoma_entity import TahomaEntity + +_LOGGER = logging.getLogger(__name__) + +COMMAND_GLOBAL_CONTROL = "globalControl" + +CORE_TARGET_TEMPERATURE_STATE = "core:TargetTemperatureState" + +PRESET_HOLIDAY_MODE = "holiday_mode" + +FAN_SPEED_STATE = ["ovp:FanSpeedState", "hlrrwifi:FanSpeedState"] +LEAVE_HOME_STATE = ["ovp::LeaveHomeState", "hlrrwifi:LeaveHomeState"] +MAIN_OPERATION_STATE = ["ovp:MainOperationState", "hlrrwifi:MainOperationState"] +MODE_CHANGE_STATE = ["ovp:ModeChangeState", "hlrrwifi:ModeChangeState"] +ROOM_TEMPERATURE_STATE = ["ovp:RoomTemperatureState", "hlrrwifi:RoomTemperatureState"] +SWING_STATE = ["ovp:SwingState", "hlrrwifi:SwingState"] + +STATE_ON = "on" +STATE_OFF = "off" + +TAHOMA_TO_HVAC_MODES = { + "autocooling": HVAC_MODE_AUTO, + "autoheating": HVAC_MODE_AUTO, + "off": HVAC_MODE_OFF, + "heating": HVAC_MODE_HEAT, + "fan": HVAC_MODE_FAN_ONLY, + "dehumidify": HVAC_MODE_DRY, + "cooling": HVAC_MODE_COOL, + "auto": HVAC_MODE_AUTO, +} + +HVAC_MODES_TO_TAHOMA = {v: k for k, v in TAHOMA_TO_HVAC_MODES.items()} + +TAHOMA_TO_SWING_MODES = { + "both": SWING_BOTH, + "horizontal": SWING_HORIZONTAL, + "stop": SWING_OFF, + "vertical": SWING_VERTICAL, +} + +SWING_MODES_TO_TAHOMA = {v: k for k, v in TAHOMA_TO_SWING_MODES.items()} + +HLRRWIFI_TAHOMA_TO_FAN_MODES = { + "auto": FAN_AUTO, + "high": FAN_HIGH, + "low": FAN_LOW, + "medium": FAN_MEDIUM, + "silent": "silent", +} + +OVP_TAHOMA_TO_FAN_MODES = { + "auto": FAN_AUTO, + "hi": FAN_HIGH, + "lo": FAN_LOW, + "med": FAN_MEDIUM, + "silent": "silent", +} + +FAN_MODES_TO_HLRRWIFI_TAHOMA = {v: k for k, v in HLRRWIFI_TAHOMA_TO_FAN_MODES.items()} +FAN_MODES_TO_OVP_TAHOMA = {v: k for k, v in OVP_TAHOMA_TO_FAN_MODES.items()} + + +class HitachiAirToAirHeatPump(TahomaEntity, ClimateEntity): + """Representation of HitachiAirToAirHeatPump.""" + + @property + def temperature_unit(self) -> str: + """Return the unit of measurement used by the platform.""" + return TEMP_CELSIUS + + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + + supported_features = ( + SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE | SUPPORT_PRESET_MODE + ) + + if self.has_state(*SWING_STATE): + supported_features |= SUPPORT_SWING_MODE + + return supported_features + + @property + def hvac_modes(self) -> List[str]: + """Return the list of available hvac operation modes.""" + return [*HVAC_MODES_TO_TAHOMA] + + @property + def hvac_mode(self) -> str: + """Return hvac operation ie. heat, cool mode.""" + if self._select_state(*MAIN_OPERATION_STATE) == STATE_OFF: + return HVAC_MODE_OFF + + return TAHOMA_TO_HVAC_MODES[self._select_state(*MODE_CHANGE_STATE)] + + async def async_set_hvac_mode(self, hvac_mode: str) -> None: + """Set new target hvac mode.""" + if hvac_mode == HVAC_MODE_OFF: + await self._global_control(main_operation=STATE_OFF) + else: + await self._global_control( + main_operation=STATE_ON, hvac_mode=HVAC_MODES_TO_TAHOMA[hvac_mode] + ) + + @property + def fan_mode(self) -> Optional[str]: + """Return the fan setting.""" + if self.device.controllable_name == "ovp:HLinkMainController": + return HLRRWIFI_TAHOMA_TO_FAN_MODES[self._select_state(*FAN_SPEED_STATE)] + + return HLRRWIFI_TAHOMA_TO_FAN_MODES[self._select_state(*FAN_SPEED_STATE)] + + @property + def fan_modes(self) -> Optional[List[str]]: + """Return the list of available fan modes.""" + if self.device.controllable_name == "ovp:HLinkMainController": + return [*FAN_MODES_TO_OVP_TAHOMA] + + return [*FAN_MODES_TO_HLRRWIFI_TAHOMA] + + async def async_set_fan_mode(self, fan_mode: str) -> None: + """Set new target fan mode.""" + if self.device.controllable_name == "ovp:HLinkMainController": + await self._global_control(fan_mode=FAN_MODES_TO_OVP_TAHOMA[fan_mode]) + else: + await self._global_control(fan_mode=FAN_MODES_TO_HLRRWIFI_TAHOMA[fan_mode]) + + @property + def swing_mode(self) -> Optional[str]: + """Return the swing setting.""" + return TAHOMA_TO_SWING_MODES[self._select_state(*SWING_STATE)] + + @property + def swing_modes(self) -> Optional[List[str]]: + """Return the list of available swing modes.""" + return [*SWING_MODES_TO_TAHOMA] + + async def async_set_swing_mode(self, swing_mode: str) -> None: + """Set new target swing operation.""" + await self._global_control(swing_mode=SWING_MODES_TO_TAHOMA[swing_mode]) + + @property + def target_temperature_step(self) -> Optional[float]: + """Return the supported step of target temperature.""" + return 1.0 + + @property + def target_temperature(self) -> None: + """Return the temperature.""" + return self._select_state(CORE_TARGET_TEMPERATURE_STATE) + + @property + def current_temperature(self) -> None: + """Return current temperature.""" + return self._select_state(*ROOM_TEMPERATURE_STATE) + + async def async_set_temperature(self, **kwargs) -> None: + """Set new temperature.""" + temperature = kwargs.get(ATTR_TEMPERATURE) + await self._global_control(target_temperature=int(temperature)) + + @property + def preset_mode(self) -> Optional[str]: + """Return the current preset mode, e.g., home, away, temp.""" + if self._select_state(*LEAVE_HOME_STATE) == STATE_ON: + return PRESET_HOLIDAY_MODE + + if self._select_state(*LEAVE_HOME_STATE) == STATE_OFF: + return PRESET_NONE + + return None + + @property + def preset_modes(self) -> Optional[List[str]]: + """Return a list of available preset modes.""" + return [PRESET_NONE, PRESET_HOLIDAY_MODE] + + async def async_set_preset_mode(self, preset_mode: str) -> None: + """Set new preset mode.""" + if preset_mode == PRESET_HOLIDAY_MODE: + await self._global_control(leave_home=STATE_ON) + + if preset_mode == PRESET_NONE: + await self._global_control(leave_home=STATE_OFF) + + @property + def device_info(self) -> Dict[str, Any]: + """Return the device state attributes.""" + device_info = super().device_info or {} + device_info["manufacturer"] = "Hitachi" + + return device_info + + async def _global_control( + self, + main_operation=None, + target_temperature=None, + fan_mode=None, + hvac_mode=None, + swing_mode=None, + leave_home=None, + ): + """Execute globalControl command with all parameters.""" + if self.device.controllable_name == "ovp:HLinkMainController": + await self.async_execute_command( + COMMAND_GLOBAL_CONTROL, + main_operation + or self._select_state(*MAIN_OPERATION_STATE), # Main Operation + target_temperature + or self._select_state( + CORE_TARGET_TEMPERATURE_STATE + ), # Target Temperature + fan_mode or self._select_state(*FAN_SPEED_STATE), # Fan Mode + hvac_mode or self._select_state(*MODE_CHANGE_STATE), # Mode + swing_mode or self._select_state(*SWING_STATE), # Swing Mode + ) + else: + await self.async_execute_command( + COMMAND_GLOBAL_CONTROL, + main_operation + or self._select_state(*MAIN_OPERATION_STATE), # Main Operation + target_temperature + or self._select_state( + CORE_TARGET_TEMPERATURE_STATE + ), # Target Temperature + fan_mode or self._select_state(*FAN_SPEED_STATE), # Fan Mode + hvac_mode or self._select_state(*MODE_CHANGE_STATE), # Mode + swing_mode or self._select_state(*SWING_STATE), # Swing Mode + leave_home or self._select_state(*LEAVE_HOME_STATE), # Leave Home + ) + + def _select_state(self, *states) -> Optional[str]: + """Make all strings lowercase, since Hi Kumo server returns capitalized strings for some devices.""" + state = self.select_state(*states) + + if state and isinstance(state, str): + return state.lower() + + return state diff --git a/custom_components/tahoma/const.py b/custom_components/tahoma/const.py index cfcbdc2fc..415f4a8cb 100644 --- a/custom_components/tahoma/const.py +++ b/custom_components/tahoma/const.py @@ -73,6 +73,7 @@ "HeatingSetPoint": CLIMATE, # widgetName, uiClass is EvoHome (not supported) "HitachiDHW": WATER_HEATER, # widgetName, uiClass is HitachiHeatingSystem (not supported) "HitachiAirToWaterHeatingZone": CLIMATE, # widgetName, uiClass is HitachiHeatingSystem (not supported) + "HitachiAirToAirHeatPump": CLIMATE, # widgetName, uiClass is HitachiHeatingSystem (not supported) "HumiditySensor": SENSOR, "Light": LIGHT, "LightSensor": SENSOR, diff --git a/custom_components/tahoma/tahoma_entity.py b/custom_components/tahoma/tahoma_entity.py index aea34174d..d5458964d 100644 --- a/custom_components/tahoma/tahoma_entity.py +++ b/custom_components/tahoma/tahoma_entity.py @@ -139,6 +139,7 @@ def device_info(self) -> Dict[str, Any]: def select_command(self, *commands: str) -> Optional[str]: """Select first existing command in a list of commands.""" existing_commands = self.device.definition.commands + return next((c for c in commands if c in existing_commands), None) def has_command(self, *commands: str) -> bool: @@ -173,7 +174,6 @@ def select_attribute(self, *attributes) -> Optional[str]: ), None, ) - return None async def async_execute_command(self, command_name: str, *args: Any): """Execute device command in async context."""