]> git.basschouten.com Git - openhab-addons.git/blob
ef61c4bf6e2be6052c74b06decc782a6787a4aff
[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.openhab.core.types.UnDefType;
51 import org.osgi.framework.BundleContext;
52 import org.osgi.framework.ServiceRegistration;
53 import org.slf4j.Logger;
54 import org.slf4j.LoggerFactory;
55
56 /**
57  *
58  * @author Hilbrand Bouwkamp - Initial Contribution
59  * @author Fabian Wolter - Add PID debug output values
60  */
61 @NonNullByDefault
62 public class PIDControllerTriggerHandler extends BaseTriggerModuleHandler implements EventSubscriber {
63     public static final String MODULE_TYPE_ID = AUTOMATION_NAME + ".trigger";
64     private static final Set<String> SUBSCRIBED_EVENT_TYPES = Set.of(ItemStateEvent.TYPE, ItemStateChangedEvent.TYPE);
65     private final Logger logger = LoggerFactory.getLogger(PIDControllerTriggerHandler.class);
66     private final ScheduledExecutorService scheduler = Executors
67             .newSingleThreadScheduledExecutor(new NamedThreadFactory("automation-" + AUTOMATION_NAME, true));
68     private final ServiceRegistration<?> eventSubscriberRegistration;
69     private final PIDController controller;
70     private final int loopTimeMs;
71     private @Nullable ScheduledFuture<?> controllerjob;
72     private long previousTimeMs = System.currentTimeMillis();
73     private Item inputItem;
74     private Item setpointItem;
75     private Optional<String> commandTopic;
76     private EventFilter eventFilter;
77     private EventPublisher eventPublisher;
78
79     public PIDControllerTriggerHandler(Trigger module, ItemRegistry itemRegistry, EventPublisher eventPublisher,
80             BundleContext bundleContext) {
81         super(module);
82         this.eventPublisher = eventPublisher;
83
84         Configuration config = module.getConfiguration();
85
86         String inputItemName = (String) requireNonNull(config.get(CONFIG_INPUT_ITEM), "Input item is not set");
87         String setpointItemName = (String) requireNonNull(config.get(CONFIG_SETPOINT_ITEM), "Setpoint item is not set");
88
89         try {
90             inputItem = itemRegistry.getItem(inputItemName);
91         } catch (ItemNotFoundException e) {
92             throw new IllegalArgumentException("Configured input item not found: " + inputItemName, e);
93         }
94
95         try {
96             setpointItem = itemRegistry.getItem(setpointItemName);
97         } catch (ItemNotFoundException e) {
98             throw new IllegalArgumentException("Configured setpoint item not found: " + setpointItemName, e);
99         }
100
101         String commandItemName = (String) config.get(CONFIG_COMMAND_ITEM);
102         if (commandItemName != null) {
103             commandTopic = Optional.of("openhab/items/" + commandItemName + "/statechanged");
104         } else {
105             commandTopic = Optional.empty();
106         }
107
108         double kpAdjuster = getDoubleFromConfig(config, CONFIG_KP_GAIN);
109         double kiAdjuster = getDoubleFromConfig(config, CONFIG_KI_GAIN);
110         double kdAdjuster = getDoubleFromConfig(config, CONFIG_KD_GAIN);
111         double kdTimeConstant = getDoubleFromConfig(config, CONFIG_KD_TIMECONSTANT);
112
113         loopTimeMs = ((BigDecimal) requireNonNull(config.get(CONFIG_LOOP_TIME), CONFIG_LOOP_TIME + " is not set"))
114                 .intValue();
115
116         controller = new PIDController(kpAdjuster, kiAdjuster, kdAdjuster, kdTimeConstant);
117
118         eventFilter = event -> {
119             String topic = event.getTopic();
120
121             return topic.equals("openhab/items/" + inputItemName + "/state")
122                     || topic.equals("openhab/items/" + inputItemName + "/statechanged")
123                     || topic.equals("openhab/items/" + setpointItemName + "/statechanged")
124                     || commandTopic.map(t -> topic.equals(t)).orElse(false);
125         };
126
127         eventSubscriberRegistration = bundleContext.registerService(EventSubscriber.class.getName(), this, null);
128
129         eventPublisher.post(ItemEventFactory.createCommandEvent(inputItemName, RefreshType.REFRESH));
130
131         controllerjob = scheduler.scheduleWithFixedDelay(this::calculate, 0, loopTimeMs, TimeUnit.MILLISECONDS);
132     }
133
134     private <T> T requireNonNull(T obj, String message) {
135         if (obj == null) {
136             throw new IllegalArgumentException(message);
137         }
138         return obj;
139     }
140
141     private double getDoubleFromConfig(Configuration config, String key) {
142         return ((BigDecimal) Objects.requireNonNull(config.get(key), key + " is not set")).doubleValue();
143     }
144
145     private void calculate() {
146         double input;
147         double setpoint;
148
149         try {
150             input = getItemValueAsNumber(inputItem);
151         } catch (PIDException e) {
152             logger.warn("Input item: {}", e.getMessage());
153             return;
154         }
155
156         try {
157             setpoint = getItemValueAsNumber(setpointItem);
158         } catch (PIDException e) {
159             logger.warn("Setpoint item: {}", e.getMessage());
160             return;
161         }
162
163         long now = System.currentTimeMillis();
164
165         PIDOutputDTO output = controller.calculate(input, setpoint, now - previousTimeMs, loopTimeMs);
166         previousTimeMs = now;
167
168         Map<String, BigDecimal> outputs = new HashMap<>();
169
170         putBigDecimal(outputs, OUTPUT, output.getOutput());
171         putBigDecimal(outputs, P_INSPECTOR, output.getProportionalPart());
172         putBigDecimal(outputs, I_INSPECTOR, output.getIntegralPart());
173         putBigDecimal(outputs, D_INSPECTOR, output.getDerivativePart());
174         putBigDecimal(outputs, E_INSPECTOR, output.getError());
175
176         ModuleHandlerCallback localCallback = callback;
177         if (localCallback != null && localCallback instanceof TriggerHandlerCallback) {
178             ((TriggerHandlerCallback) localCallback).triggered(module, outputs);
179         } else {
180             logger.warn("No callback set");
181         }
182     }
183
184     private void putBigDecimal(Map<String, BigDecimal> map, String key, double value) {
185         map.put(key, BigDecimal.valueOf(value));
186     }
187
188     private double getItemValueAsNumber(Item item) throws PIDException {
189         State setpointState = item.getState();
190
191         if (setpointState instanceof Number) {
192             double doubleValue = ((Number) setpointState).doubleValue();
193
194             if (Double.isFinite(doubleValue)) {
195                 return doubleValue;
196             }
197         } else if (setpointState instanceof StringType) {
198             try {
199                 return Double.parseDouble(setpointState.toString());
200             } catch (NumberFormatException e) {
201                 // nothing
202             }
203         }
204         throw new PIDException(
205                 "Item type is not a number: " + setpointState.getClass().getSimpleName() + ": " + setpointState);
206     }
207
208     @Override
209     public void receive(Event event) {
210         if (event instanceof ItemStateChangedEvent) {
211             if (commandTopic.isPresent() && event.getTopic().equals(commandTopic.get())) {
212                 ItemStateChangedEvent changedEvent = (ItemStateChangedEvent) event;
213                 if ("RESET".equals(changedEvent.getItemState().toString())) {
214                     controller.setIntegralResult(0);
215                     controller.setDerivativeResult(0);
216                     eventPublisher.post(ItemEventFactory.createStateEvent(changedEvent.getItemName(), UnDefType.NULL));
217                 } else if (changedEvent.getItemState() != UnDefType.NULL) {
218                     logger.warn("Unknown command: {}", changedEvent.getItemState());
219                 }
220             } else {
221                 calculate();
222             }
223         }
224     }
225
226     @Override
227     public Set<String> getSubscribedEventTypes() {
228         return SUBSCRIBED_EVENT_TYPES;
229     }
230
231     @Override
232     public @Nullable EventFilter getEventFilter() {
233         return eventFilter;
234     }
235
236     @Override
237     public void dispose() {
238         eventSubscriberRegistration.unregister();
239
240         ScheduledFuture<?> localControllerjob = controllerjob;
241         if (localControllerjob != null) {
242             localControllerjob.cancel(true);
243         }
244
245         scheduler.shutdown();
246
247         super.dispose();
248     }
249 }