]> git.basschouten.com Git - openhab-addons.git/blob
c3280c1229c775555ca0b89a3921c4025c7eb41c
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.automation.pidcontroller.internal.handler;
14
15 import static org.openhab.automation.pidcontroller.internal.PIDControllerConstants.*;
16
17 import java.math.BigDecimal;
18 import java.util.HashMap;
19 import java.util.Map;
20 import java.util.Objects;
21 import java.util.Optional;
22 import java.util.Set;
23 import java.util.concurrent.Executors;
24 import java.util.concurrent.ScheduledExecutorService;
25 import java.util.concurrent.ScheduledFuture;
26 import java.util.concurrent.TimeUnit;
27
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;
54
55 /**
56  *
57  * @author Hilbrand Bouwkamp - Initial Contribution
58  * @author Fabian Wolter - Add PID debug output values
59  */
60 @NonNullByDefault
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;
76
77     public PIDControllerTriggerHandler(Trigger module, ItemRegistry itemRegistry, EventPublisher eventPublisher,
78             BundleContext bundleContext) {
79         super(module);
80
81         Configuration config = module.getConfiguration();
82
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");
85
86         try {
87             inputItem = itemRegistry.getItem(inputItemName);
88         } catch (ItemNotFoundException e) {
89             throw new IllegalArgumentException("Configured input item not found: " + inputItemName, e);
90         }
91
92         try {
93             setpointItem = itemRegistry.getItem(setpointItemName);
94         } catch (ItemNotFoundException e) {
95             throw new IllegalArgumentException("Configured setpoint item not found: " + setpointItemName, e);
96         }
97
98         String commandItemName = (String) config.get(CONFIG_COMMAND_ITEM);
99         if (commandItemName != null) {
100             commandTopic = Optional.of("openhab/items/" + commandItemName + "/statechanged");
101         } else {
102             commandTopic = Optional.empty();
103         }
104
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);
109
110         loopTimeMs = ((BigDecimal) requireNonNull(config.get(CONFIG_LOOP_TIME), CONFIG_LOOP_TIME + " is not set"))
111                 .intValue();
112
113         controller = new PIDController(kpAdjuster, kiAdjuster, kdAdjuster, kdTimeConstant);
114
115         eventFilter = event -> {
116             String topic = event.getTopic();
117
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);
122         };
123
124         eventSubscriberRegistration = bundleContext.registerService(EventSubscriber.class.getName(), this, null);
125
126         eventPublisher.post(ItemEventFactory.createCommandEvent(inputItemName, RefreshType.REFRESH));
127
128         controllerjob = scheduler.scheduleWithFixedDelay(this::calculate, 0, loopTimeMs, TimeUnit.MILLISECONDS);
129     }
130
131     private <T> T requireNonNull(T obj, String message) {
132         if (obj == null) {
133             throw new IllegalArgumentException(message);
134         }
135         return obj;
136     }
137
138     private double getDoubleFromConfig(Configuration config, String key) {
139         return ((BigDecimal) Objects.requireNonNull(config.get(key), key + " is not set")).doubleValue();
140     }
141
142     private void calculate() {
143         double input;
144         double setpoint;
145
146         try {
147             input = getItemValueAsNumber(inputItem);
148         } catch (PIDException e) {
149             logger.warn("Input item: {}", e.getMessage());
150             return;
151         }
152
153         try {
154             setpoint = getItemValueAsNumber(setpointItem);
155         } catch (PIDException e) {
156             logger.warn("Setpoint item: {}", e.getMessage());
157             return;
158         }
159
160         long now = System.currentTimeMillis();
161
162         PIDOutputDTO output = controller.calculate(input, setpoint, now - previousTimeMs, loopTimeMs);
163         previousTimeMs = now;
164
165         Map<String, BigDecimal> outputs = new HashMap<>();
166
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());
172
173         ModuleHandlerCallback localCallback = callback;
174         if (localCallback != null && localCallback instanceof TriggerHandlerCallback) {
175             ((TriggerHandlerCallback) localCallback).triggered(module, outputs);
176         } else {
177             logger.warn("No callback set");
178         }
179     }
180
181     private void putBigDecimal(Map<String, BigDecimal> map, String key, double value) {
182         map.put(key, BigDecimal.valueOf(value));
183     }
184
185     private double getItemValueAsNumber(Item item) throws PIDException {
186         State setpointState = item.getState();
187
188         if (setpointState instanceof Number) {
189             double doubleValue = ((Number) setpointState).doubleValue();
190
191             if (Double.isFinite(doubleValue)) {
192                 return doubleValue;
193             }
194         } else if (setpointState instanceof StringType) {
195             try {
196                 return Double.parseDouble(setpointState.toString());
197             } catch (NumberFormatException e) {
198                 // nothing
199             }
200         }
201         throw new PIDException(
202                 "Item type is not a number: " + setpointState.getClass().getSimpleName() + ": " + setpointState);
203     }
204
205     @Override
206     public void receive(Event event) {
207         if (event instanceof ItemStateChangedEvent) {
208             if (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);
213                 } else {
214                     logger.warn("Unknown command: {}", changedEvent.getItemState());
215                 }
216             } else {
217                 calculate();
218             }
219         }
220     }
221
222     @Override
223     public Set<String> getSubscribedEventTypes() {
224         return SUBSCRIBED_EVENT_TYPES;
225     }
226
227     @Override
228     public @Nullable EventFilter getEventFilter() {
229         return eventFilter;
230     }
231
232     @Override
233     public void dispose() {
234         eventSubscriberRegistration.unregister();
235
236         ScheduledFuture<?> localControllerjob = controllerjob;
237         if (localControllerjob != null) {
238             localControllerjob.cancel(true);
239         }
240
241         scheduler.shutdown();
242
243         super.dispose();
244     }
245 }