From c6a1b9fc3972dcfa9519e28d1d0b5b81ca7912c4 Mon Sep 17 00:00:00 2001
From: G Johansson <goran.johansson@shiftit.se>
Date: Wed, 25 Sep 2024 21:16:14 +0200
Subject: [PATCH] Change Climate set temp action for incorrect feature will
 raise (#126692)

* Change Climate set temp action for incorrect feature will raise

* Fix some tests

* Fix review comments

* Fix tesla_fleet

* Fix tests

* Fix review comment
---
 homeassistant/components/climate/__init__.py  | 40 ++-----------
 homeassistant/components/climate/strings.json |  6 ++
 tests/components/climate/test_init.py         | 60 +++++++++----------
 tests/components/deconz/test_climate.py       |  2 +-
 tests/components/esphome/test_climate.py      | 52 ++++++++--------
 tests/components/fritzbox/test_climate.py     |  9 ---
 .../homematicip_cloud/test_climate.py         |  7 ---
 tests/components/lcn/test_climate.py          | 23 +++----
 .../maxcube/test_maxcube_climate.py           |  2 +-
 tests/components/shelly/test_climate.py       | 28 ---------
 tests/components/switcher_kis/test_climate.py |  6 +-
 tests/components/tesla_fleet/test_climate.py  |  3 +-
 tests/components/teslemetry/test_climate.py   | 13 ----
 13 files changed, 85 insertions(+), 166 deletions(-)

diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py
index cd2ce3b563b..432fbffb843 100644
--- a/homeassistant/components/climate/__init__.py
+++ b/homeassistant/components/climate/__init__.py
@@ -965,46 +965,18 @@ async def async_service_temperature_set(
         ATTR_TEMPERATURE in service_call.data
         and not entity.supported_features & ClimateEntityFeature.TARGET_TEMPERATURE
     ):
-        # Warning implemented in 2024.10 and will be changed to raising
-        # a ServiceValidationError in 2025.4
-        report_issue = async_suggest_report_issue(
-            entity.hass,
-            integration_domain=entity.platform.platform_name,
-            module=type(entity).__module__,
-        )
-        _LOGGER.warning(
-            (
-                "%s::%s set_temperature action was used with temperature but the entity does not "
-                "implement the ClimateEntityFeature.TARGET_TEMPERATURE feature. "
-                "This will stop working in 2025.4 and raise an error instead. "
-                "Please %s"
-            ),
-            entity.platform.platform_name,
-            entity.__class__.__name__,
-            report_issue,
+        raise ServiceValidationError(
+            translation_domain=DOMAIN,
+            translation_key="missing_target_temperature_entity_feature",
         )
     if (
         ATTR_TARGET_TEMP_LOW in service_call.data
         and not entity.supported_features
         & ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
     ):
-        # Warning implemented in 2024.10 and will be changed to raising
-        # a ServiceValidationError in 2025.4
-        report_issue = async_suggest_report_issue(
-            entity.hass,
-            integration_domain=entity.platform.platform_name,
-            module=type(entity).__module__,
-        )
-        _LOGGER.warning(
-            (
-                "%s::%s set_temperature action was used with target_temp_low but the entity does not "
-                "implement the ClimateEntityFeature.TARGET_TEMPERATURE_RANGE feature. "
-                "This will stop working in 2025.4 and raise an error instead. "
-                "Please %s"
-            ),
-            entity.platform.platform_name,
-            entity.__class__.__name__,
-            report_issue,
+        raise ServiceValidationError(
+            translation_domain=DOMAIN,
+            translation_key="missing_target_temperature_range_entity_feature",
         )
 
     hass = entity.hass
diff --git a/homeassistant/components/climate/strings.json b/homeassistant/components/climate/strings.json
index fc0bdaf0d72..26a06821d84 100644
--- a/homeassistant/components/climate/strings.json
+++ b/homeassistant/components/climate/strings.json
@@ -275,6 +275,12 @@
     },
     "humidity_out_of_range": {
       "message": "Provided humidity {humidity} is not valid. Accepted range is {min_humidity} to {max_humidity}."
+    },
+    "missing_target_temperature_entity_feature": {
+      "message": "Set temperature action was used with the target temperature parameter but the entity does not support it."
+    },
+    "missing_target_temperature_range_entity_feature": {
+      "message": "Set temperature action was used with the target temperature low/high parameter but the entity does not support it."
     }
   }
 }
