2 * Copyright (c) 2010-2022 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 controller = new PIDController(kpAdjuster, kiAdjuster, kdAdjuster, kdTimeConstant, iMinValue, iMaxValue);
122 eventFilter = event -> {
123 String topic = event.getTopic();
125 return ("openhab/items/" + inputItemName + "/state").equals(topic)
126 || ("openhab/items/" + inputItemName + "/statechanged").equals(topic)
127 || ("openhab/items/" + setpointItemName + "/statechanged").equals(topic)
128 || commandTopic.map(t -> topic.equals(t)).orElse(false);
131 eventSubscriberRegistration = bundleContext.registerService(EventSubscriber.class.getName(), this, null);
133 eventPublisher.post(ItemEventFactory.createCommandEvent(inputItemName, RefreshType.REFRESH));
137 public void setCallback(ModuleHandlerCallback callback) {
138 super.setCallback(callback);
139 getCallback().getScheduler().scheduleWithFixedDelay(this::calculate, 0, loopTimeMs, TimeUnit.MILLISECONDS);
142 private <T> T requireNonNull(T obj, String message) {
144 throw new IllegalArgumentException(message);
149 private double getDoubleFromConfig(Configuration config, String key) {
150 Object rawValue = config.get(key);
152 if (rawValue == null) {
156 return ((BigDecimal) rawValue).doubleValue();
159 private void calculate() {
164 input = getItemValueAsNumber(inputItem);
165 } catch (PIDException e) {
166 logger.warn("Input item: {}: {}", inputItem.getName(), e.getMessage());
171 setpoint = getItemValueAsNumber(setpointItem);
172 } catch (PIDException e) {
173 logger.warn("Setpoint item: {}: {}", setpointItem.getName(), e.getMessage());
177 long now = System.currentTimeMillis();
179 PIDOutputDTO output = controller.calculate(input, setpoint, now - previousTimeMs, loopTimeMs);
180 previousTimeMs = now;
182 updateItem(pInspector, output.getProportionalPart());
183 updateItem(iInspector, output.getIntegralPart());
184 updateItem(dInspector, output.getDerivativePart());
185 updateItem(eInspector, output.getError());
187 getCallback().triggered(module, Map.of(COMMAND, new DecimalType(output.getOutput())));
190 private void updateItem(@Nullable String itemName, double value) {
191 if (itemName != null) {
193 itemRegistry.getItem(itemName);
194 eventPublisher.post(ItemEventFactory.createStateEvent(itemName,
195 Double.isFinite(value) ? new DecimalType(value) : UnDefType.UNDEF));
196 } catch (ItemNotFoundException e) {
197 logger.warn("Item doesn't exist: {}", itemName);
202 private TriggerHandlerCallback getCallback() {
203 ModuleHandlerCallback localCallback = callback;
204 if (localCallback != null && localCallback instanceof TriggerHandlerCallback) {
205 return (TriggerHandlerCallback) localCallback;
208 throw new IllegalStateException("The module callback is not set");
211 private double getItemValueAsNumber(Item item) throws PIDException {
212 State setpointState = item.getState();
214 if (setpointState instanceof Number) {
215 double doubleValue = ((Number) setpointState).doubleValue();
217 if (Double.isFinite(doubleValue) && !Double.isNaN(doubleValue)) {
220 } else if (setpointState instanceof StringType) {
222 return Double.parseDouble(setpointState.toString());
223 } catch (NumberFormatException e) {
227 throw new PIDException("Not a number: " + setpointState.getClass().getSimpleName() + ": " + setpointState);
231 public void receive(Event event) {
232 if (event instanceof ItemStateChangedEvent) {
233 if (commandTopic.isPresent() && event.getTopic().equals(commandTopic.get())) {
234 ItemStateChangedEvent changedEvent = (ItemStateChangedEvent) event;
235 if ("RESET".equals(changedEvent.getItemState().toString())) {
236 controller.setIntegralResult(0);
237 controller.setDerivativeResult(0);
238 eventPublisher.post(ItemEventFactory.createStateEvent(changedEvent.getItemName(), UnDefType.NULL));
239 } else if (changedEvent.getItemState() != UnDefType.NULL) {
240 logger.warn("Unknown command: {}", changedEvent.getItemState());
249 public Set<String> getSubscribedEventTypes() {
250 return SUBSCRIBED_EVENT_TYPES;
254 public @Nullable EventFilter getEventFilter() {
259 public void dispose() {
260 eventSubscriberRegistration.unregister();