diff --git a/homeassistant/components/esphome/climate.py b/homeassistant/components/esphome/climate.py index b34714ff89c..73b326204b5 100644 --- a/homeassistant/components/esphome/climate.py +++ b/homeassistant/components/esphome/climate.py @@ -164,11 +164,17 @@ class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEnti ) self._attr_min_temp = static_info.visual_min_temperature self._attr_max_temp = static_info.visual_max_temperature + self._attr_min_humidity = round(static_info.visual_min_humidity) + self._attr_max_humidity = round(static_info.visual_max_humidity) features = ClimateEntityFeature(0) if self._static_info.supports_two_point_target_temperature: features |= ClimateEntityFeature.TARGET_TEMPERATURE_RANGE else: features |= ClimateEntityFeature.TARGET_TEMPERATURE + if self._static_info.supports_target_humidity: + features |= ClimateEntityFeature.TARGET_HUMIDITY + if self._static_info.supports_aux_heat: + features |= ClimateEntityFeature.AUX_HEAT if self.preset_modes: features |= ClimateEntityFeature.PRESET_MODE if self.fan_modes: @@ -234,6 +240,14 @@ class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEnti """Return the current temperature.""" return self._state.current_temperature + @property + @esphome_state_property + def current_humidity(self) -> int | None: + """Return the current humidity.""" + if not self._static_info.supports_current_humidity: + return None + return round(self._state.current_humidity) + @property @esphome_state_property def target_temperature(self) -> float | None: @@ -252,6 +266,18 @@ class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEnti """Return the highbound target temperature we try to reach.""" return self._state.target_temperature_high + @property + @esphome_state_property + def target_humidity(self) -> int: + """Return the humidity we try to reach.""" + return round(self._state.target_humidity) + + @property + @esphome_state_property + def is_aux_heat(self) -> bool: + """Return the auxiliary heater state.""" + return self._state.aux_heat + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature (and operation mode if set).""" data: dict[str, Any] = {"key": self._key} @@ -267,6 +293,10 @@ class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEnti data["target_temperature_high"] = kwargs[ATTR_TARGET_TEMP_HIGH] await self._client.climate_command(**data) + async def async_set_humidity(self, humidity: int) -> None: + """Set new target humidity.""" + await self._client.climate_command(key=self._key, target_humidity=humidity) + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set new target operation mode.""" await self._client.climate_command( @@ -296,3 +326,11 @@ class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEnti await self._client.climate_command( key=self._key, swing_mode=_SWING_MODES.from_hass(swing_mode) ) + + async def async_turn_aux_heat_on(self) -> None: + """Turn auxiliary heater on.""" + await self._client.climate_command(key=self._key, aux_heat=True) + + async def async_turn_aux_heat_off(self) -> None: + """Turn auxiliary heater off.""" + await self._client.climate_command(key=self._key, aux_heat=False) diff --git a/tests/components/esphome/test_climate.py b/tests/components/esphome/test_climate.py index 7e00fd22a1c..8f0b8f96c56 100644 --- a/tests/components/esphome/test_climate.py +++ b/tests/components/esphome/test_climate.py @@ -15,8 +15,13 @@ from aioesphomeapi import ( ) from homeassistant.components.climate import ( + ATTR_AUX_HEAT, + ATTR_CURRENT_HUMIDITY, ATTR_FAN_MODE, + ATTR_HUMIDITY, ATTR_HVAC_MODE, + ATTR_MAX_HUMIDITY, + ATTR_MIN_HUMIDITY, ATTR_PRESET_MODE, ATTR_SWING_MODE, ATTR_TARGET_TEMP_HIGH, @@ -24,7 +29,9 @@ from homeassistant.components.climate import ( ATTR_TEMPERATURE, DOMAIN as CLIMATE_DOMAIN, FAN_HIGH, + SERVICE_SET_AUX_HEAT, SERVICE_SET_FAN_MODE, + SERVICE_SET_HUMIDITY, SERVICE_SET_HVAC_MODE, SERVICE_SET_PRESET_MODE, SERVICE_SET_SWING_MODE, @@ -32,7 +39,7 @@ from homeassistant.components.climate import ( SWING_BOTH, HVACMode, ) -from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.const import ATTR_ENTITY_ID, STATE_ON from homeassistant.core import HomeAssistant @@ -312,3 +319,125 @@ async def test_climate_entity_with_step_and_target_temp( [call(key=1, swing_mode=ClimateSwingMode.BOTH)] ) mock_client.climate_command.reset_mock() + + +async def test_climate_entity_with_aux_heat( + hass: HomeAssistant, mock_client: APIClient, mock_generic_device_entry +) -> None: + """Test a generic climate entity with aux heat.""" + entity_info = [ + ClimateInfo( + object_id="myclimate", + key=1, + name="my climate", + unique_id="my_climate", + supports_current_temperature=True, + supports_two_point_target_temperature=True, + supports_action=True, + visual_min_temperature=10.0, + visual_max_temperature=30.0, + supports_aux_heat=True, + ) + ] + states = [ + ClimateState( + key=1, + mode=ClimateMode.HEAT, + action=ClimateAction.HEATING, + current_temperature=30, + target_temperature=20, + fan_mode=ClimateFanMode.AUTO, + swing_mode=ClimateSwingMode.BOTH, + aux_heat=True, + ) + ] + user_service = [] + await mock_generic_device_entry( + mock_client=mock_client, + entity_info=entity_info, + user_service=user_service, + states=states, + ) + state = hass.states.get("climate.test_myclimate") + assert state is not None + assert state.state == HVACMode.HEAT + attributes = state.attributes + assert attributes[ATTR_AUX_HEAT] == STATE_ON + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_AUX_HEAT, + {ATTR_ENTITY_ID: "climate.test_myclimate", ATTR_AUX_HEAT: False}, + blocking=True, + ) + mock_client.climate_command.assert_has_calls([call(key=1, aux_heat=False)]) + mock_client.climate_command.reset_mock() + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_AUX_HEAT, + {ATTR_ENTITY_ID: "climate.test_myclimate", ATTR_AUX_HEAT: True}, + blocking=True, + ) + mock_client.climate_command.assert_has_calls([call(key=1, aux_heat=True)]) + mock_client.climate_command.reset_mock() + + +async def test_climate_entity_with_humidity( + hass: HomeAssistant, mock_client: APIClient, mock_generic_device_entry +) -> None: + """Test a generic climate entity with humidity.""" + entity_info = [ + ClimateInfo( + object_id="myclimate", + key=1, + name="my climate", + unique_id="my_climate", + supports_current_temperature=True, + supports_two_point_target_temperature=True, + supports_action=True, + visual_min_temperature=10.0, + visual_max_temperature=30.0, + supports_current_humidity=True, + supports_target_humidity=True, + visual_min_humidity=10.1, + visual_max_humidity=29.7, + ) + ] + states = [ + ClimateState( + key=1, + mode=ClimateMode.AUTO, + action=ClimateAction.COOLING, + current_temperature=30, + target_temperature=20, + fan_mode=ClimateFanMode.AUTO, + swing_mode=ClimateSwingMode.BOTH, + current_humidity=20.1, + target_humidity=25.7, + ) + ] + user_service = [] + await mock_generic_device_entry( + mock_client=mock_client, + entity_info=entity_info, + user_service=user_service, + states=states, + ) + state = hass.states.get("climate.test_myclimate") + assert state is not None + assert state.state == HVACMode.AUTO + attributes = state.attributes + assert attributes[ATTR_CURRENT_HUMIDITY] == 20 + assert attributes[ATTR_HUMIDITY] == 26 + assert attributes[ATTR_MAX_HUMIDITY] == 30 + assert attributes[ATTR_MIN_HUMIDITY] == 10 + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HUMIDITY, + {ATTR_ENTITY_ID: "climate.test_myclimate", ATTR_HUMIDITY: 23}, + blocking=True, + ) + mock_client.climate_command.assert_has_calls([call(key=1, target_humidity=23)]) + mock_client.climate_command.reset_mock()