Compare commits

..

7 Commits

Author SHA1 Message Date
Jean-Marc Collin
64968687f5 Release 2024-11-17 10:56:17 +00:00
Jean-Marc Collin
132c98a612 Fix Testus 2024-11-17 10:51:02 +00:00
Jean-Marc Collin
3113f3b57f Work in simuated environment 2024-11-17 10:39:59 +00:00
Jean-Marc Collin
f0526a0c93 With 1rst implementation of VTherm TRVZB and underlying 2024-11-17 07:47:44 +00:00
Jean-Marc Collin
c2677bf7c5 Next (not finished) 2024-11-16 21:43:53 +00:00
Jean-Marc Collin
8e01e69585 Fix configuration 2024-11-16 21:22:13 +00:00
Jean-Marc Collin
643f061fe2 With Sonoff configuration ok 2024-11-16 18:31:11 +00:00
15 changed files with 109 additions and 253 deletions

183
README.md
View File

@@ -389,6 +389,82 @@ These three parameters make it possible to modulate the regulation and avoid mul
Self-regulation consists of forcing the equipment to go further by forcing its set temperature regularly. Its consumption can therefore be increased, as well as its wear.
#### Self-regulation in Expert mode
In **Expert** mode you can finely adjust the auto-regulation parameters to achieve your objectives and optimize as best as possible. The algorithm calculates the difference between the setpoint and the actual temperature of the room. This discrepancy is called error.
The adjustable parameters are as follows:
1. `kp`: the factor applied to the raw error,
2. `ki`: the factor applied to the accumulation of errors,
3. `k_ext`: the factor applied to the difference between the interior temperature and the exterior temperature,
4. `offset_max`: the maximum correction (offset) that the regulation can apply,
5. `stabilization_threshold`: a stabilization threshold which, when reached by the error, resets the accumulation of errors to 0,
6. `accumulated_error_threshold`: the maximum for error accumulation.
For tuning, these observations must be taken into account:
1. `kp * error` will give the offset linked to the raw error. This offset is directly proportional to the error and will be 0 when the target is reached,
2. the accumulation of the error makes it possible to correct the stabilization of the curve while there remains an error. The error accumulates and the offset therefore gradually increases which should eventually stabilize at the target temperature. For this fundamental parameter to have an effect it must not be too small. An average value is 30
3. `ki * accumulated_error_threshold` will give the maximum offset linked to the accumulation of the error,
4. `k_ext` allows a correction to be applied immediately (without waiting for errors to accumulate) when the outside temperature is very different from the target temperature. If the stabilization is done too high when the temperature differences are significant, it is because this parameter is too high. It should be possible to cancel completely to let the first 2 offsets take place
The pre-programmed values are as follows:
Slow régulation :
kp: 0.2 # 20% of the current internal regulation offset are caused by the current difference of target temperature and room temperature
ki: 0.8 / 288.0 # 80% of the current internal regulation offset are caused by the average offset of the past 24 hours
k_ext: 1.0 / 25.0 # this will add 1°C to the offset when it's 25°C colder outdoor than indoor
offset_max: 2.0 # limit to a final offset of -2°C to +2°C
stabilization_threshold: 0.0 # this needs to be disabled as otherwise the long term accumulated error will always be reset when the temp briefly crosses from/to below/above the target
accumulated_error_threshold: 2.0 * 288 # this allows up to 2°C long term offset in both directions
Light régulation :
kp: 0.2
ki: 0.05
k_ext: 0.05
offset_max: 1.5
stabilization_threshold: 0.1
accumulated_error_threshold: 10
Medium régulation :
kp: 0.3
ki: 0.05
k_ext: 0.1
offset_max: 2
stabilization_threshold: 0.1
accumulated_error_threshold: 20
Strong régulation :
"""Strong parameters for regulation
A set of parameters which doesn't take into account the external temp
and concentrate to internal temp error + accumulated error.
This should work for cold external conditions which else generates
high external_offset"""
kp: 0.4
ki: 0.08
k_ext: 0.0
offset_max: 5
stabilization_threshold: 0.1
accumulated_error_threshold: 50
To use Expert mode you must declare the values you want to use for each of these parameters in your `configuration.yaml` in the following form:
```
versatile_thermostat:
auto_regulation_expert:
kp: 0.4
ki: 0.08
k_ext: 0.0
offset_max: 5
stabilization_threshold: 0.1
accumulated_error_threshold: 50
```
and of course, configure the VTherm's self-regulation mode in **Expert** mode. All VTherms in Expert mode will use these same settings.
For the changes to be taken into account, you must either **completely restart Home Assistant** or just the **Versatile Thermostat integration** (Dev tools / Yaml / reloading the configuration / Versatile Thermostat).
#### Internal temperature compensation
Sometimes, a devices internal temperature sensor (like in a TRV or AC) can give inaccurate readings, especially if its too close to a heat source. This can cause the device to stop heating too soon.
For example:
@@ -724,113 +800,6 @@ context:
> ![Tip](images/tips.png) _*Notes*_
> Controlling a central boiler using software or hardware such as home automation can pose risks to its proper functioning. Before using these functions, make sure that your boiler has safety functions and that they are working. Turning on a boiler if all the taps are closed can generate excess pressure, for example.
## Expert Mode Settings
Expert Mode settings refer to Settings made in the Home Assistant `configuration.yaml` file under the `versatile_thermostat` section. You might have to add this section by yourself to the `configuration.yaml` file.
These settings are meant to be used only in **specific niche cases and with careful considerations**.
The following sections describe the available export mode settings in detail with examples on how to configure them. Be aware that these settings require a **complete restart** of Home Assistant or a **reload of Versatile Thermostat integration** (Dev tools / Yaml / reloading the configuration / Versatile Thermostat) to take effect.
### Self-regulation in Expert mode
In **Expert** mode you can finely adjust the auto-regulation parameters to achieve your objectives and optimize as best as possible. The algorithm calculates the difference between the setpoint and the actual temperature of the room. This discrepancy is called error.
The adjustable parameters are as follows:
1. `kp`: the factor applied to the raw error,
2. `ki`: the factor applied to the accumulation of errors,
3. `k_ext`: the factor applied to the difference between the interior temperature and the exterior temperature,
4. `offset_max`: the maximum correction (offset) that the regulation can apply,
5. `stabilization_threshold`: a stabilization threshold which, when reached by the error, resets the accumulation of errors to 0,
6. `accumulated_error_threshold`: the maximum for error accumulation.
For tuning, these observations must be taken into account:
1. `kp * error` will give the offset linked to the raw error. This offset is directly proportional to the error and will be 0 when the target is reached,
2. the accumulation of the error makes it possible to correct the stabilization of the curve while there remains an error. The error accumulates and the offset therefore gradually increases which should eventually stabilize at the target temperature. For this fundamental parameter to have an effect it must not be too small. An average value is 30
3. `ki * accumulated_error_threshold` will give the maximum offset linked to the accumulation of the error,
4. `k_ext` allows a correction to be applied immediately (without waiting for errors to accumulate) when the outside temperature is very different from the target temperature. If the stabilization is done too high when the temperature differences are significant, it is because this parameter is too high. It should be possible to cancel completely to let the first 2 offsets take place
The pre-programmed values are as follows:
Slow régulation :
kp: 0.2 # 20% of the current internal regulation offset are caused by the current difference of target temperature and room temperature
ki: 0.8 / 288.0 # 80% of the current internal regulation offset are caused by the average offset of the past 24 hours
k_ext: 1.0 / 25.0 # this will add 1°C to the offset when it's 25°C colder outdoor than indoor
offset_max: 2.0 # limit to a final offset of -2°C to +2°C
stabilization_threshold: 0.0 # this needs to be disabled as otherwise the long term accumulated error will always be reset when the temp briefly crosses from/to below/above the target
accumulated_error_threshold: 2.0 * 288 # this allows up to 2°C long term offset in both directions
Light régulation :
kp: 0.2
ki: 0.05
k_ext: 0.05
offset_max: 1.5
stabilization_threshold: 0.1
accumulated_error_threshold: 10
Medium régulation :
kp: 0.3
ki: 0.05
k_ext: 0.1
offset_max: 2
stabilization_threshold: 0.1
accumulated_error_threshold: 20
Strong régulation :
"""Strong parameters for regulation
A set of parameters which doesn't take into account the external temp
and concentrate to internal temp error + accumulated error.
This should work for cold external conditions which else generates
high external_offset"""
kp: 0.4
ki: 0.08
k_ext: 0.0
offset_max: 5
stabilization_threshold: 0.1
accumulated_error_threshold: 50
To use Expert mode you must declare the values you want to use for each of these parameters in your `configuration.yaml` in the following form:
```
versatile_thermostat:
auto_regulation_expert:
kp: 0.4
ki: 0.08
k_ext: 0.0
offset_max: 5
stabilization_threshold: 0.1
accumulated_error_threshold: 50
```
and of course, configure the VTherm's self-regulation mode in **Expert** mode. All VTherms in Expert mode will use these same settings.
For the changes to be taken into account, you must either **completely restart Home Assistant** or just the **Versatile Thermostat integration** (Dev tools / Yaml / reloading the configuration / Versatile Thermostat).
### On Time Clamping (max_on_percent)
The calculated on time percent can be limited to a maximum percentage of the cycle duration. This setting has to be made in expert mode and will be used for all Versatile Thermostats.
```
versatile_thermostat:
max_on_percent: 0.8
```
The example above limits the maximum ON time to 80% (0.8) of the cycle length. If the cycle length is for example 600 seconds (10min), the maximum ON time will be limited to 480 seconds (8min). The remaining 120 seconds of the cycle will always remain in the OFF state.
There are three debug attributes of interest regarding this feature:
* `max_on_percent` # clamping setting as configured in expert mode
* `calculated_on_percent` # calculated on percent without clamping applied
* `on_percent` # used on percent with clamping applied
<details>
<summary>Parameter summary</summary>

