From 2c4b1ddb7d3174940b6e95c631e2c83890912bea Mon Sep 17 00:00:00 2001 From: Fabian Wolter Date: Mon, 28 Dec 2020 18:31:17 +0100 Subject: [PATCH] [pidcontroller] Initial Contribution (#9512) * [pidcontroller] Initial Contribution * Incorporate review feedback No.1 Signed-off-by: Fabian Wolter Signed-off-by: Joseph Hagberg --- CODEOWNERS | 1 + bom/openhab-addons/pom.xml | 5 + .../NOTICE | 13 + .../README.md | 137 +++++++++++ .../pom.xml | 17 ++ .../src/main/feature/feature.xml | 9 + .../pidcontroller/internal/LowpassFilter.java | 44 ++++ .../internal/PIDControllerConstants.java | 41 ++++ .../pidcontroller/internal/PIDException.java | 31 +++ .../PIDControllerModuleHandlerFactory.java | 72 ++++++ .../internal/handler/PIDController.java | 82 +++++++ .../handler/PIDControllerActionHandler.java | 78 ++++++ .../handler/PIDControllerTriggerHandler.java | 226 ++++++++++++++++++ .../internal/handler/PIDOutputDTO.java | 54 +++++ .../template/PIDControllerRuleTemplate.java | 64 +++++ .../PIDControllerTemplateProvider.java | 59 +++++ .../type/PIDControllerActionType.java | 71 ++++++ .../type/PIDControllerModuleTypeProvider.java | 66 +++++ .../type/PIDControllerTriggerType.java | 88 +++++++ .../internal/LowpassFilterTest.java | 68 ++++++ bundles/pom.xml | 1 + 21 files changed, 1227 insertions(+) create mode 100644 bundles/org.openhab.automation.pidcontroller/NOTICE create mode 100644 bundles/org.openhab.automation.pidcontroller/README.md create mode 100644 bundles/org.openhab.automation.pidcontroller/pom.xml create mode 100644 bundles/org.openhab.automation.pidcontroller/src/main/feature/feature.xml create mode 100644 bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/LowpassFilter.java create mode 100644 bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/PIDControllerConstants.java create mode 100644 bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/PIDException.java create mode 100644 bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/factory/PIDControllerModuleHandlerFactory.java create mode 100644 bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/handler/PIDController.java create mode 100644 bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/handler/PIDControllerActionHandler.java create mode 100644 bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/handler/PIDControllerTriggerHandler.java create mode 100644 bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/handler/PIDOutputDTO.java create mode 100644 bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/template/PIDControllerRuleTemplate.java create mode 100644 bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/template/PIDControllerTemplateProvider.java create mode 100644 bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/type/PIDControllerActionType.java create mode 100644 bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/type/PIDControllerModuleTypeProvider.java create mode 100644 bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/type/PIDControllerTriggerType.java create mode 100644 bundles/org.openhab.automation.pidcontroller/src/test/java/org/openhab/automation/pidcontroller/internal/LowpassFilterTest.java diff --git a/CODEOWNERS b/CODEOWNERS index 9f95f07fe4325..d9f00665aeb78 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -7,6 +7,7 @@ # Add-on maintainers: /bundles/org.openhab.automation.groovyscripting/ @wborn /bundles/org.openhab.automation.jythonscripting/ @openhab/add-ons-maintainers +/bundles/org.openhab.automation.pidcontroller/ @fwolter /bundles/org.openhab.binding.adorne/ @theiding /bundles/org.openhab.binding.airquality/ @kubawolanin /bundles/org.openhab.binding.airvisualnode/ @3cky diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml index 0d3cfc75ba5bf..15def18641fa3 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -26,6 +26,11 @@ org.openhab.automation.jythonscripting ${project.version} + + org.openhab.addons.bundles + org.openhab.automation.pidcontroller + ${project.version} + org.openhab.addons.bundles org.openhab.binding.adorne diff --git a/bundles/org.openhab.automation.pidcontroller/NOTICE b/bundles/org.openhab.automation.pidcontroller/NOTICE new file mode 100644 index 0000000000000..38d625e349232 --- /dev/null +++ b/bundles/org.openhab.automation.pidcontroller/NOTICE @@ -0,0 +1,13 @@ +This content is produced and maintained by the openHAB project. + +* Project home: https://www.openhab.org + +== Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Eclipse Public License 2.0 which is available at +https://www.eclipse.org/legal/epl-2.0/. + +== Source Code + +/~https://github.com/openhab/openhab-addons diff --git a/bundles/org.openhab.automation.pidcontroller/README.md b/bundles/org.openhab.automation.pidcontroller/README.md new file mode 100644 index 0000000000000..4822a3e98112a --- /dev/null +++ b/bundles/org.openhab.automation.pidcontroller/README.md @@ -0,0 +1,137 @@ +# PID Controller Automation + +This automation implements a [PID](https://en.wikipedia.org/wiki/PID_controller)-T1 controller for openHAB. + +A PID controller can be used for closed-loop controls. For example: + +- Heating: A sensor measures the room temperature. + The PID controller calculates the heater's valve opening, so that the room temperature is kept at the setpoint. +- Lighting: A light sensor measures the room's illuminance. + The PID controller controls the dimmer of the room's lighting, so that the illuminance in the room is kept at a constant level. +- PV zero export: A meter measures the power at the grid point of the building. + The PID controller calculates the amount of power the battery storage system needs to feed-in or charge the battery, so that the building's grid power consumption is around zero, + i.e. PV generation, battery storage output power and the building's power consumption are at balance. + +## Modules + +The PID controller can be used in openHAB's [rule engine](https://www.openhab.org/docs/configuration/rules-dsl.html). This automation provides a trigger and an action module. + +### 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. +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. + +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. + +### Action + +This module writes the PID controller's output value into the `output` Item and provides debugging abilities. + +| Name | Type | Description | Required | +|--------------|------|----------------------------------------------------------------------|----------| +| `output` | Item | Name of the output Item (e.g. the valve actuator 0-100%) | 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 | +| `eInspector` | Item | Name of the debug Item for the current regulation difference (error) | N | + +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 everytime the output is updated. + +## Proportional (P) Gain Parameter + +Parameter: `kp` + +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. +If the output is the opening of a valve in %, you might want to set this parameter to higher values (`kp=10` would result in 50%). + +## Integral (I) Gain Parameter + +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 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. +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). + +## Derivative (D) Gain Parameter + +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 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). +When the temperature drops to 10°C due to an opened window (error=15°C), the output is set to 15°C - 5°C = 10. + +## Derivative Time Constant (D-T1) Parameter + +Parameter: `kdTimeConstant` + +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. + +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 + +Tuning the `Kp`, `Ki` and `Kd` parameters can be done by applying science. +It can also be done by heuristic methods like the [Ziegler–Nichols method](https://en.wikipedia.org/wiki/Ziegler%E2%80%93Nichols_method). +But it can also be done by trial and error. +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. +The visualization can be done by the analyze function in Main UI or 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: + +> *Notice:* A good starting point for the derivative time constant `kdTimeConstant` is the response time of the control loop. +E.g. the time it takes from opening the heater valve and seeing an effect of the measured temperature. + +1. Set `kp`, `ki` and `kd` to 0 +2. Increase `kp` until the system starts to oscillate (continuous over- and undershoot) +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 + +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). + +This process can take some time with slow responding control loops like heating systems. +You will get faster results with constant lighting or PV zero export applications. diff --git a/bundles/org.openhab.automation.pidcontroller/pom.xml b/bundles/org.openhab.automation.pidcontroller/pom.xml new file mode 100644 index 0000000000000..dda56417f252c --- /dev/null +++ b/bundles/org.openhab.automation.pidcontroller/pom.xml @@ -0,0 +1,17 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 3.1.0-SNAPSHOT + + + org.openhab.automation.pidcontroller + + openHAB Add-ons :: Bundles :: Automation :: PID Controller + + diff --git a/bundles/org.openhab.automation.pidcontroller/src/main/feature/feature.xml b/bundles/org.openhab.automation.pidcontroller/src/main/feature/feature.xml new file mode 100644 index 0000000000000..9ff0312f675f2 --- /dev/null +++ b/bundles/org.openhab.automation.pidcontroller/src/main/feature/feature.xml @@ -0,0 +1,9 @@ + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features + + + openhab-runtime-base + mvn:org.openhab.addons.bundles/org.openhab.automation.pidcontroller/${project.version} + + diff --git a/bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/LowpassFilter.java b/bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/LowpassFilter.java new file mode 100644 index 0000000000000..338478de754fc --- /dev/null +++ b/bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/LowpassFilter.java @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.automation.pidcontroller.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Realizes an first-order FIR low pass filter. To keep code complexity low, it is implemented as moving average (all + * FIR coefficients are set to normalized ones). + * + * The exponential decaying function is used for the calculation (see https://en.wikipedia.org/wiki/Time_constant). That + * means the output value is approx. 63% of the input value after one time constant and approx. 99% after 5 time + * constants. + * + * @author Fabian Wolter - Initial contribution + * + */ +@NonNullByDefault +public class LowpassFilter { + /** + * Executes one low pass filter step. + * + * @param lastOutput the current filter value (result of the last invocation) + * @param newValue the just sampled value + * @param timeQuotient quotient of the current time and the time constant + * @return the new filter value + */ + public static double calculate(double lastOutput, double newValue, double timeQuotient) { + double output = lastOutput * Math.exp(-timeQuotient); + output += newValue * (1 - Math.exp(-timeQuotient)); + + return output; + } +} 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 new file mode 100644 index 0000000000000..bce765b9fea61 --- /dev/null +++ b/bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/PIDControllerConstants.java @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.automation.pidcontroller.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * + * Constants for PID controller. + * + * @author Fabian Wolter - Initial contribution + * + */ +@NonNullByDefault +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_LOOP_TIME = "loopTime"; + public static final String CONFIG_KP_GAIN = "kp"; + 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 P_INSPECTOR = "pInspector"; + public static final String I_INSPECTOR = "iInspector"; + public static final String D_INSPECTOR = "dInspector"; + public static final String E_INSPECTOR = "eInspector"; + public static final String OUTPUT = "output"; +} diff --git a/bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/PIDException.java b/bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/PIDException.java new file mode 100644 index 0000000000000..c91cf53eea20c --- /dev/null +++ b/bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/PIDException.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.automation.pidcontroller.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * + * Common Exception for PID controller. + * + * @author Fabian Wolter - Initial contribution + * + */ +@NonNullByDefault +public class PIDException extends Exception { + private static final long serialVersionUID = -3029834022610530982L; + + public PIDException(String message) { + super(message); + } +} diff --git a/bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/factory/PIDControllerModuleHandlerFactory.java b/bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/factory/PIDControllerModuleHandlerFactory.java new file mode 100644 index 0000000000000..4db50280ea63d --- /dev/null +++ b/bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/factory/PIDControllerModuleHandlerFactory.java @@ -0,0 +1,72 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.automation.pidcontroller.internal.factory; + +import java.util.Collection; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.automation.pidcontroller.internal.handler.PIDControllerActionHandler; +import org.openhab.automation.pidcontroller.internal.handler.PIDControllerTriggerHandler; +import org.openhab.core.automation.Action; +import org.openhab.core.automation.Module; +import org.openhab.core.automation.Trigger; +import org.openhab.core.automation.handler.BaseModuleHandlerFactory; +import org.openhab.core.automation.handler.ModuleHandler; +import org.openhab.core.automation.handler.ModuleHandlerFactory; +import org.openhab.core.events.EventPublisher; +import org.openhab.core.items.ItemRegistry; +import org.osgi.framework.BundleContext; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +/** + * + * @author Hilbrand Bouwkamp - Initial Contribution + */ +@Component(service = ModuleHandlerFactory.class, configurationPid = "action.pidcontroller") +@NonNullByDefault +public class PIDControllerModuleHandlerFactory extends BaseModuleHandlerFactory { + private static final Collection TYPES = Set.of(PIDControllerTriggerHandler.MODULE_TYPE_ID, + PIDControllerActionHandler.MODULE_TYPE_ID); + private ItemRegistry itemRegistry; + private EventPublisher eventPublisher; + private BundleContext bundleContext; + + @Activate + public PIDControllerModuleHandlerFactory(@Reference ItemRegistry itemRegistry, + @Reference EventPublisher eventPublisher, BundleContext bundleContext) { + this.itemRegistry = itemRegistry; + this.eventPublisher = eventPublisher; + this.bundleContext = bundleContext; + } + + @Override + public Collection getTypes() { + return TYPES; + } + + @Override + protected @Nullable ModuleHandler internalCreate(Module module, String ruleUID) { + switch (module.getTypeUID()) { + case PIDControllerTriggerHandler.MODULE_TYPE_ID: + return new PIDControllerTriggerHandler((Trigger) module, itemRegistry, eventPublisher, bundleContext); + case PIDControllerActionHandler.MODULE_TYPE_ID: + return new PIDControllerActionHandler((Action) module, itemRegistry, eventPublisher); + } + + return null; + } +} 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 new file mode 100644 index 0000000000000..66450f128cff5 --- /dev/null +++ b/bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/handler/PIDController.java @@ -0,0 +1,82 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.automation.pidcontroller.internal.handler; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.automation.pidcontroller.internal.LowpassFilter; + +/** + * The {@link PIDController} provides the necessary methods for retrieving part(s) of the PID calculations + * and it provides the method for the overall PID calculations. It also resets the PID controller + * + * @author George Erhan - Initial contribution + * @author Hilbrand Bouwkamp - Adapted for new rule engine + * @author Fabian Wolter - Add T1 to D part, add debugging ability for PID values + */ +@NonNullByDefault +class PIDController { + private final double outputLowerLimit; + private final double outputUpperLimit; + + private double integralResult; + private double derivativeResult; + private double previousError; + private double output; + + private double kp; + private double ki; + 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; + this.kp = kpAdjuster; + this.ki = kiAdjuster; + this.kd = kdAdjuster; + this.derivativeTimeConstantSec = derivativeTimeConstantSec; + } + + public PIDOutputDTO calculate(double input, double setpoint, long lastInvocationMs) { + final double lastInvocationSec = lastInvocationMs / 1000d; + final double error = setpoint - input; + + // derivative T1 calculation + final double timeQuotient = lastInvocationSec / derivativeTimeConstantSec; + if (derivativeTimeConstantSec != 0) { + derivativeResult = LowpassFilter.calculate(derivativeResult, error - previousError, timeQuotient); + previousError = error; + } + + // 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)); + } + + // 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); + } +} diff --git a/bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/handler/PIDControllerActionHandler.java b/bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/handler/PIDControllerActionHandler.java new file mode 100644 index 0000000000000..3e814f76797e4 --- /dev/null +++ b/bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/handler/PIDControllerActionHandler.java @@ -0,0 +1,78 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.automation.pidcontroller.internal.handler; + +import static org.openhab.automation.pidcontroller.internal.PIDControllerConstants.*; + +import java.math.BigDecimal; +import java.util.Map; +import java.util.stream.Stream; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.automation.Action; +import org.openhab.core.automation.handler.ActionHandler; +import org.openhab.core.automation.handler.BaseModuleHandler; +import org.openhab.core.events.EventPublisher; +import org.openhab.core.items.ItemRegistry; +import org.openhab.core.items.events.ItemCommandEvent; +import org.openhab.core.items.events.ItemEventFactory; +import org.openhab.core.library.types.DecimalType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * + * @author Hilbrand Bouwkamp - Initial Contribution + * @author Fabian Wolter - Add PID debugging items + */ +@NonNullByDefault +public class PIDControllerActionHandler extends BaseModuleHandler implements ActionHandler { + public static final String MODULE_TYPE_ID = AUTOMATION_NAME + ".action"; + + private final Logger logger = LoggerFactory.getLogger(PIDControllerActionHandler.class); + + private ItemRegistry itemRegistry; + private EventPublisher eventPublisher; + + public PIDControllerActionHandler(Action module, ItemRegistry itemRegistry, EventPublisher eventPublisher) { + super(module); + this.itemRegistry = itemRegistry; + this.eventPublisher = eventPublisher; + } + + @Override + public @Nullable Map execute(Map context) { + Stream.of(OUTPUT, P_INSPECTOR, I_INSPECTOR, D_INSPECTOR, E_INSPECTOR).forEach(arg -> { + final String itemName = (String) module.getConfiguration().get(arg); + + if (itemName == null || itemName.isBlank()) { + return; + } + + final BigDecimal command = (BigDecimal) context.get("1." + arg); + + if (command != null) { + final DecimalType outputValue = new DecimalType(command); + final ItemCommandEvent itemCommandEvent = ItemEventFactory.createCommandEvent(itemName, outputValue); + + eventPublisher.post(itemCommandEvent); + } else { + logger.warn( + "Command was not posted because either the configuration was not correct or a service was missing: ItemName: {}, Command: {}, eventPublisher: {}, ItemRegistry: {}", + itemName, command, eventPublisher, itemRegistry); + } + }); + return null; + } +} 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 new file mode 100644 index 0000000000000..cfeb52b1eff0f --- /dev/null +++ b/bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/handler/PIDControllerTriggerHandler.java @@ -0,0 +1,226 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.automation.pidcontroller.internal.handler; + +import static org.openhab.automation.pidcontroller.internal.PIDControllerConstants.*; + +import java.math.BigDecimal; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.automation.pidcontroller.internal.PIDException; +import org.openhab.core.automation.ModuleHandlerCallback; +import org.openhab.core.automation.Trigger; +import org.openhab.core.automation.handler.BaseTriggerModuleHandler; +import org.openhab.core.automation.handler.TriggerHandlerCallback; +import org.openhab.core.common.NamedThreadFactory; +import org.openhab.core.config.core.Configuration; +import org.openhab.core.events.Event; +import org.openhab.core.events.EventFilter; +import org.openhab.core.events.EventPublisher; +import org.openhab.core.events.EventSubscriber; +import org.openhab.core.items.Item; +import org.openhab.core.items.ItemNotFoundException; +import org.openhab.core.items.ItemRegistry; +import org.openhab.core.items.events.ItemEventFactory; +import org.openhab.core.items.events.ItemStateChangedEvent; +import org.openhab.core.items.events.ItemStateEvent; +import org.openhab.core.library.types.StringType; +import org.openhab.core.types.RefreshType; +import org.openhab.core.types.State; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceRegistration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * + * @author Hilbrand Bouwkamp - Initial Contribution + * @author Fabian Wolter - Add PID debug output values + */ +@NonNullByDefault +public class PIDControllerTriggerHandler extends BaseTriggerModuleHandler implements EventSubscriber { + public static final String MODULE_TYPE_ID = AUTOMATION_NAME + ".trigger"; + 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)); + 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 EventFilter eventFilter; + + public PIDControllerTriggerHandler(Trigger module, ItemRegistry itemRegistry, EventPublisher eventPublisher, + BundleContext bundleContext) { + super(module); + + Configuration config = module.getConfiguration(); + + String inputItemName = (String) requireNonNull(config.get(CONFIG_INPUT_ITEM), "Input item is not set"); + String setpointItemName = (String) requireNonNull(config.get(CONFIG_SETPOINT_ITEM), "Setpoint item is not set"); + + try { + inputItem = itemRegistry.getItem(inputItemName); + } catch (ItemNotFoundException e) { + throw new IllegalArgumentException("Configured input item not found: " + inputItemName, e); + } + + try { + setpointItem = itemRegistry.getItem(setpointItemName); + } catch (ItemNotFoundException e) { + 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); + double kpAdjuster = getDoubleFromConfig(config, CONFIG_KP_GAIN); + double kiAdjuster = getDoubleFromConfig(config, CONFIG_KI_GAIN); + double kdAdjuster = getDoubleFromConfig(config, CONFIG_KD_GAIN); + double kdTimeConstant = getDoubleFromConfig(config, CONFIG_KD_TIMECONSTANT); + + loopTimeMs = ((BigDecimal) requireNonNull(config.get(CONFIG_LOOP_TIME), CONFIG_LOOP_TIME + " is not set")) + .intValue(); + + controller = new PIDController(outputLowerLimit, outputUpperLimit, 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"); + }; + + eventSubscriberRegistration = bundleContext.registerService(EventSubscriber.class.getName(), this, null); + + eventPublisher.post(ItemEventFactory.createCommandEvent(inputItemName, RefreshType.REFRESH)); + + controllerjob = scheduler.scheduleWithFixedDelay(this::calculate, 0, loopTimeMs, TimeUnit.MILLISECONDS); + } + + private T requireNonNull(T obj, String message) { + if (obj == null) { + throw new IllegalArgumentException(message); + } + return obj; + } + + private double getDoubleFromConfig(Configuration config, String key) { + return ((BigDecimal) Objects.requireNonNull(config.get(key), key + " is not set")).doubleValue(); + } + + private void calculate() { + double input; + double setpoint; + + try { + input = getItemValueAsNumber(inputItem); + } catch (PIDException e) { + logger.warn("Input item: {}", e.getMessage()); + return; + } + + try { + setpoint = getItemValueAsNumber(setpointItem); + } catch (PIDException e) { + logger.warn("Setpoint item: {}", e.getMessage()); + return; + } + + long now = System.currentTimeMillis(); + + PIDOutputDTO output = controller.calculate(input, setpoint, now - previousTimeMs); + previousTimeMs = now; + + Map outputs = new HashMap<>(); + + putBigDecimal(outputs, OUTPUT, output.getOutput()); + putBigDecimal(outputs, P_INSPECTOR, output.getProportionalPart()); + putBigDecimal(outputs, I_INSPECTOR, output.getIntegralPart()); + putBigDecimal(outputs, D_INSPECTOR, output.getDerivativePart()); + putBigDecimal(outputs, E_INSPECTOR, output.getError()); + + ModuleHandlerCallback localCallback = callback; + if (localCallback != null && localCallback instanceof TriggerHandlerCallback) { + ((TriggerHandlerCallback) localCallback).triggered(module, outputs); + } else { + logger.warn("No callback set"); + } + } + + private void putBigDecimal(Map map, String key, double value) { + map.put(key, BigDecimal.valueOf(value)); + } + + private double getItemValueAsNumber(Item item) throws PIDException { + State setpointState = item.getState(); + + if (setpointState instanceof Number) { + double doubleValue = ((Number) setpointState).doubleValue(); + + if (Double.isFinite(doubleValue)) { + return doubleValue; + } + } else if (setpointState instanceof StringType) { + try { + return Double.parseDouble(setpointState.toString()); + } catch (NumberFormatException e) { + // nothing + } + } + throw new PIDException( + "Item type is not a number: " + setpointState.getClass().getSimpleName() + ": " + setpointState); + } + + @Override + public void receive(Event event) { + if (event instanceof ItemStateChangedEvent) { + calculate(); + } + } + + @Override + public Set getSubscribedEventTypes() { + return SUBSCRIBED_EVENT_TYPES; + } + + @Override + public @Nullable EventFilter getEventFilter() { + return eventFilter; + } + + @Override + public void dispose() { + eventSubscriberRegistration.unregister(); + + ScheduledFuture localControllerjob = controllerjob; + if (localControllerjob != null) { + localControllerjob.cancel(true); + } + + super.dispose(); + } +} diff --git a/bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/handler/PIDOutputDTO.java b/bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/handler/PIDOutputDTO.java new file mode 100644 index 0000000000000..d84e6c9155f81 --- /dev/null +++ b/bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/handler/PIDOutputDTO.java @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.automation.pidcontroller.internal.handler; + +/** + * + * @author Fabian Wolter - Initial Contribution + */ +public class PIDOutputDTO { + private double output; + private double proportionalPart; + private double integralPart; + private double derivativePart; + private double error; + + public PIDOutputDTO(double output, double proportionalPart, double integralPart, double derivativePart, + double error) { + this.output = output; + this.proportionalPart = proportionalPart; + this.integralPart = integralPart; + this.derivativePart = derivativePart; + this.error = error; + } + + public double getOutput() { + return output; + } + + public double getProportionalPart() { + return proportionalPart; + } + + public double getIntegralPart() { + return integralPart; + } + + public double getDerivativePart() { + return derivativePart; + } + + public double getError() { + return error; + } +} diff --git a/bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/template/PIDControllerRuleTemplate.java b/bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/template/PIDControllerRuleTemplate.java new file mode 100644 index 0000000000000..e43eff9300230 --- /dev/null +++ b/bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/template/PIDControllerRuleTemplate.java @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.automation.pidcontroller.internal.template; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.automation.pidcontroller.internal.PIDControllerConstants; +import org.openhab.automation.pidcontroller.internal.handler.PIDControllerActionHandler; +import org.openhab.automation.pidcontroller.internal.handler.PIDControllerTriggerHandler; +import org.openhab.automation.pidcontroller.internal.type.PIDControllerActionType; +import org.openhab.core.automation.Action; +import org.openhab.core.automation.Condition; +import org.openhab.core.automation.Trigger; +import org.openhab.core.automation.Visibility; +import org.openhab.core.automation.template.RuleTemplate; +import org.openhab.core.automation.util.ModuleBuilder; +import org.openhab.core.config.core.ConfigDescriptionParameter; + +/** + * + * @author Hilbrand Bouwkamp - Initial Contribution + */ +@NonNullByDefault +public class PIDControllerRuleTemplate extends RuleTemplate { + public static final String UID = "PIDControllerRuleTemplate"; + + public static PIDControllerRuleTemplate initialize() { + final String triggerId = UUID.randomUUID().toString(); + + final List triggers = List.of(ModuleBuilder.createTrigger().withId(triggerId) + .withTypeUID(PIDControllerTriggerHandler.MODULE_TYPE_ID).withLabel("PID Controller Trigger").build()); + + final Map actionInputs = Map.of(PIDControllerActionType.INPUT, + triggerId + "." + PIDControllerConstants.OUTPUT); + + final List actions = List.of(ModuleBuilder.createAction().withId(UUID.randomUUID().toString()) + .withTypeUID(PIDControllerActionHandler.MODULE_TYPE_ID).withLabel("PID Controller Action") + .withInputs(actionInputs).build()); + + return new PIDControllerRuleTemplate(Set.of("PID Controller"), triggers, Collections.emptyList(), actions, + Collections.emptyList()); + } + + public PIDControllerRuleTemplate(Set tags, List triggers, List conditions, + List actions, List configDescriptions) { + super(UID, "PID Controller", "Template for a PID controlled rule", tags, triggers, conditions, actions, + configDescriptions, Visibility.VISIBLE); + } +} diff --git a/bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/template/PIDControllerTemplateProvider.java b/bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/template/PIDControllerTemplateProvider.java new file mode 100644 index 0000000000000..2036af30d8909 --- /dev/null +++ b/bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/template/PIDControllerTemplateProvider.java @@ -0,0 +1,59 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.automation.pidcontroller.internal.template; + +import java.util.Collection; +import java.util.Locale; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.automation.template.RuleTemplate; +import org.openhab.core.automation.template.RuleTemplateProvider; +import org.openhab.core.common.registry.ProviderChangeListener; +import org.osgi.service.component.annotations.Component; + +/** + * + * @author Hilbrand Bouwkamp - Initial Contribution + */ +@Component +@NonNullByDefault +public class PIDControllerTemplateProvider implements RuleTemplateProvider { + private static final RuleTemplate PROVIDED_RULE_TEMPLATE = PIDControllerRuleTemplate.initialize(); + + @Override + public @Nullable RuleTemplate getTemplate(String uid, @Nullable Locale locale) { + return uid.equals(PIDControllerRuleTemplate.UID) ? PROVIDED_RULE_TEMPLATE : null; + } + + @Override + public Collection getTemplates(@Nullable Locale locale) { + return Set.of(PROVIDED_RULE_TEMPLATE); + } + + @Override + public void addProviderChangeListener(ProviderChangeListener listener) { + // does nothing because this provider does not change + } + + @Override + public Collection getAll() { + return Set.of(PROVIDED_RULE_TEMPLATE); + } + + @Override + public void removeProviderChangeListener(ProviderChangeListener listener) { + // does nothing because this provider does not change + } +} diff --git a/bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/type/PIDControllerActionType.java b/bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/type/PIDControllerActionType.java new file mode 100644 index 0000000000000..63b75a9b29fc1 --- /dev/null +++ b/bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/type/PIDControllerActionType.java @@ -0,0 +1,71 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.automation.pidcontroller.internal.type; + +import static org.openhab.automation.pidcontroller.internal.PIDControllerConstants.*; + +import java.math.BigDecimal; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.automation.pidcontroller.internal.handler.PIDControllerActionHandler; +import org.openhab.core.automation.Visibility; +import org.openhab.core.automation.type.ActionType; +import org.openhab.core.automation.type.Input; +import org.openhab.core.config.core.ConfigDescriptionParameter; +import org.openhab.core.config.core.ConfigDescriptionParameter.Type; +import org.openhab.core.config.core.ConfigDescriptionParameterBuilder; + +/** + * + * @author Hilbrand Bouwkamp - Initial Contribution + */ +@NonNullByDefault +public class PIDControllerActionType extends ActionType { + public static final String INPUT = "input"; + + public static PIDControllerActionType initialize() { + final ConfigDescriptionParameter outputItem = ConfigDescriptionParameterBuilder.create(OUTPUT, Type.TEXT) + .withRequired(true).withMultiple(false).withContext("item").withLabel("Output Item") + .withDescription("Item to send output").build(); + final ConfigDescriptionParameter pInspectorItem = ConfigDescriptionParameterBuilder + .create(P_INSPECTOR, Type.TEXT).withRequired(false).withMultiple(false).withContext("item") + .withLabel("P Inspector Item").withDescription("Item for debugging the P part").build(); + final ConfigDescriptionParameter iInspectorItem = ConfigDescriptionParameterBuilder + .create(I_INSPECTOR, Type.TEXT).withRequired(false).withMultiple(false).withContext("item") + .withLabel("I Inspector Item").withDescription("Item for debugging the I part").build(); + final ConfigDescriptionParameter dInspectorItem = ConfigDescriptionParameterBuilder + .create(D_INSPECTOR, Type.TEXT).withRequired(false).withMultiple(false).withContext("item") + .withLabel("D Inspector Item").withDescription("Item for debugging the D part").build(); + final ConfigDescriptionParameter eInspectorItem = ConfigDescriptionParameterBuilder + .create(E_INSPECTOR, Type.TEXT).withRequired(false).withMultiple(false).withContext("item") + .withLabel("Error Inspector Item").withDescription("Item for debugging the error value").build(); + + List config = List.of(outputItem, pInspectorItem, iInspectorItem, dInspectorItem, + eInspectorItem); + + List inputs = List.of(createInput(INPUT), createInput(P_INSPECTOR), createInput(I_INSPECTOR), + createInput(D_INSPECTOR), createInput(E_INSPECTOR)); + + return new PIDControllerActionType(config, inputs); + } + + private static Input createInput(String name) { + return new Input(name, BigDecimal.class.getName()); + } + + public PIDControllerActionType(List configDescriptions, List inputs) { + super(PIDControllerActionHandler.MODULE_TYPE_ID, configDescriptions, "calculate PID output", null, null, + Visibility.VISIBLE, inputs, null); + } +} diff --git a/bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/type/PIDControllerModuleTypeProvider.java b/bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/type/PIDControllerModuleTypeProvider.java new file mode 100644 index 0000000000000..f1049b4efb860 --- /dev/null +++ b/bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/type/PIDControllerModuleTypeProvider.java @@ -0,0 +1,66 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.automation.pidcontroller.internal.type; + +import java.util.Collection; +import java.util.Collections; +import java.util.Locale; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.automation.pidcontroller.internal.handler.PIDControllerActionHandler; +import org.openhab.automation.pidcontroller.internal.handler.PIDControllerTriggerHandler; +import org.openhab.core.automation.type.ModuleType; +import org.openhab.core.automation.type.ModuleTypeProvider; +import org.openhab.core.common.registry.ProviderChangeListener; +import org.osgi.service.component.annotations.Component; + +/** + * + * @author Hilbrand Bouwkamp - Initial Contribution + */ +@Component +@NonNullByDefault +public class PIDControllerModuleTypeProvider implements ModuleTypeProvider { + private static final Map PROVIDED_MODULE_TYPES = Map.of( + PIDControllerActionHandler.MODULE_TYPE_ID, PIDControllerActionType.initialize(), + PIDControllerTriggerHandler.MODULE_TYPE_ID, PIDControllerTriggerType.initialize()); + + @SuppressWarnings("unchecked") + @Override + public T getModuleType(@Nullable String UID, @Nullable Locale locale) { + return (T) PROVIDED_MODULE_TYPES.get(UID); + } + + @SuppressWarnings("unchecked") + @Override + public Collection getModuleTypes(@Nullable Locale locale) { + return (Collection) PROVIDED_MODULE_TYPES.values(); + } + + @Override + public void addProviderChangeListener(ProviderChangeListener listener) { + // does nothing because this provider does not change + } + + @Override + public Collection getAll() { + return Collections.unmodifiableCollection(PROVIDED_MODULE_TYPES.values()); + } + + @Override + public void removeProviderChangeListener(ProviderChangeListener listener) { + // does nothing because this provider does not change + } +} 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 new file mode 100644 index 0000000000000..1e105d53733bf --- /dev/null +++ b/bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/type/PIDControllerTriggerType.java @@ -0,0 +1,88 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.automation.pidcontroller.internal.type; + +import static org.openhab.automation.pidcontroller.internal.PIDControllerConstants.*; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.automation.pidcontroller.internal.handler.PIDControllerTriggerHandler; +import org.openhab.core.automation.Visibility; +import org.openhab.core.automation.type.Output; +import org.openhab.core.automation.type.TriggerType; +import org.openhab.core.config.core.ConfigDescriptionParameter; +import org.openhab.core.config.core.ConfigDescriptionParameter.Type; +import org.openhab.core.config.core.ConfigDescriptionParameterBuilder; + +/** + * + * @author Hilbrand Bouwkamp - Initial Contribution + */ +@NonNullByDefault +public class PIDControllerTriggerType extends TriggerType { + private static final String DEFAULT_LOOPTIME_MS = "1000"; + + public static PIDControllerTriggerType initialize() { + List configDescriptions = new ArrayList<>(); + configDescriptions.add(ConfigDescriptionParameterBuilder.create(CONFIG_INPUT_ITEM, Type.TEXT).withRequired(true) + .withReadOnly(true).withMultiple(false).withContext("item").withLabel("Input Item") + .withDescription("Item to monitor").build()); + configDescriptions.add(ConfigDescriptionParameterBuilder.create(CONFIG_SETPOINT_ITEM, Type.TEXT) + .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)") + .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)") + .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)") + .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()); + 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()); + + Output output = new Output(OUTPUT, BigDecimal.class.getName(), "Output", "Output value of the PID Controller", + null, null, null); + Output pInspector = new Output(P_INSPECTOR, BigDecimal.class.getName(), "P Inspector", + "Current P value of the pid controller", null, null, null); + Output iInspector = new Output(I_INSPECTOR, BigDecimal.class.getName(), "I Inspector", + "Current I value of the pid controller", null, null, null); + Output dInspector = new Output(D_INSPECTOR, BigDecimal.class.getName(), "D Inspector", + "Current D value of the pid controller", null, null, null); + Output eInspector = new Output(E_INSPECTOR, BigDecimal.class.getName(), "Error Value Inspector", + "Current error value of the pid controller", null, null, null); + + List outputs = List.of(output, pInspector, iInspector, dInspector, eInspector); + + return new PIDControllerTriggerType(configDescriptions, outputs); + } + + public PIDControllerTriggerType(List configDescriptions, List outputs) { + super(PIDControllerTriggerHandler.MODULE_TYPE_ID, configDescriptions, "PID controller triggers", null, null, + Visibility.VISIBLE, outputs); + } +} diff --git a/bundles/org.openhab.automation.pidcontroller/src/test/java/org/openhab/automation/pidcontroller/internal/LowpassFilterTest.java b/bundles/org.openhab.automation.pidcontroller/src/test/java/org/openhab/automation/pidcontroller/internal/LowpassFilterTest.java new file mode 100644 index 0000000000000..d371997745593 --- /dev/null +++ b/bundles/org.openhab.automation.pidcontroller/src/test/java/org/openhab/automation/pidcontroller/internal/LowpassFilterTest.java @@ -0,0 +1,68 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.automation.pidcontroller.internal; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; + +/** + * Test for LowpassFilter. + * + * @author Fabian Wolter - Initial contribution + * + */ +@NonNullByDefault +class LowpassFilterTest { + @Test + void test0to1after1tau() { + double output = LowpassFilter.calculate(0, 1, 1); + assertEquals(0.63, output, 0.01); + } + + @Test + void test0to1after2tau() { + double output = LowpassFilter.calculate(0, 1, 1); + output = LowpassFilter.calculate(output, 1, 1); + assertEquals(0.86, output, 0.01); + } + + @Test + void test0to1after5tau() { + double output = LowpassFilter.calculate(0, 1, 1); + output = LowpassFilter.calculate(output, 1, 1); + output = LowpassFilter.calculate(output, 1, 1); + output = LowpassFilter.calculate(output, 1, 1); + output = LowpassFilter.calculate(output, 1, 1); + assertEquals(0.99, output, 0.01); + } + + @Test + void test0to1after1tau2timeConstant() { + double output = LowpassFilter.calculate(0, 1, 2); + assertEquals(0.86, output, 0.01); + } + + @Test + void test0to1after0_1tau() { + double output = LowpassFilter.calculate(0, 1, 0.1); + assertEquals(0.095162582, output, 0.000000001); + } + + @Test + void test1to0after1tau() { + double output = LowpassFilter.calculate(1, 0, 1); + assertEquals(0.36, output, 0.01); + } +} diff --git a/bundles/pom.xml b/bundles/pom.xml index 76f27d01315c4..5150853341fef 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -20,6 +20,7 @@ org.openhab.automation.groovyscripting org.openhab.automation.jythonscripting + org.openhab.automation.pidcontroller org.openhab.io.homekit org.openhab.io.hueemulation