]> git.basschouten.com Git - openhab-addons.git/commitdiff
[pidcontroller] Add ability to limit the I-part (#12565)
authorFabian Wolter <github@fabian-wolter.de>
Sun, 3 Apr 2022 15:38:56 +0000 (17:38 +0200)
committerGitHub <noreply@github.com>
Sun, 3 Apr 2022 15:38:56 +0000 (17:38 +0200)
* [pidcontroller] Add ability to limit the I-part

* Apply iMinValue & iMaxValue to the integral result accumulator

Signed-off-by: Fabian Wolter <github@fabian-wolter.de>
* Set iMinResult, iMaxResult default value to NaN

Co-authored-by: Lenno Nagel <lenno@nagel.ee>
bundles/org.openhab.automation.pidcontroller/README.md
bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/PIDControllerConstants.java
bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/handler/PIDController.java
bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/handler/PIDControllerTriggerHandler.java
bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/type/PIDControllerTriggerType.java

index d0d19702ca30adcb6f60f045d8abb191e8d66735..35ae91eaab08afeabc5f4014950bb3bee9f50e22 100644 (file)
@@ -24,7 +24,7 @@ Select the Item you like to control in the "Item Action" and leave the command e
 ### 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 |
@@ -35,18 +35,25 @@ This is then transferred to the action module.
 | `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.
 
@@ -54,7 +61,7 @@ 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.
@@ -67,7 +74,7 @@ Parameter: `ki`
 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.
@@ -81,7 +88,7 @@ Parameter: `kd`
 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).
@@ -91,13 +98,13 @@ When the temperature drops to 10°C due to an opened window (error=15°C), the o
 
 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
 
@@ -108,7 +115,7 @@ This results in quite reasonable working systems in most cases.
 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:
@@ -121,7 +128,7 @@ E.g. the time it takes from opening the heater valve and seeing an effect of the
 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).
index 0b9b74b1be9b6977c3733b6b30820795d8227609..47c75ffd85300b89eaa4ab4bb24d5522776631e5 100644 (file)
@@ -32,6 +32,8 @@ public class PIDControllerConstants {
     public static final String CONFIG_KI_GAIN = "ki";
     public static final String CONFIG_KD_GAIN = "kd";
     public static final String CONFIG_KD_TIMECONSTANT = "kdTimeConstant";
+    public static final String CONFIG_I_MAX = "integralMaxValue";
+    public static final String CONFIG_I_MIN = "integralMinValue";
     public static final String P_INSPECTOR = "pInspector";
     public static final String I_INSPECTOR = "iInspector";
     public static final String D_INSPECTOR = "dInspector";
index 8d73c758f43e95fb45ce4e91919b5f7371ff5ade..a93f89af4152e933b0f47671a71c6177c498d5d6 100644 (file)
@@ -34,12 +34,27 @@ class PIDController {
     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) {
@@ -55,11 +70,20 @@ class PIDController {
 
         // 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);
index e85ed80f445d7776bdba06d26c1c8adda2e71a99..cfd9f3237c68989fde34a4ab774675f643628c0f 100644 (file)
@@ -16,7 +16,6 @@ import static org.openhab.automation.pidcontroller.internal.PIDControllerConstan
 
 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;
@@ -108,6 +107,8 @@ public class PIDControllerTriggerHandler extends BaseTriggerModuleHandler implem
         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);
@@ -116,7 +117,7 @@ public class PIDControllerTriggerHandler extends BaseTriggerModuleHandler implem
         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();
@@ -146,7 +147,13 @@ public class PIDControllerTriggerHandler extends BaseTriggerModuleHandler implem
     }
 
     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() {
@@ -184,7 +191,8 @@ public class PIDControllerTriggerHandler extends BaseTriggerModuleHandler implem
         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);
             }
index 28961183429c5f50a907d1b8f24e21c8ead326be..528998a10dd0272cebae0d14af2c5179c61f7108 100644 (file)
@@ -83,7 +83,7 @@ public class PIDControllerTriggerType extends TriggerType {
                 .withMinimum(BigDecimal.ZERO) //
                 .withDefault("1.0") //
                 .withLabel("Derivative Time Constant") //
-                .withDescription("Slows the rate of change of the D part (T1) in seconds.") //
+                .withDescription("Slows the rate of change of the D-part (T1) in seconds.") //
                 .withUnit("s") //
                 .build());
         configDescriptions.add(ConfigDescriptionParameterBuilder.create(CONFIG_LOOP_TIME, Type.DECIMAL) //
@@ -94,6 +94,18 @@ public class PIDControllerTriggerType extends TriggerType {
                 .withDescription("The interval the output value is updated in ms") //
                 .withUnit("ms") //
                 .build());
+        configDescriptions.add(ConfigDescriptionParameterBuilder.create(CONFIG_I_MIN, Type.DECIMAL) //
+                .withRequired(false) //
+                .withMultiple(false) //
+                .withLabel("I-part Lower Limit") //
+                .withDescription("The I-part will be min this value. Can be left empty for no limit.") //
+                .build());
+        configDescriptions.add(ConfigDescriptionParameterBuilder.create(CONFIG_I_MAX, Type.DECIMAL) //
+                .withRequired(false) //
+                .withMultiple(false) //
+                .withLabel("I-part Upper Limit") //
+                .withDescription("The I-part will be max this value. Can be left empty for no limit.") //
+                .build());
         configDescriptions.add(ConfigDescriptionParameterBuilder.create(P_INSPECTOR, Type.TEXT) //
                 .withRequired(false) //
                 .withMultiple(false) //