Compare commits
9 Commits
6.8.4.beta
...
6.8.4.beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e5e304b71 | ||
|
|
8559dd144a | ||
|
|
65b4690e64 | ||
|
|
6c5ddc315c | ||
|
|
081a2351de | ||
|
|
d9791f6cb0 | ||
|
|
24f6445861 | ||
|
|
083a3a4c81 | ||
|
|
ee42a235c0 |
@@ -1,15 +1,13 @@
|
||||
// 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",
|
||||
@@ -17,52 +15,53 @@
|
||||
"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"
|
||||
],
|
||||
"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",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,6 +137,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
"temperature_slope",
|
||||
"max_on_percent",
|
||||
"have_valve_regulation",
|
||||
"last_change_time_from_vtherm",
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -219,7 +220,9 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
|
||||
self._current_tz = dt_util.get_time_zone(self._hass.config.time_zone)
|
||||
|
||||
self._last_change_time = None
|
||||
# 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._underlyings: list[T] = []
|
||||
|
||||
@@ -749,14 +752,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
|
||||
self.hass.create_task(self._check_initial_state())
|
||||
|
||||
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
|
||||
# )
|
||||
self.reset_last_change_time_from_vtherm()
|
||||
|
||||
def init_underlyings(self):
|
||||
"""Initialize all underlyings. Should be overriden if necessary"""
|
||||
@@ -1223,7 +1219,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
return
|
||||
|
||||
def save_state():
|
||||
self.reset_last_change_time()
|
||||
self.reset_last_change_time_from_vtherm()
|
||||
self.update_custom_attributes()
|
||||
self.async_write_ha_state()
|
||||
self.send_event(EventType.HVAC_MODE_EVENT, {"hvac_mode": self._hvac_mode})
|
||||
@@ -1273,7 +1269,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
):
|
||||
"""Set new preset mode."""
|
||||
|
||||
# Wer accept a new preset when:
|
||||
# We 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)
|
||||
@@ -1330,6 +1326,7 @@ 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:
|
||||
@@ -1341,26 +1338,36 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
if self._attr_preset_mode == PRESET_NONE:
|
||||
self._saved_target_temp = self._target_temp
|
||||
self._attr_preset_mode = preset_mode
|
||||
await self._async_internal_set_temperature(
|
||||
self.find_preset_temp(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)
|
||||
|
||||
self.reset_last_temperature_time(old_preset_mode)
|
||||
if recalculate:
|
||||
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(
|
||||
def reset_last_change_time_from_vtherm(
|
||||
self, old_preset_mode: str | None = None
|
||||
): # pylint: disable=unused-argument
|
||||
"""Reset to now the last change time"""
|
||||
self._last_change_time = self.now
|
||||
_LOGGER.debug("%s - last_change_time is now %s", self, self._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
|
||||
)
|
||||
|
||||
def reset_last_temperature_time(self, old_preset_mode: str | None = None):
|
||||
"""Reset to now the last temperature time if conditions are satisfied"""
|
||||
@@ -1457,19 +1464,20 @@ 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
|
||||
self.recalculate()
|
||||
self.reset_last_change_time()
|
||||
await self.async_control_heating(force=True)
|
||||
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
|
||||
|
||||
async def _async_internal_set_temperature(self, temperature: float):
|
||||
"""Set the target temperature and the target temperature of underlying climate if any
|
||||
For testing purpose you can pass an event_timestamp.
|
||||
"""
|
||||
"""Set the target temperature and the target temperature of underlying climate if any"""
|
||||
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"""
|
||||
@@ -1529,7 +1537,8 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
self._last_temperature_measure = self.get_last_updated_date_or_now(
|
||||
new_state
|
||||
)
|
||||
self.reset_last_change_time()
|
||||
# issue 690 - don't reset the last change time on lastSeen
|
||||
# self.reset_last_change_time_from_vtherm()
|
||||
_LOGGER.debug(
|
||||
"%s - new last_temperature_measure is now: %s",
|
||||
self,
|
||||
@@ -2693,6 +2702,13 @@ 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(
|
||||
|
||||
@@ -259,6 +259,21 @@ 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 = (
|
||||
@@ -399,6 +414,8 @@ 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"
|
||||
@@ -898,7 +915,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
)
|
||||
|
||||
|
||||
class VersatileThermostatConfigFlow(
|
||||
class VersatileThermostatConfigFlow( # pylint: disable=abstract-method
|
||||
VersatileThermostatBaseConfigFlow, HAConfigFlow, domain=DOMAIN
|
||||
):
|
||||
"""Handle a config flow for Versatile Thermostat."""
|
||||
@@ -912,6 +929,8 @@ class VersatileThermostatConfigFlow(
|
||||
@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, _):
|
||||
@@ -930,8 +949,12 @@ 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())
|
||||
self.config_entry = config_entry
|
||||
# #713
|
||||
# self.config_entry = config_entry
|
||||
_LOGGER.debug(
|
||||
"CTOR VersatileThermostatOptionsFlowHandler info: %s, entry_id: %s",
|
||||
self._infos,
|
||||
|
||||
@@ -219,6 +219,7 @@ STEP_VALVE_REGULATION = vol.Schema( # pylint: disable=invalid-name
|
||||
PROPORTIONAL_FUNCTION_TPI,
|
||||
]
|
||||
),
|
||||
vol.Optional(CONF_MIN_OPENING_DEGREES, default=""): str,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -123,6 +123,7 @@ 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"
|
||||
@@ -552,6 +553,10 @@ 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"""
|
||||
|
||||
|
||||
@@ -14,6 +14,6 @@
|
||||
"quality_scale": "silver",
|
||||
"requirements": [],
|
||||
"ssdp": [],
|
||||
"version": "6.8.3",
|
||||
"version": "6.8.4",
|
||||
"zeroconf": []
|
||||
}
|
||||
@@ -224,13 +224,15 @@
|
||||
"offset_calibration_entity_ids": "Offset calibration entities",
|
||||
"opening_degree_entity_ids": "Opening degree entities",
|
||||
"closing_degree_entity_ids": "Closing degree entities",
|
||||
"proportional_function": "Algorithm"
|
||||
"proportional_function": "Algorithm",
|
||||
"min_opening_degrees": "Min opening degrees"
|
||||
},
|
||||
"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)"
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -468,13 +470,15 @@
|
||||
"offset_calibration_entity_ids": "Offset calibration entities",
|
||||
"opening_degree_entity_ids": "Opening degree entities",
|
||||
"closing_degree_entity_ids": "Closing degree entities",
|
||||
"proportional_function": "Algorithm"
|
||||
"proportional_function": "Algorithm",
|
||||
"min_opening_degrees": "Min opening degrees"
|
||||
},
|
||||
"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)"
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -484,7 +488,8 @@
|
||||
"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"
|
||||
"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"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Device is already configured"
|
||||
|
||||
@@ -709,7 +709,9 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
||||
under_temp_diff = (
|
||||
(new_target_temp - last_sent_temperature) if new_target_temp else 0
|
||||
)
|
||||
if -1 < under_temp_diff < 1:
|
||||
|
||||
step = self.target_temperature_step or 1
|
||||
if -step < under_temp_diff < step:
|
||||
under_temp_diff = 0
|
||||
|
||||
# Issue 99 - some AC turn hvac_mode=cool and hvac_action=idle when sending a HVACMode_OFF command
|
||||
@@ -766,7 +768,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,
|
||||
self._last_change_time_from_vtherm,
|
||||
old_state_date_changed,
|
||||
old_state_date_updated,
|
||||
new_state_date_changed,
|
||||
@@ -809,8 +811,10 @@ 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:
|
||||
delta = (new_state_date_updated - self._last_change_time).total_seconds()
|
||||
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 delta < 10:
|
||||
_LOGGER.info(
|
||||
"%s - underlying event is received less than 10 sec after command. Forget it to avoid loop",
|
||||
|
||||
@@ -37,6 +37,7 @@ class ThermostatOverClimateValve(ThermostatOverClimate):
|
||||
"tpi_coef_int",
|
||||
"tpi_coef_ext",
|
||||
"power_percent",
|
||||
"min_opening_degrees",
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -51,6 +52,7 @@ 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)
|
||||
|
||||
@@ -86,6 +88,14 @@ 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
|
||||
@@ -98,6 +108,11 @@ 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)
|
||||
|
||||
@@ -130,6 +145,10 @@ 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
|
||||
)
|
||||
|
||||
@@ -224,13 +224,15 @@
|
||||
"offset_calibration_entity_ids": "Offset calibration entities",
|
||||
"opening_degree_entity_ids": "Opening degree entities",
|
||||
"closing_degree_entity_ids": "Closing degree entities",
|
||||
"proportional_function": "Algorithm"
|
||||
"proportional_function": "Algorithm",
|
||||
"min_opening_degrees": "Min opening degrees"
|
||||
},
|
||||
"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)"
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -468,13 +470,15 @@
|
||||
"offset_calibration_entity_ids": "Offset calibration entities",
|
||||
"opening_degree_entity_ids": "Opening degree entities",
|
||||
"closing_degree_entity_ids": "Closing degree entities",
|
||||
"proportional_function": "Algorithm"
|
||||
"proportional_function": "Algorithm",
|
||||
"min_opening_degrees": "Min opening degrees"
|
||||
},
|
||||
"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)"
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -224,13 +224,15 @@
|
||||
"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"
|
||||
"proportional_function": "Algorithme",
|
||||
"min_opening_degrees": "Ouvertures minimales"
|
||||
},
|
||||
"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)"
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -462,13 +464,15 @@
|
||||
"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"
|
||||
"proportional_function": "Algorithme",
|
||||
"min_opening_degrees": "Ouvertures minimales"
|
||||
},
|
||||
"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)"
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -478,7 +482,8 @@
|
||||
"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"
|
||||
"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"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Le device est déjà configuré"
|
||||
|
||||
@@ -252,7 +252,7 @@ class UnderlyingSwitch(UnderlyingEntity):
|
||||
self._cancel_cycle()
|
||||
|
||||
if self.hvac_mode != hvac_mode:
|
||||
super().set_hvac_mode(hvac_mode)
|
||||
await super().set_hvac_mode(hvac_mode)
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
@@ -1029,6 +1029,7 @@ 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__(
|
||||
@@ -1045,6 +1046,7 @@ 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"""
|
||||
@@ -1079,6 +1081,9 @@ 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
|
||||
@@ -1138,6 +1143,11 @@ 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"""
|
||||
|
||||
BIN
documentation/en/images/1.png
Normal file
BIN
documentation/en/images/1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 67 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 62 KiB |
@@ -271,5 +271,9 @@ 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.
|
||||
@@ -32,6 +32,7 @@ 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`.
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
- [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
|
||||
@@ -208,4 +209,8 @@ 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.
|
||||
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).
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 66 KiB |
@@ -270,5 +270,8 @@ 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.
|
||||
|
||||
@@ -32,7 +32,8 @@ 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
|
||||
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.
|
||||
|
||||
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`.
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
- [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
|
||||
@@ -205,4 +206,8 @@ 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.
|
||||
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).
|
||||
@@ -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
|
||||
send_window_change_event(vtherm, False, False, now, False)
|
||||
await 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
|
||||
send_window_change_event(vtherm, False, False, now, False)
|
||||
await 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)
|
||||
|
||||
@@ -1581,6 +1581,7 @@ 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
|
||||
@@ -1619,6 +1620,7 @@ 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
|
||||
@@ -1715,6 +1717,7 @@ 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
|
||||
|
||||
@@ -84,6 +84,8 @@ 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"
|
||||
@@ -128,9 +130,13 @@ 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
|
||||
|
||||
@@ -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 < 1 value. The value should not be taken
|
||||
# 4. Change the target temp with < 0.1 (step) 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.5,
|
||||
entity.min_temp + 1.09,
|
||||
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
|
||||
send_window_change_event(vtherm, False, False, now, False)
|
||||
await 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
|
||||
send_window_change_event(vtherm, False, False, now, False)
|
||||
await 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)
|
||||
|
||||
@@ -483,3 +483,186 @@ 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
|
||||
|
||||
@@ -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 == None
|
||||
assert entity.hvac_off_reason is 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_HEATER: "switch.mock_switch",
|
||||
CONF_UNDERLYING_LIST: ["switch.mock_switch"],
|
||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||
CONF_TPI_COEF_INT: 0.3,
|
||||
CONF_TPI_COEF_EXT: 0.01,
|
||||
@@ -1927,7 +1927,7 @@ 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 eco temp
|
||||
# The Boost temp
|
||||
assert entity.target_temperature == 21
|
||||
|
||||
# Clean the entity
|
||||
@@ -2091,3 +2091,223 @@ 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()
|
||||
|
||||
Reference in New Issue
Block a user