diff --git a/homeassistant/components/atag/climate.py b/homeassistant/components/atag/climate.py index 32630329c0f0..ca1681b41605 100644 --- a/homeassistant/components/atag/climate.py +++ b/homeassistant/components/atag/climate.py @@ -51,7 +51,7 @@ class AtagThermostat(AtagEntity, ClimateEntity): self._attr_temperature_unit = coordinator.data.climate.temp_unit @property - def hvac_mode(self) -> str | None: # type: ignore[override] + def hvac_mode(self) -> str | None: """Return hvac operation ie. heat, cool mode.""" if self.coordinator.data.climate.hvac_mode in HVAC_MODES: return self.coordinator.data.climate.hvac_mode diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index 91b4db1fc33a..5c8f27bc5188 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -75,6 +75,7 @@ from .const import ( # noqa: F401 SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_RANGE, ClimateEntityFeature, + HVACMode, ) DEFAULT_MIN_TEMP = 7 @@ -99,7 +100,7 @@ SET_TEMPERATURE_SCHEMA = vol.All( vol.Exclusive(ATTR_TEMPERATURE, "temperature"): vol.Coerce(float), vol.Inclusive(ATTR_TARGET_TEMP_HIGH, "temperature"): vol.Coerce(float), vol.Inclusive(ATTR_TARGET_TEMP_LOW, "temperature"): vol.Coerce(float), - vol.Optional(ATTR_HVAC_MODE): vol.In(HVAC_MODES), + vol.Optional(ATTR_HVAC_MODE): vol.Coerce(HVACMode), } ), ) @@ -116,7 +117,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: component.async_register_entity_service(SERVICE_TURN_OFF, {}, "async_turn_off") component.async_register_entity_service( SERVICE_SET_HVAC_MODE, - {vol.Required(ATTR_HVAC_MODE): vol.In(HVAC_MODES)}, + {vol.Required(ATTR_HVAC_MODE): vol.Coerce(HVACMode)}, "async_set_hvac_mode", ) component.async_register_entity_service( @@ -188,8 +189,8 @@ class ClimateEntity(Entity): _attr_fan_mode: str | None _attr_fan_modes: list[str] | None _attr_hvac_action: str | None = None - _attr_hvac_mode: str - _attr_hvac_modes: list[str] + _attr_hvac_mode: HVACMode | str | None + _attr_hvac_modes: list[HVACMode | str] _attr_is_aux_heat: bool | None _attr_max_humidity: int = DEFAULT_MAX_HUMIDITY _attr_max_temp: float @@ -208,10 +209,15 @@ class ClimateEntity(Entity): _attr_target_temperature: float | None = None _attr_temperature_unit: str + @final @property - def state(self) -> str: + def state(self) -> str | None: """Return the current state.""" - return self.hvac_mode + if self.hvac_mode is None: + return None + if not isinstance(self.hvac_mode, HVACMode): + return HVACMode(self.hvac_mode).value + return self.hvac_mode.value @property def precision(self) -> float: @@ -226,7 +232,7 @@ class ClimateEntity(Entity): def capability_attributes(self) -> dict[str, Any] | None: """Return the capability attributes.""" supported_features = self.supported_features - data = { + data: dict[str, Any] = { ATTR_HVAC_MODES: self.hvac_modes, ATTR_MIN_TEMP: show_temp( self.hass, self.min_temp, self.temperature_unit, self.precision @@ -329,19 +335,13 @@ class ClimateEntity(Entity): return self._attr_target_humidity @property - def hvac_mode(self) -> str: - """Return hvac operation ie. heat, cool mode. - - Need to be one of HVAC_MODE_*. - """ + def hvac_mode(self) -> HVACMode | str | None: + """Return hvac operation ie. heat, cool mode.""" return self._attr_hvac_mode @property - def hvac_modes(self) -> list[str]: - """Return the list of available hvac operation modes. - - Need to be a subset of HVAC_MODES. - """ + def hvac_modes(self) -> list[HVACMode | str]: + """Return the list of available hvac operation modes.""" return self._attr_hvac_modes @property @@ -465,11 +465,11 @@ class ClimateEntity(Entity): """Set new target fan mode.""" await self.hass.async_add_executor_job(self.set_fan_mode, fan_mode) - def set_hvac_mode(self, hvac_mode: str) -> None: + def set_hvac_mode(self, hvac_mode: HVACMode | str) -> None: """Set new target hvac mode.""" raise NotImplementedError() - async def async_set_hvac_mode(self, hvac_mode: str) -> None: + async def async_set_hvac_mode(self, hvac_mode: HVACMode | str) -> None: """Set new target hvac mode.""" await self.hass.async_add_executor_job(self.set_hvac_mode, hvac_mode) @@ -512,7 +512,7 @@ class ClimateEntity(Entity): return # Fake turn on - for mode in (HVAC_MODE_HEAT_COOL, HVAC_MODE_HEAT, HVAC_MODE_COOL): + for mode in (HVACMode.HEAT_COOL, HVACMode.HEAT, HVACMode.COOL): if mode not in self.hvac_modes: continue await self.async_set_hvac_mode(mode) @@ -525,8 +525,8 @@ class ClimateEntity(Entity): return # Fake turn off - if HVAC_MODE_OFF in self.hvac_modes: - await self.async_set_hvac_mode(HVAC_MODE_OFF) + if HVACMode.OFF in self.hvac_modes: + await self.async_set_hvac_mode(HVACMode.OFF) @property def supported_features(self) -> int: diff --git a/homeassistant/components/climate/const.py b/homeassistant/components/climate/const.py index 5ee2424e7f79..8552df943e42 100644 --- a/homeassistant/components/climate/const.py +++ b/homeassistant/components/climate/const.py @@ -2,37 +2,45 @@ from enum import IntEnum -# All activity disabled / Device is off/standby +from homeassistant.backports.enum import StrEnum + + +class HVACMode(StrEnum): + """HVAC mode for climate devices.""" + + # All activity disabled / Device is off/standby + OFF = "off" + + # Heating + HEAT = "heat" + + # Cooling + COOL = "cool" + + # The device supports heating/cooling to a range + HEAT_COOL = "heat_cool" + + # The temperature is set based on a schedule, learned behavior, AI or some + # other related mechanism. User is not able to adjust the temperature + AUTO = "auto" + + # Device is in Dry/Humidity mode + DRY = "dry" + + # Only the fan is on, not fan and another mode like cool + FAN_ONLY = "fan_only" + + +# These HVAC_MODE_* constants are deprecated as of Home Assistant 2022.5. +# Please use the HVACMode enum instead. HVAC_MODE_OFF = "off" - -# Heating HVAC_MODE_HEAT = "heat" - -# Cooling HVAC_MODE_COOL = "cool" - -# The device supports heating/cooling to a range HVAC_MODE_HEAT_COOL = "heat_cool" - -# The temperature is set based on a schedule, learned behavior, AI or some -# other related mechanism. User is not able to adjust the temperature HVAC_MODE_AUTO = "auto" - -# Device is in Dry/Humidity mode HVAC_MODE_DRY = "dry" - -# Only the fan is on, not fan and another mode like cool HVAC_MODE_FAN_ONLY = "fan_only" - -HVAC_MODES = [ - HVAC_MODE_OFF, - HVAC_MODE_HEAT, - HVAC_MODE_COOL, - HVAC_MODE_HEAT_COOL, - HVAC_MODE_AUTO, - HVAC_MODE_DRY, - HVAC_MODE_FAN_ONLY, -] +HVAC_MODES = [cls.value for cls in HVACMode] # No preset is active PRESET_NONE = "none" @@ -149,7 +157,7 @@ class ClimateEntityFeature(IntEnum): # These SUPPORT_* constants are deprecated as of Home Assistant 2022.5. -# Pleease use the ClimateEntityFeature enum instead. +# Please use the ClimateEntityFeature enum instead. SUPPORT_TARGET_TEMPERATURE = 1 SUPPORT_TARGET_TEMPERATURE_RANGE = 2 SUPPORT_TARGET_HUMIDITY = 4 diff --git a/homeassistant/components/climate/group.py b/homeassistant/components/climate/group.py index 3603e37d9703..cec41f81b283 100644 --- a/homeassistant/components/climate/group.py +++ b/homeassistant/components/climate/group.py @@ -5,7 +5,7 @@ from homeassistant.components.group import GroupIntegrationRegistry from homeassistant.const import STATE_OFF from homeassistant.core import HomeAssistant, callback -from .const import HVAC_MODE_OFF, HVAC_MODES +from .const import HVAC_MODES, HVACMode @callback @@ -14,6 +14,6 @@ def async_describe_on_off_states( ) -> None: """Describe group on off states.""" registry.on_off_states( - set(HVAC_MODES) - {HVAC_MODE_OFF}, + set(HVAC_MODES) - {HVACMode.OFF}, STATE_OFF, ) diff --git a/homeassistant/components/demo/climate.py b/homeassistant/components/demo/climate.py index 750354a567ad..ff6a535f4f95 100644 --- a/homeassistant/components/demo/climate.py +++ b/homeassistant/components/demo/climate.py @@ -7,13 +7,8 @@ from homeassistant.components.climate.const import ( ATTR_TARGET_TEMP_LOW, CURRENT_HVAC_COOL, CURRENT_HVAC_HEAT, - HVAC_MODE_AUTO, - HVAC_MODE_COOL, - HVAC_MODE_HEAT, - HVAC_MODE_HEAT_COOL, - HVAC_MODE_OFF, - HVAC_MODES, ClimateEntityFeature, + HVACMode, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT @@ -47,12 +42,12 @@ async def async_setup_platform( target_humidity=None, current_humidity=None, swing_mode=None, - hvac_mode=HVAC_MODE_HEAT, + hvac_mode=HVACMode.HEAT, hvac_action=CURRENT_HVAC_HEAT, aux=None, target_temp_high=None, target_temp_low=None, - hvac_modes=[HVAC_MODE_HEAT, HVAC_MODE_OFF], + hvac_modes=[HVACMode.HEAT, HVACMode.OFF], ), DemoClimate( unique_id="climate_2", @@ -65,12 +60,12 @@ async def async_setup_platform( target_humidity=67, current_humidity=54, swing_mode="Off", - hvac_mode=HVAC_MODE_COOL, + hvac_mode=HVACMode.COOL, hvac_action=CURRENT_HVAC_COOL, aux=False, target_temp_high=None, target_temp_low=None, - hvac_modes=[mode for mode in HVAC_MODES if mode != HVAC_MODE_HEAT_COOL], + hvac_modes=[cls.value for cls in HVACMode if cls != HVACMode.HEAT_COOL], ), DemoClimate( unique_id="climate_3", @@ -84,12 +79,12 @@ async def async_setup_platform( target_humidity=None, current_humidity=None, swing_mode="Auto", - hvac_mode=HVAC_MODE_HEAT_COOL, + hvac_mode=HVACMode.HEAT_COOL, hvac_action=None, aux=None, target_temp_high=24, target_temp_low=21, - hvac_modes=[HVAC_MODE_HEAT_COOL, HVAC_MODE_COOL, HVAC_MODE_HEAT], + hvac_modes=[cls.value for cls in HVACMode if cls != HVACMode.HEAT], ), ] ) @@ -147,7 +142,7 @@ class DemoClimate(ClimateEntity): self._support_flags = self._support_flags | ClimateEntityFeature.SWING_MODE if aux is not None: self._support_flags = self._support_flags | ClimateEntityFeature.AUX_HEAT - if HVAC_MODE_HEAT_COOL in hvac_modes or HVAC_MODE_AUTO in hvac_modes: + if HVACMode.HEAT_COOL in hvac_modes or HVACMode.AUTO in hvac_modes: self._support_flags = ( self._support_flags | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE ) diff --git a/homeassistant/components/esphome/climate.py b/homeassistant/components/esphome/climate.py index 0707d9886deb..a1fed8a50b7d 100644 --- a/homeassistant/components/esphome/climate.py +++ b/homeassistant/components/esphome/climate.py @@ -230,7 +230,7 @@ class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEnti return features @esphome_state_property - def hvac_mode(self) -> str | None: # type: ignore[override] + def hvac_mode(self) -> str | None: """Return current operation ie. heat, cool, idle.""" return _CLIMATE_MODES.from_esphome(self._state.mode) diff --git a/tests/components/demo/test_climate.py b/tests/components/demo/test_climate.py index 2f4317c49c5c..1d130203bb1e 100644 --- a/tests/components/demo/test_climate.py +++ b/tests/components/demo/test_climate.py @@ -22,9 +22,6 @@ from homeassistant.components.climate.const import ( ATTR_TARGET_TEMP_LOW, CURRENT_HVAC_COOL, DOMAIN, - HVAC_MODE_COOL, - HVAC_MODE_HEAT, - HVAC_MODE_OFF, PRESET_AWAY, PRESET_ECO, SERVICE_SET_AUX_HEAT, @@ -34,6 +31,7 @@ from homeassistant.components.climate.const import ( SERVICE_SET_PRESET_MODE, SERVICE_SET_SWING_MODE, SERVICE_SET_TEMPERATURE, + HVACMode, ) from homeassistant.const import ( ATTR_ENTITY_ID, @@ -62,7 +60,7 @@ async def setup_demo_climate(hass): def test_setup_params(hass): """Test the initial parameters.""" state = hass.states.get(ENTITY_CLIMATE) - assert state.state == HVAC_MODE_COOL + assert state.state == HVACMode.COOL assert state.attributes.get(ATTR_TEMPERATURE) == 21 assert state.attributes.get(ATTR_CURRENT_TEMPERATURE) == 22 assert state.attributes.get(ATTR_FAN_MODE) == "On High" @@ -71,12 +69,12 @@ def test_setup_params(hass): assert state.attributes.get(ATTR_SWING_MODE) == "Off" assert state.attributes.get(ATTR_AUX_HEAT) == STATE_OFF assert state.attributes.get(ATTR_HVAC_MODES) == [ - "off", - "heat", - "cool", - "auto", - "dry", - "fan_only", + HVACMode.OFF, + HVACMode.HEAT, + HVACMode.COOL, + HVACMode.AUTO, + HVACMode.DRY, + HVACMode.FAN_ONLY, ] @@ -293,7 +291,7 @@ async def test_set_hvac_bad_attr_and_state(hass): """ state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get(ATTR_HVAC_ACTION) == CURRENT_HVAC_COOL - assert state.state == HVAC_MODE_COOL + assert state.state == HVACMode.COOL with pytest.raises(vol.Invalid): await hass.services.async_call( @@ -305,23 +303,23 @@ async def test_set_hvac_bad_attr_and_state(hass): state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get(ATTR_HVAC_ACTION) == CURRENT_HVAC_COOL - assert state.state == HVAC_MODE_COOL + assert state.state == HVACMode.COOL async def test_set_hvac(hass): """Test setting of new hvac mode.""" state = hass.states.get(ENTITY_CLIMATE) - assert state.state == HVAC_MODE_COOL + assert state.state == HVACMode.COOL await hass.services.async_call( DOMAIN, SERVICE_SET_HVAC_MODE, - {ATTR_ENTITY_ID: ENTITY_CLIMATE, ATTR_HVAC_MODE: HVAC_MODE_HEAT}, + {ATTR_ENTITY_ID: ENTITY_CLIMATE, ATTR_HVAC_MODE: HVACMode.HEAT}, blocking=True, ) state = hass.states.get(ENTITY_CLIMATE) - assert state.state == HVAC_MODE_HEAT + assert state.state == HVACMode.HEAT async def test_set_hold_mode_away(hass): @@ -398,18 +396,18 @@ async def test_turn_on(hass): await hass.services.async_call( DOMAIN, SERVICE_SET_HVAC_MODE, - {ATTR_ENTITY_ID: ENTITY_CLIMATE, ATTR_HVAC_MODE: HVAC_MODE_OFF}, + {ATTR_ENTITY_ID: ENTITY_CLIMATE, ATTR_HVAC_MODE: HVACMode.OFF}, blocking=True, ) state = hass.states.get(ENTITY_CLIMATE) - assert state.state == HVAC_MODE_OFF + assert state.state == HVACMode.OFF await hass.services.async_call( DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_CLIMATE}, blocking=True ) state = hass.states.get(ENTITY_CLIMATE) - assert state.state == HVAC_MODE_HEAT + assert state.state == HVACMode.HEAT async def test_turn_off(hass): @@ -417,15 +415,15 @@ async def test_turn_off(hass): await hass.services.async_call( DOMAIN, SERVICE_SET_HVAC_MODE, - {ATTR_ENTITY_ID: ENTITY_CLIMATE, ATTR_HVAC_MODE: HVAC_MODE_HEAT}, + {ATTR_ENTITY_ID: ENTITY_CLIMATE, ATTR_HVAC_MODE: HVACMode.HEAT}, blocking=True, ) state = hass.states.get(ENTITY_CLIMATE) - assert state.state == HVAC_MODE_HEAT + assert state.state == HVACMode.HEAT await hass.services.async_call( DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_CLIMATE}, blocking=True ) state = hass.states.get(ENTITY_CLIMATE) - assert state.state == HVAC_MODE_OFF + assert state.state == HVACMode.OFF diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index 93249e768754..1228b0c575b3 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -212,7 +212,7 @@ async def test_set_operation_bad_attr_and_state(hass, mqtt_mock, caplog): with pytest.raises(vol.Invalid) as excinfo: await common.async_set_hvac_mode(hass, None, ENTITY_CLIMATE) assert ( - "value must be one of ['auto', 'cool', 'dry', 'fan_only', 'heat', 'heat_cool', 'off'] for dictionary value @ data['hvac_mode']" + "expected HVACMode or one of 'off', 'heat', 'cool', 'heat_cool', 'auto', 'dry', 'fan_only' for dictionary value @ data['hvac_mode']" ) in str(excinfo.value) state = hass.states.get(ENTITY_CLIMATE) assert state.state == "off"