From de47a3ffe1f5fcbeb2e9a33edba345652beb85c1 Mon Sep 17 00:00:00 2001 From: Jean-Marc Collin Date: Fri, 26 Jan 2024 19:49:04 +0000 Subject: [PATCH] With all features + testu ok --- .../versatile_thermostat/config_schema.py | 2 +- .../versatile_thermostat/const.py | 4 +- .../versatile_thermostat/pi_algorithm.py | 20 +-- .../versatile_thermostat/strings.json | 8 +- .../thermostat_climate.py | 15 ++ .../versatile_thermostat/translations/en.json | 8 +- .../versatile_thermostat/translations/fr.json | 8 +- .../versatile_thermostat/underlyings.py | 17 ++- tests/commons.py | 14 ++ tests/const.py | 12 +- tests/test_auto_regulation.py | 136 +++++++++++++++++- 11 files changed, 213 insertions(+), 31 deletions(-) diff --git a/custom_components/versatile_thermostat/config_schema.py b/custom_components/versatile_thermostat/config_schema.py index 1021560..f33948e 100644 --- a/custom_components/versatile_thermostat/config_schema.py +++ b/custom_components/versatile_thermostat/config_schema.py @@ -143,7 +143,7 @@ STEP_THERMOSTAT_CLIMATE = vol.Schema( # pylint: disable=invalid-name mode="dropdown", ) ), - vol.Optional(CONF_AUTO_REGULATION_USE_INTERNAL_TEMP, default=False): cv.boolean, + vol.Optional(CONF_AUTO_REGULATION_USE_DEVICE_TEMP, default=False): cv.boolean, } ) diff --git a/custom_components/versatile_thermostat/const.py b/custom_components/versatile_thermostat/const.py index fe27265..929cfbf 100644 --- a/custom_components/versatile_thermostat/const.py +++ b/custom_components/versatile_thermostat/const.py @@ -105,7 +105,7 @@ CONF_AUTO_REGULATION_STRONG = "auto_regulation_strong" CONF_AUTO_REGULATION_EXPERT = "auto_regulation_expert" CONF_AUTO_REGULATION_DTEMP = "auto_regulation_dtemp" CONF_AUTO_REGULATION_PERIOD_MIN = "auto_regulation_periode_min" -CONF_AUTO_REGULATION_USE_INTERNAL_TEMP = "auto_regulation_use_internal_temp" +CONF_AUTO_REGULATION_USE_DEVICE_TEMP = "auto_regulation_use_device_temp" CONF_INVERSE_SWITCH = "inverse_switch_command" CONF_AUTO_FAN_MODE = "auto_fan_mode" CONF_AUTO_FAN_NONE = "auto_fan_none" @@ -256,7 +256,7 @@ ALL_CONF = ( CONF_AUTO_REGULATION_MODE, CONF_AUTO_REGULATION_DTEMP, CONF_AUTO_REGULATION_PERIOD_MIN, - CONF_AUTO_REGULATION_USE_INTERNAL_TEMP, + CONF_AUTO_REGULATION_USE_DEVICE_TEMP, CONF_INVERSE_SWITCH, CONF_AUTO_FAN_MODE, CONF_USE_MAIN_CENTRAL_CONFIG, diff --git a/custom_components/versatile_thermostat/pi_algorithm.py b/custom_components/versatile_thermostat/pi_algorithm.py index a5f79fe..7edd1a7 100644 --- a/custom_components/versatile_thermostat/pi_algorithm.py +++ b/custom_components/versatile_thermostat/pi_algorithm.py @@ -53,10 +53,10 @@ class PITemperatureRegulator: self.accumulated_error = 0 def calculate_regulated_temperature( - self, internal_temp: float, external_temp: float + self, room_temp: float, external_temp: float ): # pylint: disable=unused-argument """Calculate a new target_temp given some temperature""" - if internal_temp is None: + if room_temp is None: _LOGGER.warning( "Temporarily skipping the self-regulation algorithm while the configured sensor for room temperature is unavailable" ) @@ -68,7 +68,7 @@ class PITemperatureRegulator: return self.target_temp # Calculate the error factor (P) - error = self.target_temp - internal_temp + error = self.target_temp - room_temp # Calculate the sum of error (I) self.accumulated_error += error @@ -83,22 +83,16 @@ class PITemperatureRegulator: offset = self.kp * error + self.ki * self.accumulated_error # Calculate the exterior offset - # For Maia tests - use the internal_temp vs external_temp and not target_temp - external_temp - offset_ext = self.k_ext * (internal_temp - external_temp) + offset_ext = self.k_ext * (room_temp - external_temp) - # Capping of offset_ext + # Capping of offset total_offset = offset + offset_ext total_offset = min(self.offset_max, max(-self.offset_max, total_offset)) - # If temperature is near the target_temp, reset the accumulated_error - # Issue #199 - don't reset the accumulation error - # if abs(error) < self.stabilization_threshold: - # _LOGGER.debug("Stabilisation") - # self.accumulated_error = 0 - result = round(self.target_temp + total_offset, 1) - _LOGGER.debug( + # TODO Change to debug after experimental + _LOGGER.info( "PITemperatureRegulator - Error: %.2f accumulated_error: %.2f offset: %.2f offset_ext: %.2f target_tem: %.1f regulatedTemp: %.1f", error, self.accumulated_error, diff --git a/custom_components/versatile_thermostat/strings.json b/custom_components/versatile_thermostat/strings.json index a056262..8ad749c 100644 --- a/custom_components/versatile_thermostat/strings.json +++ b/custom_components/versatile_thermostat/strings.json @@ -59,7 +59,7 @@ "auto_regulation_mode": "Self-regulation", "auto_regulation_dtemp": "Regulation threshold", "auto_regulation_periode_min": "Regulation minimal period", - "auto_regulation_use_internal_temp": "Use internal temperature of the underlying", + "auto_regulation_use_device_temp": "Use internal temperature of the underlying", "inverse_switch_command": "Inverse switch command", "auto_fan_mode": " Auto fan mode" }, @@ -81,7 +81,7 @@ "auto_regulation_mode": "Auto adjustment of the target temperature", "auto_regulation_dtemp": "The threshold in ° (or % for valve) under which the temperature change will not be sent", "auto_regulation_periode_min": "Duration in minutes between two regulation update", - "auto_regulation_use_internal_temp": "Use the eventual internal temperature sensor of the underlying to speedup the self-regulation", + "auto_regulation_use_device_temp": "Use the eventual internal temperature sensor of the underlying to speedup the self-regulation", "inverse_switch_command": "For switch with pilot wire and diode you may need to inverse the command", "auto_fan_mode": " Automatically activate fan when huge heating/cooling is necessary" } @@ -296,7 +296,7 @@ "auto_regulation_mode": "Self-regulation", "auto_regulation_dtemp": "Regulation threshold", "auto_regulation_periode_min": "Regulation minimal period", - "auto_regulation_use_internal_temp": "Use internal temperature of the underlying", + "auto_regulation_use_device_temp": "Use internal temperature of the underlying", "inverse_switch_command": "Inverse switch command", "auto_fan_mode": " Auto fan mode" }, @@ -318,7 +318,7 @@ "auto_regulation_mode": "Auto adjustment of the target temperature", "auto_regulation_dtemp": "The threshold in ° (or % for valve) under which the temperature change will not be sent", "auto_regulation_periode_min": "Duration in minutes between two regulation update", - "auto_regulation_use_internal_temp": "Use the eventual internal temperature sensor of the underlying to speedup the self-regulation", + "auto_regulation_use_device_temp": "Use the eventual internal temperature sensor of the underlying to speedup the self-regulation", "inverse_switch_command": "For switch with pilot wire and diode you may need to invert the command", "auto_fan_mode": " Automatically activate fan when huge heating/cooling is necessary" } diff --git a/custom_components/versatile_thermostat/thermostat_climate.py b/custom_components/versatile_thermostat/thermostat_climate.py index 5fdac39..720b99f 100644 --- a/custom_components/versatile_thermostat/thermostat_climate.py +++ b/custom_components/versatile_thermostat/thermostat_climate.py @@ -36,6 +36,7 @@ from .const import ( CONF_AUTO_REGULATION_EXPERT, CONF_AUTO_REGULATION_DTEMP, CONF_AUTO_REGULATION_PERIOD_MIN, + CONF_AUTO_REGULATION_USE_DEVICE_TEMP, CONF_AUTO_FAN_MODE, CONF_AUTO_FAN_NONE, CONF_AUTO_FAN_LOW, @@ -90,6 +91,7 @@ class ThermostatOverClimate(BaseThermostat): "current_auto_fan_mode", "auto_activated_fan_mode", "auto_deactivated_fan_mode", + "auto_regulation_use_device_temp", } ) ) @@ -284,6 +286,10 @@ class ThermostatOverClimate(BaseThermostat): else CONF_AUTO_FAN_NONE ) + self._auto_regulation_use_device_temp = config_entry.get( + CONF_AUTO_REGULATION_USE_DEVICE_TEMP, False + ) + def choose_auto_regulation_mode(self, auto_regulation_mode: str): """Choose or change the regulation mode""" self._auto_regulation_mode = auto_regulation_mode @@ -492,6 +498,10 @@ class ThermostatOverClimate(BaseThermostat): "auto_deactivated_fan_mode" ] = self._auto_deactivated_fan_mode + self._attr_extra_state_attributes[ + "auto_regulation_use_device_temp" + ] = self.auto_regulation_use_device_temp + self.async_write_ha_state() _LOGGER.debug( "%s - Calling update_custom_attributes: %s", @@ -770,6 +780,11 @@ class ThermostatOverClimate(BaseThermostat): """Get the auto fan mode""" return self._auto_fan_mode + @property + def auto_regulation_use_device_temp(self) -> bool | None: + """Returns the value of parameter auto_regulation_use_device_temp""" + return self._auto_regulation_use_device_temp + @property def regulated_target_temp(self) -> float | None: """Get the regulated target temperature""" diff --git a/custom_components/versatile_thermostat/translations/en.json b/custom_components/versatile_thermostat/translations/en.json index a056262..8ad749c 100644 --- a/custom_components/versatile_thermostat/translations/en.json +++ b/custom_components/versatile_thermostat/translations/en.json @@ -59,7 +59,7 @@ "auto_regulation_mode": "Self-regulation", "auto_regulation_dtemp": "Regulation threshold", "auto_regulation_periode_min": "Regulation minimal period", - "auto_regulation_use_internal_temp": "Use internal temperature of the underlying", + "auto_regulation_use_device_temp": "Use internal temperature of the underlying", "inverse_switch_command": "Inverse switch command", "auto_fan_mode": " Auto fan mode" }, @@ -81,7 +81,7 @@ "auto_regulation_mode": "Auto adjustment of the target temperature", "auto_regulation_dtemp": "The threshold in ° (or % for valve) under which the temperature change will not be sent", "auto_regulation_periode_min": "Duration in minutes between two regulation update", - "auto_regulation_use_internal_temp": "Use the eventual internal temperature sensor of the underlying to speedup the self-regulation", + "auto_regulation_use_device_temp": "Use the eventual internal temperature sensor of the underlying to speedup the self-regulation", "inverse_switch_command": "For switch with pilot wire and diode you may need to inverse the command", "auto_fan_mode": " Automatically activate fan when huge heating/cooling is necessary" } @@ -296,7 +296,7 @@ "auto_regulation_mode": "Self-regulation", "auto_regulation_dtemp": "Regulation threshold", "auto_regulation_periode_min": "Regulation minimal period", - "auto_regulation_use_internal_temp": "Use internal temperature of the underlying", + "auto_regulation_use_device_temp": "Use internal temperature of the underlying", "inverse_switch_command": "Inverse switch command", "auto_fan_mode": " Auto fan mode" }, @@ -318,7 +318,7 @@ "auto_regulation_mode": "Auto adjustment of the target temperature", "auto_regulation_dtemp": "The threshold in ° (or % for valve) under which the temperature change will not be sent", "auto_regulation_periode_min": "Duration in minutes between two regulation update", - "auto_regulation_use_internal_temp": "Use the eventual internal temperature sensor of the underlying to speedup the self-regulation", + "auto_regulation_use_device_temp": "Use the eventual internal temperature sensor of the underlying to speedup the self-regulation", "inverse_switch_command": "For switch with pilot wire and diode you may need to invert the command", "auto_fan_mode": " Automatically activate fan when huge heating/cooling is necessary" } diff --git a/custom_components/versatile_thermostat/translations/fr.json b/custom_components/versatile_thermostat/translations/fr.json index 88004e9..5dc50b5 100644 --- a/custom_components/versatile_thermostat/translations/fr.json +++ b/custom_components/versatile_thermostat/translations/fr.json @@ -59,7 +59,7 @@ "auto_regulation_mode": "Auto-régulation", "auto_regulation_dtemp": "Seuil de régulation", "auto_regulation_periode_min": "Période minimale de régulation", - "auto_regulation_use_internal_temp": "Utiliser la température interne du sous-jacent", + "auto_regulation_use_device_temp": "Utiliser la température interne du sous-jacent", "inverse_switch_command": "Inverser la commande", "auto_fan_mode": " Auto ventilation mode" }, @@ -81,7 +81,7 @@ "auto_regulation_mode": "Ajustement automatique de la température cible", "auto_regulation_dtemp": "Le seuil en ° (ou % pour les valves) en-dessous duquel la régulation ne sera pas envoyée", "auto_regulation_periode_min": "La durée en minutes entre deux mise à jour faites par la régulation", - "auto_regulation_use_internal_temp": "Utiliser la temperature interne du sous-jacent pour accélérer l'auto-régulation", + "auto_regulation_use_device_temp": "Utiliser la temperature interne du sous-jacent pour accélérer l'auto-régulation", "inverse_switch_command": "Inverse la commande du switch pour une installation avec fil pilote et diode", "auto_fan_mode": "Active la ventilation automatiquement en cas d'écart important" } @@ -308,7 +308,7 @@ "auto_regulation_mode": "Auto-regulation", "auto_regulation_dtemp": "Seuil de régulation", "auto_regulation_periode_min": "Période minimale de régulation", - "auto_regulation_use_internal_temp": "Utiliser la température interne du sous-jacent", + "auto_regulation_use_device_temp": "Utiliser la température interne du sous-jacent", "inverse_switch_command": "Inverser la commande", "auto_fan_mode": " Auto fan mode" }, @@ -330,7 +330,7 @@ "auto_regulation_mode": "Ajustement automatique de la consigne", "auto_regulation_dtemp": "Le seuil en ° (ou % pour les valves) en-dessous duquel la régulation ne sera pas envoyée", "auto_regulation_periode_min": "La durée en minutes entre deux mise à jour faites par la régulation", - "auto_regulation_use_internal_temp": "Utiliser la temperature interne du sous-jacent pour accélérer l'auto-régulation", + "auto_regulation_use_device_temp": "Utiliser la temperature interne du sous-jacent pour accélérer l'auto-régulation", "inverse_switch_command": "Inverse la commande du switch pour une installation avec fil pilote et diode", "auto_fan_mode": "Active la ventilation automatiquement en cas d'écart important" } diff --git a/custom_components/versatile_thermostat/underlyings.py b/custom_components/versatile_thermostat/underlyings.py index 307b0bd..dab4c06 100644 --- a/custom_components/versatile_thermostat/underlyings.py +++ b/custom_components/versatile_thermostat/underlyings.py @@ -567,9 +567,24 @@ class UnderlyingClimate(UnderlyingEntity): """Set the target temperature""" if not self.is_initialized: return + + # issue 348 - use device temperature if configured as offset + offset_temp = 0 + if self._thermostat.auto_regulation_use_device_temp and hasattr( + self._underlying_climate, "current_temperature" + ): + device_temp = self._underlying_climate.current_temperature + offset_temp = device_temp - self._thermostat.current_temperature + _LOGGER.debug( + "%s - the device offset temp for regulation is %.2f - internal temp is %.2f", + self, + offset_temp, + device_temp, + ) + data = { ATTR_ENTITY_ID: self._entity_id, - "temperature": self.cap_sent_value(temperature), + "temperature": self.cap_sent_value(temperature + offset_temp), "target_temp_high": max_temp, "target_temp_low": min_temp, } diff --git a/tests/commons.py b/tests/commons.py index ccefd54..68bdfda 100644 --- a/tests/commons.py +++ b/tests/commons.py @@ -51,6 +51,7 @@ from .const import ( # pylint: disable=unused-import MOCK_TH_OVER_CLIMATE_MAIN_CONFIG, MOCK_TH_OVER_CLIMATE_CENTRAL_MAIN_CONFIG, MOCK_TH_OVER_CLIMATE_TYPE_CONFIG, + MOCK_TH_OVER_CLIMATE_TYPE_USE_DEVICE_TEMP_CONFIG, MOCK_TH_OVER_CLIMATE_TYPE_AC_CONFIG, MOCK_TH_OVER_CLIMATE_TYPE_NOT_REGULATED_CONFIG, MOCK_TH_OVER_SWITCH_TPI_CONFIG, @@ -110,6 +111,15 @@ PARTIAL_CLIMATE_CONFIG = ( | MOCK_ADVANCED_CONFIG ) +PARTIAL_CLIMATE_CONFIG_USE_DEVICE_TEMP = ( + MOCK_TH_OVER_CLIMATE_USER_CONFIG + | MOCK_TH_OVER_CLIMATE_MAIN_CONFIG + | MOCK_TH_OVER_CLIMATE_CENTRAL_MAIN_CONFIG + | MOCK_TH_OVER_CLIMATE_TYPE_USE_DEVICE_TEMP_CONFIG + | MOCK_PRESETS_CONFIG + | MOCK_ADVANCED_CONFIG +) + PARTIAL_CLIMATE_NOT_REGULATED_CONFIG = ( MOCK_TH_OVER_CLIMATE_USER_CONFIG | MOCK_TH_OVER_CLIMATE_MAIN_CONFIG @@ -313,6 +323,10 @@ class MockClimate(ClimateEntity): """Set the HVACaction""" self._attr_hvac_action = hvac_action + def set_current_temperature(self, current_temperature): + """Set the current_temperature""" + self._attr_current_temperature = current_temperature + class MockUnavailableClimate(ClimateEntity): """A Mock Climate class used for Underlying climate mode""" diff --git a/tests/const.py b/tests/const.py index b9023bd..c2ef349 100644 --- a/tests/const.py +++ b/tests/const.py @@ -108,7 +108,17 @@ MOCK_TH_OVER_CLIMATE_TYPE_CONFIG = { CONF_AUTO_REGULATION_DTEMP: 0.5, CONF_AUTO_REGULATION_PERIOD_MIN: 2, CONF_AUTO_FAN_MODE: CONF_AUTO_FAN_HIGH, - CONF_AUTO_REGULATION_USE_INTERNAL_TEMP: False, + CONF_AUTO_REGULATION_USE_DEVICE_TEMP: False, +} + +MOCK_TH_OVER_CLIMATE_TYPE_USE_DEVICE_TEMP_CONFIG = { + CONF_CLIMATE: "climate.mock_climate", + CONF_AC_MODE: False, + CONF_AUTO_REGULATION_MODE: CONF_AUTO_REGULATION_STRONG, + CONF_AUTO_REGULATION_DTEMP: 0.1, + CONF_AUTO_REGULATION_PERIOD_MIN: 2, + CONF_AUTO_FAN_MODE: CONF_AUTO_FAN_HIGH, + CONF_AUTO_REGULATION_USE_DEVICE_TEMP: True, } MOCK_TH_OVER_CLIMATE_TYPE_NOT_REGULATED_CONFIG = { diff --git a/tests/test_auto_regulation.py b/tests/test_auto_regulation.py index d254153..733f024 100644 --- a/tests/test_auto_regulation.py +++ b/tests/test_auto_regulation.py @@ -1,7 +1,7 @@ # pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long """ Test the normal start of a Thermostat """ -from unittest.mock import patch # , call +from unittest.mock import patch, call from datetime import datetime, timedelta from homeassistant.core import HomeAssistant @@ -71,6 +71,7 @@ async def test_over_climate_regulation( assert entity.name == "TheOverClimateMockName" assert entity.is_over_climate is True assert entity.is_regulated is True + assert entity.auto_regulation_use_device_temp is False assert entity.hvac_mode is HVACMode.OFF assert entity.hvac_action is HVACAction.OFF assert entity.target_temperature == entity.min_temp @@ -374,3 +375,136 @@ async def test_over_climate_regulation_limitations( assert ( entity.regulated_target_temp == 17 + 1.5 ) # 0.7 without round_to_nearest + + +@pytest.mark.parametrize("expected_lingering_tasks", [True]) +@pytest.mark.parametrize("expected_lingering_timers", [True]) +async def test_over_climate_regulation_use_device_temp( + hass: HomeAssistant, skip_hass_states_is_state, skip_send_event +): + """Test the regulation of an over climate thermostat""" + + entry = MockConfigEntry( + domain=DOMAIN, + title="TheOverClimateMockName", + unique_id="uniqueId", + # This is include a medium regulation + data=PARTIAL_CLIMATE_CONFIG_USE_DEVICE_TEMP, + ) + + tz = get_tz(hass) # pylint: disable=invalid-name + now: datetime = datetime.now(tz=tz) + + fake_underlying_climate = MockClimate(hass, "mockUniqueId", "MockClimateName", {}) + + assert fake_underlying_climate.current_temperature == 15 + + # Creates the regulated VTherm over climate + # change temperature so that the heating will start + event_timestamp = now - timedelta(minutes=10) + + with patch( + "custom_components.versatile_thermostat.commons.NowClass.get_now", + return_value=event_timestamp, + ), patch( + "custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate", + return_value=fake_underlying_climate, + ): + entity: ThermostatOverClimate = await create_thermostat( + hass, entry, "climate.theoverclimatemockname" + ) + assert entity + assert isinstance(entity, ThermostatOverClimate) + + assert entity.name == "TheOverClimateMockName" + assert entity.is_over_climate is True + assert entity.is_regulated is True + assert entity.auto_regulation_use_device_temp is True + + assert entity.hvac_mode is HVACMode.OFF + assert entity.hvac_action is HVACAction.OFF + assert entity.target_temperature == entity.min_temp + assert entity.preset_modes == [ + PRESET_NONE, + PRESET_FROST_PROTECTION, + PRESET_ECO, + PRESET_COMFORT, + PRESET_BOOST, + ] + assert entity.preset_mode is PRESET_NONE + + # 1. Activate the heating by changing HVACMode and temperature + # Select a hvacmode, presence and preset + await entity.async_set_hvac_mode(HVACMode.HEAT) + assert entity.hvac_mode is HVACMode.HEAT + assert entity.regulated_target_temp == entity.min_temp + + await send_temperature_change_event(entity, 18, event_timestamp) + await send_ext_temperature_change_event(entity, 10, event_timestamp) + + # 2. set manual target temp (at now - 7) -> the regulation should occurs + event_timestamp = now - timedelta(minutes=7) + with patch( + "custom_components.versatile_thermostat.commons.NowClass.get_now", + return_value=event_timestamp, + ), patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call: + await entity.async_set_temperature(temperature=16) + + fake_underlying_climate.set_hvac_action( + HVACAction.HEATING + ) # simulate under heating + assert entity.hvac_action == HVACAction.HEATING + assert entity.preset_mode == PRESET_NONE # Manual mode + + # the regulated temperature should be lower + assert entity.regulated_target_temp < entity.target_temperature + # The calcul is the following: 16 + (16 - 18) x 0.4 (strong) + 0 x ki - 1 (device offset) + assert ( + entity.regulated_target_temp == 15 + ) # round(16 + (16 - 18) * 0.4 + 0 * 0.08) + assert entity.hvac_action == HVACAction.HEATING + + mock_service_call.assert_has_calls( + [ + call.service_call( + "climate", + "set_temperature", + { + "entity_id": "climate.mock_climate", + "temperature": 12.0, # because device offset is -3 (15 - 18) + "target_temp_high": 30, + "target_temp_low": 15, + }, + ), + ] + ) + + # 3. change temperature so that the regulated temperature should slow down + fake_underlying_climate.set_current_temperature(27) + + event_timestamp = now - timedelta(minutes=5) + with patch( + "custom_components.versatile_thermostat.commons.NowClass.get_now", + return_value=event_timestamp, + ), patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call: + await send_temperature_change_event(entity, 23, event_timestamp) + await send_ext_temperature_change_event(entity, 19, event_timestamp) + + # the regulated temperature should be under (device offset is -3) + assert entity.regulated_target_temp < entity.target_temperature + assert entity.regulated_target_temp == 12.5 + + mock_service_call.assert_has_calls( + [ + call.service_call( + "climate", + "set_temperature", + { + "entity_id": "climate.mock_climate", + "temperature": 16.5, # because device offset is +4 (27 - 23) + "target_temp_high": 30, + "target_temp_low": 15, + }, + ), + ] + )