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.Objects;
20 import java.util.Optional;
22 import java.util.concurrent.TimeUnit;
24 import org.eclipse.jdt.annotation.NonNullByDefault;
25 import org.eclipse.jdt.annotation.Nullable;
26 import org.openhab.automation.pidcontroller.internal.PIDException;
27 import org.openhab.core.automation.ModuleHandlerCallback;
28 import org.openhab.core.automation.Trigger;
29 import org.openhab.core.automation.handler.BaseTriggerModuleHandler;
30 import org.openhab.core.automation.handler.TriggerHandlerCallback;
31 import org.openhab.core.config.core.Configuration;
32 import org.openhab.core.events.Event;
33 import org.openhab.core.events.EventFilter;
34 import org.openhab.core.events.EventPublisher;
35 import org.openhab.core.events.EventSubscriber;
36 import org.openhab.core.items.Item;
37 import org.openhab.core.items.ItemNotFoundException;
38 import org.openhab.core.items.ItemRegistry;
39 import org.openhab.core.items.events.ItemEventFactory;
40 import org.openhab.core.items.events.ItemStateChangedEvent;
41 import org.openhab.core.items.events.ItemStateEvent;
42 import org.openhab.core.library.types.DecimalType;
43 import org.openhab.core.library.types.StringType;
44 import org.openhab.core.types.RefreshType;
45 import org.openhab.core.types.State;
46 import org.openhab.core.types.UnDefType;
47 import org.osgi.framework.BundleContext;
48 import org.osgi.framework.ServiceRegistration;
49 import org.slf4j.Logger;
50 import org.slf4j.LoggerFactory;
54 * @author Hilbrand Bouwkamp - Initial Contribution
55 * @author Fabian Wolter - Add PID debug output values
58 public class PIDControllerTriggerHandler extends BaseTriggerModuleHandler implements EventSubscriber {
59 public static final String MODULE_TYPE_ID = AUTOMATION_NAME + ".trigger";
60 private static final Set<String> SUBSCRIBED_EVENT_TYPES = Set.of(ItemStateEvent.TYPE, ItemStateChangedEvent.TYPE);
61 private final Logger logger = LoggerFactory.getLogger(PIDControllerTriggerHandler.class);
62 private final ServiceRegistration<?> eventSubscriberRegistration;
63 private final PIDController controller;
64 private final int loopTimeMs;
65 private long previousTimeMs = System.currentTimeMillis();
66 private Item inputItem;
67 private Item setpointItem;
68 private Optional<String> commandTopic;
69 private EventFilter eventFilter;
70 private EventPublisher eventPublisher;
71 private @Nullable String pInspector;
72 private @Nullable String iInspector;
73 private @Nullable String dInspector;
74 private @Nullable String eInspector;
75 private ItemRegistry itemRegistry;
77 public PIDControllerTriggerHandler(Trigger module, ItemRegistry itemRegistry, EventPublisher eventPublisher,
78 BundleContext bundleContext) {
80 this.itemRegistry = itemRegistry;
81 this.eventPublisher = eventPublisher;
83 Configuration config = module.getConfiguration();
85 String inputItemName = (String) requireNonNull(config.get(CONFIG_INPUT_ITEM), "Input item is not set");
86 String setpointItemName = (String) requireNonNull(config.get(CONFIG_SETPOINT_ITEM), "Setpoint item is not set");
89 inputItem = itemRegistry.getItem(inputItemName);
90 } catch (ItemNotFoundException e) {
91 throw new IllegalArgumentException("Configured input item not found: " + inputItemName, e);
95 setpointItem = itemRegistry.getItem(setpointItemName);
96 } catch (ItemNotFoundException e) {
97 throw new IllegalArgumentException("Configured setpoint item not found: " + setpointItemName, e);
100 String commandItemName = (String) config.get(CONFIG_COMMAND_ITEM);
101 if (commandItemName != null) {
102 commandTopic = Optional.of("openhab/items/" + commandItemName + "/statechanged");
104 commandTopic = Optional.empty();
107 double kpAdjuster = getDoubleFromConfig(config, CONFIG_KP_GAIN);
108 double kiAdjuster = getDoubleFromConfig(config, CONFIG_KI_GAIN);
109 double kdAdjuster = getDoubleFromConfig(config, CONFIG_KD_GAIN);
110 double kdTimeConstant = getDoubleFromConfig(config, CONFIG_KD_TIMECONSTANT);
111 pInspector = (String) config.get(P_INSPECTOR);
112 iInspector = (String) config.get(I_INSPECTOR);
113 dInspector = (String) config.get(D_INSPECTOR);
114 eInspector = (String) config.get(E_INSPECTOR);
116 loopTimeMs = ((BigDecimal) requireNonNull(config.get(CONFIG_LOOP_TIME), CONFIG_LOOP_TIME + " is not set"))
119 controller = new PIDController(kpAdjuster, kiAdjuster, kdAdjuster, kdTimeConstant);
121 eventFilter = event -> {
122 String topic = event.getTopic();
124 return ("openhab/items/" + inputItemName + "/state").equals(topic)
125 || ("openhab/items/" + inputItemName + "/statechanged").equals(topic)
126 || ("openhab/items/" + setpointItemName + "/statechanged").equals(topic)
127 || commandTopic.map(t -> topic.equals(t)).orElse(false);
130 eventSubscriberRegistration = bundleContext.registerService(EventSubscriber.class.getName(), this, null);
132 eventPublisher.post(ItemEventFactory.createCommandEvent(inputItemName, RefreshType.REFRESH));
136 public void setCallback(ModuleHandlerCallback callback) {
137 super.setCallback(callback);
138 getCallback().getScheduler().scheduleWithFixedDelay(this::calculate, 0, loopTimeMs, TimeUnit.MILLISECONDS);
141 private <T> T requireNonNull(T obj, String message) {
143 throw new IllegalArgumentException(message);
148 private double getDoubleFromConfig(Configuration config, String key) {
149 return ((BigDecimal) Objects.requireNonNull(config.get(key), key + " is not set")).doubleValue();
152 private void calculate() {
157 input = getItemValueAsNumber(inputItem);
158 } catch (PIDException e) {
159 logger.warn("Input item: {}: {}", inputItem.getName(), e.getMessage());
164 setpoint = getItemValueAsNumber(setpointItem);
165 } catch (PIDException e) {
166 logger.warn("Setpoint item: {}: {}", setpointItem.getName(), e.getMessage());
170 long now = System.currentTimeMillis();
172 PIDOutputDTO output = controller.calculate(input, setpoint, now - previousTimeMs, loopTimeMs);
173 previousTimeMs = now;
175 updateItem(pInspector, output.getProportionalPart());
176 updateItem(iInspector, output.getIntegralPart());
177 updateItem(dInspector, output.getDerivativePart());
178 updateItem(eInspector, output.getError());
180 getCallback().triggered(module, Map.of(COMMAND, new DecimalType(output.getOutput())));
183 private void updateItem(@Nullable String itemName, double value) {
184 if (itemName != null) {
186 itemRegistry.getItem(itemName);
187 eventPublisher.post(ItemEventFactory.createCommandEvent(itemName, new DecimalType(value)));
188 } catch (ItemNotFoundException e) {
189 logger.warn("Item doesn't exist: {}", itemName);
194 private TriggerHandlerCallback getCallback() {
195 ModuleHandlerCallback localCallback = callback;
196 if (localCallback != null && localCallback instanceof TriggerHandlerCallback) {
197 return (TriggerHandlerCallback) localCallback;
200 throw new IllegalStateException("The module callback is not set");
203 private double getItemValueAsNumber(Item item) throws PIDException {
204 State setpointState = item.getState();
206 if (setpointState instanceof Number) {
207 double doubleValue = ((Number) setpointState).doubleValue();
209 if (Double.isFinite(doubleValue) && !Double.isNaN(doubleValue)) {
212 } else if (setpointState instanceof StringType) {
214 return Double.parseDouble(setpointState.toString());
215 } catch (NumberFormatException e) {
219 throw new PIDException("Not a number: " + setpointState.getClass().getSimpleName() + ": " + setpointState);
223 public void receive(Event event) {
224 if (event instanceof ItemStateChangedEvent) {
225 if (commandTopic.isPresent() && event.getTopic().equals(commandTopic.get())) {
226 ItemStateChangedEvent changedEvent = (ItemStateChangedEvent) event;
227 if ("RESET".equals(changedEvent.getItemState().toString())) {
228 controller.setIntegralResult(0);
229 controller.setDerivativeResult(0);
230 eventPublisher.post(ItemEventFactory.createStateEvent(changedEvent.getItemName(), UnDefType.NULL));
231 } else if (changedEvent.getItemState() != UnDefType.NULL) {
232 logger.warn("Unknown command: {}", changedEvent.getItemState());
241 public Set<String> getSubscribedEventTypes() {
242 return SUBSCRIBED_EVENT_TYPES;
246 public @Nullable EventFilter getEventFilter() {
251 public void dispose() {
252 eventSubscriberRegistration.unregister();