Compare commits

..

1 Commits

Author SHA1 Message Date
Jean-Marc Collin
9b26d40cec Issue #683 - Window action Anti frost doesnt restore the previous temp 2024-12-08 18:43:05 +00:00
34 changed files with 175 additions and 1090 deletions

View File

@@ -1,13 +1,15 @@
// See https://aka.ms/vscode-remote/devcontainer.json for format details.
// "image": "ghcr.io/ludeeus/devcontainer/integration:latest",
{
"build": {
"dockerfile": "Dockerfile"
},
"name": "Versatile Thermostat integration",
"appPort": ["8123:8123"],
// "postCreateCommand": "container install",
"postCreateCommand": "./container dev-setup",
"build": {
"dockerfile": "Dockerfile"
},
"name": "Versatile Thermostat integration",
"appPort": [
"8123:8123"
],
// "postCreateCommand": "container install",
"postCreateCommand": "./container dev-setup",
"mounts": [
"source=${localEnv:HOME}/.ssh,target=/home/vscode/.ssh,type=bind,consistency=cached",
@@ -15,53 +17,52 @@
"source=${localEnv:HOME}/SugarSync/Projets/home-assistant/versatile-thermostat-ui-card/dist,target=/workspaces/versatile_thermostat/config/www/community/versatile-thermostat-ui-card,type=bind,consistency=cached"
],
"customizations": {
"vscode": {
"extensions": [
"ms-python.python",
"ms-python.pylint",
// Doesn't work (crash). Default in python is to use Jedi see Settings / Python / Default Language
// "ms-python.vscode-pylance",
"ms-python.isort",
"ms-python.black-formatter",
"visualstudioexptteam.vscodeintellicode",
"redhat.vscode-yaml",
"github.vscode-pull-request-github",
"ryanluker.vscode-coverage-gutters",
"ferrierbenjamin.fold-unfold-all-icone",
"LittleFoxTeam.vscode-python-test-adapter",
"donjayamanne.githistory",
"waderyan.gitblame",
"keesschollaart.vscode-home-assistant",
"vscode.markdown-math",
"yzhang.markdown-all-in-one",
"github.vscode-github-actions",
"azuretools.vscode-docker",
"huizhou.githd",
],
"settings": {
"files.eol": "\n",
"editor.tabSize": 4,
"terminal.integrated.profiles.linux": {
"bash": {
"path": "bash",
"args": []
}
},
"terminal.integrated.defaultProfile.linux": "bash",
// "terminal.integrated.shell.linux": "/bin/bash",
"python.pythonPath": "/usr/bin/python3",
"python.analysis.autoSearchPaths": true,
"pylint.lintOnChange": false,
"python.formatting.provider": "black",
"python.formatting.blackPath": "/usr/local/py-utils/bin/black",
"editor.formatOnPaste": false,
"editor.formatOnSave": true,
"editor.formatOnType": true,
"files.trimTrailingWhitespace": true
// "python.experiments.optOutFrom": ["pythonTestAdapter"],
// "python.analysis.logLevel": "Trace"
}
}
}
"customizations": {
"vscode": {
"extensions": [
"ms-python.python",
"ms-python.pylint",
// Doesn't work (crash). Default in python is to use Jedi see Settings / Python / Default Language
// "ms-python.vscode-pylance",
"ms-python.isort",
"ms-python.black-formatter",
"visualstudioexptteam.vscodeintellicode",
"redhat.vscode-yaml",
"github.vscode-pull-request-github",
"ryanluker.vscode-coverage-gutters",
"ferrierbenjamin.fold-unfold-all-icone",
"LittleFoxTeam.vscode-python-test-adapter",
"donjayamanne.githistory",
"waderyan.gitblame",
"keesschollaart.vscode-home-assistant",
"vscode.markdown-math",
"yzhang.markdown-all-in-one",
"github.vscode-github-actions",
"azuretools.vscode-docker"
],
"settings": {
"files.eol": "\n",
"editor.tabSize": 4,
"terminal.integrated.profiles.linux": {
"bash": {
"path": "bash",
"args": []
}
},
"terminal.integrated.defaultProfile.linux": "bash",
// "terminal.integrated.shell.linux": "/bin/bash",
"python.pythonPath": "/usr/bin/python3",
"python.analysis.autoSearchPaths": true,
"pylint.lintOnChange": false,
"python.formatting.provider": "black",
"python.formatting.blackPath": "/usr/local/py-utils/bin/black",
"editor.formatOnPaste": false,
"editor.formatOnSave": true,
"editor.formatOnType": true,
"files.trimTrailingWhitespace": true
// "python.experiments.optOutFrom": ["pythonTestAdapter"],
// "python.analysis.logLevel": "Trace"
}
}
}
}

View File

