Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .bumpversion.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[tool.bumpversion]
current_version = "1.4.0"
current_version = "1.4.1"
parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
serialize = ["{major}.{minor}.{patch}"]
search = "{current_version}"
Expand Down
20 changes: 4 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

---

Expand Down
2 changes: 1 addition & 1 deletion custom_components/simple_pid_controller/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@
"quality_scale": "silver",
"requirements": ["simple-pid==2.0.1"],
"ssdp": [],
"version": "1.4.0",
"version": "1.4.1",
"zeroconf": []
}
9 changes: 2 additions & 7 deletions custom_components/simple_pid_controller/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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":
Expand All @@ -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

Expand Down
12 changes: 8 additions & 4 deletions tests/test_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down