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 <jean-marc.collin-extern@renault.com>
This commit is contained in:
Jean-Marc Collin
2024-12-22 10:40:35 +01:00
committed by GitHub
parent d9791f6cb0
commit 081a2351de
9 changed files with 45 additions and 24 deletions

View File

@@ -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(

View File

@@ -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",

View File

@@ -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

View File

@@ -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.

View File

@@ -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.

View File

@@ -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)

View File

@@ -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

View File

@@ -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)

View File

@@ -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()