]> git.basschouten.com Git - openhab-addons.git/blob
6c90ac261a45efe2993ff48434881db5c117daf4
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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.Optional;
20 import java.util.Set;
21 import java.util.concurrent.TimeUnit;
22
23 import org.eclipse.jdt.annotation.NonNullByDefault;
24 import org.eclipse.jdt.annotation.Nullable;
25 import org.openhab.automation.pidcontroller.internal.PIDException;
26 import org.openhab.core.automation.ModuleHandlerCallback;
27 import org.openhab.core.automation.Trigger;
28 import org.openhab.core.automation.handler.BaseTriggerModuleHandler;
29 import org.openhab.core.automation.handler.TriggerHandlerCallback;
30 import org.openhab.core.config.core.Configuration;
31 import org.openhab.core.events.Event;
32 import org.openhab.core.events.EventFilter;
33 import org.openhab.core.events.EventPublisher;
34 import org.openhab.core.events.EventSubscriber;
35 import org.openhab.core.items.Item;
36 import org.openhab.core.items.ItemNotFoundException;
37 import org.openhab.core.items.ItemRegistry;
38 import org.openhab.core.items.events.ItemEventFactory;
39 import org.openhab.core.items.events.ItemStateChangedEvent;
40 import org.openhab.core.items.events.ItemStateEvent;
41 import org.openhab.core.library.types.DecimalType;
42 import org.openhab.core.library.types.StringType;
43 import org.openhab.core.types.RefreshType;
44 import org.openhab.core.types.State;
45 import org.openhab.core.types.UnDefType;
46 import org.osgi.framework.BundleContext;
47 import org.osgi.framework.ServiceRegistration;
48 import org.slf4j.Logger;
49 import org.slf4j.LoggerFactory;
50
51 /**
52  *
53  * @author Hilbrand Bouwkamp - Initial Contribution
54  * @author Fabian Wolter - Add PID debug output values
55  */
56 @NonNullByDefault
57 public class PIDControllerTriggerHandler extends BaseTriggerModuleHandler implements EventSubscriber {
58     public static final String MODULE_TYPE_ID = AUTOMATION_NAME + ".trigger";
59     private static final Set<String> SUBSCRIBED_EVENT_TYPES = Set.of(ItemStateEvent.TYPE, ItemStateChangedEvent.TYPE);
60     private final Logger logger = LoggerFactory.getLogger(PIDControllerTriggerHandler.class);
61     private final ServiceRegistration<?> eventSubscriberRegistration;
62     private final PIDController controller;
63     private final int loopTimeMs;
64     private long previousTimeMs = System.currentTimeMillis();
65     private Item inputItem;
66     private Item setpointItem;
67     private Optional<String> commandTopic;
68     private EventFilter eventFilter;
69     private EventPublisher eventPublisher;
70     private @Nullable String pInspector;
71     private @Nullable String iInspector;
72     private @Nullable String dInspector;
73     private @Nullable String eInspector;
74     private ItemRegistry itemRegistry;
75
76     public PIDControllerTriggerHandler(Trigger module, ItemRegistry itemRegistry, EventPublisher eventPublisher,
77             BundleContext bundleContext) {
78         super(module);
79         this.itemRegistry = itemRegistry;
80         this.eventPublisher = eventPublisher;
81
82         Configuration config = module.getConfiguration();
83
84         String inputItemName = (String) requireNonNull(config.get(CONFIG_INPUT_ITEM), "Input item is not set");
85         String setpointItemName = (String) requireNonNull(config.get(CONFIG_SETPOINT_ITEM), "Setpoint item is not set");
86
87         try {
88             inputItem = itemRegistry.getItem(inputItemName);
89         } catch (ItemNotFoundException e) {
90             throw new IllegalArgumentException("Configured input item not found: " + inputItemName, e);
91         }
92
93         try {
94             setpointItem = itemRegistry.getItem(setpointItemName);
95         } catch (ItemNotFoundException e) {
96             throw new IllegalArgumentException("Configured setpoint item not found: " + setpointItemName, e);
97         }
98
99         String commandItemName = (String) config.get(CONFIG_COMMAND_ITEM);
100         if (commandItemName != null) {
101             commandTopic = Optional.of("openhab/items/" + commandItemName + "/statechanged");
102         } else {
103             commandTopic = Optional.empty();
104         }
105
106         double kpAdjuster = getDoubleFromConfig(config, CONFIG_KP_GAIN);
107         double kiAdjuster = getDoubleFromConfig(config, CONFIG_KI_GAIN);
108         double kdAdjuster = getDoubleFromConfig(config, CONFIG_KD_GAIN);
109         double kdTimeConstant = getDoubleFromConfig(config, CONFIG_KD_TIMECONSTANT);
110         double iMinValue = getDoubleFromConfig(config, CONFIG_I_MIN);
111         double iMaxValue = getDoubleFromConfig(config, CONFIG_I_MAX);
112         pInspector = (String) config.get(P_INSPECTOR);
113         iInspector = (String) config.get(I_INSPECTOR);
114         dInspector = (String) config.get(D_INSPECTOR);
115         eInspector = (String) config.get(E_INSPECTOR);
116
117         loopTimeMs = ((BigDecimal) requireNonNull(config.get(CONFIG_LOOP_TIME), CONFIG_LOOP_TIME + " is not set"))
118                 .intValue();
119
120         double previousIntegralPart = getItemNameValueAsNumberOrZero(itemRegistry, iInspector);
121         double previousDerivativePart = getItemNameValueAsNumberOrZero(itemRegistry, dInspector);
122         double previousError = getItemNameValueAsNumberOrZero(itemRegistry, eInspector);
123
124         controller = new PIDController(kpAdjuster, kiAdjuster, kdAdjuster, kdTimeConstant, iMinValue, iMaxValue,
125                 previousIntegralPart, previousDerivativePart, previousError);
126
127         eventFilter = event -> {
128             String topic = event.getTopic();
129
130             return ("openhab/items/" + inputItemName + "/state").equals(topic)
131                     || ("openhab/items/" + inputItemName + "/statechanged").equals(topic)
132                     || ("openhab/items/" + setpointItemName + "/statechanged").equals(topic)
133                     || commandTopic.map(t -> topic.equals(t)).orElse(false);
134         };
135
136         eventSubscriberRegistration = bundleContext.registerService(EventSubscriber.class.getName(), this, null);
137
138         eventPublisher.post(ItemEventFactory.createCommandEvent(inputItemName, RefreshType.REFRESH));
139     }
140
141     @Override
142     public void setCallback(ModuleHandlerCallback callback) {
143         super.setCallback(callback);
144         getCallback().getScheduler().scheduleWithFixedDelay(this::calculate, 0, loopTimeMs, TimeUnit.MILLISECONDS);
145     }
146
147     private <T> T requireNonNull(T obj, String message) {
148         if (obj == null) {
149             throw new IllegalArgumentException(message);
150         }
151         return obj;
152     }
153
154     private double getDoubleFromConfig(Configuration config, String key) {
155         Object rawValue = config.get(key);
156
157         if (rawValue == null) {
158             return Double.NaN;
159         }
160
161         return ((BigDecimal) rawValue).doubleValue();
162     }
163
164     private void calculate() {
165         double input;
166         double setpoint;
167
168         try {
169             input = getItemValueAsNumber(inputItem);
170         } catch (PIDException e) {
171             logger.warn("Input item: {}: {}", inputItem.getName(), e.getMessage());
172             return;
173         }
174
175         try {
176             setpoint = getItemValueAsNumber(setpointItem);
177         } catch (PIDException e) {
178             logger.warn("Setpoint item: {}: {}", setpointItem.getName(), e.getMessage());
179             return;
180         }
181
182         long now = System.currentTimeMillis();
183
184         PIDOutputDTO output = controller.calculate(input, setpoint, now - previousTimeMs, loopTimeMs);
185         previousTimeMs = now;
186
187         updateItem(pInspector, output.getProportionalPart());
188         updateItem(iInspector, output.getIntegralPart());
189         updateItem(dInspector, output.getDerivativePart());
190         updateItem(eInspector, output.getError());
191
192         getCallback().triggered(module, Map.of(COMMAND, new DecimalType(output.getOutput())));
193     }
194
195     private void updateItem(@Nullable String itemName, double value) {
196         if (itemName != null) {
197             try {
198                 itemRegistry.getItem(itemName);
199                 eventPublisher.post(ItemEventFactory.createStateEvent(itemName,
200                         Double.isFinite(value) ? new DecimalType(value) : UnDefType.UNDEF));
201             } catch (ItemNotFoundException e) {
202                 logger.warn("Item doesn't exist: {}", itemName);
203             }
204         }
205     }
206
207     private TriggerHandlerCallback getCallback() {
208         ModuleHandlerCallback localCallback = callback;
209         if (localCallback != null && localCallback instanceof TriggerHandlerCallback) {
210             return (TriggerHandlerCallback) localCallback;
211         }
212
213         throw new IllegalStateException("The module callback is not set");
214     }
215
216     private double getItemNameValueAsNumberOrZero(ItemRegistry itemRegistry, @Nullable String itemName)
217             throws IllegalArgumentException {
218         double value = 0.0;
219
220         if (itemName == null) {
221             return value;
222         }
223
224         try {
225             value = getItemValueAsNumber(itemRegistry.getItem(itemName));
226             logger.debug("Item '{}' value {} recovered by PID controller", itemName, value);
227         } catch (ItemNotFoundException e) {
228             throw new IllegalArgumentException("Configured item not found: " + itemName, e);
229         } catch (PIDException e) {
230             logger.warn("Item '{}' value recovery errored: {}", itemName, e.getMessage());
231         }
232
233         return value;
234     }
235
236     private double getItemValueAsNumber(Item item) throws PIDException {
237         State setpointState = item.getState();
238
239         if (setpointState instanceof Number) {
240             double doubleValue = ((Number) setpointState).doubleValue();
241
242             if (Double.isFinite(doubleValue) && !Double.isNaN(doubleValue)) {
243                 return doubleValue;
244             }
245         } else if (setpointState instanceof StringType) {
246             try {
247                 return Double.parseDouble(setpointState.toString());
248             } catch (NumberFormatException e) {
249                 // nothing
250             }
251         }
252         throw new PIDException("Not a number: " + setpointState.getClass().getSimpleName() + ": " + setpointState);
253     }
254
255     @Override
256     public void receive(Event event) {
257         if (event instanceof ItemStateChangedEvent) {
258             if (commandTopic.isPresent() && event.getTopic().equals(commandTopic.get())) {
259                 ItemStateChangedEvent changedEvent = (ItemStateChangedEvent) event;
260                 if ("RESET".equals(changedEvent.getItemState().toString())) {
261                     controller.setIntegralResult(0);
262                     controller.setDerivativeResult(0);
263                     eventPublisher.post(ItemEventFactory.createStateEvent(changedEvent.getItemName(), UnDefType.NULL));
264                 } else if (changedEvent.getItemState() != UnDefType.NULL) {
265                     logger.warn("Unknown command: {}", changedEvent.getItemState());
266                 }
267             } else {
268                 calculate();
269             }
270         }
271     }
272
273     @Override
274     public Set<String> getSubscribedEventTypes() {
275         return SUBSCRIBED_EVENT_TYPES;
276     }
277
278     @Override
279     public @Nullable EventFilter getEventFilter() {
280         return eventFilter;
281     }
282
283     @Override
284     public void dispose() {
285         eventSubscriberRegistration.unregister();
286
287         super.dispose();
288     }
289 }