Compare commits
1 Commits
6.6.3.beta
...
6.6.1.beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae9b065387 |
@@ -152,7 +152,6 @@ climate:
|
|||||||
name: Underlying thermostat2
|
name: Underlying thermostat2
|
||||||
heater: input_boolean.fake_heater_switch3
|
heater: input_boolean.fake_heater_switch3
|
||||||
target_sensor: input_number.fake_temperature_sensor1
|
target_sensor: input_number.fake_temperature_sensor1
|
||||||
ac_mode: false
|
|
||||||
- platform: generic_thermostat
|
- platform: generic_thermostat
|
||||||
name: Underlying thermostat3
|
name: Underlying thermostat3
|
||||||
heater: input_boolean.fake_heater_switch3
|
heater: input_boolean.fake_heater_switch3
|
||||||
|
|||||||
@@ -1353,7 +1353,7 @@ Example of graph obtained with Plotly :
|
|||||||
|
|
||||||
|
|
||||||
## And always better and better with the NOTIFIER daemon app to notify events
|
## And always better and better with the NOTIFIER daemon app to notify events
|
||||||
This automation uses the excellent App Daemon named NOTIFIER developed by Horizon Domotique that you will find in demonstration [here](https://www.youtube.com/watch?v=chJylIK0ASo&ab_channel=HorizonDomotique) and the code is [here](https://github.com/jlpouffier/home-assistant-config/blob/master/appdaemon/apps/notifier.py). It allows you to notify the users of the accommodation when one of the events affecting safety occurs on one of the Versatile Thermostats.
|
This automation uses the excellent App Daemon named NOTIFIER developed by Horizon Domotique that you will find in demonstration [here](https://www.youtube.com/watch?v=chJylIK0ASo&ab_channel=HorizonDomotique) and the code is [here](https ://github.com/jlpouffier/home-assistant-config/blob/master/appdaemon/apps/notifier.py). It allows you to notify the users of the accommodation when one of the events affecting safety occurs on one of the Versatile Thermostats.
|
||||||
|
|
||||||
This is a great example of using the notifications described here [notification](#notifications).
|
This is a great example of using the notifications described here [notification](#notifications).
|
||||||
|
|
||||||
|
|||||||
@@ -178,20 +178,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
|
|
||||||
if hass.state == CoreState.running:
|
if hass.state == CoreState.running:
|
||||||
await api.reload_central_boiler_entities_list()
|
await api.reload_central_boiler_entities_list()
|
||||||
await api.init_vtherm_links(entry.entry_id)
|
await api.init_vtherm_links()
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||||
"""Update listener."""
|
"""Update listener."""
|
||||||
|
|
||||||
_LOGGER.debug(
|
|
||||||
"Calling update_listener entry: entry_id='%s', value='%s'",
|
|
||||||
entry.entry_id,
|
|
||||||
entry.data,
|
|
||||||
)
|
|
||||||
|
|
||||||
if entry.data.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_CENTRAL_CONFIG:
|
if entry.data.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_CENTRAL_CONFIG:
|
||||||
await reload_all_vtherm(hass)
|
await reload_all_vtherm(hass)
|
||||||
else:
|
else:
|
||||||
@@ -200,7 +193,7 @@ async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
|||||||
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass)
|
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass)
|
||||||
if api is not None:
|
if api is not None:
|
||||||
await api.reload_central_boiler_entities_list()
|
await api.reload_central_boiler_entities_list()
|
||||||
await api.init_vtherm_links(entry.entry_id)
|
await api.init_vtherm_links()
|
||||||
|
|
||||||
|
|
||||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
|
|||||||
@@ -19,10 +19,7 @@ from homeassistant.core import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from homeassistant.components.climate import ClimateEntity
|
from homeassistant.components.climate import ClimateEntity
|
||||||
from homeassistant.helpers.restore_state import (
|
from homeassistant.helpers.restore_state import RestoreEntity
|
||||||
RestoreEntity,
|
|
||||||
async_get as restore_async_get,
|
|
||||||
)
|
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.helpers.device_registry import DeviceInfo, DeviceEntryType
|
from homeassistant.helpers.device_registry import DeviceInfo, DeviceEntryType
|
||||||
@@ -594,24 +591,14 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
|||||||
# issue 428. Link to others entities will start at link
|
# issue 428. Link to others entities will start at link
|
||||||
# await self.async_startup()
|
# await self.async_startup()
|
||||||
|
|
||||||
async def async_will_remove_from_hass(self):
|
|
||||||
"""Try to force backup of entity"""
|
|
||||||
_LOGGER_ENERGY.debug(
|
|
||||||
"%s - force write before remove. Energy is %s", self, self.total_energy
|
|
||||||
)
|
|
||||||
# Force dump in background
|
|
||||||
await restore_async_get(self.hass).async_dump_states()
|
|
||||||
|
|
||||||
def remove_thermostat(self):
|
def remove_thermostat(self):
|
||||||
"""Called when the thermostat will be removed"""
|
"""Called when the thermostat will be removed"""
|
||||||
_LOGGER.info("%s - Removing thermostat", self)
|
_LOGGER.info("%s - Removing thermostat", self)
|
||||||
|
|
||||||
for under in self._underlyings:
|
for under in self._underlyings:
|
||||||
under.remove_entity()
|
under.remove_entity()
|
||||||
|
|
||||||
async def async_startup(self, central_configuration):
|
async def async_startup(self, central_configuration):
|
||||||
"""Triggered on startup, used to get old state and set internal states accordingly. This is triggered by
|
"""Triggered on startup, used to get old state and set internal states accordingly"""
|
||||||
VTherm API"""
|
|
||||||
_LOGGER.debug("%s - Calling async_startup", self)
|
_LOGGER.debug("%s - Calling async_startup", self)
|
||||||
|
|
||||||
_LOGGER.debug("%s - Calling async_startup_internal", self)
|
_LOGGER.debug("%s - Calling async_startup_internal", self)
|
||||||
@@ -1206,24 +1193,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
|||||||
if hvac_mode is None:
|
if hvac_mode is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
def save_state():
|
|
||||||
self.reset_last_change_time()
|
|
||||||
self.update_custom_attributes()
|
|
||||||
self.async_write_ha_state()
|
|
||||||
self.send_event(EventType.HVAC_MODE_EVENT, {"hvac_mode": self._hvac_mode})
|
|
||||||
|
|
||||||
# If we already are in OFF, the manual OFF should just overwrite the reason and saved_hvac_mode
|
|
||||||
if self._hvac_mode == HVACMode.OFF and hvac_mode == HVACMode.OFF:
|
|
||||||
_LOGGER.info(
|
|
||||||
"%s - already in OFF. Change the reason to MANUAL and erase the saved_havc_mode"
|
|
||||||
)
|
|
||||||
self._hvac_off_reason = HVAC_OFF_REASON_MANUAL
|
|
||||||
self._saved_hvac_mode = HVACMode.OFF
|
|
||||||
|
|
||||||
save_state()
|
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
self._hvac_mode = hvac_mode
|
self._hvac_mode = hvac_mode
|
||||||
|
|
||||||
# Delegate to all underlying
|
# Delegate to all underlying
|
||||||
@@ -1245,11 +1214,14 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
|||||||
|
|
||||||
# Ensure we update the current operation after changing the mode
|
# Ensure we update the current operation after changing the mode
|
||||||
self.reset_last_temperature_time()
|
self.reset_last_temperature_time()
|
||||||
|
self.reset_last_change_time()
|
||||||
|
|
||||||
if self._hvac_mode != HVACMode.OFF:
|
if self._hvac_mode != HVACMode.OFF:
|
||||||
self.set_hvac_off_reason(None)
|
self.set_hvac_off_reason(None)
|
||||||
|
|
||||||
save_state()
|
self.update_custom_attributes()
|
||||||
|
self.async_write_ha_state()
|
||||||
|
self.send_event(EventType.HVAC_MODE_EVENT, {"hvac_mode": self._hvac_mode})
|
||||||
|
|
||||||
@overrides
|
@overrides
|
||||||
async def async_set_preset_mode(
|
async def async_set_preset_mode(
|
||||||
@@ -2242,9 +2214,8 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
|||||||
save_all()
|
save_all()
|
||||||
|
|
||||||
if new_central_mode == CENTRAL_MODE_STOPPED:
|
if new_central_mode == CENTRAL_MODE_STOPPED:
|
||||||
if self.hvac_mode != HVACMode.OFF:
|
self.set_hvac_off_reason(HVAC_OFF_REASON_MANUAL)
|
||||||
self.set_hvac_off_reason(HVAC_OFF_REASON_MANUAL)
|
await self.async_set_hvac_mode(HVACMode.OFF)
|
||||||
await self.async_set_hvac_mode(HVACMode.OFF)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
if new_central_mode == CENTRAL_MODE_COOL_ONLY:
|
if new_central_mode == CENTRAL_MODE_COOL_ONLY:
|
||||||
@@ -2258,8 +2229,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
|||||||
if new_central_mode == CENTRAL_MODE_HEAT_ONLY:
|
if new_central_mode == CENTRAL_MODE_HEAT_ONLY:
|
||||||
if HVACMode.HEAT in self.hvac_modes:
|
if HVACMode.HEAT in self.hvac_modes:
|
||||||
await self.async_set_hvac_mode(HVACMode.HEAT)
|
await self.async_set_hvac_mode(HVACMode.HEAT)
|
||||||
# if not already off
|
else:
|
||||||
elif self.hvac_mode != HVACMode.OFF:
|
|
||||||
self.set_hvac_off_reason(HVAC_OFF_REASON_MANUAL)
|
self.set_hvac_off_reason(HVAC_OFF_REASON_MANUAL)
|
||||||
await self.async_set_hvac_mode(HVACMode.OFF)
|
await self.async_set_hvac_mode(HVACMode.OFF)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -109,17 +109,17 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
|||||||
or self._infos.get(CONF_WINDOW_AUTO_OPEN_THRESHOLD) is not None
|
or self._infos.get(CONF_WINDOW_AUTO_OPEN_THRESHOLD) is not None
|
||||||
)
|
)
|
||||||
self._infos[CONF_USE_MOTION_FEATURE] = self._infos.get(
|
self._infos[CONF_USE_MOTION_FEATURE] = self._infos.get(
|
||||||
CONF_USE_MOTION_FEATURE, False
|
CONF_USE_MOTION_FEATURE
|
||||||
) and (self._infos.get(CONF_MOTION_SENSOR) is not None or is_central_config)
|
) and (self._infos.get(CONF_MOTION_SENSOR) is not None or is_central_config)
|
||||||
|
|
||||||
self._infos[CONF_USE_POWER_FEATURE] = self._infos.get(
|
self._infos[CONF_USE_POWER_FEATURE] = self._infos.get(
|
||||||
CONF_USE_POWER_CENTRAL_CONFIG, False
|
CONF_USE_POWER_CENTRAL_CONFIG
|
||||||
) or (
|
) or (
|
||||||
self._infos.get(CONF_POWER_SENSOR) is not None
|
self._infos.get(CONF_POWER_SENSOR) is not None
|
||||||
and self._infos.get(CONF_MAX_POWER_SENSOR) is not None
|
and self._infos.get(CONF_MAX_POWER_SENSOR) is not None
|
||||||
)
|
)
|
||||||
self._infos[CONF_USE_PRESENCE_FEATURE] = (
|
self._infos[CONF_USE_PRESENCE_FEATURE] = (
|
||||||
self._infos.get(CONF_USE_PRESENCE_CENTRAL_CONFIG, False)
|
self._infos.get(CONF_USE_PRESENCE_CENTRAL_CONFIG)
|
||||||
or self._infos.get(CONF_PRESENCE_SENSOR) is not None
|
or self._infos.get(CONF_PRESENCE_SENSOR) is not None
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -129,7 +129,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self._infos[CONF_USE_AUTO_START_STOP_FEATURE] = (
|
self._infos[CONF_USE_AUTO_START_STOP_FEATURE] = (
|
||||||
self._infos.get(CONF_USE_AUTO_START_STOP_FEATURE, False) is True
|
self._infos.get(CONF_USE_AUTO_START_STOP_FEATURE) is True
|
||||||
and self._infos.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_CLIMATE
|
and self._infos.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_CLIMATE
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -145,17 +145,12 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
|||||||
CONF_USE_PRESETS_CENTRAL_CONFIG,
|
CONF_USE_PRESETS_CENTRAL_CONFIG,
|
||||||
CONF_USE_PRESENCE_CENTRAL_CONFIG,
|
CONF_USE_PRESENCE_CENTRAL_CONFIG,
|
||||||
CONF_USE_ADVANCED_CENTRAL_CONFIG,
|
CONF_USE_ADVANCED_CENTRAL_CONFIG,
|
||||||
CONF_USE_CENTRAL_MODE,
|
|
||||||
):
|
):
|
||||||
if not is_empty:
|
if not is_empty:
|
||||||
current_config = self._infos.get(config, None)
|
current_config = self._infos.get(config, None)
|
||||||
|
self._infos[config] = current_config is True or (
|
||||||
self._infos[config] = self._central_config is not None and (
|
current_config is None and self._central_config is not None
|
||||||
current_config is True or current_config is None
|
|
||||||
)
|
)
|
||||||
# self._infos[config] = current_config is True or (
|
|
||||||
# current_config is None and self._central_config is not None
|
|
||||||
# )
|
|
||||||
else:
|
else:
|
||||||
self._infos[config] = self._central_config is not None
|
self._infos[config] = self._central_config is not None
|
||||||
|
|
||||||
@@ -214,9 +209,6 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
|||||||
CONF_USE_PRESENCE_CENTRAL_CONFIG,
|
CONF_USE_PRESENCE_CENTRAL_CONFIG,
|
||||||
CONF_USE_PRESETS_CENTRAL_CONFIG,
|
CONF_USE_PRESETS_CENTRAL_CONFIG,
|
||||||
CONF_USE_ADVANCED_CENTRAL_CONFIG,
|
CONF_USE_ADVANCED_CENTRAL_CONFIG,
|
||||||
CONF_USE_CENTRAL_MODE,
|
|
||||||
# CONF_USE_CENTRAL_BOILER_FEATURE, this is for Central Config
|
|
||||||
CONF_USED_BY_CENTRAL_BOILER,
|
|
||||||
]:
|
]:
|
||||||
if data.get(conf) is True:
|
if data.get(conf) is True:
|
||||||
_LOGGER.error(
|
_LOGGER.error(
|
||||||
@@ -314,22 +306,6 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
|||||||
):
|
):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if (
|
|
||||||
infos.get(CONF_PROP_FUNCTION, None) == PROPORTIONAL_FUNCTION_TPI
|
|
||||||
and infos.get(CONF_USE_TPI_CENTRAL_CONFIG, False) is False
|
|
||||||
and (
|
|
||||||
infos.get(CONF_TPI_COEF_INT, None) is None
|
|
||||||
or infos.get(CONF_TPI_COEF_EXT) is None
|
|
||||||
)
|
|
||||||
):
|
|
||||||
return False
|
|
||||||
|
|
||||||
if (
|
|
||||||
infos.get(CONF_USE_PRESETS_CENTRAL_CONFIG, False) is True
|
|
||||||
and self._central_config is None
|
|
||||||
):
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def merge_user_input(self, data_schema: vol.Schema, user_input: dict):
|
def merge_user_input(self, data_schema: vol.Schema, user_input: dict):
|
||||||
|
|||||||
@@ -852,8 +852,7 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
|||||||
changes = True
|
changes = True
|
||||||
|
|
||||||
# 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
|
if not changes:
|
||||||
if 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,
|
||||||
|
|||||||
@@ -150,11 +150,10 @@ class VersatileThermostatAPI(dict):
|
|||||||
return entity.state
|
return entity.state
|
||||||
return None
|
return None
|
||||||
|
|
||||||
async def init_vtherm_links(self, entry_id=None):
|
async def init_vtherm_links(self):
|
||||||
"""Initialize all VTherms entities links
|
"""Initialize all VTherms entities links
|
||||||
This method is called when HA is fully started (and all entities should be initialized)
|
This method is called when HA is fully started (and all entities should be initialized)
|
||||||
Or when we need to reload all VTherm links (with Number temp entities, central boiler, ...)
|
Or when we need to reload all VTherm links (with Number temp entities, central boiler, ...)
|
||||||
If entry_id is set, only the VTherm of this entry will be reloaded
|
|
||||||
"""
|
"""
|
||||||
await self.reload_central_boiler_binary_listener()
|
await self.reload_central_boiler_binary_listener()
|
||||||
await self.reload_central_boiler_entities_list()
|
await self.reload_central_boiler_entities_list()
|
||||||
@@ -176,8 +175,7 @@ class VersatileThermostatAPI(dict):
|
|||||||
entity.device_info
|
entity.device_info
|
||||||
and entity.device_info.get("model", None) == DOMAIN
|
and entity.device_info.get("model", None) == DOMAIN
|
||||||
):
|
):
|
||||||
if entry_id is None or entry_id == entity.unique_id:
|
await entity.async_startup(self.find_central_configuration())
|
||||||
await entity.async_startup(self.find_central_configuration())
|
|
||||||
|
|
||||||
async def init_vtherm_preset_with_central(self):
|
async def init_vtherm_preset_with_central(self):
|
||||||
"""Init all VTherm presets when the VTherm uses central temperature"""
|
"""Init all VTherm presets when the VTherm uses central temperature"""
|
||||||
|
|||||||
@@ -630,7 +630,6 @@ async def test_climate_ac_only_change_central_mode_true(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
# 1. set hvac_mode to COOL and preet ECO
|
|
||||||
with patch("homeassistant.core.ServiceRegistry.async_call"), patch(
|
with patch("homeassistant.core.ServiceRegistry.async_call"), patch(
|
||||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
||||||
return_value=fake_underlying_climate,
|
return_value=fake_underlying_climate,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -11,8 +11,6 @@ from homeassistant.components.climate import (
|
|||||||
SERVICE_SET_TEMPERATURE,
|
SERVICE_SET_TEMPERATURE,
|
||||||
)
|
)
|
||||||
|
|
||||||
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
|
|
||||||
|
|
||||||
from custom_components.versatile_thermostat.thermostat_climate import (
|
from custom_components.versatile_thermostat.thermostat_climate import (
|
||||||
ThermostatOverClimate,
|
ThermostatOverClimate,
|
||||||
)
|
)
|
||||||
@@ -322,78 +320,6 @@ 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_bug_615(
|
|
||||||
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 don't change its own temperature target if no
|
|
||||||
target_temperature have already been sent"""
|
|
||||||
|
|
||||||
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
|
|
||||||
)
|
|
||||||
|
|
||||||
# 1. create the thermostat
|
|
||||||
with patch(
|
|
||||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
|
||||||
), patch(
|
|
||||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
|
||||||
return_value=fake_underlying_climate,
|
|
||||||
):
|
|
||||||
vtherm = await create_thermostat(hass, entry, "climate.theoverclimatemockname")
|
|
||||||
|
|
||||||
assert vtherm
|
|
||||||
|
|
||||||
assert vtherm.name == "TheOverClimateMockName"
|
|
||||||
assert vtherm.is_over_climate is True
|
|
||||||
assert vtherm.hvac_mode is HVACMode.OFF
|
|
||||||
# because in MockClimate HVACAction is HEATING if hvac_mode is not set
|
|
||||||
assert vtherm.hvac_action is HVACAction.HEATING
|
|
||||||
|
|
||||||
# Force a preset_mode without sending a temperature (as it was restored with a preset)
|
|
||||||
vtherm._attr_preset_mode = PRESET_BOOST
|
|
||||||
|
|
||||||
assert vtherm.target_temperature == vtherm.min_temp
|
|
||||||
assert vtherm.preset_mode is PRESET_BOOST
|
|
||||||
|
|
||||||
with patch(
|
|
||||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode"
|
|
||||||
) as mock_underlying_set_hvac_mode:
|
|
||||||
# 2. Change the target temp of underlying thermostat at now + 1 min
|
|
||||||
now = now + timedelta(minutes=1)
|
|
||||||
await send_climate_change_event_with_temperature(
|
|
||||||
vtherm,
|
|
||||||
HVACMode.OFF,
|
|
||||||
HVACMode.OFF,
|
|
||||||
HVACAction.OFF,
|
|
||||||
HVACAction.OFF,
|
|
||||||
now,
|
|
||||||
25,
|
|
||||||
True,
|
|
||||||
"climate.mock_climate", # the underlying climate entity id
|
|
||||||
)
|
|
||||||
# Should NOT have been taken the new target temp nor have change the preset
|
|
||||||
assert vtherm.target_temperature == vtherm.min_temp
|
|
||||||
assert vtherm.preset_mode is PRESET_BOOST
|
|
||||||
|
|
||||||
mock_underlying_set_hvac_mode.assert_not_called()
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||||
async def test_bug_508(
|
async def test_bug_508(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
@@ -705,356 +631,3 @@ async def test_ignore_temp_outside_minmax_range(
|
|||||||
"climate.mock_climate", # the underlying climate entity id
|
"climate.mock_climate", # the underlying climate entity id
|
||||||
)
|
)
|
||||||
assert entity.target_temperature == 17
|
assert entity.target_temperature == 17
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
|
||||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
|
||||||
async def test_manual_hvac_off_should_take_the_lead_over_window(
|
|
||||||
hass: HomeAssistant, skip_hass_states_is_state
|
|
||||||
):
|
|
||||||
"""Test than a manual hvac_off is taken into account over a window hvac_off"""
|
|
||||||
|
|
||||||
# The temperatures to set
|
|
||||||
temps = {
|
|
||||||
"frost": 7.0,
|
|
||||||
"eco": 17.0,
|
|
||||||
"comfort": 19.0,
|
|
||||||
"boost": 21.0,
|
|
||||||
"eco_ac": 27.0,
|
|
||||||
"comfort_ac": 25.0,
|
|
||||||
"boost_ac": 23.0,
|
|
||||||
"frost_away": 7.1,
|
|
||||||
"eco_away": 17.1,
|
|
||||||
"comfort_away": 19.1,
|
|
||||||
"boost_away": 21.1,
|
|
||||||
"eco_ac_away": 27.1,
|
|
||||||
"comfort_ac_away": 25.1,
|
|
||||||
"boost_ac_away": 23.1,
|
|
||||||
}
|
|
||||||
|
|
||||||
config_entry = MockConfigEntry(
|
|
||||||
domain=DOMAIN,
|
|
||||||
title="TheOverClimateMockName",
|
|
||||||
unique_id="overClimateUniqueId",
|
|
||||||
data={
|
|
||||||
CONF_NAME: "overClimate",
|
|
||||||
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
|
|
||||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE,
|
|
||||||
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
|
|
||||||
CONF_CYCLE_MIN: 5,
|
|
||||||
CONF_TEMP_MIN: 15,
|
|
||||||
CONF_TEMP_MAX: 30,
|
|
||||||
CONF_USE_WINDOW_FEATURE: True,
|
|
||||||
CONF_WINDOW_SENSOR: "binary_sensor.window_sensor",
|
|
||||||
CONF_WINDOW_DELAY: 10,
|
|
||||||
CONF_USE_MOTION_FEATURE: False,
|
|
||||||
CONF_USE_POWER_FEATURE: False,
|
|
||||||
CONF_USE_AUTO_START_STOP_FEATURE: True,
|
|
||||||
CONF_USE_PRESENCE_FEATURE: True,
|
|
||||||
CONF_PRESENCE_SENSOR: "binary_sensor.presence_sensor",
|
|
||||||
CONF_CLIMATE: "climate.mock_climate",
|
|
||||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
|
||||||
CONF_SECURITY_DELAY_MIN: 5,
|
|
||||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
|
||||||
CONF_AUTO_FAN_MODE: CONF_AUTO_FAN_TURBO,
|
|
||||||
CONF_AC_MODE: True,
|
|
||||||
CONF_AUTO_START_STOP_LEVEL: AUTO_START_STOP_LEVEL_FAST,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
fake_underlying_climate = MockClimate(
|
|
||||||
hass=hass,
|
|
||||||
unique_id="mock_climate",
|
|
||||||
name="mock_climate",
|
|
||||||
hvac_modes=[HVACMode.OFF, HVACMode.COOL, HVACMode.HEAT],
|
|
||||||
)
|
|
||||||
|
|
||||||
with patch(
|
|
||||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
|
||||||
return_value=fake_underlying_climate,
|
|
||||||
):
|
|
||||||
vtherm: ThermostatOverClimate = await create_thermostat(
|
|
||||||
hass, config_entry, "climate.overclimate"
|
|
||||||
)
|
|
||||||
|
|
||||||
assert vtherm is not None
|
|
||||||
|
|
||||||
# Initialize all temps
|
|
||||||
await set_all_climate_preset_temp(hass, vtherm, temps, "overclimate")
|
|
||||||
|
|
||||||
# Check correct initialization of auto_start_stop attributes
|
|
||||||
assert (
|
|
||||||
vtherm._attr_extra_state_attributes["auto_start_stop_level"]
|
|
||||||
== AUTO_START_STOP_LEVEL_FAST
|
|
||||||
)
|
|
||||||
|
|
||||||
assert vtherm.auto_start_stop_level == AUTO_START_STOP_LEVEL_FAST
|
|
||||||
enable_entity = search_entity(
|
|
||||||
hass, "switch.overclimate_enable_auto_start_stop", SWITCH_DOMAIN
|
|
||||||
)
|
|
||||||
assert enable_entity is not None
|
|
||||||
assert enable_entity.state == STATE_ON
|
|
||||||
|
|
||||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
|
||||||
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_presence_change_event(vtherm, True, False, now)
|
|
||||||
await send_temperature_change_event(vtherm, 18, now, True)
|
|
||||||
await vtherm.async_set_hvac_mode(HVACMode.HEAT)
|
|
||||||
await vtherm.async_set_preset_mode(PRESET_COMFORT)
|
|
||||||
await hass.async_block_till_done()
|
|
||||||
|
|
||||||
assert vtherm.target_temperature == 19.0
|
|
||||||
# VTherm should be heating
|
|
||||||
assert vtherm.hvac_mode == HVACMode.HEAT
|
|
||||||
# VTherm window_state should be off
|
|
||||||
assert vtherm.window_state == STATE_OFF
|
|
||||||
|
|
||||||
# 2. Open the window and wait for the delay
|
|
||||||
now = now + timedelta(minutes=2)
|
|
||||||
with patch(
|
|
||||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
|
||||||
) as mock_send_event, patch(
|
|
||||||
"homeassistant.helpers.condition.state", return_value=True
|
|
||||||
):
|
|
||||||
vtherm._set_now(now)
|
|
||||||
try_function = await send_window_change_event(
|
|
||||||
vtherm, True, False, now, sleep=False
|
|
||||||
)
|
|
||||||
|
|
||||||
await try_function(None)
|
|
||||||
|
|
||||||
# Nothing should have change (window event is ignoed as we are already OFF)
|
|
||||||
assert vtherm.hvac_mode == HVACMode.OFF
|
|
||||||
assert vtherm.hvac_off_reason == HVAC_OFF_REASON_WINDOW_DETECTION
|
|
||||||
assert vtherm._saved_hvac_mode == HVACMode.HEAT
|
|
||||||
|
|
||||||
assert mock_send_event.call_count == 2
|
|
||||||
|
|
||||||
assert vtherm.window_state == STATE_ON
|
|
||||||
|
|
||||||
# 3. Turn off manually the VTherm. This should be taken into account
|
|
||||||
now = now + timedelta(minutes=1)
|
|
||||||
vtherm._set_now(now)
|
|
||||||
with patch(
|
|
||||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
|
||||||
) as mock_send_event:
|
|
||||||
await vtherm.async_set_hvac_mode(HVACMode.OFF)
|
|
||||||
await hass.async_block_till_done()
|
|
||||||
|
|
||||||
# Should be off with reason MANUAL
|
|
||||||
assert vtherm.hvac_mode == HVACMode.OFF
|
|
||||||
assert vtherm.hvac_off_reason == HVAC_OFF_REASON_MANUAL
|
|
||||||
assert vtherm._saved_hvac_mode == HVACMode.OFF
|
|
||||||
# Window state should not change
|
|
||||||
assert vtherm.window_state == STATE_ON
|
|
||||||
|
|
||||||
assert mock_send_event.call_count == 1
|
|
||||||
mock_send_event.assert_has_calls(
|
|
||||||
[
|
|
||||||
call(EventType.HVAC_MODE_EVENT, {"hvac_mode": HVACMode.OFF}),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
# 4. close the window -> we should stay off reason manual
|
|
||||||
now = now + timedelta(minutes=1)
|
|
||||||
vtherm._set_now(now)
|
|
||||||
with patch(
|
|
||||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
|
||||||
) as mock_send_event, patch(
|
|
||||||
"homeassistant.helpers.condition.state", return_value=True
|
|
||||||
):
|
|
||||||
try_function = await send_window_change_event(
|
|
||||||
vtherm, False, True, now, sleep=False
|
|
||||||
)
|
|
||||||
|
|
||||||
await try_function(None)
|
|
||||||
|
|
||||||
# The VTherm should turn on and off again due to auto-start-stop
|
|
||||||
assert vtherm.hvac_mode == HVACMode.OFF
|
|
||||||
assert vtherm.hvac_off_reason is HVAC_OFF_REASON_MANUAL
|
|
||||||
assert vtherm._saved_hvac_mode == HVACMode.OFF
|
|
||||||
|
|
||||||
assert vtherm.window_state == STATE_OFF
|
|
||||||
assert mock_send_event.call_count == 0
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
|
||||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
|
||||||
async def test_manual_hvac_off_should_take_the_lead_over_auto_start_stop(
|
|
||||||
hass: HomeAssistant, skip_hass_states_is_state
|
|
||||||
):
|
|
||||||
"""Test than a manual hvac_off is taken into account over a auto-start/stop hvac_off"""
|
|
||||||
|
|
||||||
# The temperatures to set
|
|
||||||
temps = {
|
|
||||||
"frost": 7.0,
|
|
||||||
"eco": 17.0,
|
|
||||||
"comfort": 19.0,
|
|
||||||
"boost": 21.0,
|
|
||||||
"eco_ac": 27.0,
|
|
||||||
"comfort_ac": 25.0,
|
|
||||||
"boost_ac": 23.0,
|
|
||||||
"frost_away": 7.1,
|
|
||||||
"eco_away": 17.1,
|
|
||||||
"comfort_away": 19.1,
|
|
||||||
"boost_away": 21.1,
|
|
||||||
"eco_ac_away": 27.1,
|
|
||||||
"comfort_ac_away": 25.1,
|
|
||||||
"boost_ac_away": 23.1,
|
|
||||||
}
|
|
||||||
|
|
||||||
config_entry = MockConfigEntry(
|
|
||||||
domain=DOMAIN,
|
|
||||||
title="TheOverClimateMockName",
|
|
||||||
unique_id="overClimateUniqueId",
|
|
||||||
data={
|
|
||||||
CONF_NAME: "overClimate",
|
|
||||||
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
|
|
||||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE,
|
|
||||||
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
|
|
||||||
CONF_CYCLE_MIN: 5,
|
|
||||||
CONF_TEMP_MIN: 15,
|
|
||||||
CONF_TEMP_MAX: 30,
|
|
||||||
CONF_USE_WINDOW_FEATURE: True,
|
|
||||||
CONF_WINDOW_SENSOR: "binary_sensor.window_sensor",
|
|
||||||
CONF_WINDOW_DELAY: 10,
|
|
||||||
CONF_USE_MOTION_FEATURE: False,
|
|
||||||
CONF_USE_POWER_FEATURE: False,
|
|
||||||
CONF_USE_AUTO_START_STOP_FEATURE: True,
|
|
||||||
CONF_USE_PRESENCE_FEATURE: True,
|
|
||||||
CONF_PRESENCE_SENSOR: "binary_sensor.presence_sensor",
|
|
||||||
CONF_CLIMATE: "climate.mock_climate",
|
|
||||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
|
||||||
CONF_SECURITY_DELAY_MIN: 5,
|
|
||||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
|
||||||
CONF_AUTO_FAN_MODE: CONF_AUTO_FAN_TURBO,
|
|
||||||
CONF_AC_MODE: True,
|
|
||||||
CONF_AUTO_START_STOP_LEVEL: AUTO_START_STOP_LEVEL_FAST,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
fake_underlying_climate = MockClimate(
|
|
||||||
hass=hass,
|
|
||||||
unique_id="mock_climate",
|
|
||||||
name="mock_climate",
|
|
||||||
hvac_modes=[HVACMode.OFF, HVACMode.COOL, HVACMode.HEAT],
|
|
||||||
)
|
|
||||||
|
|
||||||
with patch(
|
|
||||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
|
||||||
return_value=fake_underlying_climate,
|
|
||||||
):
|
|
||||||
vtherm: ThermostatOverClimate = await create_thermostat(
|
|
||||||
hass, config_entry, "climate.overclimate"
|
|
||||||
)
|
|
||||||
|
|
||||||
assert vtherm is not None
|
|
||||||
|
|
||||||
# Initialize all temps
|
|
||||||
await set_all_climate_preset_temp(hass, vtherm, temps, "overclimate")
|
|
||||||
|
|
||||||
# Check correct initialization of auto_start_stop attributes
|
|
||||||
assert (
|
|
||||||
vtherm._attr_extra_state_attributes["auto_start_stop_level"]
|
|
||||||
== AUTO_START_STOP_LEVEL_FAST
|
|
||||||
)
|
|
||||||
|
|
||||||
assert vtherm.auto_start_stop_level == AUTO_START_STOP_LEVEL_FAST
|
|
||||||
enable_entity = search_entity(
|
|
||||||
hass, "switch.overclimate_enable_auto_start_stop", SWITCH_DOMAIN
|
|
||||||
)
|
|
||||||
assert enable_entity is not None
|
|
||||||
assert enable_entity.state == STATE_ON
|
|
||||||
|
|
||||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
|
||||||
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_presence_change_event(vtherm, True, False, now)
|
|
||||||
await send_temperature_change_event(vtherm, 18, now, True)
|
|
||||||
await vtherm.async_set_hvac_mode(HVACMode.HEAT)
|
|
||||||
await vtherm.async_set_preset_mode(PRESET_COMFORT)
|
|
||||||
await hass.async_block_till_done()
|
|
||||||
|
|
||||||
assert vtherm.target_temperature == 19.0
|
|
||||||
# VTherm should be heating
|
|
||||||
assert vtherm.hvac_mode == HVACMode.HEAT
|
|
||||||
|
|
||||||
# 2. Set current temperature to 21 5 min later -> should turn off VTherm
|
|
||||||
now = now + timedelta(minutes=5)
|
|
||||||
vtherm._set_now(now)
|
|
||||||
# reset accumulated error (only for testing)
|
|
||||||
vtherm._auto_start_stop_algo._accumulated_error = 0
|
|
||||||
with patch(
|
|
||||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
|
||||||
) as mock_send_event:
|
|
||||||
await send_temperature_change_event(vtherm, 21, now, True)
|
|
||||||
await hass.async_block_till_done()
|
|
||||||
|
|
||||||
# VTherm should no more be heating
|
|
||||||
assert vtherm.hvac_mode == HVACMode.OFF
|
|
||||||
assert vtherm.hvac_off_reason == HVAC_OFF_REASON_AUTO_START_STOP
|
|
||||||
assert vtherm._saved_hvac_mode == HVACMode.HEAT
|
|
||||||
assert mock_send_event.call_count == 2 # turned to off
|
|
||||||
|
|
||||||
mock_send_event.assert_has_calls(
|
|
||||||
[
|
|
||||||
call(EventType.HVAC_MODE_EVENT, {"hvac_mode": HVACMode.OFF}),
|
|
||||||
call(
|
|
||||||
event_type=EventType.AUTO_START_STOP_EVENT,
|
|
||||||
data={
|
|
||||||
"type": "stop",
|
|
||||||
"name": "overClimate",
|
|
||||||
"cause": "Auto stop conditions reached",
|
|
||||||
"hvac_mode": HVACMode.OFF,
|
|
||||||
"saved_hvac_mode": HVACMode.HEAT,
|
|
||||||
"target_temperature": 19.0,
|
|
||||||
"current_temperature": 21.0,
|
|
||||||
"temperature_slope": 0.3,
|
|
||||||
"accumulated_error": -2,
|
|
||||||
"accumulated_error_threshold": 2,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
# 3. Turn off manually the VTherm. This should be taken into account
|
|
||||||
now = now + timedelta(minutes=1)
|
|
||||||
with patch(
|
|
||||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
|
||||||
) as mock_send_event:
|
|
||||||
await vtherm.async_set_hvac_mode(HVACMode.OFF)
|
|
||||||
await hass.async_block_till_done()
|
|
||||||
|
|
||||||
# Should be off with reason MANUAL
|
|
||||||
assert vtherm.hvac_mode == HVACMode.OFF
|
|
||||||
assert vtherm.hvac_off_reason == HVAC_OFF_REASON_MANUAL
|
|
||||||
assert vtherm._saved_hvac_mode == HVACMode.OFF
|
|
||||||
|
|
||||||
assert mock_send_event.call_count == 1
|
|
||||||
mock_send_event.assert_has_calls(
|
|
||||||
[
|
|
||||||
call(EventType.HVAC_MODE_EVENT, {"hvac_mode": HVACMode.OFF}),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
# 4. removes the auto-start/stop detection
|
|
||||||
now = now + timedelta(minutes=5)
|
|
||||||
vtherm._set_now(now)
|
|
||||||
with patch(
|
|
||||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
|
||||||
) as mock_send_event, patch(
|
|
||||||
"homeassistant.helpers.condition.state", return_value=True
|
|
||||||
):
|
|
||||||
await send_temperature_change_event(vtherm, 15, now, True)
|
|
||||||
await hass.async_block_till_done()
|
|
||||||
|
|
||||||
# VTherm should no more be heating
|
|
||||||
assert vtherm.hvac_mode == HVACMode.OFF
|
|
||||||
assert vtherm.hvac_off_reason == HVAC_OFF_REASON_MANUAL
|
|
||||||
assert vtherm._saved_hvac_mode == HVACMode.OFF
|
|
||||||
assert mock_send_event.call_count == 0 # nothing have change
|
|
||||||
|
|||||||
Reference in New Issue
Block a user