diff --git a/custom_components/sat/heating_curve.py b/custom_components/sat/heating_curve.py index ec9ca74b..4a927923 100644 --- a/custom_components/sat/heating_curve.py +++ b/custom_components/sat/heating_curve.py @@ -28,9 +28,12 @@ def update(self, target_temperature: float, outside_temperature: float) -> None: heating_curve_value = self.calculate(target_temperature, outside_temperature) self._last_heating_curve_value = round(self.base_offset + ((self._coefficient / 4) * heating_curve_value), 1) - def calculate_coefficient(self, setpoint: float, target_temperature: float, outside_temperature: float) -> float: - """Convert a setpoint to a coefficient value""" + def calculate_coefficient(self, setpoint: float, target_temperature: float, outside_temperature: float) -> float | None: + """Convert a setpoint to a coefficient, if the curve is nonzero.""" heating_curve_value = self.calculate(target_temperature, outside_temperature) + if heating_curve_value == 0: + return None + return round(4 * (setpoint - self.base_offset) / heating_curve_value, 1) def autotune(self, setpoint: float, target_temperature: float, outside_temperature: float): @@ -39,6 +42,9 @@ def autotune(self, setpoint: float, target_temperature: float, outside_temperatu return coefficient = self.calculate_coefficient(setpoint, target_temperature, outside_temperature) + if coefficient is None: + return + self._coefficient_derivative = round(coefficient - self._optimal_coefficient, 1) if self._optimal_coefficient else coefficient # Fuzzy logic for when the derivative is positive diff --git a/tests/test_heating_curve.py b/tests/test_heating_curve.py new file mode 100644 index 00000000..624ad9fd --- /dev/null +++ b/tests/test_heating_curve.py @@ -0,0 +1,37 @@ +"""Autotuning cannot infer a coefficient from a zero-valued curve.""" +import pytest + +from custom_components.sat.const import HEATING_SYSTEM_UNDERFLOOR, HEATING_SYSTEM_RADIATORS +from custom_components.sat.heating_curve import HeatingCurve + + +@pytest.mark.parametrize("heating_system", [HEATING_SYSTEM_UNDERFLOOR, HEATING_SYSTEM_RADIATORS]) +@pytest.mark.parametrize("restored", [False, True]) +def test_zero_curve_autotune_preserves_history(heating_system, restored): + curve = HeatingCurve(heating_system, 2.0) + control = HeatingCurve(heating_system, 2.0) + if restored: + curve.restore_autotune(2.0, 0.4) + control.restore_autotune(2.0, 0.4) + before = (curve.optimal_coefficient, curve.coefficient_derivative) + + assert curve.calculate(20, 20) == 0 + curve.autotune(35, 20, 20) + assert (curve.optimal_coefficient, curve.coefficient_derivative) == before + + curve.autotune(35, 21, 10) + control.autotune(35, 21, 10) + assert curve.optimal_coefficient == control.optimal_coefficient + assert curve.coefficient_derivative == control.coefficient_derivative + + +@pytest.mark.parametrize("heating_system", [HEATING_SYSTEM_UNDERFLOOR, HEATING_SYSTEM_RADIATORS]) +def test_coefficient_is_undefined_for_zero_curve(heating_system): + curve = HeatingCurve(heating_system, 2.0) + assert curve.calculate_coefficient(35, 20, 20) is None + + +@pytest.mark.parametrize("heating_system, expected", [(HEATING_SYSTEM_UNDERFLOOR, 5.5), (HEATING_SYSTEM_RADIATORS, 2.8)]) +def test_nonzero_curve_coefficient_is_unchanged(heating_system, expected): + curve = HeatingCurve(heating_system, 2.0) + assert curve.calculate_coefficient(35, 21, 10) == expected