Compare commits

..

15 Commits
4.0.0 ... 4.0.3

Author SHA1 Message Date
Jean-Marc Collin
ab1c6892df Issue #164 - multiple calls to regulation 2023-11-12 09:20:52 +00:00
Jean-Marc Collin
84c8ac4f59 Beers from @Gunnar M 2023-11-11 17:32:41 +00:00
Jean-Marc Collin
faab9648a7 Add Rointe incompatility 2023-11-11 16:41:03 +00:00
Jean-Marc Collin
a30ad38a53 Add logs for issue #164 2023-11-11 15:48:12 +00:00
Jean-Marc Collin
c0b186b8c1 Issue #181 - auto-window for over_climate doesn't work 2023-11-11 15:20:52 +00:00
Jean-Marc Collin
01e761aecd FIX is_device_active flag 2023-11-11 11:39:04 +00:00
Jean-Marc Collin
55a99054fa FIX overpowering is not always saved 2023-11-11 10:30:37 +00:00
Jean-Marc Collin
2c5078cd7f With update for UI card 2023-11-11 08:41:25 +00:00
Jean-Marc Collin
82348adef2 Add Heatzy incompatibility 2023-11-10 22:26:08 +00:00
Jean-Marc Collin
71aad211c6 Add power_percent in over_switch for UI 2023-11-07 00:09:34 +00:00
Jean-Marc Collin
a40f976fd1 Enhance messages when temp are not ready 2023-11-06 16:54:19 +00:00
Jean-Marc Collin
382f6f99c6 Issue #162 - overpowering mode after preset change 2023-11-06 16:43:59 +00:00
Jean-Marc Collin
95c4aa8ae9 Issue #174 - regression following PR#150 2023-11-06 16:13:35 +00:00
Jean-Marc Collin
a6a47fde53 Resolve devcontainers warnings 2023-11-06 15:58:09 +00:00
echopage
e08f51b4f2 Update it.json (#172)
Verifica e sostituzione terminologie errate
2023-11-06 16:17:19 +01:00
17 changed files with 565 additions and 262 deletions

View File

@@ -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,

View File

@@ -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"
},

View File

@@ -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)
@@ -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 ?
@@ -735,7 +737,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 :
![image](https://github.com/jmcollin78/versatile-thermostat-ui-card/blob/master/assets/1.png?raw=true)

View File

@@ -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 ?

View File

@@ -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()

View File

@@ -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

View File

@@ -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(

View File

@@ -348,6 +348,7 @@
},
"auto_regulation_mode": {
"options": {
"auto_regulation_slow": "Slow",
"auto_regulation_strong": "Strong",
"auto_regulation_medium": "Medium",
"auto_regulation_light": "Light",

View File

@@ -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,52 @@ 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)
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)
_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 +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":

View File

@@ -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()

View File

@@ -348,6 +348,7 @@
},
"auto_regulation_mode": {
"options": {
"auto_regulation_slow": "Slow",
"auto_regulation_strong": "Strong",
"auto_regulation_medium": "Medium",
"auto_regulation_light": "Light",

View File

@@ -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",

View File

@@ -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",

View File

@@ -348,6 +348,7 @@
},
"auto_regulation_mode": {
"options": {
"auto_regulation_slow": "Slow",
"auto_regulation_strong": "Strong",
"auto_regulation_medium": "Medium",
"auto_regulation_light": "Light",

View File

@@ -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

View File

@@ -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,
},
),
],

View File

@@ -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()