Skip to content

Commit

Permalink
Issue #628 add follow underlying temp change entity (#630)
Browse files Browse the repository at this point in the history
* First commit (no test)

* With tests ok

---------

Co-authored-by: Jean-Marc Collin <[email protected]>
  • Loading branch information
jmcollin78 and Jean-Marc Collin authored Nov 13, 2024
1 parent d31376d commit b46a24f
Show file tree
Hide file tree
Showing 4 changed files with 200 additions and 8 deletions.
2 changes: 1 addition & 1 deletion custom_components/versatile_thermostat/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,6 @@
"quality_scale": "silver",
"requirements": [],
"ssdp": [],
"version": "6.6.0",
"version": "6.7.0",
"zeroconf": []
}
74 changes: 70 additions & 4 deletions custom_components/versatile_thermostat/switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,16 @@ async def async_setup_entry(
vt_type = entry.data.get(CONF_THERMOSTAT_TYPE)
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:
# Creates a switch to enable the auto-start/stop
enable_entity = AutoStartStopEnable(hass, unique_id, name, entry)
async_add_entities([enable_entity], True)
entities = []
if vt_type == CONF_THERMOSTAT_CLIMATE:
entities.append(FollowUnderlyingTemperatureChange(hass, unique_id, name, entry))

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):
Expand Down Expand Up @@ -100,3 +106,63 @@ def turn_off(self, **kwargs: Any):
def turn_on(self, **kwargs: Any):
self._attr_is_on = True
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()
22 changes: 21 additions & 1 deletion custom_components/versatile_thermostat/thermostat_climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -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_algo: AutoStartStopDetectionAlgorithm | None = None
_is_auto_start_stop_enabled: bool = False
_follow_underlying_temp_change: bool = False

_entity_component_unrecorded_attributes = (
BaseThermostat._entity_component_unrecorded_attributes.union(
Expand All @@ -82,6 +83,7 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
"auto_start_stop_enable",
"auto_start_stop_accumulated_error",
"auto_start_stop_accumulated_error_threshold",
"follow_underlying_temp_change",
}
)
)
Expand Down Expand Up @@ -552,6 +554,10 @@ def update_custom_attributes(self):
"auto_start_stop_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()

_LOGGER.debug(
Expand Down Expand Up @@ -853,7 +859,11 @@ async def end_climate_changed(changes: bool):

# 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
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(
"Do temperature check. under.last_sent_temperature is %s, new_target_temp is %s",
under.last_sent_temperature,
Expand Down Expand Up @@ -972,6 +982,11 @@ def set_auto_start_stop_enable(self, is_enabled: bool):
self._is_auto_start_stop_enabled = is_enabled
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
def auto_regulation_mode(self) -> str | None:
"""Get the regulation mode"""
Expand Down Expand Up @@ -1128,6 +1143,11 @@ def auto_start_stop_enable(self) -> bool:
"""Returns the auto_start_stop_enable"""
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
def init_underlyings(self):
"""Init the underlyings if not already done"""
Expand Down
110 changes: 108 additions & 2 deletions tests/test_overclimate.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
ThermostatOverClimate,
)

from custom_components.versatile_thermostat.switch import (
FollowUnderlyingTemperatureChange,
)

from .commons import *

logging.getLogger().setLevel(logging.DEBUG)
Expand Down Expand Up @@ -197,7 +201,7 @@ async def test_bug_82(

@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_bug_101(
async def test_underlying_change_follow(
hass: HomeAssistant,
skip_hass_states_is_state,
skip_turn_on_off_heater,
Expand Down Expand Up @@ -231,12 +235,27 @@ async def test_bug_101(
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.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
assert mock_underlying_set_hvac_mode.call_count == 1
mock_underlying_set_hvac_mode.assert_has_calls(
Expand Down Expand Up @@ -322,6 +341,93 @@ async def test_bug_101(
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_timers", [True])
async def test_bug_615(
Expand Down

0 comments on commit b46a24f

Please sign in to comment.