2 * Copyright (c) 2010-2021 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;
18 import java.util.HashMap;
20 import java.util.Objects;
21 import java.util.Optional;
23 import java.util.concurrent.Executors;
24 import java.util.concurrent.ScheduledExecutorService;
25 import java.util.concurrent.ScheduledFuture;
26 import java.util.concurrent.TimeUnit;
28 import org.eclipse.jdt.annotation.NonNullByDefault;
29 import org.eclipse.jdt.annotation.Nullable;
30 import org.openhab.automation.pidcontroller.internal.PIDException;
31 import org.openhab.core.automation.ModuleHandlerCallback;
32 import org.openhab.core.automation.Trigger;
33 import org.openhab.core.automation.handler.BaseTriggerModuleHandler;
34 import org.openhab.core.automation.handler.TriggerHandlerCallback;
35 import org.openhab.core.common.NamedThreadFactory;
36 import org.openhab.core.config.core.Configuration;
37 import org.openhab.core.events.Event;
38 import org.openhab.core.events.EventFilter;
39 import org.openhab.core.events.EventPublisher;
40 import org.openhab.core.events.EventSubscriber;
41 import org.openhab.core.items.Item;
42 import org.openhab.core.items.ItemNotFoundException;
43 import org.openhab.core.items.ItemRegistry;
44 import org.openhab.core.items.events.ItemEventFactory;
45 import org.openhab.core.items.events.ItemStateChangedEvent;
46 import org.openhab.core.items.events.ItemStateEvent;
47 import org.openhab.core.library.types.StringType;
48 import org.openhab.core.types.RefreshType;
49 import org.openhab.core.types.State;
50 import org.osgi.framework.BundleContext;
51 import org.osgi.framework.ServiceRegistration;
52 import org.slf4j.Logger;
53 import org.slf4j.LoggerFactory;
57 * @author Hilbrand Bouwkamp - Initial Contribution
58 * @author Fabian Wolter - Add PID debug output values
61 public class PIDControllerTriggerHandler extends BaseTriggerModuleHandler implements EventSubscriber {
62 public static final String MODULE_TYPE_ID = AUTOMATION_NAME + ".trigger";
63 private static final Set<String> SUBSCRIBED_EVENT_TYPES = Set.of(ItemStateEvent.TYPE, ItemStateChangedEvent.TYPE);
64 private final Logger logger = LoggerFactory.getLogger(PIDControllerTriggerHandler.class);
65 private final ScheduledExecutorService scheduler = Executors
66 .newSingleThreadScheduledExecutor(new NamedThreadFactory("automation-" + AUTOMATION_NAME, true));
67 private final ServiceRegistration<?> eventSubscriberRegistration;
68 private final PIDController controller;
69 private final int loopTimeMs;
70 private @Nullable ScheduledFuture<?> controllerjob;
71 private long previousTimeMs = System.currentTimeMillis();
72 private Item inputItem;
73 private Item setpointItem;
74 private Optional<String> commandTopic;
75 private EventFilter eventFilter;
77 public PIDControllerTriggerHandler(Trigger module, ItemRegistry itemRegistry, EventPublisher eventPublisher,
78 BundleContext bundleContext) {
81 Configuration config = module.getConfiguration();
83 String inputItemName = (String) requireNonNull(config.get(CONFIG_INPUT_ITEM), "Input item is not set");
84 String setpointItemName = (String) requireNonNull(config.get(CONFIG_SETPOINT_ITEM), "Setpoint item is not set");
87 inputItem = itemRegistry.getItem(inputItemName);
88 } catch (ItemNotFoundException e) {
89 throw new IllegalArgumentException("Configured input item not found: " + inputItemName, e);
93 setpointItem = itemRegistry.getItem(setpointItemName);
94 } catch (ItemNotFoundException e) {
95 throw new IllegalArgumentException("Configured setpoint item not found: " + setpointItemName, e);
98 String commandItemName = (String) config.get(CONFIG_COMMAND_ITEM);
99 if (commandItemName != null) {
100 commandTopic = Optional.of("openhab/items/" + commandItemName + "/statechanged");
102 commandTopic = Optional.empty();
105 double kpAdjuster = getDoubleFromConfig(config, CONFIG_KP_GAIN);
106 double kiAdjuster = getDoubleFromConfig(config, CONFIG_KI_GAIN);
107 double kdAdjuster = getDoubleFromConfig(config, CONFIG_KD_GAIN);
108 double kdTimeConstant = getDoubleFromConfig(config, CONFIG_KD_TIMECONSTANT);
110 loopTimeMs = ((BigDecimal) requireNonNull(config.get(CONFIG_LOOP_TIME), CONFIG_LOOP_TIME + " is not set"))
113 controller = new PIDController(kpAdjuster, kiAdjuster, kdAdjuster, kdTimeConstant);
115 eventFilter = event -> {
116 String topic = event.getTopic();
118 return topic.equals("openhab/items/" + inputItemName + "/state")
119 || topic.equals("openhab/items/" + inputItemName + "/statechanged")
120 || topic.equals("openhab/items/" + setpointItemName + "/statechanged")
121 || commandTopic.map(t -> topic.equals(t)).orElse(false);
124 eventSubscriberRegistration = bundleContext.registerService(EventSubscriber.class.getName(), this, null);
126 eventPublisher.post(ItemEventFactory.createCommandEvent(inputItemName, RefreshType.REFRESH));
128 controllerjob = scheduler.scheduleWithFixedDelay(this::calculate, 0, loopTimeMs, TimeUnit.MILLISECONDS);
131 private <T> T requireNonNull(T obj, String message) {
133 throw new IllegalArgumentException(message);
138 private double getDoubleFromConfig(Configuration config, String key) {
139 return ((BigDecimal) Objects.requireNonNull(config.get(key), key + " is not set")).doubleValue();
142 private void calculate() {
147 input = getItemValueAsNumber(inputItem);
148 } catch (PIDException e) {
149 logger.warn("Input item: {}", e.getMessage());
154 setpoint = getItemValueAsNumber(setpointItem);
155 } catch (PIDException e) {
156 logger.warn("Setpoint item: {}", e.getMessage());
160 long now = System.currentTimeMillis();
162 PIDOutputDTO output = controller.calculate(input, setpoint, now - previousTimeMs, loopTimeMs);
163 previousTimeMs = now;
165 Map<String, BigDecimal> outputs = new HashMap<>();
167 putBigDecimal(outputs, OUTPUT, output.getOutput());
168 putBigDecimal(outputs, P_INSPECTOR, output.getProportionalPart());
169 putBigDecimal(outputs, I_INSPECTOR, output.getIntegralPart());
170 putBigDecimal(outputs, D_INSPECTOR, output.getDerivativePart());
171 putBigDecimal(outputs, E_INSPECTOR, output.getError());
173 ModuleHandlerCallback localCallback = callback;
174 if (localCallback != null && localCallback instanceof TriggerHandlerCallback) {
175 ((TriggerHandlerCallback) localCallback).triggered(module, outputs);
177 logger.warn("No callback set");
181 private void putBigDecimal(Map<String, BigDecimal> map, String key, double value) {
182 map.put(key, BigDecimal.valueOf(value));
185 private double getItemValueAsNumber(Item item) throws PIDException {
186 State setpointState = item.getState();
188 if (setpointState instanceof Number) {
189 double doubleValue = ((Number) setpointState).doubleValue();
191 if (Double.isFinite(doubleValue)) {
194 } else if (setpointState instanceof StringType) {
196 return Double.parseDouble(setpointState.toString());
197 } catch (NumberFormatException e) {
201 throw new PIDException(
202 "Item type is not a number: " + setpointState.getClass().getSimpleName() + ": " + setpointState);
206 public void receive(Event event) {
207 if (event instanceof ItemStateChangedEvent) {
208 if (commandTopic.isPresent() && event.getTopic().equals(commandTopic.get())) {
209 ItemStateChangedEvent changedEvent = (ItemStateChangedEvent) event;
210 if ("RESET".equals(changedEvent.getItemState().toString())) {
211 controller.setIntegralResult(0);
212 controller.setDerivativeResult(0);
214 logger.warn("Unknown command: {}", changedEvent.getItemState());
223 public Set<String> getSubscribedEventTypes() {
224 return SUBSCRIBED_EVENT_TYPES;
228 public @Nullable EventFilter getEventFilter() {
233 public void dispose() {
234 eventSubscriberRegistration.unregister();
236 ScheduledFuture<?> localControllerjob = controllerjob;
237 if (localControllerjob != null) {
238 localControllerjob.cancel(true);
241 scheduler.shutdown();