Compare commits

..

14 Commits
7.1.4 ... 7.1.6

Author SHA1 Message Date
Jean-Marc Collin
ebc2d8b6ac Release 7.1.6 2025-01-25 10:57:47 +00:00
Jean-Marc Collin
cf4cc32b13 Typos 2025-01-25 10:56:42 +00:00
Jean-Marc Collin
2523fc74c2 Update over-climate.md
Add precision on "use internal device temp"
2025-01-25 11:41:11 +01:00
Jean-Marc Collin
87de91c2c5 Update over-climate.md
Add precision to "use internal device temp"
2025-01-25 11:40:10 +01:00
Jean-Marc Collin
a3b6f66f1b Issue #846 - swicth Eco preset after Window close event (#868)
Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2025-01-25 11:09:34 +01:00
Jean-Marc Collin
8cf09e5254 Update self-regulation.md 2025-01-25 09:22:13 +01:00
Jean-Marc Collin
0b67226666 Update self-regulation.md 2025-01-25 09:21:24 +01:00
Jean-Marc Collin
812bb19e10 Update self-regulation.md
Precision on min opening values
2025-01-25 09:18:28 +01:00
Eric Scherlinger
be3012af71 Fixes on FR translation (#851) 2025-01-21 21:02:51 +01:00
Jean-Marc Collin
05fe2055e2 [Feature Request] - Pwoer shedding should prevent all VTherm to start at the same time (#845)
Fixes #844

Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2025-01-19 20:45:37 +01:00
Jean-Marc Collin
8743872c09 Add config.yml into ISSUE_TEMPLATE 2025-01-19 16:30:16 +00:00
Jean-Marc Collin
c4a1631d29 [Feature Request] - Resend state to underlying device is device switch from Unknown state. (#841)
Fixes #829

Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2025-01-18 18:02:40 +01:00
Jean-Marc Collin
d1ef83f422 hvac_off_reason could stay at auto_start_stop when the VTherm is 'on' (hvac_mode=Heat) (#840)
Fixes #828

Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2025-01-18 16:34:44 +01:00
Jean-Marc Collin
42ac2b0f98 #823 - window configuration don't hold on VTherm configuration (#839)
* Improve documentation

* Fix should be not possible to validate a VTherm without underlyings

---------

Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2025-01-18 13:01:31 +01:00
32 changed files with 795 additions and 240 deletions

1
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1 @@
blank_issues_enabled: false

View File

@@ -35,6 +35,12 @@ Un grand merci à tous mes fournisseurs de bières pour leurs dons et leurs enco
_AC_ : Air conditionné. Un équipement est AC si il fait du froid. Les températures sont alors inversées : Eco est plus chaud que Confort qui est plus chaud que Boost. Les algorithmes tiennent compte de cette information.
_EMA_ : Exponential Moving Average. Utilisé pour lisser les mesures de températures de capteur. Elle correspond à une moyenne glissante de la température de la pièce. Elle est utilisée pour calculer la pente de la courbe de température (slope) qui serait trop instable sur la courbe brute.
_slope_ : la pente de la courbe de température. Elle est mesurée en °(C ou K)/h. Elle est positive si la température augmente et négative si elle diminue. Cette pente est calculée sur l'_EMA_
_PAC_ : Pompe à chaleur
# Documentation
La documentation est maintenant découpée en plusieurs pages pour faciliter la lecture et la recherche d'informations :

View File

@@ -34,6 +34,12 @@ A big thank you to all my beer sponsors for their donations and encouragements.
_AC_: Air Conditioning. An AC device cools instead of heats. Temperatures are reversed: Eco is warmer than Comfort, which is warmer than Boost. The algorithms take this information into account.
_EMA_: Exponential Moving Average. Used to smooth sensor temperature measurements. It represents a moving average of the room's temperature and is used to calculate the slope of the temperature curve, which would be too unstable on the raw data.
_slope_: The slope of the temperature curve, measured in ° (C or K)/h. It is positive when the temperature increases and negative when it decreases. This slope is calculated based on the _EMA_.
_PAC_ : Heat pump
# Documentation
The documentation is now divided into several pages for easier reading and searching:

View File

@@ -1050,7 +1050,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
self._hvac_mode in [HVACMode.COOL, HVACMode.HEAT, HVACMode.HEAT_COOL]
and self.preset_mode != PRESET_NONE
):
if self.preset_mode != PRESET_FROST_PROTECTION:
if self.preset_mode != PRESET_FROST_PROTECTION or self._hvac_mode in [HVACMode.HEAT, HVACMode.HEAT_COOL]:
await self.async_set_preset_mode_internal(self.preset_mode, True)
else:
await self.async_set_preset_mode_internal(PRESET_ECO, True, False)
@@ -1067,9 +1067,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
save_state()
@overrides
async def async_set_preset_mode(
self, preset_mode: str, overwrite_saved_preset=True
):
async def async_set_preset_mode(self, preset_mode: str, overwrite_saved_preset=True):
"""Set new preset mode."""
# We accept a new preset when:
@@ -1097,14 +1095,10 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
return
await self.async_set_preset_mode_internal(
preset_mode, force=False, overwrite_saved_preset=overwrite_saved_preset
)
await self.async_set_preset_mode_internal(preset_mode, force=False, overwrite_saved_preset=overwrite_saved_preset)
await self.async_control_heating(force=True)
async def async_set_preset_mode_internal(
self, preset_mode: str, force=False, overwrite_saved_preset=True
):
async def async_set_preset_mode_internal(self, preset_mode: str, force=False, overwrite_saved_preset=True):
"""Set new preset mode."""
_LOGGER.info("%s - Set preset_mode: %s force=%s", self, preset_mode, force)
if (
@@ -1250,12 +1244,12 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
async def async_set_humidity(self, humidity: int):
"""Set new target humidity."""
_LOGGER.info("%s - Set fan mode: %s", self, humidity)
_LOGGER.info("%s - Set humidity: %s", self, humidity)
return
async def async_set_swing_mode(self, swing_mode: str):
"""Set new target swing operation."""
_LOGGER.info("%s - Set fan mode: %s", self, swing_mode)
_LOGGER.info("%s - Set swing mode: %s", self, swing_mode)
return
async def async_set_temperature(self, **kwargs):
@@ -1534,8 +1528,8 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
is_window_detected = self._window_manager.is_window_detected
if new_central_mode == CENTRAL_MODE_AUTO:
if not is_window_detected and not first_init:
await self.restore_hvac_mode()
await self.restore_preset_mode()
await self.restore_preset_mode(force=False)
await self.restore_hvac_mode(need_control_heating=True)
elif is_window_detected and self.hvac_mode == HVACMode.OFF:
# do not restore but mark the reason of off with window detection
self.set_hvac_off_reason(HVAC_OFF_REASON_WINDOW_DETECTION)
@@ -1573,9 +1567,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
and HVACMode.HEAT in self.hvac_modes
):
await self.async_set_hvac_mode(HVACMode.HEAT)
await self.async_set_preset_mode(
PRESET_FROST_PROTECTION, overwrite_saved_preset=False
)
await self.async_set_preset_mode(PRESET_FROST_PROTECTION, overwrite_saved_preset=False)
else:
self.set_hvac_off_reason(HVAC_OFF_REASON_MANUAL)
await self.async_set_hvac_mode(HVACMode.OFF)
@@ -1800,9 +1792,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
# If the changed preset is active, change the current temperature
# Issue #119 - reload new preset temperature also in ac mode
if preset.startswith(self._attr_preset_mode):
await self.async_set_preset_mode_internal(
preset.rstrip(PRESET_AC_SUFFIX), force=True
)
await self.async_set_preset_mode_internal(preset.rstrip(PRESET_AC_SUFFIX), force=True)
await self.async_control_heating(force=True)
async def SERVICE_SET_SAFETY(

View File

@@ -47,6 +47,7 @@ class CentralFeaturePowerManager(BaseFeatureManager):
self._current_max_power: float = None
self._power_temp: float = None
self._cancel_calculate_shedding_call = None
self._started_vtherm_total_power: float = None
# Not used now
self._last_shedding_date = None
@@ -71,6 +72,7 @@ class CentralFeaturePowerManager(BaseFeatureManager):
and self._power_temp
):
self._is_configured = True
self._started_vtherm_total_power = 0
else:
_LOGGER.info("Power management is not fully configured and will be deactivated")
@@ -102,6 +104,8 @@ class CentralFeaturePowerManager(BaseFeatureManager):
"""Handle power changes."""
_LOGGER.debug("Receive new Power event")
_LOGGER.debug(event)
self._started_vtherm_total_power = 0
await self.refresh_state()
@callback
@@ -275,6 +279,12 @@ class CentralFeaturePowerManager(BaseFeatureManager):
vtherms.sort(key=cmp_to_key(cmp_temps))
return vtherms
def add_started_vtherm_total_power(self, started_power: float):
"""Add the power into the _started_vtherm_total_power which holds all VTherm started after
the last power measurement"""
self._started_vtherm_total_power += started_power
_LOGGER.debug("%s - started_vtherm_total_power is now %s", self, self._started_vtherm_total_power)
@property
def is_configured(self) -> bool:
"""True if the FeatureManager is fully configured"""
@@ -305,5 +315,10 @@ class CentralFeaturePowerManager(BaseFeatureManager):
"""Return the max power sensor entity id"""
return self._max_power_sensor_entity_id
@property
def started_vtherm_total_power(self) -> float | None:
"""Return the started_vtherm_total_power"""
return self._started_vtherm_total_power
def __str__(self):
return "CentralPowerManager"

View File

@@ -318,9 +318,8 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
):
return False
if infos.get(CONF_UNDERLYING_LIST, None) is not None and not infos.get(
CONF_UNDERLYING_LIST, None
):
# checks that at least one underlying is set but not it central configuration
if len(infos.get(CONF_UNDERLYING_LIST, [])) < 1:
return False
if (
@@ -901,10 +900,42 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
return await self.generic_step("advanced", schema, user_input, next_step)
async def async_step_finalize(self, _):
"""Should be implemented by Leaf classes"""
raise HomeAssistantError(
"async_finalize not implemented on VersatileThermostat sub-class"
)
"""Finalize the creation. Should be overriden by underlyings"""
if not self._infos[CONF_USE_WINDOW_FEATURE]:
self._infos[CONF_USE_WINDOW_CENTRAL_CONFIG] = False
if CONF_WINDOW_SENSOR in self._infos:
del self._infos[CONF_WINDOW_SENSOR]
if CONF_WINDOW_AUTO_CLOSE_THRESHOLD in self._infos:
del self._infos[CONF_WINDOW_AUTO_CLOSE_THRESHOLD]
if CONF_WINDOW_AUTO_OPEN_THRESHOLD in self._infos:
del self._infos[CONF_WINDOW_AUTO_OPEN_THRESHOLD]
if CONF_WINDOW_AUTO_MAX_DURATION in self._infos:
del self._infos[CONF_WINDOW_AUTO_MAX_DURATION]
if not self._infos[CONF_USE_MOTION_FEATURE]:
self._infos[CONF_USE_MOTION_CENTRAL_CONFIG] = False
if CONF_MOTION_SENSOR in self._infos:
del self._infos[CONF_MOTION_SENSOR]
if not self._infos[CONF_USE_POWER_FEATURE]:
self._infos[CONF_USE_POWER_CENTRAL_CONFIG] = False
if CONF_POWER_SENSOR in self._infos:
del self._infos[CONF_POWER_SENSOR]
if CONF_MAX_POWER_SENSOR in self._infos:
del self._infos[CONF_MAX_POWER_SENSOR]
if not self._infos[CONF_USE_PRESENCE_FEATURE]:
self._infos[CONF_USE_PRESENCE_CENTRAL_CONFIG] = False
if CONF_PRESENCE_SENSOR in self._infos:
del self._infos[CONF_PRESENCE_SENSOR]
if not self._infos[CONF_USE_CENTRAL_BOILER_FEATURE]:
if CONF_CENTRAL_BOILER_ACTIVATION_SRV in self._infos:
del self._infos[CONF_CENTRAL_BOILER_ACTIVATION_SRV]
if CONF_CENTRAL_BOILER_DEACTIVATION_SRV in self._infos:
del self._infos[CONF_CENTRAL_BOILER_DEACTIVATION_SRV]
if not self._infos[CONF_USE_AUTO_START_STOP_FEATURE]:
self._infos[CONF_AUTO_START_STOP_LEVEL] = AUTO_START_STOP_LEVEL_NONE
# Removes temporary value
if COMES_FROM in self._infos:
del self._infos[COMES_FROM]
class VersatileThermostatConfigFlow( # pylint: disable=abstract-method
@@ -928,9 +959,8 @@ class VersatileThermostatConfigFlow( # pylint: disable=abstract-method
async def async_step_finalize(self, _):
"""Finalization of the ConfigEntry creation"""
_LOGGER.debug("ConfigFlow.async_finalize")
# Removes temporary value
if COMES_FROM in self._infos:
del self._infos[COMES_FROM]
await super().async_step_finalize(_)
return self.async_create_entry(title=self._infos[CONF_NAME], data=self._infos)
@@ -968,37 +998,13 @@ class VersatileThermostatOptionsFlowHandler(
async def async_step_finalize(self, _):
"""Finalization of the ConfigEntry creation"""
if not self._infos[CONF_USE_WINDOW_FEATURE]:
self._infos[CONF_USE_WINDOW_CENTRAL_CONFIG] = False
self._infos[CONF_WINDOW_SENSOR] = None
self._infos[CONF_WINDOW_AUTO_CLOSE_THRESHOLD] = None
self._infos[CONF_WINDOW_AUTO_OPEN_THRESHOLD] = None
self._infos[CONF_WINDOW_AUTO_MAX_DURATION] = None
if not self._infos[CONF_USE_MOTION_FEATURE]:
self._infos[CONF_USE_MOTION_CENTRAL_CONFIG] = False
self._infos[CONF_MOTION_SENSOR] = None
if not self._infos[CONF_USE_POWER_FEATURE]:
self._infos[CONF_USE_POWER_CENTRAL_CONFIG] = False
self._infos[CONF_POWER_SENSOR] = None
self._infos[CONF_MAX_POWER_SENSOR] = None
if not self._infos[CONF_USE_PRESENCE_FEATURE]:
self._infos[CONF_USE_PRESENCE_CENTRAL_CONFIG] = False
self._infos[CONF_PRESENCE_SENSOR] = None
if not self._infos[CONF_USE_CENTRAL_BOILER_FEATURE]:
self._infos[CONF_CENTRAL_BOILER_ACTIVATION_SRV] = None
self._infos[CONF_CENTRAL_BOILER_DEACTIVATION_SRV] = None
if not self._infos[CONF_USE_AUTO_START_STOP_FEATURE]:
self._infos[CONF_AUTO_START_STOP_LEVEL] = AUTO_START_STOP_LEVEL_NONE
_LOGGER.info(
"Recreating entry %s due to configuration change. New config is now: %s",
self.config_entry.entry_id,
self._infos,
)
# Removes temporary value
if COMES_FROM in self._infos:
del self._infos[COMES_FROM]
await super().async_step_finalize(_)
self.hass.config_entries.async_update_entry(self.config_entry, data=self._infos)
return self.async_create_entry(title=None, data=None)

View File

@@ -70,6 +70,10 @@ class FeatureAutoStartStopManager(BaseFeatureManager):
self._auto_start_stop_level, self.name
)
# Fix an eventual incoherent state
if self._vtherm.is_on and self._vtherm.hvac_off_reason == HVAC_OFF_REASON_AUTO_START_STOP:
self._vtherm.hvac_off_reason = None
@overrides
async def start_listening(self):
"""Start listening the underlying entity"""

View File

@@ -104,7 +104,8 @@ class FeaturePowerManager(BaseFeatureManager):
async def check_power_available(self) -> bool:
"""Check if the Vtherm can be started considering overpowering.
Returns True if no overpowering conditions are found
Returns True if no overpowering conditions are found.
If True the vtherm power is written into the temporay vtherm started
"""
vtherm_api = VersatileThermostatAPI.get_vtherm_api()
@@ -116,6 +117,7 @@ class FeaturePowerManager(BaseFeatureManager):
current_power = vtherm_api.central_power_manager.current_power
current_max_power = vtherm_api.central_power_manager.current_max_power
started_vtherm_total_power = vtherm_api.central_power_manager.started_vtherm_total_power
if (
current_power is None
or current_max_power is None
@@ -146,7 +148,7 @@ class FeaturePowerManager(BaseFeatureManager):
self._device_power * self._vtherm.proportional_algorithm.on_percent,
)
ret = (current_power + power_consumption_max) < current_max_power
ret = (current_power + started_vtherm_total_power + power_consumption_max) < current_max_power
if not ret:
_LOGGER.info(
"%s - there is not enough power available power=%.3f, max_power=%.3f heater power=%.3f",
@@ -155,6 +157,10 @@ class FeaturePowerManager(BaseFeatureManager):
current_max_power,
self._device_power,
)
else:
# Adds the current_power_max to the started vtherm total power
vtherm_api.central_power_manager.add_started_vtherm_total_power(power_consumption_max)
return ret
async def set_overpowering(self, overpowering: bool, power_consumption_max=0):

View File

@@ -14,6 +14,6 @@
"quality_scale": "silver",
"requirements": [],
"ssdp": [],
"version": "7.1.4",
"version": "7.1.6",
"zeroconf": []
}

View File

@@ -3,7 +3,7 @@
import logging
from datetime import timedelta, datetime
from homeassistant.const import STATE_ON
from homeassistant.const import STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN
from homeassistant.core import Event, HomeAssistant, State, callback
from homeassistant.helpers.event import (
async_track_state_change_event,
@@ -614,7 +614,7 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
return
# Find the underlying which have change
under = self.find_underlying_by_entity_id(new_state.entity_id)
under: UnderlyingClimate = self.find_underlying_by_entity_id(new_state.entity_id)
if not under:
_LOGGER.warning(
@@ -626,6 +626,16 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
new_hvac_mode = new_state.state
old_state = event.data.get("old_state")
# Issue #829 - refresh underlying command if it comes back to life
if old_state is not None and new_state.state not in (STATE_UNAVAILABLE, STATE_UNKNOWN) and old_state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN):
_LOGGER.warning("%s - underlying %s come back to life. New state=%s, old_state=%s. Will refresh its status", self, under.entity_id, new_state.state, old_state.state)
# Force hvac_mode and target temperature
await under.set_hvac_mode(self.hvac_mode)
await self._send_regulated_temperature(force=True)
return
old_hvac_action = (
old_state.attributes.get("hvac_action")
if old_state and old_state.attributes
@@ -1094,7 +1104,7 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
@overrides
async def async_set_humidity(self, humidity: int):
"""Set new target humidity."""
_LOGGER.info("%s - Set fan mode: %s", self, humidity)
_LOGGER.info("%s - Set humidity: %s", self, humidity)
if humidity is None:
return
for under in self._underlyings:
@@ -1105,7 +1115,7 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
@overrides
async def async_set_swing_mode(self, swing_mode):
"""Set new target swing operation."""
_LOGGER.info("%s - Set fan mode: %s", self, swing_mode)
_LOGGER.info("%s - Set swing mode: %s", self, swing_mode)
if swing_mode is None:
return
for under in self._underlyings:

View File

@@ -155,7 +155,7 @@
},
"data_description": {
"motion_sensor_entity_id": "Id d'entité du détecteur de mouvement",
"motion_delay": "Délai avant activation lorsqu'un mouvement est détecté (secondss)",
"motion_delay": "Délai avant activation lorsqu'un mouvement est détecté (secondes)",
"motion_off_delai": "Délai avant désactivation lorsqu'aucun mouvement n'est détecté (secondes)",
"motion_preset": "Preset à utiliser si mouvement détecté",
"no_motion_preset": "Preset à utiliser si pas de mouvement détecté",
@@ -200,7 +200,7 @@
"use_advanced_central_config": "Utiliser la configuration centrale avancée"
},
"data_description": {
"minimal_activation_delay": "Délai en seondes en-dessous duquel l'équipement ne sera pas activé",
"minimal_activation_delay": "Délai en secondes en-dessous duquel l'équipement ne sera pas activé",
"safety_delay_min": "Délai maximal autorisé en minutes entre 2 mesures de températures. Au-dessus de ce délai, le thermostat se mettra en position de sécurité",
"safety_min_on_percent": "Seuil minimal de pourcentage de chauffage en-dessous duquel le préréglage sécurité ne sera jamais activé",
"safety_default_on_percent": "Valeur par défaut pour le pourcentage de chauffage en mode sécurité. Mettre 0 pour éteindre le radiateur en mode sécurité",

View File

@@ -611,7 +611,7 @@ class UnderlyingClimate(UnderlyingEntity):
async def set_humidity(self, humidity: int):
"""Set new target humidity."""
_LOGGER.info("%s - Set fan mode: %s", self, humidity)
_LOGGER.info("%s - Set humidity: %s", self, humidity)
if not self.is_initialized:
return
data = {
@@ -627,7 +627,7 @@ class UnderlyingClimate(UnderlyingEntity):
async def set_swing_mode(self, swing_mode):
"""Set new target swing operation."""
_LOGGER.info("%s - Set fan mode: %s", self, swing_mode)
_LOGGER.info("%s - Set swing mode: %s", self, swing_mode)
if not self.is_initialized:
return
data = {

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

View File

@@ -60,6 +60,8 @@ Of course, your underlying equipment must have ventilation, and it must be contr
### Compensating for the Internal Temperature of the Underlying Equipment
Warning: This option must not be used with direct valve control regulation if a calibration entity has been provided.
Sometimes, the internal thermometer of the underlying equipment (TRV, air conditioner, etc.) is inaccurate to the point that self-regulation is insufficient. This happens when the internal thermometer is placed too close to the heat source. The internal temperature rises much faster than the room temperature, leading to regulation failures.
Example:
1. Room temperature is 18°, setpoint is 20°.
@@ -99,4 +101,4 @@ When this entity is 'On', all temperature or state changes made directly on the
Be careful, if you use this feature, your equipment is now controlled in two ways: _VTherm_ and directly by you. The commands might be contradictory, which could lead to confusion about the equipment's state. _VTherm_ is equipped with a delay mechanism that prevents loops: the user gives a setpoint, which is captured by _VTherm_ and changes the setpoint, ... This delay may cause the change made directly on the equipment to be ignored if these changes are too close together in time.
Some equipment (like Daikin, for example) changes state by itself. If the checkbox is checked, it may turn off the _VTherm_ when that's not what you intended.
That's why it's better not to use it. It generates a lot of confusion and many support requests.
That's why it's better not to use it. It generates a lot of confusion and many support requests.

View File

@@ -40,4 +40,5 @@ Some TRV type thermostats are known to be incompatible with Versatile Thermostat
5. TRVs like Aqara SRTS-A01 and MOES TV01-ZB, which lack the `hvac_action` state feedback to determine whether they are heating or not. Therefore, state feedback is inaccurate, but the rest seems functional.
6. Airwell air conditioners with the "Midea AC LAN" integration. If two VTherm commands are too close together, the air conditioner stops itself.
7. Climates based on the Overkiz integration do not work. It seems impossible to turn off or even change the temperature on these systems.
8. Heating systems based on Netatmo perform poorly. Netatmo schedules conflict with _VTherm_ programming. Netatmo devices constantly revert to `Auto` mode, which is poorly managed with _VTherm_. In this mode, _VTherm_ cannot determine whether the system is heating or cooling, making it impossible to select the correct algorithm. Some users have managed to make it work using a virtual switch between _VTherm_ and the underlying system, but stability is not guaranteed. An example is provided in the [troubleshooting](troubleshooting.md) section.

View File

@@ -31,8 +31,8 @@ This allows you to configure the valve control entities:
You need to provide:
1. As many valve opening control entities as there are underlying devices, and in the same order. These parameters are mandatory.
2. As many temperature calibration entities as there are underlying devices, and in the same order. These parameters are optional; they must either all be provided or none.
3. As many valve closure control entities as there are underlying devices, and in the same order. These parameters are optional; they must either all be provided or none.
4. A list of minimum opening values for the valve when it needs to be opened. This field is a list of integers. If the valve needs to be opened, it will be opened at a minimum of this opening value. This allows enough water to pass through when it needs to be opened.
3. As many valve closure control entities as there are underlying devices, and in the same order. These parameters are optional; they must either all be provided or none. For Sonoff TRVZB, you should not configure this entity. See the note below.
4. A list of minimum opening values for the valve when it needs to be opened. This field is a list of integers. If the valve needs to be opened, it will be opened at a minimum of this opening value, else it will be set to 0 (to ensure the valve is closed). This allows enough water to pass through when it needs to be opened.
The opening rate calculation algorithm is based on the _TPI_ algorithm described [here](algorithms.md). This is the same algorithm used for _VTherms_ `over_switch` and `over_valve`.
@@ -152,4 +152,4 @@ To apply the changes, you must either **restart Home Assistant completely** or j
## Summary of the Auto-Regulation Algorithm
A summary of the auto-regulation algorithm is described [here](algorithms.md#the-auto-regulation-algorithm-without-valve-control)
A summary of the auto-regulation algorithm is described [here](algorithms.md#the-auto-regulation-algorithm-without-valve-control)

View File

@@ -4,6 +4,7 @@
- [Troubleshooting](#troubleshooting)
- [Using a Heatzy](#using-a-heatzy)
- [Using a radiator with a pilot wire (Nodon SIN-4-FP-21)](#using-a-radiator-with-a-pilot-wire-nodon-sin-4-fp-21)
- [Using a Netatmo System](#using-a-netatmo-system)
- [Only the first radiator heats](#only-the-first-radiator-heats)
- [The radiator heats even though the setpoint temperature is exceeded, or it does not heat when the room temperature is well below the setpoint](#the-radiator-heats-even-though-the-setpoint-temperature-is-exceeded-or-it-does-not-heat-when-the-room-temperature-is-well-below-the-setpoint)
- [Type `over_switch` or `over_valve`](#type-over_switch-or-over_valve)
@@ -16,6 +17,7 @@
- [Using a Group of People as a Presence Sensor](#using-a-group-of-people-as-a-presence-sensor)
- [Enable Logs for the Versatile Thermostat](#enable-logs-for-the-versatile-thermostat)
- [VTherm does not track setpoint changes made directly on the underlying device (`over_climate`)](#vtherm-does-not-track-setpoint-changes-made-directly-on-the-underlying-device-over_climate)
- [VTherm Automatically Switches to 'Cooling' or 'Heating' Mode](#vtherm-automatically-switches-to-cooling-or-heating-mode)
## Using a Heatzy
@@ -85,6 +87,17 @@ Example:
Another more complex example is [here](https://github.com/jmcollin78/versatile_thermostat/discussions/431#discussioncomment-11393065)
## Using a Netatmo System
The system based on Netatmo TRVs does not work well with _VTherm_. You can find a discussion about the specific behavior of Netatmo systems (in French) here: [https://forum.hacf.fr/t/vannes-netatmo-et-vtherm/56063](https://forum.hacf.fr/t/vannes-netatmo-et-vtherm/56063).
However, some users have successfully integrated _VTherm_ with Netatmo by incorporating a virtual switch between _VTherm_ and the Netatmo `climate` entity, as follows:
```
TODO
```
## Only the first radiator heats
In `over_switch` mode, if multiple radiators are configured for the same VTherm, the heating will be triggered sequentially to smooth out the consumption peaks as much as possible.
@@ -213,4 +226,10 @@ Be careful, in debug mode, Versatile Thermostat is very verbose and can quickly
## VTherm does not track setpoint changes made directly on the underlying device (`over_climate`)
See the details of this feature [here](over-climate.md#track-underlying-temperature-changes).
See the details of this feature [here](over-climate.md#track-underlying-temperature-changes).
## VTherm Automatically Switches to 'Cooling' or 'Heating' Mode
Some reversible heat pumps have modes that allow the heat pump to decide whether to heat or cool. These modes are labeled as 'Auto' or 'Heat_cool' depending on the brand. These two modes should not be used with _VTherm_ because _VTherm_'s algorithms require explicit knowledge of whether the system is in heating or cooling mode, which these modes do not provide.
You should only use the following modes: `Heat`, `Cool`, `Off`, or optionally `Fan` (although `Fan` has no practical purpose with _VTherm_).

View File

@@ -5,6 +5,7 @@
- [Composant Scheduler !](#composant-scheduler-)
- [Courbes de régulattion avec Plotly](#courbes-de-régulattion-avec-plotly)
- [Les notification avec l'AappDaemon NOTIFIER](#les-notification-avec-laappdaemon-notifier)
- [Une très belle carte (merci @Jeffodilo)](#une-très-belle-carte-merci-jeffodilo)
## Versatile Thermostat UI Card
@@ -213,4 +214,275 @@ action:
mode: queued
max: 30
```
</details>
## Une très belle carte (merci @Jeffodilo)
@Jeffodilo a réalisé et partagé le code d'une très belle carte adaptée au TRV :
![carte Jeffodilo](images/card-trv-jeffodilo.png)
Cette carte nutilise pas card_mod, elle utilise les cartes custom assez courante suivantes
On garde une partie de linterface UI, sauf pour lhorizontale de la 2ème ligne
- custom:vertical-stack-in-card
- custom:stack-in-card
- custom:mini-graph-card
- custom:mushroom-template-card
- custom:button-card
L'original est ici (En Français): [forum HACF](https://forum.hacf.fr/t/carte-mise-en-forme-vanne-avec-thermostant-versatile/56132)
Evidemment, vous devez l'adapter à votre code.
Le code:
<details>
```yaml
type: vertical-stack
cards:
- type: heading
icon: mdi:bed-double
heading: Parents
heading_style: title
- type: custom:vertical-stack-in-card
cards:
- type: custom:mini-graph-card
entities:
- entity: sensor.sonde_parents_temperature
name: Température
state_adaptive_color: true
- entity: climate.valve_parents
name: Temp
attribute: current_temperature
unit: °C
state_adaptive_color: true
show_graph: false
show_state: true
hour24: true
hours_to_show: 24
points_per_hour: 2
font_size: 50
show:
name: false
icon: false
legend: false
labels: true
extrema: false
color_thresholds:
- color: "#33ccff"
value: 19
- color: "#00ffff"
value: 19.5
- color: "#33ffcc"
value: 20
- color: "#00ff99"
value: 20.5
- color: "#ffff99"
value: 21
- color: "#ffff33"
value: 21.5
- color: "#ff9933"
value: 22
- color: "#cc6633"
value: 24
- color: "#ff6000"
value: 26
- type: custom:stack-in-card
mode: horizontal
cards:
- type: custom:mushroom-template-card
secondary: ""
layout: horizontal
tap_action:
action: more-info
entity: sensor.sonde_parents_temperature
fill_container: false
multiline_secondary: false
primary: >-
{% if is_state_attr('climate.versatile_parents','hvac_action',
'idle') %}
🗜️ {{ states('number.valve_parents_valve_opening_degree', with_unit=True,)}} |🔋{{ states('sensor.valve_parents_battery') }} % | Inactif
{% elif is_state_attr('climate.versatile_parents','hvac_action',
'heating') %}
🗜️ {{ states('number.valve_parents_valve_opening_degree', with_unit=True,)}} |🔋{{ states('sensor.valve_parents_battery') }} % | Chauffe
{% else %} 🗜️ {{
states('number.valve_parents_valve_opening_degree',
with_unit=True,)}} | 🔋{{ states('sensor.valve_parents_battery')
}} % | Off {% endif %}
icon: ""
- type: horizontal-stack
cards:
- type: custom:button-card
name: Conf.
entity: climate.versatile_parents
show_state: false
show_icon: true
show_name: false
icon: mdi:fire
size: 80%
styles:
icon:
- color: |
[[[
if (states['climate.versatile_parents']) {
if (states['climate.versatile_parents'].attributes.preset_mode == 'comfort')
return 'darkorange';
else
return 'white'; }
]]]
name:
- color: white
- font-size: 60%
card:
- height: 40px
- width: 30px
tap_action:
action: perform-action
perform_action: climate.set_preset_mode
target:
entity_id:
- climate.versatile_parents
data:
preset_mode: comfort
- type: custom:button-card
name: Eco
entity: climate.versatile_parents
show_state: false
show_icon: true
show_name: false
icon: mdi:leaf
size: 80%
styles:
icon:
- color: |
[[[
if (states['climate.versatile_parents']) {
if (states['climate.versatile_parents'].attributes.preset_mode == 'eco')
return 'lightgreen';
else
return 'white'; }
]]]
name:
- color: white
- font-size: 60%
card:
- height: 40px
- width: 30px
tap_action:
action: perform-action
perform_action: climate.set_preset_mode
target:
entity_id:
- climate.versatile_parents
data:
preset_mode: eco
- type: custom:button-card
name: Manu
entity: climate.versatile_parents
show_state: false
show_icon: true
show_name: false
icon: mdi:hand-back-left
size: 80%
styles:
icon:
- color: |
[[[
if (states['climate.versatile_parents']) {
if (states['climate.versatile_parents'].attributes.preset_mode == 'none')
return 'indianred';
else
return 'white'; }
]]]
name:
- color: white
- font-size: 60%
card:
- height: 40px
- width: 30px
tap_action:
action: perform-action
perform_action: climate.set_preset_mode
target:
entity_id:
- climate.versatile_parents
data:
preset_mode: none
- type: custom:button-card
name: Abs.
entity: climate.versatile_parents
show_state: false
show_icon: true
show_name: false
icon: mdi:snowflake
size: 80%
styles:
icon:
- color: |
[[[
if (states['climate.versatile_parents']) {
if (states['climate.versatile_parents'].attributes.preset_mode == 'frost')
return 'skyblue';
else
return 'white'; }
]]]
name:
- color: white
- font-size: 60%
card:
- height: 40px
- width: 30px
tap_action:
action: perform-action
perform_action: climate.set_preset_mode
target:
entity_id:
- climate.versatile_parents
data:
preset_mode: frost
- type: custom:button-card
name: Boost
entity: climate.versatile_parents
show_state: false
show_icon: true
show_name: false
icon: mdi:rocket-launch
size: 80%
styles:
icon:
- color: |
[[[
if (states['climate.versatile_parents']) {
if (states['climate.versatile_parents'].attributes.preset_mode == 'boost')
return 'red';
else
return 'white'; }
]]]
name:
- color: white
- font-size: 60%
card:
- height: 40px
- width: 30px
tap_action:
action: perform-action
perform_action: climate.set_preset_mode
target:
entity_id:
- climate.versatile_parents
data:
preset_mode: boost
- type: custom:mushroom-climate-card
entity: climate.versatile_parents
show_temperature_control: true
hvac_modes: []
tap_action:
action: more-info
primary_info: state
icon: mdi:radiator
secondary_info: last-updated
fill_container: false
layout: horizontal
```
</details>

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

View File

@@ -63,6 +63,8 @@ Une fois l'écart de température redevenu faible, la ventilation se mettra dans
### Compenser la température interne du sous-jacent
Attention : cette option ne doit pas être utilisée avec une régulation par contrôle direct de la vanne si une entité de calibrage a été fournie.
Quelque fois, il arrive que le thermomètre interne du sous-jacent (TRV, climatisation, ...) soit tellement faux que l' auto-régulation ne suffise pas à réguler.
Cela arrive lorsque le thermomètre interne est trop près de la source de chaleur. La température interne monte alors beaucoup plus vite que la température de la pièce, ce qui génère des défauts dans la régulation.
Exemple :
@@ -103,4 +105,4 @@ Lorsque cette entité est 'On', tous les changements de température ou d'état
Attention, si vous utilisez cette fonction, votre équipement est maintenant contrôlé par 2 moyens : _VTherm_ et par vous même directement. Les ordres peuvent être contradictoires et cela peut induire une incompréhension sur l'état de l'équipement. _VTherm_ est équipé d'un mécanisme de temporisation qui évite les boucles : l'utilisateur donne une consigne qui est captée par _VTherm_ qui change la consigne, ... Cette temporisation peut faire que le changement fait directement sur l'équipement est ignoré si ces changements sont trop rapprochés dans le temps.
Certains équipements (Daikin par exemple), changent d'état tout seul. Si la case est cochée, cela peut éteindre le _VTherm_ alors que ce n'est pas ce que vous souhaitiez.
C'est pour ça qu'il est préférable de ne pas l'utiliser. Cela génère beaucoup d'incompréhensions et de nombreuses demandes de support.
C'est pour ça qu'il est préférable de ne pas l'utiliser. Cela génère beaucoup d'incompréhensions et de nombreuses demandes de support.

View File

@@ -33,11 +33,12 @@ Toutes ces fonctions sont configurables de façon centralisée ou individuelle e
## Incompatibilités
Certains thermostat de type TRV sont réputés incompatibles avec le Versatile Thermostat. C'est le cas des vannes suivantes :
1. les vannes POPP de Danfoss avec retour de température. Il est impossible d'éteindre cette vanne et elle s'auto-régule d'elle-même causant des conflits avec le VTherm,
1. les vannes POPP de Danfoss avec retour de température. Il est impossible d'éteindre cette vanne et elle s'auto-régule d'elle-même causant des conflits avec le _VTherm_,
2. Les thermostats « Homematic » (et éventuellement Homematic IP) sont connus pour rencontrer des problèmes avec le Versatile Thermostat en raison des limitations du protocole RF sous-jacent. Ce problème se produit particulièrement lorsque vous essayez de contrôler plusieurs thermostats Homematic à la fois dans une seule instance de VTherm. Afin de réduire la charge du cycle de service, vous pouvez par ex. regroupez les thermostats avec des procédures spécifiques à Homematic (par exemple en utilisant un thermostat mural) et laissez Versatile Thermostat contrôler uniquement le thermostat mural directement. Une autre option consiste à contrôler un seul thermostat et à propager les changements de mode CVC et de température par un automatisme,
3. les thermostats de type Heatzy qui ne supportent pas les commandes de type set_temperature
4. les thermostats de type Rointe ont tendance a se réveiller tout seul. Le reste fonctionne normalement.
5. les TRV de type Aqara SRTS-A01 et MOES TV01-ZB qui n'ont pas le retour d'état `hvac_action` permettant de savoir si elle chauffe ou pas. Donc les retours d'état sont faussés, le reste à l'air fonctionnel.
6. La clim Airwell avec l'intégration "Midea AC LAN". Si 2 commandes de VTherm sont trop rapprochées, la clim s'arrête d'elle même.
7. Les climates basés sur l'intégration Overkiz ne fonctionnent pas. Il parait impossible d'éteindre ni même de changer la température sur ces systèmes.
8. Les systèmes de chauffage basés sur Netatmo fonctionnent mal. Les plannings Netatmo viennent en concurrence de la programmation _VTherm_. Les sous-jacents Netatmo repasse en mode `Auto` tout le temps et ce mode est très mal géré avec _VTherm_ qui ne peut pas savoir si le sysème chauffe ou refroidit et donc quel algorithme choisir. Certains ont quand même réussi à le faire fonctionner avec un switch virtuel entre le _VTherm_ et le sous-jacent mais sans garantie de stabilité. Un exemple est donné dans la section [dépannage](troubleshooting.md)

View File

@@ -32,8 +32,8 @@ Elle permet de configurer les entités de contrôle de la vanne :
Vous devez donner :
1. autant d'entités de contrôle d'ouverture de la vanne qu'il y a de sous-jacents et dans le même odre. Ces paramètres sont obligatoires,
2. autant d'entités de calibrage du décalage de température qu'il y a de sous-jacents et dans le même ordre. Ces paramètres sont facultatifs ; ils doivent être tous founis ou aucun,
3. autant d'entités de de contrôile du taux de fermture qu'il y a de sous-jacents et dans le même ordre. Ces paramètres sont facultatifs ; ils doivent être tous founis ou aucun,
4. une liste de valeurs minimales d'ouverture de la vanne lorsqu'elle doit être ouverte. Ce champ est une liste d'entier. Si la vanne doit être ouverte, elle le sera au minimum avec cette valeur d'ouverture. Cela permet de laisser passer suffisamment d'eau lorsqu'elle doit être ouverte.
3. autant d'entités de de contrôile du taux de fermture qu'il y a de sous-jacents et dans le même ordre. Ces paramètres sont facultatifs ; ils doivent être tous founis ou aucun. Pour les Sonoff TRVZB, ils ne doivent pas être fournis,
4. une liste de valeurs minimales d'ouverture de la vanne lorsqu'elle doit être ouverte. Ce champ est une liste d'entier. Si la vanne doit être ouverte, elle le sera au minimum avec cette valeur d'ouverture, sinon elle sera totalement close (0). Cela permet de laisser passer suffisamment d'eau lorsqu'elle doit être ouverte mais garde la fermeeture complète si il n'y a pas besoin de chauffer.
L'algorithme de calcul du taux d'ouverture est basé sur le _TPI_ qui est décrit [ici](algorithms.md). C'est le même algorithme qui est utilisé pour les _VTherm_ `over_switch` et `over_valve`.
@@ -152,4 +152,4 @@ Pour que les modifications soient prises en compte, il faut soit **relancer tota
## Synthèse de l'algorithme d'auto-régulation
Une synthèse de l'algorithme d'auto-régulation est décrite [ici](algorithms.md#lalgorithme-dauto-régulation-sans-contrôle-de-la-vanne)
Une synthèse de l'algorithme d'auto-régulation est décrite [ici](algorithms.md#lalgorithme-dauto-régulation-sans-contrôle-de-la-vanne)

View File

@@ -4,6 +4,7 @@
- [Dépannages](#dépannages)
- [Utilisation d'un Heatzy](#utilisation-dun-heatzy)
- [Utilisation d'un radiateur avec un fil pilote (Nodon SIN-4-FP-21)](#utilisation-dun-radiateur-avec-un-fil-pilote-nodon-sin-4-fp-21)
- [Utilisation d'un système Netatmo](#utilisation-dun-système-netatmo)
- [Seul le premier radiateur chauffe](#seul-le-premier-radiateur-chauffe)
- [Le radiateur chauffe alors que la température de consigne est dépassée ou ne chauffe pas alors que la température de la pièce est bien en-dessous de la consigne](#le-radiateur-chauffe-alors-que-la-température-de-consigne-est-dépassée-ou-ne-chauffe-pas-alors-que-la-température-de-la-pièce-est-bien-en-dessous-de-la-consigne)
- [Type `over_switch` ou `over_valve`](#type-over_switch-ou-over_valve)
@@ -84,6 +85,15 @@ Exemple :
```
Un exemple plus complet est [ici](https://github.com/jmcollin78/versatile_thermostat/discussions/431#discussioncomment-11393065)
## Utilisation d'un système Netatmo
Le système à base de TRV Netatmo fonctionne mal avec _VTherm_. Vous avez ici une discussion sur le fonctionnement particulier des systèmes Netatmo (en Français) : https://forum.hacf.fr/t/vannes-netatmo-et-vtherm/56063
Cependant certains ont réussi une intégration _VTerm_ Netatmo en intégrant un switch virtuel entre _VTherm_ et le `climate` Netatmo suivant :
```
TODO add virtual switch code
```
## Seul le premier radiateur chauffe
En mode `over_switch` si plusieurs radiateurs sont configurés pour un même VTherm, l'alllumage va se faire de façon séquentiel pour lisser au plus possible les pics de consommation.
@@ -210,4 +220,9 @@ Attention, en mode debug Versatile Thermostat est très verbeux et peut vite ral
## VTherm ne suit pas les changements de consigne faits directement depuis le sous-jacents (`over_climate`)
Voir le détail de cette fonction [ici](over-climate.md#suivre-les-changements-de-température-du-sous-jacent).
Voir le détail de cette fonction [ici](over-climate.md#suivre-les-changements-de-température-du-sous-jacent).
## VTherm passe tout seul en mode 'clim' ou en mode 'chauffage'
Certaine _PAC_ réversibles ont des modes qui permettent de laisser le choix à la _PAC_ de chauffer ou de réfroidir. Ces modes sont 'Auto' or 'Heat_cool' selon les marques. Ces 2 modes ne doivent pas être utilisés avec _VTherm_ car les algorithmes de _VTherm_ ont besoin de savoir si ils sont en mode chauffe ou refroidissement ce que ne permettent pas ces modes.
Vous devez donc utiliser uniquement les modes : `Heat`, `Cool`, `Off` ou `Fan` éventuellement (bien que fan n'a aucun sens avec _Vtherm)

View File

@@ -87,7 +87,6 @@ FULL_SWITCH_CONFIG = (
| MOCK_TH_OVER_SWITCH_CENTRAL_MAIN_CONFIG
| MOCK_TH_OVER_SWITCH_TYPE_CONFIG
| MOCK_TH_OVER_SWITCH_TPI_CONFIG
# | MOCK_PRESETS_CONFIG
| MOCK_FULL_FEATURES
| MOCK_WINDOW_CONFIG
| MOCK_MOTION_CONFIG
@@ -111,12 +110,7 @@ FULL_SWITCH_AC_CONFIG = (
)
PARTIAL_CLIMATE_CONFIG = (
MOCK_TH_OVER_CLIMATE_USER_CONFIG
| MOCK_TH_OVER_CLIMATE_MAIN_CONFIG
| MOCK_TH_OVER_CLIMATE_CENTRAL_MAIN_CONFIG
| MOCK_TH_OVER_CLIMATE_TYPE_CONFIG
# | MOCK_PRESETS_CONFIG
| MOCK_ADVANCED_CONFIG
MOCK_TH_OVER_CLIMATE_USER_CONFIG | MOCK_TH_OVER_CLIMATE_MAIN_CONFIG | MOCK_TH_OVER_CLIMATE_CENTRAL_MAIN_CONFIG | MOCK_TH_OVER_CLIMATE_TYPE_CONFIG | MOCK_ADVANCED_CONFIG
)
PARTIAL_CLIMATE_CONFIG_USE_DEVICE_TEMP = (
@@ -124,7 +118,6 @@ PARTIAL_CLIMATE_CONFIG_USE_DEVICE_TEMP = (
| MOCK_TH_OVER_CLIMATE_MAIN_CONFIG
| MOCK_TH_OVER_CLIMATE_CENTRAL_MAIN_CONFIG
| MOCK_TH_OVER_CLIMATE_TYPE_USE_DEVICE_TEMP_CONFIG
# | MOCK_PRESETS_CONFIG
| MOCK_ADVANCED_CONFIG
)
@@ -133,24 +126,17 @@ PARTIAL_CLIMATE_NOT_REGULATED_CONFIG = (
| MOCK_TH_OVER_CLIMATE_MAIN_CONFIG
| MOCK_TH_OVER_CLIMATE_CENTRAL_MAIN_CONFIG
| MOCK_TH_OVER_CLIMATE_TYPE_NOT_REGULATED_CONFIG
# | MOCK_PRESETS_CONFIG
| MOCK_ADVANCED_CONFIG
)
PARTIAL_CLIMATE_AC_CONFIG = (
MOCK_TH_OVER_CLIMATE_USER_CONFIG
| MOCK_TH_OVER_CLIMATE_TYPE_AC_CONFIG
| MOCK_TH_OVER_CLIMATE_MAIN_CONFIG
| MOCK_TH_OVER_CLIMATE_CENTRAL_MAIN_CONFIG
# | MOCK_PRESETS_CONFIG
| MOCK_ADVANCED_CONFIG
MOCK_TH_OVER_CLIMATE_USER_CONFIG | MOCK_TH_OVER_CLIMATE_TYPE_AC_CONFIG | MOCK_TH_OVER_CLIMATE_MAIN_CONFIG | MOCK_TH_OVER_CLIMATE_CENTRAL_MAIN_CONFIG | MOCK_ADVANCED_CONFIG
)
FULL_4SWITCH_CONFIG = (
MOCK_TH_OVER_4SWITCH_USER_CONFIG
| MOCK_TH_OVER_4SWITCH_TYPE_CONFIG
| MOCK_TH_OVER_SWITCH_TPI_CONFIG
# | MOCK_PRESETS_CONFIG
| MOCK_WINDOW_CONFIG
| MOCK_MOTION_CONFIG
| MOCK_POWER_CONFIG
@@ -1037,6 +1023,16 @@ default_temperatures_ac_away = {
"boost_ac_away": 23.1,
}
default_temperatures_ac = {
"frost": 7.0,
"eco": 17.0,
"comfort": 19.0,
"boost": 21.0,
"eco_ac": 27.0,
"comfort_ac": 25.0,
"boost_ac": 23.0,
}
default_temperatures_away = {
"frost": 7.0,
"eco": 17.0,

View File

@@ -1,7 +1,7 @@
# pylint: disable=wildcard-import, unused-wildcard-import, unused-argument, line-too-long, protected-access
""" Test the normal start of a Thermostat """
from unittest.mock import patch
from unittest.mock import patch, PropertyMock
from datetime import timedelta, datetime
from homeassistant.core import HomeAssistant
@@ -179,7 +179,7 @@ async def test_overpowering_binary_sensors(
)
# fmt:off
with patch("homeassistant.core.StateMachine.get", side_effect=side_effects.get_side_effects()), \
patch("custom_components.versatile_thermostat.thermostat_switch.ThermostatOverSwitch.is_device_active", return_value="True"):
patch("custom_components.versatile_thermostat.thermostat_switch.ThermostatOverSwitch.is_device_active", new_callable=PropertyMock, return_value=True):
# fmt: on
await send_power_change_event(entity, 150, now)
await send_max_power_change_event(entity, 100, now)

View File

@@ -1,5 +1,6 @@
# pylint: disable=protected-access, unused-argument, line-too-long
""" Test the Central Power management """
import asyncio
from unittest.mock import patch, AsyncMock, MagicMock, PropertyMock
from datetime import datetime, timedelta
import logging
@@ -10,6 +11,14 @@ from custom_components.versatile_thermostat.feature_power_manager import (
from custom_components.versatile_thermostat.central_feature_power_manager import (
CentralFeaturePowerManager,
)
from custom_components.versatile_thermostat.thermostat_switch import (
ThermostatOverSwitch,
)
from custom_components.versatile_thermostat.thermostat_climate import (
ThermostatOverClimate,
)
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
logging.getLogger().setLevel(logging.DEBUG)
@@ -700,3 +709,150 @@ async def test_central_power_manager_max_power_event(
assert central_power_manager.current_max_power == expected_power
assert mock_calculate_shedding.call_count == nb_call
async def test_central_power_manager_start_vtherm_power(hass: HomeAssistant, skip_hass_states_is_state, init_central_power_manager):
"""Tests the central power start VTherm power. The objective is to starts VTherm until the power max is exceeded"""
temps = {
"eco": 17,
"comfort": 18,
"boost": 19,
}
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverSwitchMockName",
unique_id="uniqueId",
data={
CONF_NAME: "TheOverSwitchMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
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: False,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: True,
CONF_USE_PRESENCE_FEATURE: False,
CONF_UNDERLYING_LIST: ["switch.mock_switch"],
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SAFETY_DELAY_MIN: 5,
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
CONF_DEVICE_POWER: 1000,
CONF_PRESET_POWER: 12,
},
)
entity: ThermostatOverSwitch = await create_thermostat(hass, entry, "climate.theoverswitchmockname", temps)
assert entity
now: datetime = NowClass.get_now(hass)
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
central_power_manager = VersatileThermostatAPI.get_vtherm_api().central_power_manager
assert central_power_manager
side_effects = SideEffects(
{
"sensor.the_power_sensor": State("sensor.the_power_sensor", 1000),
"sensor.the_max_power_sensor": State("sensor.the_max_power_sensor", 2100),
},
State("unknown.entity_id", "unknown"),
)
# 1. Make the heater heats
# fmt: off
with patch("homeassistant.core.StateMachine.get", side_effect=side_effects.get_side_effects()), \
patch("custom_components.versatile_thermostat.thermostat_switch.ThermostatOverSwitch.is_device_active", new_callable=PropertyMock, return_value=False):
# fmt: on
# make the heater heats
await send_power_change_event(entity, 1000, now)
await send_max_power_change_event(entity, 2100, now)
await send_temperature_change_event(entity, 15, now)
await send_ext_temperature_change_event(entity, 1, now)
await entity.async_set_preset_mode(PRESET_BOOST)
assert entity.preset_mode is PRESET_BOOST
assert entity.power_manager.overpowering_state is STATE_UNKNOWN
assert entity.target_temperature == 19
await hass.async_block_till_done()
await entity.async_set_hvac_mode(HVACMode.HEAT)
assert entity.hvac_mode is HVACMode.HEAT
await hass.async_block_till_done()
await asyncio.sleep(0.1)
# the power of Vtherm should have been added
assert central_power_manager.started_vtherm_total_power == 1000
# 2. Check that another heater cannot heat
entry2 = MockConfigEntry(
domain=DOMAIN,
title="TheOverClimateMockName2",
unique_id="uniqueId2",
data={
CONF_NAME: "TheOverClimateMockName2",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
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: False,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: True,
CONF_USE_PRESENCE_FEATURE: False,
CONF_UNDERLYING_LIST: ["switch.mock_climate"],
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SAFETY_DELAY_MIN: 5,
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
CONF_DEVICE_POWER: 150,
CONF_PRESET_POWER: 12,
},
)
entity2: ThermostatOverClimate = await create_thermostat(hass, entry2, "climate.theoverclimatemockname2", temps)
assert entity2
fake_underlying_climate = MockClimate(
hass=hass,
unique_id="mockUniqueId",
name="MockClimateName",
)
# fmt: off
with patch("homeassistant.core.StateMachine.get", side_effect=side_effects.get_side_effects()), \
patch("custom_components.versatile_thermostat.thermostat_switch.ThermostatOverSwitch.is_device_active", new_callable=PropertyMock, return_value=False), \
patch("custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",return_value=fake_underlying_climate):
# fmt: on
# make the heater heats
await entity2.async_set_preset_mode(PRESET_COMFORT)
assert entity2.preset_mode is PRESET_COMFORT
assert entity2.power_manager.overpowering_state is STATE_UNKNOWN
assert entity2.target_temperature == 18
await entity2.async_set_hvac_mode(HVACMode.HEAT)
assert entity2.hvac_mode is HVACMode.HEAT
await hass.async_block_till_done()
await asyncio.sleep(0.1)
# the power of Vtherm should have not been added (cause it has not started) and the entity2 should be shedding
assert central_power_manager.started_vtherm_total_power == 1000
assert entity2.power_manager.overpowering_state is STATE_ON
# 3. sends a new power sensor event
await send_max_power_change_event(entity, 2150, now)
# No change
assert central_power_manager.started_vtherm_total_power == 1000
await send_power_change_event(entity, 1010, now)
assert central_power_manager.started_vtherm_total_power == 0

View File

@@ -287,6 +287,7 @@ async def test_user_config_flow_over_switch(
CONF_USE_POWER_FEATURE: True,
CONF_USE_PRESENCE_FEATURE: True,
CONF_USE_CENTRAL_BOILER_FEATURE: False,
CONF_AUTO_START_STOP_LEVEL: AUTO_START_STOP_LEVEL_NONE,
}
)
assert result["result"]
@@ -492,9 +493,7 @@ async def test_user_config_flow_over_climate(
)
assert result["type"] == FlowResultType.CREATE_ENTRY
assert result.get("errors") is None
assert result[
"data"
] == MOCK_TH_OVER_CLIMATE_USER_CONFIG | MOCK_TH_OVER_CLIMATE_MAIN_CONFIG | MOCK_TH_OVER_CLIMATE_CENTRAL_MAIN_CONFIG | MOCK_TH_OVER_CLIMATE_TYPE_CONFIG | {
assert result["data"] == MOCK_TH_OVER_CLIMATE_USER_CONFIG | MOCK_TH_OVER_CLIMATE_MAIN_CONFIG | MOCK_TH_OVER_CLIMATE_CENTRAL_MAIN_CONFIG | MOCK_TH_OVER_CLIMATE_TYPE_CONFIG | {
CONF_MINIMAL_ACTIVATION_DELAY: 10,
CONF_SAFETY_DELAY_MIN: 5,
CONF_SAFETY_MIN_ON_PERCENT: 0.4,
@@ -517,6 +516,7 @@ async def test_user_config_flow_over_climate(
CONF_USED_BY_CENTRAL_BOILER: False,
CONF_USE_CENTRAL_MODE: False,
CONF_AUTO_REGULATION_MODE: CONF_AUTO_REGULATION_STRONG,
CONF_AUTO_START_STOP_LEVEL: AUTO_START_STOP_LEVEL_NONE,
}
assert result["result"]
assert result["result"].domain == DOMAIN
@@ -1378,6 +1378,7 @@ async def test_user_config_flow_over_switch_bug_552_tpi(
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False,
CONF_USE_CENTRAL_BOILER_FEATURE: False,
CONF_AUTO_START_STOP_LEVEL: AUTO_START_STOP_LEVEL_NONE,
}
)
assert result["result"]
@@ -1681,9 +1682,7 @@ async def test_user_config_flow_over_climate_valve(
)
assert result["type"] == FlowResultType.CREATE_ENTRY
assert result.get("errors") is None
assert result[
"data"
] == MOCK_TH_OVER_CLIMATE_USER_CONFIG | MOCK_TH_OVER_CLIMATE_MAIN_CONFIG | MOCK_TH_OVER_CLIMATE_CENTRAL_MAIN_CONFIG | MOCK_TH_OVER_CLIMATE_TYPE_CONFIG | {
assert result["data"] == MOCK_TH_OVER_CLIMATE_USER_CONFIG | MOCK_TH_OVER_CLIMATE_MAIN_CONFIG | MOCK_TH_OVER_CLIMATE_CENTRAL_MAIN_CONFIG | MOCK_TH_OVER_CLIMATE_TYPE_CONFIG | {
CONF_MINIMAL_ACTIVATION_DELAY: 10,
CONF_SAFETY_DELAY_MIN: 5,
CONF_SAFETY_MIN_ON_PERCENT: 0.4,
@@ -1717,6 +1716,7 @@ async def test_user_config_flow_over_climate_valve(
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.1,
CONF_MIN_OPENING_DEGREES: "10, 20,0",
CONF_AUTO_START_STOP_LEVEL: AUTO_START_STOP_LEVEL_NONE,
}
assert result["result"]
assert result["result"].domain == DOMAIN

View File

@@ -347,22 +347,17 @@ async def test_motion_management_time_not_enough(
assert entity.presence_state == STATE_ON
# 2. starts detecting motion with time not enough
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
) as mock_heater_on, patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=False,
), patch(
"homeassistant.helpers.condition.state", return_value=False
) as mock_condition, patch(
"homeassistant.core.StateMachine.get",
return_value=State(
entity_id="binary_sensor.mock_motion_sensor", state=STATE_OFF
),
return_value=State(entity_id="binary_sensor.mock_motion_sensor", state=STATE_OFF),
):
event_timestamp = now - timedelta(minutes=4)
try_condition = await send_motion_change_event(
@@ -387,14 +382,11 @@ async def test_motion_management_time_not_enough(
assert mock_send_event.call_count == 0
# starts detecting motion with time enough this time
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
) as mock_heater_on, patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=False,
), patch(
"homeassistant.helpers.condition.state", return_value=True
@@ -415,22 +407,17 @@ async def test_motion_management_time_not_enough(
assert entity.presence_state == STATE_ON
# stop detecting motion with off delay too low
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
) as mock_heater_on, patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=True,
) as mock_device_active, patch(
"homeassistant.helpers.condition.state", return_value=False
) as mock_condition, patch(
"homeassistant.core.StateMachine.get",
return_value=State(
entity_id="binary_sensor.mock_motion_sensor", state=STATE_OFF
),
return_value=State(entity_id="binary_sensor.mock_motion_sensor", state=STATE_OFF),
):
event_timestamp = now - timedelta(minutes=2)
try_condition = await send_motion_change_event(
@@ -454,14 +441,11 @@ async def test_motion_management_time_not_enough(
assert mock_send_event.call_count == 0
# stop detecting motion with off delay enough long
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
) as mock_heater_on, patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=True,
) as mock_device_active, patch(
"homeassistant.helpers.condition.state", return_value=True
@@ -562,14 +546,11 @@ async def test_motion_management_time_enough_and_presence(
assert entity.presence_state == "on"
# starts detecting motion
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
) as mock_heater_on, patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=False,
), patch(
"homeassistant.helpers.condition.state", return_value=True
@@ -590,14 +571,11 @@ async def test_motion_management_time_enough_and_presence(
assert mock_send_event.call_count == 0
# stop detecting motion with confirmation of stop
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
) as mock_heater_on, patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=True,
), patch(
"homeassistant.helpers.condition.state", return_value=True
@@ -693,14 +671,11 @@ async def test_motion_management_time_enough_and_not_presence(
assert entity.presence_state == STATE_OFF
# starts detecting motion
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
) as mock_heater_on, patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=False,
), patch(
"homeassistant.helpers.condition.state", return_value=True
@@ -722,14 +697,11 @@ async def test_motion_management_time_enough_and_not_presence(
assert mock_send_event.call_count == 0
# stop detecting motion with confirmation of stop
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
) as mock_heater_on, patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=True,
), patch(
"homeassistant.helpers.condition.state", return_value=True
@@ -826,14 +798,11 @@ async def test_motion_management_with_stop_during_condition(
assert entity.presence_state == STATE_OFF
# starts detecting motion
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
) as mock_heater_on, patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=True,
), patch(
"homeassistant.helpers.condition.state", return_value=True
@@ -959,12 +928,11 @@ async def test_motion_management_with_stop_during_condition_last_state_on(
# 1. starts detecting motion but the sensor is off
with patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=True,
), patch("homeassistant.helpers.condition.state", return_value=False), patch(
"homeassistant.core.StateMachine.get",
return_value=State(
entity_id="binary_sensor.mock_motion_sensor", state=STATE_OFF
),
return_value=State(entity_id="binary_sensor.mock_motion_sensor", state=STATE_OFF),
):
event_timestamp = now - timedelta(minutes=5)
try_condition1 = await send_motion_change_event(
@@ -982,12 +950,11 @@ async def test_motion_management_with_stop_during_condition_last_state_on(
# 2. starts detecting motion but the sensor is on
with patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=True,
), patch("homeassistant.helpers.condition.state", return_value=False), patch(
"homeassistant.core.StateMachine.get",
return_value=State(
entity_id="binary_sensor.mock_motion_sensor", state=STATE_ON
),
return_value=State(entity_id="binary_sensor.mock_motion_sensor", state=STATE_ON),
):
event_timestamp = now - timedelta(minutes=5)
try_condition1 = await send_motion_change_event(

View File

@@ -2,7 +2,7 @@
""" Test the Multiple switch management """
import asyncio
from unittest.mock import patch, call, ANY
from unittest.mock import patch, call, ANY, PropertyMock
from datetime import datetime, timedelta
import logging
@@ -84,14 +84,11 @@ async def test_one_switch_cycle(
assert mock_is_state.call_count == 1
# Set temperature to a low level
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
) as mock_heater_on, patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=False,
) as mock_device_active, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.call_later",
@@ -107,7 +104,8 @@ async def test_one_switch_cycle(
# assert mock_heater_on.call_count == 1
assert mock_heater_on.call_count == 0
# There is no check if active
assert mock_device_active.call_count == 0
# don't work with PropertyMock
# assert mock_device_active.call_count == 0
# 4 calls dispatched along the cycle
assert mock_call_later.call_count == 1
@@ -119,14 +117,11 @@ async def test_one_switch_cycle(
# Set a temperature at middle level
event_timestamp = now - timedelta(minutes=4)
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
) as mock_heater_on, patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=False,
) as mock_device_active:
await send_temperature_change_event(entity, 18, event_timestamp)
@@ -141,14 +136,11 @@ async def test_one_switch_cycle(
# Set another temperature at middle level
event_timestamp = now - timedelta(minutes=3)
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
) as mock_heater_on, patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=True,
) as mock_device_active:
await send_temperature_change_event(entity, 18.1, event_timestamp)
@@ -176,14 +168,11 @@ async def test_one_switch_cycle(
# Simulate the end of heater on cycle
event_timestamp = now - timedelta(minutes=3)
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
) as mock_heater_on, patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=True,
) as mock_device_active:
await entity.underlying_entity(
@@ -201,14 +190,11 @@ async def test_one_switch_cycle(
# Simulate the start of heater on cycle
event_timestamp = now - timedelta(minutes=3)
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
) as mock_heater_on, patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=True,
) as mock_device_active:
await entity.underlying_entity(
@@ -306,14 +292,11 @@ async def test_multiple_switchs(
)
# Set temperature to a low level
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
) as mock_heater_on, patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=False,
) as mock_device_active, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.call_later",
@@ -329,7 +312,8 @@ async def test_multiple_switchs(
# assert mock_heater_on.call_count == 1
assert mock_heater_on.call_count == 0
# There is no check if active
assert mock_device_active.call_count == 0
# don't work with PropertyMock
# assert mock_device_active.call_count == 0
# 4 calls dispatched along the cycle
assert mock_call_later.call_count == 4
@@ -344,14 +328,11 @@ async def test_multiple_switchs(
# Set a temperature at middle level
event_timestamp = now - timedelta(minutes=4)
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
) as mock_heater_on, patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=False,
) as mock_device_active:
await send_temperature_change_event(entity, 18, event_timestamp)
@@ -818,7 +799,7 @@ async def test_multiple_switch_power_management(
with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, \
patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on") as mock_heater_on, \
patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off, \
patch("custom_components.versatile_thermostat.thermostat_switch.ThermostatOverSwitch.is_device_active", return_value="True"):
patch("custom_components.versatile_thermostat.thermostat_switch.ThermostatOverSwitch.is_device_active", new_callable=PropertyMock, return_value=True):
#fmt: on
now = now + timedelta(seconds=30)
VersatileThermostatAPI.get_vtherm_api()._set_now(now)

View File

@@ -1233,3 +1233,111 @@ async def test_manual_hvac_off_should_take_the_lead_over_auto_start_stop(
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
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_underlying_from_comes_back_to_life(
hass: HomeAssistant,
skip_hass_states_is_state,
skip_turn_on_off_heater,
skip_send_event,
):
"""Test that when a underlying climate comes back to life (from unkwown or unavailable) the last state is send"""
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverClimateMockName",
unique_id="uniqueId",
data={
CONF_NAME: "TheOverClimateMockName",
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: False,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_AUTO_START_STOP_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False,
CONF_UNDERLYING_LIST: ["climate.mock_climate"],
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SAFETY_DELAY_MIN: 5,
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
CONF_AUTO_FAN_MODE: CONF_AUTO_FAN_NONE,
CONF_AC_MODE: True,
}, # 5 minutes security delay
)
# Underlying is in HEAT mode but should be shutdown at startup
fake_underlying_climate = MockClimate(hass, "mockUniqueId", "MockClimateName", {}, HVACMode.COOL, HVACAction.COOLING)
# 1. initialize the vtherm in COOL with Boost
# fmt: off
with patch("custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",return_value=fake_underlying_climate) as mock_find_climate:
# fmt: on
entity = await create_thermostat(hass, entry, "climate.theoverclimatemockname", temps=default_temperatures_ac)
assert entity
assert entity.name == "TheOverClimateMockName"
assert entity.is_over_climate is True
# Set hvac_mode to COOL
await entity.async_set_hvac_mode(HVACMode.COOL)
await entity.async_set_preset_mode(PRESET_BOOST)
# it is very hot today
await send_temperature_change_event(entity, 27, now, False)
await send_ext_temperature_change_event(entity, 35, now, False)
await hass.async_block_till_done()
assert entity.hvac_mode is HVACMode.COOL
# because in MockClimate HVACAction is HEATING if hvac_mode is not set
assert entity.hvac_action is HVACAction.COOLING
assert entity.preset_mode is PRESET_BOOST
assert entity.target_temperature == 23
# 2. send under state event comes back from life
# fmt: off
with patch("custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode") as mock_underlying_set_hvac_mode, \
patch("custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_temperature") as mock_underlying_set_temperature:
# fmt: on
now = now + timedelta(minutes=2)
# 2. Change the target temp of underlying thermostat at now -> the event will be disgarded because to fast (to avoid loop cf issue 121)
await send_climate_change_event_with_temperature(
entity,
HVACMode.HEAT,
STATE_UNKNOWN,
HVACAction.OFF,
STATE_UNKNOWN,
now,
entity.min_temp + 1,
True,
"climate.mock_climate", # the underlying climate entity id
)
assert mock_underlying_set_hvac_mode.call_count == 1
mock_underlying_set_hvac_mode.assert_has_calls(
[
call.set_hvac_mode(HVACMode.COOL),
]
)
assert mock_underlying_set_temperature.call_count == 1
mock_underlying_set_temperature.assert_has_calls(
[
call.set_temperature(23, 30, 15),
]
)
# Nothing should have changed
assert entity.target_temperature == 23
assert entity.preset_mode is PRESET_BOOST
assert entity.hvac_mode is HVACMode.COOL

View File

@@ -523,7 +523,7 @@ async def test_power_management_hvac_on(
patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, \
patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on") as mock_heater_on, \
patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off, \
patch("custom_components.versatile_thermostat.thermostat_switch.ThermostatOverSwitch.is_device_active", return_value="True"):
patch("custom_components.versatile_thermostat.thermostat_switch.ThermostatOverSwitch.is_device_active", new_callable=PropertyMock, return_value=True):
# fmt: on
now = now + timedelta(seconds=30)
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
@@ -913,12 +913,15 @@ async def test_power_management_turn_off_while_shedding(hass: HomeAssistant, ski
# 1. Set VTherm to overpowering
# Send power max mesurement too low and HVACMode is on and device is active
#
#
# fmt:off
with patch("homeassistant.core.StateMachine.get", side_effect=side_effects.get_side_effects()), \
patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"), \
patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on") as mock_heater_on, \
patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off, \
patch("custom_components.versatile_thermostat.thermostat_switch.ThermostatOverSwitch.is_device_active", return_value="True"):
patch("custom_components.versatile_thermostat.thermostat_switch.ThermostatOverSwitch.is_device_active", new_callable=PropertyMock, return_value=True):
# fmt: on
now = now + timedelta(seconds=30)
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
@@ -939,7 +942,7 @@ async def test_power_management_turn_off_while_shedding(hass: HomeAssistant, ski
patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, \
patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on") as mock_heater_on, \
patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off, \
patch("custom_components.versatile_thermostat.thermostat_switch.ThermostatOverSwitch.is_device_active", return_value="True"):
patch("custom_components.versatile_thermostat.thermostat_switch.ThermostatOverSwitch.is_device_active", new_callable=PropertyMock, return_value=True):
# fmt: on
now = now + timedelta(seconds=30)
VersatileThermostatAPI.get_vtherm_api()._set_now(now)

View File

@@ -2039,16 +2039,13 @@ async def test_bug_66(
assert entity.window_state is STATE_UNKNOWN
# Open the window and let the thermostat shut down
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
) as mock_heater_on, patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off, patch(
"homeassistant.helpers.condition.state", return_value=True
) as mock_condition, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=True,
):
await send_temperature_change_event(entity, 15, now)
@@ -2067,16 +2064,13 @@ async def test_bug_66(
assert entity.window_state == STATE_ON
# Close the window but too shortly
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
) as mock_heater_on, patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off, patch(
"homeassistant.helpers.condition.state", return_value=False
) as mock_condition, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=False,
):
event_timestamp = now + timedelta(minutes=1)
@@ -2090,16 +2084,13 @@ async def test_bug_66(
assert entity.window_state == STATE_ON
# Reopen immediatly with sufficient time
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
) as mock_heater_on, patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off, patch(
"homeassistant.helpers.condition.state", return_value=True
) as mock_condition, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=False,
):
try_window_condition = await send_window_change_event(
@@ -2113,16 +2104,13 @@ async def test_bug_66(
assert entity.hvac_mode == HVACMode.OFF
# Close the window but with sufficient time this time
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
) as mock_heater_on, patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off, patch(
"homeassistant.helpers.condition.state", return_value=True
) as mock_condition, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=False,
):
event_timestamp = now + timedelta(minutes=2)