Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
96076bf7c2 | ||
|
|
a3f7043f45 | ||
|
|
67c01b02ec | ||
|
|
ab1c6892df | ||
|
|
84c8ac4f59 | ||
|
|
faab9648a7 | ||
|
|
a30ad38a53 | ||
|
|
c0b186b8c1 | ||
|
|
01e761aecd | ||
|
|
55a99054fa | ||
|
|
2c5078cd7f | ||
|
|
82348adef2 | ||
|
|
71aad211c6 | ||
|
|
a40f976fd1 |
@@ -85,7 +85,7 @@ Ce composant personnalisé pour Home Assistant est une mise à niveau et est une
|
||||
La puissance de l'appareil doit maintenant être la puissance totale de tous les appareils controlée par le VTherm. Cela permet d'avoir des équipements hétérogènes de puissance différente. Dans le cas de plusieurs appareils contrôlés par un seul VTherm, vous devrez éditer et changer la valeur `device_power`. Vous devez configurer la puissance totale de tous les appareils.
|
||||
|
||||
# Merci pour la bière [buymecoffee](https://www.buymeacoffee.com/jmcollin78)
|
||||
Un grand merci à @salabur, @pvince83, @bergoglio, @EPicLURcher, @ecolorado66, @Kriss1670, @maia, @f.maymil, @moutte69, @Jerome pour les bières. Ca fait très plaisir et ça m'encourage à continuer !
|
||||
Un grand merci à @salabur, @pvince83, @bergoglio, @EPicLURcher, @ecolorado66, @Kriss1670, @maia, @f.maymil, @moutte69, @Jerome, @Gunnar M pour les bières. Ca fait très plaisir et ça m'encourage à continuer !
|
||||
|
||||
|
||||
# Quand l'utiliser et ne pas l'utiliser
|
||||
@@ -105,7 +105,9 @@ Les installations avec fil pilote et diode d'activation bénéficie d'une option
|
||||
## 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,
|
||||
2. les vannes thermstatiques "Homematic radio". Elles ont un cycle de service incompatible avec une commande par le Versatile Thermostat
|
||||
2. les vannes thermstatiques "Homematic radio". Elles ont un cycle de service incompatible avec une commande par le Versatile Thermostat,
|
||||
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.
|
||||
|
||||
# Pourquoi une nouvelle implémentation du thermostat ?
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@ This custom component for Home Assistant is an upgrade and is a complete rewrite
|
||||
The power of the device should now be the total power of all controler devices by the VTherm. This allow to have eterogeneous equipment with different power. In case of multi-devices controlled by a single VTherm you will have to edit and change the `device_power` value. Set the total power of all devices.
|
||||
|
||||
# Thanks for the beer [buymecoffee](https://www.buymeacoffee.com/jmcollin78)
|
||||
Many thanks to @salabur, @pvince83, @bergoglio, @EPicLURcher, @ecolorado66, @Kriss1670, @maia, @f.maymil, @moutte69, @Jerome for the beers. It's very nice and encourages me to continue!
|
||||
Many thanks to @salabur, @pvince83, @bergoglio, @EPicLURcher, @ecolorado66, @Kriss1670, @maia, @f.maymil, @moutte69, @Jerome, @Gunnar M for the beers. It's very nice and encourages me to continue!
|
||||
|
||||
# When to use / not use
|
||||
This thermostat can control 3 types of equipment:
|
||||
@@ -103,7 +103,9 @@ Installations with pilot wire and activation diode benefit from an option which
|
||||
|
||||
Some TRV type thermostats are known to be incompatible with the Versatile Thermostat. This is the case for the following valves:
|
||||
1. Danfoss POPP valves with temperature feedback. It is impossible to turn off this valve and it self-regulates, causing conflicts with the VTherm,
|
||||
2. “Homematic radio” thermostatic valves. They have a duty cycle incompatible with control by the Versatile Thermostat
|
||||
2. “Homematic radio” thermostatic valves. They have a duty cycle incompatible with control by the Versatile Thermostat,
|
||||
3. Thermostat of type Heatzy which doesn't supports the set_temperature command.
|
||||
4. Thermostats of type Rointe tends to awake alone even if VTherm turns it off. Others functions works fine.
|
||||
|
||||
# Why another thermostat implementation ?
|
||||
|
||||
|
||||
@@ -138,6 +138,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
ClimateEntity._entity_component_unrecorded_attributes.union(
|
||||
frozenset(
|
||||
{
|
||||
"is_on",
|
||||
"type",
|
||||
"eco_temp",
|
||||
"boost_temp",
|
||||
@@ -170,6 +171,9 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
"presence_sensor_entity_id",
|
||||
"power_sensor_entity_id",
|
||||
"max_power_sensor_entity_id",
|
||||
"temperature_unit",
|
||||
"is_device_active",
|
||||
"target_temperature_step",
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -294,6 +298,8 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
self._ext_temp_sensor_entity_id = entry_infos.get(CONF_EXTERNAL_TEMP_SENSOR)
|
||||
self._attr_max_temp = entry_infos.get(CONF_TEMP_MAX)
|
||||
self._attr_min_temp = entry_infos.get(CONF_TEMP_MIN)
|
||||
# Default value not configurable
|
||||
self._attr_target_temperature_step = 0.1
|
||||
self._power_sensor_entity_id = entry_infos.get(CONF_POWER_SENSOR)
|
||||
self._max_power_sensor_entity_id = entry_infos.get(CONF_MAX_POWER_SENSOR)
|
||||
self._window_sensor_entity_id = entry_infos.get(CONF_WINDOW_SENSOR)
|
||||
@@ -901,27 +907,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
"""Return the sensor temperature."""
|
||||
return self._cur_temp
|
||||
|
||||
@property
|
||||
def target_temperature_step(self) -> float | None:
|
||||
"""Return the supported step of target temperature."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def target_temperature_high(self) -> float | None:
|
||||
"""Return the highbound target temperature we try to reach.
|
||||
|
||||
Requires ClimateEntityFeature.TARGET_TEMPERATURE_RANGE.
|
||||
"""
|
||||
return None
|
||||
|
||||
@property
|
||||
def target_temperature_low(self) -> float | None:
|
||||
"""Return the lowbound target temperature we try to reach.
|
||||
|
||||
Requires ClimateEntityFeature.TARGET_TEMPERATURE_RANGE.
|
||||
"""
|
||||
return None
|
||||
|
||||
@property
|
||||
def is_aux_heat(self) -> bool | None:
|
||||
"""Return true if aux heater.
|
||||
@@ -1032,6 +1017,11 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
"""Returns the number of underlying entities"""
|
||||
return len(self._underlyings)
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""True if the VTherm is on (! HVAC_OFF)"""
|
||||
return self.hvac_mode and self.hvac_mode != HVACMode.OFF
|
||||
|
||||
def underlying_entity_id(self, index=0) -> str | None:
|
||||
"""The climate_entity_id. Added for retrocompatibility reason"""
|
||||
if index < self.nb_underlying_entities:
|
||||
@@ -1587,7 +1577,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
await self.async_control_heating(force=True)
|
||||
|
||||
async def _async_update_presence(self, new_state):
|
||||
_LOGGER.debug("%s - Updating presence. New state is %s", self, new_state)
|
||||
_LOGGER.info("%s - Updating presence. New state is %s", self, new_state)
|
||||
self._presence_state = new_state
|
||||
if self._attr_preset_mode in HIDDEN_PRESETS or self._presence_on is False:
|
||||
_LOGGER.info(
|
||||
@@ -1605,24 +1595,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
if self._attr_preset_mode not in [PRESET_BOOST, PRESET_COMFORT, PRESET_ECO]:
|
||||
return
|
||||
|
||||
# Change temperature with preset named _away
|
||||
# new_temp = None
|
||||
# if new_state == STATE_ON or new_state == STATE_HOME:
|
||||
# new_temp = self._presets[self._attr_preset_mode]
|
||||
# _LOGGER.info(
|
||||
# "%s - Someone is back home. Restoring temperature to %.2f",
|
||||
# self,
|
||||
# new_temp,
|
||||
# )
|
||||
# else:
|
||||
# new_temp = self._presets_away[
|
||||
# self.get_preset_away_name(self._attr_preset_mode)
|
||||
# ]
|
||||
# _LOGGER.info(
|
||||
# "%s - No one is at home. Apply temperature %.2f",
|
||||
# self,
|
||||
# new_temp,
|
||||
# )
|
||||
new_temp = self.find_preset_temp(self.preset_mode)
|
||||
if new_temp is not None:
|
||||
_LOGGER.debug(
|
||||
@@ -1715,8 +1687,8 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
and self.hvac_mode != HVACMode.OFF
|
||||
):
|
||||
if (
|
||||
not self.proportional_algorithm
|
||||
or self.proportional_algorithm.on_percent <= 0.0
|
||||
self.proportional_algorithm
|
||||
and self.proportional_algorithm.on_percent <= 0.0
|
||||
):
|
||||
_LOGGER.info(
|
||||
"%s - Start auto detection of open window slope=%.3f but no heating detected (on_percent<=0). Forget the event",
|
||||
@@ -1881,7 +1853,10 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
},
|
||||
)
|
||||
|
||||
self._overpowering_state = ret
|
||||
if self._overpowering_state != ret:
|
||||
self._overpowering_state = ret
|
||||
self.update_custom_attributes()
|
||||
|
||||
return self._overpowering_state
|
||||
|
||||
async def check_security(self) -> bool:
|
||||
@@ -2107,6 +2082,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
"""Update the custom extra attributes for the entity"""
|
||||
|
||||
self._attr_extra_state_attributes: dict(str, str) = {
|
||||
"is_on": self.is_on,
|
||||
"hvac_action": self.hvac_action,
|
||||
"hvac_mode": self.hvac_mode,
|
||||
"preset_mode": self.preset_mode,
|
||||
@@ -2126,6 +2102,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
"power_temp": self._power_temp,
|
||||
# Already in super class - "target_temp": self.target_temperature,
|
||||
# Already in super class - "current_temp": self._cur_temp,
|
||||
"target_temperature_step": self.target_temperature_step,
|
||||
"ext_current_temperature": self._cur_ext_temp,
|
||||
"ac_mode": self._ac_mode,
|
||||
"current_power": self._current_power,
|
||||
@@ -2166,6 +2143,8 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
"presence_sensor_entity_id": self._presence_sensor_entity_id,
|
||||
"power_sensor_entity_id": self._power_sensor_entity_id,
|
||||
"max_power_sensor_entity_id": self._max_power_sensor_entity_id,
|
||||
"temperature_unit": self.temperature_unit,
|
||||
"is_device_active": self.is_device_active,
|
||||
}
|
||||
|
||||
@callback
|
||||
|
||||
@@ -12,7 +12,7 @@ from datetime import datetime
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# To filter bad values
|
||||
MIN_DELTA_T_SEC = 10 # two temp mesure should be > 10 sec
|
||||
MIN_DELTA_T_SEC = 30 # two temp mesure should be > 10 sec
|
||||
MAX_SLOPE_VALUE = 2 # slope cannot be > 2 or < -2 -> else this is an aberrant point
|
||||
|
||||
|
||||
|
||||
@@ -5,8 +5,9 @@ import logging
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PITemperatureRegulator:
|
||||
""" A class implementing a PI Algorithm
|
||||
"""A class implementing a PI Algorithm
|
||||
PI algorithms calculate a target temperature by adding an offset which is calculating as follow:
|
||||
- offset = kp * error + ki * accumulated_error
|
||||
|
||||
@@ -16,30 +17,48 @@ class PITemperatureRegulator:
|
||||
- call set_target_temp when the target temperature change.
|
||||
"""
|
||||
|
||||
def __init__(self, target_temp: float, kp: float, ki: float, k_ext: float, offset_max: float, stabilization_threshold: float, accumulated_error_threshold: float):
|
||||
self.target_temp:float = target_temp
|
||||
self.kp:float = kp # proportionnel gain
|
||||
self.ki:float = ki # integral gain
|
||||
self.k_ext:float = k_ext # exterior gain
|
||||
self.offset_max:float = offset_max
|
||||
self.stabilization_threshold:float = stabilization_threshold
|
||||
self.accumulated_error:float = 0
|
||||
self.accumulated_error_threshold:float = accumulated_error_threshold
|
||||
def __init__(
|
||||
self,
|
||||
target_temp: float,
|
||||
kp: float,
|
||||
ki: float,
|
||||
k_ext: float,
|
||||
offset_max: float,
|
||||
stabilization_threshold: float,
|
||||
accumulated_error_threshold: float,
|
||||
):
|
||||
self.target_temp: float = target_temp
|
||||
self.kp: float = kp # proportionnel gain
|
||||
self.ki: float = ki # integral gain
|
||||
self.k_ext: float = k_ext # exterior gain
|
||||
self.offset_max: float = offset_max
|
||||
self.stabilization_threshold: float = stabilization_threshold
|
||||
self.accumulated_error: float = 0
|
||||
self.accumulated_error_threshold: float = accumulated_error_threshold
|
||||
|
||||
def reset_accumulated_error(self):
|
||||
""" Reset the accumulated error """
|
||||
"""Reset the accumulated error"""
|
||||
self.accumulated_error = 0
|
||||
|
||||
def set_target_temp(self, target_temp):
|
||||
""" Set the new target_temp"""
|
||||
"""Set the new target_temp"""
|
||||
self.target_temp = target_temp
|
||||
# Do not reset the accumulated error
|
||||
# self.accumulated_error = 0
|
||||
|
||||
def calculate_regulated_temperature(self, internal_temp: float, external_temp:float): # pylint: disable=unused-argument
|
||||
""" Calculate a new target_temp given some temperature"""
|
||||
if internal_temp is None or external_temp is None:
|
||||
_LOGGER.warning("Internal_temp or external_temp are not set. Regulation will be suspended")
|
||||
def calculate_regulated_temperature(
|
||||
self, internal_temp: float, external_temp: float
|
||||
): # pylint: disable=unused-argument
|
||||
"""Calculate a new target_temp given some temperature"""
|
||||
if internal_temp is None:
|
||||
_LOGGER.warning(
|
||||
"Temporarily skipping the self-regulation algorithm while the configured sensor for room temperature is unavailable"
|
||||
)
|
||||
return self.target_temp
|
||||
if external_temp is None:
|
||||
_LOGGER.warning(
|
||||
"Temporarily skipping the self-regulation algorithm while the configured sensor for outdoor temperature is unavailable"
|
||||
)
|
||||
return self.target_temp
|
||||
|
||||
# Calculate the error factor (P)
|
||||
@@ -49,7 +68,10 @@ class PITemperatureRegulator:
|
||||
self.accumulated_error += error
|
||||
|
||||
# Capping of the error
|
||||
self.accumulated_error = min(self.accumulated_error_threshold, max(-self.accumulated_error_threshold, self.accumulated_error))
|
||||
self.accumulated_error = min(
|
||||
self.accumulated_error_threshold,
|
||||
max(-self.accumulated_error_threshold, self.accumulated_error),
|
||||
)
|
||||
|
||||
# Calculate the offset (proportionnel + intégral)
|
||||
offset = self.kp * error + self.ki * self.accumulated_error
|
||||
@@ -62,7 +84,6 @@ class PITemperatureRegulator:
|
||||
total_offset = offset + offset_ext
|
||||
total_offset = min(self.offset_max, max(-self.offset_max, total_offset))
|
||||
|
||||
|
||||
# If temperature is near the target_temp, reset the accumulated_error
|
||||
if abs(error) < self.stabilization_threshold:
|
||||
_LOGGER.debug("Stabilisation")
|
||||
@@ -70,7 +91,14 @@ class PITemperatureRegulator:
|
||||
|
||||
result = round(self.target_temp + total_offset, 1)
|
||||
|
||||
_LOGGER.debug("PITemperatureRegulator - Error: %.2f accumulated_error: %.2f offset: %.2f offset_ext: %.2f target_tem: %.1f regulatedTemp: %.1f",
|
||||
error, self.accumulated_error, offset, offset_ext, self.target_temp, result)
|
||||
_LOGGER.debug(
|
||||
"PITemperatureRegulator - Error: %.2f accumulated_error: %.2f offset: %.2f offset_ext: %.2f target_tem: %.1f regulatedTemp: %.1f",
|
||||
error,
|
||||
self.accumulated_error,
|
||||
offset,
|
||||
offset_ext,
|
||||
self.target_temp,
|
||||
result,
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
@@ -11,7 +11,7 @@ from homeassistant.components.sensor import (
|
||||
SensorEntity,
|
||||
SensorDeviceClass,
|
||||
SensorStateClass,
|
||||
UnitOfTemperature
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
|
||||
@@ -54,7 +54,10 @@ async def async_setup_entry(
|
||||
]
|
||||
if entry.data.get(CONF_DEVICE_POWER):
|
||||
entities.append(EnergySensor(hass, unique_id, name, entry.data))
|
||||
if entry.data.get(CONF_THERMOSTAT_TYPE) in [CONF_THERMOSTAT_SWITCH, CONF_THERMOSTAT_VALVE]:
|
||||
if entry.data.get(CONF_THERMOSTAT_TYPE) in [
|
||||
CONF_THERMOSTAT_SWITCH,
|
||||
CONF_THERMOSTAT_VALVE,
|
||||
]:
|
||||
entities.append(MeanPowerSensor(hass, unique_id, name, entry.data))
|
||||
|
||||
if entry.data.get(CONF_PROP_FUNCTION) == PROPORTIONAL_FUNCTION_TPI:
|
||||
@@ -202,6 +205,9 @@ class OnPercentSensor(VersatileThermostatBaseEntity, SensorEntity):
|
||||
if self.my_climate and self.my_climate.proportional_algorithm
|
||||
else None
|
||||
)
|
||||
if on_percent is None:
|
||||
return
|
||||
|
||||
if math.isnan(on_percent) or math.isinf(on_percent):
|
||||
raise ValueError(f"Sensor has illegal state {on_percent}")
|
||||
|
||||
@@ -234,6 +240,7 @@ class OnPercentSensor(VersatileThermostatBaseEntity, SensorEntity):
|
||||
"""Return the suggested number of decimal digits for display."""
|
||||
return 1
|
||||
|
||||
|
||||
class ValveOpenPercentSensor(VersatileThermostatBaseEntity, SensorEntity):
|
||||
"""Representation of a on percent sensor which exposes the on_percent in a cycle"""
|
||||
|
||||
@@ -295,6 +302,10 @@ class OnTimeSensor(VersatileThermostatBaseEntity, SensorEntity):
|
||||
if self.my_climate and self.my_climate.proportional_algorithm
|
||||
else None
|
||||
)
|
||||
|
||||
if on_time is None:
|
||||
return
|
||||
|
||||
if math.isnan(on_time) or math.isinf(on_time):
|
||||
raise ValueError(f"Sensor has illegal state {on_time}")
|
||||
|
||||
@@ -340,6 +351,9 @@ class OffTimeSensor(VersatileThermostatBaseEntity, SensorEntity):
|
||||
if self.my_climate and self.my_climate.proportional_algorithm
|
||||
else None
|
||||
)
|
||||
if off_time is None:
|
||||
return
|
||||
|
||||
if math.isnan(off_time) or math.isinf(off_time):
|
||||
raise ValueError(f"Sensor has illegal state {off_time}")
|
||||
|
||||
@@ -476,6 +490,7 @@ class TemperatureSlopeSensor(VersatileThermostatBaseEntity, SensorEntity):
|
||||
"""Return the suggested number of decimal digits for display."""
|
||||
return 2
|
||||
|
||||
|
||||
class RegulatedTemperatureSensor(VersatileThermostatBaseEntity, SensorEntity):
|
||||
"""Representation of a Energy sensor which exposes the energy"""
|
||||
|
||||
@@ -493,7 +508,9 @@ class RegulatedTemperatureSensor(VersatileThermostatBaseEntity, SensorEntity):
|
||||
if math.isnan(self.my_climate.regulated_target_temp) or math.isinf(
|
||||
self.my_climate.regulated_target_temp
|
||||
):
|
||||
raise ValueError(f"Sensor has illegal state {self.my_climate.regulated_target_temp}")
|
||||
raise ValueError(
|
||||
f"Sensor has illegal state {self.my_climate.regulated_target_temp}"
|
||||
)
|
||||
|
||||
old_state = self._attr_native_value
|
||||
self._attr_native_value = round(
|
||||
|
||||
@@ -4,7 +4,10 @@ import logging
|
||||
from datetime import timedelta, datetime
|
||||
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.event import async_track_state_change_event, async_track_time_interval
|
||||
from homeassistant.helpers.event import (
|
||||
async_track_state_change_event,
|
||||
async_track_time_interval,
|
||||
)
|
||||
|
||||
from homeassistant.components.climate import HVACAction, HVACMode
|
||||
|
||||
@@ -29,27 +32,40 @@ from .const import (
|
||||
RegulationParamSlow,
|
||||
RegulationParamLight,
|
||||
RegulationParamMedium,
|
||||
RegulationParamStrong
|
||||
RegulationParamStrong,
|
||||
)
|
||||
|
||||
from .underlyings import UnderlyingClimate
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ThermostatOverClimate(BaseThermostat):
|
||||
"""Representation of a base class for a Versatile Thermostat over a climate"""
|
||||
_auto_regulation_mode:str = None
|
||||
|
||||
_auto_regulation_mode: str = None
|
||||
_regulation_algo = None
|
||||
_regulated_target_temp: float = None
|
||||
_auto_regulation_dtemp: float = None
|
||||
_auto_regulation_period_min: int = None
|
||||
_last_regulation_change: datetime = None
|
||||
|
||||
_entity_component_unrecorded_attributes = BaseThermostat._entity_component_unrecorded_attributes.union(frozenset(
|
||||
{
|
||||
"is_over_climate", "start_hvac_action_date", "underlying_climate_0", "underlying_climate_1",
|
||||
"underlying_climate_2", "underlying_climate_3", "regulation_accumulated_error"
|
||||
}))
|
||||
_entity_component_unrecorded_attributes = (
|
||||
BaseThermostat._entity_component_unrecorded_attributes.union(
|
||||
frozenset(
|
||||
{
|
||||
"is_over_climate",
|
||||
"start_hvac_action_date",
|
||||
"underlying_climate_0",
|
||||
"underlying_climate_1",
|
||||
"underlying_climate_2",
|
||||
"underlying_climate_3",
|
||||
"regulation_accumulated_error",
|
||||
"auto_regulation_mode",
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
|
||||
"""Initialize the thermostat over switch."""
|
||||
@@ -60,12 +76,12 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
|
||||
@property
|
||||
def is_over_climate(self) -> bool:
|
||||
""" True if the Thermostat is over_climate"""
|
||||
"""True if the Thermostat is over_climate"""
|
||||
return True
|
||||
|
||||
@property
|
||||
def hvac_action(self) -> HVACAction | None:
|
||||
""" Returns the current hvac_action by checking all hvac_action of the underlyings """
|
||||
"""Returns the current hvac_action by checking all hvac_action of the underlyings"""
|
||||
|
||||
# if one not IDLE or OFF -> return it
|
||||
# else if one IDLE -> IDLE
|
||||
@@ -92,29 +108,53 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
await self._send_regulated_temperature(force=True)
|
||||
|
||||
async def _send_regulated_temperature(self, force=False):
|
||||
""" Sends the regulated temperature to all underlying """
|
||||
"""Sends the regulated temperature to all underlying"""
|
||||
_LOGGER.info(
|
||||
"%s - Calling ThermostatClimate._send_regulated_temperature force=%s",
|
||||
self,
|
||||
force,
|
||||
)
|
||||
|
||||
now: datetime = NowClass.get_now(self._hass)
|
||||
period = float((now - self._last_regulation_change).total_seconds()) / 60.0
|
||||
if not force and period < self._auto_regulation_period_min:
|
||||
_LOGGER.info(
|
||||
"%s - period (%.1f) min is < %.0f min -> forget the regulation send",
|
||||
self,
|
||||
period,
|
||||
self._auto_regulation_period_min,
|
||||
)
|
||||
return
|
||||
|
||||
if not self._regulated_target_temp:
|
||||
self._regulated_target_temp = self.target_temperature
|
||||
|
||||
_LOGGER.info("%s - regulation calculation will be done", self)
|
||||
self._last_regulation_change = now
|
||||
|
||||
new_regulated_temp = round_to_nearest(
|
||||
self._regulation_algo.calculate_regulated_temperature(self.current_temperature, self._cur_ext_temp),
|
||||
self._auto_regulation_dtemp)
|
||||
self._regulation_algo.calculate_regulated_temperature(
|
||||
self.current_temperature, self._cur_ext_temp
|
||||
),
|
||||
self._auto_regulation_dtemp,
|
||||
)
|
||||
dtemp = new_regulated_temp - self._regulated_target_temp
|
||||
|
||||
if not force and abs(dtemp) < self._auto_regulation_dtemp:
|
||||
_LOGGER.debug("%s - dtemp (%.1f) is < %.1f -> forget the regulation send", self, dtemp, self._auto_regulation_dtemp)
|
||||
_LOGGER.info(
|
||||
"%s - dtemp (%.1f) is < %.1f -> forget the regulation send",
|
||||
self,
|
||||
dtemp,
|
||||
self._auto_regulation_dtemp,
|
||||
)
|
||||
return
|
||||
|
||||
now:datetime = NowClass.get_now(self._hass)
|
||||
period = float((now - self._last_regulation_change).total_seconds()) / 60.
|
||||
if not force and period < self._auto_regulation_period_min:
|
||||
_LOGGER.debug("%s - period (%.1f) is < %.0f -> forget the regulation send", self, period, self._auto_regulation_period_min)
|
||||
return
|
||||
|
||||
|
||||
self._regulated_target_temp = new_regulated_temp
|
||||
_LOGGER.info("%s - Regulated temp have changed to %.1f. Resend it to underlyings", self, new_regulated_temp)
|
||||
self._last_regulation_change = now
|
||||
_LOGGER.info(
|
||||
"%s - Regulated temp have changed to %.1f. Resend it to underlyings",
|
||||
self,
|
||||
new_regulated_temp,
|
||||
)
|
||||
|
||||
for under in self._underlyings:
|
||||
await under.set_temperature(
|
||||
@@ -123,7 +163,7 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
|
||||
@overrides
|
||||
def post_init(self, entry_infos):
|
||||
""" Initialize the Thermostat"""
|
||||
"""Initialize the Thermostat"""
|
||||
|
||||
super().post_init(entry_infos)
|
||||
for climate in [
|
||||
@@ -142,14 +182,24 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
)
|
||||
|
||||
self.choose_auto_regulation_mode(
|
||||
entry_infos.get(CONF_AUTO_REGULATION_MODE) if entry_infos.get(CONF_AUTO_REGULATION_MODE) is not None else CONF_AUTO_REGULATION_NONE
|
||||
entry_infos.get(CONF_AUTO_REGULATION_MODE)
|
||||
if entry_infos.get(CONF_AUTO_REGULATION_MODE) is not None
|
||||
else CONF_AUTO_REGULATION_NONE
|
||||
)
|
||||
|
||||
self._auto_regulation_dtemp = entry_infos.get(CONF_AUTO_REGULATION_DTEMP) if entry_infos.get(CONF_AUTO_REGULATION_DTEMP) is not None else 0.5
|
||||
self._auto_regulation_period_min = entry_infos.get(CONF_AUTO_REGULATION_PERIOD_MIN) if entry_infos.get(CONF_AUTO_REGULATION_PERIOD_MIN) is not None else 5
|
||||
self._auto_regulation_dtemp = (
|
||||
entry_infos.get(CONF_AUTO_REGULATION_DTEMP)
|
||||
if entry_infos.get(CONF_AUTO_REGULATION_DTEMP) is not None
|
||||
else 0.5
|
||||
)
|
||||
self._auto_regulation_period_min = (
|
||||
entry_infos.get(CONF_AUTO_REGULATION_PERIOD_MIN)
|
||||
if entry_infos.get(CONF_AUTO_REGULATION_PERIOD_MIN) is not None
|
||||
else 5
|
||||
)
|
||||
|
||||
def choose_auto_regulation_mode(self, auto_regulation_mode):
|
||||
""" Choose or change the regulation mode"""
|
||||
"""Choose or change the regulation mode"""
|
||||
self._auto_regulation_mode = auto_regulation_mode
|
||||
if self._auto_regulation_mode == CONF_AUTO_REGULATION_LIGHT:
|
||||
self._regulation_algo = PITemperatureRegulator(
|
||||
@@ -159,7 +209,8 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
RegulationParamLight.k_ext,
|
||||
RegulationParamLight.offset_max,
|
||||
RegulationParamLight.stabilization_threshold,
|
||||
RegulationParamLight.accumulated_error_threshold)
|
||||
RegulationParamLight.accumulated_error_threshold,
|
||||
)
|
||||
elif self._auto_regulation_mode == CONF_AUTO_REGULATION_MEDIUM:
|
||||
self._regulation_algo = PITemperatureRegulator(
|
||||
self.target_temperature,
|
||||
@@ -168,7 +219,8 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
RegulationParamMedium.k_ext,
|
||||
RegulationParamMedium.offset_max,
|
||||
RegulationParamMedium.stabilization_threshold,
|
||||
RegulationParamMedium.accumulated_error_threshold)
|
||||
RegulationParamMedium.accumulated_error_threshold,
|
||||
)
|
||||
elif self._auto_regulation_mode == CONF_AUTO_REGULATION_STRONG:
|
||||
self._regulation_algo = PITemperatureRegulator(
|
||||
self.target_temperature,
|
||||
@@ -177,7 +229,8 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
RegulationParamStrong.k_ext,
|
||||
RegulationParamStrong.offset_max,
|
||||
RegulationParamStrong.stabilization_threshold,
|
||||
RegulationParamStrong.accumulated_error_threshold)
|
||||
RegulationParamStrong.accumulated_error_threshold,
|
||||
)
|
||||
elif self._auto_regulation_mode == CONF_AUTO_REGULATION_SLOW:
|
||||
self._regulation_algo = PITemperatureRegulator(
|
||||
self.target_temperature,
|
||||
@@ -186,11 +239,13 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
RegulationParamSlow.k_ext,
|
||||
RegulationParamSlow.offset_max,
|
||||
RegulationParamSlow.stabilization_threshold,
|
||||
RegulationParamSlow.accumulated_error_threshold)
|
||||
RegulationParamSlow.accumulated_error_threshold,
|
||||
)
|
||||
else:
|
||||
# A default empty algo (which does nothing)
|
||||
self._regulation_algo = PITemperatureRegulator(
|
||||
self.target_temperature, 0, 0, 0, 0, 0.1, 0)
|
||||
self.target_temperature, 0, 0, 0, 0, 0.1, 0
|
||||
)
|
||||
|
||||
@overrides
|
||||
async def async_added_to_hass(self):
|
||||
@@ -219,27 +274,37 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
|
||||
@overrides
|
||||
def update_custom_attributes(self):
|
||||
""" Custom attributes """
|
||||
"""Custom attributes"""
|
||||
super().update_custom_attributes()
|
||||
|
||||
self._attr_extra_state_attributes["is_over_climate"] = self.is_over_climate
|
||||
self._attr_extra_state_attributes["start_hvac_action_date"] = (
|
||||
self._underlying_climate_start_hvac_action_date)
|
||||
self._attr_extra_state_attributes["underlying_climate_0"] = (
|
||||
self._underlyings[0].entity_id)
|
||||
self._attr_extra_state_attributes[
|
||||
"start_hvac_action_date"
|
||||
] = self._underlying_climate_start_hvac_action_date
|
||||
self._attr_extra_state_attributes["underlying_climate_0"] = self._underlyings[
|
||||
0
|
||||
].entity_id
|
||||
self._attr_extra_state_attributes["underlying_climate_1"] = (
|
||||
self._underlyings[1].entity_id if len(self._underlyings) > 1 else None
|
||||
)
|
||||
self._underlyings[1].entity_id if len(self._underlyings) > 1 else None
|
||||
)
|
||||
self._attr_extra_state_attributes["underlying_climate_2"] = (
|
||||
self._underlyings[2].entity_id if len(self._underlyings) > 2 else None
|
||||
)
|
||||
self._underlyings[2].entity_id if len(self._underlyings) > 2 else None
|
||||
)
|
||||
self._attr_extra_state_attributes["underlying_climate_3"] = (
|
||||
self._underlyings[3].entity_id if len(self._underlyings) > 3 else None
|
||||
)
|
||||
self._underlyings[3].entity_id if len(self._underlyings) > 3 else None
|
||||
)
|
||||
|
||||
if self.is_regulated:
|
||||
self._attr_extra_state_attributes["regulated_target_temperature"] = self._regulated_target_temp
|
||||
self._attr_extra_state_attributes["regulation_accumulated_error"] = self._regulation_algo.accumulated_error
|
||||
self._attr_extra_state_attributes["is_regulated"] = self.is_regulated
|
||||
self._attr_extra_state_attributes[
|
||||
"regulated_target_temperature"
|
||||
] = self._regulated_target_temp
|
||||
self._attr_extra_state_attributes[
|
||||
"auto_regulation_mode"
|
||||
] = self.auto_regulation_mode
|
||||
self._attr_extra_state_attributes[
|
||||
"regulation_accumulated_error"
|
||||
] = self._regulation_algo.accumulated_error
|
||||
|
||||
self.async_write_ha_state()
|
||||
_LOGGER.debug(
|
||||
@@ -473,17 +538,17 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
|
||||
@property
|
||||
def auto_regulation_mode(self):
|
||||
""" Get the regulation mode """
|
||||
"""Get the regulation mode"""
|
||||
return self._auto_regulation_mode
|
||||
|
||||
@property
|
||||
def regulated_target_temp(self):
|
||||
""" Get the regulated target temperature """
|
||||
"""Get the regulated target temperature"""
|
||||
return self._regulated_target_temp
|
||||
|
||||
@property
|
||||
def is_regulated(self):
|
||||
""" Check if the ThermostatOverClimate is regulated """
|
||||
"""Check if the ThermostatOverClimate is regulated"""
|
||||
return self.auto_regulation_mode != CONF_AUTO_REGULATION_NONE
|
||||
|
||||
@property
|
||||
@@ -668,7 +733,11 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
target:
|
||||
entity_id: climate.thermostat_1
|
||||
"""
|
||||
_LOGGER.info("%s - Calling service_set_auto_regulation_mode, auto_regulation_mode: %s", self, auto_regulation_mode)
|
||||
_LOGGER.info(
|
||||
"%s - Calling service_set_auto_regulation_mode, auto_regulation_mode: %s",
|
||||
self,
|
||||
auto_regulation_mode,
|
||||
)
|
||||
if auto_regulation_mode == "None":
|
||||
self.choose_auto_regulation_mode(CONF_AUTO_REGULATION_NONE)
|
||||
elif auto_regulation_mode == "Light":
|
||||
|
||||
@@ -12,7 +12,7 @@ from .const import (
|
||||
CONF_HEATER_3,
|
||||
CONF_HEATER_4,
|
||||
CONF_INVERSE_SWITCH,
|
||||
overrides
|
||||
overrides,
|
||||
)
|
||||
|
||||
from .base_thermostat import BaseThermostat
|
||||
@@ -21,15 +21,31 @@ from .prop_algorithm import PropAlgorithm
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ThermostatOverSwitch(BaseThermostat):
|
||||
"""Representation of a base class for a Versatile Thermostat over a switch."""
|
||||
|
||||
_entity_component_unrecorded_attributes = BaseThermostat._entity_component_unrecorded_attributes.union(frozenset(
|
||||
{
|
||||
"is_over_switch", "underlying_switch_0", "underlying_switch_1",
|
||||
"underlying_switch_2", "underlying_switch_3", "on_time_sec", "off_time_sec",
|
||||
"cycle_min", "function", "tpi_coef_int", "tpi_coef_ext"
|
||||
}))
|
||||
_entity_component_unrecorded_attributes = (
|
||||
BaseThermostat._entity_component_unrecorded_attributes.union(
|
||||
frozenset(
|
||||
{
|
||||
"is_over_switch",
|
||||
"is_inversed",
|
||||
"underlying_switch_0",
|
||||
"underlying_switch_1",
|
||||
"underlying_switch_2",
|
||||
"underlying_switch_3",
|
||||
"on_time_sec",
|
||||
"off_time_sec",
|
||||
"cycle_min",
|
||||
"function",
|
||||
"tpi_coef_int",
|
||||
"tpi_coef_ext",
|
||||
"power_percent",
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
# useless for now
|
||||
# def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
|
||||
@@ -39,17 +55,25 @@ class ThermostatOverSwitch(BaseThermostat):
|
||||
|
||||
@property
|
||||
def is_over_switch(self) -> bool:
|
||||
""" True if the Thermostat is over_switch"""
|
||||
"""True if the Thermostat is over_switch"""
|
||||
return True
|
||||
|
||||
@property
|
||||
def is_inversed(self) -> bool:
|
||||
""" True if the switch is inversed (for pilot wire and diode)"""
|
||||
"""True if the switch is inversed (for pilot wire and diode)"""
|
||||
return self._is_inversed is True
|
||||
|
||||
@property
|
||||
def power_percent(self) -> float | None:
|
||||
"""Get the current on_percent value"""
|
||||
if self._prop_algorithm:
|
||||
return round(self._prop_algorithm.on_percent * 100, 0)
|
||||
else:
|
||||
return None
|
||||
|
||||
@overrides
|
||||
def post_init(self, entry_infos):
|
||||
""" Initialize the Thermostat"""
|
||||
"""Initialize the Thermostat"""
|
||||
|
||||
super().post_init(entry_infos)
|
||||
|
||||
@@ -96,31 +120,34 @@ class ThermostatOverSwitch(BaseThermostat):
|
||||
async_track_state_change_event(
|
||||
self.hass, [switch.entity_id], self._async_switch_changed
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
self.hass.create_task(self.async_control_heating())
|
||||
|
||||
@overrides
|
||||
def update_custom_attributes(self):
|
||||
""" Custom attributes """
|
||||
"""Custom attributes"""
|
||||
super().update_custom_attributes()
|
||||
|
||||
self._attr_extra_state_attributes["is_over_switch"] = self.is_over_switch
|
||||
self._attr_extra_state_attributes["underlying_switch_0"] = (
|
||||
self._underlyings[0].entity_id)
|
||||
self._attr_extra_state_attributes["is_inversed"] = self.is_inversed
|
||||
self._attr_extra_state_attributes["underlying_switch_0"] = self._underlyings[
|
||||
0
|
||||
].entity_id
|
||||
self._attr_extra_state_attributes["underlying_switch_1"] = (
|
||||
self._underlyings[1].entity_id if len(self._underlyings) > 1 else None
|
||||
)
|
||||
self._underlyings[1].entity_id if len(self._underlyings) > 1 else None
|
||||
)
|
||||
self._attr_extra_state_attributes["underlying_switch_2"] = (
|
||||
self._underlyings[2].entity_id if len(self._underlyings) > 2 else None
|
||||
)
|
||||
self._underlyings[2].entity_id if len(self._underlyings) > 2 else None
|
||||
)
|
||||
self._attr_extra_state_attributes["underlying_switch_3"] = (
|
||||
self._underlyings[3].entity_id if len(self._underlyings) > 3 else None
|
||||
)
|
||||
self._underlyings[3].entity_id if len(self._underlyings) > 3 else None
|
||||
)
|
||||
|
||||
self._attr_extra_state_attributes[
|
||||
"on_percent"
|
||||
] = self._prop_algorithm.on_percent
|
||||
"on_percent"
|
||||
] = self._prop_algorithm.on_percent
|
||||
self._attr_extra_state_attributes["power_percent"] = self.power_percent
|
||||
self._attr_extra_state_attributes[
|
||||
"on_time_sec"
|
||||
] = self._prop_algorithm.on_time_sec
|
||||
@@ -182,3 +209,4 @@ class ThermostatOverSwitch(BaseThermostat):
|
||||
if old_state is None:
|
||||
self.hass.create_task(self._check_initial_state())
|
||||
self.async_write_ha_state()
|
||||
self.update_custom_attributes()
|
||||
|
||||
@@ -242,7 +242,7 @@ async def test_window_management_time_enough(
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_window_auto_fast(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
"""Test the Power management"""
|
||||
"""Test the Window management"""
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
@@ -430,11 +430,11 @@ async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_st
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOverSwitchMockName",
|
||||
title="TheOverClimateMockName",
|
||||
unique_id="uniqueId",
|
||||
data={
|
||||
CONF_NAME: "TheOverSwitchMockName",
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
|
||||
CONF_NAME: "TheOverClimateMockName",
|
||||
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,
|
||||
@@ -447,10 +447,7 @@ async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_st
|
||||
CONF_USE_MOTION_FEATURE: False,
|
||||
CONF_USE_POWER_FEATURE: False,
|
||||
CONF_USE_PRESENCE_FEATURE: False,
|
||||
CONF_HEATER: "switch.mock_switch",
|
||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||
CONF_TPI_COEF_INT: 0.3,
|
||||
CONF_TPI_COEF_EXT: 0.01,
|
||||
CONF_CLIMATE: "switch.mock_climate",
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
@@ -461,7 +458,7 @@ async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_st
|
||||
)
|
||||
|
||||
entity: BaseThermostat = await create_thermostat(
|
||||
hass, entry, "climate.theoverswitchmockname"
|
||||
hass, entry, "climate.theoverclimatemockname"
|
||||
)
|
||||
assert entity
|
||||
|
||||
@@ -469,7 +466,7 @@ async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_st
|
||||
now = datetime.now(tz)
|
||||
|
||||
tpi_algo = entity._prop_algorithm
|
||||
assert tpi_algo
|
||||
assert tpi_algo is None
|
||||
|
||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
await entity.async_set_preset_mode(PRESET_BOOST)
|
||||
@@ -484,18 +481,16 @@ async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_st
|
||||
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.underlyings.UnderlyingSwitch.is_device_active",
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode"
|
||||
) as mock_set_hvac_mode, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.is_device_active",
|
||||
return_value=True,
|
||||
):
|
||||
event_timestamp = now - timedelta(minutes=4)
|
||||
await send_temperature_change_event(entity, 19, event_timestamp)
|
||||
|
||||
# The heater turns on
|
||||
assert mock_heater_on.call_count == 1
|
||||
# The climate turns on but was alredy on
|
||||
assert mock_set_hvac_mode.call_count == 0
|
||||
assert entity.last_temperature_slope is None
|
||||
assert entity._window_auto_algo.is_window_open_detected() is False
|
||||
assert entity._window_auto_algo.is_window_close_detected() is False
|
||||
@@ -505,10 +500,8 @@ async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_st
|
||||
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.underlyings.UnderlyingClimate.set_hvac_mode"
|
||||
) as mock_set_hvac_mode, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=True,
|
||||
):
|
||||
@@ -531,8 +524,7 @@ async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_st
|
||||
],
|
||||
any_order=True,
|
||||
)
|
||||
assert mock_heater_on.call_count == 0
|
||||
assert mock_heater_off.call_count >= 1
|
||||
assert mock_set_hvac_mode.call_count >= 1
|
||||
assert entity.last_temperature_slope == -1
|
||||
assert entity._window_auto_algo.is_window_open_detected() is True
|
||||
assert entity._window_auto_algo.is_window_close_detected() is False
|
||||
@@ -543,17 +535,14 @@ async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_st
|
||||
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.underlyings.UnderlyingClimate.set_hvac_mode"
|
||||
) as mock_set_hvac_mode, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=False,
|
||||
):
|
||||
await asyncio.sleep(0.3)
|
||||
|
||||
assert mock_heater_on.call_count == 1
|
||||
assert mock_heater_off.call_count == 0
|
||||
assert mock_set_hvac_mode.call_count == 1
|
||||
assert round(entity.last_temperature_slope, 3) == -1
|
||||
# Because the algorithm is not aware of the expiration, for the algo we are still in alert
|
||||
assert entity._window_auto_algo.is_window_open_detected() is True
|
||||
@@ -674,12 +663,11 @@ async def test_window_auto_no_on_percent(
|
||||
# Clean the entity
|
||||
entity.remove_thermostat()
|
||||
|
||||
#PR - Adding Window Bypass
|
||||
|
||||
# PR - Adding Window Bypass
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_window_bypass(
|
||||
hass: HomeAssistant, skip_hass_states_is_state
|
||||
):
|
||||
async def test_window_bypass(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
"""Test the Window management when bypass enabled"""
|
||||
|
||||
entry = MockConfigEntry(
|
||||
@@ -810,7 +798,8 @@ async def test_window_bypass(
|
||||
# Clean the entity
|
||||
entity.remove_thermostat()
|
||||
|
||||
#PR - Adding Window bypass for window auto algorithm
|
||||
|
||||
# PR - Adding Window bypass for window auto algorithm
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_window_auto_bypass(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
@@ -921,7 +910,8 @@ async def test_window_auto_bypass(hass: HomeAssistant, skip_hass_states_is_state
|
||||
# Clean the entity
|
||||
entity.remove_thermostat()
|
||||
|
||||
#PR - Adding Window bypass AFTER detection have been done should reactivate the heater
|
||||
|
||||
# PR - Adding Window bypass AFTER detection have been done should reactivate the heater
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_window_bypass_reactivate(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
@@ -1049,4 +1039,4 @@ async def test_window_bypass_reactivate(hass: HomeAssistant, skip_hass_states_is
|
||||
)
|
||||
|
||||
# Clean the entity
|
||||
entity.remove_thermostat()
|
||||
entity.remove_thermostat()
|
||||
|
||||
Reference in New Issue
Block a user