]> git.basschouten.com Git - openhab-addons.git/blob
a3de00be26596eba3ee15366159c40b7a9927f49
[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.Map;
19 import java.util.Objects;
20 import java.util.Optional;
21 import java.util.Set;
22 import java.util.concurrent.TimeUnit;
23
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;
51
52 /**
53  *
54  * @author Hilbrand Bouwkamp - Initial Contribution
55  * @author Fabian Wolter - Add PID debug output values
56  */
57 @NonNullByDefault
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
76     public PIDControllerTriggerHandler(Trigger module, ItemRegistry itemRegistry, EventPublisher eventPublisher,
77             BundleContext bundleContext) {
78         super(module);
79         this.eventPublisher = eventPublisher;
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         pInspector = (String) config.get(P_INSPECTOR);
110         iInspector = (String) config.get(I_INSPECTOR);
111         dInspector = (String) config.get(D_INSPECTOR);
112         eInspector = (String) config.get(E_INSPECTOR);
113
114         loopTimeMs = ((BigDecimal) requireNonNull(config.get(CONFIG_LOOP_TIME), CONFIG_LOOP_TIME + " is not set"))
115                 .intValue();
116
117         controller = new PIDController(kpAdjuster, kiAdjuster, kdAdjuster, kdTimeConstant);
118
119         eventFilter = event -> {
120             String topic = event.getTopic();
121
122             return ("openhab/items/" + inputItemName + "/state").equals(topic)
123                     || ("openhab/items/" + inputItemName + "/statechanged").equals(topic)
124                     || ("openhab/items/" + setpointItemName + "/statechanged").equals(topic)
125                     || commandTopic.map(t -> topic.equals(t)).orElse(false);
126         };
127
128         eventSubscriberRegistration = bundleContext.registerService(EventSubscriber.class.getName(), this, null);
129
130         eventPublisher.post(ItemEventFactory.createCommandEvent(inputItemName, RefreshType.REFRESH));
131     }
132
133     @Override
134     public void setCallback(ModuleHandlerCallback callback) {
135         super.setCallback(callback);
136         getCallback().getScheduler().scheduleWithFixedDelay(this::calculate, 0, loopTimeMs, TimeUnit.MILLISECONDS);
137     }
138
139     private <T> T requireNonNull(T obj, String message) {
140         if (obj == null) {
141             throw new IllegalArgumentException(message);
142         }
143         return obj;
144     }
145
146     private double getDoubleFromConfig(Configuration config, String key) {
147         return ((BigDecimal) Objects.requireNonNull(config.get(key), key + " is not set")).doubleValue();
148     }
149
150     private void calculate() {
151         double input;
152         double setpoint;
153
154         try {
155             input = getItemValueAsNumber(inputItem);
156         } catch (PIDException e) {
157             logger.warn("Input item: {}", e.getMessage());
158             return;
159         }
160
161         try {
162             setpoint = getItemValueAsNumber(setpointItem);
163         } catch (PIDException e) {
164             logger.warn("Setpoint item: {}", e.getMessage());
165             return;
166         }
167
168         long now = System.currentTimeMillis();
169
170         PIDOutputDTO output = controller.calculate(input, setpoint, now - previousTimeMs, loopTimeMs);
171         previousTimeMs = now;
172
173         updateItem(pInspector, output.getProportionalPart());
174         updateItem(iInspector, output.getIntegralPart());
175         updateItem(dInspector, output.getDerivativePart());
176         updateItem(eInspector, output.getError());
177
178         getCallback().triggered(module, Map.of(COMMAND, new DecimalType(output.getOutput())));
179     }
180
181     private void updateItem(@Nullable String itemName, double value) {
182         if (itemName != null) {
183             eventPublisher.post(ItemEventFactory.createCommandEvent(itemName, new DecimalType(value)));
184         }
185     }
186
187     private TriggerHandlerCallback getCallback() {
188         ModuleHandlerCallback localCallback = callback;
189         if (localCallback != null && localCallback instanceof TriggerHandlerCallback) {
190             return (TriggerHandlerCallback) localCallback;
191         }
192
193         throw new IllegalStateException("The module callback is not set");
194     }
195
196     private double getItemValueAsNumber(Item item) throws PIDException {
197         State setpointState = item.getState();
198
199         if (setpointState instanceof Number) {
200             double doubleValue = ((Number) setpointState).doubleValue();
201
202             if (Double.isFinite(doubleValue) && !Double.isNaN(doubleValue)) {
203                 return doubleValue;
204             }
205         } else if (setpointState instanceof StringType) {
206             try {
207                 return Double.parseDouble(setpointState.toString());
208             } catch (NumberFormatException e) {
209                 // nothing
210             }
211         }
212         throw new PIDException(
213                 "Item type is not a number: " + setpointState.getClass().getSimpleName() + ": " + setpointState);
214     }
215
216     @Override
217     public void receive(Event event) {
218         if (event instanceof ItemStateChangedEvent) {
219             if (commandTopic.isPresent() && event.getTopic().equals(commandTopic.get())) {
220                 ItemStateChangedEvent changedEvent = (ItemStateChangedEvent) event;
221                 if ("RESET".equals(changedEvent.getItemState().toString())) {
222                     controller.setIntegralResult(0);
223                     controller.setDerivativeResult(0);
224                     eventPublisher.post(ItemEventFactory.createStateEvent(changedEvent.getItemName(), UnDefType.NULL));
225                 } else if (changedEvent.getItemState() != UnDefType.NULL) {
226                     logger.warn("Unknown command: {}", changedEvent.getItemState());
227                 }
228             } else {
229                 calculate();
230             }
231         }
232     }
233
234     @Override
235     public Set<String> getSubscribedEventTypes() {
236         return SUBSCRIBED_EVENT_TYPES;
237     }
238
239     @Override
240     public @Nullable EventFilter getEventFilter() {
241         return eventFilter;
242     }
243
244     @Override
245     public void dispose() {
246         eventSubscriberRegistration.unregister();
247
248         super.dispose();
249     }
250 }