* First commit (no test) * With tests ok --------- Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
This commit is contained in:
@@ -14,6 +14,6 @@
|
|||||||
"quality_scale": "silver",
|
"quality_scale": "silver",
|
||||||
"requirements": [],
|
"requirements": [],
|
||||||
"ssdp": [],
|
"ssdp": [],
|
||||||
"version": "6.6.0",
|
"version": "6.7.0",
|
||||||
"zeroconf": []
|
"zeroconf": []
|
||||||
}
|
}
|
||||||
@@ -34,10 +34,16 @@ async def async_setup_entry(
|
|||||||
vt_type = entry.data.get(CONF_THERMOSTAT_TYPE)
|
vt_type = entry.data.get(CONF_THERMOSTAT_TYPE)
|
||||||
auto_start_stop_feature = entry.data.get(CONF_USE_AUTO_START_STOP_FEATURE)
|
auto_start_stop_feature = entry.data.get(CONF_USE_AUTO_START_STOP_FEATURE)
|
||||||
|
|
||||||
if vt_type == CONF_THERMOSTAT_CLIMATE and auto_start_stop_feature is True:
|
entities = []
|
||||||
# Creates a switch to enable the auto-start/stop
|
if vt_type == CONF_THERMOSTAT_CLIMATE:
|
||||||
enable_entity = AutoStartStopEnable(hass, unique_id, name, entry)
|
entities.append(FollowUnderlyingTemperatureChange(hass, unique_id, name, entry))
|
||||||
async_add_entities([enable_entity], True)
|
|
||||||
|
if auto_start_stop_feature is True:
|
||||||
|
# Creates a switch to enable the auto-start/stop
|
||||||
|
enable_entity = AutoStartStopEnable(hass, unique_id, name, entry)
|
||||||
|
entities.append(enable_entity)
|
||||||
|
|
||||||
|
async_add_entities(entities, True)
|
||||||
|
|
||||||
|
|
||||||
class AutoStartStopEnable(VersatileThermostatBaseEntity, SwitchEntity, RestoreEntity):
|
class AutoStartStopEnable(VersatileThermostatBaseEntity, SwitchEntity, RestoreEntity):
|
||||||
@@ -100,3 +106,63 @@ class AutoStartStopEnable(VersatileThermostatBaseEntity, SwitchEntity, RestoreEn
|
|||||||
def turn_on(self, **kwargs: Any):
|
def turn_on(self, **kwargs: Any):
|
||||||
self._attr_is_on = True
|
self._attr_is_on = True
|
||||||
self.update_my_state_and_vtherm()
|
self.update_my_state_and_vtherm()
|
||||||
|
|
||||||
|
|
||||||
|
class FollowUnderlyingTemperatureChange(
|
||||||
|
VersatileThermostatBaseEntity, SwitchEntity, RestoreEntity
|
||||||
|
):
|
||||||
|
"""The that enables the ManagedDevice optimisation with"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, hass: HomeAssistant, unique_id: str, name: str, entry_infos: ConfigEntry
|
||||||
|
):
|
||||||
|
super().__init__(hass, unique_id, name)
|
||||||
|
self._attr_name = "Follow underlying temp change"
|
||||||
|
self._attr_unique_id = f"{self._device_name}_follow_underlying_temp_change"
|
||||||
|
self._attr_is_on = False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def icon(self) -> str | None:
|
||||||
|
"""The icon"""
|
||||||
|
return "mdi:content-copy"
|
||||||
|
|
||||||
|
async def async_added_to_hass(self):
|
||||||
|
await super().async_added_to_hass()
|
||||||
|
|
||||||
|
# Récupérer le dernier état sauvegardé de l'entité
|
||||||
|
last_state = await self.async_get_last_state()
|
||||||
|
|
||||||
|
# Si l'état précédent existe, vous pouvez l'utiliser
|
||||||
|
if last_state is not None:
|
||||||
|
self._attr_is_on = last_state.state == "on"
|
||||||
|
else:
|
||||||
|
# If no previous state set it to false by default
|
||||||
|
self._attr_is_on = False
|
||||||
|
|
||||||
|
self.update_my_state_and_vtherm()
|
||||||
|
|
||||||
|
def update_my_state_and_vtherm(self):
|
||||||
|
"""Update the follow flag in my VTherm"""
|
||||||
|
self.async_write_ha_state()
|
||||||
|
if self.my_climate is not None:
|
||||||
|
self.my_climate.set_follow_underlying_temp_change(self._attr_is_on)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||||
|
"""Turn the entity on."""
|
||||||
|
self.turn_on()
|
||||||
|
|
||||||
|
@callback
|
||||||
|
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||||
|
"""Turn the entity off."""
|
||||||
|
self.turn_off()
|
||||||
|
|
||||||
|
@overrides
|
||||||
|
def turn_off(self, **kwargs: Any):
|
||||||
|
self._attr_is_on = False
|
||||||
|
self.update_my_state_and_vtherm()
|
||||||
|
|
||||||
|
@overrides
|
||||||
|
def turn_on(self, **kwargs: Any):
|
||||||
|
self._attr_is_on = True
|
||||||
|
self.update_my_state_and_vtherm()
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
|||||||
_auto_start_stop_level: TYPE_AUTO_START_STOP_LEVELS = AUTO_START_STOP_LEVEL_NONE
|
_auto_start_stop_level: TYPE_AUTO_START_STOP_LEVELS = AUTO_START_STOP_LEVEL_NONE
|
||||||
_auto_start_stop_algo: AutoStartStopDetectionAlgorithm | None = None
|
_auto_start_stop_algo: AutoStartStopDetectionAlgorithm | None = None
|
||||||
_is_auto_start_stop_enabled: bool = False
|
_is_auto_start_stop_enabled: bool = False
|
||||||
|
_follow_underlying_temp_change: bool = False
|
||||||
|
|
||||||
_entity_component_unrecorded_attributes = (
|
_entity_component_unrecorded_attributes = (
|
||||||
BaseThermostat._entity_component_unrecorded_attributes.union(
|
BaseThermostat._entity_component_unrecorded_attributes.union(
|
||||||
@@ -82,6 +83,7 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
|||||||
"auto_start_stop_enable",
|
"auto_start_stop_enable",
|
||||||
"auto_start_stop_accumulated_error",
|
"auto_start_stop_accumulated_error",
|
||||||
"auto_start_stop_accumulated_error_threshold",
|
"auto_start_stop_accumulated_error_threshold",
|
||||||
|
"follow_underlying_temp_change",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -552,6 +554,10 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
|||||||
"auto_start_stop_accumulated_error_threshold"
|
"auto_start_stop_accumulated_error_threshold"
|
||||||
] = self._auto_start_stop_algo.accumulated_error_threshold
|
] = self._auto_start_stop_algo.accumulated_error_threshold
|
||||||
|
|
||||||
|
self._attr_extra_state_attributes["follow_underlying_temp_change"] = (
|
||||||
|
self._follow_underlying_temp_change
|
||||||
|
)
|
||||||
|
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
@@ -853,7 +859,11 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
|||||||
|
|
||||||
# try to manage new target temperature set if state if no other changes have been found
|
# try to manage new target temperature set if state if no other changes have been found
|
||||||
# and if a target temperature have already been sent
|
# and if a target temperature have already been sent
|
||||||
if not changes and under.last_sent_temperature is not None:
|
if (
|
||||||
|
self._follow_underlying_temp_change
|
||||||
|
and not changes
|
||||||
|
and under.last_sent_temperature is not None
|
||||||
|
):
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"Do temperature check. under.last_sent_temperature is %s, new_target_temp is %s",
|
"Do temperature check. under.last_sent_temperature is %s, new_target_temp is %s",
|
||||||
under.last_sent_temperature,
|
under.last_sent_temperature,
|
||||||
@@ -972,6 +982,11 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
|||||||
self._is_auto_start_stop_enabled = is_enabled
|
self._is_auto_start_stop_enabled = is_enabled
|
||||||
self.update_custom_attributes()
|
self.update_custom_attributes()
|
||||||
|
|
||||||
|
def set_follow_underlying_temp_change(self, follow: bool):
|
||||||
|
"""Set the flaf follow the underlying temperature changes"""
|
||||||
|
self._follow_underlying_temp_change = follow
|
||||||
|
self.update_custom_attributes()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def auto_regulation_mode(self) -> str | None:
|
def auto_regulation_mode(self) -> str | None:
|
||||||
"""Get the regulation mode"""
|
"""Get the regulation mode"""
|
||||||
@@ -1128,6 +1143,11 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
|||||||
"""Returns the auto_start_stop_enable"""
|
"""Returns the auto_start_stop_enable"""
|
||||||
return self._is_auto_start_stop_enabled
|
return self._is_auto_start_stop_enabled
|
||||||
|
|
||||||
|
@property
|
||||||
|
def follow_underlying_temp_change(self) -> bool:
|
||||||
|
"""Get the follow underlying temp change flag"""
|
||||||
|
return self._follow_underlying_temp_change
|
||||||
|
|
||||||
@overrides
|
@overrides
|
||||||
def init_underlyings(self):
|
def init_underlyings(self):
|
||||||
"""Init the underlyings if not already done"""
|
"""Init the underlyings if not already done"""
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ from custom_components.versatile_thermostat.thermostat_climate import (
|
|||||||
ThermostatOverClimate,
|
ThermostatOverClimate,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from custom_components.versatile_thermostat.switch import (
|
||||||
|
FollowUnderlyingTemperatureChange,
|
||||||
|
)
|
||||||
|
|
||||||
from .commons import *
|
from .commons import *
|
||||||
|
|
||||||
logging.getLogger().setLevel(logging.DEBUG)
|
logging.getLogger().setLevel(logging.DEBUG)
|
||||||
@@ -197,7 +201,7 @@ async def test_bug_82(
|
|||||||
|
|
||||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||||
async def test_bug_101(
|
async def test_underlying_change_follow(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
skip_hass_states_is_state,
|
skip_hass_states_is_state,
|
||||||
skip_turn_on_off_heater,
|
skip_turn_on_off_heater,
|
||||||
@@ -231,12 +235,27 @@ async def test_bug_101(
|
|||||||
entity = await create_thermostat(hass, entry, "climate.theoverclimatemockname")
|
entity = await create_thermostat(hass, entry, "climate.theoverclimatemockname")
|
||||||
|
|
||||||
assert entity
|
assert entity
|
||||||
|
|
||||||
assert entity.name == "TheOverClimateMockName"
|
assert entity.name == "TheOverClimateMockName"
|
||||||
assert entity.is_over_climate is True
|
assert entity.is_over_climate is True
|
||||||
assert entity.hvac_mode is HVACMode.OFF
|
assert entity.hvac_mode is HVACMode.OFF
|
||||||
# because in MockClimate HVACAction is HEATING if hvac_mode is not set
|
# because in MockClimate HVACAction is HEATING if hvac_mode is not set
|
||||||
assert entity.hvac_action is HVACAction.HEATING
|
assert entity.hvac_action is HVACAction.HEATING
|
||||||
|
assert entity.follow_underlying_temp_change is False
|
||||||
|
|
||||||
|
follow_entity: FollowUnderlyingTemperatureChange = search_entity(
|
||||||
|
hass,
|
||||||
|
"switch.theoverclimatemockname_follow_underlying_temp_change",
|
||||||
|
SWITCH_DOMAIN,
|
||||||
|
)
|
||||||
|
assert follow_entity is not None
|
||||||
|
assert follow_entity.state is STATE_OFF
|
||||||
|
|
||||||
|
# follow the underlying temp change
|
||||||
|
follow_entity.turn_on()
|
||||||
|
|
||||||
|
assert entity.follow_underlying_temp_change is True
|
||||||
|
assert follow_entity.state is STATE_ON
|
||||||
|
|
||||||
# Underlying should have been shutdown
|
# Underlying should have been shutdown
|
||||||
assert mock_underlying_set_hvac_mode.call_count == 1
|
assert mock_underlying_set_hvac_mode.call_count == 1
|
||||||
mock_underlying_set_hvac_mode.assert_has_calls(
|
mock_underlying_set_hvac_mode.assert_has_calls(
|
||||||
@@ -322,6 +341,93 @@ async def test_bug_101(
|
|||||||
assert entity.preset_mode is PRESET_NONE
|
assert entity.preset_mode is PRESET_NONE
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||||
|
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||||
|
async def test_underlying_change_not_follow(
|
||||||
|
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_NOT_REGULATED_CONFIG, # 5 minutes security delay
|
||||||
|
)
|
||||||
|
|
||||||
|
# Underlying is in HEAT mode but should be shutdown at startup
|
||||||
|
fake_underlying_climate = MockClimate(
|
||||||
|
hass, "mockUniqueId", "MockClimateName", {}, HVACMode.HEAT, HVACAction.HEATING
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.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, patch(
|
||||||
|
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode"
|
||||||
|
) as mock_underlying_set_hvac_mode:
|
||||||
|
entity = await create_thermostat(hass, entry, "climate.theoverclimatemockname")
|
||||||
|
|
||||||
|
assert entity
|
||||||
|
|
||||||
|
assert entity.name == "TheOverClimateMockName"
|
||||||
|
assert entity.is_over_climate is True
|
||||||
|
assert entity.hvac_mode is HVACMode.OFF
|
||||||
|
# because in MockClimate HVACAction is HEATING if hvac_mode is not set
|
||||||
|
assert entity.hvac_action is HVACAction.HEATING
|
||||||
|
assert entity.target_temperature == 15
|
||||||
|
assert entity.preset_mode is PRESET_NONE
|
||||||
|
|
||||||
|
# default value
|
||||||
|
assert entity.follow_underlying_temp_change is False
|
||||||
|
|
||||||
|
follow_entity: FollowUnderlyingTemperatureChange = search_entity(
|
||||||
|
hass,
|
||||||
|
"switch.theoverclimatemockname_follow_underlying_temp_change",
|
||||||
|
SWITCH_DOMAIN,
|
||||||
|
)
|
||||||
|
assert follow_entity is not None
|
||||||
|
assert follow_entity.state is STATE_OFF
|
||||||
|
|
||||||
|
# follow the underlying temp change
|
||||||
|
follow_entity.turn_off()
|
||||||
|
|
||||||
|
assert entity.follow_underlying_temp_change is False
|
||||||
|
assert follow_entity.state is STATE_OFF
|
||||||
|
|
||||||
|
# 1. 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
|
||||||
|
assert entity.target_temperature == 17
|
||||||
|
|
||||||
|
# 2. Change the target temp of underlying thermostat at 11 sec later to avoid temporal filter
|
||||||
|
event_timestamp = now + timedelta(seconds=30)
|
||||||
|
await send_climate_change_event_with_temperature(
|
||||||
|
entity,
|
||||||
|
HVACMode.HEAT,
|
||||||
|
HVACMode.HEAT,
|
||||||
|
HVACAction.OFF,
|
||||||
|
HVACAction.OFF,
|
||||||
|
event_timestamp,
|
||||||
|
21,
|
||||||
|
True,
|
||||||
|
"climate.mock_climate", # the underlying climate entity id
|
||||||
|
)
|
||||||
|
# Should NOT have been switched to Manual preset
|
||||||
|
assert entity.target_temperature == 17
|
||||||
|
assert entity.preset_mode is PRESET_COMFORT
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||||
async def test_bug_615(
|
async def test_bug_615(
|
||||||
|
|||||||
Reference in New Issue
Block a user