View File

@@ -54,7 +54,6 @@ from .const import (
CONF_THERMOSTAT_SWITCH,
CONF_THERMOSTAT_CLIMATE,
CONF_THERMOSTAT_VALVE,
CONF_MAX_ON_PERCENT,
)
from .vtherm_api import VersatileThermostatAPI
@@ -87,7 +86,6 @@ CONFIG_SCHEMA = vol.Schema(
CONF_AUTO_REGULATION_EXPERT: vol.Schema(SELF_REGULATION_PARAM_SCHEMA),
CONF_SHORT_EMA_PARAMS: vol.Schema(EMA_PARAM_SCHEMA),
CONF_SAFETY_MODE: vol.Schema(SAFETY_MODE_PARAM_SCHEMA),
vol.Optional(CONF_MAX_ON_PERCENT): vol.Coerce(float),
}
),
},

View File

@@ -137,8 +137,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
"is_device_active",
"target_temperature_step",
"is_used_by_central_boiler",
"temperature_slope",
"max_on_percent"
"temperature_slope"
}
)
)
@@ -504,8 +503,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
entry_infos.get(CONF_WINDOW_ACTION) or CONF_WINDOW_TURN_OFF
)
self._max_on_percent = api.max_on_percent
_LOGGER.debug(
"%s - Creation of a new VersatileThermostat entity: unique_id=%s",
self,
@@ -2665,7 +2662,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
"is_used_by_central_boiler": self.is_used_by_central_boiler,
"temperature_slope": round(self.last_temperature_slope or 0, 3),
"hvac_off_reason": self.hvac_off_reason,
"max_on_percent": self._max_on_percent,
}
_LOGGER.debug(

View File

@@ -137,7 +137,6 @@ CONF_VALVE_4 = "valve_entity4_id"
# Global params into configuration.yaml
CONF_SHORT_EMA_PARAMS = "short_ema_params"
CONF_SAFETY_MODE = "safety_mode"
CONF_MAX_ON_PERCENT = "max_on_percent"
CONF_USE_MAIN_CENTRAL_CONFIG = "use_main_central_config"
CONF_USE_TPI_CENTRAL_CONFIG = "use_tpi_central_config"
@@ -464,9 +463,9 @@ class RegulationParamVeryStrong:
kp: float = 0.6
ki: float = 0.1
k_ext: float = 0.2
offset_max: float = 8
offset_max: float = 4
stabilization_threshold: float = 0.1
accumulated_error_threshold: float = 80
accumulated_error_threshold: float = 30
class EventType(Enum):

View File

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

View File

@@ -31,7 +31,6 @@ class PropAlgorithm:
cycle_min: int,
minimal_activation_delay: int,
vtherm_entity_id: str = None,
max_on_percent: float = None,
) -> None:
"""Initialisation of the Proportional Algorithm"""
_LOGGER.debug(
@@ -79,7 +78,6 @@ class PropAlgorithm:
self._off_time_sec = self._cycle_min * 60
self._security = False
self._default_on_percent = 0
self._max_on_percent = max_on_percent
def calculate(
self,
@@ -163,15 +161,6 @@ class PropAlgorithm:
)
self._on_percent = self._calculated_on_percent
if self._max_on_percent is not None and self._on_percent > self._max_on_percent:
_LOGGER.debug(
"%s - Heating period clamped to %s (instead of %s) due to max_on_percent setting.",
self._vtherm_entity_id,
self._max_on_percent,
self._on_percent,
)
self._on_percent = self._max_on_percent
self._on_time_sec = self._on_percent * self._cycle_min * 60
# Do not heat for less than xx sec

View File

@@ -151,13 +151,15 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
"""True if the Thermostat is over_climate"""
return True
def calculate_hvac_action(self, under_list: list) -> HVACAction | None:
"""Calculate an hvac action based on the hvac_action of the list in argument"""
@property
def hvac_action(self) -> HVACAction | None:
"""Returns the current hvac_action by checking all hvac_action of the underlyings"""
# if one not IDLE or OFF -> return it
# else if one IDLE -> IDLE
# else OFF
one_idle = False
for under in under_list:
for under in self._underlyings:
if (action := under.hvac_action) not in [
HVACAction.IDLE,
HVACAction.OFF,
@@ -169,11 +171,6 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
return HVACAction.IDLE
return HVACAction.OFF
@property
def hvac_action(self) -> HVACAction | None:
"""Returns the current hvac_action by checking all hvac_action of the underlyings"""
return self.calculate_hvac_action(self._underlyings)
@overrides
async def _async_internal_set_temperature(self, temperature: float):
"""Set the target temperature and the target temperature of underlying climate if any"""
@@ -924,10 +921,7 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
# Stop here
return False
elif (
action == AUTO_START_STOP_ACTION_ON
and self.hvac_off_reason == HVAC_OFF_REASON_AUTO_START_STOP
):
elif action == AUTO_START_STOP_ACTION_ON:
_LOGGER.info(
"%s - Turning ON the Vtherm due to auto-start-stop conditions", self
)

View File

@@ -5,7 +5,7 @@ import logging
from datetime import datetime
from homeassistant.core import HomeAssistant
from homeassistant.components.climate import HVACMode, HVACAction
from homeassistant.components.climate import HVACMode
from .underlyings import UnderlyingSonoffTRVZB
@@ -96,7 +96,6 @@ class ThermostatOverSonoffTRVZB(ThermostatOverClimate):
offset_calibration_entity_id=offset,
opening_degree_entity_id=opening,
closing_degree_entity_id=closing,
climate_underlying=self._underlyings[idx],
)
self._underlyings_sonoff_trvzb.append(under)
@@ -241,9 +240,3 @@ class ThermostatOverSonoffTRVZB(ThermostatOverClimate):
return 0
else:
return self._valve_open_percent
@property
def hvac_action(self) -> HVACAction | None:
"""Returns the current hvac_action by checking all hvac_action of the _underlyings_sonoff_trvzb"""
return self.calculate_hvac_action(self._underlyings_sonoff_trvzb)

View File

@@ -25,23 +25,20 @@ _LOGGER = logging.getLogger(__name__)
class ThermostatOverSwitch(BaseThermostat[UnderlyingSwitch]):
"""Representation of a base class for a Versatile Thermostat over a switch."""
_entity_component_unrecorded_attributes = (
BaseThermostat._entity_component_unrecorded_attributes.union(
frozenset(
{
"is_over_switch",
"is_inversed",
"underlying_entities",
"on_time_sec",
"off_time_sec",
"cycle_min",
"function",
"tpi_coef_int",
"tpi_coef_ext",
"power_percent",
"calculated_on_percent",
}
)
_entity_component_unrecorded_attributes = BaseThermostat._entity_component_unrecorded_attributes.union( # pylint: disable=protected-access
frozenset(
{
"is_over_switch",
"is_inversed",
"underlying_entities",
"on_time_sec",
"off_time_sec",
"cycle_min",
"function",
"tpi_coef_int",
"tpi_coef_ext",
"power_percent",
}
)
)
@@ -82,7 +79,6 @@ class ThermostatOverSwitch(BaseThermostat[UnderlyingSwitch]):
self._cycle_min,
self._minimal_activation_delay,
self.name,
max_on_percent=self._max_on_percent,
)
lst_switches = config_entry.get(CONF_UNDERLYING_LIST)
@@ -148,9 +144,6 @@ class ThermostatOverSwitch(BaseThermostat[UnderlyingSwitch]):
self._attr_extra_state_attributes["function"] = self._proportional_function
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[
"calculated_on_percent"
] = self._prop_algorithm.calculated_on_percent
self.async_write_ha_state()
_LOGGER.debug(

View File

@@ -43,7 +43,6 @@ class ThermostatOverValve(BaseThermostat[UnderlyingValve]): # pylint: disable=a
"auto_regulation_dpercent",
"auto_regulation_period_min",
"last_calculation_timestamp",
"calculated_on_percent",
}
)
)
@@ -97,7 +96,6 @@ class ThermostatOverValve(BaseThermostat[UnderlyingValve]): # pylint: disable=a
self._cycle_min,
self._minimal_activation_delay,
self.name,
max_on_percent=self._max_on_percent,
)
lst_valves = config_entry.get(CONF_UNDERLYING_LIST)
@@ -181,9 +179,6 @@ class ThermostatOverValve(BaseThermostat[UnderlyingValve]): # pylint: disable=a
if self._last_calculation_timestamp
else None
)
self._attr_extra_state_attributes[
"calculated_on_percent"
] = self._prop_algorithm.calculated_on_percent
self.async_write_ha_state()
_LOGGER.debug(

View File

@@ -1030,18 +1030,15 @@ class UnderlyingSonoffTRVZB(UnderlyingValve):
offset_calibration_entity_id: str,
opening_degree_entity_id: str,
closing_degree_entity_id: str,
climate_underlying: UnderlyingClimate,
) -> None:
"""Initialize the underlying Sonoff TRV"""
super().__init__(hass, thermostat, opening_degree_entity_id)
self._offset_calibration_entity_id = offset_calibration_entity_id
self._opening_degree_entity_id = opening_degree_entity_id
self._closing_degree_entity_id = closing_degree_entity_id
self._climate_underlying = climate_underlying
self._is_min_max_initialized = False
self._max_opening_degree = None
self._min_offset_calibration = None
self._max_offset_calibration = None
async def send_percent_open(self):
"""Send the percent open to the underlying valve"""
@@ -1055,9 +1052,6 @@ class UnderlyingSonoffTRVZB(UnderlyingValve):
self._min_offset_calibration = self._hass.states.get(
self._offset_calibration_entity_id
).attributes.get("min")
self._max_offset_calibration = self._hass.states.get(
self._offset_calibration_entity_id
).attributes.get("max")
self._is_min_max_initialized = (
self._max_opening_degree is not None
@@ -1073,32 +1067,15 @@ class UnderlyingSonoffTRVZB(UnderlyingValve):
# Send opening_degree
await super().send_percent_open()
# Send closing_degree.
# Send closing_degree. TODO 100 hard-coded or take the max of the _closing_degree_entity_id ?
await self._send_value_to_number(
self._closing_degree_entity_id,
closing_degree := self._max_opening_degree - self._percent_open,
self._max_opening_degree - self._percent_open,
)
# send offset_calibration to the difference between target temp and local temp
offset = 0
if (
local_temp := self._climate_underlying.underlying_current_temperature
) is not None and (
room_temp := self._thermostat.current_temperature
) is not None:
offset = min(
self._max_offset_calibration,
max(self._min_offset_calibration, room_temp - local_temp),
)
await self._send_value_to_number(self._offset_calibration_entity_id, offset)
_LOGGER.debug(
"%s - SonoffTRVZB - I have sent offset_calibration=%s opening_degree=%s closing_degree=%s",
self,
offset,
self._percent_open,
closing_degree,
# send offset_calibration to the min value
await self._send_value_to_number(
self._offset_calibration_entity_id, self._min_offset_calibration
)
@property

View File

@@ -15,7 +15,6 @@ from .const import (
CONF_SAFETY_MODE,
CONF_THERMOSTAT_TYPE,
CONF_THERMOSTAT_CENTRAL_CONFIG,
CONF_MAX_ON_PERCENT,
)
VTHERM_API_NAME = "vtherm_api"
@@ -61,7 +60,6 @@ class VersatileThermostatAPI(dict):
self._central_mode_select = None
# A dict that will store all Number entities which holds the temperature
self._number_temperatures = dict()
self._max_on_percent = None
def find_central_configuration(self):
"""Search for a central configuration"""
@@ -109,12 +107,6 @@ class VersatileThermostatAPI(dict):
if self._safety_mode:
_LOGGER.debug("We have found safet_mode params %s", self._safety_mode)
self._max_on_percent = config.get(CONF_MAX_ON_PERCENT)
if self._max_on_percent:
_LOGGER.debug(
"We have found max_on_percent setting %s", self._max_on_percent
)
def register_central_boiler(self, central_boiler_entity):
"""Register the central boiler entity. This is used by the CentralBoilerBinarySensor
class to register itself at creation"""
@@ -179,8 +171,7 @@ class VersatileThermostatAPI(dict):
# ):
# await entity.init_presets(self.find_central_configuration())
# A little hack to test if the climate is a VTherm. Cannot use isinstance
# due to circular dependency of BaseThermostat
# A little hack to test if the climate is a VTherm. Cannot use isinstance due to circular dependency of BaseThermostat
if (
entity.device_info
and entity.device_info.get("model", None) == DOMAIN
@@ -250,11 +241,6 @@ class VersatileThermostatAPI(dict):
"""Get the safety_mode params"""
return self._safety_mode
@property
def max_on_percent(self):
"""Get the max_open_percent params"""
return self._max_on_percent
@property
def central_boiler_entity(self):
"""Get the central boiler binary_sensor entity"""

View File

@@ -1401,7 +1401,7 @@ async def test_auto_start_stop_fast_heat_window_mixed(
assert vtherm.hvac_off_reason == HVAC_OFF_REASON_WINDOW_DETECTION
assert vtherm._saved_hvac_mode == HVACMode.HEAT
assert mock_send_event.call_count == 1
assert mock_send_event.call_count == 2
assert vtherm.window_state == STATE_ON

View File

@@ -972,7 +972,7 @@ async def test_manual_hvac_off_should_take_the_lead_over_window(
assert vtherm.hvac_off_reason == HVAC_OFF_REASON_WINDOW_DETECTION
assert vtherm._saved_hvac_mode == HVACMode.HEAT
assert mock_send_event.call_count == 1
assert mock_send_event.call_count == 2
assert vtherm.window_state == STATE_ON

View File

@@ -125,39 +125,6 @@ async def test_tpi_calculation(
assert tpi_algo.on_time_sec == 0
assert tpi_algo.off_time_sec == 300
"""
Test the max_on_percent clamping calculations
"""
tpi_algo._max_on_percent = 0.8
# no clamping
tpi_algo.calculate(15, 14.7, 15, HVACMode.HEAT)
assert tpi_algo.on_percent == 0.09
assert tpi_algo.calculated_on_percent == 0.09
assert tpi_algo.on_time_sec == 0
assert tpi_algo.off_time_sec == 300
# no clamping (calculated_on_percent = 0.79)
tpi_algo.calculate(15, 12.5, 11, HVACMode.HEAT)
assert tpi_algo.on_percent == 0.79
assert tpi_algo.calculated_on_percent == 0.79
assert tpi_algo.on_time_sec == 237
assert tpi_algo.off_time_sec == 63
# clamping to 80% (calculated_on_percent = 1)
tpi_algo.calculate(15, 10, 7, HVACMode.HEAT)
assert tpi_algo.on_percent == 0.8 # should be clamped to 80%
assert tpi_algo.calculated_on_percent == 1 # calculated percentage should not be affected by clamping
assert tpi_algo.on_time_sec == 240 # capped at 80%
assert tpi_algo.off_time_sec == 60
# clamping to 80% (calculated_on_percent = 0.81)
tpi_algo.calculate(15, 12.5, 9, HVACMode.HEAT)
assert tpi_algo.on_percent == 0.80 # should be clamped to 80%
assert tpi_algo.calculated_on_percent == 0.81 # calculated percentage should not be affected by clamping
assert tpi_algo.on_time_sec == 240 # capped at 80%
assert tpi_algo.off_time_sec == 60
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])