@@ -82,10 +82,6 @@ T = TypeVar("T", bound=UnderlyingEntity)
class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
"""Representation of a base class for all Versatile Thermostat device."""
# breaking change with 2024.12 climate workaround
_attr_swing_horizontal_modes = []
_attr_swing_horizontal_mode = ""
_entity_component_unrecorded_attributes = (
ClimateEntity._entity_component_unrecorded_attributes.union(
frozenset(
@@ -131,13 +127,12 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
"max_power_sensor_entity_id",
"temperature_unit",
"is_device_active",
"device_actives",
"nb_device_actives",
"target_temperature_step",
"is_used_by_central_boiler",
"temperature_slope",
"max_on_percent",
"have_valve_regulation",
"last_change_time_from_vtherm",
}
)
)
@@ -220,9 +215,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
self._current_tz = dt_util.get_time_zone(self._hass.config.time_zone)
# Last change time is the datetime of the last change sent by VTherm to the device
# it is used in `over_cliamte` when a state have change from underlying to avoid loops
self._last_change_time_from_vtherm = None
self._last_change_time = None
self._underlyings: list[T] = []
@@ -752,7 +745,14 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
self.hass.create_task(self._check_initial_state())
self.reset_last_change_time_from_vtherm()
self.reset_last_change_time()
# if self.hass.state == CoreState.running:
# await _async_startup_internal()
# else:
# self.hass.bus.async_listen_once(
# EVENT_HOMEASSISTANT_START, _async_startup_internal
# )
def init_underlyings(self):
"""Initialize all underlyings. Should be overriden if necessary"""
@@ -996,19 +996,14 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
return True
return False
@property
def device_actives(self) -> int:
"""Calculate the active devices"""
ret = []
for under in self._underlyings:
if under.is_device_active:
ret.append(under.entity_id)
return ret
@property
def nb_device_actives(self) -> int:
"""Calculate the number of active devices"""
return len(self.device_actives)
ret = 0
for under in self._underlyings:
if under.is_device_active:
ret += 1
return ret
@property
def current_temperature(self) -> float | None:
@@ -1219,7 +1214,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
return
def save_state():
self.reset_last_change_time_from_vtherm()
self.reset_last_change_time()
self.update_custom_attributes()
self.async_write_ha_state()
self.send_event(EventType.HVAC_MODE_EVENT, {"hvac_mode": self._hvac_mode})
@@ -1269,7 +1264,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
):
"""Set new preset mode."""
# We accept a new preset when:
# Wer accept a new preset when:
# 1. last_central_mode is not set,
# 2. or last_central_mode is AUTO,
# 3. or last_central_mode is CENTRAL_MODE_FROST_PROTECTION and preset_mode is PRESET_FROST_PROTECTION (to be abel to re-set the preset_mode)
@@ -1326,7 +1321,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
return
old_preset_mode = self._attr_preset_mode
recalculate = True
if preset_mode == PRESET_NONE:
self._attr_preset_mode = PRESET_NONE
if self._saved_target_temp:
@@ -1335,39 +1329,29 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
self._attr_preset_mode = PRESET_ACTIVITY
await self._async_update_motion_temp()
else:
if self._attr_preset_mode == PRESET_NONE:
self._saved_target_temp = self._target_temp
# if self._attr_preset_mode == PRESET_NONE:
# self._saved_target_temp = self._target_temp
self._attr_preset_mode = preset_mode
# Switch the temperature if window is not 'on'
if self.window_state != STATE_ON:
await self._async_internal_set_temperature(
self.find_preset_temp(preset_mode)
)
else:
# Window is on, so we just save the new expected temp
# so that closing the window will restore it
recalculate = False
self._saved_target_temp = self.find_preset_temp(preset_mode)
await self._async_internal_set_temperature(
self.find_preset_temp(preset_mode)
)
if recalculate:
self.reset_last_temperature_time(old_preset_mode)
self.reset_last_temperature_time(old_preset_mode)
if overwrite_saved_preset:
self.save_preset_mode()
if overwrite_saved_preset:
self.save_preset_mode()
self.recalculate()
self.recalculate()
# Notify only if there was a real change
if self._attr_preset_mode != old_preset_mode:
self.send_event(EventType.PRESET_EVENT, {"preset": self._attr_preset_mode})
def reset_last_change_time_from_vtherm(
def reset_last_change_time(
self, old_preset_mode: str | None = None
): # pylint: disable=unused-argument
"""Reset to now the last change time"""
self._last_change_time_from_vtherm = self.now
_LOGGER.debug(
"%s - last_change_time is now %s", self, self._last_change_time_from_vtherm
)
self._last_change_time = self.now
_LOGGER.debug("%s - last_change_time is now %s", self, self._last_change_time)
def reset_last_temperature_time(self, old_preset_mode: str | None = None):
"""Reset to now the last temperature time if conditions are satisfied"""
@@ -1464,20 +1448,19 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
_LOGGER.info("%s - Set target temp: %s", self, temperature)
if temperature is None:
return
await self._async_internal_set_temperature(temperature)
self._attr_preset_mode = PRESET_NONE
if self.window_state != STATE_ON:
await self._async_internal_set_temperature(temperature)
self.recalculate()
self.reset_last_change_time_from_vtherm()
await self.async_control_heating(force=True)
else:
self._saved_target_temp = temperature
self.recalculate()
self.reset_last_change_time()
await self.async_control_heating(force=True)
async def _async_internal_set_temperature(self, temperature: float):
"""Set the target temperature and the target temperature of underlying climate if any"""
"""Set the target temperature and the target temperature of underlying climate if any
For testing purpose you can pass an event_timestamp.
"""
if temperature:
self._target_temp = temperature
return
def get_state_date_or_now(self, state: State) -> datetime:
"""Extract the last_changed state from State or return now if not available"""
@@ -1537,8 +1520,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
self._last_temperature_measure = self.get_last_updated_date_or_now(
new_state
)
# issue 690 - don't reset the last change time on lastSeen
# self.reset_last_change_time_from_vtherm()
self.reset_last_change_time()
_LOGGER.debug(
"%s - new last_temperature_measure is now: %s",
self,
@@ -2694,7 +2676,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
"timezone": str(self._current_tz),
"temperature_unit": self.temperature_unit,
"is_device_active": self.is_device_active,
"device_actives": self.device_actives,
"nb_device_actives": self.nb_device_actives,
"ema_temp": self._ema_temp,
"is_used_by_central_boiler": self.is_used_by_central_boiler,
@@ -2702,13 +2683,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
"hvac_off_reason": self.hvac_off_reason,
"max_on_percent": self._max_on_percent,
"have_valve_regulation": self.have_valve_regulation,
"last_change_time_from_vtherm": (
self._last_change_time_from_vtherm.astimezone(
self._current_tz
).isoformat()
if self._last_change_time_from_vtherm is not None
else None
),
}
_LOGGER.debug(

View File

@@ -118,7 +118,7 @@ async def async_setup_entry(
SERVICE_SET_AUTO_REGULATION_MODE,
{
vol.Required("auto_regulation_mode"): vol.In(
["None", "Light", "Medium", "Strong", "Slow", "Expert"]
["None", "Light", "Medium", "Strong", "Slow"]
),
},
"service_set_auto_regulation_mode",

View File

@@ -259,21 +259,6 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
if not self.check_valve_regulation_nb_entities(data, step_id):
raise ValveRegulationNbEntitiesIncorrect()
# Check that the min_opening_degrees is correctly set
raw_list = data.get(CONF_MIN_OPENING_DEGREES, None)
if raw_list:
try:
# Validation : Convertir la liste saisie
int_list = [int(x.strip()) for x in raw_list.split(",")]
# Optionnel : Vérifiez des conditions supplémentaires sur la liste
if any(x < 0 for x in int_list):
raise ValueError
except ValueError as exc:
raise ValveRegulationMinOpeningDegreesIncorrect(
CONF_MIN_OPENING_DEGREES
) from exc
def check_config_complete(self, infos) -> bool:
"""True if the config is now complete (ie all mandatory attributes are set)"""
is_central_config = (
@@ -414,8 +399,6 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
errors["base"] = "configuration_not_complete"
except ValveRegulationNbEntitiesIncorrect as err:
errors["base"] = "valve_regulation_nb_entities_incorrect"
except ValveRegulationMinOpeningDegreesIncorrect as err:
errors[str(err)] = "min_opening_degrees_format"
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
@@ -915,7 +898,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
)
class VersatileThermostatConfigFlow( # pylint: disable=abstract-method
class VersatileThermostatConfigFlow(
VersatileThermostatBaseConfigFlow, HAConfigFlow, domain=DOMAIN
):
"""Handle a config flow for Versatile Thermostat."""
@@ -929,8 +912,6 @@ class VersatileThermostatConfigFlow( # pylint: disable=abstract-method
@callback
def async_get_options_flow(config_entry: ConfigEntry):
"""Get options flow for this handler"""
# #713 doesn't work as explained here:https://developers.home-assistant.io/blog/2024/11/12/options-flow
# should be - return VersatileThermostatOptionsFlowHandler() but hass is not initialized
return VersatileThermostatOptionsFlowHandler(config_entry)
async def async_step_finalize(self, _):
@@ -949,12 +930,8 @@ class VersatileThermostatOptionsFlowHandler(
def __init__(self, config_entry: ConfigEntry) -> None:
"""Initialize options flow."""
self._conf_app_id: str | None = None
super().__init__(config_entry.data.copy())
# #713
# self.config_entry = config_entry
self.config_entry = config_entry
_LOGGER.debug(
"CTOR VersatileThermostatOptionsFlowHandler info: %s, entry_id: %s",
self._infos,

View File

@@ -219,7 +219,6 @@ STEP_VALVE_REGULATION = vol.Schema( # pylint: disable=invalid-name
PROPORTIONAL_FUNCTION_TPI,
]
),
vol.Optional(CONF_MIN_OPENING_DEGREES, default=""): str,
}
)

View File

@@ -123,7 +123,6 @@ CONF_STEP_TEMPERATURE = "step_temperature"
CONF_OFFSET_CALIBRATION_LIST = "offset_calibration_entity_ids"
CONF_OPENING_DEGREE_LIST = "opening_degree_entity_ids"
CONF_CLOSING_DEGREE_LIST = "closing_degree_entity_ids"
CONF_MIN_OPENING_DEGREES = "min_opening_degrees"
# Deprecated
CONF_HEATER = "heater_entity_id"
@@ -553,10 +552,6 @@ class ValveRegulationNbEntitiesIncorrect(HomeAssistantError):
The number of specific entities is incorrect."""
class ValveRegulationMinOpeningDegreesIncorrect(HomeAssistantError):
"""Error to indicate that the minimal opening degrees is not a list of int separated by coma"""
class overrides: # pylint: disable=invalid-name
"""An annotation to inform overrides"""

View File

@@ -14,6 +14,6 @@
"quality_scale": "silver",
"requirements": [],
"ssdp": [],
"version": "6.8.4",
"version": "6.8.0",
"zeroconf": []
}

View File

@@ -644,10 +644,6 @@ class NbActiveDeviceForBoilerSensor(SensorEntity):
"""Representation of the threshold of the number of VTherm
which should be active to activate the boiler"""
_entity_component_unrecorded_attributes = SensorEntity._entity_component_unrecorded_attributes.union( # pylint: disable=protected-access
frozenset({"active_device_ids"})
)
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
"""Initialize the energy sensor"""
self._hass = hass
@@ -657,14 +653,6 @@ class NbActiveDeviceForBoilerSensor(SensorEntity):
self._attr_unique_id = "nb_device_active_boiler"
self._attr_value = self._attr_native_value = None # default value
self._entities = []
self._attr_active_device_ids = [] # Holds the entity ids of active devices
@property
def extra_state_attributes(self) -> dict:
"""Return additional attributes for the sensor."""
return {
"active_device_ids": self._attr_active_device_ids,
}
@property
def icon(self) -> str | None:
@@ -730,19 +718,19 @@ class NbActiveDeviceForBoilerSensor(SensorEntity):
self.calculate_nb_active_devices,
)
_LOGGER.info(
"%s - the underlyings that could control the central boiler are %s",
"%s - the underlyings that could controls the central boiler are %s",
self,
underlying_entities_id,
)
self.async_on_remove(listener_cancel)
else:
_LOGGER.debug("%s - no VTherm could control the central boiler", self)
_LOGGER.debug("%s - no VTherm could controls the central boiler", self)
await self.calculate_nb_active_devices(None)
async def calculate_nb_active_devices(self, event: Event):
"""Calculate the number of active VTherm that have an
influence on the central boiler and update the list of active device names."""
influence on central boiler"""
# _LOGGER.debug("%s- calculate_nb_active_devices - the event is %s ", self, event)
@@ -769,8 +757,6 @@ class NbActiveDeviceForBoilerSensor(SensorEntity):
old_state is not None
and new_state.state == old_state.state
and new_hvac_action == old_hvac_action
# issue 698 - force recalculation when underlying climate doesn't have any hvac_action
and new_hvac_action is not None
):
# A false state change
return
@@ -788,28 +774,20 @@ class NbActiveDeviceForBoilerSensor(SensorEntity):
)
nb_active = 0
active_device_ids = []
for entity in self._entities:
device_actives = entity.device_actives
nb_active += entity.nb_device_actives
_LOGGER.debug(
"After examining the hvac_action of %s, device_actives is %s",
"After examining the hvac_action of %s, nb_active is %s",
entity.name,
device_actives,
nb_active,
)
nb_active += len(device_actives)
active_device_ids.extend(device_actives)
self._attr_native_value = nb_active
self._attr_active_device_ids = active_device_ids
_LOGGER.debug(
"%s - Number of active underlying entities is %s", self, nb_active
)
self.async_write_ha_state()
@property
def active_device_ids(self) -> list:
"""Get the list of active device id"""
return self._attr_active_device_ids
def __str__(self):
return f"VersatileThermostat-{self.name}"

View File

@@ -224,15 +224,13 @@
"offset_calibration_entity_ids": "Offset calibration entities",
"opening_degree_entity_ids": "Opening degree entities",
"closing_degree_entity_ids": "Closing degree entities",
"proportional_function": "Algorithm",
"min_opening_degrees": "Min opening degrees"
"proportional_function": "Algorithm"
},
"data_description": {
"offset_calibration_entity_ids": "The list of the 'offset calibration' entities. Set it if your TRV have the entity for better regulation. There should be one per underlying climate entities",
"opening_degree_entity_ids": "The list of the 'opening degree' entities. There should be one per underlying climate entities",
"closing_degree_entity_ids": "The list of the 'closing degree' entities. Set it if your TRV have the entity for better regulation. There should be one per underlying climate entities",
"proportional_function": "Algorithm to use (TPI is the only one for now)",
"min_opening_degrees": "A comma seperated list of minimal opening degrees. Default to 0. Example: 20, 25, 30"
"proportional_function": "Algorithm to use (TPI is the only one for now)"
}
}
},
@@ -470,15 +468,13 @@
"offset_calibration_entity_ids": "Offset calibration entities",
"opening_degree_entity_ids": "Opening degree entities",
"closing_degree_entity_ids": "Closing degree entities",
"proportional_function": "Algorithm",
"min_opening_degrees": "Min opening degrees"
"proportional_function": "Algorithm"
},
"data_description": {
"offset_calibration_entity_ids": "The list of the 'offset calibration' entities. Set it if your TRV have the entity for better regulation. There should be one per underlying climate entities",
"opening_degree_entity_ids": "The list of the 'opening degree' entities. There should be one per underlying climate entities",
"closing_degree_entity_ids": "The list of the 'closing degree' entities. Set it if your TRV have the entity for better regulation. There should be one per underlying climate entities",
"proportional_function": "Algorithm to use (TPI is the only one for now)",
"min_opening_degrees": "A comma seperated list of minimal opening degrees. Default to 0. Example: 20, 25, 30"
"proportional_function": "Algorithm to use (TPI is the only one for now)"
}
}
},
@@ -488,8 +484,7 @@
"window_open_detection_method": "Only one window open detection method should be used. Use either window sensor or automatic detection through temperature threshold but not both",
"no_central_config": "You cannot check 'use central configuration' because no central configuration was found. You need to create a Versatile Thermostat of type 'Central Configuration' to use it.",
"service_configuration_format": "The format of the service configuration is wrong",
"valve_regulation_nb_entities_incorrect": "The number of valve entities for valve regulation should be equal to the number of underlyings",
"min_opening_degrees_format": "A comma separated list of positive integer is expected. Example: 20, 25, 30"
"valve_regulation_nb_entities_incorrect": "The number of valve entities for valve regulation should be equal to the number of underlyings"
},
"abort": {
"already_configured": "Device is already configured"

View File

@@ -183,8 +183,8 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
await super()._async_internal_set_temperature(temperature)
self._regulation_algo.set_target_temp(self.target_temperature)
# Is necessary cause control_heating method will not force the update.
await self._send_regulated_temperature(force=True)
# is done by control_heating method. No need to do it here
# await self._send_regulated_temperature(force=True)
async def _send_regulated_temperature(self, force=False):
"""Sends the regulated temperature to all underlying"""
@@ -709,9 +709,7 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
under_temp_diff = (
(new_target_temp - last_sent_temperature) if new_target_temp else 0
)
step = self.target_temperature_step or 1
if -step < under_temp_diff < step:
if -1 < under_temp_diff < 1:
under_temp_diff = 0
# Issue 99 - some AC turn hvac_mode=cool and hvac_action=idle when sending a HVACMode_OFF command
@@ -768,7 +766,7 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
_LOGGER.debug(
"%s - last_change_time=%s old_state_date_changed=%s old_state_date_updated=%s new_state_date_changed=%s new_state_date_updated=%s",
self,
self._last_change_time_from_vtherm,
self._last_change_time,
old_state_date_changed,
old_state_date_updated,
new_state_date_changed,
@@ -811,10 +809,8 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
# Filter new state when received just after a change from VTherm
# Issue #120 - Some TRV are changing target temperature a very long time (6 sec) after the change.
# In that case a loop is possible if a user change multiple times during this 6 sec.
if new_state_date_updated and self._last_change_time_from_vtherm:
delta = (
new_state_date_updated - self._last_change_time_from_vtherm
).total_seconds()
if new_state_date_updated and self._last_change_time:
delta = (new_state_date_updated - self._last_change_time).total_seconds()
if delta < 10:
_LOGGER.info(
"%s - underlying event is received less than 10 sec after command. Forget it to avoid loop",

View File

@@ -37,7 +37,6 @@ class ThermostatOverClimateValve(ThermostatOverClimate):
"tpi_coef_int",
"tpi_coef_ext",
"power_percent",
"min_opening_degrees",
}
)
)
@@ -52,7 +51,6 @@ class ThermostatOverClimateValve(ThermostatOverClimate):
self._last_calculation_timestamp: datetime | None = None
self._auto_regulation_dpercent: float | None = None
self._auto_regulation_period_min: int | None = None
self._min_opening_degress: list[int] = []
super().__init__(hass, unique_id, name, entry_infos)
@@ -88,14 +86,6 @@ class ThermostatOverClimateValve(ThermostatOverClimate):
offset_list = config_entry.get(CONF_OFFSET_CALIBRATION_LIST, [])
opening_list = config_entry.get(CONF_OPENING_DEGREE_LIST)
closing_list = config_entry.get(CONF_CLOSING_DEGREE_LIST, [])
self._min_opening_degrees = config_entry.get(CONF_MIN_OPENING_DEGREES, None)
min_opening_degrees_list = []
if self._min_opening_degrees:
min_opening_degrees_list = [
int(x.strip()) for x in self._min_opening_degrees.split(",")
]
for idx, _ in enumerate(config_entry.get(CONF_UNDERLYING_LIST)):
offset = offset_list[idx] if idx < len(offset_list) else None
# number of opening should equal number of underlying
@@ -108,11 +98,6 @@ class ThermostatOverClimateValve(ThermostatOverClimate):
opening_degree_entity_id=opening,
closing_degree_entity_id=closing,
climate_underlying=self._underlyings[idx],
min_opening_degree=(
min_opening_degrees_list[idx]
if idx < len(min_opening_degrees_list)
else 0
),
)
self._underlyings_valve_regulation.append(under)
@@ -145,10 +130,6 @@ class ThermostatOverClimateValve(ThermostatOverClimate):
self._attr_extra_state_attributes["tpi_coef_int"] = self._tpi_coef_int
self._attr_extra_state_attributes["tpi_coef_ext"] = self._tpi_coef_ext
self._attr_extra_state_attributes["min_opening_degrees"] = (
self._min_opening_degrees
)
self._attr_extra_state_attributes["valve_open_percent"] = (
self.valve_open_percent
)
@@ -296,15 +277,12 @@ class ThermostatOverClimateValve(ThermostatOverClimate):
return self.valve_open_percent > 0
@property
def device_actives(self) -> int:
def nb_device_actives(self) -> int:
"""Calculate the number of active devices"""
if self.is_device_active:
return [
under.opening_degree_entity_id
for under in self._underlyings_valve_regulation
]
return len(self._underlyings_valve_regulation)
else:
return []
return 0
@property
def activable_underlying_entities(self) -> list | None:

