Skip to content

Commit

Permalink
[pidcontroller] Remove limits, make Ki dependent from the loop time, …
Browse files Browse the repository at this point in the history
…add reset command (openhab#9759)

* [pidcontroller] Remove limits, make Ki dependent from the loop time

Also fix naming of thread and shutdown executor.

Signed-off-by: Fabian Wolter <github@fabian-wolter.de>
Signed-off-by: John Marshall <john.marshall.au@gmail.com>
  • Loading branch information
fwolter authored and themillhousegroup committed May 10, 2021
1 parent 13f18ad commit 8c65162
Show file tree
Hide file tree
Showing 5 changed files with 62 additions and 55 deletions.
29 changes: 13 additions & 16 deletions bundles/org.openhab.automation.pidcontroller/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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).

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,6 @@
*/
@NonNullByDefault
class PIDController {
private final double outputLowerLimit;
private final double outputUpperLimit;

private double integralResult;
private double derivativeResult;
private double previousError;
Expand All @@ -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;

Expand All @@ -60,23 +54,22 @@ 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;
final double integralPart = ki * integralResult;
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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -62,14 +63,15 @@ public class PIDControllerTriggerHandler extends BaseTriggerModuleHandler implem
private static final Set<String> 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;
private @Nullable ScheduledFuture<?> controllerjob;
private long previousTimeMs = System.currentTimeMillis();
private Item inputItem;
private Item setpointItem;
private Optional<String> commandTopic;
private EventFilter eventFilter;

public PIDControllerTriggerHandler(Trigger module, ItemRegistry itemRegistry, EventPublisher eventPublisher,
Expand All @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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<String, BigDecimal> outputs = new HashMap<>();
Expand Down Expand Up @@ -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();
}
}
}

Expand All @@ -221,6 +238,8 @@ public void dispose() {
localControllerjob.cancel(true);
}

scheduler.shutdown();

super.dispose();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down

0 comments on commit 8c65162

Please sign in to comment.