Compare commits
6 Commits
5.0.1
...
5.2.0-alph
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a17aba45fa | ||
|
|
c222feda1a | ||
|
|
d05df021ab | ||
|
|
27a267139f | ||
|
|
707f40d406 | ||
|
|
a01f5770d9 |
@@ -59,8 +59,8 @@ input_number:
|
|||||||
unit_of_measurement: kW
|
unit_of_measurement: kW
|
||||||
fake_valve1:
|
fake_valve1:
|
||||||
name: The valve 1
|
name: The valve 1
|
||||||
min: 0
|
min: 10
|
||||||
max: 100
|
max: 90
|
||||||
icon: mdi:pipe-valve
|
icon: mdi:pipe-valve
|
||||||
unit_of_measurement: percentage
|
unit_of_measurement: percentage
|
||||||
|
|
||||||
|
|||||||
@@ -30,13 +30,8 @@
|
|||||||
"waderyan.gitblame",
|
"waderyan.gitblame",
|
||||||
"keesschollaart.vscode-home-assistant",
|
"keesschollaart.vscode-home-assistant",
|
||||||
"vscode.markdown-math",
|
"vscode.markdown-math",
|
||||||
"yzhang.markdown-all-in-one",
|
"yzhang.markdown-all-in-one"
|
||||||
"ms-python.vscode-pylance"
|
|
||||||
],
|
],
|
||||||
// "mounts": [
|
|
||||||
// "source=${localWorkspaceFolder}/.devcontainer/configuration.yaml,target=${localWorkspaceFolder}/config/www/community/,type=bind,consistency=cached",
|
|
||||||
// "source=${localWorkspaceFolder}/custom_components,target=/home/vscode/core/config/custom_components,type=bind,consistency=cached"
|
|
||||||
// ],
|
|
||||||
"settings": {
|
"settings": {
|
||||||
"files.eol": "\n",
|
"files.eol": "\n",
|
||||||
"editor.tabSize": 4,
|
"editor.tabSize": 4,
|
||||||
|
|||||||
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -14,7 +14,8 @@
|
|||||||
"python.testing.pytestEnabled": true,
|
"python.testing.pytestEnabled": true,
|
||||||
"python.analysis.extraPaths": [
|
"python.analysis.extraPaths": [
|
||||||
// "/home/vscode/core",
|
// "/home/vscode/core",
|
||||||
"/workspaces/versatile_thermostat/custom_components/versatile_thermostat"
|
"/workspaces/versatile_thermostat/custom_components/versatile_thermostat",
|
||||||
|
"/home/vscode/.local/lib/python3.11/site-packages/homeassistant"
|
||||||
],
|
],
|
||||||
"python.formatting.provider": "none"
|
"python.formatting.provider": "none"
|
||||||
}
|
}
|
||||||
@@ -114,7 +114,7 @@ En conséquence toute la phase de paramètrage d'un VTherm a été profondemment
|
|||||||
**Note :** les copies d'écran de la configuration d'un VTherm n'ont pas été mises à jour.
|
**Note :** les copies d'écran de la configuration d'un VTherm n'ont pas été mises à jour.
|
||||||
|
|
||||||
# Merci pour la bière [buymecoffee](https://www.buymeacoffee.com/jmcollin78)
|
# 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, @Gunnar M, @Greg.o, @John Burgess, @abyssmal, @capinfo26, @Helge, @MattG @Mexx62, @Someone 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, @Greg.o, @John Burgess, @abyssmal, @capinfo26, @Helge, @MattG @Mexx62, @Someone, @Lajull pour les bières. Ca fait très plaisir et ça m'encourage à continuer !
|
||||||
|
|
||||||
|
|
||||||
# Quand l'utiliser et ne pas l'utiliser
|
# Quand l'utiliser et ne pas l'utiliser
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ Consequently, the entire configuration phase of a VTherm has been profoundly mod
|
|||||||
**Note:** the VTherm configuration screenshots have not been updated.
|
**Note:** the VTherm configuration screenshots have not been updated.
|
||||||
|
|
||||||
# Thanks for the beer [buymecoffee](https://www.buymeacoffee.com/jmcollin78)
|
# Thanks for the beer [buymecoffee](https://www.buymeacoffee.com/jmcollin78)
|
||||||
Many thanks to @salabur, @pvince83, @bergoglio, @EPicLURcher, @ecolorado66, @Kriss1670, @maia, @f.maymil, @moutte69, @Jerome, @Gunnar M, @Greg.o, @John Burgess, @abyssmal, @capinfo26, @Helge, @MattG, @MattG, @Mexx62, @Someone 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, @Greg.o, @John Burgess, @abyssmal, @capinfo26, @Helge, @MattG, @MattG, @Mexx62, @Someone, @Lajull for the beers. It's very nice and encourages me to continue!
|
||||||
|
|
||||||
# When to use / not use
|
# When to use / not use
|
||||||
This thermostat can control 3 types of equipment:
|
This thermostat can control 3 types of equipment:
|
||||||
|
|||||||
@@ -116,6 +116,11 @@ from .const import (
|
|||||||
ATTR_TOTAL_ENERGY,
|
ATTR_TOTAL_ENERGY,
|
||||||
PRESET_AC_SUFFIX,
|
PRESET_AC_SUFFIX,
|
||||||
DEFAULT_SHORT_EMA_PARAMS,
|
DEFAULT_SHORT_EMA_PARAMS,
|
||||||
|
CENTRAL_MODE_AUTO,
|
||||||
|
CENTRAL_MODE_STOPPED,
|
||||||
|
CENTRAL_MODE_HEAT_ONLY,
|
||||||
|
CENTRAL_MODE_COOL_ONLY,
|
||||||
|
CENTRAL_MODE_FROST_PROTECTION,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .config_schema import * # pylint: disable=wildcard-import, unused-wildcard-import
|
from .config_schema import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||||
@@ -158,6 +163,8 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
|||||||
frozenset(
|
frozenset(
|
||||||
{
|
{
|
||||||
"is_on",
|
"is_on",
|
||||||
|
"is_controlled_by_central_mode",
|
||||||
|
"last_central_mode",
|
||||||
"type",
|
"type",
|
||||||
"frost_temp",
|
"frost_temp",
|
||||||
"eco_temp",
|
"eco_temp",
|
||||||
@@ -273,6 +280,9 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
|||||||
self._now = None
|
self._now = None
|
||||||
|
|
||||||
self._attr_fan_mode = None
|
self._attr_fan_mode = None
|
||||||
|
|
||||||
|
self._is_central_mode = None
|
||||||
|
self._last_central_mode = None
|
||||||
self.post_init(entry_infos)
|
self.post_init(entry_infos)
|
||||||
|
|
||||||
def clean_central_config_doublon(self, config_entry, central_config) -> dict:
|
def clean_central_config_doublon(self, config_entry, central_config) -> dict:
|
||||||
@@ -434,6 +444,8 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
|||||||
self._presence_on = self._presence_sensor_entity_id is not None
|
self._presence_on = self._presence_sensor_entity_id is not None
|
||||||
|
|
||||||
if self._ac_mode:
|
if self._ac_mode:
|
||||||
|
# Added by https://github.com/jmcollin78/versatile_thermostat/pull/144
|
||||||
|
# Some over_switch can do both heating and cooling
|
||||||
self._hvac_list = [HVACMode.HEAT, HVACMode.COOL, HVACMode.OFF]
|
self._hvac_list = [HVACMode.HEAT, HVACMode.COOL, HVACMode.OFF]
|
||||||
else:
|
else:
|
||||||
self._hvac_list = [HVACMode.HEAT, HVACMode.OFF]
|
self._hvac_list = [HVACMode.HEAT, HVACMode.OFF]
|
||||||
@@ -552,6 +564,10 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
|||||||
short_ema_params.get("max_alpha"),
|
short_ema_params.get("max_alpha"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self._is_central_mode = not (
|
||||||
|
entry_infos.get(CONF_USE_CENTRAL_MODE) is False
|
||||||
|
) # Default value (None) is True
|
||||||
|
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"%s - Creation of a new VersatileThermostat entity: unique_id=%s",
|
"%s - Creation of a new VersatileThermostat entity: unique_id=%s",
|
||||||
self,
|
self,
|
||||||
@@ -1130,6 +1146,17 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
|||||||
"""True if the VTherm is on (! HVAC_OFF)"""
|
"""True if the VTherm is on (! HVAC_OFF)"""
|
||||||
return self.hvac_mode and self.hvac_mode != HVACMode.OFF
|
return self.hvac_mode and self.hvac_mode != HVACMode.OFF
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_controlled_by_central_mode(self) -> bool:
|
||||||
|
"""Returns True if this VTherm can be controlled by the central_mode"""
|
||||||
|
return self._is_central_mode
|
||||||
|
|
||||||
|
@property
|
||||||
|
def last_central_mode(self) -> str | None:
|
||||||
|
"""Returns the last central_mode taken into account.
|
||||||
|
Is None if the VTherm is not controlled by central_mode"""
|
||||||
|
return self._last_central_mode
|
||||||
|
|
||||||
def underlying_entity_id(self, index=0) -> str | None:
|
def underlying_entity_id(self, index=0) -> str | None:
|
||||||
"""The climate_entity_id. Added for retrocompatibility reason"""
|
"""The climate_entity_id. Added for retrocompatibility reason"""
|
||||||
if index < self.nb_underlying_entities:
|
if index < self.nb_underlying_entities:
|
||||||
@@ -1177,11 +1204,11 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# If AC is on maybe we have to change the temperature in force mode, but not in frost mode (there is no Frost protection possible in AC mode)
|
# If AC is on maybe we have to change the temperature in force mode, but not in frost mode (there is no Frost protection possible in AC mode)
|
||||||
if self._ac_mode:
|
if self._hvac_mode == HVACMode.COOL:
|
||||||
if self.preset_mode != PRESET_FROST_PROTECTION:
|
if self.preset_mode != PRESET_FROST_PROTECTION:
|
||||||
await self._async_set_preset_mode_internal(self._attr_preset_mode, True)
|
await self._async_set_preset_mode_internal(self._attr_preset_mode, True)
|
||||||
else:
|
else:
|
||||||
await self._async_set_preset_mode_internal(PRESET_ECO, True)
|
await self._async_set_preset_mode_internal(PRESET_ECO, True, False)
|
||||||
|
|
||||||
if need_control_heating and sub_need_control_heating:
|
if need_control_heating and sub_need_control_heating:
|
||||||
await self.async_control_heating(force=True)
|
await self.async_control_heating(force=True)
|
||||||
@@ -1195,12 +1222,17 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
|||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
self.send_event(EventType.HVAC_MODE_EVENT, {"hvac_mode": self._hvac_mode})
|
self.send_event(EventType.HVAC_MODE_EVENT, {"hvac_mode": self._hvac_mode})
|
||||||
|
|
||||||
async def async_set_preset_mode(self, preset_mode):
|
@overrides
|
||||||
|
async def async_set_preset_mode(self, preset_mode, overwrite_saved_preset=True):
|
||||||
"""Set new preset mode."""
|
"""Set new preset mode."""
|
||||||
await self._async_set_preset_mode_internal(preset_mode)
|
await self._async_set_preset_mode_internal(
|
||||||
|
preset_mode, force=False, overwrite_saved_preset=overwrite_saved_preset
|
||||||
|
)
|
||||||
await self.async_control_heating(force=True)
|
await self.async_control_heating(force=True)
|
||||||
|
|
||||||
async def _async_set_preset_mode_internal(self, preset_mode, force=False):
|
async def _async_set_preset_mode_internal(
|
||||||
|
self, preset_mode, force=False, overwrite_saved_preset=True
|
||||||
|
):
|
||||||
"""Set new preset mode."""
|
"""Set new preset mode."""
|
||||||
_LOGGER.info("%s - Set preset_mode: %s force=%s", self, preset_mode, force)
|
_LOGGER.info("%s - Set preset_mode: %s force=%s", self, preset_mode, force)
|
||||||
if (
|
if (
|
||||||
@@ -1242,7 +1274,8 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
|||||||
|
|
||||||
self.reset_last_temperature_time(old_preset_mode)
|
self.reset_last_temperature_time(old_preset_mode)
|
||||||
|
|
||||||
self.save_preset_mode()
|
if overwrite_saved_preset:
|
||||||
|
self.save_preset_mode()
|
||||||
self.recalculate()
|
self.recalculate()
|
||||||
self.send_event(EventType.PRESET_EVENT, {"preset": self._attr_preset_mode})
|
self.send_event(EventType.PRESET_EVENT, {"preset": self._attr_preset_mode})
|
||||||
|
|
||||||
@@ -1998,6 +2031,66 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
|||||||
|
|
||||||
return self._overpowering_state
|
return self._overpowering_state
|
||||||
|
|
||||||
|
async def check_central_mode(self, new_central_mode, old_central_mode) -> None:
|
||||||
|
"""Take into account a central mode change"""
|
||||||
|
if not self.is_controlled_by_central_mode:
|
||||||
|
self._last_central_mode = None
|
||||||
|
return
|
||||||
|
|
||||||
|
_LOGGER.info(
|
||||||
|
"%s - Central mode have change from %s to %s",
|
||||||
|
self,
|
||||||
|
old_central_mode,
|
||||||
|
new_central_mode,
|
||||||
|
)
|
||||||
|
|
||||||
|
self._last_central_mode = new_central_mode
|
||||||
|
|
||||||
|
def save_all():
|
||||||
|
"""save preset and hvac_mode"""
|
||||||
|
self.save_preset_mode()
|
||||||
|
self.save_hvac_mode()
|
||||||
|
|
||||||
|
if new_central_mode == CENTRAL_MODE_AUTO:
|
||||||
|
await self.restore_hvac_mode()
|
||||||
|
await self.restore_preset_mode()
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
if old_central_mode == CENTRAL_MODE_AUTO:
|
||||||
|
save_all()
|
||||||
|
|
||||||
|
if new_central_mode == CENTRAL_MODE_STOPPED:
|
||||||
|
await self.async_set_hvac_mode(HVACMode.OFF)
|
||||||
|
return
|
||||||
|
|
||||||
|
if new_central_mode == CENTRAL_MODE_COOL_ONLY:
|
||||||
|
if HVACMode.COOL in self.hvac_modes:
|
||||||
|
await self.async_set_hvac_mode(HVACMode.COOL)
|
||||||
|
else:
|
||||||
|
await self.async_set_hvac_mode(HVACMode.OFF)
|
||||||
|
return
|
||||||
|
|
||||||
|
if new_central_mode == CENTRAL_MODE_HEAT_ONLY:
|
||||||
|
if HVACMode.HEAT in self.hvac_modes:
|
||||||
|
await self.async_set_hvac_mode(HVACMode.HEAT)
|
||||||
|
else:
|
||||||
|
await self.async_set_hvac_mode(HVACMode.OFF)
|
||||||
|
return
|
||||||
|
|
||||||
|
if new_central_mode == CENTRAL_MODE_FROST_PROTECTION:
|
||||||
|
if (
|
||||||
|
PRESET_FROST_PROTECTION in self.preset_modes
|
||||||
|
and HVACMode.HEAT in self.hvac_modes
|
||||||
|
):
|
||||||
|
await self.async_set_hvac_mode(HVACMode.HEAT)
|
||||||
|
await self.async_set_preset_mode(
|
||||||
|
PRESET_FROST_PROTECTION, overwrite_saved_preset=False
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await self.async_set_hvac_mode(HVACMode.OFF)
|
||||||
|
return
|
||||||
|
|
||||||
def _set_now(self, now: datetime):
|
def _set_now(self, now: datetime):
|
||||||
"""Set the now timestamp. This is only for tests purpose"""
|
"""Set the now timestamp. This is only for tests purpose"""
|
||||||
self._now = now
|
self._now = now
|
||||||
@@ -2239,6 +2332,8 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
|||||||
"hvac_mode": self.hvac_mode,
|
"hvac_mode": self.hvac_mode,
|
||||||
"preset_mode": self.preset_mode,
|
"preset_mode": self.preset_mode,
|
||||||
"type": self._thermostat_type,
|
"type": self._thermostat_type,
|
||||||
|
"is_controlled_by_central_mode": self.is_controlled_by_central_mode,
|
||||||
|
"last_central_mode": self.last_central_mode,
|
||||||
"frost_temp": self._presets[PRESET_FROST_PROTECTION],
|
"frost_temp": self._presets[PRESET_FROST_PROTECTION],
|
||||||
"eco_temp": self._presets[PRESET_ECO],
|
"eco_temp": self._presets[PRESET_ECO],
|
||||||
"boost_temp": self._presets[PRESET_BOOST],
|
"boost_temp": self._presets[PRESET_BOOST],
|
||||||
|
|||||||
@@ -364,7 +364,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
|||||||
schema = schema_ac_or_not
|
schema = schema_ac_or_not
|
||||||
elif user_input and user_input.get(CONF_USE_PRESETS_CENTRAL_CONFIG) is False:
|
elif user_input and user_input.get(CONF_USE_PRESETS_CENTRAL_CONFIG) is False:
|
||||||
next_step = self.async_step_spec_presets
|
next_step = self.async_step_spec_presets
|
||||||
schema = schema_ac_or_not
|
schema = STEP_PRESETS_DATA_SCHEMA
|
||||||
|
|
||||||
return await self.generic_step("presets", schema, user_input, next_step)
|
return await self.generic_step("presets", schema, user_input, next_step)
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ STEP_MAIN_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
|
|||||||
),
|
),
|
||||||
vol.Required(CONF_CYCLE_MIN, default=5): cv.positive_int,
|
vol.Required(CONF_CYCLE_MIN, default=5): cv.positive_int,
|
||||||
vol.Optional(CONF_DEVICE_POWER, default="1"): vol.Coerce(float),
|
vol.Optional(CONF_DEVICE_POWER, default="1"): vol.Coerce(float),
|
||||||
|
vol.Optional(CONF_USE_CENTRAL_MODE, default=True): cv.boolean,
|
||||||
vol.Optional(CONF_USE_WINDOW_FEATURE, default=False): cv.boolean,
|
vol.Optional(CONF_USE_WINDOW_FEATURE, default=False): cv.boolean,
|
||||||
vol.Optional(CONF_USE_MOTION_FEATURE, default=False): cv.boolean,
|
vol.Optional(CONF_USE_MOTION_FEATURE, default=False): cv.boolean,
|
||||||
vol.Optional(CONF_USE_POWER_FEATURE, default=False): cv.boolean,
|
vol.Optional(CONF_USE_POWER_FEATURE, default=False): cv.boolean,
|
||||||
|
|||||||
@@ -35,7 +35,12 @@ HIDDEN_PRESETS = [PRESET_POWER, PRESET_SECURITY]
|
|||||||
|
|
||||||
DOMAIN = "versatile_thermostat"
|
DOMAIN = "versatile_thermostat"
|
||||||
|
|
||||||
PLATFORMS: list[Platform] = [Platform.CLIMATE, Platform.BINARY_SENSOR, Platform.SENSOR]
|
PLATFORMS: list[Platform] = [
|
||||||
|
Platform.CLIMATE,
|
||||||
|
Platform.BINARY_SENSOR,
|
||||||
|
Platform.SENSOR,
|
||||||
|
Platform.SELECT,
|
||||||
|
]
|
||||||
|
|
||||||
CONF_HEATER = "heater_entity_id"
|
CONF_HEATER = "heater_entity_id"
|
||||||
CONF_HEATER_2 = "heater_entity2_id"
|
CONF_HEATER_2 = "heater_entity2_id"
|
||||||
@@ -113,6 +118,8 @@ CONF_USE_PRESENCE_CENTRAL_CONFIG = "use_presence_central_config"
|
|||||||
CONF_USE_PRESETS_CENTRAL_CONFIG = "use_presets_central_config"
|
CONF_USE_PRESETS_CENTRAL_CONFIG = "use_presets_central_config"
|
||||||
CONF_USE_ADVANCED_CENTRAL_CONFIG = "use_advanced_central_config"
|
CONF_USE_ADVANCED_CENTRAL_CONFIG = "use_advanced_central_config"
|
||||||
|
|
||||||
|
CONF_USE_CENTRAL_MODE = "use_central_mode"
|
||||||
|
|
||||||
DEFAULT_SHORT_EMA_PARAMS = {
|
DEFAULT_SHORT_EMA_PARAMS = {
|
||||||
"max_alpha": 0.5,
|
"max_alpha": 0.5,
|
||||||
# In sec
|
# In sec
|
||||||
@@ -242,6 +249,7 @@ ALL_CONF = (
|
|||||||
CONF_USE_POWER_CENTRAL_CONFIG,
|
CONF_USE_POWER_CENTRAL_CONFIG,
|
||||||
CONF_USE_PRESENCE_CENTRAL_CONFIG,
|
CONF_USE_PRESENCE_CENTRAL_CONFIG,
|
||||||
CONF_USE_ADVANCED_CENTRAL_CONFIG,
|
CONF_USE_ADVANCED_CENTRAL_CONFIG,
|
||||||
|
CONF_USE_CENTRAL_MODE,
|
||||||
]
|
]
|
||||||
+ CONF_PRESETS_VALUES
|
+ CONF_PRESETS_VALUES
|
||||||
+ CONF_PRESETS_AWAY_VALUES
|
+ CONF_PRESETS_AWAY_VALUES
|
||||||
@@ -297,6 +305,19 @@ AUTO_FAN_DEACTIVATED_MODES = ["mute", "auto", "low"]
|
|||||||
|
|
||||||
CENTRAL_CONFIG_NAME = "Central configuration"
|
CENTRAL_CONFIG_NAME = "Central configuration"
|
||||||
|
|
||||||
|
CENTRAL_MODE_AUTO = "Auto"
|
||||||
|
CENTRAL_MODE_STOPPED = "Stopped"
|
||||||
|
CENTRAL_MODE_HEAT_ONLY = "Heat only"
|
||||||
|
CENTRAL_MODE_COOL_ONLY = "Cool only"
|
||||||
|
CENTRAL_MODE_FROST_PROTECTION = "Frost protection"
|
||||||
|
CENTRAL_MODES = [
|
||||||
|
CENTRAL_MODE_AUTO,
|
||||||
|
CENTRAL_MODE_STOPPED,
|
||||||
|
CENTRAL_MODE_HEAT_ONLY,
|
||||||
|
CENTRAL_MODE_COOL_ONLY,
|
||||||
|
CENTRAL_MODE_FROST_PROTECTION,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
# A special regulation parameter suggested by @Maia here: https://github.com/jmcollin78/versatile_thermostat/discussions/154
|
# A special regulation parameter suggested by @Maia here: https://github.com/jmcollin78/versatile_thermostat/discussions/154
|
||||||
class RegulationParamSlow:
|
class RegulationParamSlow:
|
||||||
|
|||||||
134
custom_components/versatile_thermostat/select.py
Normal file
134
custom_components/versatile_thermostat/select.py
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
# pylint: disable=unused-argument
|
||||||
|
|
||||||
|
""" Implements the VersatileThermostat select component """
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from homeassistant.const import EVENT_HOMEASSISTANT_START
|
||||||
|
from homeassistant.core import HomeAssistant, CoreState, callback
|
||||||
|
|
||||||
|
from homeassistant.components.climate import ClimateEntity, DOMAIN as CLIMATE_DOMAIN
|
||||||
|
from homeassistant.components.select import SelectEntity
|
||||||
|
from homeassistant.helpers.device_registry import DeviceInfo, DeviceEntryType
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.helpers.restore_state import RestoreEntity
|
||||||
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
from homeassistant.helpers.entity_component import EntityComponent
|
||||||
|
|
||||||
|
|
||||||
|
from custom_components.versatile_thermostat.base_thermostat import BaseThermostat
|
||||||
|
from .const import (
|
||||||
|
DOMAIN,
|
||||||
|
DEVICE_MANUFACTURER,
|
||||||
|
CONF_NAME,
|
||||||
|
CONF_THERMOSTAT_TYPE,
|
||||||
|
CONF_THERMOSTAT_CENTRAL_CONFIG,
|
||||||
|
CENTRAL_MODE_AUTO,
|
||||||
|
CENTRAL_MODES,
|
||||||
|
overrides,
|
||||||
|
)
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entry: ConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up the VersatileThermostat selects with config flow."""
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Calling async_setup_entry entry=%s, data=%s", entry.entry_id, entry.data
|
||||||
|
)
|
||||||
|
|
||||||
|
unique_id = entry.entry_id
|
||||||
|
name = entry.data.get(CONF_NAME)
|
||||||
|
vt_type = entry.data.get(CONF_THERMOSTAT_TYPE)
|
||||||
|
|
||||||
|
if vt_type != CONF_THERMOSTAT_CENTRAL_CONFIG:
|
||||||
|
return
|
||||||
|
|
||||||
|
entities = [
|
||||||
|
CentralModeSelect(hass, unique_id, name, entry.data),
|
||||||
|
]
|
||||||
|
|
||||||
|
async_add_entities(entities, True)
|
||||||
|
|
||||||
|
|
||||||
|
class CentralModeSelect(SelectEntity, RestoreEntity):
|
||||||
|
"""Representation of a Energy sensor which exposes the energy"""
|
||||||
|
|
||||||
|
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
|
||||||
|
"""Initialize the energy sensor"""
|
||||||
|
self._config_id = unique_id
|
||||||
|
self._device_name = entry_infos.get(CONF_NAME)
|
||||||
|
self._attr_name = "Central Mode"
|
||||||
|
self._attr_unique_id = "central_mode"
|
||||||
|
self._attr_options = CENTRAL_MODES
|
||||||
|
self._attr_current_option = CENTRAL_MODE_AUTO
|
||||||
|
|
||||||
|
@property
|
||||||
|
def icon(self) -> str | None:
|
||||||
|
return "mdi:form-select"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_info(self) -> DeviceInfo:
|
||||||
|
"""Return the device info."""
|
||||||
|
return DeviceInfo(
|
||||||
|
entry_type=DeviceEntryType.SERVICE,
|
||||||
|
identifiers={(DOMAIN, self._config_id)},
|
||||||
|
name=self._device_name,
|
||||||
|
manufacturer=DEVICE_MANUFACTURER,
|
||||||
|
model=DOMAIN,
|
||||||
|
)
|
||||||
|
|
||||||
|
@overrides
|
||||||
|
async def async_added_to_hass(self) -> None:
|
||||||
|
await super().async_added_to_hass()
|
||||||
|
|
||||||
|
old_state = await self.async_get_last_state()
|
||||||
|
_LOGGER.debug(
|
||||||
|
"%s - Calling async_added_to_hass old_state is %s", self, old_state
|
||||||
|
)
|
||||||
|
if old_state is not None:
|
||||||
|
self.async_select_option = old_state.state
|
||||||
|
|
||||||
|
@callback
|
||||||
|
async def _async_startup_internal(*_):
|
||||||
|
_LOGGER.debug("%s - Calling async_startup_internal", self)
|
||||||
|
await self.notify_central_mode_change()
|
||||||
|
|
||||||
|
if self.hass.state == CoreState.running:
|
||||||
|
await _async_startup_internal()
|
||||||
|
else:
|
||||||
|
self.hass.bus.async_listen_once(
|
||||||
|
EVENT_HOMEASSISTANT_START, _async_startup_internal
|
||||||
|
)
|
||||||
|
|
||||||
|
@overrides
|
||||||
|
async def async_select_option(self, option: str) -> None:
|
||||||
|
"""Change the selected option."""
|
||||||
|
old_option = self._attr_current_option
|
||||||
|
|
||||||
|
if option == old_option:
|
||||||
|
return
|
||||||
|
|
||||||
|
if option in CENTRAL_MODES:
|
||||||
|
self._attr_current_option = option
|
||||||
|
await self.notify_central_mode_change(old_central_mode=old_option)
|
||||||
|
|
||||||
|
async def notify_central_mode_change(self, old_central_mode=None):
|
||||||
|
"""Notify all VTherm that the central_mode have change"""
|
||||||
|
# Update all VTherm states
|
||||||
|
component: EntityComponent[ClimateEntity] = self.hass.data[CLIMATE_DOMAIN]
|
||||||
|
for entity in component.entities:
|
||||||
|
if isinstance(entity, BaseThermostat):
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Changing the central_mode. We have find %s to update",
|
||||||
|
entity.name,
|
||||||
|
)
|
||||||
|
await entity.check_central_mode(
|
||||||
|
self._attr_current_option, old_central_mode
|
||||||
|
)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"VersatileThermostat-{self.name}"
|
||||||
@@ -24,6 +24,7 @@
|
|||||||
"temp_min": "Minimal temperature allowed",
|
"temp_min": "Minimal temperature allowed",
|
||||||
"temp_max": "Maximal temperature allowed",
|
"temp_max": "Maximal temperature allowed",
|
||||||
"device_power": "Device power",
|
"device_power": "Device power",
|
||||||
|
"use_central_mode": "Enable the control by central mode ('central_mode')",
|
||||||
"use_window_feature": "Use window detection",
|
"use_window_feature": "Use window detection",
|
||||||
"use_motion_feature": "Use motion detection",
|
"use_motion_feature": "Use motion detection",
|
||||||
"use_power_feature": "Use power management",
|
"use_power_feature": "Use power management",
|
||||||
@@ -31,6 +32,7 @@
|
|||||||
"use_main_central_config": "Use central main configuration"
|
"use_main_central_config": "Use central main configuration"
|
||||||
},
|
},
|
||||||
"data_description": {
|
"data_description": {
|
||||||
|
"use_central_mode": "Check to enable the control of the VTherm with the select central_mode entities",
|
||||||
"use_main_central_config": "Check to use the central main configuration. Uncheck to use a specific main configuration for this VTherm",
|
"use_main_central_config": "Check to use the central main configuration. Uncheck to use a specific main configuration for this VTherm",
|
||||||
"external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id. Not used if central configuration is selected"
|
"external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id. Not used if central configuration is selected"
|
||||||
}
|
}
|
||||||
@@ -254,6 +256,7 @@
|
|||||||
"temp_min": "Minimal temperature allowed",
|
"temp_min": "Minimal temperature allowed",
|
||||||
"temp_max": "Maximal temperature allowed",
|
"temp_max": "Maximal temperature allowed",
|
||||||
"device_power": "Device power",
|
"device_power": "Device power",
|
||||||
|
"use_central_mode": "Enable the control by central mode ('central_mode')",
|
||||||
"use_window_feature": "Use window detection",
|
"use_window_feature": "Use window detection",
|
||||||
"use_motion_feature": "Use motion detection",
|
"use_motion_feature": "Use motion detection",
|
||||||
"use_power_feature": "Use power management",
|
"use_power_feature": "Use power management",
|
||||||
@@ -261,6 +264,7 @@
|
|||||||
"use_main_central_config": "Use central main configuration"
|
"use_main_central_config": "Use central main configuration"
|
||||||
},
|
},
|
||||||
"data_description": {
|
"data_description": {
|
||||||
|
"use_central_mode": "Check to enable the control of the VTherm with the select central_mode entities",
|
||||||
"use_main_central_config": "Check to use the central main configuration. Uncheck to use a specific configuration for this VTherm",
|
"use_main_central_config": "Check to use the central main configuration. Uncheck to use a specific configuration for this VTherm",
|
||||||
"external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id. Not used if central configuration is selected"
|
"external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id. Not used if central configuration is selected"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -136,6 +136,11 @@ class ThermostatOverClimate(BaseThermostat):
|
|||||||
|
|
||||||
async def _send_regulated_temperature(self, force=False):
|
async def _send_regulated_temperature(self, force=False):
|
||||||
"""Sends the regulated temperature to all underlying"""
|
"""Sends the regulated temperature to all underlying"""
|
||||||
|
|
||||||
|
if self.hvac_mode == HVACMode.OFF:
|
||||||
|
_LOGGER.debug("%s - don't send regulated temperature cause VTherm is off ")
|
||||||
|
return
|
||||||
|
|
||||||
_LOGGER.info(
|
_LOGGER.info(
|
||||||
"%s - Calling ThermostatClimate._send_regulated_temperature force=%s",
|
"%s - Calling ThermostatClimate._send_regulated_temperature force=%s",
|
||||||
self,
|
self,
|
||||||
@@ -234,45 +239,45 @@ class ThermostatOverClimate(BaseThermostat):
|
|||||||
await self.async_set_fan_mode(self._auto_deactivated_fan_mode)
|
await self.async_set_fan_mode(self._auto_deactivated_fan_mode)
|
||||||
|
|
||||||
@overrides
|
@overrides
|
||||||
def post_init(self, entry_infos):
|
def post_init(self, config_entry):
|
||||||
"""Initialize the Thermostat"""
|
"""Initialize the Thermostat"""
|
||||||
|
|
||||||
super().post_init(entry_infos)
|
super().post_init(config_entry)
|
||||||
for climate in [
|
for climate in [
|
||||||
CONF_CLIMATE,
|
CONF_CLIMATE,
|
||||||
CONF_CLIMATE_2,
|
CONF_CLIMATE_2,
|
||||||
CONF_CLIMATE_3,
|
CONF_CLIMATE_3,
|
||||||
CONF_CLIMATE_4,
|
CONF_CLIMATE_4,
|
||||||
]:
|
]:
|
||||||
if entry_infos.get(climate):
|
if config_entry.get(climate):
|
||||||
self._underlyings.append(
|
self._underlyings.append(
|
||||||
UnderlyingClimate(
|
UnderlyingClimate(
|
||||||
hass=self._hass,
|
hass=self._hass,
|
||||||
thermostat=self,
|
thermostat=self,
|
||||||
climate_entity_id=entry_infos.get(climate),
|
climate_entity_id=config_entry.get(climate),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
self.choose_auto_regulation_mode(
|
self.choose_auto_regulation_mode(
|
||||||
entry_infos.get(CONF_AUTO_REGULATION_MODE)
|
config_entry.get(CONF_AUTO_REGULATION_MODE)
|
||||||
if entry_infos.get(CONF_AUTO_REGULATION_MODE) is not None
|
if config_entry.get(CONF_AUTO_REGULATION_MODE) is not None
|
||||||
else CONF_AUTO_REGULATION_NONE
|
else CONF_AUTO_REGULATION_NONE
|
||||||
)
|
)
|
||||||
|
|
||||||
self._auto_regulation_dtemp = (
|
self._auto_regulation_dtemp = (
|
||||||
entry_infos.get(CONF_AUTO_REGULATION_DTEMP)
|
config_entry.get(CONF_AUTO_REGULATION_DTEMP)
|
||||||
if entry_infos.get(CONF_AUTO_REGULATION_DTEMP) is not None
|
if config_entry.get(CONF_AUTO_REGULATION_DTEMP) is not None
|
||||||
else 0.5
|
else 0.5
|
||||||
)
|
)
|
||||||
self._auto_regulation_period_min = (
|
self._auto_regulation_period_min = (
|
||||||
entry_infos.get(CONF_AUTO_REGULATION_PERIOD_MIN)
|
config_entry.get(CONF_AUTO_REGULATION_PERIOD_MIN)
|
||||||
if entry_infos.get(CONF_AUTO_REGULATION_PERIOD_MIN) is not None
|
if config_entry.get(CONF_AUTO_REGULATION_PERIOD_MIN) is not None
|
||||||
else 5
|
else 5
|
||||||
)
|
)
|
||||||
|
|
||||||
self._auto_fan_mode = (
|
self._auto_fan_mode = (
|
||||||
entry_infos.get(CONF_AUTO_FAN_MODE)
|
config_entry.get(CONF_AUTO_FAN_MODE)
|
||||||
if entry_infos.get(CONF_AUTO_FAN_MODE) is not None
|
if config_entry.get(CONF_AUTO_FAN_MODE) is not None
|
||||||
else CONF_AUTO_FAN_NONE
|
else CONF_AUTO_FAN_NONE
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -48,9 +48,9 @@ class ThermostatOverSwitch(BaseThermostat):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# useless for now
|
# useless for now
|
||||||
# def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
|
# def __init__(self, hass: HomeAssistant, unique_id, name, config_entry) -> None:
|
||||||
# """Initialize the thermostat over switch."""
|
# """Initialize the thermostat over switch."""
|
||||||
# super().__init__(hass, unique_id, name, entry_infos)
|
# super().__init__(hass, unique_id, name, config_entry)
|
||||||
_is_inversed: bool = None
|
_is_inversed: bool = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -72,10 +72,10 @@ class ThermostatOverSwitch(BaseThermostat):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
@overrides
|
@overrides
|
||||||
def post_init(self, entry_infos):
|
def post_init(self, config_entry):
|
||||||
"""Initialize the Thermostat"""
|
"""Initialize the Thermostat"""
|
||||||
|
|
||||||
super().post_init(entry_infos)
|
super().post_init(config_entry)
|
||||||
|
|
||||||
self._prop_algorithm = PropAlgorithm(
|
self._prop_algorithm = PropAlgorithm(
|
||||||
self._proportional_function,
|
self._proportional_function,
|
||||||
@@ -85,13 +85,13 @@ class ThermostatOverSwitch(BaseThermostat):
|
|||||||
self._minimal_activation_delay,
|
self._minimal_activation_delay,
|
||||||
)
|
)
|
||||||
|
|
||||||
lst_switches = [entry_infos.get(CONF_HEATER)]
|
lst_switches = [config_entry.get(CONF_HEATER)]
|
||||||
if entry_infos.get(CONF_HEATER_2):
|
if config_entry.get(CONF_HEATER_2):
|
||||||
lst_switches.append(entry_infos.get(CONF_HEATER_2))
|
lst_switches.append(config_entry.get(CONF_HEATER_2))
|
||||||
if entry_infos.get(CONF_HEATER_3):
|
if config_entry.get(CONF_HEATER_3):
|
||||||
lst_switches.append(entry_infos.get(CONF_HEATER_3))
|
lst_switches.append(config_entry.get(CONF_HEATER_3))
|
||||||
if entry_infos.get(CONF_HEATER_4):
|
if config_entry.get(CONF_HEATER_4):
|
||||||
lst_switches.append(entry_infos.get(CONF_HEATER_4))
|
lst_switches.append(config_entry.get(CONF_HEATER_4))
|
||||||
|
|
||||||
delta_cycle = self._cycle_min * 60 / len(lst_switches)
|
delta_cycle = self._cycle_min * 60 / len(lst_switches)
|
||||||
for idx, switch in enumerate(lst_switches):
|
for idx, switch in enumerate(lst_switches):
|
||||||
@@ -104,7 +104,7 @@ class ThermostatOverSwitch(BaseThermostat):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
self._is_inversed = entry_infos.get(CONF_INVERSE_SWITCH) is True
|
self._is_inversed = config_entry.get(CONF_INVERSE_SWITCH) is True
|
||||||
self._should_relaunch_control_heating = False
|
self._should_relaunch_control_heating = False
|
||||||
|
|
||||||
@overrides
|
@overrides
|
||||||
|
|||||||
@@ -3,7 +3,10 @@
|
|||||||
import logging
|
import logging
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
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.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.components.climate import HVACMode
|
from homeassistant.components.climate import HVACMode
|
||||||
|
|
||||||
@@ -16,39 +19,53 @@ from .underlyings import UnderlyingValve
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class ThermostatOverValve(BaseThermostat):
|
class ThermostatOverValve(BaseThermostat):
|
||||||
"""Representation of a class for a Versatile Thermostat over a Valve"""
|
"""Representation of a class for a Versatile Thermostat over a Valve"""
|
||||||
|
|
||||||
_entity_component_unrecorded_attributes = BaseThermostat._entity_component_unrecorded_attributes.union(frozenset(
|
_entity_component_unrecorded_attributes = (
|
||||||
{
|
BaseThermostat._entity_component_unrecorded_attributes.union(
|
||||||
"is_over_valve", "underlying_valve_0", "underlying_valve_1",
|
frozenset(
|
||||||
"underlying_valve_2", "underlying_valve_3", "on_time_sec", "off_time_sec",
|
{
|
||||||
"cycle_min", "function", "tpi_coef_int", "tpi_coef_ext"
|
"is_over_valve",
|
||||||
}))
|
"underlying_valve_0",
|
||||||
|
"underlying_valve_1",
|
||||||
|
"underlying_valve_2",
|
||||||
|
"underlying_valve_3",
|
||||||
|
"on_time_sec",
|
||||||
|
"off_time_sec",
|
||||||
|
"cycle_min",
|
||||||
|
"function",
|
||||||
|
"tpi_coef_int",
|
||||||
|
"tpi_coef_ext",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
# Useless for now
|
# Useless for now
|
||||||
# def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
|
# def __init__(self, hass: HomeAssistant, unique_id, name, config_entry) -> None:
|
||||||
# """Initialize the thermostat over switch."""
|
# """Initialize the thermostat over switch."""
|
||||||
# super().__init__(hass, unique_id, name, entry_infos)
|
# super().__init__(hass, unique_id, name, config_entry)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_over_valve(self) -> bool:
|
def is_over_valve(self) -> bool:
|
||||||
""" True if the Thermostat is over_valve"""
|
"""True if the Thermostat is over_valve"""
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def valve_open_percent(self) -> int:
|
def valve_open_percent(self) -> int:
|
||||||
""" Gives the percentage of valve needed"""
|
"""Gives the percentage of valve needed"""
|
||||||
if self._hvac_mode == HVACMode.OFF:
|
if self._hvac_mode == HVACMode.OFF:
|
||||||
return 0
|
return 0
|
||||||
else:
|
else:
|
||||||
return round(max(0, min(self.proportional_algorithm.on_percent, 1)) * 100)
|
return round(max(0, min(self.proportional_algorithm.on_percent, 1)) * 100)
|
||||||
|
|
||||||
@overrides
|
@overrides
|
||||||
def post_init(self, entry_infos):
|
def post_init(self, config_entry):
|
||||||
""" Initialize the Thermostat"""
|
"""Initialize the Thermostat"""
|
||||||
|
|
||||||
super().post_init(entry_infos)
|
super().post_init(config_entry)
|
||||||
self._prop_algorithm = PropAlgorithm(
|
self._prop_algorithm = PropAlgorithm(
|
||||||
self._proportional_function,
|
self._proportional_function,
|
||||||
self._tpi_coef_int,
|
self._tpi_coef_int,
|
||||||
@@ -57,21 +74,17 @@ class ThermostatOverValve(BaseThermostat):
|
|||||||
self._minimal_activation_delay,
|
self._minimal_activation_delay,
|
||||||
)
|
)
|
||||||
|
|
||||||
lst_valves = [entry_infos.get(CONF_VALVE)]
|
lst_valves = [config_entry.get(CONF_VALVE)]
|
||||||
if entry_infos.get(CONF_VALVE_2):
|
if config_entry.get(CONF_VALVE_2):
|
||||||
lst_valves.append(entry_infos.get(CONF_VALVE_2))
|
lst_valves.append(config_entry.get(CONF_VALVE_2))
|
||||||
if entry_infos.get(CONF_VALVE_3):
|
if config_entry.get(CONF_VALVE_3):
|
||||||
lst_valves.append(entry_infos.get(CONF_VALVE_3))
|
lst_valves.append(config_entry.get(CONF_VALVE_3))
|
||||||
if entry_infos.get(CONF_VALVE_4):
|
if config_entry.get(CONF_VALVE_4):
|
||||||
lst_valves.append(entry_infos.get(CONF_VALVE_4))
|
lst_valves.append(config_entry.get(CONF_VALVE_4))
|
||||||
|
|
||||||
for _, valve in enumerate(lst_valves):
|
for _, valve in enumerate(lst_valves):
|
||||||
self._underlyings.append(
|
self._underlyings.append(
|
||||||
UnderlyingValve(
|
UnderlyingValve(hass=self._hass, thermostat=self, valve_entity_id=valve)
|
||||||
hass=self._hass,
|
|
||||||
thermostat=self,
|
|
||||||
valve_entity_id=valve
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
self._should_relaunch_control_heating = False
|
self._should_relaunch_control_heating = False
|
||||||
@@ -89,7 +102,7 @@ class ThermostatOverValve(BaseThermostat):
|
|||||||
async_track_state_change_event(
|
async_track_state_change_event(
|
||||||
self.hass, [valve.entity_id], self._async_valve_changed
|
self.hass, [valve.entity_id], self._async_valve_changed
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Start the control_heating
|
# Start the control_heating
|
||||||
# starts a cycle
|
# starts a cycle
|
||||||
@@ -107,29 +120,34 @@ class ThermostatOverValve(BaseThermostat):
|
|||||||
This method just log the change. It changes nothing to avoid loops.
|
This method just log the change. It changes nothing to avoid loops.
|
||||||
"""
|
"""
|
||||||
new_state = event.data.get("new_state")
|
new_state = event.data.get("new_state")
|
||||||
_LOGGER.debug("%s - _async_valve_changed new_state is %s", self, new_state.state)
|
_LOGGER.debug(
|
||||||
|
"%s - _async_valve_changed new_state is %s", self, new_state.state
|
||||||
|
)
|
||||||
|
|
||||||
@overrides
|
@overrides
|
||||||
def update_custom_attributes(self):
|
def update_custom_attributes(self):
|
||||||
""" Custom attributes """
|
"""Custom attributes"""
|
||||||
super().update_custom_attributes()
|
super().update_custom_attributes()
|
||||||
self._attr_extra_state_attributes["valve_open_percent"] = self.valve_open_percent
|
self._attr_extra_state_attributes[
|
||||||
|
"valve_open_percent"
|
||||||
|
] = self.valve_open_percent
|
||||||
self._attr_extra_state_attributes["is_over_valve"] = self.is_over_valve
|
self._attr_extra_state_attributes["is_over_valve"] = self.is_over_valve
|
||||||
self._attr_extra_state_attributes["underlying_valve_0"] = (
|
self._attr_extra_state_attributes["underlying_valve_0"] = self._underlyings[
|
||||||
self._underlyings[0].entity_id)
|
0
|
||||||
|
].entity_id
|
||||||
self._attr_extra_state_attributes["underlying_valve_1"] = (
|
self._attr_extra_state_attributes["underlying_valve_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_valve_2"] = (
|
self._attr_extra_state_attributes["underlying_valve_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_valve_3"] = (
|
self._attr_extra_state_attributes["underlying_valve_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[
|
self._attr_extra_state_attributes[
|
||||||
"on_percent"
|
"on_percent"
|
||||||
] = self._prop_algorithm.on_percent
|
] = self._prop_algorithm.on_percent
|
||||||
self._attr_extra_state_attributes[
|
self._attr_extra_state_attributes[
|
||||||
"on_time_sec"
|
"on_time_sec"
|
||||||
] = self._prop_algorithm.on_time_sec
|
] = self._prop_algorithm.on_time_sec
|
||||||
@@ -162,9 +180,7 @@ class ThermostatOverValve(BaseThermostat):
|
|||||||
)
|
)
|
||||||
|
|
||||||
for under in self._underlyings:
|
for under in self._underlyings:
|
||||||
under.set_valve_open_percent(
|
under.set_valve_open_percent()
|
||||||
self._prop_algorithm.on_percent
|
|
||||||
)
|
|
||||||
|
|
||||||
self.update_custom_attributes()
|
self.update_custom_attributes()
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
@@ -185,4 +201,4 @@ class ThermostatOverValve(BaseThermostat):
|
|||||||
self,
|
self,
|
||||||
added_energy,
|
added_energy,
|
||||||
self._total_energy,
|
self._total_energy,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
"temp_min": "Minimal temperature allowed",
|
"temp_min": "Minimal temperature allowed",
|
||||||
"temp_max": "Maximal temperature allowed",
|
"temp_max": "Maximal temperature allowed",
|
||||||
"device_power": "Device power",
|
"device_power": "Device power",
|
||||||
|
"use_central_mode": "Enable the control by central mode ('central_mode')",
|
||||||
"use_window_feature": "Use window detection",
|
"use_window_feature": "Use window detection",
|
||||||
"use_motion_feature": "Use motion detection",
|
"use_motion_feature": "Use motion detection",
|
||||||
"use_power_feature": "Use power management",
|
"use_power_feature": "Use power management",
|
||||||
@@ -31,6 +32,7 @@
|
|||||||
"use_main_central_config": "Use central main configuration"
|
"use_main_central_config": "Use central main configuration"
|
||||||
},
|
},
|
||||||
"data_description": {
|
"data_description": {
|
||||||
|
"use_central_mode": "Check to enable the control of the VTherm with the select central_mode entities",
|
||||||
"use_main_central_config": "Check to use the central main configuration. Uncheck to use a specific main configuration for this VTherm",
|
"use_main_central_config": "Check to use the central main configuration. Uncheck to use a specific main configuration for this VTherm",
|
||||||
"external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id. Not used if central configuration is selected"
|
"external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id. Not used if central configuration is selected"
|
||||||
}
|
}
|
||||||
@@ -254,6 +256,7 @@
|
|||||||
"temp_min": "Minimal temperature allowed",
|
"temp_min": "Minimal temperature allowed",
|
||||||
"temp_max": "Maximal temperature allowed",
|
"temp_max": "Maximal temperature allowed",
|
||||||
"device_power": "Device power",
|
"device_power": "Device power",
|
||||||
|
"use_central_mode": "Enable the control by central mode ('central_mode')",
|
||||||
"use_window_feature": "Use window detection",
|
"use_window_feature": "Use window detection",
|
||||||
"use_motion_feature": "Use motion detection",
|
"use_motion_feature": "Use motion detection",
|
||||||
"use_power_feature": "Use power management",
|
"use_power_feature": "Use power management",
|
||||||
@@ -261,6 +264,7 @@
|
|||||||
"use_main_central_config": "Use central main configuration"
|
"use_main_central_config": "Use central main configuration"
|
||||||
},
|
},
|
||||||
"data_description": {
|
"data_description": {
|
||||||
|
"use_central_mode": "Check to enable the control of the VTherm with the select central_mode entities",
|
||||||
"use_main_central_config": "Check to use the central main configuration. Uncheck to use a specific configuration for this VTherm",
|
"use_main_central_config": "Check to use the central main configuration. Uncheck to use a specific configuration for this VTherm",
|
||||||
"external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id. Not used if central configuration is selected"
|
"external_temperature_sensor_entity_id": "Outdoor temperature sensor entity id. Not used if central configuration is selected"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
"temp_min": "Température minimale permise",
|
"temp_min": "Température minimale permise",
|
||||||
"temp_max": "Température maximale permise",
|
"temp_max": "Température maximale permise",
|
||||||
"device_power": "Puissance de l'équipement",
|
"device_power": "Puissance de l'équipement",
|
||||||
|
"use_central_mode": "Autoriser le controle par le mode central ('central_mode`)",
|
||||||
"use_window_feature": "Avec détection des ouvertures",
|
"use_window_feature": "Avec détection des ouvertures",
|
||||||
"use_motion_feature": "Avec détection de mouvement",
|
"use_motion_feature": "Avec détection de mouvement",
|
||||||
"use_power_feature": "Avec gestion de la puissance",
|
"use_power_feature": "Avec gestion de la puissance",
|
||||||
@@ -31,6 +32,7 @@
|
|||||||
"use_main_central_config": "Utiliser la configuration centrale principale"
|
"use_main_central_config": "Utiliser la configuration centrale principale"
|
||||||
},
|
},
|
||||||
"data_description": {
|
"data_description": {
|
||||||
|
"use_central_mode": "Cochez pour autoriser le contrôle du VTherm par la liste déroulante 'central_mode' de l'entité configuration centrale",
|
||||||
"external_temperature_sensor_entity_id": "Entity id du capteur de température extérieure. N'est pas utilisé si la configuration centrale est utilisée",
|
"external_temperature_sensor_entity_id": "Entity id du capteur de température extérieure. N'est pas utilisé si la configuration centrale est utilisée",
|
||||||
"use_main_central_config": "Cochez pour utiliser la configuration centrale principale. Décochez et saisissez les attributs pour utiliser une configuration spécifique principale"
|
"use_main_central_config": "Cochez pour utiliser la configuration centrale principale. Décochez et saisissez les attributs pour utiliser une configuration spécifique principale"
|
||||||
}
|
}
|
||||||
@@ -254,6 +256,7 @@
|
|||||||
"temp_min": "Température minimale permise",
|
"temp_min": "Température minimale permise",
|
||||||
"temp_max": "Température maximale permise",
|
"temp_max": "Température maximale permise",
|
||||||
"device_power": "Puissance de l'équipement",
|
"device_power": "Puissance de l'équipement",
|
||||||
|
"use_central_mode": "Autoriser le controle par le mode central ('central_mode`)",
|
||||||
"use_window_feature": "Avec détection des ouvertures",
|
"use_window_feature": "Avec détection des ouvertures",
|
||||||
"use_motion_feature": "Avec détection de mouvement",
|
"use_motion_feature": "Avec détection de mouvement",
|
||||||
"use_power_feature": "Avec gestion de la puissance",
|
"use_power_feature": "Avec gestion de la puissance",
|
||||||
@@ -261,6 +264,7 @@
|
|||||||
"use_main_central_config": "Utiliser la configuration centrale"
|
"use_main_central_config": "Utiliser la configuration centrale"
|
||||||
},
|
},
|
||||||
"data_description": {
|
"data_description": {
|
||||||
|
"use_central_mode": "Cochez pour autoriser le contrôle du VTherm par la liste déroulante 'central_mode' de l'entité configuration centrale",
|
||||||
"use_main_central_config": "Cochez pour utiliser la configuration centrale. Décochez et saisissez les attributs pour utiliser une configuration spécifique",
|
"use_main_central_config": "Cochez pour utiliser la configuration centrale. Décochez et saisissez les attributs pour utiliser une configuration spécifique",
|
||||||
"external_temperature_sensor_entity_id": "Entity id du capteur de température extérieure. N'est pas utilisé si la configuration centrale est utilisée"
|
"external_temperature_sensor_entity_id": "Entity id du capteur de température extérieure. N'est pas utilisé si la configuration centrale est utilisée"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from typing import Any
|
|||||||
from enum import StrEnum
|
from enum import StrEnum
|
||||||
|
|
||||||
from homeassistant.const import ATTR_ENTITY_ID, STATE_ON, UnitOfTemperature
|
from homeassistant.const import ATTR_ENTITY_ID, STATE_ON, UnitOfTemperature
|
||||||
|
from homeassistant.core import State
|
||||||
|
|
||||||
from homeassistant.exceptions import ServiceNotFound
|
from homeassistant.exceptions import ServiceNotFound
|
||||||
|
|
||||||
@@ -111,18 +112,18 @@ class UnderlyingEntity:
|
|||||||
# This should be the correct way to handle turn_off and turn_on but this breaks the unit test
|
# This should be the correct way to handle turn_off and turn_on but this breaks the unit test
|
||||||
# will an not understandable error: TypeError: object MagicMock can't be used in 'await' expression
|
# will an not understandable error: TypeError: object MagicMock can't be used in 'await' expression
|
||||||
async def turn_off(self):
|
async def turn_off(self):
|
||||||
""" Turn off the underlying equipement.
|
"""Turn off the underlying equipement.
|
||||||
Need to be overriden"""
|
Need to be overriden"""
|
||||||
return NotImplementedError
|
return NotImplementedError
|
||||||
|
|
||||||
async def turn_on(self):
|
async def turn_on(self):
|
||||||
""" Turn off the underlying equipement.
|
"""Turn off the underlying equipement.
|
||||||
Need to be overriden"""
|
Need to be overriden"""
|
||||||
return NotImplementedError
|
return NotImplementedError
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_inversed(self):
|
def is_inversed(self):
|
||||||
""" Tells if the switch command should be inversed"""
|
"""Tells if the switch command should be inversed"""
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def remove_entity(self):
|
def remove_entity(self):
|
||||||
@@ -164,7 +165,11 @@ class UnderlyingEntity:
|
|||||||
"""Starting cycle for switch"""
|
"""Starting cycle for switch"""
|
||||||
|
|
||||||
def _cancel_cycle(self):
|
def _cancel_cycle(self):
|
||||||
""" Stops an eventual cycle """
|
"""Stops an eventual cycle"""
|
||||||
|
|
||||||
|
def cap_sent_value(self, value) -> float:
|
||||||
|
"""capping of the value send to the underlying eqt"""
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
class UnderlyingSwitch(UnderlyingEntity):
|
class UnderlyingSwitch(UnderlyingEntity):
|
||||||
@@ -205,7 +210,7 @@ class UnderlyingSwitch(UnderlyingEntity):
|
|||||||
@overrides
|
@overrides
|
||||||
@property
|
@property
|
||||||
def is_inversed(self):
|
def is_inversed(self):
|
||||||
""" Tells if the switch command should be inversed"""
|
"""Tells if the switch command should be inversed"""
|
||||||
return self._thermostat.is_inversed
|
return self._thermostat.is_inversed
|
||||||
|
|
||||||
# @overrides this breaks some unit tests TypeError: object MagicMock can't be used in 'await' expression
|
# @overrides this breaks some unit tests TypeError: object MagicMock can't be used in 'await' expression
|
||||||
@@ -227,14 +232,16 @@ class UnderlyingSwitch(UnderlyingEntity):
|
|||||||
def is_device_active(self):
|
def is_device_active(self):
|
||||||
"""If the toggleable device is currently active."""
|
"""If the toggleable device is currently active."""
|
||||||
real_state = self._hass.states.is_state(self._entity_id, STATE_ON)
|
real_state = self._hass.states.is_state(self._entity_id, STATE_ON)
|
||||||
return (self.is_inversed and not real_state) or (not self.is_inversed and real_state)
|
return (self.is_inversed and not real_state) or (
|
||||||
|
not self.is_inversed and real_state
|
||||||
|
)
|
||||||
|
|
||||||
# @overrides this breaks some unit tests TypeError: object MagicMock can't be used in 'await' expression
|
# @overrides this breaks some unit tests TypeError: object MagicMock can't be used in 'await' expression
|
||||||
async def turn_off(self):
|
async def turn_off(self):
|
||||||
"""Turn heater toggleable device off."""
|
"""Turn heater toggleable device off."""
|
||||||
_LOGGER.debug("%s - Stopping underlying entity %s", self, self._entity_id)
|
_LOGGER.debug("%s - Stopping underlying entity %s", self, self._entity_id)
|
||||||
command = SERVICE_TURN_OFF if not self.is_inversed else SERVICE_TURN_ON
|
command = SERVICE_TURN_OFF if not self.is_inversed else SERVICE_TURN_ON
|
||||||
domain = self._entity_id.split('.')[0]
|
domain = self._entity_id.split(".")[0]
|
||||||
# This may fails if called after shutdown
|
# This may fails if called after shutdown
|
||||||
try:
|
try:
|
||||||
data = {ATTR_ENTITY_ID: self._entity_id}
|
data = {ATTR_ENTITY_ID: self._entity_id}
|
||||||
@@ -250,7 +257,7 @@ class UnderlyingSwitch(UnderlyingEntity):
|
|||||||
"""Turn heater toggleable device on."""
|
"""Turn heater toggleable device on."""
|
||||||
_LOGGER.debug("%s - Starting underlying entity %s", self, self._entity_id)
|
_LOGGER.debug("%s - Starting underlying entity %s", self, self._entity_id)
|
||||||
command = SERVICE_TURN_ON if not self.is_inversed else SERVICE_TURN_OFF
|
command = SERVICE_TURN_ON if not self.is_inversed else SERVICE_TURN_OFF
|
||||||
domain = self._entity_id.split('.')[0]
|
domain = self._entity_id.split(".")[0]
|
||||||
try:
|
try:
|
||||||
data = {ATTR_ENTITY_ID: self._entity_id}
|
data = {ATTR_ENTITY_ID: self._entity_id}
|
||||||
await self._hass.services.async_call(
|
await self._hass.services.async_call(
|
||||||
@@ -261,7 +268,6 @@ class UnderlyingSwitch(UnderlyingEntity):
|
|||||||
except ServiceNotFound as err:
|
except ServiceNotFound as err:
|
||||||
_LOGGER.error(err)
|
_LOGGER.error(err)
|
||||||
|
|
||||||
|
|
||||||
@overrides
|
@overrides
|
||||||
async def start_cycle(
|
async def start_cycle(
|
||||||
self,
|
self,
|
||||||
@@ -490,10 +496,14 @@ class UnderlyingClimate(UnderlyingEntity):
|
|||||||
def is_device_active(self):
|
def is_device_active(self):
|
||||||
"""If the toggleable device is currently active."""
|
"""If the toggleable device is currently active."""
|
||||||
if self.is_initialized:
|
if self.is_initialized:
|
||||||
return self._underlying_climate.hvac_mode != HVACMode.OFF and self._underlying_climate.hvac_action not in [
|
return (
|
||||||
HVACAction.IDLE,
|
self._underlying_climate.hvac_mode != HVACMode.OFF
|
||||||
HVACAction.OFF,
|
and self._underlying_climate.hvac_action
|
||||||
]
|
not in [
|
||||||
|
HVACAction.IDLE,
|
||||||
|
HVACAction.OFF,
|
||||||
|
]
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -550,7 +560,7 @@ class UnderlyingClimate(UnderlyingEntity):
|
|||||||
return
|
return
|
||||||
data = {
|
data = {
|
||||||
ATTR_ENTITY_ID: self._entity_id,
|
ATTR_ENTITY_ID: self._entity_id,
|
||||||
"temperature": temperature,
|
"temperature": self.cap_sent_value(temperature),
|
||||||
"target_temp_high": max_temp,
|
"target_temp_high": max_temp,
|
||||||
"target_temp_low": min_temp,
|
"target_temp_low": min_temp,
|
||||||
}
|
}
|
||||||
@@ -664,6 +674,40 @@ class UnderlyingClimate(UnderlyingEntity):
|
|||||||
return None
|
return None
|
||||||
return self._underlying_climate.turn_aux_heat_off()
|
return self._underlying_climate.turn_aux_heat_off()
|
||||||
|
|
||||||
|
@overrides
|
||||||
|
def cap_sent_value(self, value) -> float:
|
||||||
|
"""Try to adapt the target temp value to the min_temp / max_temp found
|
||||||
|
in the underlying entity (if any)"""
|
||||||
|
|
||||||
|
if not self.is_initialized:
|
||||||
|
return value
|
||||||
|
|
||||||
|
# Gets the min_temp and max_temp
|
||||||
|
if (
|
||||||
|
self._underlying_climate.min_temp is not None
|
||||||
|
and self._underlying_climate is not None
|
||||||
|
):
|
||||||
|
min_val = self._underlying_climate.min_temp
|
||||||
|
max_val = self._underlying_climate.max_temp
|
||||||
|
|
||||||
|
new_value = max(min_val, min(value, max_val))
|
||||||
|
else:
|
||||||
|
_LOGGER.debug("%s - no min and max attributes on underlying", self)
|
||||||
|
new_value = value
|
||||||
|
|
||||||
|
if new_value != value:
|
||||||
|
_LOGGER.info(
|
||||||
|
"%s - Target temp have been updated due min, max of the underlying entity. new_value=%.0f value=%.0f min=%.0f max=%.0f",
|
||||||
|
self,
|
||||||
|
new_value,
|
||||||
|
value,
|
||||||
|
min_val,
|
||||||
|
max_val,
|
||||||
|
)
|
||||||
|
|
||||||
|
return new_value
|
||||||
|
|
||||||
|
|
||||||
class UnderlyingValve(UnderlyingEntity):
|
class UnderlyingValve(UnderlyingEntity):
|
||||||
"""Represent a underlying switch"""
|
"""Represent a underlying switch"""
|
||||||
|
|
||||||
@@ -672,10 +716,7 @@ class UnderlyingValve(UnderlyingEntity):
|
|||||||
_percent_open: int
|
_percent_open: int
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self, hass: HomeAssistant, thermostat: Any, valve_entity_id: str
|
||||||
hass: HomeAssistant,
|
|
||||||
thermostat: Any,
|
|
||||||
valve_entity_id: str
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the underlying switch"""
|
"""Initialize the underlying switch"""
|
||||||
|
|
||||||
@@ -689,13 +730,14 @@ class UnderlyingValve(UnderlyingEntity):
|
|||||||
self._should_relaunch_control_heating = False
|
self._should_relaunch_control_heating = False
|
||||||
self._hvac_mode = None
|
self._hvac_mode = None
|
||||||
self._percent_open = self._thermostat.valve_open_percent
|
self._percent_open = self._thermostat.valve_open_percent
|
||||||
|
self._valve_entity_id = valve_entity_id
|
||||||
|
|
||||||
async def send_percent_open(self):
|
async def send_percent_open(self):
|
||||||
""" Send the percent open to the underlying valve """
|
"""Send the percent open to the underlying valve"""
|
||||||
# This may fails if called after shutdown
|
# This may fails if called after shutdown
|
||||||
try:
|
try:
|
||||||
data = { ATTR_ENTITY_ID: self._entity_id, "value": self._percent_open }
|
data = {ATTR_ENTITY_ID: self._entity_id, "value": self._percent_open}
|
||||||
domain = self._entity_id.split('.')[0]
|
domain = self._entity_id.split(".")[0]
|
||||||
await self._hass.services.async_call(
|
await self._hass.services.async_call(
|
||||||
domain,
|
domain,
|
||||||
SERVICE_SET_VALUE,
|
SERVICE_SET_VALUE,
|
||||||
@@ -734,7 +776,7 @@ class UnderlyingValve(UnderlyingEntity):
|
|||||||
# To test if real device is open but this is causing some side effect
|
# To test if real device is open but this is causing some side effect
|
||||||
# because the activation can be deferred -
|
# because the activation can be deferred -
|
||||||
# or float(self._hass.states.get(self._entity_id).state) > 0
|
# or float(self._hass.states.get(self._entity_id).state) > 0
|
||||||
except Exception: # pylint: disable=broad-exception-caught
|
except Exception: # pylint: disable=broad-exception-caught
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@overrides
|
@overrides
|
||||||
@@ -748,11 +790,43 @@ class UnderlyingValve(UnderlyingEntity):
|
|||||||
):
|
):
|
||||||
"""We use this function to change the on_percent"""
|
"""We use this function to change the on_percent"""
|
||||||
if force:
|
if force:
|
||||||
|
self._percent_open = self.cap_sent_value(self._percent_open)
|
||||||
await self.send_percent_open()
|
await self.send_percent_open()
|
||||||
|
|
||||||
def set_valve_open_percent(self, percent):
|
@overrides
|
||||||
""" Update the valve open percent """
|
def cap_sent_value(self, value) -> float:
|
||||||
caped_val = self._thermostat.valve_open_percent
|
"""Try to adapt the open_percent value to the min / max found
|
||||||
|
in the underlying entity (if any)"""
|
||||||
|
|
||||||
|
# Gets the last number state
|
||||||
|
valve_state: State = self._hass.states.get(self._valve_entity_id)
|
||||||
|
if valve_state is None:
|
||||||
|
return value
|
||||||
|
|
||||||
|
if "min" in valve_state.attributes and "max" in valve_state.attributes:
|
||||||
|
min_val = valve_state.attributes["min"]
|
||||||
|
max_val = valve_state.attributes["max"]
|
||||||
|
|
||||||
|
new_value = round(max(min_val, min(value, max_val)))
|
||||||
|
else:
|
||||||
|
_LOGGER.debug("%s - no min and max attributes on underlying", self)
|
||||||
|
new_value = value
|
||||||
|
|
||||||
|
if new_value != value:
|
||||||
|
_LOGGER.info(
|
||||||
|
"%s - Valve open percent have been updated due min, max of the underlying entity. new_value=%.0f value=%.0f min=%.0f max=%.0f",
|
||||||
|
self,
|
||||||
|
new_value,
|
||||||
|
value,
|
||||||
|
min_val,
|
||||||
|
max_val,
|
||||||
|
)
|
||||||
|
|
||||||
|
return new_value
|
||||||
|
|
||||||
|
def set_valve_open_percent(self):
|
||||||
|
"""Update the valve open percent"""
|
||||||
|
caped_val = self.cap_sent_value(self._thermostat.valve_open_percent)
|
||||||
if self._percent_open == caped_val:
|
if self._percent_open == caped_val:
|
||||||
# No changes
|
# No changes
|
||||||
return
|
return
|
||||||
@@ -760,7 +834,9 @@ class UnderlyingValve(UnderlyingEntity):
|
|||||||
self._percent_open = caped_val
|
self._percent_open = caped_val
|
||||||
# Send the new command to valve via a service call
|
# Send the new command to valve via a service call
|
||||||
|
|
||||||
_LOGGER.info("%s - Setting valve ouverture percent to %s", self, self._percent_open)
|
_LOGGER.info(
|
||||||
|
"%s - Setting valve ouverture percent to %s", self, self._percent_open
|
||||||
|
)
|
||||||
# Send the change to the valve, in background
|
# Send the change to the valve, in background
|
||||||
self._hass.create_task(self.send_percent_open())
|
self._hass.create_task(self.send_percent_open())
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ class VersatileThermostatAPI(dict):
|
|||||||
"""The VersatileThermostatAPI"""
|
"""The VersatileThermostatAPI"""
|
||||||
|
|
||||||
_hass: HomeAssistant = None
|
_hass: HomeAssistant = None
|
||||||
# _entries: Dict(str, ConfigEntry)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_vtherm_api(cls, hass=None):
|
def get_vtherm_api(cls, hass=None):
|
||||||
@@ -64,14 +63,12 @@ class VersatileThermostatAPI(dict):
|
|||||||
def add_entry(self, entry: ConfigEntry):
|
def add_entry(self, entry: ConfigEntry):
|
||||||
"""Add a new entry"""
|
"""Add a new entry"""
|
||||||
_LOGGER.debug("Add the entry %s", entry.entry_id)
|
_LOGGER.debug("Add the entry %s", entry.entry_id)
|
||||||
# self._entries[entry.entry_id] = entry
|
|
||||||
# Add the entry in hass.data
|
# Add the entry in hass.data
|
||||||
VersatileThermostatAPI._hass.data[DOMAIN][entry.entry_id] = entry
|
VersatileThermostatAPI._hass.data[DOMAIN][entry.entry_id] = entry
|
||||||
|
|
||||||
def remove_entry(self, entry: ConfigEntry):
|
def remove_entry(self, entry: ConfigEntry):
|
||||||
"""Remove an entry"""
|
"""Remove an entry"""
|
||||||
_LOGGER.debug("Remove the entry %s", entry.entry_id)
|
_LOGGER.debug("Remove the entry %s", entry.entry_id)
|
||||||
# self._entries.pop(entry.entry_id)
|
|
||||||
VersatileThermostatAPI._hass.data[DOMAIN].pop(entry.entry_id)
|
VersatileThermostatAPI._hass.data[DOMAIN].pop(entry.entry_id)
|
||||||
# If not more entries are preset, remove the API
|
# If not more entries are preset, remove the API
|
||||||
if len(self) == 0:
|
if len(self) == 0:
|
||||||
|
|||||||
7
pyrightconfig.json
Normal file
7
pyrightconfig.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"include": [
|
||||||
|
"custom_components/versatile_thermostat/**",
|
||||||
|
"homeassistant/**"
|
||||||
|
],
|
||||||
|
"reportShadowedImports": false
|
||||||
|
}
|
||||||
@@ -185,6 +185,7 @@ class MockClimate(ClimateEntity):
|
|||||||
hvac_mode: HVACMode = HVACMode.OFF,
|
hvac_mode: HVACMode = HVACMode.OFF,
|
||||||
hvac_action: HVACAction = HVACAction.OFF,
|
hvac_action: HVACAction = HVACAction.OFF,
|
||||||
fan_modes: list[str] = None,
|
fan_modes: list[str] = None,
|
||||||
|
hvac_modes: list[str] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the thermostat."""
|
"""Initialize the thermostat."""
|
||||||
|
|
||||||
@@ -200,7 +201,11 @@ class MockClimate(ClimateEntity):
|
|||||||
HVACAction.OFF if hvac_mode == HVACMode.OFF else HVACAction.HEATING
|
HVACAction.OFF if hvac_mode == HVACMode.OFF else HVACAction.HEATING
|
||||||
)
|
)
|
||||||
self._attr_hvac_mode = hvac_mode
|
self._attr_hvac_mode = hvac_mode
|
||||||
self._attr_hvac_modes = [HVACMode.OFF, HVACMode.COOL, HVACMode.HEAT]
|
self._attr_hvac_modes = (
|
||||||
|
hvac_modes
|
||||||
|
if hvac_modes is not None
|
||||||
|
else [HVACMode.OFF, HVACMode.COOL, HVACMode.HEAT]
|
||||||
|
)
|
||||||
self._attr_temperature_unit = UnitOfTemperature.CELSIUS
|
self._attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||||
self._attr_target_temperature = 20
|
self._attr_target_temperature = 20
|
||||||
self._attr_current_temperature = 15
|
self._attr_current_temperature = 15
|
||||||
@@ -336,6 +341,14 @@ class MagicMockClimate(MagicMock):
|
|||||||
def supported_features(self): # pylint: disable=missing-function-docstring
|
def supported_features(self): # pylint: disable=missing-function-docstring
|
||||||
return ClimateEntityFeature.TARGET_TEMPERATURE
|
return ClimateEntityFeature.TARGET_TEMPERATURE
|
||||||
|
|
||||||
|
@property
|
||||||
|
def min_temp(self): # pylint: disable=missing-function-docstring
|
||||||
|
return 15
|
||||||
|
|
||||||
|
@property
|
||||||
|
def max_temp(self): # pylint: disable=missing-function-docstring
|
||||||
|
return 19
|
||||||
|
|
||||||
|
|
||||||
async def create_thermostat(
|
async def create_thermostat(
|
||||||
hass: HomeAssistant, entry: MockConfigEntry, entity_id: str
|
hass: HomeAssistant, entry: MockConfigEntry, entity_id: str
|
||||||
|
|||||||
@@ -50,7 +50,8 @@ MOCK_TH_OVER_CLIMATE_MAIN_CONFIG = {
|
|||||||
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
|
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
|
||||||
CONF_CYCLE_MIN: 5,
|
CONF_CYCLE_MIN: 5,
|
||||||
CONF_DEVICE_POWER: 1,
|
CONF_DEVICE_POWER: 1,
|
||||||
CONF_USE_MAIN_CENTRAL_CONFIG: False
|
CONF_USE_MAIN_CENTRAL_CONFIG: False,
|
||||||
|
CONF_USE_CENTRAL_MODE: True
|
||||||
# Keep default values which are False
|
# Keep default values which are False
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ from datetime import datetime, timedelta
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from homeassistant.components.climate import (
|
||||||
|
SERVICE_SET_TEMPERATURE,
|
||||||
|
)
|
||||||
|
|
||||||
from .commons import *
|
from .commons import *
|
||||||
|
|
||||||
logging.getLogger().setLevel(logging.DEBUG)
|
logging.getLogger().setLevel(logging.DEBUG)
|
||||||
@@ -568,3 +572,141 @@ async def test_bug_101(
|
|||||||
)
|
)
|
||||||
assert entity.target_temperature == 12.75
|
assert entity.target_temperature == 12.75
|
||||||
assert entity.preset_mode is PRESET_NONE
|
assert entity.preset_mode is PRESET_NONE
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||||
|
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||||
|
async def test_bug_272(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
skip_hass_states_is_state,
|
||||||
|
skip_turn_on_off_heater,
|
||||||
|
skip_send_event,
|
||||||
|
):
|
||||||
|
"""Test that it not possible to set the target temperature under the min_temp setting"""
|
||||||
|
|
||||||
|
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||||
|
now: datetime = datetime.now(tz=tz)
|
||||||
|
|
||||||
|
entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
title="TheOverClimateMockName",
|
||||||
|
unique_id="uniqueId",
|
||||||
|
data=PARTIAL_CLIMATE_CONFIG, # 5 minutes security delay
|
||||||
|
)
|
||||||
|
|
||||||
|
# Min_temp is 15 and max_temp is 19
|
||||||
|
fake_underlying_climate = MagicMockClimate()
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||||
|
), patch(
|
||||||
|
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
||||||
|
return_value=fake_underlying_climate,
|
||||||
|
), patch(
|
||||||
|
"homeassistant.core.ServiceRegistry.async_call"
|
||||||
|
) as mock_service_call:
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
await hass.config_entries.async_setup(entry.entry_id)
|
||||||
|
assert entry.state is ConfigEntryState.LOADED
|
||||||
|
|
||||||
|
def find_my_entity(entity_id) -> ClimateEntity:
|
||||||
|
"""Find my new entity"""
|
||||||
|
component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN]
|
||||||
|
for entity in component.entities:
|
||||||
|
if entity.entity_id == entity_id:
|
||||||
|
return entity
|
||||||
|
|
||||||
|
entity = find_my_entity("climate.theoverclimatemockname")
|
||||||
|
|
||||||
|
assert entity
|
||||||
|
|
||||||
|
assert entity.name == "TheOverClimateMockName"
|
||||||
|
assert entity.is_over_climate is True
|
||||||
|
assert entity.hvac_mode is HVACMode.OFF
|
||||||
|
assert entity.target_temperature == entity.min_temp
|
||||||
|
assert entity.is_regulated is True
|
||||||
|
|
||||||
|
assert mock_service_call.call_count == 0
|
||||||
|
|
||||||
|
# Set the hvac_mode to HEAT
|
||||||
|
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||||
|
|
||||||
|
# In the accepted interval
|
||||||
|
await entity.async_set_temperature(temperature=17.5)
|
||||||
|
assert mock_service_call.call_count == 2
|
||||||
|
mock_service_call.assert_has_calls(
|
||||||
|
[
|
||||||
|
call.async_call(
|
||||||
|
"climate",
|
||||||
|
SERVICE_SET_HVAC_MODE,
|
||||||
|
{"entity_id": "climate.mock_climate", "hvac_mode": HVACMode.HEAT},
|
||||||
|
),
|
||||||
|
call.async_call(
|
||||||
|
"climate",
|
||||||
|
SERVICE_SET_TEMPERATURE,
|
||||||
|
{
|
||||||
|
"entity_id": "climate.mock_climate",
|
||||||
|
"temperature": 17.5,
|
||||||
|
"target_temp_high": 30,
|
||||||
|
"target_temp_low": 15,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||||
|
now: datetime = datetime.now(tz=tz)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||||
|
), patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call:
|
||||||
|
# Set room temperature to something very cold
|
||||||
|
event_timestamp = now + timedelta(minutes=1)
|
||||||
|
|
||||||
|
await send_temperature_change_event(entity, 13, event_timestamp)
|
||||||
|
await send_ext_temperature_change_event(entity, 9, event_timestamp)
|
||||||
|
|
||||||
|
# Not in the accepted interval (15-19)
|
||||||
|
await entity.async_set_temperature(temperature=10)
|
||||||
|
assert mock_service_call.call_count == 1
|
||||||
|
mock_service_call.assert_has_calls(
|
||||||
|
[
|
||||||
|
call.async_call(
|
||||||
|
"climate",
|
||||||
|
SERVICE_SET_TEMPERATURE,
|
||||||
|
{
|
||||||
|
"entity_id": "climate.mock_climate",
|
||||||
|
"temperature": 15, # the minimum acceptable
|
||||||
|
"target_temp_high": 30,
|
||||||
|
"target_temp_low": 15,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||||
|
), patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call:
|
||||||
|
# Set room temperature to something very cold
|
||||||
|
event_timestamp = now + timedelta(minutes=1)
|
||||||
|
|
||||||
|
await send_temperature_change_event(entity, 13, event_timestamp)
|
||||||
|
await send_ext_temperature_change_event(entity, 9, event_timestamp)
|
||||||
|
|
||||||
|
# In the accepted interval
|
||||||
|
await entity.async_set_temperature(temperature=20.8)
|
||||||
|
assert mock_service_call.call_count == 1
|
||||||
|
mock_service_call.assert_has_calls(
|
||||||
|
[
|
||||||
|
call.async_call(
|
||||||
|
"climate",
|
||||||
|
SERVICE_SET_TEMPERATURE,
|
||||||
|
{
|
||||||
|
"entity_id": "climate.mock_climate",
|
||||||
|
"temperature": 19, # the maximum acceptable
|
||||||
|
"target_temp_high": 30,
|
||||||
|
"target_temp_low": 15,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|||||||
734
tests/test_central_mode.py
Normal file
734
tests/test_central_mode.py
Normal file
@@ -0,0 +1,734 @@
|
|||||||
|
# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long
|
||||||
|
|
||||||
|
""" Test the central_configuration """
|
||||||
|
from unittest.mock import patch # , call
|
||||||
|
|
||||||
|
# from datetime import datetime # , timedelta
|
||||||
|
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from homeassistant.components.climate import HVACMode
|
||||||
|
|
||||||
|
# from homeassistant.helpers.entity_component import EntityComponent
|
||||||
|
from homeassistant.components.select import DOMAIN as SELECT_DOMAIN
|
||||||
|
|
||||||
|
from pytest_homeassistant_custom_component.common import MockConfigEntry
|
||||||
|
|
||||||
|
from custom_components.versatile_thermostat.thermostat_switch import (
|
||||||
|
ThermostatOverSwitch,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||||
|
from .const 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_config_with_central_mode_true(
|
||||||
|
hass: HomeAssistant, skip_hass_states_is_state
|
||||||
|
):
|
||||||
|
"""A config with central_mode True"""
|
||||||
|
|
||||||
|
# Add a Switch VTherm
|
||||||
|
entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
title="TheOverSwitchMockName",
|
||||||
|
unique_id="uniqueId",
|
||||||
|
data={
|
||||||
|
CONF_NAME: "TheOverSwitchMockName",
|
||||||
|
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
|
||||||
|
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
|
||||||
|
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
|
||||||
|
CONF_USE_CENTRAL_MODE: True,
|
||||||
|
CONF_CYCLE_MIN: 5,
|
||||||
|
CONF_TEMP_MIN: 8,
|
||||||
|
CONF_TEMP_MAX: 18,
|
||||||
|
"frost_temp": 10,
|
||||||
|
"eco_temp": 17,
|
||||||
|
"comfort_temp": 18,
|
||||||
|
"boost_temp": 21,
|
||||||
|
CONF_USE_WINDOW_FEATURE: False,
|
||||||
|
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_MINIMAL_ACTIVATION_DELAY: 30,
|
||||||
|
CONF_SECURITY_DELAY_MIN: 5,
|
||||||
|
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||||
|
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.1,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch("homeassistant.core.ServiceRegistry.async_call"):
|
||||||
|
entity: ThermostatOverSwitch = await create_thermostat(
|
||||||
|
hass, entry, "climate.theoverswitchmockname"
|
||||||
|
)
|
||||||
|
assert entity
|
||||||
|
assert entity.name == "TheOverSwitchMockName"
|
||||||
|
assert entity.is_over_switch
|
||||||
|
assert entity.is_controlled_by_central_mode
|
||||||
|
assert entity.last_central_mode is None # cause no central config exists
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||||
|
async def test_config_with_central_mode_false(
|
||||||
|
hass: HomeAssistant, skip_hass_states_is_state
|
||||||
|
):
|
||||||
|
"""A config with central_mode False"""
|
||||||
|
|
||||||
|
# Add a Climate VTherm
|
||||||
|
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_USE_CENTRAL_MODE: False,
|
||||||
|
CONF_CYCLE_MIN: 5,
|
||||||
|
CONF_TEMP_MIN: 8,
|
||||||
|
CONF_TEMP_MAX: 18,
|
||||||
|
"frost_temp": 10,
|
||||||
|
"eco_temp": 17,
|
||||||
|
"comfort_temp": 18,
|
||||||
|
"boost_temp": 21,
|
||||||
|
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_SECURITY_DEFAULT_ON_PERCENT: 0.1,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch("homeassistant.core.ServiceRegistry.async_call"):
|
||||||
|
entity: ThermostatOverSwitch = await create_thermostat(
|
||||||
|
hass, entry, "climate.theoverclimatemockname"
|
||||||
|
)
|
||||||
|
assert entity
|
||||||
|
assert entity.name == "TheOverClimateMockName"
|
||||||
|
assert entity.is_over_climate
|
||||||
|
assert entity.is_controlled_by_central_mode is False
|
||||||
|
assert entity.last_central_mode is None # cause no central config exists
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||||
|
async def test_config_with_central_mode_none(
|
||||||
|
hass: HomeAssistant, skip_hass_states_is_state
|
||||||
|
):
|
||||||
|
"""A config with central_mode is None"""
|
||||||
|
|
||||||
|
# Add a Switch VTherm
|
||||||
|
entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
title="TheOverValveMockName",
|
||||||
|
unique_id="uniqueId",
|
||||||
|
data={
|
||||||
|
CONF_NAME: "TheOverValveMockName",
|
||||||
|
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_VALVE,
|
||||||
|
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
|
||||||
|
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
|
||||||
|
CONF_USE_CENTRAL_MODE: True,
|
||||||
|
CONF_CYCLE_MIN: 5,
|
||||||
|
CONF_TEMP_MIN: 8,
|
||||||
|
CONF_TEMP_MAX: 18,
|
||||||
|
"frost_temp": 10,
|
||||||
|
"eco_temp": 17,
|
||||||
|
"comfort_temp": 18,
|
||||||
|
"boost_temp": 21,
|
||||||
|
CONF_USE_WINDOW_FEATURE: False,
|
||||||
|
CONF_USE_MOTION_FEATURE: False,
|
||||||
|
CONF_USE_POWER_FEATURE: False,
|
||||||
|
CONF_USE_PRESENCE_FEATURE: False,
|
||||||
|
CONF_VALVE: "number.mock_valve",
|
||||||
|
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||||
|
CONF_TPI_COEF_INT: 0.3,
|
||||||
|
CONF_TPI_COEF_EXT: 0.01,
|
||||||
|
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||||
|
CONF_SECURITY_DELAY_MIN: 5,
|
||||||
|
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||||
|
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.1,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch("homeassistant.core.ServiceRegistry.async_call"):
|
||||||
|
entity: ThermostatOverSwitch = await create_thermostat(
|
||||||
|
hass, entry, "climate.theovervalvemockname"
|
||||||
|
)
|
||||||
|
assert entity
|
||||||
|
assert entity.name == "TheOverValveMockName"
|
||||||
|
assert entity.is_over_valve
|
||||||
|
assert entity.is_controlled_by_central_mode
|
||||||
|
assert entity.last_central_mode is None # cause no central config exists
|
||||||
|
|
||||||
|
|
||||||
|
async def test_switch_change_central_mode_true(
|
||||||
|
hass: HomeAssistant, skip_hass_states_is_state, init_central_config
|
||||||
|
):
|
||||||
|
"""test that changes with over_switch config with central_mode True are
|
||||||
|
taken into account"""
|
||||||
|
|
||||||
|
# Add a Switch VTherm
|
||||||
|
entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
title="TheOverSwitchMockName",
|
||||||
|
unique_id="uniqueId",
|
||||||
|
data={
|
||||||
|
CONF_NAME: "TheOverSwitchMockName",
|
||||||
|
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
|
||||||
|
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
|
||||||
|
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
|
||||||
|
CONF_USE_CENTRAL_MODE: True,
|
||||||
|
CONF_CYCLE_MIN: 5,
|
||||||
|
CONF_TEMP_MIN: 8,
|
||||||
|
CONF_TEMP_MAX: 18,
|
||||||
|
"frost_temp": 10,
|
||||||
|
"eco_temp": 17,
|
||||||
|
"comfort_temp": 18,
|
||||||
|
"boost_temp": 21,
|
||||||
|
CONF_USE_WINDOW_FEATURE: False,
|
||||||
|
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_MINIMAL_ACTIVATION_DELAY: 30,
|
||||||
|
CONF_SECURITY_DELAY_MIN: 5,
|
||||||
|
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||||
|
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.1,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# 1 initialize entity and find select entity
|
||||||
|
with patch("homeassistant.core.ServiceRegistry.async_call"):
|
||||||
|
entity: ThermostatOverSwitch = await create_thermostat(
|
||||||
|
hass, entry, "climate.theoverswitchmockname"
|
||||||
|
)
|
||||||
|
assert entity
|
||||||
|
assert entity.is_controlled_by_central_mode
|
||||||
|
assert entity.last_central_mode is None
|
||||||
|
|
||||||
|
# Find the select entity
|
||||||
|
select_entity = search_entity(hass, "select.central_mode", SELECT_DOMAIN)
|
||||||
|
|
||||||
|
assert select_entity
|
||||||
|
assert select_entity.current_option == CENTRAL_MODE_AUTO
|
||||||
|
assert select_entity.options == CENTRAL_MODES
|
||||||
|
|
||||||
|
# start entity
|
||||||
|
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||||
|
await entity.async_set_preset_mode(PRESET_BOOST)
|
||||||
|
|
||||||
|
assert entity.hvac_mode == HVACMode.HEAT
|
||||||
|
assert entity.preset_mode == PRESET_BOOST
|
||||||
|
|
||||||
|
# 2 change central_mode to STOPPED
|
||||||
|
with patch("homeassistant.core.ServiceRegistry.async_call"):
|
||||||
|
await select_entity.async_select_option(CENTRAL_MODE_STOPPED)
|
||||||
|
|
||||||
|
assert entity.last_central_mode is CENTRAL_MODE_STOPPED
|
||||||
|
assert entity.hvac_mode == HVACMode.OFF
|
||||||
|
assert entity.preset_mode == PRESET_BOOST
|
||||||
|
|
||||||
|
# 3 change back central_mode to AUTO
|
||||||
|
with patch("homeassistant.core.ServiceRegistry.async_call"):
|
||||||
|
await entity.async_set_preset_mode(PRESET_COMFORT)
|
||||||
|
assert entity.preset_mode == PRESET_COMFORT
|
||||||
|
|
||||||
|
await select_entity.async_select_option(CENTRAL_MODE_AUTO)
|
||||||
|
|
||||||
|
# hvac_mode should be restored as before the STOP and preset should be restored with the last choosen preset (COMFORT here)
|
||||||
|
assert entity.last_central_mode is CENTRAL_MODE_AUTO
|
||||||
|
assert entity.hvac_mode == HVACMode.HEAT
|
||||||
|
assert entity.preset_mode == PRESET_COMFORT
|
||||||
|
|
||||||
|
# 4 change central_mode to COOL_ONLY
|
||||||
|
with patch("homeassistant.core.ServiceRegistry.async_call"):
|
||||||
|
await select_entity.async_select_option(CENTRAL_MODE_COOL_ONLY)
|
||||||
|
|
||||||
|
# hvac_mode should be set to OFF because there is no COOL mode for this VTherm
|
||||||
|
assert entity.last_central_mode is CENTRAL_MODE_COOL_ONLY
|
||||||
|
assert entity.hvac_mode == HVACMode.OFF
|
||||||
|
assert entity.preset_mode == PRESET_COMFORT
|
||||||
|
|
||||||
|
# 5 change back central_mode to AUTO
|
||||||
|
with patch("homeassistant.core.ServiceRegistry.async_call"):
|
||||||
|
await select_entity.async_select_option(CENTRAL_MODE_AUTO)
|
||||||
|
|
||||||
|
# hvac_mode should be restored to HEAT
|
||||||
|
assert entity.last_central_mode is CENTRAL_MODE_AUTO
|
||||||
|
assert entity.hvac_mode == HVACMode.HEAT
|
||||||
|
assert entity.preset_mode == PRESET_COMFORT
|
||||||
|
|
||||||
|
# 6 change central_mode to HEAT_ONLY
|
||||||
|
with patch("homeassistant.core.ServiceRegistry.async_call"):
|
||||||
|
await select_entity.async_select_option(CENTRAL_MODE_HEAT_ONLY)
|
||||||
|
|
||||||
|
# hvac_mode should stay in HEAT mode
|
||||||
|
assert entity.last_central_mode is CENTRAL_MODE_HEAT_ONLY
|
||||||
|
assert entity.hvac_mode == HVACMode.HEAT
|
||||||
|
# No change
|
||||||
|
assert entity.preset_mode == PRESET_COMFORT
|
||||||
|
|
||||||
|
# 7 change back central_mode to AUTO
|
||||||
|
with patch("homeassistant.core.ServiceRegistry.async_call"):
|
||||||
|
await select_entity.async_select_option(CENTRAL_MODE_AUTO)
|
||||||
|
|
||||||
|
# hvac_mode should be restored to HEAT
|
||||||
|
assert entity.last_central_mode is CENTRAL_MODE_AUTO
|
||||||
|
assert entity.hvac_mode == HVACMode.HEAT
|
||||||
|
assert entity.preset_mode == PRESET_COMFORT
|
||||||
|
|
||||||
|
# 8 change central_mode to FROST_PROTECTION
|
||||||
|
with patch("homeassistant.core.ServiceRegistry.async_call"):
|
||||||
|
await select_entity.async_select_option(CENTRAL_MODE_FROST_PROTECTION)
|
||||||
|
|
||||||
|
# hvac_mode should stay in HEAT mode
|
||||||
|
assert entity.last_central_mode is CENTRAL_MODE_FROST_PROTECTION
|
||||||
|
assert entity.hvac_mode == HVACMode.HEAT
|
||||||
|
# change to Frost
|
||||||
|
assert entity.preset_mode == PRESET_FROST_PROTECTION
|
||||||
|
|
||||||
|
# 9 change back central_mode to AUTO
|
||||||
|
with patch("homeassistant.core.ServiceRegistry.async_call"):
|
||||||
|
await select_entity.async_select_option(CENTRAL_MODE_AUTO)
|
||||||
|
|
||||||
|
# hvac_mode should be restored to HEAT
|
||||||
|
assert entity.last_central_mode is CENTRAL_MODE_AUTO
|
||||||
|
assert entity.hvac_mode == HVACMode.HEAT
|
||||||
|
# preset restored to COMFORT
|
||||||
|
assert entity.preset_mode == PRESET_COMFORT
|
||||||
|
|
||||||
|
|
||||||
|
async def test_switch_ac_change_central_mode_true(
|
||||||
|
hass: HomeAssistant, skip_hass_states_is_state, init_central_config
|
||||||
|
):
|
||||||
|
"""test that changes with over_switch config with central_mode True are
|
||||||
|
taken into account"""
|
||||||
|
|
||||||
|
# Add a Switch VTherm
|
||||||
|
entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
title="TheOverSwitchMockName",
|
||||||
|
unique_id="uniqueId",
|
||||||
|
data={
|
||||||
|
CONF_NAME: "TheOverSwitchMockName",
|
||||||
|
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
|
||||||
|
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
|
||||||
|
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
|
||||||
|
CONF_USE_CENTRAL_MODE: True,
|
||||||
|
CONF_CYCLE_MIN: 5,
|
||||||
|
CONF_TEMP_MIN: 8,
|
||||||
|
CONF_TEMP_MAX: 18,
|
||||||
|
"frost_temp": 10,
|
||||||
|
"eco_temp": 17,
|
||||||
|
"comfort_temp": 18,
|
||||||
|
"boost_temp": 21,
|
||||||
|
CONF_USE_WINDOW_FEATURE: False,
|
||||||
|
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_MINIMAL_ACTIVATION_DELAY: 30,
|
||||||
|
CONF_SECURITY_DELAY_MIN: 5,
|
||||||
|
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||||
|
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.1,
|
||||||
|
CONF_AC_MODE: True,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# 1 initialize entity and find select entity
|
||||||
|
with patch("homeassistant.core.ServiceRegistry.async_call"):
|
||||||
|
entity: ThermostatOverSwitch = await create_thermostat(
|
||||||
|
hass, entry, "climate.theoverswitchmockname"
|
||||||
|
)
|
||||||
|
assert entity
|
||||||
|
assert entity.is_controlled_by_central_mode
|
||||||
|
assert entity.ac_mode is True
|
||||||
|
assert entity.hvac_modes == [HVACMode.HEAT, HVACMode.COOL, HVACMode.OFF]
|
||||||
|
|
||||||
|
# Find the select entity
|
||||||
|
select_entity = search_entity(hass, "select.central_mode", SELECT_DOMAIN)
|
||||||
|
|
||||||
|
assert select_entity
|
||||||
|
assert select_entity.current_option == CENTRAL_MODE_AUTO
|
||||||
|
assert select_entity.options == CENTRAL_MODES
|
||||||
|
|
||||||
|
# start entity in cooling mode
|
||||||
|
await entity.async_set_hvac_mode(HVACMode.COOL)
|
||||||
|
await entity.async_set_preset_mode(PRESET_BOOST)
|
||||||
|
|
||||||
|
assert entity.hvac_mode == HVACMode.COOL
|
||||||
|
assert entity.preset_mode == PRESET_BOOST
|
||||||
|
|
||||||
|
# 2 change central_mode to STOPPED
|
||||||
|
with patch("homeassistant.core.ServiceRegistry.async_call"):
|
||||||
|
await select_entity.async_select_option(CENTRAL_MODE_STOPPED)
|
||||||
|
|
||||||
|
assert entity.hvac_mode == HVACMode.OFF
|
||||||
|
assert entity.preset_mode == PRESET_BOOST
|
||||||
|
|
||||||
|
# 3 change back central_mode to AUTO
|
||||||
|
with patch("homeassistant.core.ServiceRegistry.async_call"):
|
||||||
|
await entity.async_set_preset_mode(PRESET_COMFORT)
|
||||||
|
assert entity.preset_mode == PRESET_COMFORT
|
||||||
|
|
||||||
|
await select_entity.async_select_option(CENTRAL_MODE_AUTO)
|
||||||
|
|
||||||
|
# hvac_mode should be restored as before the STOP and preset should be restored with the last choosen preset (COMFORT here)
|
||||||
|
assert entity.hvac_mode == HVACMode.COOL
|
||||||
|
assert entity.preset_mode == PRESET_COMFORT
|
||||||
|
|
||||||
|
# 4 change central_mode to COOL_ONLY
|
||||||
|
with patch("homeassistant.core.ServiceRegistry.async_call"):
|
||||||
|
await select_entity.async_select_option(CENTRAL_MODE_COOL_ONLY)
|
||||||
|
|
||||||
|
# hvac_mode should be set to OFF because there is no COOL mode for this VTherm
|
||||||
|
assert entity.hvac_mode == HVACMode.COOL
|
||||||
|
assert entity.preset_mode == PRESET_COMFORT
|
||||||
|
|
||||||
|
# 5 change back central_mode to AUTO
|
||||||
|
with patch("homeassistant.core.ServiceRegistry.async_call"):
|
||||||
|
await select_entity.async_select_option(CENTRAL_MODE_AUTO)
|
||||||
|
|
||||||
|
# hvac_mode should be restored to HEAT
|
||||||
|
assert entity.hvac_mode == HVACMode.COOL
|
||||||
|
assert entity.preset_mode == PRESET_COMFORT
|
||||||
|
|
||||||
|
# 6 change central_mode to HEAT_ONLY
|
||||||
|
with patch("homeassistant.core.ServiceRegistry.async_call"):
|
||||||
|
await select_entity.async_select_option(CENTRAL_MODE_HEAT_ONLY)
|
||||||
|
|
||||||
|
# hvac_mode should stay in HEAT mode
|
||||||
|
assert entity.hvac_mode == HVACMode.HEAT
|
||||||
|
# No change
|
||||||
|
assert entity.preset_mode == PRESET_COMFORT
|
||||||
|
|
||||||
|
# 7 change back central_mode to AUTO
|
||||||
|
with patch("homeassistant.core.ServiceRegistry.async_call"):
|
||||||
|
await select_entity.async_select_option(CENTRAL_MODE_AUTO)
|
||||||
|
|
||||||
|
# hvac_mode should be restored to COOL
|
||||||
|
assert entity.hvac_mode == HVACMode.COOL
|
||||||
|
assert entity.preset_mode == PRESET_COMFORT
|
||||||
|
|
||||||
|
# 8 change central_mode to FROST_PROTECTION
|
||||||
|
with patch("homeassistant.core.ServiceRegistry.async_call"):
|
||||||
|
await select_entity.async_select_option(CENTRAL_MODE_FROST_PROTECTION)
|
||||||
|
|
||||||
|
# hvac_mode should stay in COOL mode
|
||||||
|
assert entity.hvac_mode == HVACMode.HEAT
|
||||||
|
# change to Frost
|
||||||
|
assert entity.preset_mode == PRESET_FROST_PROTECTION
|
||||||
|
|
||||||
|
# 9 change back central_mode to AUTO
|
||||||
|
with patch("homeassistant.core.ServiceRegistry.async_call"):
|
||||||
|
await select_entity.async_select_option(CENTRAL_MODE_AUTO)
|
||||||
|
|
||||||
|
# hvac_mode should be restored to COOL
|
||||||
|
assert entity.hvac_mode == HVACMode.COOL
|
||||||
|
# preset restored to COMFORT
|
||||||
|
assert entity.preset_mode == PRESET_COMFORT
|
||||||
|
|
||||||
|
|
||||||
|
async def test_climate_ac_change_central_mode_false(
|
||||||
|
hass: HomeAssistant, skip_hass_states_is_state, init_central_config
|
||||||
|
):
|
||||||
|
"""test that changes with over_climate config with central_mode False are
|
||||||
|
not taken into account"""
|
||||||
|
|
||||||
|
fake_underlying_climate = MockClimate(hass, "mockUniqueId", "MockClimateName", {})
|
||||||
|
|
||||||
|
# Add a Climate VTherm
|
||||||
|
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_USE_CENTRAL_MODE: False,
|
||||||
|
CONF_CYCLE_MIN: 5,
|
||||||
|
CONF_TEMP_MIN: 8,
|
||||||
|
CONF_TEMP_MAX: 18,
|
||||||
|
"frost_temp": 10,
|
||||||
|
"eco_temp": 17,
|
||||||
|
"comfort_temp": 18,
|
||||||
|
"boost_temp": 21,
|
||||||
|
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_SECURITY_DEFAULT_ON_PERCENT: 0.1,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch("homeassistant.core.ServiceRegistry.async_call"), patch(
|
||||||
|
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
||||||
|
return_value=fake_underlying_climate,
|
||||||
|
):
|
||||||
|
entity: ThermostatOverSwitch = await create_thermostat(
|
||||||
|
hass, entry, "climate.theoverclimatemockname"
|
||||||
|
)
|
||||||
|
assert entity
|
||||||
|
assert entity.name == "TheOverClimateMockName"
|
||||||
|
assert entity.is_over_climate
|
||||||
|
assert entity.is_controlled_by_central_mode is False
|
||||||
|
assert entity.hvac_modes == [HVACMode.OFF, HVACMode.COOL, HVACMode.HEAT]
|
||||||
|
|
||||||
|
# Find the select entity
|
||||||
|
select_entity = search_entity(hass, "select.central_mode", SELECT_DOMAIN)
|
||||||
|
|
||||||
|
assert select_entity
|
||||||
|
assert select_entity.current_option == CENTRAL_MODE_AUTO
|
||||||
|
assert select_entity.options == CENTRAL_MODES
|
||||||
|
|
||||||
|
# start entity in Heating mode
|
||||||
|
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||||
|
await entity.async_set_preset_mode(PRESET_BOOST)
|
||||||
|
|
||||||
|
assert entity.hvac_mode == HVACMode.HEAT
|
||||||
|
assert entity.preset_mode == PRESET_BOOST
|
||||||
|
|
||||||
|
# 2 change central_mode to STOPPED
|
||||||
|
with patch("homeassistant.core.ServiceRegistry.async_call"):
|
||||||
|
await select_entity.async_select_option(CENTRAL_MODE_STOPPED)
|
||||||
|
|
||||||
|
# No change
|
||||||
|
assert entity.hvac_mode == HVACMode.HEAT
|
||||||
|
assert entity.preset_mode == PRESET_BOOST
|
||||||
|
|
||||||
|
# 3 change back central_mode to AUTO
|
||||||
|
with patch("homeassistant.core.ServiceRegistry.async_call"):
|
||||||
|
await entity.async_set_preset_mode(PRESET_COMFORT)
|
||||||
|
assert entity.preset_mode == PRESET_COMFORT
|
||||||
|
|
||||||
|
await select_entity.async_select_option(CENTRAL_MODE_AUTO)
|
||||||
|
|
||||||
|
# No change
|
||||||
|
assert entity.hvac_mode == HVACMode.HEAT
|
||||||
|
assert entity.preset_mode == PRESET_COMFORT
|
||||||
|
|
||||||
|
# 4 change central_mode to COOL_ONLY
|
||||||
|
with patch("homeassistant.core.ServiceRegistry.async_call"):
|
||||||
|
await select_entity.async_select_option(CENTRAL_MODE_COOL_ONLY)
|
||||||
|
|
||||||
|
# No change
|
||||||
|
assert entity.hvac_mode == HVACMode.HEAT
|
||||||
|
assert entity.preset_mode == PRESET_COMFORT
|
||||||
|
|
||||||
|
# 5 change back central_mode to AUTO
|
||||||
|
with patch("homeassistant.core.ServiceRegistry.async_call"):
|
||||||
|
await select_entity.async_select_option(CENTRAL_MODE_AUTO)
|
||||||
|
|
||||||
|
# No change
|
||||||
|
assert entity.hvac_mode == HVACMode.HEAT
|
||||||
|
assert entity.preset_mode == PRESET_COMFORT
|
||||||
|
|
||||||
|
# 6 change central_mode to HEAT_ONLY
|
||||||
|
with patch("homeassistant.core.ServiceRegistry.async_call"):
|
||||||
|
await select_entity.async_select_option(CENTRAL_MODE_HEAT_ONLY)
|
||||||
|
|
||||||
|
# No change
|
||||||
|
assert entity.hvac_mode == HVACMode.HEAT
|
||||||
|
assert entity.preset_mode == PRESET_COMFORT
|
||||||
|
|
||||||
|
# 7 change back central_mode to AUTO
|
||||||
|
with patch("homeassistant.core.ServiceRegistry.async_call"):
|
||||||
|
await select_entity.async_select_option(CENTRAL_MODE_AUTO)
|
||||||
|
|
||||||
|
# No change
|
||||||
|
assert entity.hvac_mode == HVACMode.HEAT
|
||||||
|
assert entity.preset_mode == PRESET_COMFORT
|
||||||
|
|
||||||
|
# 8 change central_mode to FROST_PROTECTION
|
||||||
|
with patch("homeassistant.core.ServiceRegistry.async_call"):
|
||||||
|
await select_entity.async_select_option(CENTRAL_MODE_FROST_PROTECTION)
|
||||||
|
|
||||||
|
# No change
|
||||||
|
assert entity.hvac_mode == HVACMode.HEAT
|
||||||
|
assert entity.preset_mode == PRESET_COMFORT
|
||||||
|
|
||||||
|
# 9 change back central_mode to AUTO
|
||||||
|
with patch("homeassistant.core.ServiceRegistry.async_call"):
|
||||||
|
await select_entity.async_select_option(CENTRAL_MODE_AUTO)
|
||||||
|
|
||||||
|
# No change
|
||||||
|
assert entity.hvac_mode == HVACMode.HEAT
|
||||||
|
assert entity.preset_mode == PRESET_COMFORT
|
||||||
|
|
||||||
|
|
||||||
|
async def test_climate_ac_only_change_central_mode_true(
|
||||||
|
hass: HomeAssistant, skip_hass_states_is_state, init_central_config
|
||||||
|
):
|
||||||
|
"""test that changes with over_climate with AC only config with central_mode True are
|
||||||
|
taken into account
|
||||||
|
Test also switching from central_mode without coming to AUTO each time"""
|
||||||
|
|
||||||
|
fake_underlying_climate = MockClimate(
|
||||||
|
hass,
|
||||||
|
"mockUniqueId",
|
||||||
|
"MockClimateName",
|
||||||
|
entry_infos={},
|
||||||
|
hvac_modes=[HVACMode.OFF, HVACMode.COOL],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add a Climate VTherm
|
||||||
|
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_USE_CENTRAL_MODE: True,
|
||||||
|
CONF_CYCLE_MIN: 5,
|
||||||
|
CONF_TEMP_MIN: 8,
|
||||||
|
CONF_TEMP_MAX: 18,
|
||||||
|
"frost_temp": 10,
|
||||||
|
"eco_temp": 17,
|
||||||
|
"comfort_temp": 18,
|
||||||
|
"boost_temp": 21,
|
||||||
|
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_SECURITY_DEFAULT_ON_PERCENT: 0.1,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch("homeassistant.core.ServiceRegistry.async_call"), patch(
|
||||||
|
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
||||||
|
return_value=fake_underlying_climate,
|
||||||
|
):
|
||||||
|
entity: ThermostatOverSwitch = await create_thermostat(
|
||||||
|
hass, entry, "climate.theoverclimatemockname"
|
||||||
|
)
|
||||||
|
assert entity
|
||||||
|
assert entity.name == "TheOverClimateMockName"
|
||||||
|
assert entity.is_over_climate
|
||||||
|
assert entity.is_controlled_by_central_mode is True
|
||||||
|
assert entity.hvac_modes == [HVACMode.OFF, HVACMode.COOL]
|
||||||
|
|
||||||
|
# Find the select entity
|
||||||
|
select_entity = search_entity(hass, "select.central_mode", SELECT_DOMAIN)
|
||||||
|
|
||||||
|
assert select_entity
|
||||||
|
assert select_entity.current_option == CENTRAL_MODE_AUTO
|
||||||
|
assert select_entity.options == CENTRAL_MODES
|
||||||
|
|
||||||
|
# start entity in Cooling mode
|
||||||
|
await entity.async_set_hvac_mode(HVACMode.COOL)
|
||||||
|
await entity.async_set_preset_mode(PRESET_ECO)
|
||||||
|
|
||||||
|
assert entity.hvac_mode == HVACMode.COOL
|
||||||
|
assert entity.preset_mode == PRESET_ECO
|
||||||
|
|
||||||
|
# 2 change central_mode to STOPPED
|
||||||
|
with patch("homeassistant.core.ServiceRegistry.async_call"):
|
||||||
|
await select_entity.async_select_option(CENTRAL_MODE_STOPPED)
|
||||||
|
|
||||||
|
# No change
|
||||||
|
assert entity.hvac_mode == HVACMode.OFF
|
||||||
|
assert entity.preset_mode == PRESET_ECO
|
||||||
|
|
||||||
|
# 3 change central_mode to HEAT ONLY after switching to COMFORT preset
|
||||||
|
with patch("homeassistant.core.ServiceRegistry.async_call"):
|
||||||
|
await entity.async_set_preset_mode(PRESET_COMFORT)
|
||||||
|
assert entity.preset_mode == PRESET_COMFORT
|
||||||
|
|
||||||
|
await select_entity.async_select_option(CENTRAL_MODE_HEAT_ONLY)
|
||||||
|
|
||||||
|
# Stay in OFF because HEAT is not permitted
|
||||||
|
assert entity.hvac_mode == HVACMode.OFF
|
||||||
|
assert entity.preset_mode == PRESET_COMFORT
|
||||||
|
|
||||||
|
# 4 change central_mode to COOL_ONLY
|
||||||
|
with patch("homeassistant.core.ServiceRegistry.async_call"):
|
||||||
|
await select_entity.async_select_option(CENTRAL_MODE_COOL_ONLY)
|
||||||
|
|
||||||
|
# switch back to COOL restoring the preset
|
||||||
|
assert entity.hvac_mode == HVACMode.COOL
|
||||||
|
assert entity.preset_mode == PRESET_COMFORT
|
||||||
|
|
||||||
|
# 5 change back central_mode to AUTO
|
||||||
|
with patch("homeassistant.core.ServiceRegistry.async_call"):
|
||||||
|
await select_entity.async_select_option(CENTRAL_MODE_AUTO)
|
||||||
|
|
||||||
|
# No change
|
||||||
|
assert entity.hvac_mode == HVACMode.COOL
|
||||||
|
assert entity.preset_mode == PRESET_COMFORT
|
||||||
|
|
||||||
|
# 6 change central_mode to FROST_PROTECTION
|
||||||
|
with patch("homeassistant.core.ServiceRegistry.async_call"):
|
||||||
|
await select_entity.async_select_option(CENTRAL_MODE_FROST_PROTECTION)
|
||||||
|
|
||||||
|
# No change
|
||||||
|
assert entity.hvac_mode == HVACMode.OFF
|
||||||
|
assert entity.preset_mode == PRESET_COMFORT
|
||||||
|
|
||||||
|
# 7 change back central_mode to AUTO
|
||||||
|
with patch("homeassistant.core.ServiceRegistry.async_call"):
|
||||||
|
await select_entity.async_select_option(CENTRAL_MODE_AUTO)
|
||||||
|
|
||||||
|
# No change
|
||||||
|
assert entity.hvac_mode == HVACMode.COOL
|
||||||
|
assert entity.preset_mode == PRESET_COMFORT
|
||||||
|
|
||||||
|
# 8 change central_mode to FROST_PROTECTION
|
||||||
|
with patch("homeassistant.core.ServiceRegistry.async_call"):
|
||||||
|
await select_entity.async_select_option(CENTRAL_MODE_FROST_PROTECTION)
|
||||||
|
|
||||||
|
# No change
|
||||||
|
assert entity.hvac_mode == HVACMode.OFF
|
||||||
|
assert entity.preset_mode == PRESET_COMFORT
|
||||||
|
|
||||||
|
# 9 change back central_mode to COOL_ONLY
|
||||||
|
with patch("homeassistant.core.ServiceRegistry.async_call"):
|
||||||
|
await select_entity.async_select_option(CENTRAL_MODE_COOL_ONLY)
|
||||||
|
|
||||||
|
# No change
|
||||||
|
assert entity.hvac_mode == HVACMode.COOL
|
||||||
|
assert entity.preset_mode == PRESET_COMFORT
|
||||||
|
|
||||||
|
await entity.async_set_preset_mode(PRESET_ECO)
|
||||||
|
# 10 change back central_mode to HEAT_ONLY
|
||||||
|
with patch("homeassistant.core.ServiceRegistry.async_call"):
|
||||||
|
await select_entity.async_select_option(CENTRAL_MODE_HEAT_ONLY)
|
||||||
|
|
||||||
|
# Shutdown cause no HEAT
|
||||||
|
assert entity.hvac_mode == HVACMode.OFF
|
||||||
|
assert entity.preset_mode == PRESET_ECO
|
||||||
|
|
||||||
|
# 11 change back central_mode to AUTO
|
||||||
|
with patch("homeassistant.core.ServiceRegistry.async_call"):
|
||||||
|
await select_entity.async_select_option(CENTRAL_MODE_AUTO)
|
||||||
|
|
||||||
|
# No change
|
||||||
|
assert entity.hvac_mode == HVACMode.COOL
|
||||||
|
assert entity.preset_mode == PRESET_ECO
|
||||||
@@ -363,6 +363,7 @@ async def test_user_config_flow_window_auto_ok(
|
|||||||
CONF_USE_POWER_FEATURE: False,
|
CONF_USE_POWER_FEATURE: False,
|
||||||
CONF_USE_PRESENCE_FEATURE: False,
|
CONF_USE_PRESENCE_FEATURE: False,
|
||||||
CONF_WINDOW_DELAY: 30, # the default value is added
|
CONF_WINDOW_DELAY: 30, # the default value is added
|
||||||
|
CONF_USE_CENTRAL_MODE: True, # True is the defaulf value
|
||||||
} | MOCK_TH_OVER_SWITCH_TYPE_CONFIG | MOCK_TH_OVER_SWITCH_TPI_CONFIG | MOCK_WINDOW_AUTO_CONFIG | {
|
} | MOCK_TH_OVER_SWITCH_TYPE_CONFIG | MOCK_TH_OVER_SWITCH_TPI_CONFIG | MOCK_WINDOW_AUTO_CONFIG | {
|
||||||
CONF_USE_MAIN_CENTRAL_CONFIG: True,
|
CONF_USE_MAIN_CENTRAL_CONFIG: True,
|
||||||
CONF_USE_TPI_CENTRAL_CONFIG: False,
|
CONF_USE_TPI_CENTRAL_CONFIG: False,
|
||||||
@@ -510,6 +511,7 @@ async def test_user_config_flow_over_4_switches(
|
|||||||
CONF_USE_POWER_FEATURE: False,
|
CONF_USE_POWER_FEATURE: False,
|
||||||
CONF_USE_PRESENCE_FEATURE: False,
|
CONF_USE_PRESENCE_FEATURE: False,
|
||||||
CONF_USE_MAIN_CENTRAL_CONFIG: True,
|
CONF_USE_MAIN_CENTRAL_CONFIG: True,
|
||||||
|
CONF_USE_CENTRAL_MODE: False,
|
||||||
}
|
}
|
||||||
|
|
||||||
TYPE_CONFIG = { # pylint: disable=wildcard-import, invalid-name
|
TYPE_CONFIG = { # pylint: disable=wildcard-import, invalid-name
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ async def test_over_switch_ac_full_start(
|
|||||||
|
|
||||||
assert entity.hvac_mode is HVACMode.OFF
|
assert entity.hvac_mode is HVACMode.OFF
|
||||||
assert entity.hvac_action is HVACAction.OFF
|
assert entity.hvac_action is HVACAction.OFF
|
||||||
assert entity.target_temperature == 16 # eco_ac_away
|
assert entity.target_temperature == 27 # eco_ac_away (no change)
|
||||||
|
|
||||||
# Close a window
|
# Close a window
|
||||||
with patch("homeassistant.helpers.condition.state", return_value=True):
|
with patch("homeassistant.helpers.condition.state", return_value=True):
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
from unittest.mock import patch, call
|
from unittest.mock import patch, call
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant, State
|
||||||
from homeassistant.components.climate import HVACAction, HVACMode
|
from homeassistant.components.climate import HVACAction, HVACMode
|
||||||
from homeassistant.config_entries import ConfigEntryState
|
from homeassistant.config_entries import ConfigEntryState
|
||||||
|
|
||||||
@@ -214,20 +214,60 @@ async def test_over_valve_full_start(
|
|||||||
assert entity.hvac_action == HVACAction.HEATING
|
assert entity.hvac_action == HVACAction.HEATING
|
||||||
|
|
||||||
# Change internal temperature
|
# Change internal temperature
|
||||||
|
expected_state = State(
|
||||||
|
entity_id="number.mock_valve", state="0", attributes={"min": 10, "max": 50}
|
||||||
|
)
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||||
) as mock_send_event, patch(
|
) as mock_send_event, patch(
|
||||||
"homeassistant.core.ServiceRegistry.async_call"
|
"homeassistant.core.ServiceRegistry.async_call"
|
||||||
) as mock_service_call, patch(
|
) as mock_service_call, patch(
|
||||||
"homeassistant.core.StateMachine.get", return_value=0
|
"homeassistant.core.StateMachine.get", return_value=expected_state
|
||||||
):
|
):
|
||||||
event_timestamp = now - timedelta(minutes=3)
|
event_timestamp = now - timedelta(minutes=3)
|
||||||
await send_temperature_change_event(entity, 20, datetime.now())
|
await send_temperature_change_event(entity, 20, datetime.now())
|
||||||
assert entity.valve_open_percent == 0
|
assert entity.valve_open_percent == 0
|
||||||
assert entity.is_device_active is False
|
assert entity.is_device_active is True # Should be 0 but in fact 10 is send
|
||||||
assert entity.hvac_action == HVACAction.IDLE
|
assert (
|
||||||
|
entity.hvac_action == HVACAction.HEATING
|
||||||
|
) # Should be IDLE but heating due to 10
|
||||||
|
|
||||||
|
assert mock_service_call.call_count == 1
|
||||||
|
# The VTherm valve is 0, but the underlying have received 10 which is the min
|
||||||
|
mock_service_call.assert_has_calls(
|
||||||
|
[
|
||||||
|
call.async_call(
|
||||||
|
"number",
|
||||||
|
"set_value",
|
||||||
|
{"entity_id": "number.mock_valve", "value": 10},
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
await send_temperature_change_event(entity, 17, datetime.now())
|
await send_temperature_change_event(entity, 17, datetime.now())
|
||||||
|
assert mock_service_call.call_count == 2
|
||||||
|
# The VTherm valve is 0, but the underlying have received 10 which is the min
|
||||||
|
mock_service_call.assert_has_calls(
|
||||||
|
[
|
||||||
|
call.async_call(
|
||||||
|
"number",
|
||||||
|
"set_value",
|
||||||
|
{
|
||||||
|
"entity_id": "number.mock_valve",
|
||||||
|
"value": 10,
|
||||||
|
}, # the min allowed value
|
||||||
|
),
|
||||||
|
call.async_call(
|
||||||
|
"number",
|
||||||
|
"set_value",
|
||||||
|
{
|
||||||
|
"entity_id": "number.mock_valve",
|
||||||
|
"value": 50,
|
||||||
|
}, # the max allowed value
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
# switch to Eco
|
# switch to Eco
|
||||||
await entity.async_set_preset_mode(PRESET_ECO)
|
await entity.async_set_preset_mode(PRESET_ECO)
|
||||||
assert entity.preset_mode is PRESET_ECO
|
assert entity.preset_mode is PRESET_ECO
|
||||||
@@ -243,6 +283,18 @@ async def test_over_valve_full_start(
|
|||||||
assert entity.is_device_active is True
|
assert entity.is_device_active is True
|
||||||
assert entity.hvac_action == HVACAction.HEATING
|
assert entity.hvac_action == HVACAction.HEATING
|
||||||
|
|
||||||
|
# Test window open/close (with a normal min/max so that is_device_active is False when open_percent is 0)
|
||||||
|
expected_state = State(
|
||||||
|
entity_id="number.mock_valve", state="0", attributes={"min": 0, "max": 99}
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||||
|
) as mock_send_event, patch(
|
||||||
|
"homeassistant.core.ServiceRegistry.async_call"
|
||||||
|
) as mock_service_call, patch(
|
||||||
|
"homeassistant.core.StateMachine.get", return_value=expected_state
|
||||||
|
):
|
||||||
# Open a window
|
# Open a window
|
||||||
with patch("homeassistant.helpers.condition.state", return_value=True):
|
with patch("homeassistant.helpers.condition.state", return_value=True):
|
||||||
event_timestamp = now - timedelta(minutes=1)
|
event_timestamp = now - timedelta(minutes=1)
|
||||||
|
|||||||
Reference in New Issue
Block a user