Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a30ad38a53 | |||
| c0b186b8c1 | |||
| 01e761aecd | |||
| 55a99054fa | |||
| 2c5078cd7f | |||
| 82348adef2 | |||
| 71aad211c6 | |||
| a40f976fd1 | |||
| 382f6f99c6 | |||
| 95c4aa8ae9 | |||
| a6a47fde53 | |||
| e08f51b4f2 |
@@ -21,7 +21,9 @@
|
||||
"ms-python.python",
|
||||
"github.vscode-pull-request-github",
|
||||
"ryanluker.vscode-coverage-gutters",
|
||||
"ms-python.vscode-pylance"
|
||||
"ms-python.black-formatter",
|
||||
"ms-python.pylint",
|
||||
"ferrierbenjamin.fold-unfold-all-icone"
|
||||
],
|
||||
// "mounts": [
|
||||
// "source=${localWorkspaceFolder}/.devcontainer/configuration.yaml,target=${localWorkspaceFolder}/config/www/community/,type=bind,consistency=cached",
|
||||
@@ -40,8 +42,7 @@
|
||||
// "terminal.integrated.shell.linux": "/bin/bash",
|
||||
"python.pythonPath": "/usr/bin/python3",
|
||||
"python.analysis.autoSearchPaths": true,
|
||||
"python.linting.pylintEnabled": true,
|
||||
"python.linting.enabled": true,
|
||||
"pylint.lintOnChange": false,
|
||||
"python.formatting.provider": "black",
|
||||
"python.formatting.blackPath": "/usr/local/py-utils/bin/black",
|
||||
"editor.formatOnPaste": false,
|
||||
|
||||
Vendored
+3
-3
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"[python]": {
|
||||
"editor.defaultFormatter": "ms-python.python"
|
||||
"editor.defaultFormatter": "ms-python.black-formatter",
|
||||
"editor.formatOnSave": true
|
||||
},
|
||||
"python.linting.pylintEnabled": true,
|
||||
"python.linting.enabled": true,
|
||||
"pylint.lintOnChange": false,
|
||||
"files.associations": {
|
||||
"*.yaml": "home-assistant"
|
||||
},
|
||||
|
||||
+4
-3
@@ -51,7 +51,7 @@
|
||||
- [Attributs personnalisés](#attributs-personnalisés)
|
||||
- [Quelques résultats](#quelques-résultats)
|
||||
- [Encore mieux](#encore-mieux)
|
||||
- [Bien mieux avec le Veersatile Thermostat UI Card](#bien-mieux-avec-le-veersatile-thermostat-ui-card)
|
||||
- [Bien mieux avec le Versatile Thermostat UI Card](#bien-mieux-avec-le-versatile-thermostat-ui-card)
|
||||
- [Encore mieux avec le composant Scheduler !](#encore-mieux-avec-le-composant-scheduler-)
|
||||
- [Encore bien mieux avec la custom:simple-thermostat front integration](#encore-bien-mieux-avec-la-customsimple-thermostat-front-integration)
|
||||
- [Toujours mieux avec Apex-chart pour régler votre thermostat](#toujours-mieux-avec-apex-chart-pour-régler-votre-thermostat)
|
||||
@@ -105,7 +105,8 @@ 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
|
||||
|
||||
# Pourquoi une nouvelle implémentation du thermostat ?
|
||||
|
||||
@@ -735,7 +736,7 @@ Enjoy !
|
||||
|
||||
# Encore mieux
|
||||
|
||||
## Bien mieux avec le Veersatile Thermostat UI Card
|
||||
## Bien mieux avec le Versatile Thermostat UI Card
|
||||
Une carte spéciale pour le Versatile Thermostat a été développée (sur la base du Better Thermostat). Elle est dispo ici [Versatile Thermostat UI Card](https://github.com/jmcollin78/versatile-thermostat-ui-card) et propose une vision moderne de tous les status du VTherm :
|
||||
|
||||

|
||||
|
||||
@@ -103,7 +103,8 @@ 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.
|
||||
|
||||
# Why another thermostat implementation ?
|
||||
|
||||
|
||||
@@ -130,47 +130,53 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
_motion_state: bool
|
||||
_presence_state: bool
|
||||
_window_auto_state: bool
|
||||
#PR - Adding Window ByPass
|
||||
_window_bypass_state: bool
|
||||
_underlyings: list[UnderlyingEntity]
|
||||
_last_change_time: datetime
|
||||
|
||||
_entity_component_unrecorded_attributes = ClimateEntity._entity_component_unrecorded_attributes.union(frozenset(
|
||||
{
|
||||
"type",
|
||||
"eco_temp",
|
||||
"boost_temp",
|
||||
"comfort_temp",
|
||||
"eco_away_temp",
|
||||
"boost_away_temp",
|
||||
"comfort_away_temp",
|
||||
"power_temp",
|
||||
"ac_mode",
|
||||
"current_power_max",
|
||||
"saved_preset_mode",
|
||||
"saved_target_temp",
|
||||
"saved_hvac_mode",
|
||||
"security_delay_min",
|
||||
"security_min_on_percent",
|
||||
"security_default_on_percent",
|
||||
"last_temperature_datetime",
|
||||
"last_ext_temperature_datetime",
|
||||
"minimal_activation_delay_sec",
|
||||
"device_power",
|
||||
"mean_cycle_power",
|
||||
"last_update_datetime",
|
||||
"timezone",
|
||||
"window_sensor_entity_id",
|
||||
"window_delay_sec",
|
||||
"window_auto_open_threshold",
|
||||
"window_auto_close_threshold",
|
||||
"window_auto_max_duration",
|
||||
"motion_sensor_entity_id",
|
||||
"presence_sensor_entity_id",
|
||||
"power_sensor_entity_id",
|
||||
"max_power_sensor_entity_id",
|
||||
}
|
||||
))
|
||||
_entity_component_unrecorded_attributes = (
|
||||
ClimateEntity._entity_component_unrecorded_attributes.union(
|
||||
frozenset(
|
||||
{
|
||||
"is_on",
|
||||
"type",
|
||||
"eco_temp",
|
||||
"boost_temp",
|
||||
"comfort_temp",
|
||||
"eco_away_temp",
|
||||
"boost_away_temp",
|
||||
"comfort_away_temp",
|
||||
"power_temp",
|
||||
"ac_mode",
|
||||
"current_power_max",
|
||||
"saved_preset_mode",
|
||||
"saved_target_temp",
|
||||
"saved_hvac_mode",
|
||||
"security_delay_min",
|
||||
"security_min_on_percent",
|
||||
"security_default_on_percent",
|
||||
"last_temperature_datetime",
|
||||
"last_ext_temperature_datetime",
|
||||
"minimal_activation_delay_sec",
|
||||
"device_power",
|
||||
"mean_cycle_power",
|
||||
"last_update_datetime",
|
||||
"timezone",
|
||||
"window_sensor_entity_id",
|
||||
"window_delay_sec",
|
||||
"window_auto_open_threshold",
|
||||
"window_auto_close_threshold",
|
||||
"window_auto_max_duration",
|
||||
"motion_sensor_entity_id",
|
||||
"presence_sensor_entity_id",
|
||||
"power_sensor_entity_id",
|
||||
"max_power_sensor_entity_id",
|
||||
"temperature_unit",
|
||||
"is_device_active",
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
|
||||
"""Initialize the thermostat."""
|
||||
@@ -621,7 +627,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
):
|
||||
self._window_state = (window_state.state == STATE_ON)
|
||||
self._window_state = window_state.state == STATE_ON
|
||||
_LOGGER.debug(
|
||||
"%s - Window state have been retrieved: %s",
|
||||
self,
|
||||
@@ -762,17 +768,17 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
|
||||
@property
|
||||
def is_over_climate(self) -> bool:
|
||||
""" True if the Thermostat is over_climate"""
|
||||
"""True if the Thermostat is over_climate"""
|
||||
return False
|
||||
|
||||
@property
|
||||
def is_over_switch(self) -> bool:
|
||||
""" True if the Thermostat is over_switch"""
|
||||
"""True if the Thermostat is over_switch"""
|
||||
return False
|
||||
|
||||
@property
|
||||
def is_over_valve(self) -> bool:
|
||||
""" True if the Thermostat is over_valve"""
|
||||
"""True if the Thermostat is over_valve"""
|
||||
return False
|
||||
|
||||
@property
|
||||
@@ -933,10 +939,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
if not self._device_power:
|
||||
return None
|
||||
|
||||
return float(
|
||||
self._device_power
|
||||
* self._prop_algorithm.on_percent
|
||||
)
|
||||
return float(self._device_power * self._prop_algorithm.on_percent)
|
||||
|
||||
@property
|
||||
def total_energy(self) -> float | None:
|
||||
@@ -963,7 +966,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
"""Get the window_auto_state"""
|
||||
return STATE_ON if self._window_auto_state else STATE_OFF
|
||||
|
||||
#PR - Adding Window ByPass
|
||||
@property
|
||||
def window_bypass_state(self) -> bool | None:
|
||||
"""Get the Window Bypass"""
|
||||
@@ -1033,6 +1035,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:
|
||||
@@ -1219,7 +1226,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
|
||||
async def _async_internal_set_temperature(self, temperature):
|
||||
"""Set the target temperature and the target temperature of underlying climate if any
|
||||
For testing purpose you can pass an event_timestamp.
|
||||
For testing purpose you can pass an event_timestamp.
|
||||
"""
|
||||
self._target_temp = temperature
|
||||
return
|
||||
@@ -1307,7 +1314,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
_LOGGER.debug(
|
||||
"Window delay condition is not satisfied. Ignore window event"
|
||||
)
|
||||
self._window_state = (old_state.state == STATE_ON)
|
||||
self._window_state = old_state.state == STATE_ON
|
||||
return
|
||||
|
||||
_LOGGER.debug("%s - Window delay condition is satisfied", self)
|
||||
@@ -1318,13 +1325,14 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
_LOGGER.debug("%s - no change in window state. Forget the event")
|
||||
return
|
||||
|
||||
self._window_state = new_state.state == STATE_ON
|
||||
|
||||
self._window_state = (new_state.state == STATE_ON)
|
||||
|
||||
#PR - Adding Window ByPass
|
||||
# PR - Adding Window ByPass
|
||||
_LOGGER.debug("%s - Window ByPass is : %s", self, self._window_bypass_state)
|
||||
if self._window_bypass_state:
|
||||
_LOGGER.info("%s - Window ByPass is activated. Ignore window event", self)
|
||||
_LOGGER.info(
|
||||
"%s - Window ByPass is activated. Ignore window event", self
|
||||
)
|
||||
else:
|
||||
if not self._window_state:
|
||||
_LOGGER.info(
|
||||
@@ -1587,7 +1595,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 +1613,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 +1705,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",
|
||||
@@ -1827,7 +1817,15 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
self._device_power,
|
||||
)
|
||||
|
||||
ret = (self._current_power + self._device_power) >= self._current_power_max
|
||||
if self.is_over_climate:
|
||||
power_consumption_max = self._device_power
|
||||
else:
|
||||
power_consumption_max = max(
|
||||
self._device_power / self.nb_underlying_entities,
|
||||
self._device_power * self._prop_algorithm.on_percent,
|
||||
)
|
||||
|
||||
ret = (self._current_power + power_consumption_max) >= self._current_power_max
|
||||
if not self._overpowering_state and ret and self._hvac_mode != HVACMode.OFF:
|
||||
_LOGGER.warning(
|
||||
"%s - overpowering is detected. Heater preset will be set to 'power'",
|
||||
@@ -1845,6 +1843,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
"current_power": self._current_power,
|
||||
"device_power": self._device_power,
|
||||
"current_power_max": self._current_power_max,
|
||||
"current_power_consumption": power_consumption_max,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1872,7 +1871,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:
|
||||
@@ -2098,6 +2100,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,
|
||||
@@ -2129,7 +2132,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
"overpowering_state": self.overpowering_state,
|
||||
"presence_state": self._presence_state,
|
||||
"window_auto_state": self.window_auto_state,
|
||||
#PR - Adding Window ByPass
|
||||
"window_bypass_state": self._window_bypass_state,
|
||||
"security_delay_min": self._security_delay_min,
|
||||
"security_min_on_percent": self._security_min_on_percent,
|
||||
@@ -2158,6 +2160,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
|
||||
@@ -2256,14 +2260,25 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
target:
|
||||
entity_id: climate.thermostat_1
|
||||
"""
|
||||
_LOGGER.info("%s - Calling service_set_window_bypass, window_bypass: %s", self, window_bypass)
|
||||
_LOGGER.info(
|
||||
"%s - Calling service_set_window_bypass, window_bypass: %s",
|
||||
self,
|
||||
window_bypass,
|
||||
)
|
||||
self._window_bypass_state = window_bypass
|
||||
if not self._window_bypass_state and self._window_state:
|
||||
_LOGGER.info("%s - Last window state was open & ByPass is now off. Set hvac_mode to '%s'", self, HVACMode.OFF)
|
||||
_LOGGER.info(
|
||||
"%s - Last window state was open & ByPass is now off. Set hvac_mode to '%s'",
|
||||
self,
|
||||
HVACMode.OFF,
|
||||
)
|
||||
self.save_hvac_mode()
|
||||
await self.async_set_hvac_mode(HVACMode.OFF)
|
||||
if self._window_bypass_state and self._window_state:
|
||||
_LOGGER.info("%s - Last window state was open & ByPass is now on. Set hvac_mode to last available mode", self)
|
||||
_LOGGER.info(
|
||||
"%s - Last window state was open & ByPass is now on. Set hvac_mode to last available mode",
|
||||
self,
|
||||
)
|
||||
await self.restore_hvac_mode(True)
|
||||
self.update_custom_attributes()
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -348,6 +348,7 @@
|
||||
},
|
||||
"auto_regulation_mode": {
|
||||
"options": {
|
||||
"auto_regulation_slow": "Slow",
|
||||
"auto_regulation_strong": "Strong",
|
||||
"auto_regulation_medium": "Medium",
|
||||
"auto_regulation_light": "Light",
|
||||
|
||||
@@ -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,28 +108,50 @@ 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,
|
||||
)
|
||||
|
||||
if not self._regulated_target_temp:
|
||||
self._regulated_target_temp = self.target_temperature
|
||||
|
||||
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.debug(
|
||||
"%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.
|
||||
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.debug("%s - period (%.1f) is < %.0f -> forget the regulation send", self, 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)
|
||||
_LOGGER.info(
|
||||
"%s - Regulated temp have changed to %.1f. Resend it to underlyings",
|
||||
self,
|
||||
new_regulated_temp,
|
||||
)
|
||||
self._last_regulation_change = now
|
||||
|
||||
for under in self._underlyings:
|
||||
@@ -123,7 +161,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 +180,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 +207,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 +217,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 +227,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 +237,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 +272,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 +536,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 +731,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()
|
||||
|
||||
@@ -348,6 +348,7 @@
|
||||
},
|
||||
"auto_regulation_mode": {
|
||||
"options": {
|
||||
"auto_regulation_slow": "Slow",
|
||||
"auto_regulation_strong": "Strong",
|
||||
"auto_regulation_medium": "Medium",
|
||||
"auto_regulation_light": "Light",
|
||||
|
||||
@@ -349,6 +349,7 @@
|
||||
},
|
||||
"auto_regulation_mode": {
|
||||
"options": {
|
||||
"auto_regulation_slow": "Lente",
|
||||
"auto_regulation_strong": "Forte",
|
||||
"auto_regulation_medium": "Moyenne",
|
||||
"auto_regulation_light": "Légère",
|
||||
|
||||
@@ -30,15 +30,15 @@
|
||||
"heater_entity3_id": "Terzo riscaldatore",
|
||||
"heater_entity4_id": "Quarto riscaldatore",
|
||||
"proportional_function": "Algoritmo",
|
||||
"climate_entity_id": "Termostato sottostante",
|
||||
"climate_entity2_id": "Secundo termostato sottostante",
|
||||
"climate_entity3_id": "Terzo termostato sottostante",
|
||||
"climate_entity4_id": "Quarto termostato sottostante",
|
||||
"climate_entity_id": "Primo termostato",
|
||||
"climate_entity2_id": "Secondo termostato",
|
||||
"climate_entity3_id": "Terzo termostato",
|
||||
"climate_entity4_id": "Quarto termostato",
|
||||
"ac_mode": "AC mode ?",
|
||||
"valve_entity_id": "Primo valvola numero",
|
||||
"valve_entity2_id": "Secondo valvola numero",
|
||||
"valve_entity3_id": "Terzo valvola numero",
|
||||
"valve_entity4_id": "Quarto valvola numero",
|
||||
"valve_entity_id": "Prima valvola",
|
||||
"valve_entity2_id": "Seconda valvolao",
|
||||
"valve_entity3_id": "Terza valvola",
|
||||
"valve_entity4_id": "Quarta valvola",
|
||||
"auto_regulation_mode": "Autoregolamentazione",
|
||||
"inverse_switch_command": "Comando inverso"
|
||||
},
|
||||
@@ -48,15 +48,15 @@
|
||||
"heater_entity3_id": "Entity id del terzo riscaldatore facoltativo. Lasciare vuoto se non utilizzato",
|
||||
"heater_entity4_id": "Entity id del quarto riscaldatore facoltativo. Lasciare vuoto se non utilizzato",
|
||||
"proportional_function": "Algoritmo da utilizzare (il TPI per adesso è l'unico)",
|
||||
"climate_entity_id": "Entity id del termostato sottostante",
|
||||
"climate_entity2_id": "Entity id del secundo termostato sottostante",
|
||||
"climate_entity3_id": "Entity id del terzo termostato sottostante",
|
||||
"climate_entity4_id": "Entity id del quarto termostato sottostante",
|
||||
"climate_entity_id": "Entity id del primo termostato",
|
||||
"climate_entity2_id": "Entity id del secondo termostato",
|
||||
"climate_entity3_id": "Entity id del terzo termostato",
|
||||
"climate_entity4_id": "Entity id del quarto termostato",
|
||||
"ac_mode": "Utilizzare la modalità AC (Air Conditioned) ?",
|
||||
"valve_entity_id": "Entity id del primo valvola numero",
|
||||
"valve_entity2_id": "Entity id del secondo valvola numero",
|
||||
"valve_entity3_id": "Entity id del terzo valvola numero",
|
||||
"valve_entity4_id": "Entity id del quarto valvola numero",
|
||||
"valve_entity_id": "Entity id della prima valvola",
|
||||
"valve_entity2_id": "Entity id della seconda valvola",
|
||||
"valve_entity3_id": "Entity id della terza valvola",
|
||||
"valve_entity4_id": "Entity id della quarta valvola",
|
||||
"auto_regulation_mode": "Regolazione automatica della temperatura target",
|
||||
"inverse_switch_command": "Inverte il controllo dell'interruttore per un'installazione con filo pilota e diodo"
|
||||
}
|
||||
@@ -188,15 +188,15 @@
|
||||
"heater_entity3_id": "Terzo riscaldatore",
|
||||
"heater_entity4_id": "Quarto riscaldatore",
|
||||
"proportional_function": "Algoritmo",
|
||||
"climate_entity_id": "Termostato sottostante",
|
||||
"climate_entity2_id": "Secundo termostato sottostante",
|
||||
"climate_entity3_id": "Terzo termostato sottostante",
|
||||
"climate_entity4_id": "Quarto termostato sottostante",
|
||||
"climate_entity_id": "Primo termostato",
|
||||
"climate_entity2_id": "Secondo termostato",
|
||||
"climate_entity3_id": "Terzo termostato",
|
||||
"climate_entity4_id": "Quarto termostato",
|
||||
"ac_mode": "AC mode ?",
|
||||
"valve_entity_id": "Primo valvola numero",
|
||||
"valve_entity2_id": "Secondo valvola numero",
|
||||
"valve_entity3_id": "Terzo valvola numero",
|
||||
"valve_entity4_id": "Quarto valvola numero",
|
||||
"valve_entity_id": "Prima valvola",
|
||||
"valve_entity2_id": "Seconda valvola",
|
||||
"valve_entity3_id": "Terza valvola",
|
||||
"valve_entity4_id": "Quarta valvola",
|
||||
"auto_regulation_mode": "Autoregolamentazione",
|
||||
"inverse_switch_command": "Comando inverso"
|
||||
},
|
||||
@@ -206,15 +206,15 @@
|
||||
"heater_entity3_id": "Entity id del terzo riscaldatore facoltativo. Lasciare vuoto se non utilizzato",
|
||||
"heater_entity4_id": "Entity id del quarto riscaldatore facoltativo. Lasciare vuoto se non utilizzato",
|
||||
"proportional_function": "Algoritmo da utilizzare (il TPI per adesso è l'unico)",
|
||||
"climate_entity_id": "Entity id del termostato sottostante",
|
||||
"climate_entity2_id": "Entity id del secundo termostato sottostante",
|
||||
"climate_entity3_id": "Entity id del terzo termostato sottostante",
|
||||
"climate_entity4_id": "Entity id del quarto termostato sottostante",
|
||||
"climate_entity_id": "Entity id del primo termostato",
|
||||
"climate_entity2_id": "Entity id del secondo termostato",
|
||||
"climate_entity3_id": "Entity id del terzo termostato",
|
||||
"climate_entity4_id": "Entity id del quarto termostato",
|
||||
"ac_mode": "Utilizzare la modalità AC (Air Conditioned) ?",
|
||||
"valve_entity_id": "Entity id del primo valvola numero",
|
||||
"valve_entity2_id": "Entity id del secondo valvola numero",
|
||||
"valve_entity3_id": "Entity id del terzo valvola numero",
|
||||
"valve_entity4_id": "Entity id del quarto valvola numero",
|
||||
"valve_entity_id": "Entity id della prima valvola",
|
||||
"valve_entity2_id": "Entity id della seconda valvola",
|
||||
"valve_entity3_id": "Entity id della terza valvola",
|
||||
"valve_entity4_id": "Entity id della quarta valvola",
|
||||
"auto_regulation_mode": "Autoregolamentazione",
|
||||
"inverse_switch_command": "Inverte il controllo dell'interruttore per un'installazione con filo pilota e diodo"
|
||||
}
|
||||
@@ -252,9 +252,9 @@
|
||||
"data_description": {
|
||||
"window_sensor_entity_id": "Lasciare vuoto se non deve essere utilizzato alcun sensore finestra",
|
||||
"window_delay": "Ritardo in secondi prima che il rilevamento del sensore sia preso in considerazione",
|
||||
"window_auto_open_threshold": "Valore consigliato: tra 0.05 e 0.1. Lasciare vuoto se il rilevamento automatico della finestra aperta non è utilizzato",
|
||||
"window_auto_close_threshold": "Valore consigliato: 0. Lasciare vuoto se il rilevamento automatico della finestra aperta non è utilizzato",
|
||||
"window_auto_max_duration": "Valore consigliato: 60 (un'ora). Lasciare vuoto se il rilevamento automatico della finestra aperta non è utilizzato"
|
||||
"window_auto_open_threshold": "Valore consigliato: tra 0.05 e 0.1 - Lasciare vuoto se il rilevamento automatico della finestra aperta non è utilizzato",
|
||||
"window_auto_close_threshold": "Valore consigliato: 0 - Lasciare vuoto se il rilevamento automatico della finestra aperta non è utilizzato",
|
||||
"window_auto_max_duration": "Valore consigliato: 60 minuti. Lasciare vuoto se il rilevamento automatico della finestra aperta non è utilizzato"
|
||||
}
|
||||
},
|
||||
"motion": {
|
||||
@@ -320,12 +320,13 @@
|
||||
"thermostat_type": {
|
||||
"options": {
|
||||
"thermostat_over_switch": "Termostato su un interruttore",
|
||||
"thermostat_over_climate": "Termostato sopra un altro termostato",
|
||||
"thermostat_over_valve": "Thermostato su una valvola"
|
||||
"thermostat_over_climate": "Termostato su un climatizzatore",
|
||||
"thermostat_over_valve": "Termostato su una valvola"
|
||||
}
|
||||
},
|
||||
"auto_regulation_mode": {
|
||||
"options": {
|
||||
"auto_regulation_slow": "Lento",
|
||||
"auto_regulation_strong": "Forte",
|
||||
"auto_regulation_medium": "Media",
|
||||
"auto_regulation_light": "Leggera",
|
||||
|
||||
@@ -348,6 +348,7 @@
|
||||
},
|
||||
"auto_regulation_mode": {
|
||||
"options": {
|
||||
"auto_regulation_slow": "Slow",
|
||||
"auto_regulation_strong": "Strong",
|
||||
"auto_regulation_medium": "Medium",
|
||||
"auto_regulation_light": "Light",
|
||||
|
||||
@@ -163,7 +163,9 @@ async def test_one_switch_cycle(
|
||||
# assert entity.underlying_entity(0)._should_relaunch_control_heating is True
|
||||
|
||||
# Simulate the relaunch
|
||||
await entity.underlying_entity(0)._turn_on_later( # pylint: disable=protected-access
|
||||
await entity.underlying_entity(
|
||||
0
|
||||
)._turn_on_later( # pylint: disable=protected-access
|
||||
None
|
||||
)
|
||||
# wait restart
|
||||
@@ -184,7 +186,9 @@ async def test_one_switch_cycle(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=True,
|
||||
) as mock_device_active:
|
||||
await entity.underlying_entity(0)._turn_off_later( # pylint: disable=protected-access
|
||||
await entity.underlying_entity(
|
||||
0
|
||||
)._turn_off_later( # pylint: disable=protected-access
|
||||
None
|
||||
)
|
||||
|
||||
@@ -207,7 +211,9 @@ async def test_one_switch_cycle(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=True,
|
||||
) as mock_device_active:
|
||||
await entity.underlying_entity(0)._turn_on_later( # pylint: disable=protected-access
|
||||
await entity.underlying_entity(
|
||||
0
|
||||
)._turn_on_later( # pylint: disable=protected-access
|
||||
None
|
||||
)
|
||||
|
||||
@@ -591,3 +597,139 @@ async def test_multiple_climates_underlying_changes(
|
||||
assert entity.hvac_mode == HVACMode.HEAT
|
||||
assert entity.hvac_action == HVACAction.IDLE
|
||||
assert entity.is_device_active is False # pylint: disable=protected-access
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_multiple_switch_power_management(
|
||||
hass: HomeAssistant, skip_hass_states_is_state
|
||||
):
|
||||
"""Test the Power management"""
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOverSwitchMockName",
|
||||
unique_id="uniqueId",
|
||||
data={
|
||||
CONF_NAME: "TheOver4SwitchMockName",
|
||||
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: 8,
|
||||
CONF_TEMP_MIN: 15,
|
||||
CONF_TEMP_MAX: 30,
|
||||
"eco_temp": 17,
|
||||
"comfort_temp": 18,
|
||||
"boost_temp": 19,
|
||||
CONF_USE_WINDOW_FEATURE: False,
|
||||
CONF_USE_MOTION_FEATURE: False,
|
||||
CONF_USE_POWER_FEATURE: True,
|
||||
CONF_USE_PRESENCE_FEATURE: False,
|
||||
CONF_HEATER: "switch.mock_switch1",
|
||||
CONF_HEATER_2: "switch.mock_switch2",
|
||||
CONF_HEATER_3: "switch.mock_switch3",
|
||||
CONF_HEATER_4: "switch.mock_switch4",
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||
CONF_TPI_COEF_INT: 0.3,
|
||||
CONF_TPI_COEF_EXT: 0.01,
|
||||
CONF_POWER_SENSOR: "sensor.mock_power_sensor",
|
||||
CONF_MAX_POWER_SENSOR: "sensor.mock_power_max_sensor",
|
||||
CONF_DEVICE_POWER: 100,
|
||||
CONF_PRESET_POWER: 12,
|
||||
},
|
||||
)
|
||||
|
||||
entity: BaseThermostat = await create_thermostat(
|
||||
hass, entry, "climate.theover4switchmockname"
|
||||
)
|
||||
assert entity
|
||||
assert entity.is_over_climate is False
|
||||
assert entity.nb_underlying_entities == 4
|
||||
|
||||
tpi_algo = entity._prop_algorithm
|
||||
assert tpi_algo
|
||||
|
||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
await entity.async_set_preset_mode(PRESET_BOOST)
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
assert entity.preset_mode is PRESET_BOOST
|
||||
assert entity.overpowering_state is None
|
||||
assert entity.target_temperature == 19
|
||||
|
||||
# 1. Send power mesurement
|
||||
await send_power_change_event(entity, 50, datetime.now())
|
||||
# Send power max mesurement
|
||||
await send_max_power_change_event(entity, 300, datetime.now())
|
||||
assert await entity.check_overpowering() is False
|
||||
# All configuration is complete and power is < power_max
|
||||
assert entity.preset_mode is PRESET_BOOST
|
||||
assert entity.overpowering_state is False
|
||||
|
||||
# 2. Send power max mesurement too low and HVACMode is on
|
||||
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:
|
||||
# 100 of the device / 4 -> 25, current power 50 so max is 75
|
||||
await send_max_power_change_event(entity, 74, datetime.now())
|
||||
assert await entity.check_overpowering() is True
|
||||
# All configuration is complete and power is > power_max we switch to POWER preset
|
||||
assert entity.preset_mode is PRESET_POWER
|
||||
assert entity.overpowering_state is True
|
||||
assert entity.target_temperature == 12
|
||||
|
||||
assert mock_send_event.call_count == 2
|
||||
mock_send_event.assert_has_calls(
|
||||
[
|
||||
call.send_event(EventType.PRESET_EVENT, {"preset": PRESET_POWER}),
|
||||
call.send_event(
|
||||
EventType.POWER_EVENT,
|
||||
{
|
||||
"type": "start",
|
||||
"current_power": 50,
|
||||
"device_power": 100,
|
||||
"current_power_max": 74,
|
||||
"current_power_consumption": 25.0,
|
||||
},
|
||||
),
|
||||
],
|
||||
any_order=True,
|
||||
)
|
||||
assert mock_heater_on.call_count == 0
|
||||
assert mock_heater_off.call_count == 4 # The fourth are shutdown
|
||||
|
||||
# 3. change PRESET
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event:
|
||||
await entity.async_set_preset_mode(PRESET_ECO)
|
||||
assert entity.preset_mode is PRESET_ECO
|
||||
# No change
|
||||
assert entity.overpowering_state is True
|
||||
|
||||
# 4. Send hugh power max mesurement to release overpowering
|
||||
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:
|
||||
# 100 of the device / 4 -> 25, current power 50 so max is 75. With 150 no overheating
|
||||
await send_max_power_change_event(entity, 150, datetime.now())
|
||||
assert await entity.check_overpowering() is False
|
||||
# All configuration is complete and power is > power_max we switch to POWER preset
|
||||
assert entity.preset_mode is PRESET_ECO
|
||||
assert entity.overpowering_state is False
|
||||
assert entity.target_temperature == 17
|
||||
|
||||
assert (
|
||||
mock_heater_on.call_count == 0
|
||||
) # The fourth are not restarted because temperature is enought
|
||||
assert mock_heater_off.call_count == 0
|
||||
|
||||
+5
-1
@@ -4,8 +4,11 @@ from unittest.mock import patch, call
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
|
||||
from custom_components.versatile_thermostat.thermostat_switch import ThermostatOverSwitch
|
||||
from custom_components.versatile_thermostat.thermostat_switch import (
|
||||
ThermostatOverSwitch,
|
||||
)
|
||||
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
|
||||
logging.getLogger().setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
@@ -185,6 +188,7 @@ async def test_power_management_hvac_on(hass: HomeAssistant, skip_hass_states_is
|
||||
"current_power": 50,
|
||||
"device_power": 100,
|
||||
"current_power_max": 149,
|
||||
"current_power_consumption": 100.0,
|
||||
},
|
||||
),
|
||||
],
|
||||
|
||||
+26
-36
@@ -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