View File

@@ -224,15 +224,13 @@
"offset_calibration_entity_ids": "Offset calibration entities",
"opening_degree_entity_ids": "Opening degree entities",
"closing_degree_entity_ids": "Closing degree entities",
"proportional_function": "Algorithm",
"min_opening_degrees": "Min opening degrees"
"proportional_function": "Algorithm"
},
"data_description": {
"offset_calibration_entity_ids": "The list of the 'offset calibration' entities. Set it if your TRV have the entity for better regulation. There should be one per underlying climate entities",
"opening_degree_entity_ids": "The list of the 'opening degree' entities. There should be one per underlying climate entities",
"closing_degree_entity_ids": "The list of the 'closing degree' entities. Set it if your TRV have the entity for better regulation. There should be one per underlying climate entities",
"proportional_function": "Algorithm to use (TPI is the only one for now)",
"min_opening_degrees": "A comma seperated list of minimal opening degrees. Default to 0. Example: 20, 25, 30"
"proportional_function": "Algorithm to use (TPI is the only one for now)"
}
}
},
@@ -470,15 +468,13 @@
"offset_calibration_entity_ids": "Offset calibration entities",
"opening_degree_entity_ids": "Opening degree entities",
"closing_degree_entity_ids": "Closing degree entities",
"proportional_function": "Algorithm",
"min_opening_degrees": "Min opening degrees"
"proportional_function": "Algorithm"
},
"data_description": {
"offset_calibration_entity_ids": "The list of the 'offset calibration' entities. Set it if your TRV have the entity for better regulation. There should be one per underlying climate entities",
"opening_degree_entity_ids": "The list of the 'opening degree' entities. There should be one per underlying climate entities",
"closing_degree_entity_ids": "The list of the 'closing degree' entities. Set it if your TRV have the entity for better regulation. There should be one per underlying climate entities",
"proportional_function": "Algorithm to use (TPI is the only one for now)",
"min_opening_degrees": "A comma seperated list of minimal opening degrees. Default to 0. Example: 20, 25, 30"
"proportional_function": "Algorithm to use (TPI is the only one for now)"
}
}
},

View File

