### Trigger
This module triggers whenever the `input` or the `setpoint` changes or the `loopTime` expires.
-Every trigger calculates the P, the I and the D part and sums them up to form the `output` value.
+Every trigger calculates the P-, the I- and the D-part and sums them up to form the `output` value.
This is then transferred to the action module.
| Name | Type | Description | Required |
| `ki` | Decimal | I: [Integral Gain](#integral-i-gain-parameter) Parameter | Y |
| `kd` | Decimal | D: [Derivative Gain](#derivative-d-gain-parameter) Parameter | Y |
| `kdTimeConstant` | Decimal | D-T1: [Derivative Gain Time Constant](#derivative-time-constant-d-t1-parameter) in sec. | Y |
-| `commandItem` | String | Send a String "RESET" to this item to reset the I and the D part to 0. | N |
+| `commandItem` | String | Send a String "RESET" to this item to reset the I- and the D-part to 0. | N |
| `loopTime` | Decimal | The interval the output value will be updated in milliseconds. Note: the output will also be updated when the input value or the setpoint changes. | Y |
-| `pInspector` | Item | Name of the debug Item for the current P part | N |
-| `iInspector` | Item | Name of the debug Item for the current I part | N |
-| `dInspector` | Item | Name of the debug Item for the current D part | N |
+| `integralMinValue` | Decimal | The I-part will be limited (min) to this value. | N |
+| `integralMaxValue` | Decimal | The I-part will be limited (max) to this value. | N |
+| `pInspector` | Item | Name of the debug Item for the current P-part | N |
+| `iInspector` | Item | Name of the debug Item for the current I-part | N |
+| `dInspector` | Item | Name of the debug Item for the current D-part | N |
| `eInspector` | Item | Name of the debug Item for the current regulation difference (error) | N |
The `loopTime` should be max a tenth of the system response.
E.g. the heating needs 10 min to heat up the room, the loop time should be max 1 min.
Lower values won't harm, but need more calculation resources.
-You can view the internal P, I and D parts of the controller with the inspector Items.
+The I-part can be limited via `integralMinValue`/`integralMaxValue`.
+This is useful if the regulation cannot meet its setpoint from time to time.
+E.g. a heating controller in the summer, which can not cool (min limit) or when the heating valve is already at 100% and the room is only slowly heating up (max limit).
+When controlling a heating valve, reasonable values are 0% (min limit) and 100% (max limit).
+
+You can view the internal P-, I- and D-parts of the controller with the inspector Items.
These values are useful when tuning the controller.
They are updated every time the output is updated.
Parameter: `kp`
-A value of 0 disables the P part.
+A value of 0 disables the P-part.
A value of 1 sets the output to the current setpoint deviation (error).
E.g. the setpoint is 25°C and the measured value is 20°C, the output will be set to 5.
The purpose of this parameter is to let the output drift towards the setpoint.
The bigger this parameter, the faster the drifting.
-A value of 0 disables the I part.
+A value of 0 disables the I-part.
A value of 1 adds the current setpoint deviation (error) to the output each `loopTime` (in milliseconds).
E.g. (`loopTimeMs=1000`) the setpoint is 25°C and the measured value is 20°C, the output will be set to 5 after 1 sec.
The purpose of this parameter is to react to sudden changes (e.g. an opened window) and also to damp the regulation.
This makes the regulation more resilient against oscillations, i.e. bigger `kp` and `ki` values can be set.
-A value of 0 disables the D part.
+A value of 0 disables the D-part.
A value of 1 sets the output to the difference between the last setpoint deviation (error) and the current.
E.g. the setpoint is 25°C and the measured value is 20°C (error=5°C).
Parameter: `kdTimeConstant`
-The purpose of this parameter is to slow down the impact of the D part.
+The purpose of this parameter is to slow down the impact of the D-part.
This parameter behaves like a [low-pass](https://en.wikipedia.org/wiki/Low-pass_filter) filter.
-The D part will become 63% of its actual value after `kdTimeConstant` seconds and 99% after 5 times `kdTimeConstant`. E.g. `kdTimeConstant` is set to 10s, the D part will become 99% after 50s.
+The D-part will become 63% of its actual value after `kdTimeConstant` seconds and 99% after 5 times `kdTimeConstant`. E.g. `kdTimeConstant` is set to 10s, the D-part will become 99% after 50s.
-Higher values lead to a longer lasting impact of the D part (stretching) after a change in the setpoint deviation (error).
-The "stretching" also results in a lower amplitude, i.e. if you increase this value, you might want to also increase `kd` to keep the height of the D part at the same level.
+Higher values lead to a longer lasting impact of the D-part (stretching) after a change in the setpoint deviation (error).
+The "stretching" also results in a lower amplitude, i.e. if you increase this value, you might want to also increase `kd` to keep the height of the D-part at the same level.
## Tuning
So, this will be described in the following.
To be able to proceed with this method, you need to visualize the input and the output value of the PID controller over time.
-It's also good to visualize the individual P, I and D parts (these are forming the output value) via the inspector items.
+It's also good to visualize the individual P-, I- and D-parts (these are forming the output value) via the inspector items.
The visualization could be done by adding a persistence and use Grafana for example.
After you added a [Rule](https://www.openhab.org/docs/configuration/rules-dsl.html) with above trigger and action module and configured those, proceed with the following steps:
3. Decrease `kp` a bit, that the system doesn't oscillate anymore
4. Repeat the two steps for the `ki` parameter (keep `kp` set)
5. Repeat the two steps for the `kd` parameter (keep `kp` and `ki` set)
-6. As the D part acts as a damper, you should now be able to increase `kp` and `ki` further without resulting in oscillations
+6. As the D-part acts as a damper, you should now be able to increase `kp` and `ki` further without resulting in oscillations
After each modification of above parameters, test the system response by introducing a setpoint deviation (error).
This can be done either by changing the setpoint (e.g. 20°C -> 25°C) or by forcing the measured value to change (e.g. by opening a window).
private double ki;
private double kd;
private double derivativeTimeConstantSec;
+ private double iMinResult;
+ private double iMaxResult;
- public PIDController(double kpAdjuster, double kiAdjuster, double kdAdjuster, double derivativeTimeConstantSec) {
+ public PIDController(double kpAdjuster, double kiAdjuster, double kdAdjuster, double derivativeTimeConstantSec,
+ double iMinValue, double iMaxValue) {
this.kp = kpAdjuster;
this.ki = kiAdjuster;
this.kd = kdAdjuster;
this.derivativeTimeConstantSec = derivativeTimeConstantSec;
+ this.iMinResult = Double.NaN;
+ this.iMaxResult = Double.NaN;
+
+ // prepare min/max for the integral result accumulator
+ if (Double.isFinite(kiAdjuster) && Math.abs(kiAdjuster) > 0.0) {
+ if (Double.isFinite(iMinValue)) {
+ this.iMinResult = iMinValue / kiAdjuster;
+ }
+ if (Double.isFinite(iMaxValue)) {
+ this.iMaxResult = iMaxValue / kiAdjuster;
+ }
+ }
}
public PIDOutputDTO calculate(double input, double setpoint, long lastInvocationMs, int loopTimeMs) {
// integral calculation
integralResult += error * lastInvocationMs / loopTimeMs;
+ if (Double.isFinite(iMinResult)) {
+ integralResult = Math.max(integralResult, iMinResult);
+ }
+ if (Double.isFinite(iMaxResult)) {
+ integralResult = Math.min(integralResult, iMaxResult);
+ }
// calculate parts
final double proportionalPart = kp * error;
- final double integralPart = ki * integralResult;
+
+ double integralPart = ki * integralResult;
+
final double derivativePart = kd * derivativeResult;
+
output = proportionalPart + integralPart + derivativePart;
return new PIDOutputDTO(output, proportionalPart, integralPart, derivativePart, error);
import java.math.BigDecimal;
import java.util.Map;
-import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.TimeUnit;
double kiAdjuster = getDoubleFromConfig(config, CONFIG_KI_GAIN);
double kdAdjuster = getDoubleFromConfig(config, CONFIG_KD_GAIN);
double kdTimeConstant = getDoubleFromConfig(config, CONFIG_KD_TIMECONSTANT);
+ double iMinValue = getDoubleFromConfig(config, CONFIG_I_MIN);
+ double iMaxValue = getDoubleFromConfig(config, CONFIG_I_MAX);
pInspector = (String) config.get(P_INSPECTOR);
iInspector = (String) config.get(I_INSPECTOR);
dInspector = (String) config.get(D_INSPECTOR);
loopTimeMs = ((BigDecimal) requireNonNull(config.get(CONFIG_LOOP_TIME), CONFIG_LOOP_TIME + " is not set"))
.intValue();
- controller = new PIDController(kpAdjuster, kiAdjuster, kdAdjuster, kdTimeConstant);
+ controller = new PIDController(kpAdjuster, kiAdjuster, kdAdjuster, kdTimeConstant, iMinValue, iMaxValue);
eventFilter = event -> {
String topic = event.getTopic();
}
private double getDoubleFromConfig(Configuration config, String key) {
- return ((BigDecimal) Objects.requireNonNull(config.get(key), key + " is not set")).doubleValue();
+ Object rawValue = config.get(key);
+
+ if (rawValue == null) {
+ return Double.NaN;
+ }
+
+ return ((BigDecimal) rawValue).doubleValue();
}
private void calculate() {
if (itemName != null) {
try {
itemRegistry.getItem(itemName);
- eventPublisher.post(ItemEventFactory.createCommandEvent(itemName, new DecimalType(value)));
+ eventPublisher.post(ItemEventFactory.createStateEvent(itemName,
+ Double.isFinite(value) ? new DecimalType(value) : UnDefType.UNDEF));
} catch (ItemNotFoundException e) {
logger.warn("Item doesn't exist: {}", itemName);
}