From 8c651623944c9546c31a7134a7fed705244f1605 Mon Sep 17 00:00:00 2001 From: Fabian Wolter Date: Tue, 12 Jan 2021 17:16:29 +0100 Subject: [PATCH] [pidcontroller] Remove limits, make Ki dependent from the loop time, add reset command (#9759) * [pidcontroller] Remove limits, make Ki dependent from the loop time Also fix naming of thread and shutdown executor. Signed-off-by: Fabian Wolter Signed-off-by: John Marshall --- .../README.md | 29 +++++++-------- .../internal/PIDControllerConstants.java | 3 +- .../internal/handler/PIDController.java | 29 ++++++--------- .../handler/PIDControllerTriggerHandler.java | 35 ++++++++++++++----- .../type/PIDControllerTriggerType.java | 21 ++++++----- 5 files changed, 62 insertions(+), 55 deletions(-) diff --git a/bundles/org.openhab.automation.pidcontroller/README.md b/bundles/org.openhab.automation.pidcontroller/README.md index 4822a3e98112a..1630ef814ee17 100644 --- a/bundles/org.openhab.automation.pidcontroller/README.md +++ b/bundles/org.openhab.automation.pidcontroller/README.md @@ -22,20 +22,17 @@ This module triggers whenever the `input` or the `setpoint` changes or the `loop 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 | -|--------------------|---------|----------------------------------------------------------------------------------------------------------------------------------------------------|----------| -| `input` | Item | Name of the input [Item](https://www.openhab.org/docs/configuration/items.html) (e.g. temperature sensor value) | Y | -| `setpoint` | Item | Name of the setpoint Item (e.g. desired room temperature) | Y | -| `kp` | Decimal | P: [Proportional Gain](#proportional-p-gain-parameter) Parameter | Y | -| `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 | -| `outputLowerLimit` | Decimal | The output of the PID controller will be max this value | Y | -| `outputUpperLimit` | Decimal | The output of the PID controller will be min this value | Y | -| `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 | - -The purpose of the limit parameters are to keep the output value and the integral value in a reasonable range, if the regulation cannot meet its setpoint. -E.g. the window is open and the heater doesn't manage to heat up the room. +| Name | Type | Description | Required | +|------------------|---------|----------------------------------------------------------------------------------------------------------------------------------------------------|----------| +| `input` | Item | Name of the input [Item](https://www.openhab.org/docs/configuration/items.html) (e.g. temperature sensor value) | Y | +| `setpoint` | Item | Name of the setpoint Item (e.g. desired room temperature) | Y | +| `kp` | Decimal | P: [Proportional Gain](#proportional-p-gain-parameter) Parameter | Y | +| `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 | +| `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 | + 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. @@ -76,8 +73,8 @@ The bigger this parameter, the faster the drifting. A value of 0 disables the I part. -A value of 1 adds the current setpoint deviation (error) to the output each second. -E.g. the setpoint is 25°C and the measured value is 20°C, the output will be set to 5 after 1 sec. +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. After 2 sec the output will be 10. If the output is the opening of a valve in %, you might want to set this parameter to a lower value (`ki=0.1` would result in 30% after 60 sec: 5\*0.1\*60=30). diff --git a/bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/PIDControllerConstants.java b/bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/PIDControllerConstants.java index 7522bc37a716a..a3f21fac4b856 100644 --- a/bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/PIDControllerConstants.java +++ b/bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/PIDControllerConstants.java @@ -26,8 +26,7 @@ public class PIDControllerConstants { public static final String AUTOMATION_NAME = "pidcontroller"; public static final String CONFIG_INPUT_ITEM = "input"; public static final String CONFIG_SETPOINT_ITEM = "setpoint"; - public static final String CONFIG_OUTPUT_LOWER_LIMIT = "outputLowerLimit"; - public static final String CONFIG_OUTPUT_UPPER_LIMIT = "outputUpperLimit"; + public static final String CONFIG_COMMAND_ITEM = "commandItem"; public static final String CONFIG_LOOP_TIME = "loopTime"; public static final String CONFIG_KP_GAIN = "kp"; public static final String CONFIG_KI_GAIN = "ki"; diff --git a/bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/handler/PIDController.java b/bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/handler/PIDController.java index 36e5d84b37de6..8ba4060bf4929 100644 --- a/bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/handler/PIDController.java +++ b/bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/handler/PIDController.java @@ -25,9 +25,6 @@ */ @NonNullByDefault class PIDController { - private final double outputLowerLimit; - private final double outputUpperLimit; - private double integralResult; private double derivativeResult; private double previousError; @@ -38,17 +35,14 @@ class PIDController { private double kd; private double derivativeTimeConstantSec; - public PIDController(double outputLowerLimit, double outputUpperLimit, double kpAdjuster, double kiAdjuster, - double kdAdjuster, double derivativeTimeConstantSec) { - this.outputLowerLimit = outputLowerLimit; - this.outputUpperLimit = outputUpperLimit; + public PIDController(double kpAdjuster, double kiAdjuster, double kdAdjuster, double derivativeTimeConstantSec) { this.kp = kpAdjuster; this.ki = kiAdjuster; this.kd = kdAdjuster; this.derivativeTimeConstantSec = derivativeTimeConstantSec; } - public PIDOutputDTO calculate(double input, double setpoint, long lastInvocationMs) { + public PIDOutputDTO calculate(double input, double setpoint, long lastInvocationMs, int loopTimeMs) { final double lastInvocationSec = lastInvocationMs / 1000d; final double error = setpoint - input; @@ -60,13 +54,7 @@ public PIDOutputDTO calculate(double input, double setpoint, long lastInvocation } // integral calculation - integralResult += error * lastInvocationSec; - // limit to output limits - if (ki != 0) { - final double maxIntegral = outputUpperLimit / ki; - final double minIntegral = outputLowerLimit / ki; - integralResult = Math.min(maxIntegral, Math.max(minIntegral, integralResult)); - } + integralResult += error * lastInvocationMs / loopTimeMs; // calculate parts final double proportionalPart = kp * error; @@ -74,9 +62,14 @@ public PIDOutputDTO calculate(double input, double setpoint, long lastInvocation final double derivativePart = kd * derivativeResult; output = proportionalPart + integralPart + derivativePart; - // limit output value - output = Math.min(outputUpperLimit, Math.max(outputLowerLimit, output)); - return new PIDOutputDTO(output, proportionalPart, integralPart, derivativePart, error); } + + public void setIntegralResult(double integralResult) { + this.integralResult = integralResult; + } + + public void setDerivativeResult(double derivativeResult) { + this.derivativeResult = derivativeResult; + } } diff --git a/bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/handler/PIDControllerTriggerHandler.java b/bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/handler/PIDControllerTriggerHandler.java index 6806928011dc7..c3280c1229c77 100644 --- a/bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/handler/PIDControllerTriggerHandler.java +++ b/bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/handler/PIDControllerTriggerHandler.java @@ -18,6 +18,7 @@ import java.util.HashMap; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; @@ -62,7 +63,7 @@ public class PIDControllerTriggerHandler extends BaseTriggerModuleHandler implem private static final Set SUBSCRIBED_EVENT_TYPES = Set.of(ItemStateEvent.TYPE, ItemStateChangedEvent.TYPE); private final Logger logger = LoggerFactory.getLogger(PIDControllerTriggerHandler.class); private final ScheduledExecutorService scheduler = Executors - .newSingleThreadScheduledExecutor(new NamedThreadFactory("OH-automation-" + AUTOMATION_NAME, true)); + .newSingleThreadScheduledExecutor(new NamedThreadFactory("automation-" + AUTOMATION_NAME, true)); private final ServiceRegistration eventSubscriberRegistration; private final PIDController controller; private final int loopTimeMs; @@ -70,6 +71,7 @@ public class PIDControllerTriggerHandler extends BaseTriggerModuleHandler implem private long previousTimeMs = System.currentTimeMillis(); private Item inputItem; private Item setpointItem; + private Optional commandTopic; private EventFilter eventFilter; public PIDControllerTriggerHandler(Trigger module, ItemRegistry itemRegistry, EventPublisher eventPublisher, @@ -93,8 +95,13 @@ public PIDControllerTriggerHandler(Trigger module, ItemRegistry itemRegistry, Ev throw new IllegalArgumentException("Configured setpoint item not found: " + setpointItemName, e); } - double outputLowerLimit = getDoubleFromConfig(config, CONFIG_OUTPUT_LOWER_LIMIT); - double outputUpperLimit = getDoubleFromConfig(config, CONFIG_OUTPUT_UPPER_LIMIT); + String commandItemName = (String) config.get(CONFIG_COMMAND_ITEM); + if (commandItemName != null) { + commandTopic = Optional.of("openhab/items/" + commandItemName + "/statechanged"); + } else { + commandTopic = Optional.empty(); + } + double kpAdjuster = getDoubleFromConfig(config, CONFIG_KP_GAIN); double kiAdjuster = getDoubleFromConfig(config, CONFIG_KI_GAIN); double kdAdjuster = getDoubleFromConfig(config, CONFIG_KD_GAIN); @@ -103,15 +110,15 @@ public PIDControllerTriggerHandler(Trigger module, ItemRegistry itemRegistry, Ev loopTimeMs = ((BigDecimal) requireNonNull(config.get(CONFIG_LOOP_TIME), CONFIG_LOOP_TIME + " is not set")) .intValue(); - controller = new PIDController(outputLowerLimit, outputUpperLimit, kpAdjuster, kiAdjuster, kdAdjuster, - kdTimeConstant); + controller = new PIDController(kpAdjuster, kiAdjuster, kdAdjuster, kdTimeConstant); eventFilter = event -> { String topic = event.getTopic(); return topic.equals("openhab/items/" + inputItemName + "/state") || topic.equals("openhab/items/" + inputItemName + "/statechanged") - || topic.equals("openhab/items/" + setpointItemName + "/statechanged"); + || topic.equals("openhab/items/" + setpointItemName + "/statechanged") + || commandTopic.map(t -> topic.equals(t)).orElse(false); }; eventSubscriberRegistration = bundleContext.registerService(EventSubscriber.class.getName(), this, null); @@ -152,7 +159,7 @@ private void calculate() { long now = System.currentTimeMillis(); - PIDOutputDTO output = controller.calculate(input, setpoint, now - previousTimeMs); + PIDOutputDTO output = controller.calculate(input, setpoint, now - previousTimeMs, loopTimeMs); previousTimeMs = now; Map outputs = new HashMap<>(); @@ -198,7 +205,17 @@ private double getItemValueAsNumber(Item item) throws PIDException { @Override public void receive(Event event) { if (event instanceof ItemStateChangedEvent) { - calculate(); + if (event.getTopic().equals(commandTopic.get())) { + ItemStateChangedEvent changedEvent = (ItemStateChangedEvent) event; + if ("RESET".equals(changedEvent.getItemState().toString())) { + controller.setIntegralResult(0); + controller.setDerivativeResult(0); + } else { + logger.warn("Unknown command: {}", changedEvent.getItemState()); + } + } else { + calculate(); + } } } @@ -221,6 +238,8 @@ public void dispose() { localControllerjob.cancel(true); } + scheduler.shutdown(); + super.dispose(); } } diff --git a/bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/type/PIDControllerTriggerType.java b/bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/type/PIDControllerTriggerType.java index 2cdd7c74b935f..856ded793f3f4 100644 --- a/bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/type/PIDControllerTriggerType.java +++ b/bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/type/PIDControllerTriggerType.java @@ -44,23 +44,22 @@ public static PIDControllerTriggerType initialize() { .withRequired(true).withReadOnly(true).withMultiple(false).withContext("item").withLabel("Setpoint") .withDescription("Targeted setpoint").build()); configDescriptions.add(ConfigDescriptionParameterBuilder.create(CONFIG_KP_GAIN, Type.DECIMAL).withRequired(true) - .withMultiple(false).withDefault("1.0").withLabel("Proportional Gain (Kp)") + .withMultiple(false).withDefault("1.0").withMinimum(BigDecimal.ZERO).withLabel("Proportional Gain (Kp)") .withDescription("Change to output propertional to current error value.").build()); configDescriptions.add(ConfigDescriptionParameterBuilder.create(CONFIG_KI_GAIN, Type.DECIMAL).withRequired(true) - .withMultiple(false).withDefault("1.0").withLabel("Integral Gain (Ki)") + .withMultiple(false).withDefault("1.0").withMinimum(BigDecimal.ZERO).withLabel("Integral Gain (Ki)") .withDescription("Accelerate movement towards the setpoint.").build()); configDescriptions.add(ConfigDescriptionParameterBuilder.create(CONFIG_KD_GAIN, Type.DECIMAL).withRequired(true) - .withMultiple(false).withDefault("1.0").withLabel("Derivative Gain (Kd)") + .withMultiple(false).withDefault("1.0").withMinimum(BigDecimal.ZERO).withLabel("Derivative Gain (Kd)") .withDescription("Slows the rate of change of the output.").build()); configDescriptions.add(ConfigDescriptionParameterBuilder.create(CONFIG_KD_TIMECONSTANT, Type.DECIMAL) - .withRequired(true).withMultiple(false).withDefault("1.0").withLabel("Derivative Time Constant") - .withDescription("Slows the rate of change of the D Part (T1) in seconds.").withUnit("s").build()); - configDescriptions.add(ConfigDescriptionParameterBuilder.create(CONFIG_OUTPUT_LOWER_LIMIT, Type.DECIMAL) - .withRequired(true).withMultiple(false).withDefault("0").withLabel("Output Lower Limit") - .withDescription("The output of the PID controller will be min this value").build()); - configDescriptions.add(ConfigDescriptionParameterBuilder.create(CONFIG_OUTPUT_UPPER_LIMIT, Type.DECIMAL) - .withRequired(true).withMultiple(false).withDefault("100").withLabel("Output Upper Limit") - .withDescription("The output of the PID controller will be max this value").build()); + .withRequired(true).withMultiple(false).withMinimum(BigDecimal.ZERO).withDefault("1.0") + .withLabel("Derivative Time Constant") + .withDescription("Slows the rate of change of the D part (T1) in seconds.").withUnit("s").build()); + configDescriptions + .add(ConfigDescriptionParameterBuilder.create(CONFIG_COMMAND_ITEM, Type.TEXT).withRequired(false) + .withReadOnly(true).withMultiple(false).withContext("item").withLabel("Command Item") + .withDescription("You can send String commands to this Item like \"RESET\".").build()); configDescriptions.add(ConfigDescriptionParameterBuilder.create(CONFIG_LOOP_TIME, Type.DECIMAL) .withRequired(true).withMultiple(false).withDefault(DEFAULT_LOOPTIME_MS).withLabel("Loop Time") .withDescription("The interval the output value is updated in ms").withUnit("ms").build());