Compare commits

..

6 Commits

Author SHA1 Message Date
Jean-Marc Collin
a17aba45fa Normal algo working and testu ok 2024-01-03 09:57:18 +00:00
Jean-Marc Collin
c222feda1a Issue #295 - No floating point value for target temp 2024-01-01 16:55:41 +00:00
Jean-Marc Collin
d05df021ab Beer from Lajull 2023-12-24 09:07:59 +00:00
Jean-Marc Collin
27a267139f FIX #159 - Doesn't send target temp if VTherm is off 2023-12-20 19:06:34 +00:00
Jean-Marc Collin
707f40d406 FIX issue #284 - preset not saved 2023-12-20 18:54:35 +00:00
Jean-Marc Collin
a01f5770d9 FIX issue #272 and #24ç - min and max values depending of the underlying 2023-12-19 19:39:33 +00:00
26 changed files with 1429 additions and 125 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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}"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

@@ -0,0 +1,7 @@
{
"include": [
"custom_components/versatile_thermostat/**",
"homeassistant/**"
],
"reportShadowedImports": false
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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