Skip to content

Commit

Permalink
Add TURN_OFF and TURN_ON to ClimateEntityFeature (#101673)
Browse files Browse the repository at this point in the history
* Add ClimateEntityFeature.TURN_OFF

* Fixes

* Fixes

* wording

* Change to services

* Fixing

* Fixing

* Last bits

* Review comments

* Add hvac_modes checks

* Fixes

* Add tests

* Review comments

* Update snapshots

* balboa

* coolmaster

* ecobee

* mqtt

* nest

* plugwise

* smarttub

* whirlpool

* zwave_js

* fix test climate

* test climate

* zwave

* nexia

* nuheat

* venstar

* tado

* smartthings

* self.hvac_modes not None

* more tests

* homekit_controller

* homekit controller snapshot
  • Loading branch information
gjohansson-ST committed Jan 30, 2024
1 parent cece117 commit bc720b4
Show file tree
Hide file tree
Showing 31 changed files with 534 additions and 110 deletions.
143 changes: 124 additions & 19 deletions homeassistant/components/climate/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Provides functionality to interact with climate devices."""
from __future__ import annotations

import asyncio
from datetime import timedelta
import functools as ft
import logging
Expand Down Expand Up @@ -34,6 +35,7 @@
)
from homeassistant.helpers.entity import Entity, EntityDescription
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.entity_platform import EntityPlatform
from homeassistant.helpers.temperature import display_temp as show_temp
from homeassistant.helpers.typing import ConfigType
from homeassistant.util.unit_conversion import TemperatureConverter
Expand Down Expand Up @@ -152,8 +154,18 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
)
await component.async_setup(config)

component.async_register_entity_service(SERVICE_TURN_ON, {}, "async_turn_on")
component.async_register_entity_service(SERVICE_TURN_OFF, {}, "async_turn_off")
component.async_register_entity_service(
SERVICE_TURN_ON,
{},
"async_turn_on",
[ClimateEntityFeature.TURN_ON],
)
component.async_register_entity_service(
SERVICE_TURN_OFF,
{},
"async_turn_off",
[ClimateEntityFeature.TURN_OFF],
)
component.async_register_entity_service(
SERVICE_SET_HVAC_MODE,
{vol.Required(ATTR_HVAC_MODE): vol.Coerce(HVACMode)},
Expand Down Expand Up @@ -288,6 +300,102 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
_attr_target_temperature: float | None = None
_attr_temperature_unit: str

__mod_supported_features: ClimateEntityFeature = ClimateEntityFeature(0)

def __getattribute__(self, __name: str) -> Any:
"""Get attribute.
Modify return of `supported_features` to
include `_mod_supported_features` if attribute is set.
"""
if __name != "supported_features":
return super().__getattribute__(__name)

# Convert the supported features to ClimateEntityFeature.
# Remove this compatibility shim in 2025.1 or later.
_supported_features = super().__getattribute__(__name)
if type(_supported_features) is int: # noqa: E721
new_features = ClimateEntityFeature(_supported_features)
self._report_deprecated_supported_features_values(new_features)

# Add automatically calculated ClimateEntityFeature.TURN_OFF/TURN_ON to
# supported features and return it
return _supported_features | super().__getattribute__(
"_ClimateEntity__mod_supported_features"
)

@callback
def add_to_platform_start(
self,
hass: HomeAssistant,
platform: EntityPlatform,
parallel_updates: asyncio.Semaphore | None,
) -> None:
"""Start adding an entity to a platform."""
super().add_to_platform_start(hass, platform, parallel_updates)

def _report_turn_on_off(feature: str, method: str) -> None:
"""Log warning not implemented turn on/off feature."""
report_issue = self._suggest_report_issue()
if feature.startswith("TURN"):
message = (
"Entity %s (%s) does not set ClimateEntityFeature.%s"
" but implements the %s method. Please %s"
)
else:
message = (
"Entity %s (%s) implements HVACMode(s): %s and therefore implicitly"
" supports the %s service without setting the proper"
" ClimateEntityFeature. Please %s"
)
_LOGGER.warning(
message,
self.entity_id,
type(self),
feature,
feature.lower(),
report_issue,
)

# Adds ClimateEntityFeature.TURN_OFF/TURN_ON depending on service calls implemented
# This should be removed in 2025.1.
if not self.supported_features & ClimateEntityFeature.TURN_OFF:
if (
type(self).async_turn_off is not ClimateEntity.async_turn_off
or type(self).turn_off is not ClimateEntity.turn_off
):
# turn_off implicitly supported by implementing turn_off method
_report_turn_on_off("TURN_OFF", "turn_off")
self.__mod_supported_features |= ( # pylint: disable=unused-private-member
ClimateEntityFeature.TURN_OFF
)
elif self.hvac_modes and HVACMode.OFF in self.hvac_modes:
# turn_off implicitly supported by including HVACMode.OFF
_report_turn_on_off("off", "turn_off")
self.__mod_supported_features |= ( # pylint: disable=unused-private-member
ClimateEntityFeature.TURN_OFF
)

if not self.supported_features & ClimateEntityFeature.TURN_ON:
if (
type(self).async_turn_on is not ClimateEntity.async_turn_on
or type(self).turn_on is not ClimateEntity.turn_on
):
# turn_on implicitly supported by implementing turn_on method
_report_turn_on_off("TURN_ON", "turn_on")
self.__mod_supported_features |= ( # pylint: disable=unused-private-member
ClimateEntityFeature.TURN_ON
)
elif self.hvac_modes and any(
_mode != HVACMode.OFF and _mode is not None for _mode in self.hvac_modes
):
# turn_on implicitly supported by including any other HVACMode than HVACMode.OFF
_modes = [_mode for _mode in self.hvac_modes if _mode != HVACMode.OFF]
_report_turn_on_off(", ".join(_modes or []), "turn_on")
self.__mod_supported_features |= ( # pylint: disable=unused-private-member
ClimateEntityFeature.TURN_ON
)

@final
@property
def state(self) -> str | None:
Expand All @@ -312,7 +420,7 @@ def precision(self) -> float:
@property
def capability_attributes(self) -> dict[str, Any] | None:
"""Return the capability attributes."""
supported_features = self.supported_features_compat
supported_features = self.supported_features
temperature_unit = self.temperature_unit
precision = self.precision
hass = self.hass
Expand Down Expand Up @@ -345,7 +453,7 @@ def capability_attributes(self) -> dict[str, Any] | None:
@property
def state_attributes(self) -> dict[str, Any]:
"""Return the optional state attributes."""
supported_features = self.supported_features_compat
supported_features = self.supported_features
temperature_unit = self.temperature_unit
precision = self.precision
hass = self.hass
Expand Down Expand Up @@ -625,9 +733,14 @@ async def async_turn_aux_heat_off(self) -> None:
"""Turn auxiliary heater off."""
await self.hass.async_add_executor_job(self.turn_aux_heat_off)

def turn_on(self) -> None:
"""Turn the entity on."""
raise NotImplementedError

async def async_turn_on(self) -> None:
"""Turn the entity on."""
if hasattr(self, "turn_on"):
# Forward to self.turn_on if it's been overridden.
if type(self).turn_on is not ClimateEntity.turn_on:
await self.hass.async_add_executor_job(self.turn_on)
return

Expand All @@ -646,9 +759,14 @@ async def async_turn_on(self) -> None:
await self.async_set_hvac_mode(mode)
break

def turn_off(self) -> None:
"""Turn the entity off."""
raise NotImplementedError

async def async_turn_off(self) -> None:
"""Turn the entity off."""
if hasattr(self, "turn_off"):
# Forward to self.turn_on if it's been overridden.
if type(self).turn_off is not ClimateEntity.turn_off:
await self.hass.async_add_executor_job(self.turn_off)
return

Expand All @@ -661,19 +779,6 @@ def supported_features(self) -> ClimateEntityFeature:
"""Return the list of supported features."""
return self._attr_supported_features

@property
def supported_features_compat(self) -> ClimateEntityFeature:
"""Return the supported features as ClimateEntityFeature.
Remove this compatibility shim in 2025.1 or later.
"""
features = self.supported_features
if type(features) is int: # noqa: E721
new_features = ClimateEntityFeature(features)
self._report_deprecated_supported_features_values(new_features)
return new_features
return features

@cached_property
def min_temp(self) -> float:
"""Return the minimum temperature."""
Expand Down
2 changes: 2 additions & 0 deletions homeassistant/components/climate/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,8 @@ class ClimateEntityFeature(IntFlag):
PRESET_MODE = 16
SWING_MODE = 32
AUX_HEAT = 64
TURN_OFF = 128
TURN_ON = 256


# These SUPPORT_* constants are deprecated as of Home Assistant 2022.5.
Expand Down
4 changes: 4 additions & 0 deletions homeassistant/components/climate/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,12 @@ turn_on:
target:
entity:
domain: climate
supported_features:
- climate.ClimateEntityFeature.TURN_ON

turn_off:
target:
entity:
domain: climate
supported_features:
- climate.ClimateEntityFeature.TURN_OFF
2 changes: 1 addition & 1 deletion tests/components/advantage_air/snapshots/test_climate.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
]),
'max_temp': 32,
'min_temp': 16,
'supported_features': <ClimateEntityFeature: 11>,
'supported_features': <ClimateEntityFeature: 395>,
'target_temp_high': 24,
'target_temp_low': 20,
'target_temp_step': 1,
Expand Down
12 changes: 10 additions & 2 deletions tests/components/balboa/test_climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,10 @@ async def test_spa_defaults(
assert state
assert (
state.attributes["supported_features"]
== ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE
== ClimateEntityFeature.TARGET_TEMPERATURE
| ClimateEntityFeature.PRESET_MODE
| ClimateEntityFeature.TURN_OFF
| ClimateEntityFeature.TURN_ON
)
assert state.state == HVACMode.HEAT
assert state.attributes[ATTR_MIN_TEMP] == 10.0
Expand All @@ -71,7 +74,10 @@ async def test_spa_defaults_fake_tscale(
assert state
assert (
state.attributes["supported_features"]
== ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE
== ClimateEntityFeature.TARGET_TEMPERATURE
| ClimateEntityFeature.PRESET_MODE
| ClimateEntityFeature.TURN_OFF
| ClimateEntityFeature.TURN_ON
)
assert state.state == HVACMode.HEAT
assert state.attributes[ATTR_MIN_TEMP] == 10.0
Expand Down Expand Up @@ -174,6 +180,8 @@ async def test_spa_with_blower(hass: HomeAssistant, client: MagicMock) -> None:
== ClimateEntityFeature.TARGET_TEMPERATURE
| ClimateEntityFeature.PRESET_MODE
| ClimateEntityFeature.FAN_MODE
| ClimateEntityFeature.TURN_OFF
| ClimateEntityFeature.TURN_ON
)
assert state.state == HVACMode.HEAT
assert state.attributes[ATTR_MIN_TEMP] == 10.0
Expand Down
16 changes: 8 additions & 8 deletions tests/components/ccm15/snapshots/test_climate.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
'original_name': None,
'platform': 'ccm15',
'previous_unique_id': None,
'supported_features': <ClimateEntityFeature: 41>,
'supported_features': <ClimateEntityFeature: 425>,
'translation_key': None,
'unique_id': '1.1.1.1.0',
'unit_of_measurement': None,
Expand Down Expand Up @@ -97,7 +97,7 @@
'original_name': None,
'platform': 'ccm15',
'previous_unique_id': None,
'supported_features': <ClimateEntityFeature: 41>,
'supported_features': <ClimateEntityFeature: 425>,
'translation_key': None,
'unique_id': '1.1.1.1.1',
'unit_of_measurement': None,
Expand Down Expand Up @@ -125,7 +125,7 @@
]),
'max_temp': 35,
'min_temp': 7,
'supported_features': <ClimateEntityFeature: 41>,
'supported_features': <ClimateEntityFeature: 425>,
'swing_mode': 'off',
'swing_modes': list([
'off',
Expand Down Expand Up @@ -163,7 +163,7 @@
]),
'max_temp': 35,
'min_temp': 7,
'supported_features': <ClimateEntityFeature: 41>,
'supported_features': <ClimateEntityFeature: 425>,
'swing_mode': 'off',
'swing_modes': list([
'off',
Expand Down Expand Up @@ -225,7 +225,7 @@
'original_name': None,
'platform': 'ccm15',
'previous_unique_id': None,
'supported_features': <ClimateEntityFeature: 41>,
'supported_features': <ClimateEntityFeature: 425>,
'translation_key': None,
'unique_id': '1.1.1.1.0',
'unit_of_measurement': None,
Expand Down Expand Up @@ -277,7 +277,7 @@
'original_name': None,
'platform': 'ccm15',
'previous_unique_id': None,
'supported_features': <ClimateEntityFeature: 41>,
'supported_features': <ClimateEntityFeature: 425>,
'translation_key': None,
'unique_id': '1.1.1.1.1',
'unit_of_measurement': None,
Expand All @@ -302,7 +302,7 @@
]),
'max_temp': 35,
'min_temp': 7,
'supported_features': <ClimateEntityFeature: 41>,
'supported_features': <ClimateEntityFeature: 425>,
'swing_modes': list([
'off',
'on',
Expand Down Expand Up @@ -335,7 +335,7 @@
]),
'max_temp': 35,
'min_temp': 7,
'supported_features': <ClimateEntityFeature: 41>,
'supported_features': <ClimateEntityFeature: 425>,
'swing_modes': list([
'off',
'on',
Expand Down

0 comments on commit bc720b4

Please sign in to comment.