Not valid hvac modes now fails in Climate (#145242)

* Not valid hvac modes now fails

* Fix some tests

* Some more

* More

* fix ruff

* HVAC

* Fritzbox

* Clean up

* Use dict[key]

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
pull/146599/head^2
G Johansson 2025-06-12 07:15:07 +02:00 committed by GitHub
parent 8bf562b7b6
commit 25e6eab008
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
32 changed files with 63 additions and 127 deletions

View File

@ -27,7 +27,6 @@ from homeassistant.helpers.entity import Entity, EntityDescription
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.temperature import display_temp as show_temp
from homeassistant.helpers.typing import ConfigType
from homeassistant.loader import async_suggest_report_issue
from homeassistant.util.hass_dict import HassKey
from homeassistant.util.unit_conversion import TemperatureConverter
@ -535,26 +534,6 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
return
modes_str: str = ", ".join(modes) if modes else ""
translation_key = f"not_valid_{mode_type}_mode"
if mode_type == "hvac":
report_issue = async_suggest_report_issue(
self.hass,
integration_domain=self.platform.platform_name,
module=type(self).__module__,
)
_LOGGER.warning(
(
"%s::%s sets the hvac_mode %s which is not "
"valid for this entity with modes: %s. "
"This will stop working in 2025.4 and raise an error instead. "
"Please %s"
),
self.platform.platform_name,
self.__class__.__name__,
mode,
modes_str,
report_issue,
)
return
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key=translation_key,

View File

@ -258,6 +258,9 @@
"not_valid_preset_mode": {
"message": "Preset mode {mode} is not valid. Valid preset modes are: {modes}."
},
"not_valid_hvac_mode": {
"message": "HVAC mode {mode} is not valid. Valid HVAC modes are: {modes}."
},
"not_valid_swing_mode": {
"message": "Swing mode {mode} is not valid. Valid swing modes are: {modes}."
},

View File

@ -164,8 +164,6 @@ class DeconzThermostat(DeconzDevice[Thermostat], ClimateEntity):
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set new target hvac mode."""
if hvac_mode not in self._attr_hvac_modes:
raise ValueError(f"Unsupported HVAC mode {hvac_mode}")
if len(self._attr_hvac_modes) == 2: # Only allow turn on and off thermostat
await self.hub.api.sensors.thermostat.set_config(

View File

@ -125,8 +125,6 @@ class Device(CoordinatorEntity[FreedomproDataUpdateCoordinator], ClimateEntity):
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Async function to set mode to climate."""
if hvac_mode not in SUPPORTED_HVAC_MODES:
raise ValueError(f"Got unsupported hvac_mode {hvac_mode}")
payload = {"heatingCoolingState": HVAC_INVERT_MAP[hvac_mode]}
await put_state(

View File

@ -216,8 +216,6 @@ class HomematicipHeatingGroup(HomematicipGenericEntity, ClimateEntity):
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set new target hvac mode."""
if hvac_mode not in self.hvac_modes:
return
if hvac_mode == HVACMode.AUTO:
await self._device.set_control_mode_async(HMIP_AUTOMATIC_CM)

View File

@ -133,8 +133,6 @@ class MaxCubeClimate(ClimateEntity):
self._set_target(MAX_DEVICE_MODE_MANUAL, temp)
elif hvac_mode == HVACMode.AUTO:
self._set_target(MAX_DEVICE_MODE_AUTOMATIC, None)
else:
raise ValueError(f"unsupported HVAC mode {hvac_mode}")
def _set_target(self, mode: int | None, temp: float | None) -> None:
"""Set the mode and/or temperature of the thermostat.

View File

@ -267,8 +267,6 @@ class ThermostatEntity(ClimateEntity):
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set new target hvac mode."""
if hvac_mode not in self.hvac_modes:
raise ValueError(f"Unsupported hvac_mode '{hvac_mode}'")
api_mode = THERMOSTAT_INV_MODE_MAP[hvac_mode]
trait = self._device.traits[ThermostatModeTrait.NAME]
try:

View File

@ -15,7 +15,6 @@ from homeassistant.components.climate import (
)
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN, MASTER_THERMOSTATS
@ -216,17 +215,6 @@ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity):
@plugwise_command
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set the hvac mode."""
if hvac_mode not in self.hvac_modes:
hvac_modes = ", ".join(self.hvac_modes)
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="unsupported_hvac_mode_requested",
translation_placeholders={
"hvac_mode": hvac_mode,
"hvac_modes": hvac_modes,
},
)
if hvac_mode == self.hvac_mode:
return

View File

@ -316,9 +316,6 @@
},
"unsupported_firmware": {
"message": "[%key:component::plugwise::config::error::unsupported%]"
},
"unsupported_hvac_mode_requested": {
"message": "Unsupported mode {hvac_mode} requested, valid modes are: {hvac_modes}."
}
}
}

