Skip to content

Commit 0dee1d3

Browse files
committed
Make sure the integral update every minute
1 parent aa9491b commit 0dee1d3

File tree

4 files changed

+20
-45
lines changed

4 files changed

+20
-45
lines changed

custom_components/sat/area.py

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from .entry_data import SatConfig
1919
from .heating_curve import HeatingCurve
2020
from .helpers import float_value, is_state_stale, state_age_seconds
21-
from .pid import PID
21+
from .pid import PID, PID_UPDATE_INTERVAL
2222
from .temperature.state import TemperatureStates, TemperatureState
2323

2424
_LOGGER = logging.getLogger(__name__)
@@ -208,15 +208,12 @@ async def async_added_to_hass(self, hass: HomeAssistant, device_id) -> None:
208208
await self.pid.async_added_to_hass(hass, self._entity_id, device_id)
209209

210210
if hass.state is CoreState.running:
211-
self.update()
211+
self.control_pid()
212212
else:
213-
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, self.update)
213+
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, self.control_pid)
214214

215215
self._time_interval = async_track_time_interval(
216-
self._hass,
217-
self.update,
218-
timedelta(seconds=30),
219-
cancel_on_shutdown=True,
216+
self._hass, self.control_pid, timedelta(seconds=PID_UPDATE_INTERVAL), cancel_on_shutdown=True
220217
)
221218

