From 7851df84ecb39663621efbdf218eb1e9e350a160 Mon Sep 17 00:00:00 2001 From: Jean-Marc Collin Date: Sat, 9 Dec 2023 18:39:54 +0100 Subject: [PATCH] Feature 223 use fan control in over climate (#260) * Issue #223 - add auto_fan_mode * Update README --------- Co-authored-by: Jean-Marc Collin --- .devcontainer/devcontainer.json | 3 +- .gitignore | 1 + README-fr.md | 123 +++++--- README.md | 124 +++++--- .../versatile_thermostat/base_thermostat.py | 42 ++- .../versatile_thermostat/climate.py | 28 +- .../versatile_thermostat/config_flow.py | 11 + .../versatile_thermostat/const.py | 18 ++ .../versatile_thermostat/services.yaml | 22 ++ .../versatile_thermostat/strings.json | 87 +++--- .../thermostat_climate.py | 184 ++++++++++- .../versatile_thermostat/translations/el.json | 21 +- .../versatile_thermostat/translations/en.json | 21 +- .../versatile_thermostat/translations/fr.json | 21 +- .../versatile_thermostat/translations/it.json | 21 +- .../versatile_thermostat/translations/sk.json | 21 +- images/config-linked-entity2.png | Bin 47449 -> 41049 bytes tests/commons.py | 72 +++-- tests/const.py | 22 +- tests/test_auto_fan_mode.py | 285 ++++++++++++++++++ 20 files changed, 919 insertions(+), 208 deletions(-) create mode 100644 tests/test_auto_fan_mode.py diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 5422c257..c3c0b240 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -29,7 +29,8 @@ "donjayamanne.githistory", "waderyan.gitblame", "keesschollaart.vscode-home-assistant", - "vscode.markdown-math" + "vscode.markdown-math", + "yzhang.markdown-all-in-one" ], // "mounts": [ // "source=${localWorkspaceFolder}/.devcontainer/configuration.yaml,target=${localWorkspaceFolder}/config/www/community/,type=bind,consistency=cached", diff --git a/.gitignore b/.gitignore index 67d03c42..e7e27b9c 100644 --- a/.gitignore +++ b/.gitignore @@ -109,3 +109,4 @@ __pycache__ config/** custom_components/hacs +custom_components/localtuya diff --git a/README-fr.md b/README-fr.md index a31154fa..92c55e6a 100644 --- a/README-fr.md +++ b/README-fr.md @@ -23,6 +23,7 @@ - [Pour un thermostat de type ```thermostat_over_climate```:](#pour-un-thermostat-de-type-thermostat_over_climate) - [L'auto-régulation](#lauto-régulation) - [L'auto-régulation en mode Expert](#lauto-régulation-en-mode-expert) + - [Le mode auto-fan](#le-mode-auto-fan) - [Pour un thermostat de type ```thermostat_over_valve```:](#pour-un-thermostat-de-type-thermostat_over_valve) - [Configurez les coefficients de l'algorithme TPI](#configurez-les-coefficients-de-lalgorithme-tpi) - [Configurer la température préréglée](#configurer-la-température-préréglée) @@ -55,7 +56,7 @@ - [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) + - [Toujours mieux avec Plotly pour régler votre thermostat](#toujours-mieux-avec-plotly-pour-régler-votre-thermostat) - [Et toujours de mieux en mieux avec l'AappDaemon NOTIFIER pour notifier les évènements](#et-toujours-de-mieux-en-mieux-avec-laappdaemon-notifier-pour-notifier-les-évènements) - [Les contributions sont les bienvenues !](#les-contributions-sont-les-bienvenues) - [Dépannages](#dépannages) @@ -68,6 +69,7 @@ Ce composant personnalisé pour Home Assistant est une mise à niveau et est une > ![Nouveau](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/new-icon.png?raw=true) _*Nouveautés*_ +> * **Release 4.3** : Ajout d'un mode auto-fan pour le type `over_climate` permettant d'activer la ventilation si l'écart de température est important [#223](https://github.com/jmcollin78/versatile_thermostat/issues/223). > * **Release 4.2** : Le calcul de la pente de la courbe de température se fait maintenant en °/heure et non plus en °/min [#242](https://github.com/jmcollin78/versatile_thermostat/issues/242). Correction de la détection automatique des ouvertures par l'ajout d'un lissage de la courbe de température . > * **Release 4.1** : Ajout d'un mode de régulation **Expert** dans lequel l'utilisateur peut spécifier ses propres paramètres d'auto-régulation au lieu d'utiliser les pre-programmés [#194](https://github.com/jmcollin78/versatile_thermostat/issues/194). > * **Release 4.0** : Ajout de la prise en charge de la **Versatile Thermostat UI Card**. Voir [Versatile Thermostat UI Card](https://github.com/jmcollin78/versatile-thermostat-ui-card). Ajout d'un mode de régulation **Slow** pour les appareils de chauffage à latence lente [#168](https://github.com/jmcollin78/versatile_thermostat/issues/168). Changement de la façon dont **la puissance est calculée** dans le cas de VTherm avec des équipements multi-sous-jacents [#146](https://github.com/jmcollin78/versatile_thermostat/issues/146). Ajout de la prise en charge de AC et Heat pour VTherm via un interrupteur également [#144](https://github.com/jmcollin78/versatile_thermostat/pull/144) @@ -315,6 +317,15 @@ et bien sur, configurer le mode auto-régulation du VTherm en mode Expert. Tous Pour que les modifications soient prises en compte, il faut soit **relancer totalement Home Assistant** soit juste l'intégration Versatile Thermostat (Outils de dev / Yaml / rechargement de la configuration / Versatile Thermostat). + +#### Le mode auto-fan +Ce mode introduit en 4.3 permet de forcer l'usage de la ventilation si l'écart de température est important. En effet, en activant la ventilation, la répartition se fait plus rapidement ce qui permet de gagner du temps dans l'atteinte de la température cible. +Vous pouvez choisir quelle ventilation vous voulez activer entre les paramètres suivants : Faible, Moyenne, Forte, Turbo. + +Il faut évidemment que votre équipement sous-jacent soit équipée d'une ventilation et quelle soit pilotable pour que cela fonctionne. +Si votre équipement ne comprend pas le mode Turbo, le mode Forte` sera utilisé en remplacement. +Une fois l'écart de température redevenu faible, la ventilation se mettra dans un mode "normal" qui dépend de votre équipement à savoir (dans l'ordre) : `Silence (mute)`, `Auto (auto)`, `Faible (low)`. La première valeur qui est possible pour votre équipement sera choisie. + ### Pour un thermostat de type ```thermostat_over_valve```: ![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-linked-entity3.png?raw=true) Vous pouvez choisir jusqu'à entité du domaine ```number``` ou ```ìnput_number``` qui vont commander les vannes. @@ -902,53 +913,73 @@ Vous pouvez personnaliser ce composant à l'aide du composant HACS card-mod pour ``` ![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/custom-css-thermostat.png?raw=true) -## Toujours mieux avec Apex-chart pour régler votre thermostat -Vous pouvez obtenir une courbe comme celle présentée dans [some results](#some-results) avec une sorte de configuration de graphique Apex uniquement en utilisant les attributs personnalisés du thermostat décrits [ici](#custom-attributes) : +## Toujours mieux avec Plotly pour régler votre thermostat +Vous pouvez obtenir une courbe comme celle présentée dans [some results](#some-results) avec une sorte de configuration de graphique Plotly uniquement en utilisant les attributs personnalisés du thermostat décrits [ici](#custom-attributes) : +Remplacez les valeurs entre [[ ]] par les votres. ``` -type: custom:apexcharts-card -header: - show: true - title: Tuning chauffage - show_states: true - colorize_states: true -update_interval: 60sec -graph_span: 4h -yaxis: - - id: left - show: true - decimals: 2 - - id: right - decimals: 2 - show: true - opposite: true -series: - - entity: climate.thermostat_mythermostat - attribute: temperature - type: line - name: Target temp - curve: smooth - yaxis_id: left - - entity: climate.thermostat_mythermostat - attribute: current_temperature - name: Current temp - curve: smooth - yaxis_id: left - - entity: climate.thermostat_mythermostat <--- for over_switch - attribute: on_percent - name: Power percent - curve: stepline - yaxis_id: right - - entity: climate.thermostat_mythermostat <--- for over_thermostast - attribute: regulated_target_temperature - name: Regulated temperature - curve: stepline - yaxis_id: left - - entity: climate.thermostat_mythermostat <--- for over_valve - attribute: valve_open_percent - name: Valve open percent - curve: stepline - yaxis_id: right +- type: custom:plotly-graph + entities: + - entity: '[[climate]]' + attribute: temperature + yaxis: y1 + name: Consigne + - entity: '[[climate]]' + attribute: current_temperature + yaxis: y1 + name: T° + - entity: '[[climate]]' + attribute: ema_temp + yaxis: y1 + name: Ema + - entity: '[[climate]]' + attribute: regulated_target_temperature + yaxis: y1 + name: Regulated T° + - entity: '[[slope]]' + name: Slope + fill: tozeroy + yaxis: y9 + fillcolor: rgba(100, 100, 100, 0.3) + line: + color: rgba(100, 100, 100, 0.9) + hours_to_show: 4 + refresh_interval: 10 + height: 800 + config: + scrollZoom: true + layout: + margin: + r: 50 + legend: + x: 0 + 'y': 1.2 + groupclick: togglegroup + title: + side: top right + yaxis: + visible: true + position: 0 + yaxis9: + visible: true + fixedrange: false + range: + - -0.5 + - 0.5 + position: 1 + xaxis: + rangeselector: + 'y': 1.1 + x: 0.7 + buttons: + - count: 1 + step: hour + - count: 12 + step: hour + - count: 1 + step: day + - count: 7 + step: day ``` ## Et toujours de mieux en mieux avec l'AappDaemon NOTIFIER pour notifier les évènements diff --git a/README.md b/README.md index 0bf53c74..a0e3db98 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ - [For a thermostat of type ```thermostat_over_climate```:](#for-a-thermostat-of-type-thermostat_over_climate) - [Self-regulation](#self-regulation) - [Self-regulation in Expert mode](#self-regulation-in-expert-mode) + - [Auto-fan mode](#auto-fan-mode) - [For a thermostat of type ```thermostat_over_valve```:](#for-a-thermostat-of-type-thermostat_over_valve) - [Configure the TPI algorithm coefficients](#configure-the-tpi-algorithm-coefficients) - [Configure the preset temperature](#configure-the-preset-temperature) @@ -54,7 +55,7 @@ - [Much better with the Veersatile Thermostat UI Card](#much-better-with-the-veersatile-thermostat-ui-card) - [Even Better with Scheduler Component !](#even-better-with-scheduler-component-) - [Even-even better with custom:simple-thermostat front integration](#even-even-better-with-customsimple-thermostat-front-integration) - - [Even better with Apex-chart to tune your Thermostat](#even-better-with-apex-chart-to-tune-your-thermostat) + - [Even better with Plotly to tune your Thermostat](#even-better-with-plotly-to-tune-your-thermostat) - [And always better and better with the NOTIFIER daemon app to notify events](#and-always-better-and-better-with-the-notifier-daemon-app-to-notify-events) - [Contributions are welcome!](#contributions-are-welcome) - [Troubleshooting](#troubleshooting) @@ -67,7 +68,8 @@ This custom component for Home Assistant is an upgrade and is a complete rewrite of the component "Awesome thermostat" (see [Github](https://github.com/dadge/awesome_thermostat)) with addition of features. >![New](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/new-icon.png?raw=true) _*News*_ -> * **Release 4.2**: The calculation of the slope of the temperature curve is now done in °/hour and no longer in °/min [#242](https://github.com/jmcollin78/versatile_thermostat/ issues/242). Correction of automatic detection of openings by adding smoothing of the temperature curve. +> * **Release 4.3**: Added an auto-fan mode for the `over_climate` type allowing ventilation to be activated if the temperature difference is significant [#223](https://github.com/jmcollin78/versatile_thermostat/issues/223). +> * **Release 4.2**: The calculation of the slope of the temperature curve is now done in °/hour and no longer in °/min [#242](https://github.com/jmcollin78/versatile_thermostat/issues/242). Correction of automatic detection of openings by adding smoothing of the temperature curve. > * **Release 4.1**: Added an **Expert** regulation mode in which the user can specify their own auto-regulation parameters instead of using the pre-programmed ones [#194]( https://github.com/jmcollin78/versatile_thermostat/issues/194). > * **Release 4.0**: Added the support of **Versatile Thermostat UI Card**. See [Versatile Thermostat UI Card](https://github.com/jmcollin78/versatile-thermostat-ui-card). Added a **Slow** regulation mode for slow latency heating devices [#168](https://github.com/jmcollin78/versatile_thermostat/issues/168). Change the way **the power is calculated** in case of VTherm with multi-underlying equipements [#146](https://github.com/jmcollin78/versatile_thermostat/issues/146). Added the support of AC and Heat for VTherm over switch alse [#144](https://github.com/jmcollin78/versatile_thermostat/pull/144) > * **Release 3.8**: Added a **self-regulation function** for `over climate` thermostats whose regulation is done by the underlying climate. See [Self-regulation](#self-regulation) and [#129](https://github.com/jmcollin78/versatile_thermostat/issues/129). Added the possibility of **inverting the command** for an `over switch` thermostat to address installations with pilot wire and diode [#124](https://github.com/jmcollin78/versatile_thermostat/issues/124). @@ -311,6 +313,14 @@ and of course, configure the VTherm's self-regulation mode in **Expert** mode. A For the changes to be taken into account, you must either **completely restart Home Assistant** or just the **Versatile Thermostat integration** (Dev tools / Yaml / reloading the configuration / Versatile Thermostat). +#### Auto-fan mode +This mode introduced in 4.3 makes it possible to force the use of ventilation if the temperature difference is significant. In fact, by activating ventilation, distribution occurs more quickly, which saves time in reaching the target temperature. +You can choose which ventilation you want to activate between the following settings: Low, Medium, High, Turbo. + +Obviously your underlying equipment must be equipped with ventilation and be controllable for this to work. +If your equipment does not include Turbo mode, Forte` mode will be used as a replacement. +Once the temperature difference becomes low again, the ventilation will go into a "normal" mode which depends on your equipment, namely (in order): `Silence (mute)`, `Auto (auto)`, `Low (low)`. The first value that is possible for your equipment will be chosen. + ### For a thermostat of type ```thermostat_over_valve```: ![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-linked-entity3.png?raw=true) You can choose up to domain entity ```number``` or ```ìnput_number``` which will control the valves. @@ -884,53 +894,73 @@ You can customize this component using the HACS card-mod component to adjust the ``` ![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/custom-css-thermostat.png?raw=true) -## Even better with Apex-chart to tune your Thermostat -You can get curve like presented in [some results](#some-results) with kind of Apex-chart configuration only using the custom attributes of the thermostat described [here](#custom-attributes): +## Even better with Plotly to tune your Thermostat +You can get curve like presented in [some results](#some-results) with kind of Plotly configuration only using the custom attributes of the thermostat described [here](#custom-attributes): +Replace values in [[ ]] by yours. ``` -type: custom:apexcharts-card -header: - show: true - title: Tuning chauffage - show_states: true - colorize_states: true -update_interval: 60sec -graph_span: 4h -yaxis: - - id: left - show: true - decimals: 2 - - id: right - decimals: 2 - show: true - opposite: true -series: - - entity: climate.thermostat_mythermostat - attribute: temperature - type: line - name: Target temp - curve: smooth - yaxis_id: left - - entity: climate.thermostat_mythermostat - attribute: current_temperature - name: Current temp - curve: smooth - yaxis_id: left - - entity: climate.thermostat_mythermostat <--- for over_switch - attribute: on_percent - name: Power percent - curve: stepline - yaxis_id: right - - entity: climate.thermostat_mythermostat <--- for over_thermostast - attribute: regulated_target_temperature - name: Regulated temperature - curve: stepline - yaxis_id: left - - entity: climate.thermostat_mythermostat <--- for over_valve - attribute: valve_open_percent - name: Valve open percent - curve: stepline - yaxis_id: right +- type: custom:plotly-graph + entities: + - entity: '[[climate]]' + attribute: temperature + yaxis: y1 + name: Consigne + - entity: '[[climate]]' + attribute: current_temperature + yaxis: y1 + name: T° + - entity: '[[climate]]' + attribute: ema_temp + yaxis: y1 + name: Ema + - entity: '[[climate]]' + attribute: regulated_target_temperature + yaxis: y1 + name: Regulated T° + - entity: '[[slope]]' + name: Slope + fill: tozeroy + yaxis: y9 + fillcolor: rgba(100, 100, 100, 0.3) + line: + color: rgba(100, 100, 100, 0.9) + hours_to_show: 4 + refresh_interval: 10 + height: 800 + config: + scrollZoom: true + layout: + margin: + r: 50 + legend: + x: 0 + 'y': 1.2 + groupclick: togglegroup + title: + side: top right + yaxis: + visible: true + position: 0 + yaxis9: + visible: true + fixedrange: false + range: + - -0.5 + - 0.5 + position: 1 + xaxis: + rangeselector: + 'y': 1.1 + x: 0.7 + buttons: + - count: 1 + step: hour + - count: 12 + step: hour + - count: 1 + step: day + - count: 7 + step: day ``` ## And always better and better with the NOTIFIER daemon app to notify events diff --git a/custom_components/versatile_thermostat/base_thermostat.py b/custom_components/versatile_thermostat/base_thermostat.py index 915d499f..3617145c 100644 --- a/custom_components/versatile_thermostat/base_thermostat.py +++ b/custom_components/versatile_thermostat/base_thermostat.py @@ -102,7 +102,6 @@ CONF_TEMP_MIN, HIDDEN_PRESETS, CONF_AC_MODE, - UnknownEntity, EventType, ATTR_MEAN_POWER_CYCLE, ATTR_TOTAL_ENERGY, @@ -259,6 +258,8 @@ def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None: self._ema_temp = None self._ema_algo = None self._now = None + + self._attr_fan_mode = None self.post_init(entry_infos) def post_init(self, entry_infos): @@ -555,11 +556,7 @@ async def async_added_to_hass(self): self.async_on_remove(self.remove_thermostat) - try: - await self.async_startup() - except UnknownEntity: - # Ingore this error which is possible if underlying climate is not found temporary - pass + await self.async_startup() def remove_thermostat(self): """Called when the thermostat will be removed""" @@ -577,12 +574,7 @@ async def _async_startup_internal(*_): need_write_state = False # Initialize all UnderlyingEntities - for under in self._underlyings: - try: - under.startup() - except UnknownEntity: - # Not found, we will try later - pass + self.init_underlyings() temperature_state = self.hass.states.get(self._temp_sensor_entity_id) if temperature_state and temperature_state.state not in ( @@ -723,6 +715,9 @@ async def _async_startup_internal(*_): EVENT_HOMEASSISTANT_START, _async_startup_internal ) + def init_underlyings(self): + """Initialize all underlyings. Should be overriden if necessary""" + def restore_specific_previous_state(self, old_state): """Should be overriden in each specific thermostat if a specific previous state or attribute should be @@ -2089,6 +2084,13 @@ async def check_security(self) -> bool: return shouldBeInSecurity + @property + def is_initialized(self) -> bool: + """Check if all underlyings are initialized + This is usefull only for over_climate in which we + should have found the underlying climate to be operational""" + return True + async def async_control_heating(self, force=False, _=None): """The main function used to run the calculation at each cycle""" @@ -2104,18 +2106,10 @@ async def async_control_heating(self, force=False, _=None): await self._async_manage_window_auto(in_cycle=True) # Issue 56 in over_climate mode, if the underlying climate is not initialized, try to initialize it - for under in self._underlyings: - if not under.is_initialized: - _LOGGER.info( - "%s - Underlying %s is not initialized. Try to initialize it", - self, - under.entity_id, - ) - try: - under.startup() - except UnknownEntity: - # still not found, we an stop here - return False + if not self.is_initialized: + if not self.init_underlyings(): + # still not found, we an stop here + return False # Check overpowering condition # Not necessary for switch because each switch is checking at startup diff --git a/custom_components/versatile_thermostat/climate.py b/custom_components/versatile_thermostat/climate.py index ea2dc1b6..6ccd5589 100644 --- a/custom_components/versatile_thermostat/climate.py +++ b/custom_components/versatile_thermostat/climate.py @@ -15,7 +15,13 @@ from homeassistant.helpers import entity_platform -from homeassistant.const import CONF_NAME, STATE_ON, STATE_OFF, STATE_HOME, STATE_NOT_HOME +from homeassistant.const import ( + CONF_NAME, + STATE_ON, + STATE_OFF, + STATE_HOME, + STATE_NOT_HOME, +) from .const import ( DOMAIN, @@ -26,10 +32,11 @@ SERVICE_SET_SECURITY, SERVICE_SET_WINDOW_BYPASS, SERVICE_SET_AUTO_REGULATION_MODE, + SERVICE_SET_AUTO_FAN_MODE, CONF_THERMOSTAT_TYPE, CONF_THERMOSTAT_SWITCH, CONF_THERMOSTAT_CLIMATE, - CONF_THERMOSTAT_VALVE + CONF_THERMOSTAT_VALVE, ) from .thermostat_switch import ThermostatOverSwitch @@ -102,8 +109,7 @@ async def async_setup_entry( platform.async_register_entity_service( SERVICE_SET_WINDOW_BYPASS, { - vol.Required("window_bypass"): vol.In([True, False] - ), + vol.Required("window_bypass"): vol.In([True, False]), }, "service_set_window_bypass_state", ) @@ -111,7 +117,19 @@ async def async_setup_entry( platform.async_register_entity_service( SERVICE_SET_AUTO_REGULATION_MODE, { - vol.Required("auto_regulation_mode"): vol.In(["None", "Light", "Medium", "Strong", "Slow"]), + vol.Required("auto_regulation_mode"): vol.In( + ["None", "Light", "Medium", "Strong", "Slow"] + ), }, "service_set_auto_regulation_mode", ) + + platform.async_register_entity_service( + SERVICE_SET_AUTO_FAN_MODE, + { + vol.Required("auto_fan_mode"): vol.In( + ["None", "Low", "Medium", "High", "Turbo"] + ), + }, + "service_set_auto_fan_mode", + ) diff --git a/custom_components/versatile_thermostat/config_flow.py b/custom_components/versatile_thermostat/config_flow.py index 3b6b9cd1..5a6a0880 100644 --- a/custom_components/versatile_thermostat/config_flow.py +++ b/custom_components/versatile_thermostat/config_flow.py @@ -107,6 +107,9 @@ CONF_INVERSE_SWITCH, UnknownEntity, WindowOpenDetectionMethod, + CONF_AUTO_FAN_MODES, + CONF_AUTO_FAN_MODE, + CONF_AUTO_FAN_HIGH, ) _LOGGER = logging.getLogger(__name__) @@ -275,6 +278,14 @@ def __init__(self, infos) -> None: vol.Optional( CONF_AUTO_REGULATION_PERIOD_MIN, default=5 ): cv.positive_int, + vol.Optional( + CONF_AUTO_FAN_MODE, default=CONF_AUTO_FAN_HIGH + ): selector.SelectSelector( + selector.SelectSelectorConfig( + options=CONF_AUTO_FAN_MODES, + translation_key="auto_fan_mode", + ) + ), } ) diff --git a/custom_components/versatile_thermostat/const.py b/custom_components/versatile_thermostat/const.py index 1b5770c1..9e58a6fc 100644 --- a/custom_components/versatile_thermostat/const.py +++ b/custom_components/versatile_thermostat/const.py @@ -95,6 +95,12 @@ CONF_AUTO_REGULATION_PERIOD_MIN = "auto_regulation_periode_min" CONF_INVERSE_SWITCH = "inverse_switch_command" CONF_SHORT_EMA_PARAMS = "short_ema_params" +CONF_AUTO_FAN_MODE = "auto_fan_mode" +CONF_AUTO_FAN_NONE = "auto_fan_none" +CONF_AUTO_FAN_LOW = "auto_fan_low" +CONF_AUTO_FAN_MEDIUM = "auto_fan_medium" +CONF_AUTO_FAN_HIGH = "auto_fan_high" +CONF_AUTO_FAN_TURBO = "auto_fan_turbo" DEFAULT_SHORT_EMA_PARAMS = { "max_alpha": 0.5, @@ -233,6 +239,14 @@ CONF_THERMOSTAT_VALVE, ] +CONF_AUTO_FAN_MODES = [ + CONF_AUTO_FAN_NONE, + CONF_AUTO_FAN_LOW, + CONF_AUTO_FAN_MEDIUM, + CONF_AUTO_FAN_HIGH, + CONF_AUTO_FAN_TURBO, +] + SUPPORT_FLAGS = ClimateEntityFeature.TARGET_TEMPERATURE SERVICE_SET_PRESENCE = "set_presence" @@ -240,6 +254,7 @@ SERVICE_SET_SECURITY = "set_security" SERVICE_SET_WINDOW_BYPASS = "set_window_bypass" SERVICE_SET_AUTO_REGULATION_MODE = "set_auto_regulation_mode" +SERVICE_SET_AUTO_FAN_MODE = "set_auto_fan_mode" DEFAULT_SECURITY_MIN_ON_PERCENT = 0.5 DEFAULT_SECURITY_DEFAULT_ON_PERCENT = 0.1 @@ -247,6 +262,9 @@ ATTR_TOTAL_ENERGY = "total_energy" ATTR_MEAN_POWER_CYCLE = "mean_cycle_power" +AUTO_FAN_DTEMP_THRESHOLD = 2 +AUTO_FAN_DEACTIVATED_MODES = ["mute", "auto", "low"] + # A special regulation parameter suggested by @Maia here: https://github.com/jmcollin78/versatile_thermostat/discussions/154 class RegulationParamSlow: diff --git a/custom_components/versatile_thermostat/services.yaml b/custom_components/versatile_thermostat/services.yaml index 18f0916d..54367797 100644 --- a/custom_components/versatile_thermostat/services.yaml +++ b/custom_components/versatile_thermostat/services.yaml @@ -161,3 +161,25 @@ set_auto_regulation_mode: - "Strong" - "Slow" - "Expert" + +set_auto_fan_mode: + name: Set Auto Fan mode + description: Change the mode of auto-fan (only for VTherm over climate) + target: + entity: + integration: versatile_thermostat + fields: + auto_fan_mode: + name: Auto fan mode + description: Possible values + required: true + advanced: false + default: true + selector: + select: + options: + - "None" + - "Low" + - "Medium" + - "High" + - "Turbo" diff --git a/custom_components/versatile_thermostat/strings.json b/custom_components/versatile_thermostat/strings.json index 2e0391f5..26294a58 100644 --- a/custom_components/versatile_thermostat/strings.json +++ b/custom_components/versatile_thermostat/strings.json @@ -25,24 +25,25 @@ "title": "Linked entities", "description": "Linked entities attributes", "data": { - "heater_entity_id": "1rst heater switch", + "heater_entity_id": "1st heater switch", "heater_entity2_id": "2nd heater switch", "heater_entity3_id": "3rd heater switch", "heater_entity4_id": "4th heater switch", "proportional_function": "Algorithm", - "climate_entity_id": "1rst underlying climate", + "climate_entity_id": "1st underlying climate", "climate_entity2_id": "2nd underlying climate", "climate_entity3_id": "3rd underlying climate", "climate_entity4_id": "4th underlying climate", "ac_mode": "AC mode", - "valve_entity_id": "1rst valve number", + "valve_entity_id": "1st valve number", "valve_entity2_id": "2nd valve number", "valve_entity3_id": "3rd valve number", "valve_entity4_id": "4th valve number", "auto_regulation_mode": "Self-regulation", "auto_regulation_dtemp": "Regulation threshold", "auto_regulation_periode_min": "Regulation minimal period", - "inverse_switch_command": "Inverse switch command" + "inverse_switch_command": "Inverse switch command", + "auto_fan_mode": " Auto fan mode" }, "data_description": { "heater_entity_id": "Mandatory heater entity id", @@ -55,14 +56,15 @@ "climate_entity3_id": "3rd underlying climate entity id", "climate_entity4_id": "4th underlying climate entity id", "ac_mode": "Use the Air Conditioning (AC) mode", - "valve_entity_id": "1rst valve number entity id", + "valve_entity_id": "1st valve number entity id", "valve_entity2_id": "2nd valve number entity id", "valve_entity3_id": "3rd valve number entity id", "valve_entity4_id": "4th valve number entity id", "auto_regulation_mode": "Auto adjustment of the target temperature", - "auto_regulation_dtemp": "The threshold in ° under which the temperature change will not be send", + "auto_regulation_dtemp": "The threshold in ° under which the temperature change will not be sent", "auto_regulation_periode_min": "Duration in minutes between two regulation update", - "inverse_switch_command": "For switch with pilot wire and diode you may need to inverse the command" + "inverse_switch_command": "For switch with pilot wire and diode you may need to inverse the command", + "auto_fan_mode": " Automatically activate fan when huge heating/cooling is necessary" } }, "tpi": { @@ -75,7 +77,7 @@ }, "presets": { "title": "Presets", - "description": "For each presets, give the target temperature (0 to ignore preset)", + "description": "For each preset set the target temperature (0 to ignore preset)", "data": { "eco_temp": "Temperature in Eco preset", "comfort_temp": "Temperature in Comfort preset", @@ -96,16 +98,16 @@ "window_auto_max_duration": "Maximum duration of automatic window open detection (in min)" }, "data_description": { - "window_sensor_entity_id": "Leave empty if no window sensor should be use", + "window_sensor_entity_id": "Leave empty if no window sensor should be used", "window_delay": "The delay in seconds before sensor detection is taken into account", - "window_auto_open_threshold": "Recommended value: between 3 and 10. Leave empty if automatic window open detection is not use", - "window_auto_close_threshold": "Recommended value: 0. Leave empty if automatic window open detection is not use", - "window_auto_max_duration": "Recommended value: 60 (one hour). Leave empty if automatic window open detection is not use" + "window_auto_open_threshold": "Recommended value: between 3 and 10. Leave empty if automatic window open detection is not used", + "window_auto_close_threshold": "Recommended value: 0. Leave empty if automatic window open detection is not used", + "window_auto_max_duration": "Recommended value: 60 (one hour). Leave empty if automatic window open detection is not used" } }, "motion": { "title": "Motion management", - "description": "Motion sensor management. Preset can switch automatically depending of a motion detection\nLeave corresponding entity_id empty if not used.\nmotion_preset and no_motion_preset should be set to the corresponding preset name", + "description": "Motion sensor management. Preset can switch automatically depending on motion detection\nLeave corresponding entity_id empty if not used.\nmotion_preset and no_motion_preset should be set to the corresponding preset name", "data": { "motion_sensor_entity_id": "Motion sensor entity id", "motion_delay": "Activation delay", @@ -115,7 +117,7 @@ }, "data_description": { "motion_sensor_entity_id": "The entity id of the motion sensor", - "motion_delay": "Motion activation activation delay (seconds)", + "motion_delay": "Motion activation delay (seconds)", "motion_off_delay": "Motion deactivation delay (seconds)", "motion_preset": "Preset to use when motion is detected", "no_motion_preset": "Preset to use when no motion is detected" @@ -145,7 +147,7 @@ }, "advanced": { "title": "Advanced parameters", - "description": "Configuration of advanced parameters. Leave the default values if you don't know what you are doing.\nThis parameters can lead to a very bad temperature or power regulation.", + "description": "Configuration of advanced parameters. Leave the default values if you don't know what you are doing.\nThese parameters can lead to very poor temperature control or bad power regulation.", "data": { "minimal_activation_delay": "Minimal activation delay", "security_delay_min": "Security delay (in minutes)", @@ -154,16 +156,16 @@ }, "data_description": { "minimal_activation_delay": "Delay in seconds under which the equipment will not be activated", - "security_delay_min": "Maximum allowed delay in minutes between two temperature mesures. Above this delay, the thermostat will turn to a security off state", + "security_delay_min": "Maximum allowed delay in minutes between two temperature measurements. Above this delay the thermostat will turn to a security off state", "security_min_on_percent": "Minimal heating percent value for security preset activation. Below this amount of power percent the thermostat won't go into security preset", - "security_default_on_percent": "The default heating power percent value in security preset. Set to 0 to switch off heater in security present" + "security_default_on_percent": "The default heating power percent value in security preset. Set to 0 to switch off heater in security preset" } } }, "error": { "unknown": "Unexpected error", "unknown_entity": "Unknown entity id", - "window_open_detection_method": "Only one window open detection method should be used. Use sensor or automatic detection through temperature threshold but not both" + "window_open_detection_method": "Only one window open detection method should be used. Use either window sensor or automatic detection through temperature threshold but not both" }, "abort": { "already_configured": "Device is already configured" @@ -194,24 +196,25 @@ "title": "Linked entities", "description": "Linked entities attributes", "data": { - "heater_entity_id": "1rst heater switch", + "heater_entity_id": "1st heater switch", "heater_entity2_id": "2nd heater switch", "heater_entity3_id": "3rd heater switch", "heater_entity4_id": "4th heater switch", "proportional_function": "Algorithm", - "climate_entity_id": "1rst underlying climate", + "climate_entity_id": "1st underlying climate", "climate_entity2_id": "2nd underlying climate", "climate_entity3_id": "3rd underlying climate", "climate_entity4_id": "4th underlying climate", "ac_mode": "AC mode", - "valve_entity_id": "1rst valve number", + "valve_entity_id": "1st valve number", "valve_entity2_id": "2nd valve number", "valve_entity3_id": "3rd valve number", "valve_entity4_id": "4th valve number", "auto_regulation_mode": "Self-regulation", "auto_regulation_dtemp": "Regulation threshold", "auto_regulation_periode_min": "Regulation minimal period", - "inverse_switch_command": "Inverse switch command" + "inverse_switch_command": "Inverse switch command", + "auto_fan_mode": " Auto fan mode" }, "data_description": { "heater_entity_id": "Mandatory heater entity id", @@ -224,14 +227,15 @@ "climate_entity3_id": "3rd underlying climate entity id", "climate_entity4_id": "4th underlying climate entity id", "ac_mode": "Use the Air Conditioning (AC) mode", - "valve_entity_id": "1rst valve number entity id", + "valve_entity_id": "1st valve number entity id", "valve_entity2_id": "2nd valve number entity id", "valve_entity3_id": "3rd valve number entity id", "valve_entity4_id": "4th valve number entity id", "auto_regulation_mode": "Auto adjustment of the target temperature", - "auto_regulation_dtemp": "The threshold in ° under which the temperature change will not be send", + "auto_regulation_dtemp": "The threshold in ° under which the temperature change will not be sent", "auto_regulation_periode_min": "Duration in minutes between two regulation update", - "inverse_switch_command": "For switch with pilot wire and diode you may need to inverse the command" + "inverse_switch_command": "For switch with pilot wire and diode you may need to invert the command", + "auto_fan_mode": " Automatically activate fan when huge heating/cooling is necessary" } }, "tpi": { @@ -244,7 +248,7 @@ }, "presets": { "title": "Presets", - "description": "For each presets, give the target temperature (0 to ignore preset)", + "description": "For each preset set the target temperature (0 to ignore preset)", "data": { "eco_temp": "Temperature in Eco preset", "comfort_temp": "Temperature in Comfort preset", @@ -265,11 +269,11 @@ "window_auto_max_duration": "Maximum duration of automatic window open detection (in min)" }, "data_description": { - "window_sensor_entity_id": "Leave empty if no window sensor should be use", + "window_sensor_entity_id": "Leave empty if no window sensor should be used", "window_delay": "The delay in seconds before sensor detection is taken into account", - "window_auto_open_threshold": "Recommended value: between 3 and 10. Leave empty if automatic window open detection is not use", - "window_auto_close_threshold": "Recommended value: 0. Leave empty if automatic window open detection is not use", - "window_auto_max_duration": "Recommended value: 60 (one hour). Leave empty if automatic window open detection is not use" + "window_auto_open_threshold": "Recommended value: between 3 and 10. Leave empty if automatic window open detection is not used", + "window_auto_close_threshold": "Recommended value: 0. Leave empty if automatic window open detection is not used", + "window_auto_max_duration": "Recommended value: 60 (one hour). Leave empty if automatic window open detection is not used" } }, "motion": { @@ -284,7 +288,7 @@ }, "data_description": { "motion_sensor_entity_id": "The entity id of the motion sensor", - "motion_delay": "Motion activation activation delay (seconds)", + "motion_delay": "Motion activation delay (seconds)", "motion_off_delay": "Motion deactivation delay (seconds)", "motion_preset": "Preset to use when motion is detected", "no_motion_preset": "Preset to use when no motion is detected" @@ -303,7 +307,7 @@ "title": "Presence management", "description": "Presence management attributes.\nGives the a presence sensor of your home (true is someone is present).\nThen specify either the preset to use when presence sensor is false or the offset in temperature to apply.\nIf preset is given, the offset will not be used.\nLeave corresponding entity_id empty if not used.", "data": { - "presence_sensor_entity_id": "Presence sensor entity id (true is present)", + "presence_sensor_entity_id": "Presence sensor entity id", "eco_away_temp": "Temperature in Eco preset when no presence", "comfort_away_temp": "Temperature in Comfort preset when no presence", "boost_away_temp": "Temperature in Boost preset when no presence", @@ -314,25 +318,25 @@ }, "advanced": { "title": "Advanced parameters", - "description": "Configuration of advanced parameters. Leave the default values if you don't know what you are doing.\nThis parameters can lead to a very bad temperature or power regulation.", + "description": "Configuration of advanced parameters. Leave the default values if you don't know what you are doing.\nThese parameters can lead to very poor temperature control or bad power regulation.", "data": { "minimal_activation_delay": "Minimal activation delay", "security_delay_min": "Security delay (in minutes)", - "security_min_on_percent": "Minimal power percent for security mode", + "security_min_on_percent": "Minimal power percent to enable security mode", "security_default_on_percent": "Power percent to use in security mode" }, "data_description": { "minimal_activation_delay": "Delay in seconds under which the equipment will not be activated", - "security_delay_min": "Maximum allowed delay in minutes between two temperature mesures. Above this delay, the thermostat will turn to a security off state", + "security_delay_min": "Maximum allowed delay in minutes between two temperature measurements. Above this delay the thermostat will turn to a security off state", "security_min_on_percent": "Minimal heating percent value for security preset activation. Below this amount of power percent the thermostat won't go into security preset", - "security_default_on_percent": "The default heating power percent value in security preset. Set to 0 to switch off heater in security present" + "security_default_on_percent": "The default heating power percent value in security preset. Set to 0 to switch off heater in security preset" } } }, "error": { "unknown": "Unexpected error", "unknown_entity": "Unknown entity id", - "window_open_detection_method": "Only one window open detection method should be used. Use sensor or automatic detection through temperature threshold but not both" + "window_open_detection_method": "Only one window open detection method should be used. Use either window sensor or automatic detection through temperature threshold but not both" }, "abort": { "already_configured": "Device is already configured" @@ -355,6 +359,15 @@ "auto_regulation_expert": "Expert", "auto_regulation_none": "No auto-regulation" } + }, + "auto_fan_mode": { + "options": { + "auto_fan_none": "No auto fan", + "auto_fan_low": "Low", + "auto_fan_medium": "Medium", + "auto_fan_high": "High", + "auto_fan_turbo": "Turbo" + } } }, "entity": { diff --git a/custom_components/versatile_thermostat/thermostat_climate.py b/custom_components/versatile_thermostat/thermostat_climate.py index 5a008fed..cb489564 100644 --- a/custom_components/versatile_thermostat/thermostat_climate.py +++ b/custom_components/versatile_thermostat/thermostat_climate.py @@ -9,7 +9,7 @@ async_track_time_interval, ) -from homeassistant.components.climate import HVACAction, HVACMode +from homeassistant.components.climate import HVACAction, HVACMode, ClimateEntityFeature from .commons import NowClass, round_to_nearest from .base_thermostat import BaseThermostat @@ -31,10 +31,19 @@ CONF_AUTO_REGULATION_EXPERT, CONF_AUTO_REGULATION_DTEMP, CONF_AUTO_REGULATION_PERIOD_MIN, + CONF_AUTO_FAN_MODE, + CONF_AUTO_FAN_NONE, + CONF_AUTO_FAN_LOW, + CONF_AUTO_FAN_MEDIUM, + CONF_AUTO_FAN_HIGH, + CONF_AUTO_FAN_TURBO, RegulationParamSlow, RegulationParamLight, RegulationParamMedium, RegulationParamStrong, + AUTO_FAN_DTEMP_THRESHOLD, + AUTO_FAN_DEACTIVATED_MODES, + UnknownEntity, ) from .vtherm_api import VersatileThermostatAPI @@ -52,6 +61,9 @@ class ThermostatOverClimate(BaseThermostat): _auto_regulation_dtemp: float = None _auto_regulation_period_min: int = None _last_regulation_change: datetime = None + _auto_fan_mode: str = None + _auto_activated_fan_mode: str = None + _auto_deactivated_fan_mode: str = None _entity_component_unrecorded_attributes = ( BaseThermostat._entity_component_unrecorded_attributes.union( @@ -65,6 +77,9 @@ class ThermostatOverClimate(BaseThermostat): "underlying_climate_3", "regulation_accumulated_error", "auto_regulation_mode", + "auto_fan_mode", + "auto_activated_fan_mode", + "auto_deactivated_fan_mode", } ) ) @@ -164,6 +179,41 @@ async def _send_regulated_temperature(self, force=False): self.regulated_target_temp, self._attr_max_temp, self._attr_min_temp ) + async def _send_auto_fan_mode(self): + """Send the fan mode if auto_fan_mode and temperature gap is > threshold""" + if not self._auto_fan_mode or not self._auto_activated_fan_mode: + return + + dtemp = ( + self.regulated_target_temp if self.is_regulated else self.target_temperature + ) + if dtemp is None or self.current_temperature is None: + return + + dtemp = dtemp - self.current_temperature + should_activate_auto_fan = ( + dtemp >= AUTO_FAN_DTEMP_THRESHOLD or dtemp <= -AUTO_FAN_DTEMP_THRESHOLD + ) + if should_activate_auto_fan and self.fan_mode != self._auto_activated_fan_mode: + _LOGGER.info( + "%s - Activate the auto fan mode with %s because delta temp is %.2f", + self, + self._auto_fan_mode, + dtemp, + ) + await self.async_set_fan_mode(self._auto_activated_fan_mode) + if ( + not should_activate_auto_fan + and self.fan_mode not in AUTO_FAN_DEACTIVATED_MODES + ): + _LOGGER.info( + "%s - DeActivate the auto fan mode with %s because delta temp is %.2f", + self, + self._auto_deactivated_fan_mode, + dtemp, + ) + await self.async_set_fan_mode(self._auto_deactivated_fan_mode) + @overrides def post_init(self, entry_infos): """Initialize the Thermostat""" @@ -201,6 +251,12 @@ def post_init(self, entry_infos): else 5 ) + self._auto_fan_mode = ( + entry_infos.get(CONF_AUTO_FAN_MODE) + if entry_infos.get(CONF_AUTO_FAN_MODE) is not None + else CONF_AUTO_FAN_NONE + ) + def choose_auto_regulation_mode(self, auto_regulation_mode): """Choose or change the regulation mode""" self._auto_regulation_mode = auto_regulation_mode @@ -277,6 +333,47 @@ def choose_auto_regulation_mode(self, auto_regulation_mode): self.target_temperature, 0, 0, 0, 0, 0.1, 0 ) + def choose_auto_fan_mode(self, auto_fan_mode): + """Choose the correct fan mode depending of the underlying capacities and the configuration""" + + # Get the supported feature of the first underlying. We suppose each underlying have the same fan attributes + fan_supported = self.supported_features & ClimateEntityFeature.FAN_MODE > 0 + + if auto_fan_mode == CONF_AUTO_FAN_NONE or not fan_supported: + self._auto_activated_fan_mode = self._auto_deactivated_fan_mode = None + return + + def find_fan_mode(fan_modes, fan_mode) -> str: + """Return the fan_mode if it exist of None if not""" + try: + return fan_mode if fan_modes.index(fan_mode) >= 0 else None + except ValueError: + return None + + fan_modes = self.fan_modes + if auto_fan_mode == CONF_AUTO_FAN_LOW: + self._auto_activated_fan_mode = find_fan_mode(fan_modes, "low") + elif auto_fan_mode == CONF_AUTO_FAN_MEDIUM: + self._auto_activated_fan_mode = find_fan_mode(fan_modes, "mid") + elif auto_fan_mode == CONF_AUTO_FAN_HIGH: + self._auto_activated_fan_mode = find_fan_mode(fan_modes, "high") + elif auto_fan_mode == CONF_AUTO_FAN_TURBO: + self._auto_activated_fan_mode = find_fan_mode( + fan_modes, "turbo" + ) or find_fan_mode(fan_modes, "high") + + for val in AUTO_FAN_DEACTIVATED_MODES: + if find_fan_mode(fan_modes, val): + self._auto_deactivated_fan_mode = val + break + + _LOGGER.info( + "%s - choose_auto_fan_mode founds auto_activated_fan_mode=%s and auto_deactivated_fan_mode=%s", + self, + self._auto_activated_fan_mode, + self._auto_deactivated_fan_mode, + ) + @overrides async def async_added_to_hass(self): """Run when entity about to be added.""" @@ -302,6 +399,9 @@ async def async_added_to_hass(self): ) ) + # init auto_regulation_mode + self.choose_auto_regulation_mode(self._auto_regulation_mode) + @overrides def restore_specific_previous_state(self, old_state): """Restore my specific attributes from previous state""" @@ -348,6 +448,14 @@ def update_custom_attributes(self): "regulation_accumulated_error" ] = self._regulation_algo.accumulated_error + self._attr_extra_state_attributes["auto_fan_mode"] = self.auto_fan_mode + self._attr_extra_state_attributes[ + "auto_activated_fan_mode" + ] = self._auto_activated_fan_mode + self._attr_extra_state_attributes[ + "auto_deactivated_fan_mode" + ] = self._auto_deactivated_fan_mode + self.async_write_ha_state() _LOGGER.debug( "%s - Calling update_custom_attributes: %s", @@ -435,6 +543,12 @@ async def end_climate_changed(changes): else None ) + new_fan_mode = ( + new_state.attributes.get("fan_mode") + if new_state and new_state.attributes + else None + ) + old_state_date_changed = ( old_state.last_changed if old_state and old_state.last_changed else None ) @@ -545,6 +659,11 @@ async def end_climate_changed(changes): for under in self._underlyings: await under.set_hvac_mode(new_hvac_mode) + # A quick win to known if it has change by using the self._attr_fan_mode and not only underlying[0].fan_mode + if new_fan_mode != self._attr_fan_mode: + self._attr_fan_mode = new_fan_mode + changes = True + if not changes: # try to manage new target temperature set if state _LOGGER.debug( @@ -576,6 +695,9 @@ async def async_control_heating(self, force=False, _=None): await self._send_regulated_temperature() + if self._auto_fan_mode and self._auto_fan_mode != CONF_AUTO_FAN_NONE: + await self._send_auto_fan_mode() + return ret @property @@ -583,6 +705,11 @@ def auto_regulation_mode(self): """Get the regulation mode""" return self._auto_regulation_mode + @property + def auto_fan_mode(self): + """Get the auto fan mode""" + return self._auto_fan_mode + @property def regulated_target_temp(self): """Get the regulated target temperature""" @@ -613,7 +740,8 @@ def fan_mode(self) -> str | None: Requires ClimateEntityFeature.FAN_MODE. """ if self.underlying_entity(0): - return self.underlying_entity(0).fan_mode + self._attr_fan_mode = self.underlying_entity(0).fan_mode + return self._attr_fan_mode return None @@ -707,6 +835,31 @@ def is_aux_heat(self) -> bool | None: return None + @property + def is_initialized(self) -> bool: + """Check if all underlyings are initialized""" + for under in self._underlyings: + if not under.is_initialized: + return False + return True + + @overrides + def init_underlyings(self): + """Init the underlyings if not already done""" + for under in self._underlyings: + if not under.is_initialized: + _LOGGER.info( + "%s - Underlying %s is not initialized. Try to initialize it", + self, + under.entity_id, + ) + try: + under.startup() + except UnknownEntity: + # still not found, we an stop here + return False + self.choose_auto_fan_mode(self._auto_fan_mode) + @overrides def turn_aux_heat_on(self) -> None: """Turn auxiliary heater on.""" @@ -795,3 +948,30 @@ async def service_set_auto_regulation_mode(self, auto_regulation_mode): await self._send_regulated_temperature() self.update_custom_attributes() + + async def service_set_auto_fan_mode(self, auto_fan_mode): + """Called by a service call: + service: versatile_thermostat.set_auto_fan_mode + data: + auto_fan_mode: [None | Low | Medium | High | Turbo] + target: + entity_id: climate.thermostat_1 + """ + _LOGGER.info( + "%s - Calling service_set_auto_fan_mode, auto_fan_mode: %s", + self, + auto_fan_mode, + ) + if auto_fan_mode == "None": + self.choose_auto_fan_mode(CONF_AUTO_FAN_NONE) + elif auto_fan_mode == "Low": + self.choose_auto_fan_mode(CONF_AUTO_FAN_LOW) + elif auto_fan_mode == "Medium": + self.choose_auto_fan_mode(CONF_AUTO_FAN_MEDIUM) + elif auto_fan_mode == "High": + self.choose_auto_fan_mode(CONF_AUTO_FAN_HIGH) + elif auto_fan_mode == "Turbo": + self.choose_auto_fan_mode(CONF_AUTO_FAN_TURBO) + + await self._send_regulated_temperature() + self.update_custom_attributes() diff --git a/custom_components/versatile_thermostat/translations/el.json b/custom_components/versatile_thermostat/translations/el.json index de4676c6..aa62c3d4 100644 --- a/custom_components/versatile_thermostat/translations/el.json +++ b/custom_components/versatile_thermostat/translations/el.json @@ -42,7 +42,8 @@ "auto_regulation_mode": "Αυτόματη ρύθμιση", "auto_regulation_dtemp": "Όριο ρύθμισης", "auto_regulation_periode_min": "Ελάχιστη περίοδος ρύθμισης", - "inverse_switch_command": "Αντίστροφη εντολή διακόπτη" + "inverse_switch_command": "Αντίστροφη εντολή διακόπτη", + "auto_fan_mode": " Auto fan mode" }, "data_description": { "heater_entity_id": "Υποχρεωτική ταυτότητα οντότητας θερμαντήρα", @@ -62,7 +63,8 @@ "auto_regulation_mode": "Αυτόματη προσαρμογή της στοχευμένης θερμοκρασίας", "auto_regulation_dtemp": "Το όριο σε ° κάτω από το οποίο η αλλαγή θερμοκρασίας δεν θα αποστέλλεται", "auto_regulation_periode_min": "Διάρκεια σε λεπτά μεταξύ δύο ενημερώσεων ρύθμισης", - "inverse_switch_command": "Για διακόπτη με πιλοτικό καλώδιο και δίοδο μπορεί να χρειαστεί να αντιστρέψετε την εντολή" + "inverse_switch_command": "Για διακόπτη με πιλοτικό καλώδιο και δίοδο μπορεί να χρειαστεί να αντιστρέψετε την εντολή", + "auto_fan_mode": " Automatically activate fan when huge heating/cooling is necessary" } }, "tpi": { @@ -211,7 +213,8 @@ "auto_regulation_mode": "Αυτορύθμιση", "auto_regulation_dtemp": "Όριο ρύθμισης", "auto_regulation_periode_min": "Ελάχιστη περίοδος ρύθμισης", - "inverse_switch_command": "Αντίστροφη εντολή διακόπτη" + "inverse_switch_command": "Αντίστροφη εντολή διακόπτη", + "auto_fan_mode": " Auto fan mode" }, "data_description": { "heater_entity_id": "Υποχρεωτική ταυτότητα οντότητας θερμαντήρα", @@ -231,7 +234,8 @@ "auto_regulation_mode": "Αυτόματη ρύθμιση της στοχευόμενης θερμοκρασίας", "auto_regulation_dtemp": "Το κατώφλι σε °C κάτω από το οποίο η αλλαγή της θερμοκρασίας δεν θα αποστέλλεται", "auto_regulation_periode_min": "Διάρκεια σε λεπτά μεταξύ δύο ενημερώσεων ρύθμισης", - "inverse_switch_command": "Για διακόπτες με πιλοτικό καλώδιο και δίοδο μπορεί να χρειαστεί να αντιστραφεί η εντολή" + "inverse_switch_command": "Για διακόπτες με πιλοτικό καλώδιο και δίοδο μπορεί να χρειαστεί να αντιστραφεί η εντολή", + "auto_fan_mode": " Automatically activate fan when huge heating/cooling is necessary" } }, "tpi": { @@ -355,6 +359,15 @@ "auto_regulation_expert": "Εμπειρογνώμων", "auto_regulation_none": "Χωρίς αυτόματη ρύθμιση" } + }, + "auto_fan_mode": { + "options": { + "auto_fan_none": "No auto fan", + "auto_fan_low": "Low", + "auto_fan_medium": "Medium", + "auto_fan_high": "High", + "auto_fan_turbo": "Turbo" + } } }, "entity": { diff --git a/custom_components/versatile_thermostat/translations/en.json b/custom_components/versatile_thermostat/translations/en.json index 633260ab..26294a58 100644 --- a/custom_components/versatile_thermostat/translations/en.json +++ b/custom_components/versatile_thermostat/translations/en.json @@ -42,7 +42,8 @@ "auto_regulation_mode": "Self-regulation", "auto_regulation_dtemp": "Regulation threshold", "auto_regulation_periode_min": "Regulation minimal period", - "inverse_switch_command": "Inverse switch command" + "inverse_switch_command": "Inverse switch command", + "auto_fan_mode": " Auto fan mode" }, "data_description": { "heater_entity_id": "Mandatory heater entity id", @@ -62,7 +63,8 @@ "auto_regulation_mode": "Auto adjustment of the target temperature", "auto_regulation_dtemp": "The threshold in ° under which the temperature change will not be sent", "auto_regulation_periode_min": "Duration in minutes between two regulation update", - "inverse_switch_command": "For switch with pilot wire and diode you may need to invert the command" + "inverse_switch_command": "For switch with pilot wire and diode you may need to inverse the command", + "auto_fan_mode": " Automatically activate fan when huge heating/cooling is necessary" } }, "tpi": { @@ -211,7 +213,8 @@ "auto_regulation_mode": "Self-regulation", "auto_regulation_dtemp": "Regulation threshold", "auto_regulation_periode_min": "Regulation minimal period", - "inverse_switch_command": "Inverse switch command" + "inverse_switch_command": "Inverse switch command", + "auto_fan_mode": " Auto fan mode" }, "data_description": { "heater_entity_id": "Mandatory heater entity id", @@ -231,7 +234,8 @@ "auto_regulation_mode": "Auto adjustment of the target temperature", "auto_regulation_dtemp": "The threshold in ° under which the temperature change will not be sent", "auto_regulation_periode_min": "Duration in minutes between two regulation update", - "inverse_switch_command": "For switch with pilot wire and diode you may need to invert the command" + "inverse_switch_command": "For switch with pilot wire and diode you may need to invert the command", + "auto_fan_mode": " Automatically activate fan when huge heating/cooling is necessary" } }, "tpi": { @@ -355,6 +359,15 @@ "auto_regulation_expert": "Expert", "auto_regulation_none": "No auto-regulation" } + }, + "auto_fan_mode": { + "options": { + "auto_fan_none": "No auto fan", + "auto_fan_low": "Low", + "auto_fan_medium": "Medium", + "auto_fan_high": "High", + "auto_fan_turbo": "Turbo" + } } }, "entity": { diff --git a/custom_components/versatile_thermostat/translations/fr.json b/custom_components/versatile_thermostat/translations/fr.json index 777f3f3c..e9c6263f 100644 --- a/custom_components/versatile_thermostat/translations/fr.json +++ b/custom_components/versatile_thermostat/translations/fr.json @@ -42,7 +42,8 @@ "auto_regulation_mode": "Auto-régulation", "auto_regulation_dtemp": "Seuil de régulation", "auto_regulation_periode_min": "Période minimale de régulation", - "inverse_switch_command": "Inverser la commande" + "inverse_switch_command": "Inverser la commande", + "auto_fan_mode": " Auto ventilation mode" }, "data_description": { "heater_entity_id": "Entity id du 1er radiateur obligatoire", @@ -62,7 +63,8 @@ "auto_regulation_mode": "Ajustement automatique de la température cible", "auto_regulation_dtemp": "Le seuil en ° au-dessous duquel la régulation ne sera pas envoyée", "auto_regulation_periode_min": "La durée en minutes entre deux mise à jour faites par la régulation", - "inverse_switch_command": "Inverse la commande du switch pour une installation avec fil pilote et diode" + "inverse_switch_command": "Inverse la commande du switch pour une installation avec fil pilote et diode", + "auto_fan_mode": "Active la ventilation automatiquement en cas d'écart important" } }, "tpi": { @@ -212,7 +214,8 @@ "auto_regulation_mode": "Auto-regulation", "auto_regulation_dtemp": "Seuil de régulation", "auto_regulation_periode_min": "Période minimale de régulation", - "inverse_switch_command": "Inverser la commande" + "inverse_switch_command": "Inverser la commande", + "auto_fan_mode": " Auto fan mode" }, "data_description": { "heater_entity_id": "Entity id du 1er radiateur obligatoire", @@ -232,7 +235,8 @@ "auto_regulation_mode": "Ajustement automatique de la consigne", "auto_regulation_dtemp": "Le seuil en ° au-dessous duquel la régulation ne sera pas envoyée", "auto_regulation_periode_min": "La durée en minutes entre deux mise à jour faites par la régulation", - "inverse_switch_command": "Inverse la commande du switch pour une installation avec fil pilote et diode" + "inverse_switch_command": "Inverse la commande du switch pour une installation avec fil pilote et diode", + "auto_fan_mode": "Active la ventilation automatiquement en cas d'écart important" } }, "tpi": { @@ -356,6 +360,15 @@ "auto_regulation_expert": "Expert", "auto_regulation_none": "Aucune" } + }, + "auto_fan_mode": { + "options": { + "auto_fan_none": "Pas d'auto fan", + "auto_fan_low": "Faible", + "auto_fan_medium": "Moyenne", + "auto_fan_high": "Forte", + "auto_fan_turbo": "Turbo" + } } }, "entity": { diff --git a/custom_components/versatile_thermostat/translations/it.json b/custom_components/versatile_thermostat/translations/it.json index 43c9a66e..12608d43 100644 --- a/custom_components/versatile_thermostat/translations/it.json +++ b/custom_components/versatile_thermostat/translations/it.json @@ -40,7 +40,8 @@ "valve_entity3_id": "Terza valvola", "valve_entity4_id": "Quarta valvola", "auto_regulation_mode": "Autoregolamentazione", - "inverse_switch_command": "Comando inverso" + "inverse_switch_command": "Comando inverso", + "auto_fan_mode": " Auto fan mode" }, "data_description": { "heater_entity_id": "Entity id obbligatoria del primo riscaldatore", @@ -58,7 +59,8 @@ "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" + "inverse_switch_command": "Inverte il controllo dell'interruttore per un'installazione con filo pilota e diodo", + "auto_fan_mode": " Automatically activate fan when huge heating/cooling is necessary" } }, "tpi": { @@ -198,7 +200,8 @@ "valve_entity3_id": "Terza valvola", "valve_entity4_id": "Quarta valvola", "auto_regulation_mode": "Autoregolamentazione", - "inverse_switch_command": "Comando inverso" + "inverse_switch_command": "Comando inverso", + "auto_fan_mode": " Auto fan mode" }, "data_description": { "heater_entity_id": "Entity id obbligatoria del primo riscaldatore", @@ -216,7 +219,8 @@ "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" + "inverse_switch_command": "Inverte il controllo dell'interruttore per un'installazione con filo pilota e diodo", + "auto_fan_mode": " Automatically activate fan when huge heating/cooling is necessary" } }, "tpi": { @@ -333,6 +337,15 @@ "auto_regulation_expert": "Esperto", "auto_regulation_none": "Nessuna autoregolamentazione" } + }, + "auto_fan_mode": { + "options": { + "auto_fan_none": "Nessune autofan", + "auto_fan_low": "Leggera", + "auto_fan_medium": "Media", + "auto_fan_high": "Forte", + "auto_fan_turbo": "Turbo" + } } }, "entity": { diff --git a/custom_components/versatile_thermostat/translations/sk.json b/custom_components/versatile_thermostat/translations/sk.json index 3dc7415b..154aec6c 100644 --- a/custom_components/versatile_thermostat/translations/sk.json +++ b/custom_components/versatile_thermostat/translations/sk.json @@ -42,7 +42,8 @@ "auto_regulation_mode": "Self-regulation", "auto_regulation_dtemp": "Regulation threshold", "auto_regulation_periode_min": "Regulation minimal period", - "inverse_switch_command": "Inverse switch command" + "inverse_switch_command": "Inverse switch command", + "auto_fan_mode": " Auto fan mode" }, "data_description": { "heater_entity_id": "ID entity povinného ohrievača", @@ -62,7 +63,8 @@ "auto_regulation_mode": "Auto adjustment of the target temperature", "auto_regulation_dtemp": "The threshold in ° under which the temperature change will not be send", "auto_regulation_periode_min": "Duration in minutes between two regulation update", - "inverse_switch_command": "For switch with pilot wire and diode you may need to inverse the command" + "inverse_switch_command": "For switch with pilot wire and diode you may need to inverse the command", + "auto_fan_mode": " Automatically activate fan when huge heating/cooling is necessary" } }, "tpi": { @@ -211,7 +213,8 @@ "auto_regulation_mode": "Self-regulation", "auto_regulation_dtemp": "Regulation threshold", "auto_regulation_periode_min": "Regulation minimal period", - "inverse_switch_command": "Inverse switch command" + "inverse_switch_command": "Inverse switch command", + "auto_fan_mode": " Auto fan mode" }, "data_description": { "heater_entity_id": "ID entity povinného ohrievača", @@ -231,7 +234,8 @@ "auto_regulation_mode": "Auto adjustment of the target temperature", "auto_regulation_dtemp": "The threshold in ° under which the temperature change will not be send", "auto_regulation_periode_min": "Duration in minutes between two regulation update", - "inverse_switch_command": "For switch with pilot wire and diode you may need to inverse the command" + "inverse_switch_command": "For switch with pilot wire and diode you may need to inverse the command", + "auto_fan_mode": " Automatically activate fan when huge heating/cooling is necessary" } }, "tpi": { @@ -355,6 +359,15 @@ "auto_regulation_expert": "Expert", "auto_regulation_none": "No auto-regulation" } + }, + "auto_fan_mode": { + "options": { + "auto_fan_none": "No auto-fan", + "auto_fan_low": "Low", + "auto_fan_medium": "Medium", + "auto_fan_high": "High", + "auto_fan_turbo": "Turbo" + } } }, "entity": { diff --git a/images/config-linked-entity2.png b/images/config-linked-entity2.png index 0aecb11ef773fd7105f0a9ff39391df57e67adba..2c4e45165ef3a3d584f430e52ed23226b5e1d3b1 100644 GIT binary patch literal 41049 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 literal 47449 zcmc$`byQSe|27Obz>L%Y(gH(B2r`5M!q7+~peQI^BGS^`B`qyoih@!~cOytjcXxNc zd%pMm`~B|qK5M<}{pWce*J8P6&W>~TIcI;a>-t=KLR6p165OV~je&tdATKBV39C9EiEOWEc%9MZtdn-z~J z=iT&8Akvp4ei%&sYGUv0{Y2UbN1Ah-KjHm?VF+C{55evO|4A6ce%N(TX}(@hevz z!`h?cEDO;o+Rekb-ft&875f4=Mw^}L5~Ln+w8Qq<&@zU(-Pda6&N>zCQQ7|GoKFRu zN9LvdPBxM>nVH@1Pv)BZ&Ds4`#JcxK#cLsV^24#MpF=HETzdEi$KS`Kt)c1i?hwiZ ztLxtLjWKxFs)c^kJ>@_~F5mgpMLC#CjI7a}$RZ8hgZWaS>nWYVt_TSPiyo~&?O%e} zrwwP5hd8~N7np<^4nwN-)W2TDHf8lr%tny>v`l{ELO4F9TFv|CUFSn{w?0=q**K@S zmnPv-^NF5ds$ZID%D+)o#<&m4Pz=l8z7u5Ev)Q>oJAP_sUZZ)=&zBCh<~csn~N}NDyt%-tlyg;__?^b zxEV!nBM=DGd*e4k&!lDk-5h)pVSHzc)qlJbwHb$<2%8<$VBZ zJg{@IvNv#kU}eYjuTK7}A88XiqxWVu_GZ>r2z0*&hSm=DB8-gaf&Tm7zs}Rd+3bHt zvao202MY05uk7izKnC z7}-Vp356Ol4ZrFg>26kEESx9e@4O+B-u7}tpt{VsSpB^VPdiq-9+8u2yPXubp6axf zl%z3-?@qm*rr$;EdSdf63a8lc8DH)*t;aeCSjwOj$fH%10Ygd*C+s^w(8>lwsFv~rG!jY=~G-um@ z&UQDNI|n!i$u*6}H<_k7#6yb;BC8H5KvQ}wK?j@UNGm3h#t&p1LZT!L2#CLU8RKmB zBp*s*R1K2&y_u^ToCTB-V`Pa*q8^8mGsokfO8iVwkq}#=dy?W%sE@WJA`;@^o}$>+ z^%8y5I~w7nc$rEf@vlp`2ML3a;j9cq{d3e8no>vcOjj%bk1-3~*ZbR;OpPfenr*i+ z!6z(e?8s|<41z82DasCf*zvWq>NY6?mx>A5r;CQ=m}x+v;20k?7-IJ(&m<2VQ3K^{ zRkcr`_}I@|j@vZwbs%UD4*=!cWVx_^S7!?Rgxm2vt|?ae*PoA^3E%3z#&gK)43)vz z2q4eWw{kO;*$?}(p5Tpq*Ame9^=_zR8i#;-(ewHsU3U4&y=k|dTC0vI2DMkwX0*bN z_N%It>~bsW%){CxI#Z7_A!TV&V6Ta=*78nGLikr=iA%e&|dnrwSc~ z38*-%4YW$U^Sav5o7!LMdj2ki-_Y5auASN_tk>0dA!DO~{(Lv|yT@y} zVY=3>D{$amfywI|shiExUdIt_yKt>~&)V7{_JWHVmxIamq5S4f`N;a+0Axk3V#0DX zPQcz`TO|EXn%DIi>e<#zeLankeV*H5wCtqMEnLQl%VmN3HS!0qjp?GjIu(j$1?u() zPWHQy)hlUkT!|_6H)E#h4kE6HHb1YDdfk`t4V7P?*}sW>&?tnxc9%2xt=DLg!FGyW zkL`%A+xLljFRv`avjHMnA>+mC-DaX{VkVgifoW$Zy*k3*gjjI;6%2~TB5nRapDqnE}>(jjVP zrzO~Hn_Xl-TeIDklT0V$A86ga{|V`}HU4WE`|R7s20P?hY9w;A)b|BN#CSj7qSKQL$%Zl6fuw|p(w6_G4pz0FmB`Cj1N zUp9^Wm*4q*&ssXI|LwF9VR{au3Kvhr*gLl&@jM?)ALo~Ioh!+;)VzQ7<=JO8*MwkJ z%_v5rwlG@_J&#{*^C7I9?ktDC-I~anv*qMJMh4G}9x``S-4VFsd0d)4X8dMr&s^`~ z)5WH#n7Qs$dHt0o)2;KX%M~%}xzh*blZNkh8RO`0PTQB)z>Cq#a^90FJau!~dqcUV z24&15dKy&(pC_5m#zk5MIzM*F>n)~0J+G0oGPD>iYGkFTAQ<1D3!;C0rf&Zk;eXRi zf894t;BmOB)*Pium*QU0+q(4k>vOBt4`gTdX5&A<4q^Egy`1~yv zug-eC%3P!3S;a{-SNvwLFY0e5blmq^x7%o}e|Glp2q9KOIJL^U%dSI{$z#2?evNDf z5*<|*H|ZDA-+4;clCIiV)3p61%MPq1lSH1ow*>5#m9%gw4hIyczzoVZ&tD$&_HGQs z@IokJk)C^@KO8oOx1Xn<$7XX$d!6nYFrCb%zZ1_G4Qyv)7lt}Z?oqqVBRmFjBJq;0>ey^qUU)o1?IyA z#YQ*3EGPIUEQbq{RPGP(zer{9q|-b5W9=DJetn)$f7uGPovg6k9@5bJb)fLZLu@G*$hCOlnfHJD^i={hMnpnPXyG6A5vRT#Gm4)M$uza`B6wlK*jq|jfBBQ^@N0>Q6JZK z+j^>MsA?rpw#4*?-*Vh|3F;+aJ)NRrLf5v8Lr6a?^onnei#v_WOc%Pq@N6bt1-S|C zAbXHgTR5{9Df1+Se?-uF^SRA+Qve<~#q%X8Z9A|t#^z~@|{2M(chs{cu%9`bN_Ge+LFch1<7wF{3FVN#1L zr@K2!IB@uakr_HC4f9e`)gurCl~LDpWLL>ig$s%SQ6^=G+y#@Qq@S zq;qWBm`kcedaNnRc*h#d*I#T`N60Fsd7hhnR64cf^*Av29(MF-pn|C@JRn2d_tI04 zmhSm|p#uwg|Mt?5Nt2+(3N>E=Sr6oFYpOA-ZC1z7Q?XlcFSy&XeCmxu4=GVWL+x!bEDcv z>aUO7rAZ6){$3diUGP`#H5Q6Auqt>0ct!L0W|39wF%ylid0&Q)%jmU?S`nNmSIQst zjG0{v>TC#mx7FO8hw5k}eOmO?Y%`mvMd?ng+%xu=Jgbt!pVy&EB@rKD+86!*X0e!6 z=sHYeKbvb7lv zV3;HeBlL4LVt?h;6KEt*>Jb#Wnna?}Tnb1L$R#h);(S1|+1=8mg@BTq>y!E`gRjehPWUZ6MzGRUrLNE=)5y|g9N70clmp6RvxR;=9D2B<*Ngzg(q;NIBCKcRs z;G2GZ_+@KE;F|c8vByCl|DtU0-5R{Kf{1-yA|GjKN@nqzdO;Ys)n5f;Wp1nN594C2 z_#0l6-109@nQ)u*t;UT@;3NxE?+LF){4VmK`R{^Yt6sH7w%&YWn*#OMK9 zv%LVVb8nXmk^bKp!Y7PHLibmQ{pM)D0|0^QKAENGM;q04J&##)Jzc&E@9ZcuTaN#< zC1p{Pi`{x%ZnfpS#o8ns=4TC6)MRhPk2ETo2k3a43b|o5Q&(G?|5&ZIX;hj<&2_iw zw9ey#Wt~w^$og=>qd)Hci{A-!sMFsXb@TVt^WaJYb#S>4;^Xo~3UT+h^={4+Xi&~J zbrH$@mg}yEtI-MMqCH)w&MR4=T<>R2+XOa)NAwBlMN!&04Mxy{cSGN>;}z92OuFK? zKbilKk4;5hwToRBCyRM`*8QBtL>jL5W%||5iX?8IxE-x8wS`d`1(~LMI7Z9UU;4Gs zw(z8^TznRr{zTzU%WBQE5O82!clH_S*jC2PkA1XDuov-m+B{`_17%$?hvR1QE=%Lv z!1jD==oAR@CNlwLk{RNVfv3JqGGRp&v61v*qIxSJ?Aj*idAjJnJ{3eRETmHCx%W>2 zls300Ny|s&JY+1d2*}jkod;=;?cNOhxZVvC8#PDnz5NI~Q$t3oylh{Q;H=^4RfMT!87Gb8ajp;H~^-M2FziY$ZG8@7HxAub911Fu(7ozl>YQ*Fk0 zeN0@`-o?yK9gVPKLB3{jmR6Ph*B1+|%hvT*Wm=W*b7@J(r(IT3w`0;TbFymzdJ@vT z-FxA=_crNJqA*k;RoKZO`3ixEz0={ry;-Yd_DgUVH@GyU3C|e)x)*H@1hKQPIU*ggEs)L zI@Jgb#IUbW7&w00i73N4iYtZY43Sy@bZ9LyH7C2^EHVr zN+*>3L0FAd)fx+4Bed7Ow&v;6oyqrI4`edZZ!T9X4bM|<&d=ghelpu(-UH}|Z z*~L}et>d=!Yk7=-R_JfZS5o(oqpT;Po7mgV%J6G2e=Es6SM2E@m^Wh1r)k=lmRMvA zO!_h;-r&-4Oj`nSx`jhP!6(PZhDX6=KXX1@ezyf?;CVV1mos?) zamNu~`dL^sE9o`BzK;r#a{iMqE?2W7IX7>YE3>s3YyEl`XHtCB;3 zjiDS>Vgrgp_+!SfuXX9mw4LGHgzGKX>wt_sL?$P5bR{62>Q7sd1s33>70!RZf($If zj(28ehK)7xj75Ta(WDh+9Tg)?=@}S_N9_ZYy2n)l6Au(SwpY<@1WmF*Z<{N)ooqWyetqJ*RZJcr;9IVcS8vo#C_5Tb6hZFp9~4Dc zxL({&XQv`^6uV}Qt>QITeC8mK9{)DEpjX=A;~aHQvw8bxD!w;~%Cp#O8T97m*PlrQ zi$#M^9MAyhN%-FGGs@(=m<}#3#)5#XTa*iG7kDz1MqF>TjWr8f`3Pii0*2ZrGA*qb zqxeZ$stq3?+3uFU4IgP^d?YO)7vLZ1ca(Ju3)quMbKjR2FzvZp?IQk|r7<@_cir=1 zVrD!CI-Dxx@NJPrp?<0?ol6S~nXZ~m)`I*sKrH1oL3FMi z!~IWF1Pxx83CsERq7K5U0igVjN*g3qK}C;X8G-G|$ZO)(D0IZbg-5x}Ur)zm(ry>MS8E`B zP~d;TnJ1dYW=wi-4z+2*>3h1DM@lCkP*S6cld?xyd#9acT+UcSU8v=B%i3$g%K7ZN znR20Vp6Y#RyIzj_E6V`!J6L81kZfwAFD}UDFF}zWt|gXtFrSGE`uH8aQrw5g36ToQ zsW+B*rgz2!W3ZDB`&9R#2wSLMsQN-Wbj-J@7aYi*GO-2*UR?F z(`Lj%06=eO{8cHmU!ny>T7b*)NF@M5OOXKxZLk@Mt^tRw-$l#TUjTwzKHTkc2GCC0 zR|ppk{mek=vB)K_E@&K`(Sz25-%|tB_mrqU4CvLMJ3F-8{J*igyu_za=P%s!A!unm z<00`Wc%1L1XUUDDB#8CZW;JNRDGm6D+n%z1XHFzE`WyI9JvQi_N4J$}VB}BCh5$Mh zhqicA8Jt!A5f-;$>0~eUXNg!Y^&|w}SLAd~w zjRN#i@qmCWfMs^oDrXS>Rj0ZvU;XPx4vilv!Y=zc!j7AX`L9Yp(^`YbSoDXi(*10I z>3c6J>r?46pNPdwBYF9uZ!f04KL0Wa7P*1pCjqy~3X}^4JoIJN(hF3Q4d2N@jln5= z)CZAJ`|A8iCtcKI62PmKAC&|6(UG|}l%IfJawlrtoQyq% zNX&=6nf%F#fe72L>L&uQIyIE9(TkR<(bC;_t@56M!OZe;IF<>m&+RWFh1CNF(rRhL zZ{BZ@7B@Qpby4JEv$Sj`N1k-#hmP<_wUae2Pk~j#y zAY;d)6?vvo^m+~Q6jgtc1+=DkjM^$Zug)u*gNSINCF;8XA9 z37u+32lVoamg_;ia?$=)w@|d?3?NM%8b=-ZUHx?FdAySNCBPDZavQ}2u1B=zCp$a* zR+DcX`{@_!%?3ZCc1Wthn!7EqBX74(VAdF&?>G1ge1xLQ_9m^P_@L7MA2J1+5Dri4B^ zd>$_0s+f3rgAnt?3Km{CO5E)w1q)HjQwq=%*pMXl{Ew12mZ#~m-T+>nW+ zvz(sjLe^Ud6&p7MEW~N(7MDal-db3y-EG2?iQlZhxxTO(|7m8m(v!N~mnmfh9>te9 zsp!nmC9aQV^PIGwx9P2HslX_8AgIwYaO8+no9ieU(+f$mEzZ5zH)vXbNj@zI0)(mS zcgeDhRwxD1(b>;g)A===R8n*+2De{A_mGU--Y*Ikb45)fEz{w?J1bER6wMlbKZky& zuJ{Lpk#Vk3I$+iwTn1gfO|U=ozU;%K+2v>8t0^DR^O_n~W{~V4qRI2?03TpVQX|SF1PTRn7`a&Y+zp|jM9K#;@E$j-hJ1H=`iAB#Ek~w#=_MCU3_!&@=4QkmkRQ<&B zl6lTv1?&U-4ELn{G;ou|-^6}V4)#Me84u$#&_P>HJX*;#H1p&|&Y3LGZ{HuqmgH0( zqd*z~tCU|$4or*SEh+eg*^h(Gxk{SsPIc~U@oI*tWd(=C^1Dv;@S*?ewEKZFlYmV& z0ZPyRm)I~{e|VH8$nZ5JL1Tq$tsK_77n{CtS1P{VZA&~1APWvtDYB|~5WxT!jWwZ( zwQ<>RGW#p#>+swRZ-nJ8M{b43+gj=esrBEmz55ied|?BpoUs8xJx+mT z%BcETZdd83lqmo%+|T!6%pkJ>j+GQD-Zn$oslA$$1n+nN0ST7)K7Bedj(kQE&U_}; z+5mBOMvP8dJKhga`D(yXm=2om??VFhW}f~e7-3&C3TVC;UIco7<&Cj1gfhKN%!oSk z7WdAHUK!+MYNQarWL5rT+BQ`ddh}gIx;q#g`ME6 z6(3C-e1kymcbg2?yKFx?RrRWZMzv4h<|qzozRUyf-j0(O(C;RJ%8%!!-a@li+3V5c zJwG>x#$K#nEdR-R@qeL@cv`-jylnoXlI6jleeol(QY6Ng`G)Vhz4MF@-n9`R3Rm*LKTd2MVAqKS*YCbdV?|&r!c)Kvl5b^x&#$<438U@oe3J+f+Nf*Cgr-8r{>q zU7ma6Dc@B1h*Hk;B2=I&{VoE9x(-0PH(vV$is@GX___7PcIIl^X)A$!^db7-=HJEPv?XHeSwA^lv^lp|sLBADf{XUS7a*%!j(xZ1VK7ud% zw`wksW;21~2F&c_R{6~K=>*r0)m1UDmP7x<#S%avBN)X&2b6nuqlC{ceW6Kwwu%LF z+@wJvckf(d-;?uR+{?$U_iRvEIThLN^*rWt8dMe-(_s(ZLg#$0gu6@VP4nFdr5trT`0EeFsQO~X$ypI`lJxkisIg0{i^12!%?q0r0JQ5ne z=y9rB5Isoh)%2)4_1FW_w7|(&ujlZ8 zA|D6Go?j=hEP5?*=-JYHo@Np+ie2v!nMc}ROtHj?re}#H&4V9Xz zFez{1k!Y*acMj-!9KFygT@v_>n*H;KT67dvxj$myoOb?Im-kNB8{I^VTC(X|i;OA| zlFOkOWX#%xricU-Uxks)KWUPaJxtuN(_iexuGepI@aAEYC@{mR1k6seebH~ha`CjX=KT^e4 z2|7dW#ic0N-HOqqj2+2n|NTA>6ULaxUJGfoLE#CsixCscyns2_Nya&V!f#f-STtdK z#b2|W$)feZKTPu^t5DikLnco_MAEY6(K@+IMYgh>lE{ybmlukfr#E6XLRMJCGL>)p zvvM|k3aI7@_MwDZC24C3qe}Hx>xbFFonJMTFAd z2rbZ%M2Ov-^=5x~MJ+(LTUO!Xf1MHwmX!1*k%MkN$1kb%+qcV_tC$638b0lh5|dvK zM$w4I8tFueYUVT7G(IPAP4r)nLUv(6%_}B<`mQp(t}$)8G$v)4td9r~Pk|bD>zNhP zk>ejJVBNXajFTrI{>(Ue2O8M_^)DAo#pG2LRTI$vQ`%xyUY`s-)N{&Ws zlAZ0G0}Ki(7@FT>bwMQLabGqb|3pGQ_?~@g!UodCmdE^k=37sX|UnNn5Fg7UBv9G<P^1I`%JKL)-Dp1a7EqJWz~pymL(M%gkExZb^6?0jf7o*Ij>F>CJgYtX4N49o1I6?)Uc`7eePg5edRttmf& zl({`o{2P!Rpzvd21~eye-yT47#! z2V=pq5wu{eVDw2_^yM4Psp>z8 zvj)xrKo*`E3+JHMGhU>cA;9)JfPaygtVp}w>3M8W5yp7{ZBg(wYL9S0yML;eKbdbn zFSnYK6W1!USVM3bpYG26aNHU%69YDn36_i7@h3}iXu2oXUH)AyJMbtP$up108$4S{ z-}$Egl|=2Eam+W3!tE-jowTBuj0hNBE_*g`wg3^?(d^yU%_lT@0~WFUn|p6npgB~4 zk#)}gcvznF-cF~=-u9mmxADDv5@c$p0LKd3L}Xb_RaM5Ys=U^s(6NsOTrWJf@pYG0d%TXaPe8 zoT~!?^+WEktJ6NNH15-Eimh~y<1xbSe!Ml1{m53f&raFl3nn_1a31xh3u~^hUFeQ~ zWIn7Zd`^P3CWCmX0yjcbOZeeXx!6vYS!xLwwm_NU0CAdRxdhIj$@a{pBlTP>1$sv#c`Z46A+UzUv+usDsm5SdYaclVNbJp~B zY#6pf7F-t&Y0G)COSs3+@Bxru&GA+WJ&Q#dk7IC~me4sRE0AO}c@0 zbr6q_B1A{WUy3wvBh{i?`^`*m@Z-63-Z}E|YZoC70sTs{g6rQA1$WvBhz)kL1Y^Fe zIi0O4j1Yy}KBMwcuo~d*5T2`z2CT()W28`^(D#v0M@T1}SpdbgW5zwVTd>Pdj?~>u z?ksQJNC8v?JpN-;F&{(`s^_^%67v@X>kpl_waq7j4x@x~8is>HMn8;%vO-+`q>Ig< z^gA%!7=V)*>IN)5GH(YzEAusF64G~sJnV=&MyPj$o-dxP4YmUU%}tb?5iv?OMA)K| zJR2-7t>&9{b#YQ&01__^!Ir@O<#HVDBdU+TCgB~$!F+l=VVxWiDRP`R6xkHSlgniU z%`~DoH~b1XKC-qR@kG@`G5 zX3^oY%vrr?whpLu)Zyol9i~x;E=x|tEE)0dc@N<38w&OjxInx8_OB@BSPl4?Lp2iR zvc?V0Fq$MaX6PA2=8zt47IJQf{jv}K-uvr$JZ3rtTz>@|!4wSYGAHcMKUbhN`~l5p zyQ6NtDrEHL;(C%95aQre2y83S9U6mD&^%!5W;VvZJ$)yWdU^uw;{)P^r81Xh+fHel z6Ci}6Rd;k{R1u8B%!T#Wt_S?J?UnC_?&A?L5C*feh1l<#^sRkV5peh!m7S2rs!ytgTp}QJ6O8Lm>oQkaEJiD0Zi{+yo$zBR1xLEvrG~bOv7GEiOov8e6h3imyvAJ;^;gCS6-R7z_;8%JIy6;7IU#FuN^uVOa!={{;DE5&uGGzVSy`BI(g? zLAFX{bUXXD@;q}uOQBdHa5la)ea2g{Km-Kgk`RQ;`HU}BAY8_}fKX-P2{l1iYXvb? z^X{QKaK8HRMtaK?brWUcqXV)aIw1R{tpl^G>M}Yde#Q`siH_j7AQ;;AJ=X!j@HYzd z$}8s2i6 zMS`nee}L04p#!}p(MwVxKx>q@sb^j&L& zfQpxSN+%~*q=QNa@1uGb+25zO2ULU0f08N6lVUe;+j*80?z2HeK~y0z9suh{AKDl! z3cQMrzsyJYhd_d9$#HXZj+)Nl}CY-twm?tSlpMG1q zZIPFV{SG8i1tMB4?&q#I+A4kZy$$oh0MfGQns_z))^MI$^Ka#J(Wz9F%fLSgL5a<& z>X9b>ra52)sw%Ubr~(0a-1Fji3k?sn!D6oi+|iu0$cP1?s#XJ<3U` z1nATo=2KM;GThUHO>fo*O)FB$<19f^N{Qld9QcOQxc+8a+AyXbQ{mZ6S2^Sh+st~e z^k@|PYQW(EEPlzxBL%PcN^vdZ zzKC$W{E107+gZW$dCqpRiJcfLKmyz$|5@NTHi)?;>1`t?mG~B%x4jZK3Qx`on8^vP zqeEI2zu?Ej$u}_r@KdM!ngtg!k|A-6qn-JX^p9WjqF)&BUzq43JO*(|qaM()krEin z{T)8tTR{7?t+vE{L5|lBS+nb_Y+;Y~b4TATGw6zCf8JdO(2P(Gf5+X4K3d<*UbGqx zvih%+m0VdK31x8zNj(FzQtDCE^k!3b#pba!hZf4JBb-QDrFxTy`e8@#?Tfv@N`&}g zb0C2oI((wm0GR8CIivvFM-c~Fm>&!X#SoI}31eEsc2NNV%Yv3$-#eF&pEIBa$Yd|+ z@Eeu^7<+jQ)Ff_XDlc%G@bVfbK~T*LY$V+RcZB3HfdFh!69$0j7_)Jf&Agj+h&;4k zf>3Gy`syP2@pS&D$9A6=ZdGFfj+C}<4zXGvjMS*%^UK5gefe9kxdqoF?SXvfB=|hi2pJ z5^4GGlKY6un})*Q&l8*#g*-UgTfWTs=?kkvn9x%X;_AMZu$*&{e5vbOH5WJg2LXRv zC9$dGXpv24{MeugGDt*3k%fh4Gb^Z2gj-1L>qGgr9FfFl8sU;Y+T4nRcI}&983_1t z0aR%<@HxxgSQ~l~jUd0ffGL^bJjy9BYiTG(;SW|bYGPMj|H;~*Fp6+*q;b$JgAtPc z@XL1_HY@C-xMHXcA-88IvpeBEj^v0M0Pc=NYa)TCGz0&Yzho{J-5Fm^t1w|;+Qby4J5P_*A>1&( zJ^Q@Kk+~2SP5O`i(!1YC(AV^h{%O$Yo;P%d!D|>7X4y}K3_g-Y-DRmiVcg?t#402w zVWy2F+zBo5AC)W8F4C(3t~vPhAkorpm1gMzC2e2spEdYk(B!0k6VuWU``!LrX`unS z^j%7c25mV6zobbGM%yPi{U(Efq><>T3}qPiau}_nn<3%&okLLQI7$R3ZKac925GtA=cXJBIyGB?aAR$+s^g)^D# zBH;Fj)Bq0_9MVWp_hh)LDkYrdkuC{+M^houJocQ2q)f;-Bfg1FkBU3XgP3HaAI?!^ zRg9}4E6XDiu#=($-U7>#dqf^@DRe64ymeP$nMTN#)?y;$-ZGn%SmXP~82ovk%?fr* zOyc);(TG$Vl8;hi^>2c&SLa7=-X^@CO_6Iy{P@|fsY*B1H-*GjrAHE3G*Y^)CI8Z^ zk!p*)uB5>ijWyjgBckmiW{7k|XdWIcbU4fN(qQ(ql5xBo4VQ{f8IbqJ*5S^MVs~=o z&0W@gGDMhT`z+pH97l~#=9`C_*ci36!8kJSySl^v((Xq=h9;6YLd;uxJdkSByvOKfwb5In%2Ty}G;M7jnwao8WiLX~%SbxjRys=5nOVy{^G9|~-hP)y& z{mSgz&lM4`{Sc3XO2s$ek=)Y46L0vZKG)%bSIcK@`Qfg;j{uIUTuqMqxVqWoj``PA49xGH_b_BSsY1mIoPzks$=F!{py~n8tUD<)J`*4k zX$FuRmV9Ccii`LFsH9N6qq-03+yP}a_1OQL)y*!%8aIAabD4_ApwHBp1q;w9{Y)~& ziJ1+GD$^GLmV$3FuxBDn;0Y2J8Uks7k5hu6+9P~wcOGMoUm0E&8VqA1{s1($p z@&@HwI1={(7KPJ;t2`~z=>1nmjE(q|kgsUS=?-w|LbI+2fNxQlp!~mKfY`?*oEks0 zGy6W=pF!upzdpt6uw@Jo#uu+bF%tVM8Wspu|KG*OmdGB`1L2%C0bP;f(T+X%y8UWncgK z^ytc<&Jvgn3nJY^h;eC!>}^1|ytNmHYex5gQ)UhXJqvbd$0r1(fL-j(~cwB7l=-#Py8}Zh-Ng2q!U?pJ>Fb% z+@4$msMExuS7?Ki!|U?YwjDN8=ixSzr^ZzNeo4UzXjZ5}_K`w8u^<85Ij+|~JAgy1 z%MxrN$W%F4{xg*(;rh>Eub=(Tp+eng@+z|vWq3_{f!P9=Kdl_f9{*aLPNG}Im= z?7W*1O_fIL$^-zQ6L6ZAEny$6flX>@9<#pN&R;>-cEAG>To5|F_`D1l6n+5P-wz;p zeRcv#U|MItDOq|CQCwk zz!rbfC^+3A7Rnf-)xJ!A_P2O@OA|lM7J5WZwDWIe24Y*shM>J0H3bIEfn%aEpJ4Q{ zXcH!n+3TNg4XUD@uFqDIgJ2AiRD5k8q@tpTneCei1S-Ij^@+~eNlDW}yWA?Zdb6af zn{Y<9PjsMaaoG90^mPT#9zTJM=i~>5z9Ia58|sa1(Mqw zxNGlyBv1C24${*+Q4JWr{$Y)G8*!k~co;@$&~)RK_(wT~DN=}=SLA+YN}4R+>Rq#& zKHgKD1P19X>2@ja6yUB%h@4F9n`SaF1l*>dWe_-onZ7sRefpO&l;jfID0ztKGk`RC zM#jP$a}F$MCHLV%Y4}=AhLHJB!0g~s6-DdF)0uP_g$eP|fPEFm(e*u!4y4%Jn18|Y z?E(lp)qu&o<^!G$2e1K&r*Io==jGcWeh3@+<5vp5X&_X5Y-0DrlSKGTyi^O)%Tu zioZkXv5T{24RW9S5h1(sOOv;cbFVgK5ti1;9QW0o-Ie~%LyhI++F#GasEAUH-7(%6 z!DOh-P@O?m;U2il(JCDrB|1CXKL4!56B%W1-ynqoq$Oaej#F#qC3koc?L@lEG8IR! z^wSqv>+@fq$Y<3RW9sbL@4bV?(1TPOp35s=XCN0HeDXt8_UR*a%^4fr{1*3|Uo~f2 zqg>V*8|x5{TNb*oF-e+*m$m|c)a!_L7{nfPazY- zp4$v?VHn9VH!(4bcz&=%S�WGEcAV>w;pVgA-%*0PfdXBjJ#8Ngl;3CW7+m_mK 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

}*a1N{_-BBu|2yD><*mAqe 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 diff --git a/tests/commons.py b/tests/commons.py index 32a33204..c2a85145 100644 --- a/tests/commons.py +++ b/tests/commons.py @@ -24,7 +24,10 @@ from custom_components.versatile_thermostat.base_thermostat import BaseThermostat from custom_components.versatile_thermostat.const import * # pylint: disable=wildcard-import, unused-wildcard-import from custom_components.versatile_thermostat.underlyings import * # pylint: disable=wildcard-import, unused-wildcard-import -from custom_components.versatile_thermostat.commons import get_tz, NowClass # pylint: disable=unused-import +from custom_components.versatile_thermostat.commons import ( # pylint: disable=unused-import + get_tz, + NowClass, +) from .const import ( # pylint: disable=unused-import MOCK_TH_OVER_SWITCH_USER_CONFIG, @@ -117,47 +120,80 @@ class MockClimate(ClimateEntity): """A Mock Climate class used for Underlying climate mode""" - def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos, hvac_mode:HVACMode = HVACMode.OFF, hvac_action:HVACAction = HVACAction.OFF) -> None: # pylint: disable=unused-argument + def __init__( # pylint: disable=unused-argument, dangerous-default-value + self, + hass: HomeAssistant, + unique_id, + name, + entry_infos={}, + hvac_mode: HVACMode = HVACMode.OFF, + hvac_action: HVACAction = HVACAction.OFF, + fan_modes: list[str] = None, + ) -> None: """Initialize the thermostat.""" super().__init__() self.hass = hass - self.platform = 'climate' - self.entity_id= self.platform+'.'+unique_id + self.platform = "climate" + self.entity_id = self.platform + "." + unique_id self._attr_extra_state_attributes = {} self._unique_id = unique_id self._name = name - self._attr_hvac_action = HVACAction.OFF if hvac_mode == HVACMode.OFF else HVACAction.HEATING + self._attr_hvac_action = ( + HVACAction.OFF if hvac_mode == HVACMode.OFF else HVACAction.HEATING + ) self._attr_hvac_mode = hvac_mode self._attr_hvac_modes = [HVACMode.OFF, HVACMode.COOL, HVACMode.HEAT] self._attr_temperature_unit = UnitOfTemperature.CELSIUS self._attr_target_temperature = 20 self._attr_current_temperature = 15 self._attr_hvac_action = hvac_action + self._fan_modes = fan_modes if fan_modes else None + self._attr_fan_mode = None + + @property + def hvac_action(self): + """The hvac action of the mock climate""" + return self._attr_hvac_action + + @property + def fan_modes(self) -> list[str] | None: + """The list of fan_modes""" + return self._fan_modes + + def set_fan_mode(self, fan_mode): + """Set the fan mode""" + self._attr_fan_mode = fan_mode + + @property + def supported_features(self) -> int: + """The supported feature of this climate entity""" + ret = ClimateEntityFeature.TARGET_TEMPERATURE + if self._fan_modes: + ret = ret | ClimateEntityFeature.FAN_MODE + return ret def set_temperature(self, **kwargs): - """ Set the target temperature""" + """Set the target temperature""" temperature = kwargs.get(ATTR_TEMPERATURE) self._attr_target_temperature = temperature async def async_set_hvac_mode(self, hvac_mode): - """ The hvac mode""" + """The hvac mode""" self._attr_hvac_mode = hvac_mode - @property - def hvac_action(self): - """ The hvac action of the mock climate""" - return self._attr_hvac_action - def set_hvac_action(self, hvac_action: HVACAction): - """ Set the HVACaction """ + """Set the HVACaction""" self._attr_hvac_action = hvac_action + class MockUnavailableClimate(ClimateEntity): """A Mock Climate class used for Underlying climate mode""" - def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None: # pylint: disable=unused-argument + def __init__( + self, hass: HomeAssistant, unique_id, name, entry_infos + ) -> None: # pylint: disable=unused-argument """Initialize the thermostat.""" super().__init__() @@ -170,6 +206,8 @@ def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None: self._attr_hvac_mode = None self._attr_hvac_modes = [HVACMode.OFF, HVACMode.COOL, HVACMode.HEAT] self._attr_temperature_unit = UnitOfTemperature.CELSIUS + self._attr_fan_mode = None + class MagicMockClimate(MagicMock): """A Magic Mock class for a underlying climate entity""" @@ -325,9 +363,7 @@ async def send_ext_temperature_change_event( await asyncio.sleep(0.1) -async def send_power_change_event( - entity: BaseThermostat, new_power, date, sleep=True -): +async def send_power_change_event(entity: BaseThermostat, new_power, date, sleep=True): """Sending a new power event simulating a change on power sensor""" _LOGGER.info( "------- Testu: sending send_temperature_change_event, new_power=%.2f date=%s on %s", @@ -478,6 +514,7 @@ async def send_presence_change_event( await asyncio.sleep(0.1) return ret + async def send_climate_change_event( entity: BaseThermostat, new_hvac_mode: HVACMode, @@ -521,6 +558,7 @@ async def send_climate_change_event( await asyncio.sleep(0.1) return ret + async def send_climate_change_event_with_temperature( entity: BaseThermostat, new_hvac_mode: HVACMode, diff --git a/tests/const.py b/tests/const.py index 71c117a3..6cf6cb02 100644 --- a/tests/const.py +++ b/tests/const.py @@ -55,8 +55,11 @@ CONF_AUTO_REGULATION_NONE, CONF_AUTO_REGULATION_DTEMP, CONF_AUTO_REGULATION_PERIOD_MIN, - CONF_INVERSE_SWITCH + CONF_INVERSE_SWITCH, + CONF_AUTO_FAN_HIGH, + CONF_AUTO_FAN_MODE, ) + MOCK_TH_OVER_SWITCH_USER_CONFIG = { CONF_NAME: "TheOverSwitchMockName", CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH, @@ -103,14 +106,14 @@ CONF_HEATER: "switch.mock_switch", CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI, CONF_AC_MODE: False, - CONF_INVERSE_SWITCH: False + CONF_INVERSE_SWITCH: False, } MOCK_TH_OVER_SWITCH_AC_TYPE_CONFIG = { CONF_HEATER: "switch.mock_air_conditioner", CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI, CONF_AC_MODE: True, - CONF_INVERSE_SWITCH: False + CONF_INVERSE_SWITCH: False, } MOCK_TH_OVER_4SWITCH_TYPE_CONFIG = { @@ -120,7 +123,7 @@ CONF_HEATER_4: "switch.mock_4switch3", CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI, CONF_AC_MODE: False, - CONF_INVERSE_SWITCH: False + CONF_INVERSE_SWITCH: False, } MOCK_TH_OVER_SWITCH_TPI_CONFIG = { @@ -133,13 +136,14 @@ CONF_AC_MODE: False, CONF_AUTO_REGULATION_MODE: CONF_AUTO_REGULATION_STRONG, CONF_AUTO_REGULATION_DTEMP: 0.5, - CONF_AUTO_REGULATION_PERIOD_MIN: 2 + CONF_AUTO_REGULATION_PERIOD_MIN: 2, + CONF_AUTO_FAN_MODE: CONF_AUTO_FAN_HIGH, } MOCK_TH_OVER_CLIMATE_TYPE_NOT_REGULATED_CONFIG = { CONF_CLIMATE: "climate.mock_climate", CONF_AC_MODE: False, - CONF_AUTO_REGULATION_MODE: CONF_AUTO_REGULATION_NONE + CONF_AUTO_REGULATION_MODE: CONF_AUTO_REGULATION_NONE, } MOCK_TH_OVER_CLIMATE_TYPE_AC_CONFIG = { @@ -147,7 +151,7 @@ CONF_AC_MODE: True, CONF_AUTO_REGULATION_MODE: CONF_AUTO_REGULATION_STRONG, CONF_AUTO_REGULATION_DTEMP: 0.5, - CONF_AUTO_REGULATION_PERIOD_MIN: 1 + CONF_AUTO_REGULATION_PERIOD_MIN: 1, } MOCK_PRESETS_CONFIG = { @@ -203,8 +207,8 @@ PRESET_COMFORT + PRESET_AWAY_SUFFIX + "_temp": 17, PRESET_BOOST + PRESET_AWAY_SUFFIX + "_temp": 18, PRESET_ECO + "_ac" + PRESET_AWAY_SUFFIX + "_temp": 27, - PRESET_COMFORT + "_ac" + PRESET_AWAY_SUFFIX + "_temp": 26, - PRESET_BOOST + "_ac" + PRESET_AWAY_SUFFIX + "_temp": 25, + PRESET_COMFORT + "_ac" + PRESET_AWAY_SUFFIX + "_temp": 26, + PRESET_BOOST + "_ac" + PRESET_AWAY_SUFFIX + "_temp": 25, } MOCK_ADVANCED_CONFIG = { diff --git a/tests/test_auto_fan_mode.py b/tests/test_auto_fan_mode.py new file mode 100644 index 00000000..a360c08d --- /dev/null +++ b/tests/test_auto_fan_mode.py @@ -0,0 +1,285 @@ +# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long + +""" Test the auto fan mode of a over_climate thermostat """ +from unittest.mock import patch, call + +from datetime import datetime # , timedelta + +from homeassistant.core import HomeAssistant + +# from homeassistant.components.climate import HVACAction, HVACMode +from homeassistant.config_entries import ConfigEntryState + +# from homeassistant.helpers.entity_component import EntityComponent +# from homeassistant.components.climate import ClimateEntity, DOMAIN as CLIMATE_DOMAIN + +from pytest_homeassistant_custom_component.common import MockConfigEntry + +# from custom_components.versatile_thermostat.base_thermostat import BaseThermostat +from custom_components.versatile_thermostat.thermostat_climate import ( + ThermostatOverClimate, +) + +from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import + + +@pytest.mark.parametrize("expected_lingering_tasks", [True]) +@pytest.mark.parametrize("expected_lingering_timers", [True]) +async def test_over_climate_auto_fan_mode_turbo( + hass: HomeAssistant, skip_hass_states_is_state, skip_send_event +): + """Test the init of an over climate thermostat with auto_fan_mode = Turbo which exists""" + + fan_modes = ["low", "medium", "high", "boost", "mute", "auto", "turbo"] + + entry = MockConfigEntry( + domain=DOMAIN, + title="TheOverClimateMockName", + unique_id="uniqueId", + data={ + 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, + 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: False, + CONF_USE_PRESENCE_FEATURE: False, + CONF_CLIMATE: "climate.mock_climate", + CONF_MINIMAL_ACTIVATION_DELAY: 30, + CONF_SECURITY_DELAY_MIN: 5, + CONF_SECURITY_MIN_ON_PERCENT: 0.3, + CONF_AUTO_FAN_MODE: CONF_AUTO_FAN_TURBO, + }, + ) + + fake_underlying_climate = MockClimate( + hass=hass, + unique_id="mockUniqueId", + name="MockClimateName", + fan_modes=fan_modes, + ) + + with patch( + "custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate", + return_value=fake_underlying_climate, + ): + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + assert entry.state is ConfigEntryState.LOADED + + entity: ThermostatOverClimate = search_entity( + hass, "climate.theoverclimatemockname", "climate" + ) + + assert entity + assert isinstance(entity, ThermostatOverClimate) + + assert entity.name == "TheOverClimateMockName" + assert entity.is_over_climate is True + assert entity.fan_modes == fan_modes + assert entity._auto_fan_mode == "auto_fan_turbo" + assert entity._auto_activated_fan_mode == "turbo" + assert entity._auto_deactivated_fan_mode == "mute" + + +@pytest.mark.parametrize("expected_lingering_tasks", [True]) +@pytest.mark.parametrize("expected_lingering_timers", [True]) +async def test_over_climate_auto_fan_mode_not_turbo( + hass: HomeAssistant, skip_hass_states_is_state, skip_send_event +): + """Test the init of an over climate thermostat with auto_fan_mode = Turbo which doesn't exists""" + + fan_modes = ["low", "medium", "high", "boost", "auto"] + + entry = MockConfigEntry( + domain=DOMAIN, + title="TheOverClimateMockName", + unique_id="uniqueId", + data={ + 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, + 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: False, + CONF_USE_PRESENCE_FEATURE: False, + CONF_CLIMATE: "climate.mock_climate", + CONF_MINIMAL_ACTIVATION_DELAY: 30, + CONF_SECURITY_DELAY_MIN: 5, + CONF_SECURITY_MIN_ON_PERCENT: 0.3, + CONF_AUTO_FAN_MODE: CONF_AUTO_FAN_TURBO, + }, + ) + + fake_underlying_climate = MockClimate( + hass=hass, + unique_id="mockUniqueId", + name="MockClimateName", + fan_modes=fan_modes, + ) + + with patch( + "custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate", + return_value=fake_underlying_climate, + ): + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + assert entry.state is ConfigEntryState.LOADED + + entity: ThermostatOverClimate = search_entity( + hass, "climate.theoverclimatemockname", "climate" + ) + + assert entity + assert isinstance(entity, ThermostatOverClimate) + + assert entity.name == "TheOverClimateMockName" + assert entity.is_over_climate is True + assert entity.fan_modes == fan_modes + assert entity._auto_fan_mode == "auto_fan_turbo" + # Turbo doesn't exists -> fallback to high + assert entity._auto_activated_fan_mode == "high" + # Mute doesn't exists -> fallback to auto + assert entity._auto_deactivated_fan_mode == "auto" + + +@pytest.mark.parametrize("expected_lingering_timers", [True]) +async def test_over_climate_auto_fan_mode_turbo_activation( + hass: HomeAssistant, skip_hass_states_is_state, skip_send_event +): + """Test the init of an over climate thermostat with auto_fan_mode = Turbo which exists""" + + fan_modes = ["low", "medium", "high", "boost", "mute", "auto", "turbo"] + + entry = MockConfigEntry( + domain=DOMAIN, + title="TheOverClimateMockName", + unique_id="uniqueId", + data={ + 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, + 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: False, + CONF_USE_PRESENCE_FEATURE: False, + CONF_CLIMATE: "climate.mock_climate", + CONF_MINIMAL_ACTIVATION_DELAY: 30, + CONF_SECURITY_DELAY_MIN: 5, + CONF_SECURITY_MIN_ON_PERCENT: 0.3, + CONF_AUTO_FAN_MODE: CONF_AUTO_FAN_TURBO, + }, + ) + + tz = get_tz(hass) # pylint: disable=invalid-name + now: datetime = datetime.now(tz=tz) + + fake_underlying_climate = MockClimate( + hass=hass, + unique_id="mockUniqueId", + name="MockClimateName", + fan_modes=fan_modes, + ) + + # 1. Init fan mode + with patch( + "custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate", + return_value=fake_underlying_climate, + ): + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + assert entry.state is ConfigEntryState.LOADED + + entity: ThermostatOverClimate = search_entity( + hass, "climate.theoverclimatemockname", "climate" + ) + + assert entity + assert isinstance(entity, ThermostatOverClimate) + + assert entity.name == "TheOverClimateMockName" + assert entity.is_over_climate is True + assert entity.fan_modes == fan_modes + assert entity.fan_mode is None + assert entity._auto_fan_mode == "auto_fan_turbo" + assert entity._auto_activated_fan_mode == "turbo" + assert entity._auto_deactivated_fan_mode == "mute" + + # 2. Turn on and set temperature cold + with patch( + "custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_fan_mode" + ) as mock_send_fan_mode: + # Force preset mode + await entity.async_set_hvac_mode(HVACMode.HEAT) + assert entity.hvac_mode == HVACMode.HEAT + await entity.async_set_preset_mode(PRESET_COMFORT) + assert entity.preset_mode == PRESET_COMFORT + + # Change the current temperature to 16 which is 2° under + await send_temperature_change_event(entity, 16, now, True) + fake_underlying_climate.set_fan_mode("turbo") + + assert mock_send_fan_mode.call_count == 1 + mock_send_fan_mode.assert_has_calls([call.set_fan_mode("turbo")]) + + assert entity.fan_mode == "turbo" + + # 3. Set another low temperature + with patch( + "custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_fan_mode" + ) as mock_send_fan_mode: + fake_underlying_climate.set_fan_mode("turbo") + + # Change the current temperature to 17 which is 1° under + await send_temperature_change_event(entity, 15, now, True) + + # Nothing is send cause we are already in turbo fan mode + assert mock_send_fan_mode.call_count == 0 + + assert entity.fan_mode == "turbo" + + # 4. Set temperature not so cold + with patch( + "custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_fan_mode" + ) as mock_send_fan_mode: + # Change the current temperature to 17 which is 1° under + await send_temperature_change_event(entity, 17, now, True) + fake_underlying_climate.set_fan_mode("mute") + + assert mock_send_fan_mode.call_count == 1 + mock_send_fan_mode.assert_has_calls([call.set_fan_mode("mute")]) + + assert entity.fan_mode == "mute" + + # 5. Set temperature not so cold another time + with patch( + "custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_fan_mode" + ) as mock_send_fan_mode: + fake_underlying_climate.set_fan_mode("mute") + + # Change the current temperature to 17 which is 1° under + await send_temperature_change_event(entity, 17.1, now, True) + + assert mock_send_fan_mode.call_count == 0 + assert entity.fan_mode == "mute"