View File

@ -164,12 +164,6 @@ class TeslaFleetClimateEntity(TeslaFleetVehicleEntity, ClimateEntity):
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set the climate mode and state."""
if hvac_mode not in self.hvac_modes:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="invalid_hvac_mode",
translation_placeholders={"hvac_mode": hvac_mode},
)
if hvac_mode == HVACMode.OFF:
await self.async_turn_off()
else:

View File

@ -579,9 +579,6 @@
"invalid_cop_temp": {
"message": "Cabin overheat protection does not support that temperature."
},
"invalid_hvac_mode": {
"message": "Climate mode {hvac_mode} is not supported."
},
"missing_temperature": {
"message": "Temperature is required for this action."
},

View File

@ -21,11 +21,10 @@ from homeassistant.components.climate import (
)
from homeassistant.const import PRECISION_TENTHS, UnitOfTemperature
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import TPLinkConfigEntry, legacy_device_id
from .const import DOMAIN, UNIT_MAPPING
from .const import UNIT_MAPPING
from .coordinator import TPLinkDataUpdateCoordinator
from .entity import (
CoordinatedTPLinkModuleEntity,
@ -161,14 +160,6 @@ class TPLinkClimateEntity(CoordinatedTPLinkModuleEntity, ClimateEntity):
await self._thermostat_module.set_state(True)
elif hvac_mode is HVACMode.OFF:
await self._thermostat_module.set_state(False)
else:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="unsupported_mode",
translation_placeholders={
"mode": hvac_mode,
},
)
@async_refresh_after
async def async_turn_on(self) -> None:

View File

@ -487,9 +487,6 @@
"unexpected_device": {
"message": "Unexpected device found at {host}; expected {expected}, found {found}"
},
"unsupported_mode": {
"message": "Tried to set unsupported mode: {mode}"
},
"invalid_alarm_duration": {
"message": "Invalid duration {duration} available: 1-{duration_max}s"
}

View File

@ -130,9 +130,7 @@ class AirConEntity(WhirlpoolEntity, ClimateEntity):
await self._appliance.set_power_on(False)
return
if not (mode := HVAC_MODE_TO_AIRCON_MODE.get(hvac_mode)):
raise ValueError(f"Invalid hvac mode {hvac_mode}")
mode = HVAC_MODE_TO_AIRCON_MODE[hvac_mode]
await self._appliance.set_mode(mode)
if not self._appliance.get_power_on():
await self._appliance.set_power_on(True)

View File

@ -492,8 +492,6 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity):
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set new target hvac mode."""
if (hvac_mode_id := self._hvac_modes.get(hvac_mode)) is None:
raise ValueError(f"Received an invalid hvac mode: {hvac_mode}")
if not self._current_mode:
# Thermostat(valve) has no support for setting a mode, so we make it a no-op
@ -503,7 +501,7 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity):
# can set it again when turning the device back on.
if hvac_mode == HVACMode.OFF and self._current_mode.value != ThermostatMode.OFF:
self._last_hvac_mode_id_before_off = self._current_mode.value
await self._async_set_value(self._current_mode, hvac_mode_id)
await self._async_set_value(self._current_mode, self._hvac_modes[hvac_mode])
async def async_turn_off(self) -> None:
"""Turn the entity off."""

View File

@ -177,7 +177,7 @@ async def test_climate_myzone_zone(
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_HVAC_MODE,
{ATTR_ENTITY_ID: [entity_id], ATTR_HVAC_MODE: HVACMode.FAN_ONLY},
{ATTR_ENTITY_ID: [entity_id], ATTR_HVAC_MODE: HVACMode.HEAT_COOL},
blocking=True,
)
mock_update.assert_called_once()

View File

