Compare commits

...

2 Commits

Author SHA1 Message Date
Jean-Marc Collin
0a658b7a2a Add on_percent into Plotly graph 2024-11-20 10:38:32 +01:00
ms5
289ccc7bb7 Implementing max_on_percent setting (#632)
* implementing max_on_percent setting

* remove % sign from log message

* README updated: created new export-mode section, moved self-regulation expert settings to new section, added new section about on-time clamping
2024-11-17 18:28:24 +01:00
9 changed files with 193 additions and 82 deletions

204
README.md
View File

@@ -389,82 +389,6 @@ 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 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 #### 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. 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: For example:
@@ -800,6 +724,113 @@ context:
> ![Tip](images/tips.png) _*Notes*_ > ![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. > 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> <details>
<summary>Parameter summary</summary> <summary>Parameter summary</summary>
@@ -1248,9 +1279,13 @@ Replace values in [[ ]] by yours.
yaxis: y1 yaxis: y1
name: Ema name: Ema
- entity: '[[climate]]' - entity: '[[climate]]'
attribute: regulated_target_temperature attribute: on_percent
yaxis: y1 yaxis: y2
name: Regulated T° name: Power percent
fill: tozeroy
fillcolor: rgba(200, 10, 10, 0.3)
line:
color: rgba(200, 10, 10, 0.9)
- entity: '[[slope]]' - entity: '[[slope]]'
name: Slope name: Slope
fill: tozeroy fill: tozeroy
@@ -1275,12 +1310,19 @@ Replace values in [[ ]] by yours.
yaxis: yaxis:
visible: true visible: true
position: 0 position: 0
yaxis2:
visible: true
position: 0
fixedrange: true
range:
- 0
- 1
yaxis9: yaxis9:
visible: true visible: true
fixedrange: false fixedrange: false
range: range:
- -0.5 - -2
- 0.5 - 2
position: 1 position: 1
xaxis: xaxis:
rangeselector: rangeselector:

View File

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

View File

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

View File

@@ -133,6 +133,7 @@ CONF_VALVE_4 = "valve_entity4_id"
# Global params into configuration.yaml # Global params into configuration.yaml
CONF_SHORT_EMA_PARAMS = "short_ema_params" CONF_SHORT_EMA_PARAMS = "short_ema_params"
CONF_SAFETY_MODE = "safety_mode" CONF_SAFETY_MODE = "safety_mode"
CONF_MAX_ON_PERCENT = "max_on_percent"
CONF_USE_MAIN_CENTRAL_CONFIG = "use_main_central_config" CONF_USE_MAIN_CENTRAL_CONFIG = "use_main_central_config"
CONF_USE_TPI_CENTRAL_CONFIG = "use_tpi_central_config" CONF_USE_TPI_CENTRAL_CONFIG = "use_tpi_central_config"

View File

@@ -31,6 +31,7 @@ class PropAlgorithm:
cycle_min: int, cycle_min: int,
minimal_activation_delay: int, minimal_activation_delay: int,
vtherm_entity_id: str = None, vtherm_entity_id: str = None,
max_on_percent: float = None,
) -> None: ) -> None:
"""Initialisation of the Proportional Algorithm""" """Initialisation of the Proportional Algorithm"""
_LOGGER.debug( _LOGGER.debug(
@@ -78,6 +79,7 @@ class PropAlgorithm:
self._off_time_sec = self._cycle_min * 60 self._off_time_sec = self._cycle_min * 60
self._security = False self._security = False
self._default_on_percent = 0 self._default_on_percent = 0
self._max_on_percent = max_on_percent
def calculate( def calculate(
self, self,
@@ -161,6 +163,15 @@ class PropAlgorithm:
) )
self._on_percent = self._calculated_on_percent 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 self._on_time_sec = self._on_percent * self._cycle_min * 60
# Do not heat for less than xx sec # Do not heat for less than xx sec

View File

@@ -42,6 +42,7 @@ class ThermostatOverSwitch(BaseThermostat[UnderlyingSwitch]):
"tpi_coef_int", "tpi_coef_int",
"tpi_coef_ext", "tpi_coef_ext",
"power_percent", "power_percent",
"calculated_on_percent",
} }
) )
) )
@@ -84,6 +85,7 @@ class ThermostatOverSwitch(BaseThermostat[UnderlyingSwitch]):
self._cycle_min, self._cycle_min,
self._minimal_activation_delay, self._minimal_activation_delay,
self.name, self.name,
max_on_percent=self._max_on_percent,
) )
lst_switches = config_entry.get(CONF_UNDERLYING_LIST) lst_switches = config_entry.get(CONF_UNDERLYING_LIST)
@@ -149,6 +151,9 @@ class ThermostatOverSwitch(BaseThermostat[UnderlyingSwitch]):
self._attr_extra_state_attributes["function"] = self._proportional_function 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_int"] = self._tpi_coef_int
self._attr_extra_state_attributes["tpi_coef_ext"] = self._tpi_coef_ext 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() self.async_write_ha_state()
_LOGGER.debug( _LOGGER.debug(

View File

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

View File

@@ -15,6 +15,7 @@ from .const import (
CONF_SAFETY_MODE, CONF_SAFETY_MODE,
CONF_THERMOSTAT_TYPE, CONF_THERMOSTAT_TYPE,
CONF_THERMOSTAT_CENTRAL_CONFIG, CONF_THERMOSTAT_CENTRAL_CONFIG,
CONF_MAX_ON_PERCENT,
) )
VTHERM_API_NAME = "vtherm_api" VTHERM_API_NAME = "vtherm_api"
@@ -60,6 +61,7 @@ class VersatileThermostatAPI(dict):
self._central_mode_select = None self._central_mode_select = None
# A dict that will store all Number entities which holds the temperature # A dict that will store all Number entities which holds the temperature
self._number_temperatures = dict() self._number_temperatures = dict()
self._max_on_percent = None
def find_central_configuration(self): def find_central_configuration(self):
"""Search for a central configuration""" """Search for a central configuration"""
@@ -107,6 +109,12 @@ class VersatileThermostatAPI(dict):
if self._safety_mode: if self._safety_mode:
_LOGGER.debug("We have found safet_mode params %s", 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): def register_central_boiler(self, central_boiler_entity):
"""Register the central boiler entity. This is used by the CentralBoilerBinarySensor """Register the central boiler entity. This is used by the CentralBoilerBinarySensor
class to register itself at creation""" class to register itself at creation"""

View File

@@ -125,6 +125,39 @@ async def test_tpi_calculation(
assert tpi_algo.on_time_sec == 0 assert tpi_algo.on_time_sec == 0
assert tpi_algo.off_time_sec == 300 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_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True]) @pytest.mark.parametrize("expected_lingering_timers", [True])