From 081a2351de615cd52a04c89e16f58ec1c6aad377 Mon Sep 17 00:00:00 2001 From: Jean-Marc Collin Date: Sun, 22 Dec 2024 10:40:35 +0100 Subject: [PATCH] Issue #690 - VTherm don't follow underlying change with lastSeen activated (#732) * Issue #690 - VTherm don't follow underlying change with lastSeen activated * Fix tests warnings * Add a lastSeen test --------- Co-authored-by: Jean-Marc Collin --- .../versatile_thermostat/base_thermostat.py | 36 +++++++++++-------- .../thermostat_climate.py | 8 +++-- .../versatile_thermostat/underlyings.py | 2 +- documentation/en/reference.md | 4 +++ documentation/fr/reference.md | 3 ++ tests/test_auto_start_stop.py | 4 +-- tests/test_last_seen.py | 6 ++++ tests/test_overclimate.py | 4 +-- tests/test_window.py | 2 +- 9 files changed, 45 insertions(+), 24 deletions(-) diff --git a/custom_components/versatile_thermostat/base_thermostat.py b/custom_components/versatile_thermostat/base_thermostat.py index d92340c..edc6fe5 100644 --- a/custom_components/versatile_thermostat/base_thermostat.py +++ b/custom_components/versatile_thermostat/base_thermostat.py @@ -137,6 +137,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]): "temperature_slope", "max_on_percent", "have_valve_regulation", + "last_change_time_from_vtherm", } ) ) @@ -219,7 +220,9 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]): self._current_tz = dt_util.get_time_zone(self._hass.config.time_zone) - self._last_change_time = None + # Last change time is the datetime of the last change sent by VTherm to the device + # it is used in `over_cliamte` when a state have change from underlying to avoid loops + self._last_change_time_from_vtherm = None self._underlyings: list[T] = [] @@ -749,14 +752,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]): self.hass.create_task(self._check_initial_state()) - self.reset_last_change_time() - - # if self.hass.state == CoreState.running: - # await _async_startup_internal() - # else: - # self.hass.bus.async_listen_once( - # EVENT_HOMEASSISTANT_START, _async_startup_internal - # ) + self.reset_last_change_time_from_vtherm() def init_underlyings(self): """Initialize all underlyings. Should be overriden if necessary""" @@ -1223,7 +1219,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]): return def save_state(): - self.reset_last_change_time() + self.reset_last_change_time_from_vtherm() self.update_custom_attributes() self.async_write_ha_state() self.send_event(EventType.HVAC_MODE_EVENT, {"hvac_mode": self._hvac_mode}) @@ -1355,12 +1351,14 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]): if self._attr_preset_mode != old_preset_mode: self.send_event(EventType.PRESET_EVENT, {"preset": self._attr_preset_mode}) - def reset_last_change_time( + def reset_last_change_time_from_vtherm( self, old_preset_mode: str | None = None ): # pylint: disable=unused-argument """Reset to now the last change time""" - self._last_change_time = self.now - _LOGGER.debug("%s - last_change_time is now %s", self, self._last_change_time) + self._last_change_time_from_vtherm = self.now + _LOGGER.debug( + "%s - last_change_time is now %s", self, self._last_change_time_from_vtherm + ) def reset_last_temperature_time(self, old_preset_mode: str | None = None): """Reset to now the last temperature time if conditions are satisfied""" @@ -1460,7 +1458,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]): await self._async_internal_set_temperature(temperature) self._attr_preset_mode = PRESET_NONE self.recalculate() - self.reset_last_change_time() + self.reset_last_change_time_from_vtherm() await self.async_control_heating(force=True) async def _async_internal_set_temperature(self, temperature: float): @@ -1529,7 +1527,8 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]): self._last_temperature_measure = self.get_last_updated_date_or_now( new_state ) - self.reset_last_change_time() + # issue 690 - don't reset the last change time on lastSeen + # self.reset_last_change_time_from_vtherm() _LOGGER.debug( "%s - new last_temperature_measure is now: %s", self, @@ -2693,6 +2692,13 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]): "hvac_off_reason": self.hvac_off_reason, "max_on_percent": self._max_on_percent, "have_valve_regulation": self.have_valve_regulation, + "last_change_time_from_vtherm": ( + self._last_change_time_from_vtherm.astimezone( + self._current_tz + ).isoformat() + if self._last_change_time_from_vtherm is not None + else None + ), } _LOGGER.debug( diff --git a/custom_components/versatile_thermostat/thermostat_climate.py b/custom_components/versatile_thermostat/thermostat_climate.py index 7753e82..afcf67c 100644 --- a/custom_components/versatile_thermostat/thermostat_climate.py +++ b/custom_components/versatile_thermostat/thermostat_climate.py @@ -766,7 +766,7 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]): _LOGGER.debug( "%s - last_change_time=%s old_state_date_changed=%s old_state_date_updated=%s new_state_date_changed=%s new_state_date_updated=%s", self, - self._last_change_time, + self._last_change_time_from_vtherm, old_state_date_changed, old_state_date_updated, new_state_date_changed, @@ -809,8 +809,10 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]): # Filter new state when received just after a change from VTherm # Issue #120 - Some TRV are changing target temperature a very long time (6 sec) after the change. # In that case a loop is possible if a user change multiple times during this 6 sec. - if new_state_date_updated and self._last_change_time: - delta = (new_state_date_updated - self._last_change_time).total_seconds() + if new_state_date_updated and self._last_change_time_from_vtherm: + delta = ( + new_state_date_updated - self._last_change_time_from_vtherm + ).total_seconds() if delta < 10: _LOGGER.info( "%s - underlying event is received less than 10 sec after command. Forget it to avoid loop", diff --git a/custom_components/versatile_thermostat/underlyings.py b/custom_components/versatile_thermostat/underlyings.py index 267b2b8..90cac59 100644 --- a/custom_components/versatile_thermostat/underlyings.py +++ b/custom_components/versatile_thermostat/underlyings.py @@ -252,7 +252,7 @@ class UnderlyingSwitch(UnderlyingEntity): self._cancel_cycle() if self.hvac_mode != hvac_mode: - super().set_hvac_mode(hvac_mode) + await super().set_hvac_mode(hvac_mode) return True else: return False diff --git a/documentation/en/reference.md b/documentation/en/reference.md index 367cd92..e5b2cd1 100644 --- a/documentation/en/reference.md +++ b/documentation/en/reference.md @@ -271,5 +271,9 @@ The custom attributes are as follows: | ``auto_start_stop_enable`` | Indicates if the VTherm is allowed to auto start/stop | | ``auto_start_stop_level`` | Indicates the auto start/stop level | | ``hvac_off_reason`` | Indicates the reason for the thermostat's off state (hvac_off). It can be Window, Auto-start/stop, or Manual | +| ``last_change_time_from_vtherm`` | The date and time of the last change done by VTherm | +| ``nb_device_actives`` | The number of underlying devices seen as active | +| ``device_actives`` | The list of underlying devices seen as active | + These attributes will be requested when you need assistance. \ No newline at end of file diff --git a/documentation/fr/reference.md b/documentation/fr/reference.md index f5d706d..0802a6d 100644 --- a/documentation/fr/reference.md +++ b/documentation/fr/reference.md @@ -270,5 +270,8 @@ Les attributs personnalisés sont les suivants : | ``auto_start_stop_enable`` | Indique si le VTherm est autorisé à s'auto démarrer/arrêter | | ``auto_start_stop_level`` | Indique le niveau d'auto start/stop | | ``hvac_off_reason`` | Indique la raison de l'arrêt (hvac_off) du VTherm. Ce peut être Window, Auto-start/stop ou Manuel | +| ``last_change_time_from_vtherm`` | La date/heure du dernier changement fait par VTherm | +| ``nb_device_actives`` | Le nombre de devices sous-jacents actuellement vus comme actifs | +| ``device_actives`` | La liste des devices sous-jacents actuellement vus comme actifs | Ces attributs vous seront demandés lors d'une demande d'aide. diff --git a/tests/test_auto_start_stop.py b/tests/test_auto_start_stop.py index 9448d00..212890b 100644 --- a/tests/test_auto_start_stop.py +++ b/tests/test_auto_start_stop.py @@ -1299,7 +1299,7 @@ async def test_auto_start_stop_fast_heat_window( now: datetime = datetime.now(tz=tz) # 2. Set mode to Heat and preset to Comfort and close the window - send_window_change_event(vtherm, False, False, now, False) + await send_window_change_event(vtherm, False, False, now, False) await send_presence_change_event(vtherm, True, False, now) await send_temperature_change_event(vtherm, 18, now, True) await vtherm.async_set_hvac_mode(HVACMode.HEAT) @@ -1474,7 +1474,7 @@ async def test_auto_start_stop_fast_heat_window_mixed( now: datetime = datetime.now(tz=tz) # 2. Set mode to Heat and preset to Comfort and close the window - send_window_change_event(vtherm, False, False, now, False) + await send_window_change_event(vtherm, False, False, now, False) await send_presence_change_event(vtherm, True, False, now) await send_temperature_change_event(vtherm, 18, now, True) await vtherm.async_set_hvac_mode(HVACMode.HEAT) diff --git a/tests/test_last_seen.py b/tests/test_last_seen.py index e026add..906023c 100644 --- a/tests/test_last_seen.py +++ b/tests/test_last_seen.py @@ -84,6 +84,8 @@ async def test_last_seen_feature(hass: HomeAssistant, skip_hass_states_is_state) await entity.async_set_hvac_mode(HVACMode.HEAT) assert entity.hvac_mode == HVACMode.HEAT + last_change_time_from_vtherm = entity._last_change_time_from_vtherm + # 2. activate security feature when date is expired with patch( "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event" @@ -128,9 +130,13 @@ async def test_last_seen_feature(hass: HomeAssistant, skip_hass_states_is_state) assert mock_heater_on.call_count == 1 + assert entity._last_change_time_from_vtherm == last_change_time_from_vtherm + # 3. change the last seen sensor event_timestamp = now - timedelta(minutes=4) await send_last_seen_temperature_change_event(entity, event_timestamp) assert entity.security_state is False assert entity.preset_mode is PRESET_COMFORT assert entity._last_temperature_measure == event_timestamp + + assert entity._last_change_time_from_vtherm == last_change_time_from_vtherm diff --git a/tests/test_overclimate.py b/tests/test_overclimate.py index 35cb646..8a72f6d 100644 --- a/tests/test_overclimate.py +++ b/tests/test_overclimate.py @@ -949,7 +949,7 @@ async def test_manual_hvac_off_should_take_the_lead_over_window( now: datetime = datetime.now(tz=tz) # 1. Set mode to Heat and preset to Comfort and close the window - send_window_change_event(vtherm, False, False, now, False) + await send_window_change_event(vtherm, False, False, now, False) await send_presence_change_event(vtherm, True, False, now) await send_temperature_change_event(vtherm, 18, now, True) await vtherm.async_set_hvac_mode(HVACMode.HEAT) @@ -1123,7 +1123,7 @@ async def test_manual_hvac_off_should_take_the_lead_over_auto_start_stop( now: datetime = datetime.now(tz=tz) # 1. Set mode to Heat and preset to Comfort - send_window_change_event(vtherm, False, False, now, False) + await send_window_change_event(vtherm, False, False, now, False) await send_presence_change_event(vtherm, True, False, now) await send_temperature_change_event(vtherm, 18, now, True) await vtherm.async_set_hvac_mode(HVACMode.HEAT) diff --git a/tests/test_window.py b/tests/test_window.py index d3ac4ed..ec3f066 100644 --- a/tests/test_window.py +++ b/tests/test_window.py @@ -246,7 +246,7 @@ async def test_window_management_time_enough( assert entity.preset_mode is PRESET_BOOST assert entity.hvac_mode is HVACMode.HEAT assert entity._saved_hvac_mode is HVACMode.HEAT # No change - assert entity.hvac_off_reason == None + assert entity.hvac_off_reason is None # Clean the entity entity.remove_thermostat()