[pidcontroller] Initial Contribution (#9512)

* [pidcontroller] Initial Contribution
* Incorporate review feedback No.1

Signed-off-by: Fabian Wolter <github@fabian-wolter.de>
This commit is contained in:
Fabian Wolter 2020-12-28 18:31:17 +01:00 committed by GitHub
parent fe5e9b85e8
commit 924eca29f2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 1227 additions and 0 deletions

View 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

View File

@ -26,6 +26,11 @@
<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>

View File

@ -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

View File

@ -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 [ZieglerNichols 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.

View File

@ -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>

View File

@ -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>

View File

@ -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;
}
}

View File

@ -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";
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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
}
}

View File

@ -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);
}
}

View File

@ -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
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View 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>