@@ -218,21 +218,19 @@
}
},
"valve_regulation": {
"title": "Auto-régulation par vanne",
"title": "Auto-régulation par vanne - {name}",
"description": "Configuration de l'auto-régulation par controle direct de la vanne",
"data": {
"offset_calibration_entity_ids": "Entités de 'calibrage du décalage''",
"opening_degree_entity_ids": "Entités 'ouverture de vanne'",
"closing_degree_entity_ids": "Entités 'fermeture de la vanne'",
"proportional_function": "Algorithme",
"min_opening_degrees": "Ouvertures minimales"
"proportional_function": "Algorithme"
},
"data_description": {
"offset_calibration_entity_ids": "La liste des entités 'calibrage du décalage' (offset calibration). Configurez le si votre TRV possède cette fonction pour une meilleure régulation. Il doit y en avoir une par entité climate sous-jacente",
"opening_degree_entity_ids": "La liste des entités 'ouverture de vanne'. Il doit y en avoir une par entité climate sous-jacente",
"closing_degree_entity_ids": "La liste des entités 'fermeture de la vanne'. Configurez le si votre TRV possède cette fonction pour une meilleure régulation. Il doit y en avoir une par entité climate sous-jacente",
"proportional_function": "Algorithme à utiliser (seulement TPI est disponible)",
"min_opening_degrees": "Une liste séparée par des virgules de minimum d'ouverture. Valeur par défaut : 0. Exemple : 20, 25, 30"
"proportional_function": "Algorithme à utiliser (seulement TPI est disponible)"
}
}
},
@@ -259,7 +257,7 @@
}
},
"menu": {
"title": "Menu",
"title": "Menu - {name}",
"description": "Paramétrez votre thermostat. Vous pourrez finaliser la configuration quand tous les paramètres auront été saisis.",
"menu_options": {
"main": "Principaux Attributs",
@@ -464,15 +462,13 @@
"offset_calibration_entity_ids": "Entités de 'calibrage du décalage''",
"opening_degree_entity_ids": "Entités 'ouverture de vanne'",
"closing_degree_entity_ids": "Entités 'fermeture de la vanne'",
"proportional_function": "Algorithme",
"min_opening_degrees": "Ouvertures minimales"
"proportional_function": "Algorithme"
},
"data_description": {
"offset_calibration_entity_ids": "La liste des entités 'calibrage du décalage' (offset calibration). Configurez le si votre TRV possède cette fonction pour une meilleure régulation. Il doit y en avoir une par entité climate sous-jacente",
"opening_degree_entity_ids": "La liste des entités 'ouverture de vanne'. Il doit y en avoir une par entité climate sous-jacente",
"closing_degree_entity_ids": "La liste des entités 'fermeture de la vanne'. Configurez le si votre TRV possède cette fonction pour une meilleure régulation. Il doit y en avoir une par entité climate sous-jacente",
"proportional_function": "Algorithme à utiliser (seulement TPI est disponible)",
"min_opening_degrees": "Une liste séparée par des virgules de minimum d'ouverture. Valeur par défaut : 0. Exemple : 20, 25, 30"
"proportional_function": "Algorithme à utiliser (seulement TPI est disponible)"
}
}
},
@@ -482,8 +478,7 @@
"window_open_detection_method": "Une seule méthode de détection des ouvertures ouvertes doit être utilisée. Utilisez le détecteur d'ouverture ou les seuils de température mais pas les deux.",
"no_central_config": "Vous ne pouvez pas cocher 'Utiliser la configuration centrale' car aucune configuration centrale n'a été trouvée. Vous devez créer un Versatile Thermostat de type 'Central Configuration' pour pouvoir l'utiliser.",
"service_configuration_format": "Mauvais format de la configuration du service",
"valve_regulation_nb_entities_incorrect": "Le nombre d'entités pour la régulation par vanne doit être égal au nombre d'entité sous-jacentes",
"min_opening_degrees_format": "Une liste d'entiers positifs séparés par des ',' est attendu. Exemple : 20, 25, 30"
"valve_regulation_nb_entities_incorrect": "Le nombre d'entités pour la régulation par vanne doit être égal au nombre d'entité sous-jacentes"
},
"abort": {
"already_configured": "Le device est déjà configuré"

View File

@@ -252,7 +252,7 @@ class UnderlyingSwitch(UnderlyingEntity):
self._cancel_cycle()
if self.hvac_mode != hvac_mode:
await super().set_hvac_mode(hvac_mode)
super().set_hvac_mode(hvac_mode)
return True
else:
return False
@@ -1029,7 +1029,6 @@ class UnderlyingValveRegulation(UnderlyingValve):
opening_degree_entity_id: str,
closing_degree_entity_id: str,
climate_underlying: UnderlyingClimate,
min_opening_degree: int = 0,
) -> None:
"""Initialize the underlying TRV with valve regulation"""
super().__init__(
@@ -1046,7 +1045,6 @@ class UnderlyingValveRegulation(UnderlyingValve):
self._max_opening_degree: float = None
self._min_offset_calibration: float = None
self._max_offset_calibration: float = None
self._min_opening_degree: int = min_opening_degree
async def send_percent_open(self):
"""Send the percent open to the underlying valve"""
@@ -1081,9 +1079,6 @@ class UnderlyingValveRegulation(UnderlyingValve):
return
# Send opening_degree
if 0 < self._percent_open < self._min_opening_degree:
self._percent_open = self._min_opening_degree
await super().send_percent_open()
# Send closing_degree if set
@@ -1143,11 +1138,6 @@ class UnderlyingValveRegulation(UnderlyingValve):
"""The offset_calibration_entity_id"""
return self._closing_degree_entity_id
@property
def min_opening_degree(self) -> int:
"""The minimum opening degree"""
return self._min_opening_degree
@property
def have_closing_degree_entity(self) -> bool:
"""Return True if the underlying have a closing_degree entity"""

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 67 KiB

View File

@@ -271,9 +271,5 @@ The custom attributes are as follows:
| ``auto_start_stop_enable`` | Indicates if the VTherm is allowed to auto start/stop |
| ``auto_start_stop_level`` | Indicates the auto start/stop level |
| ``hvac_off_reason`` | Indicates the reason for the thermostat's off state (hvac_off). It can be Window, Auto-start/stop, or Manual |
| ``last_change_time_from_vtherm`` | The date and time of the last change done by VTherm |
| ``nb_device_actives`` | The number of underlying devices seen as active |
| ``device_actives`` | The list of underlying devices seen as active |
These attributes will be requested when you need assistance.

View File

@@ -32,7 +32,6 @@ You need to provide:
1. As many valve opening control entities as there are underlying devices, and in the same order. These parameters are mandatory.
2. As many temperature calibration entities as there are underlying devices, and in the same order. These parameters are optional; they must either all be provided or none.
3. As many valve closure control entities as there are underlying devices, and in the same order. These parameters are optional; they must either all be provided or none.
4. A list of minimum opening values for the valve when it needs to be opened. This field is a list of integers. If the valve needs to be opened, it will be opened at a minimum of this opening value. This allows enough water to pass through when it needs to be opened.
The opening rate calculation algorithm is based on the _TPI_ algorithm described [here](algorithms.md). This is the same algorithm used for _VTherms_ `over_switch` and `over_valve`.

View File

@@ -15,7 +15,6 @@
- [How to Fix It?](#how-to-fix-it)
- [Using a Group of People as a Presence Sensor](#using-a-group-of-people-as-a-presence-sensor)
- [Enable Logs for the Versatile Thermostat](#enable-logs-for-the-versatile-thermostat)
- [VTherm does not track setpoint changes made directly on the underlying device (`over_climate`)](#vtherm-does-not-track-setpoint-changes-made-directly-on-the-underlying-device-over_climate)
## Using a Heatzy
@@ -209,8 +208,4 @@ logs:
```
You must reload the YAML configuration (Developer Tools / YAML / Reload all YAML configuration) or restart Home Assistant for this change to take effect.
Be careful, in debug mode, Versatile Thermostat is very verbose and can quickly slow down Home Assistant or saturate your hard drive. If you switch to debug mode for anomaly analysis, do so only for the time needed to reproduce the bug and disable debug mode immediately afterward.
## VTherm does not track setpoint changes made directly on the underlying device (`over_climate`)
See the details of this feature [here](over-climate.md#track-underlying-temperature-changes).
Be careful, in debug mode, Versatile Thermostat is very verbose and can quickly slow down Home Assistant or saturate your hard drive. If you switch to debug mode for anomaly analysis, do so only for the time needed to reproduce the bug and disable debug mode immediately afterward.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 71 KiB

View File

@@ -270,8 +270,5 @@ Les attributs personnalisés sont les suivants :
| ``auto_start_stop_enable`` | Indique si le VTherm est autorisé à s'auto démarrer/arrêter |
| ``auto_start_stop_level`` | Indique le niveau d'auto start/stop |
| ``hvac_off_reason`` | Indique la raison de l'arrêt (hvac_off) du VTherm. Ce peut être Window, Auto-start/stop ou Manuel |
| ``last_change_time_from_vtherm`` | La date/heure du dernier changement fait par VTherm |
| ``nb_device_actives`` | Le nombre de devices sous-jacents actuellement vus comme actifs |
| ``device_actives`` | La liste des devices sous-jacents actuellement vus comme actifs |
Ces attributs vous seront demandés lors d'une demande d'aide.

View File

@@ -32,8 +32,7 @@ Elle permet de configurer les entités de contrôle de la vanne :
Vous devez donner :
1. autant d'entités de contrôle d'ouverture de la vanne qu'il y a de sous-jacents et dans le même odre. Ces paramètres sont obligatoires,
2. autant d'entités de calibrage du décalage de température qu'il y a de sous-jacents et dans le même ordre. Ces paramètres sont facultatifs ; ils doivent être tous founis ou aucun,
3. autant d'entités de de contrôile du taux de fermture qu'il y a de sous-jacents et dans le même ordre. Ces paramètres sont facultatifs ; ils doivent être tous founis ou aucun,
4. une liste de valeurs minimales d'ouverture de la vanne lorsqu'elle doit être ouverte. Ce champ est une liste d'entier. Si la vanne doit être ouverte, elle le sera au minimum avec cette valeur d'ouverture. Cela permet de laisser passer suffisamment d'eau lorsqu'elle doit être ouverte.
3. autant d'entités de de contrôile du taux de fermture qu'il y a de sous-jacents et dans le même ordre. Ces paramètres sont facultatifs ; ils doivent être tous founis ou aucun
L'algorithme de calcul du taux d'ouverture est basé sur le _TPI_ qui est décrit [ici](algorithms.md). C'est le même algorithme qui est utilisé pour les _VTherm_ `over_switch` et `over_valve`.

View File

@@ -15,7 +15,6 @@
- [Comment réparer ?](#comment-réparer-)
- [Utilisation d'un groupe de personnes comme capteur de présence](#utilisation-dun-groupe-de-personnes-comme-capteur-de-présence)
- [Activer les logs du Versatile Thermostat](#activer-les-logs-du-versatile-thermostat)
- [VTherm ne suit pas les changements de consigne faits directement depuis le sous-jacents (`over_climate`)](#vtherm-ne-suit-pas-les-changements-de-consigne-faits-directement-depuis-le-sous-jacents-over_climate)
## Utilisation d'un Heatzy
@@ -206,8 +205,4 @@ logs:
```
Vous devez recharger la configuration yaml (Outils de dev / Yaml / Toute la configuration Yaml) ou redémarrer Home Assistant pour que ce changement soit pris en compte.
Attention, en mode debug Versatile Thermostat est très verbeux et peut vite ralentir Home Assistant ou saturer votre disque dur. Si vous passez en mode debug pour une analyse d'anomalie il faut s'y mettre juste le temps de reproduire le bug et désactiver le mode debug juste après.
## VTherm ne suit pas les changements de consigne faits directement depuis le sous-jacents (`over_climate`)
Voir le détail de cette fonction [ici](over-climate.md#suivre-les-changements-de-température-du-sous-jacent).
Attention, en mode debug Versatile Thermostat est très verbeux et peut vite ralentir Home Assistant ou saturer votre disque dur. Si vous passez en mode debug pour une analyse d'anomalie il faut s'y mettre juste le temps de reproduire le bug et désactiver le mode debug juste après.

View File

@@ -3,5 +3,5 @@
"content_in_root": false,
"render_readme": true,
"hide_default_branch": false,
"homeassistant": "2024.12.3"
"homeassistant": "2024.10.4"
}

View File

@@ -1 +1 @@
homeassistant==2024.12.3
homeassistant==2024.10.4

View File

@@ -579,7 +579,6 @@ class MockNumber(NumberEntity):
def set_native_value(self, value: float):
"""Change the value"""
self._attr_native_value = value
self.async_write_ha_state()
async def create_thermostat(

View File

@@ -309,7 +309,7 @@ async def test_over_climate_regulation_limitations(
assert entity.hvac_action == HVACAction.HEATING
# the regulated temperature will not change because when we set temp manually it is forced
assert entity.regulated_target_temp == 19.5
assert entity.regulated_target_temp == 17 # 19.5
# 2. set manual target temp (at now - 18) -> the regulation should be taken into account
event_timestamp = now - timedelta(minutes=18)

View File

@@ -1299,7 +1299,7 @@ async def test_auto_start_stop_fast_heat_window(
now: datetime = datetime.now(tz=tz)
# 2. Set mode to Heat and preset to Comfort and close the window
await send_window_change_event(vtherm, False, False, now, False)
send_window_change_event(vtherm, False, False, now, False)
await send_presence_change_event(vtherm, True, False, now)
await send_temperature_change_event(vtherm, 18, now, True)
await vtherm.async_set_hvac_mode(HVACMode.HEAT)
@@ -1474,7 +1474,7 @@ async def test_auto_start_stop_fast_heat_window_mixed(
now: datetime = datetime.now(tz=tz)
# 2. Set mode to Heat and preset to Comfort and close the window
await send_window_change_event(vtherm, False, False, now, False)
send_window_change_event(vtherm, False, False, now, False)
await send_presence_change_event(vtherm, True, False, now)
await send_temperature_change_event(vtherm, 18, now, True)
await vtherm.async_set_hvac_mode(HVACMode.HEAT)

View File

@@ -2,7 +2,7 @@
""" Test the central_configuration """
import asyncio
from datetime import datetime, timedelta
from datetime import datetime
from unittest.mock import patch, call
@@ -29,8 +29,6 @@ from custom_components.versatile_thermostat.binary_sensor import (
CentralBoilerBinarySensor,
)
from custom_components.versatile_thermostat.sensor import NbActiveDeviceForBoilerSensor
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
from .const import * # pylint: disable=wildcard-import, unused-wildcard-import
@@ -105,7 +103,7 @@ async def test_update_central_boiler_state_simple(
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False,
CONF_UNDERLYING_LIST: [switch1.entity_id],
CONF_HEATER: switch1.entity_id,
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_INVERSE_SWITCH: False,
CONF_TPI_COEF_INT: 0.3,
@@ -149,13 +147,6 @@ async def test_update_central_boiler_state_simple(
assert boiler_binary_sensor is not None
assert boiler_binary_sensor.state == STATE_OFF
nb_device_active_sensor: NbActiveDeviceForBoilerSensor = search_entity(
hass, "sensor.nb_device_active_for_boiler", "sensor"
)
assert nb_device_active_sensor is not None
assert nb_device_active_sensor.state == 0
assert nb_device_active_sensor.active_device_ids == []
# 1. start a heater
with patch(
"homeassistant.core.ServiceRegistry.async_call"
@@ -204,9 +195,6 @@ async def test_update_central_boiler_state_simple(
assert api.nb_active_device_for_boiler == 1
assert boiler_binary_sensor.state == STATE_ON
assert nb_device_active_sensor.state == 1
assert nb_device_active_sensor.active_device_ids == ["switch.switch1"]
# 2. stop a heater
with patch(
"homeassistant.core.ServiceRegistry.async_call"
@@ -247,9 +235,6 @@ async def test_update_central_boiler_state_simple(
assert api.nb_active_device_for_boiler == 0
assert boiler_binary_sensor.state == STATE_OFF
assert nb_device_active_sensor.state == 0
assert nb_device_active_sensor.active_device_ids == []
entity.remove_thermostat()
@@ -287,12 +272,10 @@ async def test_update_central_boiler_state_multiple(
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False,
CONF_UNDERLYING_LIST: [
switch1.entity_id,
switch2.entity_id,
switch3.entity_id,
switch4.entity_id,
],
CONF_HEATER: switch1.entity_id,
CONF_HEATER_2: switch2.entity_id,
CONF_HEATER_3: switch3.entity_id,
CONF_HEATER_4: switch4.entity_id,
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_INVERSE_SWITCH: False,
CONF_TPI_COEF_INT: 0.3,
@@ -319,18 +302,10 @@ async def test_update_central_boiler_state_multiple(
assert entity.underlying_entities[1].entity_id == "switch.switch2"
assert entity.underlying_entities[2].entity_id == "switch.switch3"
assert entity.underlying_entities[3].entity_id == "switch.switch4"
assert entity.device_actives == []
assert entity.nb_device_actives == 0
assert api.nb_active_device_for_boiler_threshold == 1
assert api.nb_active_device_for_boiler == 0
nb_device_active_sensor: NbActiveDeviceForBoilerSensor = search_entity(
hass, "sensor.nb_device_active_for_boiler", "sensor"
)
assert nb_device_active_sensor is not None
assert nb_device_active_sensor.state == 0
assert nb_device_active_sensor.active_device_ids == []
# Force the VTherm to heat
await entity.async_set_hvac_mode(HVACMode.HEAT)
await entity.async_set_preset_mode(PRESET_BOOST)
@@ -363,7 +338,7 @@ async def test_update_central_boiler_state_multiple(
await asyncio.sleep(0.1)
assert entity.hvac_action == HVACAction.HEATING
assert entity.device_actives == ["switch.switch1"]
assert entity.nb_device_actives == 1
assert mock_service_call.call_count == 1
# No switch of the boiler
@@ -381,9 +356,6 @@ async def test_update_central_boiler_state_multiple(
assert api.nb_active_device_for_boiler == 1
assert boiler_binary_sensor.state == STATE_OFF
assert nb_device_active_sensor.state == 1
assert nb_device_active_sensor.active_device_ids == ["switch.switch1"]
# 2. start a 2nd heater
with patch(
"homeassistant.core.ServiceRegistry.async_call"
@@ -396,7 +368,7 @@ async def test_update_central_boiler_state_multiple(
await asyncio.sleep(0.1)
assert entity.hvac_action == HVACAction.HEATING
assert entity.device_actives == ["switch.switch1", "switch.switch2"]
assert entity.nb_device_actives == 2
# Only the first heater is started by the algo
assert mock_service_call.call_count == 1
@@ -416,12 +388,6 @@ async def test_update_central_boiler_state_multiple(
assert api.nb_active_device_for_boiler == 2
assert boiler_binary_sensor.state == STATE_OFF
assert nb_device_active_sensor.state == 2
assert nb_device_active_sensor.active_device_ids == [
"switch.switch1",
"switch.switch2",
]
# 3. start a 3rd heater
with patch(
"homeassistant.core.ServiceRegistry.async_call"
@@ -470,13 +436,6 @@ async def test_update_central_boiler_state_multiple(
assert api.nb_active_device_for_boiler == 3
assert boiler_binary_sensor.state == STATE_ON
assert nb_device_active_sensor.state == 3
assert nb_device_active_sensor.active_device_ids == [
"switch.switch1",
"switch.switch2",
"switch.switch3",
]
# 4. start a 4th heater
with patch(
"homeassistant.core.ServiceRegistry.async_call"
@@ -507,14 +466,6 @@ async def test_update_central_boiler_state_multiple(
assert api.nb_active_device_for_boiler == 4
assert boiler_binary_sensor.state == STATE_ON
assert nb_device_active_sensor.state == 4
assert nb_device_active_sensor.active_device_ids == [
"switch.switch1",
"switch.switch2",
"switch.switch3",
"switch.switch4",
]
# 5. stop a heater
with patch(
"homeassistant.core.ServiceRegistry.async_call"
@@ -533,13 +484,6 @@ async def test_update_central_boiler_state_multiple(
assert api.nb_active_device_for_boiler == 3
assert boiler_binary_sensor.state == STATE_ON
assert nb_device_active_sensor.state == 3
assert nb_device_active_sensor.active_device_ids == [
"switch.switch2",
"switch.switch3",
"switch.switch4",
]
# 6. stop a 2nd heater
with patch(
"homeassistant.core.ServiceRegistry.async_call"
@@ -580,12 +524,6 @@ async def test_update_central_boiler_state_multiple(
assert api.nb_active_device_for_boiler == 2
assert boiler_binary_sensor.state == STATE_OFF
assert nb_device_active_sensor.state == 2
assert nb_device_active_sensor.active_device_ids == [
"switch.switch2",
"switch.switch3",
]
entity.remove_thermostat()
@@ -620,7 +558,7 @@ async def test_update_central_boiler_state_simple_valve(
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False,
CONF_UNDERLYING_LIST: [valve1.entity_id],
CONF_VALVE: valve1.entity_id,
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_INVERSE_SWITCH: False,
CONF_TPI_COEF_INT: 0.3,
@@ -656,7 +594,7 @@ async def test_update_central_boiler_state_simple_valve(
now: datetime = datetime.now(tz=tz)
assert entity.hvac_mode == HVACMode.HEAT
assert entity.device_actives == []
assert entity.nb_device_actives == 0
boiler_binary_sensor: CentralBoilerBinarySensor = search_entity(
hass, "binary_sensor.central_boiler", "binary_sensor"
@@ -664,13 +602,6 @@ async def test_update_central_boiler_state_simple_valve(
assert boiler_binary_sensor is not None
assert boiler_binary_sensor.state == STATE_OFF
nb_device_active_sensor: NbActiveDeviceForBoilerSensor = search_entity(
hass, "sensor.nb_device_active_for_boiler", "sensor"
)
assert nb_device_active_sensor is not None
assert nb_device_active_sensor.state == 0
assert nb_device_active_sensor.active_device_ids == []
# 1. start a valve
with patch(
"homeassistant.core.ServiceRegistry.async_call"
@@ -685,7 +616,7 @@ async def test_update_central_boiler_state_simple_valve(
await asyncio.sleep(0.1)
assert entity.hvac_action == HVACAction.HEATING
assert entity.device_actives == ["number.valve1"]
assert entity.nb_device_actives == 1
assert mock_service_call.call_count >= 1
mock_service_call.assert_has_calls(
@@ -713,11 +644,6 @@ async def test_update_central_boiler_state_simple_valve(
assert api.nb_active_device_for_boiler == 1
assert boiler_binary_sensor.state == STATE_ON
assert nb_device_active_sensor.state == 1
assert nb_device_active_sensor.active_device_ids == [
"number.valve1",
]
# 2. stop a heater
with patch(
"homeassistant.core.ServiceRegistry.async_call"
@@ -732,7 +658,7 @@ async def test_update_central_boiler_state_simple_valve(
await asyncio.sleep(0.1)
assert entity.hvac_action == HVACAction.IDLE
assert entity.device_actives == []
assert entity.nb_device_actives == 0
assert mock_service_call.call_count >= 1
mock_service_call.assert_has_calls(
@@ -761,9 +687,6 @@ async def test_update_central_boiler_state_simple_valve(
assert api.nb_active_device_for_boiler == 0
assert boiler_binary_sensor.state == STATE_OFF
assert nb_device_active_sensor.state == 0
assert nb_device_active_sensor.active_device_ids == []
entity.remove_thermostat()
@@ -798,7 +721,7 @@ async def test_update_central_boiler_state_simple_climate(
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False,
CONF_UNDERLYING_LIST: [climate1.entity_id],
CONF_CLIMATE: climate1.entity_id,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
@@ -825,13 +748,6 @@ async def test_update_central_boiler_state_simple_climate(
assert api.nb_active_device_for_boiler_threshold == 1
assert api.nb_active_device_for_boiler == 0
nb_device_active_sensor: NbActiveDeviceForBoilerSensor = search_entity(
hass, "sensor.nb_device_active_for_boiler", "sensor"
)
assert nb_device_active_sensor is not None
assert nb_device_active_sensor.state == 0
assert nb_device_active_sensor.active_device_ids == []
# Force the VTherm to heat
await entity.async_set_hvac_mode(HVACMode.HEAT)
await entity.async_set_preset_mode(PRESET_BOOST)
@@ -840,7 +756,7 @@ async def test_update_central_boiler_state_simple_climate(
now: datetime = datetime.now(tz=tz)
assert entity.hvac_mode == HVACMode.HEAT
assert entity.device_actives == []
assert entity.nb_device_actives == 0
boiler_binary_sensor: CentralBoilerBinarySensor = search_entity(
hass, "binary_sensor.central_boiler", "binary_sensor"
@@ -863,7 +779,7 @@ async def test_update_central_boiler_state_simple_climate(
await asyncio.sleep(0.5)
assert entity.hvac_action == HVACAction.HEATING
assert entity.device_actives == ["climate.climate1"]
assert entity.nb_device_actives == 1
assert mock_service_call.call_count >= 1
mock_service_call.assert_has_calls(
@@ -891,11 +807,6 @@ async def test_update_central_boiler_state_simple_climate(
assert api.nb_active_device_for_boiler == 1
assert boiler_binary_sensor.state == STATE_ON
assert nb_device_active_sensor.state == 1
assert nb_device_active_sensor.active_device_ids == [
"climate.climate1",
]
# 2. stop a climate
with patch(
"homeassistant.core.ServiceRegistry.async_call"
@@ -910,7 +821,7 @@ async def test_update_central_boiler_state_simple_climate(
await asyncio.sleep(0.5)
assert entity.hvac_action == HVACAction.IDLE
assert entity.device_actives == []
assert entity.nb_device_actives == 0
assert mock_service_call.call_count >= 1
mock_service_call.assert_has_calls(
@@ -939,277 +850,6 @@ async def test_update_central_boiler_state_simple_climate(
assert api.nb_active_device_for_boiler == 0
assert boiler_binary_sensor.state == STATE_OFF
assert nb_device_active_sensor.state == 0
assert nb_device_active_sensor.active_device_ids == []
entity.remove_thermostat()
async def test_update_central_boiler_state_simple_climate_valve_regulation(
hass: HomeAssistant,
# skip_hass_states_is_state,
# skip_hass_states_get,
init_central_config_with_boiler_fixture,
):
"""Test that the central boiler state behavior with a climate with valve regulation"""
api = VersatileThermostatAPI.get_vtherm_api(hass)
climate1 = MockClimate(hass, "climate1", "theClimate1")
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverClimateMockName",
unique_id="uniqueId",
data={
CONF_NAME: "TheOverClimateMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 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_UNDERLYING_LIST: [climate1.entity_id],
CONF_OPENING_DEGREE_LIST: ["number.mock_opening_degree"],
CONF_CLOSING_DEGREE_LIST: [],
CONF_OFFSET_CALIBRATION_LIST: [],
CONF_AUTO_REGULATION_MODE: CONF_AUTO_REGULATION_VALVE,
CONF_AUTO_REGULATION_DTEMP: 0,
CONF_AUTO_REGULATION_PERIOD_MIN: 0,
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.1,
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_AUTO_FAN_MODE: CONF_AUTO_FAN_HIGH,
CONF_AUTO_REGULATION_USE_DEVICE_TEMP: False,
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_USE_MAIN_CENTRAL_CONFIG: True,
CONF_USE_PRESETS_CENTRAL_CONFIG: True,
CONF_USE_ADVANCED_CENTRAL_CONFIG: True,
CONF_USED_BY_CENTRAL_BOILER: True,
},
)
open_degree_entity = MockNumber(hass, "mock_opening_degree", "Opening degree")
open_degree_entity.set_native_value(0)
# mock_get_state will be called for each OPENING/CLOSING/OFFSET_CALIBRATION list
mock_get_state_side_effect = SideEffects(
{
open_degree_entity.entity_id: State(
open_degree_entity.entity_id,
open_degree_entity.state,
{"min": 0, "max": 100},
),
"number.mock_closing_degree": State(
"number.mock_closing_degree", "0", {"min": 0, "max": 100}
),
"number.mock_offset_calibration": State(
"number.mock_offset_calibration", "0", {"min": -12, "max": 12}
),
},
State("unknown.entity_id", "unknown"),
)
with patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
return_value=climate1,
), patch(
"homeassistant.core.StateMachine.get",
side_effect=mock_get_state_side_effect.get_side_effects(),
):
entity: ThermostatOverClimate = await create_thermostat(
hass, entry, "climate.theoverclimatemockname"
)
assert entity
assert entity.name == "TheOverClimateMockName"
assert entity.is_over_climate
assert entity.underlying_entities[0].entity_id == "climate.climate1"
assert api.nb_active_device_for_boiler_threshold == 1
assert api.nb_active_device_for_boiler == 0
nb_device_active_sensor: NbActiveDeviceForBoilerSensor = search_entity(
hass, "sensor.nb_device_active_for_boiler", "sensor"
)
assert nb_device_active_sensor is not None
assert nb_device_active_sensor.state == 0
assert nb_device_active_sensor.active_device_ids == []
# Force the VTherm to heat
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
entity._set_now(now)
await send_temperature_change_event(entity, 30, now)
await send_ext_temperature_change_event(entity, 30, now)
await hass.async_block_till_done()
await entity.async_set_hvac_mode(HVACMode.HEAT)
await entity.async_set_preset_mode(PRESET_BOOST)
# the VTherm should not heat now
assert entity.hvac_mode == HVACMode.HEAT
assert entity.hvac_action == HVACAction.OFF
assert entity.activable_underlying_entities[0]._percent_open == 0
assert entity.device_actives == []
boiler_binary_sensor: CentralBoilerBinarySensor = search_entity(
hass, "binary_sensor.central_boiler", "binary_sensor"
)
assert boiler_binary_sensor is not None
assert boiler_binary_sensor.state == STATE_OFF
# 1. start a climate
open_degree_entity.set_native_value(100)
mock_get_state_side_effect = SideEffects(
{
open_degree_entity.entity_id: State(
open_degree_entity.entity_id,
open_degree_entity.state,
{"min": 0, "max": 100},
),
"number.mock_closing_degree": State(
"number.mock_closing_degree", "0", {"min": 0, "max": 100}
),
"number.mock_offset_calibration": State(
"number.mock_offset_calibration", "0", {"min": -12, "max": 12}
),
},
State("unknown.entity_id", "unknown"),
)
with patch(
"homeassistant.core.ServiceRegistry.async_call"
) as mock_service_call, patch(
"custom_components.versatile_thermostat.binary_sensor.send_vtherm_event"
) as mock_send_event, patch(
"homeassistant.core.StateMachine.get",
side_effect=mock_get_state_side_effect.get_side_effects(),
):
now = now + timedelta(minutes=1)
entity._set_now(now)
await send_temperature_change_event(entity, 10, now)
# we have to simulate the climate also else the test don't work
climate1.set_hvac_mode(HVACMode.HEAT)
climate1.set_hvac_action(HVACAction.HEATING)
climate1.async_write_ha_state()
open_degree_entity.set_native_value(100)
# Wait for state event propagation
await hass.async_block_till_done()
assert entity.hvac_action == HVACAction.HEATING
assert entity.device_actives == ["number.mock_opening_degree"]
assert api.nb_active_device_for_boiler == 1
assert boiler_binary_sensor.state == STATE_ON
assert nb_device_active_sensor.state == 1
assert nb_device_active_sensor.active_device_ids == [
"number.mock_opening_degree",
]
assert mock_service_call.call_count >= 1
mock_service_call.assert_has_calls(
[
call.service_call(
"switch",
"turn_on",
service_data={},
target={"entity_id": "switch.pompe_chaudiere"},
),
]
)
assert mock_send_event.call_count >= 1
mock_send_event.assert_has_calls(
[
call.send_vtherm_event(
hass=hass,
event_type=EventType.CENTRAL_BOILER_EVENT,
entity=api.central_boiler_entity,
data={"central_boiler": True},
)
]
)
# 2. stop a climate
open_degree_entity.set_native_value(0)
mock_get_state_side_effect = SideEffects(
{
open_degree_entity.entity_id: State(
open_degree_entity.entity_id,
open_degree_entity.state,
{"min": 0, "max": 100},
),
"number.mock_closing_degree": State(
"number.mock_closing_degree", "0", {"min": 0, "max": 100}
),
"number.mock_offset_calibration": State(
"number.mock_offset_calibration", "0", {"min": -12, "max": 12}
),
},
State("unknown.entity_id", "unknown"),
)
with patch(
"homeassistant.core.ServiceRegistry.async_call"
) as mock_service_call, patch(
"custom_components.versatile_thermostat.binary_sensor.send_vtherm_event"
) as mock_send_event, patch(
"homeassistant.core.StateMachine.get",
side_effect=mock_get_state_side_effect.get_side_effects(),
):
await send_temperature_change_event(entity, 25, now)
climate1.set_hvac_mode(HVACMode.HEAT)
climate1.set_hvac_action(HVACAction.IDLE)
climate1.async_write_ha_state()
open_degree_entity.set_native_value(0)
# Wait for state event propagation
await asyncio.sleep(0.5)
assert entity.hvac_action == HVACAction.OFF
assert entity.device_actives == []
assert mock_service_call.call_count >= 1
mock_service_call.assert_has_calls(
[
call(
"switch",
"turn_off",
service_data={},
target={"entity_id": "switch.pompe_chaudiere"},
)
]
)
assert mock_send_event.call_count >= 1
mock_send_event.assert_has_calls(
[
call.send_vtherm_event(
hass=hass,
event_type=EventType.CENTRAL_BOILER_EVENT,
entity=api.central_boiler_entity,
data={"central_boiler": False},
)
]
)
assert api.nb_active_device_for_boiler == 0
assert boiler_binary_sensor.state == STATE_OFF
assert nb_device_active_sensor.state == 0
assert nb_device_active_sensor.active_device_ids == []
entity.remove_thermostat()

View File

@@ -1581,7 +1581,6 @@ async def test_user_config_flow_over_climate_valve(
CONF_OFFSET_CALIBRATION_LIST: ["number.offset_calibration1"],
CONF_OPENING_DEGREE_LIST: ["number.opening_degree1"],
CONF_CLOSING_DEGREE_LIST: ["number.closing_degree1"],
CONF_MIN_OPENING_DEGREES: "10, 20,0",
},
)
assert result["type"] == FlowResultType.FORM
@@ -1620,7 +1619,6 @@ async def test_user_config_flow_over_climate_valve(
"number.opening_degree2",
],
CONF_CLOSING_DEGREE_LIST: [],
CONF_MIN_OPENING_DEGREES: "10, 20,0",
},
)
assert result["type"] == FlowResultType.MENU
@@ -1717,7 +1715,6 @@ async def test_user_config_flow_over_climate_valve(
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.1,
CONF_MIN_OPENING_DEGREES: "10, 20,0",
}
assert result["result"]
assert result["result"].domain == DOMAIN

View File

@@ -84,8 +84,6 @@ async def test_last_seen_feature(hass: HomeAssistant, skip_hass_states_is_state)
await entity.async_set_hvac_mode(HVACMode.HEAT)
assert entity.hvac_mode == HVACMode.HEAT
last_change_time_from_vtherm = entity._last_change_time_from_vtherm
# 2. activate security feature when date is expired
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
@@ -130,13 +128,9 @@ async def test_last_seen_feature(hass: HomeAssistant, skip_hass_states_is_state)
assert mock_heater_on.call_count == 1
assert entity._last_change_time_from_vtherm == last_change_time_from_vtherm
# 3. change the last seen sensor
event_timestamp = now - timedelta(minutes=4)
await send_last_seen_temperature_change_event(entity, event_timestamp)
assert entity.security_state is False
assert entity.preset_mode is PRESET_COMFORT
assert entity._last_temperature_measure == event_timestamp
assert entity._last_change_time_from_vtherm == last_change_time_from_vtherm

View File

@@ -323,7 +323,7 @@ async def test_underlying_change_follow(
assert entity.target_temperature == entity.min_temp + 1
assert entity.preset_mode is PRESET_NONE
# 4. Change the target temp with < 0.1 (step) value. The value should not be taken
# 4. Change the target temp with < 1 value. The value should not be taken
# Wait 11 sec
event_timestamp = now + timedelta(seconds=11)
await send_climate_change_event_with_temperature(
@@ -333,7 +333,7 @@ async def test_underlying_change_follow(
HVACAction.OFF,
HVACAction.OFF,
event_timestamp,
entity.min_temp + 1.09,
entity.min_temp + 1.5,
True,
"climate.mock_climate", # the underlying climate entity id
)
@@ -949,7 +949,7 @@ async def test_manual_hvac_off_should_take_the_lead_over_window(
now: datetime = datetime.now(tz=tz)
# 1. Set mode to Heat and preset to Comfort and close the window
await send_window_change_event(vtherm, False, False, now, False)
send_window_change_event(vtherm, False, False, now, False)
await send_presence_change_event(vtherm, True, False, now)
await send_temperature_change_event(vtherm, 18, now, True)
await vtherm.async_set_hvac_mode(HVACMode.HEAT)
@@ -1123,7 +1123,7 @@ async def test_manual_hvac_off_should_take_the_lead_over_auto_start_stop(
now: datetime = datetime.now(tz=tz)
# 1. Set mode to Heat and preset to Comfort
await send_window_change_event(vtherm, False, False, now, False)
send_window_change_event(vtherm, False, False, now, False)
await send_presence_change_event(vtherm, True, False, now)
await send_temperature_change_event(vtherm, 18, now, True)
await vtherm.async_set_hvac_mode(HVACMode.HEAT)

View File

@@ -18,7 +18,7 @@ from .const import *
logging.getLogger().setLevel(logging.DEBUG)
@pytest.mark.parametrize("expected_lingering_tasks", [True])
# @pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
# this test fails if run in // with the next because the underlying_valve_regulation is mixed. Don't know why
# @pytest.mark.skip
@@ -138,13 +138,13 @@ async def test_over_climate_valve_mono(hass: HomeAssistant, skip_hass_states_get
assert mock_service_call.call_count == 3
mock_service_call.assert_has_calls(
[
call(domain='number', service='set_value', service_data={'value': 0}, target={'entity_id': 'number.mock_opening_degree'}),
call(domain='number', service='set_value', service_data={'value': 100}, target={'entity_id': 'number.mock_closing_degree'}),
call("climate","set_temperature",{
"entity_id": "climate.mock_climate",
"temperature": 15, # temp-min
},
),
call(domain='number', service='set_value', service_data={'value': 0}, target={'entity_id': 'number.mock_opening_degree'}),
call(domain='number', service='set_value', service_data={'value': 100}, target={'entity_id': 'number.mock_closing_degree'}),
# we have no current_temperature yet
# call(domain='number', service='set_value', service_data={'value': 12}, target={'entity_id': 'number.mock_offset_calibration'}),
]
@@ -300,7 +300,6 @@ async def test_over_climate_valve_mono(hass: HomeAssistant, skip_hass_states_get
await hass.async_block_till_done()
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_over_climate_valve_multi_presence(
hass: HomeAssistant, skip_hass_states_get
):
@@ -483,186 +482,3 @@ async def test_over_climate_valve_multi_presence(
)
assert vtherm.nb_device_actives == 0
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_over_climate_valve_multi_min_opening_degrees(
hass: HomeAssistant, skip_hass_states_get
):
"""Test the normal full start of a thermostat in thermostat_over_climate type
with valve_regulation and min_opening_degreess set"""
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverClimateMockName",
unique_id="uniqueId",
data={
CONF_NAME: "TheOverClimateMockName",
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_CYCLE_MIN: 5,
CONF_DEVICE_POWER: 1,
CONF_USE_MAIN_CENTRAL_CONFIG: False,
CONF_USE_CENTRAL_MODE: False,
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE,
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
CONF_STEP_TEMPERATURE: 0.1,
CONF_UNDERLYING_LIST: ["climate.mock_climate1", "climate.mock_climate2"],
CONF_AC_MODE: False,
CONF_AUTO_REGULATION_MODE: CONF_AUTO_REGULATION_VALVE,
CONF_AUTO_REGULATION_DTEMP: 0.01,
CONF_AUTO_REGULATION_PERIOD_MIN: 0,
CONF_AUTO_FAN_MODE: CONF_AUTO_FAN_HIGH,
CONF_AUTO_REGULATION_USE_DEVICE_TEMP: False,
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.1,
CONF_OPENING_DEGREE_LIST: [
"number.mock_opening_degree1",
"number.mock_opening_degree2",
],
CONF_CLOSING_DEGREE_LIST: [
"number.mock_closing_degree1",
"number.mock_closing_degree2",
],
CONF_OFFSET_CALIBRATION_LIST: [
"number.mock_offset_calibration1",
"number.mock_offset_calibration2",
],
CONF_USE_PRESENCE_FEATURE: False,
CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_MIN_OPENING_DEGREES: "60,70",
}
| MOCK_DEFAULT_CENTRAL_CONFIG
| MOCK_ADVANCED_CONFIG,
)
fake_underlying_climate1 = MockClimate(
hass, "mockUniqueId1", "MockClimateName1", {}
)
fake_underlying_climate2 = MockClimate(
hass, "mockUniqueId2", "MockClimateName2", {}
)
# mock_get_state will be called for each OPENING/CLOSING/OFFSET_CALIBRATION list
mock_get_state_side_effect = SideEffects(
{
# Valve 1 is open
"number.mock_opening_degree1": State(
"number.mock_opening_degree1", "10", {"min": 0, "max": 100}
),
"number.mock_closing_degree1": State(
"number.mock_closing_degree1", "90", {"min": 0, "max": 100}
),
"number.mock_offset_calibration1": State(
"number.mock_offset_calibration1", "0", {"min": -12, "max": 12}
),
# Valve 2 is closed
"number.mock_opening_degree2": State(
"number.mock_opening_degree2", "0", {"min": 0, "max": 100}
),
"number.mock_closing_degree2": State(
"number.mock_closing_degree2", "100", {"min": 0, "max": 100}
),
"number.mock_offset_calibration2": State(
"number.mock_offset_calibration2", "10", {"min": -12, "max": 12}
),
},
State("unknown.entity_id", "unknown"),
)
# 1. initialize the VTherm
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
# fmt: off
with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, \
patch("custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate", side_effect=[fake_underlying_climate1, fake_underlying_climate2]) as mock_find_climate, \
patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call,\
patch("homeassistant.core.StateMachine.get", side_effect=mock_get_state_side_effect.get_side_effects()) as mock_get_state:
# fmt: on
vtherm: ThermostatOverClimateValve = await create_thermostat(hass, entry, "climate.theoverclimatemockname")
assert vtherm
assert isinstance(vtherm, ThermostatOverClimateValve)
assert vtherm.name == "TheOverClimateMockName"
assert vtherm.is_over_climate is True
assert vtherm.have_valve_regulation is True
vtherm._set_now(now)
# initialize the temps
await set_all_climate_preset_temp(hass, vtherm, default_temperatures, "theoverclimatemockname")
await send_temperature_change_event(vtherm, 20, now, True)
await send_ext_temperature_change_event(vtherm, 20, now, True)
await send_presence_change_event(vtherm, False, True, now)
await vtherm.async_set_preset_mode(PRESET_COMFORT)
await vtherm.async_set_hvac_mode(HVACMode.HEAT)
assert vtherm.target_temperature == 19
assert vtherm.nb_device_actives == 0
# 2: set temperature -> should activate the valve and change target
# fmt: off
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", side_effect=mock_get_state_side_effect.get_side_effects()) as mock_get_state:
# fmt: on
now = now + timedelta(minutes=3)
vtherm._set_now(now)
await send_temperature_change_event(vtherm, 18, now, True)
await hass.async_block_till_done()
assert vtherm.is_device_active is True
assert vtherm.valve_open_percent == 20
# the underlying set temperature call and the call to the valve
assert mock_service_call.call_count == 6
mock_service_call.assert_has_calls([
# min is 60
call(domain='number', service='set_value', service_data={'value': 60}, target={'entity_id': 'number.mock_opening_degree1'}),
call(domain='number', service='set_value', service_data={'value': 40}, target={'entity_id': 'number.mock_closing_degree1'}),
call(domain='number', service='set_value', service_data={'value': 3.0}, target={'entity_id': 'number.mock_offset_calibration1'}),
call(domain='number', service='set_value', service_data={'value': 70}, target={'entity_id': 'number.mock_opening_degree2'}),
call(domain='number', service='set_value', service_data={'value': 30}, target={'entity_id': 'number.mock_closing_degree2'}),
call(domain='number', service='set_value', service_data={'value': 12}, target={'entity_id': 'number.mock_offset_calibration2'})
]
)
assert vtherm.nb_device_actives >= 2 # should be 2 but when run in // with the first test it give 3
# 3: set high temperature -> should deactivate the valve and change target
# fmt: off
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", side_effect=mock_get_state_side_effect.get_side_effects()) as mock_get_state:
# fmt: on
now = now + timedelta(minutes=3)
vtherm._set_now(now)
await send_temperature_change_event(vtherm, 22, now, True)
await hass.async_block_till_done()
assert vtherm.is_device_active is False
assert vtherm.valve_open_percent == 0
# the underlying set temperature call and the call to the valve
assert mock_service_call.call_count == 6
mock_service_call.assert_has_calls([
call(domain='number', service='set_value', service_data={'value': 0}, target={'entity_id': 'number.mock_opening_degree1'}),
call(domain='number', service='set_value', service_data={'value': 100}, target={'entity_id': 'number.mock_closing_degree1'}),
call(domain='number', service='set_value', service_data={'value': 7.0}, target={'entity_id': 'number.mock_offset_calibration1'}),
call(domain='number', service='set_value', service_data={'value': 0}, target={'entity_id': 'number.mock_opening_degree2'}),
call(domain='number', service='set_value', service_data={'value': 100}, target={'entity_id': 'number.mock_closing_degree2'}),
call(domain='number', service='set_value', service_data={'value': 12}, target={'entity_id': 'number.mock_offset_calibration2'})
]
)
assert vtherm.nb_device_actives == 0

View File

@@ -246,7 +246,7 @@ async def test_window_management_time_enough(
assert entity.preset_mode is PRESET_BOOST
assert entity.hvac_mode is HVACMode.HEAT
assert entity._saved_hvac_mode is HVACMode.HEAT # No change
assert entity.hvac_off_reason is None
assert entity.hvac_off_reason == None
# Clean the entity
entity.remove_thermostat()
@@ -1762,7 +1762,7 @@ async def test_window_action_frost_temp(hass: HomeAssistant, skip_hass_states_is
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False,
CONF_UNDERLYING_LIST: ["switch.mock_switch"],
CONF_HEATER: "switch.mock_switch",
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
@@ -1802,7 +1802,7 @@ async def test_window_action_frost_temp(hass: HomeAssistant, skip_hass_states_is
event_timestamp = event_timestamp + timedelta(minutes=1)
await send_temperature_change_event(entity, 19, event_timestamp)
# 2. Make the temperature down
# 2. Make the temperature down -> no change
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
@@ -1824,7 +1824,7 @@ async def test_window_action_frost_temp(hass: HomeAssistant, skip_hass_states_is
assert entity.window_state is STATE_OFF
assert entity.window_auto_state is STATE_OFF
# 3. send one degre down in one minute
# 3. send one degre down in one minute -> window is on
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
@@ -1852,6 +1852,8 @@ async def test_window_action_frost_temp(hass: HomeAssistant, skip_hass_states_is
assert entity.preset_mode is PRESET_BOOST
# The eco temp
assert entity.target_temperature == 10
# The last temp is saved
assert entity._saved_target_temp == 21
mock_send_event.assert_has_calls(
[
@@ -1889,6 +1891,7 @@ async def test_window_action_frost_temp(hass: HomeAssistant, skip_hass_states_is
assert entity.preset_mode is PRESET_BOOST
# The eco temp
assert entity.target_temperature == 10
assert entity._saved_target_temp == 21
# 5. send another plus 1.1 degre in one minute -> restore state
with patch(
@@ -1927,8 +1930,9 @@ async def test_window_action_frost_temp(hass: HomeAssistant, skip_hass_states_is
assert entity.hvac_mode is HVACMode.HEAT
# No change on preset
assert entity.preset_mode is PRESET_BOOST
# The Boost temp
# The eco temp
assert entity.target_temperature == 21
assert entity._saved_target_temp == 21
# Clean the entity
entity.remove_thermostat()
@@ -2091,223 +2095,3 @@ async def test_bug_66(
assert entity.window_state == STATE_OFF
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_BOOST
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_window_action_frost_temp_preset_change(
hass: HomeAssistant, skip_hass_states_is_state
):
"""Test the Window management with the frost_temp option and change the preset during
the window is open. This should restore the new preset temperature"""
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_CYCLE_MIN: 5,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
CONF_USE_WINDOW_FEATURE: True,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False,
CONF_UNDERLYING_LIST: ["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_WINDOW_ACTION: CONF_WINDOW_FROST_TEMP,
CONF_WINDOW_SENSOR: "binary_sensor.fake_window_sensor",
CONF_WINDOW_DELAY: 1,
},
)
vtherm: BaseThermostat = await create_thermostat(
hass, entry, "climate.theoverswitchmockname"
)
assert vtherm
await set_all_climate_preset_temp(
hass, vtherm, default_temperatures, "theoverswitchmockname"
)
tz = get_tz(hass) # pylint: disable=invalid-name
now = datetime.now(tz)
await vtherm.async_set_hvac_mode(HVACMode.HEAT)
await vtherm.async_set_preset_mode(PRESET_BOOST)
await hass.async_block_till_done()
assert vtherm.hvac_mode is HVACMode.HEAT
assert vtherm.preset_mode is PRESET_BOOST
assert vtherm.target_temperature == 21
assert vtherm.window_state is STATE_OFF
assert vtherm.is_window_auto_enabled is False
# 1. Turn on the window sensor
now = now + timedelta(minutes=1)
vtherm._set_now(now)
with patch("homeassistant.helpers.condition.state", return_value=True):
try_function = await send_window_change_event(vtherm, True, False, now)
now = now + timedelta(minutes=2)
vtherm._set_now(now)
await try_function(None)
# VTherm should have taken the window action
assert vtherm.target_temperature == 7 # Frost
# No change
assert vtherm.preset_mode is PRESET_BOOST
assert vtherm.hvac_mode is HVACMode.HEAT
# 2. Change the preset to comfort
now = now + timedelta(minutes=1)
vtherm._set_now(now)
await vtherm.async_set_preset_mode(PRESET_COMFORT)
await hass.async_block_till_done()
# VTherm should have taken the new preset temperature
assert vtherm.target_temperature == 7 # frost (window is still open)
assert vtherm.preset_mode is PRESET_COMFORT
assert vtherm.hvac_mode is HVACMode.HEAT
# 3.Turn off the window sensor
now = now + timedelta(minutes=1)
vtherm._set_now(now)
with patch("homeassistant.helpers.condition.state", return_value=True):
try_function = await send_window_change_event(vtherm, False, True, now)
now = now + timedelta(minutes=2)
vtherm._set_now(now)
await try_function(None)
# VTherm should have restore the Comfort preset temperature
assert vtherm.target_temperature == 19 # restore comfort
# No change
assert vtherm.preset_mode is PRESET_COMFORT
assert vtherm.hvac_mode is HVACMode.HEAT
# Clean the entity
vtherm.remove_thermostat()
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_window_action_frost_temp_temp_change(
hass: HomeAssistant, skip_hass_states_is_state
):
"""Test the Window management with the frost_temp option and change the target temp during
the window is open. This should restore the new temperature"""
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_CYCLE_MIN: 5,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
CONF_USE_WINDOW_FEATURE: True,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False,
CONF_UNDERLYING_LIST: ["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_WINDOW_ACTION: CONF_WINDOW_FROST_TEMP,
CONF_WINDOW_SENSOR: "binary_sensor.fake_window_sensor",
CONF_WINDOW_DELAY: 1,
},
)
vtherm: BaseThermostat = await create_thermostat(
hass, entry, "climate.theoverswitchmockname"
)
assert vtherm
await set_all_climate_preset_temp(
hass, vtherm, default_temperatures, "theoverswitchmockname"
)
tz = get_tz(hass) # pylint: disable=invalid-name
now = datetime.now(tz)
await vtherm.async_set_hvac_mode(HVACMode.HEAT)
await vtherm.async_set_preset_mode(PRESET_BOOST)
await hass.async_block_till_done()
assert vtherm.hvac_mode is HVACMode.HEAT
assert vtherm.preset_mode is PRESET_BOOST
assert vtherm.target_temperature == 21
assert vtherm.window_state is STATE_OFF
assert vtherm.is_window_auto_enabled is False
# 1. Turn on the window sensor
now = now + timedelta(minutes=1)
vtherm._set_now(now)
with patch("homeassistant.helpers.condition.state", return_value=True):
try_function = await send_window_change_event(vtherm, True, False, now)
now = now + timedelta(minutes=2)
vtherm._set_now(now)
await try_function(None)
# VTherm should have taken the window action
assert vtherm.target_temperature == 7 # Frost
# No change
assert vtherm.preset_mode is PRESET_BOOST
assert vtherm.hvac_mode is HVACMode.HEAT
# 2. Change the target temperature
now = now + timedelta(minutes=1)
vtherm._set_now(now)
await vtherm.async_set_temperature(temperature=18.5)
await hass.async_block_till_done()
# VTherm should have taken the new preset temperature
assert vtherm.target_temperature == 7 # frost (window is still open)
assert vtherm.preset_mode is PRESET_NONE
assert vtherm.hvac_mode is HVACMode.HEAT
# 3.Turn off the window sensor
now = now + timedelta(minutes=1)
vtherm._set_now(now)
with patch("homeassistant.helpers.condition.state", return_value=True):
try_function = await send_window_change_event(vtherm, False, True, now)
now = now + timedelta(minutes=2)
vtherm._set_now(now)
await try_function(None)
# VTherm should have restore the new target temperature
assert vtherm.target_temperature == 18.5 # restore new target temperature
# No change
assert vtherm.preset_mode is PRESET_NONE
assert vtherm.hvac_mode is HVACMode.HEAT
# Clean the entity
vtherm.remove_thermostat()