@ -127,9 +127,6 @@ async def test_spa_hvac_action(
state = await _patch_spa_heatstate(hass, client, 1)
assert state.attributes[ATTR_HVAC_ACTION] == HVACAction.HEATING
state = await _patch_spa_heatstate(hass, client, 2)
assert state.attributes[ATTR_HVAC_ACTION] == HVACAction.IDLE
async def test_spa_preset_modes(
hass: HomeAssistant, client: MagicMock, integration: MockConfigEntry

View File

@ -323,22 +323,23 @@ async def test_mode_validation(
assert state.attributes.get(ATTR_SWING_MODE) == "off"
assert state.attributes.get(ATTR_SWING_HORIZONTAL_MODE) == "off"
await hass.services.async_call(
DOMAIN,
SERVICE_SET_HVAC_MODE,
{
"entity_id": "climate.test",
"hvac_mode": "auto",
},
blocking=True,
)
with pytest.raises(
ServiceValidationError,
match="HVAC mode auto is not valid. Valid HVAC modes are: off, heat",
) as exc:
await hass.services.async_call(
DOMAIN,
SERVICE_SET_HVAC_MODE,
{
"entity_id": "climate.test",
"hvac_mode": "auto",
},
blocking=True,
)
assert (
"MockClimateEntity sets the hvac_mode auto which is not valid "
"for this entity with modes: off, heat. This will stop working "
"in 2025.4 and raise an error instead. "
"Please" in caplog.text
str(exc.value) == "HVAC mode auto is not valid. Valid HVAC modes are: off, heat"
)
assert exc.value.translation_key == "not_valid_hvac_mode"
with pytest.raises(
ServiceValidationError,

View File

@ -136,7 +136,7 @@ async def test_simple_climate_device(
# Service set HVAC mode to unsupported value
with pytest.raises(ValueError):
with pytest.raises(ServiceValidationError):
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_HVAC_MODE,
@ -239,7 +239,7 @@ async def test_climate_device_without_cooling_support(
# Service set HVAC mode to unsupported value
with pytest.raises(ValueError):
with pytest.raises(ServiceValidationError):
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_HVAC_MODE,

View File

@ -19,6 +19,7 @@ from homeassistant.components.climate import (
)
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.util.dt import utcnow
@ -135,7 +136,7 @@ async def test_climate_set_unsupported_hvac_mode(
assert entry
assert entry.unique_id == uid
with pytest.raises(ValueError):
with pytest.raises(ServiceValidationError):
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_HVAC_MODE,

View File

@ -896,7 +896,7 @@ async def test_heating_cooling_switch_toggles_when_outside_min_cycle_duration(
"expected_triggered_service_call",
),
[
(True, HVACMode.COOL, False, 30, 25, HVACMode.HEAT, SERVICE_TURN_ON),
(True, HVACMode.COOL, False, 30, 25, HVACMode.COOL, SERVICE_TURN_ON),
(True, HVACMode.COOL, True, 25, 30, HVACMode.OFF, SERVICE_TURN_OFF),
(False, HVACMode.HEAT, False, 25, 30, HVACMode.HEAT, SERVICE_TURN_ON),
(False, HVACMode.HEAT, True, 30, 25, HVACMode.OFF, SERVICE_TURN_OFF),

View File

@ -205,13 +205,14 @@ async def test_hmip_heating_group_heat(
ha_state = hass.states.get(entity_id)
assert ha_state.state == HVACMode.AUTO
# hvac mode "dry" is not available. expect a valueerror.
await hass.services.async_call(
"climate",
"set_hvac_mode",
{"entity_id": entity_id, "hvac_mode": "dry"},
blocking=True,
)
# hvac mode "dry" is not available.
with pytest.raises(ServiceValidationError):
await hass.services.async_call(
"climate",
"set_hvac_mode",
{"entity_id": entity_id, "hvac_mode": "dry"},
blocking=True,
)
assert len(hmip_device.mock_calls) == service_call_counter + 24
# Only fire event from last async_manipulate_test_data available.

View File

@ -179,7 +179,7 @@ async def test_thermostat_set_invalid_hvac_mode(
hass: HomeAssistant, cube: MaxCube, thermostat: MaxThermostat
) -> None:
"""Set hvac mode to heat."""
with pytest.raises(ValueError):
with pytest.raises(ServiceValidationError):
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_HVAC_MODE,

View File

@ -405,13 +405,6 @@ async def test_turn_on_and_off_optimistic_with_power_command(
"heat",
None,
),
(
help_custom_config(
climate.DOMAIN, DEFAULT_CONFIG, ({"modes": ["off", "dry"]},)
),
None,
"off",
),
(
help_custom_config(
climate.DOMAIN, DEFAULT_CONFIG, ({"modes": ["off", "cool"]},)

View File

@ -520,7 +520,7 @@ async def test_thermostat_invalid_hvac_mode(
assert thermostat.state == HVACMode.OFF
assert thermostat.attributes[ATTR_HVAC_ACTION] == HVACAction.OFF
with pytest.raises(ValueError):
with pytest.raises(ServiceValidationError):
await common.async_set_hvac_mode(hass, HVACMode.DRY)
assert thermostat.state == HVACMode.OFF
@ -1396,7 +1396,7 @@ async def test_thermostat_unexpected_hvac_status(
assert ATTR_FAN_MODE not in thermostat.attributes
assert ATTR_FAN_MODES not in thermostat.attributes
with pytest.raises(ValueError):
with pytest.raises(ServiceValidationError):
await common.async_set_hvac_mode(hass, HVACMode.DRY)
assert thermostat.state == HVACMode.OFF

View File

@ -297,7 +297,6 @@ async def test_set_temperature_unsupported_cooling(
[
(Model.S320, "s1", "climate.climate_system_s1"),
(Model.F1155, "s2", "climate.climate_system_s2"),
(Model.F730, "s1", "climate.climate_system_s1"),
],
)
@pytest.mark.usefixtures("entity_registry_enabled_by_default")

View File

@ -242,7 +242,10 @@ async def test_adam_climate_entity_climate_changes(
"c50f167537524366a5af7aa3942feb1e", HVACMode.OFF
)
with pytest.raises(ServiceValidationError, match="valid modes are"):
with pytest.raises(
ServiceValidationError,
match="HVAC mode dry is not valid. Valid HVAC modes are: auto, heat",
):
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_HVAC_MODE,

View File

@ -401,7 +401,8 @@ async def test_climate_noscope(
entity_id = "climate.test_climate"
with pytest.raises(
ServiceValidationError, match="Climate mode off is not supported"
ServiceValidationError,
match="HVAC mode off is not valid. Valid HVAC modes are: heat_cool",
):
await hass.services.async_call(
CLIMATE_DOMAIN,

View File

@ -161,7 +161,7 @@ async def test_set_hvac_mode(
)
therm_module.set_state.assert_called_with(True)
msg = "Tried to set unsupported mode: dry"
msg = "HVAC mode dry is not valid. Valid HVAC modes are: heat, off"
with pytest.raises(ServiceValidationError, match=msg):
await hass.services.async_call(
CLIMATE_DOMAIN,

View File

@ -300,7 +300,7 @@ async def test_service_hvac_mode_turn_on(
(
SERVICE_SET_HVAC_MODE,
{ATTR_HVAC_MODE: HVACMode.DRY},
ValueError,
ServiceValidationError,
),
(
SERVICE_SET_FAN_MODE,

View File

@ -522,20 +522,28 @@ async def test_set_hvac_mode(
state = hass.states.get(entity_id)
assert state.state == HVACMode.OFF
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_HVAC_MODE,
{ATTR_ENTITY_ID: entity_id, ATTR_HVAC_MODE: hvac_mode},
blocking=True,
)
state = hass.states.get(entity_id)
if sys_mode is not None:
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_HVAC_MODE,
{ATTR_ENTITY_ID: entity_id, ATTR_HVAC_MODE: hvac_mode},
blocking=True,
)
state = hass.states.get(entity_id)
assert state.state == hvac_mode
assert thrm_cluster.write_attributes.call_count == 1
assert thrm_cluster.write_attributes.call_args[0][0] == {
"system_mode": sys_mode
}
else:
with pytest.raises(ServiceValidationError):
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_HVAC_MODE,
{ATTR_ENTITY_ID: entity_id, ATTR_HVAC_MODE: hvac_mode},
blocking=True,
)
state = hass.states.get(entity_id)
assert thrm_cluster.write_attributes.call_count == 0
assert state.state == HVACMode.OFF

View File

@ -264,7 +264,7 @@ async def test_thermostat_v2(
client.async_send_command.reset_mock()
# Test setting invalid hvac mode
with pytest.raises(ValueError):
with pytest.raises(ServiceValidationError):
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_HVAC_MODE,
@ -574,7 +574,7 @@ async def test_setpoint_thermostat(
)
# Test setting illegal mode raises an error
with pytest.raises(ValueError):
with pytest.raises(ServiceValidationError):
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_HVAC_MODE,