]> git.basschouten.com Git - openhab-addons.git/commitdiff
[pidcontroller] Initial Contribution (#9512)
authorFabian Wolter <github@fabian-wolter.de>
Mon, 28 Dec 2020 17:31:17 +0000 (18:31 +0100)
committerGitHub <noreply@github.com>
Mon, 28 Dec 2020 17:31:17 +0000 (09:31 -0800)
* [pidcontroller] Initial Contribution
* Incorporate review feedback No.1

Signed-off-by: Fabian Wolter <github@fabian-wolter.de>
21 files changed:
CODEOWNERS
bom/openhab-addons/pom.xml
bundles/org.openhab.automation.pidcontroller/NOTICE [new file with mode: 0644]
bundles/org.openhab.automation.pidcontroller/README.md [new file with mode: 0644]
bundles/org.openhab.automation.pidcontroller/pom.xml [new file with mode: 0644]
bundles/org.openhab.automation.pidcontroller/src/main/feature/feature.xml [new file with mode: 0644]
bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/LowpassFilter.java [new file with mode: 0644]
bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/PIDControllerConstants.java [new file with mode: 0644]
bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/PIDException.java [new file with mode: 0644]
bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/factory/PIDControllerModuleHandlerFactory.java [new file with mode: 0644]
bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/handler/PIDController.java [new file with mode: 0644]
bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/handler/PIDControllerActionHandler.java [new file with mode: 0644]
bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/handler/PIDControllerTriggerHandler.java [new file with mode: 0644]
bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/handler/PIDOutputDTO.java [new file with mode: 0644]
bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/template/PIDControllerRuleTemplate.java [new file with mode: 0644]
bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/template/PIDControllerTemplateProvider.java [new file with mode: 0644]
bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/type/PIDControllerActionType.java [new file with mode: 0644]
bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/type/PIDControllerModuleTypeProvider.java [new file with mode: 0644]
bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/type/PIDControllerTriggerType.java [new file with mode: 0644]
bundles/org.openhab.automation.pidcontroller/src/test/java/org/openhab/automation/pidcontroller/internal/LowpassFilterTest.java [new file with mode: 0644]
bundles/pom.xml

index 4f4cbc4e7a4a48bb398eb01e0aca4f1cb048b4cb..1eaef04779b8acb931246388875c5546d3422446 100644 (file)
@@ -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
index 33a19ae51f5772e3949a185543cfb2622d31dbb3..384488517f5f40b045f6d72f8d4fac80b1705781 100644 (file)
       <artifactId>org.openhab.automation.jythonscripting</artifactId>
       <version>${project.version}</version>
     </dependency>
+    <dependency>
+      <groupId>org.openhab.addons.bundles</groupId>
+      <artifactId>org.openhab.automation.pidcontroller</artifactId>
+      <version>${project.version}</version>
+    </dependency>
     <dependency>
       <groupId>org.openhab.addons.bundles</groupId>
       <artifactId>org.openhab.binding.adorne</artifactId>
diff --git a/bundles/org.openhab.automation.pidcontroller/NOTICE b/bundles/org.openhab.automation.pidcontroller/NOTICE
new file mode 100644 (file)
index 0000000..38d625e
--- /dev/null
@@ -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 (file)
index 0000000..4822a3e
--- /dev/null
@@ -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 (file)
index 0000000..dda5641
--- /dev/null
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.openhab.addons.bundles</groupId>
+    <artifactId>org.openhab.addons.reactor.bundles</artifactId>
+    <version>3.1.0-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>org.openhab.automation.pidcontroller</artifactId>
+
+  <name>openHAB Add-ons :: Bundles :: Automation :: PID Controller</name>
+
+</project>
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 (file)
index 0000000..9ff0312
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<features name="org.openhab.automation.pidcontroller-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
+       <repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
+
+       <feature name="openhab-automation-pidcontroller" description="PID Controller" version="${project.version}">
+               <feature>openhab-runtime-base</feature>
+               <bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.automation.pidcontroller/${project.version}</bundle>
+       </feature>
+</features>
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 (file)
index 0000000..338478d
--- /dev/null
@@ -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 (file)
index 0000000..bce765b
--- /dev/null
@@ -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 (file)
index 0000000..c91cf53
--- /dev/null
@@ -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 (file)
index 0000000..4db5028
--- /dev/null
@@ -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<String> 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<String> 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 (file)
index 0000000..66450f1
--- /dev/null
@@ -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 (file)
index 0000000..3e814f7
--- /dev/null
@@ -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<Action> 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<String, Object> execute(Map<String, Object> 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 (file)
index 0000000..cfeb52b
--- /dev/null
@@ -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<String> SUBSCRIBED_EVENT_TYPES = Set.of(ItemStateEvent.TYPE, ItemStateChangedEvent.TYPE);
+    private final Logger logger = LoggerFactory.getLogger(PIDControllerTriggerHandler.class);
+    private final ScheduledExecutorService scheduler = Executors
+            .newSingleThreadScheduledExecutor(new NamedThreadFactory("OH-automation-" + AUTOMATION_NAME, true));
+    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> 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<String, BigDecimal> 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<String, BigDecimal> 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<String> 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 (file)
index 0000000..d84e6c9
--- /dev/null
@@ -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 (file)
index 0000000..e43eff9
--- /dev/null
@@ -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<Trigger> triggers = List.of(ModuleBuilder.createTrigger().withId(triggerId)
+                .withTypeUID(PIDControllerTriggerHandler.MODULE_TYPE_ID).withLabel("PID Controller Trigger").build());
+
+        final Map<String, String> actionInputs = Map.of(PIDControllerActionType.INPUT,
+                triggerId + "." + PIDControllerConstants.OUTPUT);
+
+        final List<Action> 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<String> tags, List<Trigger> triggers, List<Condition> conditions,
+            List<Action> actions, List<ConfigDescriptionParameter> 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 (file)
index 0000000..2036af3
--- /dev/null
@@ -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<RuleTemplate> getTemplates(@Nullable Locale locale) {
+        return Set.of(PROVIDED_RULE_TEMPLATE);
+    }
+
+    @Override
+    public void addProviderChangeListener(ProviderChangeListener<RuleTemplate> listener) {
+        // does nothing because this provider does not change
+    }
+
+    @Override
+    public Collection<RuleTemplate> getAll() {
+        return Set.of(PROVIDED_RULE_TEMPLATE);
+    }
+
+    @Override
+    public void removeProviderChangeListener(ProviderChangeListener<RuleTemplate> 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 (file)
index 0000000..63b75a9
--- /dev/null
@@ -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<ConfigDescriptionParameter> config = List.of(outputItem, pInspectorItem, iInspectorItem, dInspectorItem,
+                eInspectorItem);
+
+        List<Input> 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<ConfigDescriptionParameter> configDescriptions, List<Input> 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 (file)
index 0000000..f1049b4
--- /dev/null
@@ -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<String, ModuleType> PROVIDED_MODULE_TYPES = Map.of(
+            PIDControllerActionHandler.MODULE_TYPE_ID, PIDControllerActionType.initialize(),
+            PIDControllerTriggerHandler.MODULE_TYPE_ID, PIDControllerTriggerType.initialize());
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public <T extends ModuleType> T getModuleType(@Nullable String UID, @Nullable Locale locale) {
+        return (T) PROVIDED_MODULE_TYPES.get(UID);
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public <T extends ModuleType> Collection<T> getModuleTypes(@Nullable Locale locale) {
+        return (Collection<T>) PROVIDED_MODULE_TYPES.values();
+    }
+
+    @Override
+    public void addProviderChangeListener(ProviderChangeListener<ModuleType> listener) {
+        // does nothing because this provider does not change
+    }
+
+    @Override
+    public Collection<ModuleType> getAll() {
+        return Collections.unmodifiableCollection(PROVIDED_MODULE_TYPES.values());
+    }
+
+    @Override
+    public void removeProviderChangeListener(ProviderChangeListener<ModuleType> 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 (file)
index 0000000..1e105d5
--- /dev/null
@@ -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<ConfigDescriptionParameter> 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<Output> outputs = List.of(output, pInspector, iInspector, dInspector, eInspector);
+
+        return new PIDControllerTriggerType(configDescriptions, outputs);
+    }
+
+    public PIDControllerTriggerType(List<ConfigDescriptionParameter> configDescriptions, List<Output> 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 (file)
index 0000000..d371997
--- /dev/null
@@ -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);
+    }
+}
index 45d609f7d3c8b008c56dfbb63285afc4b9a8cf41..5586147c43ef9bdfc83221359a2f1154cfe6871e 100644 (file)
@@ -20,6 +20,7 @@
     <!-- automation -->
     <module>org.openhab.automation.groovyscripting</module>
     <module>org.openhab.automation.jythonscripting</module>
+    <module>org.openhab.automation.pidcontroller</module>
     <!-- io -->
     <module>org.openhab.io.homekit</module>
     <module>org.openhab.io.hueemulation</module>