Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions custom_components/sat/heating_curve.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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
Expand Down
37 changes: 37 additions & 0 deletions tests/test_heating_curve.py
Original file line number Diff line number Diff line change
@@ -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