From 487c118b44b736b631833bcb6a999f5701bcbf09 Mon Sep 17 00:00:00 2001 From: Jean-Marc Collin Date: Sat, 7 Oct 2023 17:54:50 +0200 Subject: [PATCH] Issue #101 - Use target temperature of underlying if changed --- .../versatile_thermostat/climate.py | 5 ++ .../versatile_thermostat/tests/commons.py | 56 ++++++++++++- .../versatile_thermostat/tests/test_bugs.py | 78 +++++++++++++++++++ 3 files changed, 138 insertions(+), 1 deletion(-) diff --git a/custom_components/versatile_thermostat/climate.py b/custom_components/versatile_thermostat/climate.py index 9c7fe9a..7e16569 100644 --- a/custom_components/versatile_thermostat/climate.py +++ b/custom_components/versatile_thermostat/climate.py @@ -1659,6 +1659,11 @@ class VersatileThermostat(ClimateEntity, RestoreEntity): self._underlying_climate_delta_t, ) + # Manage new target temperature set + if self._is_over_climate and new_state.attributes and (new_target_temp := new_state.attributes.get("temperature")) and new_target_temp != self.target_temperature: + _LOGGER.info("%s - Target temp have change to %s", self, new_target_temp) + await self.async_set_temperature(temperature = new_target_temp) + self.update_custom_attributes() await self._async_control_heating() diff --git a/custom_components/versatile_thermostat/tests/commons.py b/custom_components/versatile_thermostat/tests/commons.py index 8b7d690..b4c1537 100644 --- a/custom_components/versatile_thermostat/tests/commons.py +++ b/custom_components/versatile_thermostat/tests/commons.py @@ -88,7 +88,9 @@ class MockClimate(ClimateEntity): super().__init__() - self._hass = hass + self.hass = hass + self.platform = 'climate' + self.entity_id= self.platform+'.'+unique_id self._attr_extra_state_attributes = {} self._unique_id = unique_id self._name = name @@ -96,6 +98,13 @@ class MockClimate(ClimateEntity): self._attr_hvac_mode = hvac_mode self._attr_hvac_modes = [HVACMode.OFF, HVACMode.COOL, HVACMode.HEAT] self._attr_temperature_unit = UnitOfTemperature.CELSIUS + self._attr_target_temperature = 20 + self._attr_current_temperature = 15 + + def set_temperature(self, temperature): + """ Set the target temperature""" + self._attr_target_temperature = temperature + self.async_write_ha_state() class MockUnavailableClimate(ClimateEntity): """A Mock Climate class used for Underlying climate mode""" @@ -471,6 +480,51 @@ async def send_climate_change_event( await asyncio.sleep(0.1) return ret +async def send_climate_change_event_with_temperature( + entity: VersatileThermostat, + new_hvac_mode: HVACMode, + old_hvac_mode: HVACMode, + new_hvac_action: HVACAction, + old_hvac_action: HVACAction, + date, + temperature, + sleep=True, +): + """Sending a new climate event simulating a change on the underlying climate state""" + _LOGGER.info( + "------- Testu: sending send_temperature_change_event, new_hvac_mode=%s old_hvac_mode=%s new_hvac_action=%s old_hvac_action=%s date=%s temperature=%s on %s", + new_hvac_mode, + old_hvac_mode, + new_hvac_action, + old_hvac_action, + date, + temperature, + entity, + ) + climate_event = Event( + EVENT_STATE_CHANGED, + { + "new_state": State( + entity_id=entity.entity_id, + state=new_hvac_mode, + attributes={"hvac_action": new_hvac_action, "temperature": temperature}, + last_changed=date, + last_updated=date, + ), + "old_state": State( + entity_id=entity.entity_id, + state=old_hvac_mode, + attributes={"hvac_action": old_hvac_action}, + last_changed=date, + last_updated=date, + ), + }, + ) + ret = await entity._async_climate_changed(climate_event) + if sleep: + await asyncio.sleep(0.1) + return ret + def cancel_switchs_cycles(entity: VersatileThermostat): """This method will cancel all running cycle on all underlying switch entity""" diff --git a/custom_components/versatile_thermostat/tests/test_bugs.py b/custom_components/versatile_thermostat/tests/test_bugs.py index 2b5e38d..c85f993 100644 --- a/custom_components/versatile_thermostat/tests/test_bugs.py +++ b/custom_components/versatile_thermostat/tests/test_bugs.py @@ -446,5 +446,83 @@ async def test_bug_82( assert entity.preset_mode == 'none' assert entity._saved_preset_mode == 'none' +@pytest.mark.parametrize("expected_lingering_tasks", [True]) +@pytest.mark.parametrize("expected_lingering_timers", [True]) +async def test_bug_101( + hass: HomeAssistant, + skip_hass_states_is_state, + skip_turn_on_off_heater, + skip_send_event, +): + """Test that when a underlying climate target temp is changed, the VTherm change its own temperature target and switch to manual""" + + tz = get_tz(hass) # pylint: disable=invalid-name + now: datetime = datetime.now(tz=tz) + + entry = MockConfigEntry( + domain=DOMAIN, + title="TheOverClimateMockName", + unique_id="uniqueId", + data=PARTIAL_CLIMATE_CONFIG, # 5 minutes security delay + ) + + fake_underlying_climate = MockClimate(hass, "mockUniqueId", "MockClimateName", {}, HVACMode.HEAT) + + with patch( + "custom_components.versatile_thermostat.climate.VersatileThermostat.send_event" + ) as mock_send_event, patch( + "custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate", + return_value=fake_underlying_climate, + ) as mock_find_climate: + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + assert entry.state is ConfigEntryState.LOADED + + def find_my_entity(entity_id) -> ClimateEntity: + """Find my new entity""" + component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN] + for entity in component.entities: + if entity.entity_id == entity_id: + return entity + + entity = find_my_entity("climate.theoverclimatemockname") + + assert entity + + assert entity.name == "TheOverClimateMockName" + assert entity._is_over_climate is True + assert entity.hvac_action is HVACAction.OFF + assert entity.hvac_mode is HVACMode.HEAT + assert entity.target_temperature == entity.min_temp + assert entity.preset_mode is PRESET_NONE + + # should have been called with EventType.PRESET_EVENT and EventType.HVAC_MODE_EVENT + assert mock_send_event.call_count == 2 + mock_send_event.assert_has_calls( + [ + call.send_event(EventType.PRESET_EVENT, {"preset": PRESET_NONE}), + call.send_event( + EventType.HVAC_MODE_EVENT, + {"hvac_mode": HVACMode.OFF}, + ), + ] + ) + + assert mock_find_climate.call_count == 1 + assert mock_find_climate.mock_calls[0] == call() + mock_find_climate.assert_has_calls([call.find_underlying_entity()]) + + # Force preset mode + await entity.async_set_hvac_mode(HVACMode.HEAT) + assert entity.hvac_mode == HVACMode.HEAT + await entity.async_set_preset_mode(PRESET_COMFORT) + assert entity.preset_mode == PRESET_COMFORT + + # 2. Change the target temp of underlying thermostat + await send_climate_change_event_with_temperature(entity, HVACMode.HEAT, HVACMode.HEAT, HVACAction.OFF, HVACAction.OFF, now, 12.75) + # Should have been switched to Manual preset + assert entity.target_temperature == 12.75 + assert entity.preset_mode is PRESET_NONE +