diff --git a/CODEOWNERS b/CODEOWNERS
index 4f4cbc4e7..1eaef0477 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 33a19ae51..384488517 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.bundlesorg.openhab.binding.adorne
diff --git a/bundles/org.openhab.automation.pidcontroller/NOTICE b/bundles/org.openhab.automation.pidcontroller/NOTICE
new file mode 100644
index 000000000..38d625e34
--- /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 000000000..4822a3e98
--- /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 000000000..dda56417f
--- /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 000000000..9ff0312f6
--- /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 000000000..338478de7
--- /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 000000000..bce765b9f
--- /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 000000000..c91cf53ee
--- /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 000000000..4db50280e
--- /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 000000000..66450f128
--- /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 000000000..3e814f767
--- /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 000000000..cfeb52b1e
--- /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 000000000..d84e6c915
--- /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 000000000..e43eff930
--- /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 000000000..2036af30d
--- /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 000000000..63b75a9b2
--- /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 000000000..f1049b4ef
--- /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 000000000..1e105d537
--- /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