From e12cf5a0e333cd581e6df150065603366c9f9fda Mon Sep 17 00:00:00 2001 From: Anssi Hannula Date: Mon, 23 Jun 2025 12:25:35 +0300 Subject: [PATCH 1/4] Update README.md to reflect timer changes We are now relying on a single timer (the HA coordinator timer), so remove the descriptions of PID timer and timer drift. --- README.md | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 6a3b142..a6954ed 100644 --- a/README.md +++ b/README.md @@ -148,29 +148,17 @@ A PID controller continuously corrects the difference between a **setpoint** and 1. **Initialization** - On startup (or when options change), we set up a single `sample_time` value (in seconds). - We register a periodic callback with Home Assistant’s scheduler (`async_track_time_interval` or `DataUpdateCoordinator`) using that same `sample_time`. - - We also configure the PID algorithm’s internal timer to the same `sample_time`. 2. **Coordinator Tick** - Every `sample_time` seconds, Home Assistant’s scheduler invokes our update method. - We immediately read the current process variable (e.g. temperature sensor) and pass it to the PID logic. 3. **PID Logic & Output** - - The PID algorithm checks whether at least `sample_time` seconds have elapsed since its own last computation. - - If so, it calculates the Proportional, Integral, and Derivative terms and writes the result to your target entity (e.g. a heater or set-point). - - If not (because the PID’s internal timer hasn’t quite reached the next tick), it skips the computation until its own timer allows it. - -4. **Timer Drift & Overlap** - - Both the coordinator and the PID controller schedule their next run relative to when the current one started. Under heavy load, one callback may run a few milliseconds later than expected. - - Because each timer is independent, occasional “double-ticks” or small gaps can occur: - - If the scheduler drifts early but the PID timer hasn’t yet reached `sample_time`, no computation runs. - - If the PID timer elapses first and the scheduler callback is slightly late, the update happens immediately when the scheduler finally fires. - - Over time, these small variances average out, preserving an approximately consistent interval. - -5. **Adjusting Sample Time** - - Changing `sample_time` in your integration options takes effect at the end of the current interval—no Home Assistant restart is required. - - On the next tick, both the coordinator and the PID logic will use the new interval. + - The PID algorithm calculates the Proportional, Integral, and Derivative terms and writes the result to your target entity (e.g. a heater or set-point). -By using a single **Sample Time** for both scheduling and calculation—and understanding that each component tracks its own clock—you get predictable, evenly-spaced control updates while allowing Home Assistant’s event loop to manage timing drifts gracefully.``` +4. **Adjusting Sample Time** + - Changing `sample_time` in your integration options takes effect at the end of the current interval—no Home Assistant restart is required. + - On the next tick, the coordinator will use the new interval. --- From 4212e5550fc3f4f5a34edfcefd011033b1928dd0 Mon Sep 17 00:00:00 2001 From: bvweerd Date: Mon, 23 Jun 2025 11:32:30 +0200 Subject: [PATCH 2/4] Update sensor.py --- custom_components/simple_pid_controller/sensor.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/custom_components/simple_pid_controller/sensor.py b/custom_components/simple_pid_controller/sensor.py index 4af2a8f..1aa242c 100644 --- a/custom_components/simple_pid_controller/sensor.py +++ b/custom_components/simple_pid_controller/sensor.py @@ -33,10 +33,9 @@ async def async_setup_entry( ) -> None: """Set up PID output and diagnostic sensors.""" handle: PIDDeviceHandle = entry.runtime_data.handle - handle.init_phase = True # Init PID with default values - handle.pid = PID(1.0, 0.1, 0.05, setpoint=50, sample_time=None) + handle.pid = PID(1.0, 0.1, 0.05, setpoint=50, sample_time=None, auto_mode=False) handle.pid.output_limits = (-10.0, 10.0) handle.last_contributions = (0, 0, 0, 0) @@ -72,10 +71,7 @@ async def update_pid(): handle.pid.output_limits = (None, None) _LOGGER.debug("Start mode = %s (type: %s)", start_mode, type(start_mode)) - if (handle.init_phase and auto_mode) or ( - not handle.pid.auto_mode and auto_mode - ): - handle.init_phase = False + if not handle.pid.auto_mode and auto_mode): if start_mode == "Zero start": handle.pid.set_auto_mode(True, 0) elif start_mode == "Last known value": @@ -86,7 +82,6 @@ async def update_pid(): handle.pid.set_auto_mode(True) else: handle.pid.auto_mode = auto_mode - handle.init_phase = False handle.pid.proportional_on_measurement = p_on_m From 63a84a5798eeb0456221c6f57e6088bcaaaaf494 Mon Sep 17 00:00:00 2001 From: bvweerd Date: Mon, 23 Jun 2025 09:41:38 +0000 Subject: [PATCH 3/4] Fixed tests --- custom_components/simple_pid_controller/sensor.py | 2 +- tests/test_sensor.py | 12 ++++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/custom_components/simple_pid_controller/sensor.py b/custom_components/simple_pid_controller/sensor.py index 1aa242c..5b4d5c6 100644 --- a/custom_components/simple_pid_controller/sensor.py +++ b/custom_components/simple_pid_controller/sensor.py @@ -71,7 +71,7 @@ async def update_pid(): handle.pid.output_limits = (None, None) _LOGGER.debug("Start mode = %s (type: %s)", start_mode, type(start_mode)) - if not handle.pid.auto_mode and auto_mode): + if not handle.pid.auto_mode and auto_mode: if start_mode == "Zero start": handle.pid.set_auto_mode(True, 0) elif start_mode == "Last known value": diff --git a/tests/test_sensor.py b/tests/test_sensor.py index cfe7729..dfd2b11 100644 --- a/tests/test_sensor.py +++ b/tests/test_sensor.py @@ -159,13 +159,15 @@ async def test_update_pid_output_limits_none_when_windup_protection_disabled( # Dummy PID class class DummyPID: - def __init__(self, kp=0, ki=0, kd=0, setpoint=0, sample_time=None): + def __init__( + self, kp=0, ki=0, kd=0, setpoint=0, sample_time=None, auto_mode=False + ): self.Kp = kp self.Ki = ki self.Kd = kd self.setpoint = setpoint self.sample_time = sample_time - self.auto_mode = False + self.auto_mode = auto_mode self.proportional_on_measurement = False self.tunings = (kp, ki, kd) self.output_limits = (123, 456) # dummy init waarde @@ -269,13 +271,15 @@ async def test_update_pid_invalid_start_mode_defaults(monkeypatch, hass, config_ # Dummy PID class matching existing tests class DummyPID: - def __init__(self, kp=0, ki=0, kd=0, setpoint=0, sample_time=None): + def __init__( + self, kp=0, ki=0, kd=0, setpoint=0, sample_time=None, auto_mode=False + ): self.Kp = kp self.Ki = ki self.Kd = kd self.setpoint = setpoint self.sample_time = sample_time - self.auto_mode = False + self.auto_mode = auto_mode self.proportional_on_measurement = False self.tunings = (kp, ki, kd) self.output_limits = (123, 456) From 49928dcb9cfd588caf193ce126953b9de6ef0501 Mon Sep 17 00:00:00 2001 From: bvweerd Date: Mon, 23 Jun 2025 11:45:53 +0200 Subject: [PATCH 4/4] Update version --- .bumpversion.toml | 2 +- custom_components/simple_pid_controller/manifest.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.bumpversion.toml b/.bumpversion.toml index b466598..af5ff6d 100644 --- a/.bumpversion.toml +++ b/.bumpversion.toml @@ -1,5 +1,5 @@ [tool.bumpversion] -current_version = "1.4.0" +current_version = "1.4.1" parse = "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)" serialize = ["{major}.{minor}.{patch}"] search = "{current_version}" diff --git a/custom_components/simple_pid_controller/manifest.json b/custom_components/simple_pid_controller/manifest.json index dfe1cca..ad3db97 100644 --- a/custom_components/simple_pid_controller/manifest.json +++ b/custom_components/simple_pid_controller/manifest.json @@ -11,6 +11,6 @@ "quality_scale": "silver", "requirements": ["simple-pid==2.0.1"], "ssdp": [], - "version": "1.4.0", + "version": "1.4.1", "zeroconf": [] }