222219
async def async_will_remove_from_hass(self) -> None:
@@ -229,7 +226,7 @@ async def async_outside_entity_changed(self, _event: Event[EventStateChangedData
229226
"""Handle changes to the outside entity."""
230227
self.heating_curve.update(self.target_temperature, self.current_temperature)
231228

232-
def update(self, _time: Optional[datetime] = None) -> None:
229+
def control_pid(self, _time: Optional[datetime] = None) -> None:
233230
"""Update the PID controller with the current error and heating curve."""
234231
if (error := self.error) is None:
235232
_LOGGER.debug("Skipping control loop for %s because error could not be computed", self._entity_id)

custom_components/sat/climate.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
from .heating_control import HeatingDemand, SatHeatingControl
3737
from .heating_curve import HeatingCurve
3838
from .helpers import is_state_stale, state_age_seconds, clamp, ensure_list, event_timestamp
39-
from .pid import PID
39+
from .pid import PID, PID_UPDATE_INTERVAL
4040
from .summer_simmer import SummerSimmer
4141
from .temperature.history import TemperatureHistory
4242
from .temperature.history import TemperatureStatistics
@@ -629,7 +629,7 @@ def _register_event_listeners(self) -> None:
629629

630630
self.async_on_remove(
631631
async_track_time_interval(
632-
self.hass, self.control_pid, timedelta(seconds=30)
632+
self.hass, self.control_pid, timedelta(seconds=PID_UPDATE_INTERVAL), cancel_on_shutdown=True
633633
)
634634
)
635635

custom_components/sat/pid.py

Lines changed: 5 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -10,23 +10,23 @@
1010
from .const import *
1111
from .entry_data import PidConfig, SatConfig
1212
from .heating_curve import HeatingCurve
13-
from .helpers import float_value, timestamp as _timestamp, clamp_to_range
13+
from .helpers import float_value, clamp_to_range
1414
from .temperature.state import TemperatureState
1515
from .types import HeatingSystem
1616

1717
_LOGGER = logging.getLogger(__name__)
18-
timestamp = _timestamp # keep public name for tests
1918

2019
DERIVATIVE_ALPHA1 = 0.8
2120
DERIVATIVE_ALPHA2 = 0.6
2221
DERIVATIVE_RAW_CAP = 5.0
2322

23+
PID_UPDATE_INTERVAL = 60
24+
2425
STORAGE_VERSION = 1
2526
STORAGE_KEY_INTEGRAL = "integral"
2627
STORAGE_KEY_LAST_ERROR = "last_error"
2728
STORAGE_KEY_RAW_DERIVATIVE = "raw_derivative"
2829
STORAGE_KEY_LAST_TEMPERATURE = "last_temperature"
29-
STORAGE_KEY_LAST_INTEGRAL_UPDATED = "last_integral_updated"
3030
STORAGE_KEY_LAST_DERIVATIVE_UPDATED = "last_derivative_updated"
3131

3232

@@ -40,7 +40,6 @@ def __init__(self, heating_system: HeatingSystem, heating_curve: HeatingCurve, c
4040

4141
self._integral: float = 0.0
4242
self._last_error: Optional[float] = None
43-
self._last_integral_updated: Optional[float] = None
4443

4544
self._raw_derivative: float = 0.0
4645
self._last_temperature: Optional[float] = None
@@ -139,7 +138,6 @@ def reset(self) -> None:
139138
"""Reset the PID controller to a clean state."""
140139
self._integral = 0.0
141140
self._last_error = None
142-
self._last_integral_updated = None
143141

144142
async def async_added_to_hass(self, hass: HomeAssistant, entity_id: str, device_id: str) -> None:
145143
"""Restore PID controller state from storage when the integration loads."""
@@ -155,7 +153,6 @@ async def async_added_to_hass(self, hass: HomeAssistant, entity_id: str, device_
155153
self._last_derivative_updated = float_value(data.get(STORAGE_KEY_LAST_DERIVATIVE_UPDATED))
156154

157155
self._integral = float(data.get(STORAGE_KEY_INTEGRAL, self._integral))
158-
self._last_integral_updated = float_value(data.get(STORAGE_KEY_LAST_INTEGRAL_UPDATED))
159156
self._raw_derivative = float(data.get(STORAGE_KEY_RAW_DERIVATIVE, self._raw_derivative))
160157

161158
_LOGGER.debug("Loaded PID state from storage for entity=%s", self._entity_id)
@@ -188,35 +185,13 @@ def _update_integral(self, state: TemperatureState) -> None:
188185
"""Update the integral value in the PID controller."""
189186
if abs(state.error) > DEADBAND:
190187
self._integral = 0.0
191-
self._last_integral_updated = None
192-
return
193-
194-
if self._last_integral_updated is None:
195-
self._last_integral_updated = state.last_changed.timestamp()
196-
return
197-
198-
delta_time = state.last_changed.timestamp() - self._last_integral_updated
199-
200-
# Ignore non-forward timestamps.
201-
if delta_time <= 0:
202-
self._last_integral_updated = state.last_changed.timestamp()
203188
return
204189

205-
# Skip integration when integral gain is disabled.
206-
if self.ki is None:
207-
return
208-
209-
self._integral += self.ki * state.error * delta_time
190+
self._integral += self.ki * state.error * PID_UPDATE_INTERVAL
210191
self._integral = clamp_to_range(self._integral, self._heating_curve.value)
211192

212-
# Record the timestamp used for this integration step.
213-
self._last_integral_updated = state.last_changed.timestamp()
214-
215193
def _update_derivative(self, state: TemperatureState) -> None:
216194
"""Update the derivative term of the PID controller based on temperature slope."""
217-
if self.kd is None:
218-
return
219-
220195
if self._last_temperature is None or self._last_derivative_updated is None:
221196
self._last_temperature = state.current
222197
self._last_derivative_updated = state.last_changed.timestamp()
@@ -228,6 +203,7 @@ def _update_derivative(self, state: TemperatureState) -> None:
228203
return
229204

230205
temperature_delta = state.current - self._last_temperature
206+
231207
if temperature_delta == 0.0:
232208
self._last_temperature = state.current
233209
self._last_derivative_updated = state.last_changed.timestamp()
@@ -265,7 +241,6 @@ async def _async_save_state(self) -> None:
265241
STORAGE_KEY_LAST_ERROR: self._last_error,
266242
STORAGE_KEY_RAW_DERIVATIVE: self._raw_derivative,
267243
STORAGE_KEY_LAST_TEMPERATURE: self._last_temperature,
268-
STORAGE_KEY_LAST_INTEGRAL_UPDATED: self._last_integral_updated,
269244
STORAGE_KEY_LAST_DERIVATIVE_UPDATED: self._last_derivative_updated,
270245
}
271246

tests/test_pid.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
DERIVATIVE_ALPHA1,
1212
DERIVATIVE_ALPHA2,
1313
DERIVATIVE_RAW_CAP,
14-
PID,
14+
PID, PID_UPDATE_INTERVAL,
1515
)
1616
from custom_components.sat.temperature.state import TemperatureState
1717
from custom_components.sat.types import HeatingSystem
@@ -98,9 +98,9 @@ def test_manual_gains_output_and_availability():
9898
assert pid.ki == 1.0
9999
assert pid.kd == 0.5
100100
assert pid.proportional == 0.1
101-
assert pid.integral == 0.5
101+
assert pid.integral == 6.0
102102
assert pid.derivative == 0.0
103-
assert pid.output == 30.6
103+
assert pid.output == 36.1
104104

105105

106106
def test_automatic_gains_calculation():
@@ -136,10 +136,13 @@ def test_integral_timebase_reset_and_accumulation():
136136
assert pid.integral == 0.0
137137

138138
pid.update(_state_for_error(DEADBAND / 2, 20.0))
139-
assert pid.integral == 0.0
139+
assert pid.integral == 3.0
140140

141141
pid.update(_state_for_error(DEADBAND / 2, 30.0))
142-
assert pid.integral == 0.5
142+
assert pid.integral == 6.0
143+
144+
pid.update(_state_for_error(DEADBAND / 2, 20.0 + PID_UPDATE_INTERVAL))
145+
assert pid.integral == 9.0
143146

144147

145148
def test_integral_clamped_to_heating_curve():

0 commit comments

Comments
 (0)