2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.automation.pidcontroller.internal.handler;
15 import static org.openhab.automation.pidcontroller.internal.PIDControllerConstants.*;
17 import java.math.BigDecimal;
19 import java.util.Optional;
21 import java.util.concurrent.TimeUnit;
23 import org.eclipse.jdt.annotation.NonNullByDefault;
24 import org.eclipse.jdt.annotation.Nullable;
25 import org.openhab.automation.pidcontroller.internal.PIDException;
26 import org.openhab.core.automation.ModuleHandlerCallback;
27 import org.openhab.core.automation.Trigger;
28 import org.openhab.core.automation.handler.BaseTriggerModuleHandler;
29 import org.openhab.core.automation.handler.TriggerHandlerCallback;
30 import org.openhab.core.config.core.Configuration;
31 import org.openhab.core.events.Event;
32 import org.openhab.core.events.EventFilter;
33 import org.openhab.core.events.EventPublisher;
34 import org.openhab.core.events.EventSubscriber;
35 import org.openhab.core.items.Item;
36 import org.openhab.core.items.ItemNotFoundException;
37 import org.openhab.core.items.ItemRegistry;
38 import org.openhab.core.items.events.ItemEventFactory;
39 import org.openhab.core.items.events.ItemStateChangedEvent;
40 import org.openhab.core.items.events.ItemStateEvent;
41 import org.openhab.core.library.types.DecimalType;
42 import org.openhab.core.library.types.StringType;
43 import org.openhab.core.types.RefreshType;
44 import org.openhab.core.types.State;
45 import org.openhab.core.types.UnDefType;
46 import org.osgi.framework.BundleContext;
47 import org.osgi.framework.ServiceRegistration;
48 import org.slf4j.Logger;
49 import org.slf4j.LoggerFactory;
53 * @author Hilbrand Bouwkamp - Initial Contribution
54 * @author Fabian Wolter - Add PID debug output values
57 public class PIDControllerTriggerHandler extends BaseTriggerModuleHandler implements EventSubscriber {
58 public static final String MODULE_TYPE_ID = AUTOMATION_NAME + ".trigger";
59 private static final Set<String> SUBSCRIBED_EVENT_TYPES = Set.of(ItemStateEvent.TYPE, ItemStateChangedEvent.TYPE);
60 private final Logger logger = LoggerFactory.getLogger(PIDControllerTriggerHandler.class);
61 private final ServiceRegistration<?> eventSubscriberRegistration;
62 private final PIDController controller;
63 private final int loopTimeMs;
64 private long previousTimeMs = System.currentTimeMillis();
65 private Item inputItem;
66 private Item setpointItem;
67 private Optional<String> commandTopic;
68 private EventFilter eventFilter;
69 private EventPublisher eventPublisher;
70 private @Nullable String pInspector;
71 private @Nullable String iInspector;
72 private @Nullable String dInspector;
73 private @Nullable String eInspector;
74 private ItemRegistry itemRegistry;
76 public PIDControllerTriggerHandler(Trigger module, ItemRegistry itemRegistry, EventPublisher eventPublisher,
77 BundleContext bundleContext) {
79 this.itemRegistry = itemRegistry;
80 this.eventPublisher = eventPublisher;
82 Configuration config = module.getConfiguration();
84 String inputItemName = (String) requireNonNull(config.get(CONFIG_INPUT_ITEM), "Input item is not set");
85 String setpointItemName = (String) requireNonNull(config.get(CONFIG_SETPOINT_ITEM), "Setpoint item is not set");
88 inputItem = itemRegistry.getItem(inputItemName);
89 } catch (ItemNotFoundException e) {
90 throw new IllegalArgumentException("Configured input item not found: " + inputItemName, e);
94 setpointItem = itemRegistry.getItem(setpointItemName);
95 } catch (ItemNotFoundException e) {
96 throw new IllegalArgumentException("Configured setpoint item not found: " + setpointItemName, e);
99 String commandItemName = (String) config.get(CONFIG_COMMAND_ITEM);
100 if (commandItemName != null) {
101 commandTopic = Optional.of("openhab/items/" + commandItemName + "/statechanged");
103 commandTopic = Optional.empty();
106 double kpAdjuster = getDoubleFromConfig(config, CONFIG_KP_GAIN);
107 double kiAdjuster = getDoubleFromConfig(config, CONFIG_KI_GAIN);
108 double kdAdjuster = getDoubleFromConfig(config, CONFIG_KD_GAIN);
109 double kdTimeConstant = getDoubleFromConfig(config, CONFIG_KD_TIMECONSTANT);
110 double iMinValue = getDoubleFromConfig(config, CONFIG_I_MIN);
111 double iMaxValue = getDoubleFromConfig(config, CONFIG_I_MAX);
112 pInspector = (String) config.get(P_INSPECTOR);
113 iInspector = (String) config.get(I_INSPECTOR);
114 dInspector = (String) config.get(D_INSPECTOR);
115 eInspector = (String) config.get(E_INSPECTOR);
117 loopTimeMs = ((BigDecimal) requireNonNull(config.get(CONFIG_LOOP_TIME), CONFIG_LOOP_TIME + " is not set"))
120 double previousIntegralPart = getItemNameValueAsNumberOrZero(itemRegistry, iInspector);
121 double previousDerivativePart = getItemNameValueAsNumberOrZero(itemRegistry, dInspector);
122 double previousError = getItemNameValueAsNumberOrZero(itemRegistry, eInspector);
124 controller = new PIDController(kpAdjuster, kiAdjuster, kdAdjuster, kdTimeConstant, iMinValue, iMaxValue,
125 previousIntegralPart, previousDerivativePart, previousError);
127 eventFilter = event -> {
128 String topic = event.getTopic();
130 return ("openhab/items/" + inputItemName + "/state").equals(topic)
131 || ("openhab/items/" + inputItemName + "/statechanged").equals(topic)
132 || ("openhab/items/" + setpointItemName + "/statechanged").equals(topic)
133 || commandTopic.map(t -> topic.equals(t)).orElse(false);
136 eventSubscriberRegistration = bundleContext.registerService(EventSubscriber.class.getName(), this, null);
138 eventPublisher.post(ItemEventFactory.createCommandEvent(inputItemName, RefreshType.REFRESH));
142 public void setCallback(ModuleHandlerCallback callback) {
143 super.setCallback(callback);
144 getCallback().getScheduler().scheduleWithFixedDelay(this::calculate, 0, loopTimeMs, TimeUnit.MILLISECONDS);
147 private <T> T requireNonNull(T obj, String message) {
149 throw new IllegalArgumentException(message);
154 private double getDoubleFromConfig(Configuration config, String key) {
155 Object rawValue = config.get(key);
157 if (rawValue == null) {
161 return ((BigDecimal) rawValue).doubleValue();
164 private void calculate() {
169 input = getItemValueAsNumber(inputItem);
170 } catch (PIDException e) {
171 logger.warn("Input item: {}: {}", inputItem.getName(), e.getMessage());
176 setpoint = getItemValueAsNumber(setpointItem);
177 } catch (PIDException e) {
178 logger.warn("Setpoint item: {}: {}", setpointItem.getName(), e.getMessage());
182 long now = System.currentTimeMillis();
184 PIDOutputDTO output = controller.calculate(input, setpoint, now - previousTimeMs, loopTimeMs);
185 previousTimeMs = now;
187 updateItem(pInspector, output.getProportionalPart());
188 updateItem(iInspector, output.getIntegralPart());
189 updateItem(dInspector, output.getDerivativePart());
190 updateItem(eInspector, output.getError());
192 getCallback().triggered(module, Map.of(COMMAND, new DecimalType(output.getOutput())));
195 private void updateItem(@Nullable String itemName, double value) {
196 if (itemName != null) {
198 itemRegistry.getItem(itemName);
199 eventPublisher.post(ItemEventFactory.createStateEvent(itemName,
200 Double.isFinite(value) ? new DecimalType(value) : UnDefType.UNDEF));
201 } catch (ItemNotFoundException e) {
202 logger.warn("Item doesn't exist: {}", itemName);
207 private TriggerHandlerCallback getCallback() {
208 ModuleHandlerCallback localCallback = callback;
209 if (localCallback != null && localCallback instanceof TriggerHandlerCallback) {
210 return (TriggerHandlerCallback) localCallback;
213 throw new IllegalStateException("The module callback is not set");
216 private double getItemNameValueAsNumberOrZero(ItemRegistry itemRegistry, @Nullable String itemName)
217 throws IllegalArgumentException {
220 if (itemName == null) {
225 value = getItemValueAsNumber(itemRegistry.getItem(itemName));
226 logger.debug("Item '{}' value {} recovered by PID controller", itemName, value);
227 } catch (ItemNotFoundException e) {
228 throw new IllegalArgumentException("Configured item not found: " + itemName, e);
229 } catch (PIDException e) {
230 logger.warn("Item '{}' value recovery errored: {}", itemName, e.getMessage());
236 private double getItemValueAsNumber(Item item) throws PIDException {
237 State setpointState = item.getState();
239 if (setpointState instanceof Number) {
240 double doubleValue = ((Number) setpointState).doubleValue();
242 if (Double.isFinite(doubleValue) && !Double.isNaN(doubleValue)) {
245 } else if (setpointState instanceof StringType) {
247 return Double.parseDouble(setpointState.toString());
248 } catch (NumberFormatException e) {
252 throw new PIDException("Not a number: " + setpointState.getClass().getSimpleName() + ": " + setpointState);
256 public void receive(Event event) {
257 if (event instanceof ItemStateChangedEvent) {
258 if (commandTopic.isPresent() && event.getTopic().equals(commandTopic.get())) {
259 ItemStateChangedEvent changedEvent = (ItemStateChangedEvent) event;
260 if ("RESET".equals(changedEvent.getItemState().toString())) {
261 controller.setIntegralResult(0);
262 controller.setDerivativeResult(0);
263 eventPublisher.post(ItemEventFactory.createStateEvent(changedEvent.getItemName(), UnDefType.NULL));
264 } else if (changedEvent.getItemState() != UnDefType.NULL) {
265 logger.warn("Unknown command: {}", changedEvent.getItemState());
274 public Set<String> getSubscribedEventTypes() {
275 return SUBSCRIBED_EVENT_TYPES;
279 public @Nullable EventFilter getEventFilter() {
284 public void dispose() {
285 eventSubscriberRegistration.unregister();