diff --git a/tests/components/climate/test_init.py b/tests/components/climate/test_init.py
index 2b09c2801df..aa162e0b683 100644
--- a/tests/components/climate/test_init.py
+++ b/tests/components/climate/test_init.py
@@ -290,40 +290,34 @@ async def test_temperature_features_is_valid(
     await hass.config_entries.async_setup(register_test_integration.entry_id)
     await hass.async_block_till_done()
 
-    await hass.services.async_call(
-        DOMAIN,
-        SERVICE_SET_TEMPERATURE,
-        {
-            "entity_id": "climate.test_temp",
-            "temperature": 20,
-        },
-        blocking=True,
-    )
-    assert (
-        "MockClimateTempEntity set_temperature action was used "
-        "with temperature but the entity does not "
-        "implement the ClimateEntityFeature.TARGET_TEMPERATURE feature. "
-        "This will stop working in 2025.4 and raise an error instead. "
-        "Please"
-    ) in caplog.text
+    with pytest.raises(
+        ServiceValidationError,
+        match="Set temperature action was used with the target temperature parameter but the entity does not support it",
+    ):
+        await hass.services.async_call(
+            DOMAIN,
+            SERVICE_SET_TEMPERATURE,
+            {
+                "entity_id": "climate.test_temp",
+                "temperature": 20,
+            },
+            blocking=True,
+        )
 
-    await hass.services.async_call(
-        DOMAIN,
-        SERVICE_SET_TEMPERATURE,
-        {
-            "entity_id": "climate.test_range",
-            "target_temp_low": 20,
-            "target_temp_high": 25,
-        },
-        blocking=True,
-    )
-    assert (
-        "MockClimateTempRangeEntity set_temperature action was used with "
-        "target_temp_low but the entity does not "
-        "implement the ClimateEntityFeature.TARGET_TEMPERATURE_RANGE feature. "
-        "This will stop working in 2025.4 and raise an error instead. "
-        "Please"
-    ) in caplog.text
+    with pytest.raises(
+        ServiceValidationError,
+        match="Set temperature action was used with the target temperature low/high parameter but the entity does not support it",
+    ):
+        await hass.services.async_call(
+            DOMAIN,
+            SERVICE_SET_TEMPERATURE,
+            {
+                "entity_id": "climate.test_range",
+                "target_temp_low": 20,
+                "target_temp_high": 25,
+            },
+            blocking=True,
+        )
 
 
 async def test_mode_validation(
diff --git a/tests/components/deconz/test_climate.py b/tests/components/deconz/test_climate.py
index 7f456e81976..e1000f0b4d6 100644
--- a/tests/components/deconz/test_climate.py
+++ b/tests/components/deconz/test_climate.py
@@ -259,7 +259,7 @@ async def test_climate_device_without_cooling_support(
 
     # Service set temperature without providing temperature attribute
 
-    with pytest.raises(ValueError):
+    with pytest.raises(ServiceValidationError):
         await hass.services.async_call(
             CLIMATE_DOMAIN,
             SERVICE_SET_TEMPERATURE,
diff --git a/tests/components/esphome/test_climate.py b/tests/components/esphome/test_climate.py
index 4ec7fee6447..189b86fc5fd 100644
--- a/tests/components/esphome/test_climate.py
+++ b/tests/components/esphome/test_climate.py
@@ -13,6 +13,7 @@ from aioesphomeapi import (
     ClimateState,
     ClimateSwingMode,
 )
+import pytest
 from syrupy import SnapshotAssertion
 
 from homeassistant.components.climate import (
@@ -41,6 +42,7 @@ from homeassistant.components.climate import (
 )
 from homeassistant.const import ATTR_ENTITY_ID
 from homeassistant.core import HomeAssistant
+from homeassistant.exceptions import ServiceValidationError
 
 
 async def test_climate_entity(
@@ -54,7 +56,6 @@ async def test_climate_entity(
             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,
@@ -134,14 +135,13 @@ async def test_climate_entity_with_step_and_two_point(
     assert state is not None
     assert state.state == HVACMode.COOL
 
-    await hass.services.async_call(
-        CLIMATE_DOMAIN,
-        SERVICE_SET_TEMPERATURE,
-        {ATTR_ENTITY_ID: "climate.test_myclimate", ATTR_TEMPERATURE: 25},
-        blocking=True,
-    )
-    mock_client.climate_command.assert_has_calls([call(key=1, target_temperature=25.0)])
-    mock_client.climate_command.reset_mock()
+    with pytest.raises(ServiceValidationError):
+        await hass.services.async_call(
+            CLIMATE_DOMAIN,
+            SERVICE_SET_TEMPERATURE,
+            {ATTR_ENTITY_ID: "climate.test_myclimate", ATTR_TEMPERATURE: 25},
+            blocking=True,
+        )
 
     await hass.services.async_call(
         CLIMATE_DOMAIN,
@@ -213,38 +213,34 @@ async def test_climate_entity_with_step_and_target_temp(
     assert state is not None
     assert state.state == HVACMode.COOL
 
-    await hass.services.async_call(
-        CLIMATE_DOMAIN,
-        SERVICE_SET_TEMPERATURE,
-        {ATTR_ENTITY_ID: "climate.test_myclimate", ATTR_TEMPERATURE: 25},
-        blocking=True,
-    )
-    mock_client.climate_command.assert_has_calls([call(key=1, target_temperature=25.0)])
-    mock_client.climate_command.reset_mock()
-
     await hass.services.async_call(
         CLIMATE_DOMAIN,
         SERVICE_SET_TEMPERATURE,
         {
             ATTR_ENTITY_ID: "climate.test_myclimate",
             ATTR_HVAC_MODE: HVACMode.AUTO,
-            ATTR_TARGET_TEMP_LOW: 20,
-            ATTR_TARGET_TEMP_HIGH: 30,
+            ATTR_TEMPERATURE: 25,
         },
         blocking=True,
     )
     mock_client.climate_command.assert_has_calls(
-        [
-            call(
-                key=1,
-                mode=ClimateMode.AUTO,
-                target_temperature_low=20.0,
-                target_temperature_high=30.0,
-            )
-        ]
+        [call(key=1, mode=ClimateMode.AUTO, target_temperature=25.0)]
     )
     mock_client.climate_command.reset_mock()
 
+    with pytest.raises(ServiceValidationError):
+        await hass.services.async_call(
+            CLIMATE_DOMAIN,
+            SERVICE_SET_TEMPERATURE,
+            {
+                ATTR_ENTITY_ID: "climate.test_myclimate",
+                ATTR_HVAC_MODE: HVACMode.AUTO,
+                ATTR_TARGET_TEMP_LOW: 20,
+                ATTR_TARGET_TEMP_HIGH: 30,
+            },
+            blocking=True,
+        )
+
     await hass.services.async_call(
         CLIMATE_DOMAIN,
         SERVICE_SET_HVAC_MODE,
diff --git a/tests/components/fritzbox/test_climate.py b/tests/components/fritzbox/test_climate.py
index f43e77e9861..61fe6b48a7a 100644
--- a/tests/components/fritzbox/test_climate.py
+++ b/tests/components/fritzbox/test_climate.py
@@ -15,8 +15,6 @@ from homeassistant.components.climate import (
     ATTR_MIN_TEMP,
     ATTR_PRESET_MODE,
     ATTR_PRESET_MODES,
-    ATTR_TARGET_TEMP_HIGH,
-    ATTR_TARGET_TEMP_LOW,
     DOMAIN as CLIMATE_DOMAIN,
     PRESET_COMFORT,
     PRESET_ECO,
@@ -290,13 +288,6 @@ async def test_update_error(hass: HomeAssistant, fritz: Mock) -> None:
             },
             [call(23)],
         ),
-        (
-            {
-                ATTR_TARGET_TEMP_HIGH: 16,
-                ATTR_TARGET_TEMP_LOW: 10,
-            },
-            [],
-        ),
     ],
 )
 async def test_set_temperature(
diff --git a/tests/components/homematicip_cloud/test_climate.py b/tests/components/homematicip_cloud/test_climate.py
index c059ed4b744..d4711440288 100644
--- a/tests/components/homematicip_cloud/test_climate.py
+++ b/tests/components/homematicip_cloud/test_climate.py
@@ -141,13 +141,6 @@ async def test_hmip_heating_group_heat(
     ha_state = hass.states.get(entity_id)
     assert ha_state.attributes[ATTR_PRESET_MODE] == "STD"
 
-    # Not required for hmip, but a possibility to send no temperature.
-    await hass.services.async_call(
-        "climate",
-        "set_temperature",
-        {"entity_id": entity_id, "target_temp_low": 10, "target_temp_high": 10},
-        blocking=True,
-    )
     # No new service call should be in mock_calls.
     assert len(hmip_device.mock_calls) == service_call_counter + 12
     # Only fire event from last async_manipulate_test_data available.
diff --git a/tests/components/lcn/test_climate.py b/tests/components/lcn/test_climate.py
index b7fcc2fbe4b..7ba263bd597 100644
--- a/tests/components/lcn/test_climate.py
+++ b/tests/components/lcn/test_climate.py
@@ -5,6 +5,7 @@ from unittest.mock import patch
 from pypck.inputs import ModStatusVar, Unknown
 from pypck.lcn_addr import LcnAddr
 from pypck.lcn_defs import Var, VarUnit, VarValue
+import pytest
 from syrupy.assertion import SnapshotAssertion
 
 from homeassistant.components.climate import (
@@ -25,6 +26,7 @@ from homeassistant.const import (
     Platform,
 )
 from homeassistant.core import HomeAssistant
+from homeassistant.exceptions import ServiceValidationError
 from homeassistant.helpers import entity_registry as er
 
 from .conftest import MockConfigEntry, MockModuleConnection, init_integration
@@ -140,16 +142,17 @@ async def test_set_temperature(hass: HomeAssistant, entry: MockConfigEntry) -> N
         # wrong temperature set via service call with high/low attributes
         var_abs.return_value = False
 
-        await hass.services.async_call(
-            DOMAIN_CLIMATE,
-            SERVICE_SET_TEMPERATURE,
-            {
-                ATTR_ENTITY_ID: "climate.climate1",
-                ATTR_TARGET_TEMP_LOW: 24.5,
-                ATTR_TARGET_TEMP_HIGH: 25.5,
-            },
-            blocking=True,
-        )
+        with pytest.raises(ServiceValidationError):
+            await hass.services.async_call(
+                DOMAIN_CLIMATE,
+                SERVICE_SET_TEMPERATURE,
+                {
+                    ATTR_ENTITY_ID: "climate.climate1",
+                    ATTR_TARGET_TEMP_LOW: 24.5,
+                    ATTR_TARGET_TEMP_HIGH: 25.5,
+                },
+                blocking=True,
+            )
 
         var_abs.assert_not_awaited()
 
diff --git a/tests/components/maxcube/test_maxcube_climate.py b/tests/components/maxcube/test_maxcube_climate.py
index 48e616f8fd2..8b56ee6a6de 100644
--- a/tests/components/maxcube/test_maxcube_climate.py
+++ b/tests/components/maxcube/test_maxcube_climate.py
@@ -216,7 +216,7 @@ async def test_thermostat_set_no_temperature(
     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_TEMPERATURE,
diff --git a/tests/components/shelly/test_climate.py b/tests/components/shelly/test_climate.py
index 997cf945626..aeeeca30edd 100644
--- a/tests/components/shelly/test_climate.py
+++ b/tests/components/shelly/test_climate.py
@@ -13,8 +13,6 @@ from homeassistant.components.climate import (
     ATTR_HVAC_ACTION,
     ATTR_HVAC_MODE,
     ATTR_PRESET_MODE,
-    ATTR_TARGET_TEMP_HIGH,
-    ATTR_TARGET_TEMP_LOW,
     DOMAIN as CLIMATE_DOMAIN,
     PRESET_NONE,
     SERVICE_SET_HVAC_MODE,
@@ -138,19 +136,6 @@ async def test_climate_set_temperature(
     assert state.state == HVACMode.OFF
     assert state.attributes[ATTR_TEMPERATURE] == 4
 
-    # Test set temperature without target temperature
-    await hass.services.async_call(
-        CLIMATE_DOMAIN,
-        SERVICE_SET_TEMPERATURE,
-        {
-            ATTR_ENTITY_ID: ENTITY_ID,
-            ATTR_TARGET_TEMP_LOW: 20,
-            ATTR_TARGET_TEMP_HIGH: 30,
-        },
-        blocking=True,
-    )
-    mock_block_device.http_request.assert_not_called()
-
     # Test set temperature
     await hass.services.async_call(
         CLIMATE_DOMAIN,
@@ -684,19 +669,6 @@ async def test_rpc_climate_set_temperature(
     state = hass.states.get(entity_id)
     assert state.attributes[ATTR_TEMPERATURE] == 23
 
-    # test set temperature without target temperature
-    await hass.services.async_call(
-        CLIMATE_DOMAIN,
-        SERVICE_SET_TEMPERATURE,
-        {
-            ATTR_ENTITY_ID: entity_id,
-            ATTR_TARGET_TEMP_LOW: 20,
-            ATTR_TARGET_TEMP_HIGH: 30,
-        },
-        blocking=True,
-    )
-    mock_rpc_device.call_rpc.assert_not_called()
-
     monkeypatch.setitem(mock_rpc_device.status["thermostat:0"], "target_C", 28)
     await hass.services.async_call(
         CLIMATE_DOMAIN,
diff --git a/tests/components/switcher_kis/test_climate.py b/tests/components/switcher_kis/test_climate.py
index 5da9684bf2a..c9f7abf34dc 100644
--- a/tests/components/switcher_kis/test_climate.py
+++ b/tests/components/switcher_kis/test_climate.py
@@ -98,6 +98,10 @@ async def test_climate_temperature(
     await init_integration(hass)
     assert mock_bridge
 
+    monkeypatch.setattr(DEVICE, "mode", ThermostatMode.HEAT)
+    mock_bridge.mock_callbacks([DEVICE])
+    await hass.async_block_till_done()
+
     # Test initial target temperature
     state = hass.states.get(ENTITY_ID)
     assert state.attributes["temperature"] == 23
@@ -126,7 +130,7 @@ async def test_climate_temperature(
     with patch(
         "homeassistant.components.switcher_kis.climate.SwitcherType2Api.control_breeze_device",
     ) as mock_control_device:
-        with pytest.raises(ValueError):
+        with pytest.raises(ServiceValidationError):
             await hass.services.async_call(
                 CLIMATE_DOMAIN,
                 SERVICE_SET_TEMPERATURE,
diff --git a/tests/components/tesla_fleet/test_climate.py b/tests/components/tesla_fleet/test_climate.py
index 75474698d09..b8cb7f1269b 100644
--- a/tests/components/tesla_fleet/test_climate.py
+++ b/tests/components/tesla_fleet/test_climate.py
@@ -436,7 +436,8 @@ async def test_climate_notemp(
     await setup_platform(hass, normal_config_entry, [Platform.CLIMATE])
 
     with pytest.raises(
-        ServiceValidationError, match="Temperature is required for this action"
+        ServiceValidationError,
+        match="Set temperature action was used with the target temperature low/high parameter but the entity does not support it",
     ):
         await hass.services.async_call(
             CLIMATE_DOMAIN,
diff --git a/tests/components/teslemetry/test_climate.py b/tests/components/teslemetry/test_climate.py
index 3cb4b67dc54..800748f4c77 100644
--- a/tests/components/teslemetry/test_climate.py
+++ b/tests/components/teslemetry/test_climate.py
@@ -10,8 +10,6 @@ from tesla_fleet_api.exceptions import InvalidCommand, VehicleOffline
 from homeassistant.components.climate import (
     ATTR_HVAC_MODE,
     ATTR_PRESET_MODE,
-    ATTR_TARGET_TEMP_HIGH,
-    ATTR_TARGET_TEMP_LOW,
     ATTR_TEMPERATURE,
     DOMAIN as CLIMATE_DOMAIN,
     SERVICE_SET_HVAC_MODE,
@@ -175,17 +173,6 @@ async def test_climate(
     state = hass.states.get(entity_id)
     assert state.state == HVACMode.COOL
 
-    # Set Temp do nothing
-    await hass.services.async_call(
-        CLIMATE_DOMAIN,
-        SERVICE_SET_TEMPERATURE,
-        {
-            ATTR_ENTITY_ID: [entity_id],
-            ATTR_TARGET_TEMP_HIGH: 30,
-            ATTR_TARGET_TEMP_LOW: 30,
-        },
-        blocking=True,
-    )
     state = hass.states.get(entity_id)
     assert state.attributes[ATTR_TEMPERATURE] == 40
     assert state.state == HVACMode.COOL
-- 
GitLab