]> git.basschouten.com Git - openhab-addons.git/blob
221d111022b7ef40dd18ed7be24c8b78ab668cf0
[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.binding.networkupstools.internal;
14
15 import java.time.Duration;
16 import java.util.ArrayList;
17 import java.util.Collection;
18 import java.util.Collections;
19 import java.util.HashMap;
20 import java.util.List;
21 import java.util.Map;
22 import java.util.concurrent.ScheduledFuture;
23 import java.util.concurrent.TimeUnit;
24
25 import org.eclipse.jdt.annotation.NonNullByDefault;
26 import org.eclipse.jdt.annotation.Nullable;
27 import org.openhab.binding.networkupstools.internal.NUTBindingConstants.Parameters;
28 import org.openhab.binding.networkupstools.internal.nut.NutApi;
29 import org.openhab.binding.networkupstools.internal.nut.NutException;
30 import org.openhab.binding.networkupstools.internal.nut.NutFunction;
31 import org.openhab.core.cache.ExpiringCache;
32 import org.openhab.core.library.CoreItemFactory;
33 import org.openhab.core.library.types.DecimalType;
34 import org.openhab.core.library.types.OnOffType;
35 import org.openhab.core.library.types.QuantityType;
36 import org.openhab.core.library.types.StringType;
37 import org.openhab.core.thing.Channel;
38 import org.openhab.core.thing.ChannelUID;
39 import org.openhab.core.thing.Thing;
40 import org.openhab.core.thing.ThingStatus;
41 import org.openhab.core.thing.ThingStatusDetail;
42 import org.openhab.core.thing.binding.BaseThingHandler;
43 import org.openhab.core.thing.binding.ThingHandlerService;
44 import org.openhab.core.thing.binding.builder.ThingBuilder;
45 import org.openhab.core.types.Command;
46 import org.openhab.core.types.RefreshType;
47 import org.openhab.core.types.State;
48 import org.openhab.core.types.UnDefType;
49 import org.slf4j.Logger;
50 import org.slf4j.LoggerFactory;
51
52 /**
53  * The {@link NUTHandler} is responsible for handling commands, which are
54  * sent to one of the channels.
55  *
56  * @author Hilbrand Bouwkamp - Initial contribution
57  */
58 @NonNullByDefault
59 public class NUTHandler extends BaseThingHandler {
60     private static final int REFRESH_RATE_SECONDS = 3;
61
62     private final Logger logger = LoggerFactory.getLogger(NUTHandler.class);
63     /**
64      * Map to cache user configured channels with their configuration. Channels are dynamically created at
65      * initialization phase of the thing.
66      */
67     private final Map<ChannelUID, NUTDynamicChannelConfiguration> userChannelToNutMap = new HashMap<>();
68     /**
69      * Cache of the UPS status. When expired makes a call to the NUT server is done to get the actual status. Expires at
70      * the
71      * short time refresh rate. Used to avoid triggering multiple calls to the server in a short time frame.
72      */
73     private final ExpiringCache<String> upsStatusCache = new ExpiringCache<>(Duration.ofSeconds(REFRESH_RATE_SECONDS),
74             this::retrieveUpsStatus);
75     /**
76      * Cache of the NUT variables. When expired makes a call to the NUT server is done to get the variables. Expires at
77      * the short time refresh rate. Used to avoid triggering multiple calls to the server in a short time frame.
78      */
79     private final ExpiringCache<Map<String, String>> variablesCache = new ExpiringCache<>(
80             Duration.ofSeconds(REFRESH_RATE_SECONDS), this::retrieveVariables);
81     /**
82      * Cache used to manage update frequency of the thing properties. The properties are NUT variables that don't
83      * change much or not at all. So updating can be done on a larger time frame. A call to get this cache will trigger
84      * updating the properties when the cache is expired.
85      */
86     private final ExpiringCache<Boolean> refreshPropertiesCache = new ExpiringCache<>(Duration.ofHours(1),
87             this::updateProperties);
88     private final ChannelUID upsStatusChannelUID;
89
90     private @Nullable NUTChannelTypeProvider channelTypeProvider;
91     private @Nullable NUTDynamicChannelFactory dynamicChannelFactory;
92     private @Nullable NUTConfiguration config;
93     private @Nullable ScheduledFuture<?> poller;
94     /**
95      * Cache used to manage the update frequency of the thing channels. The channels are updated based on the user
96      * configured refresh rate. The cache is called in the status update scheduled task and when the cache expires at
97      * the user configured refresh rate it will trigger an update of the channels. This way no separate scheduled task
98      * is needed.
99      */
100     private @Nullable ExpiringCache<Boolean> refreshVariablesCache;
101     private @Nullable NutApi nutApi;
102
103     /**
104      * Keep track of the last ups status to avoid updating the status every 3 seconds when nothing changed.
105      */
106     private String lastUpsStatus = "";
107
108     public NUTHandler(final Thing thing) {
109         super(thing);
110         upsStatusChannelUID = new ChannelUID(getThing().getUID(), NutName.UPS_STATUS.getChannelId());
111     }
112
113     @Override
114     public Collection<Class<? extends ThingHandlerService>> getServices() {
115         return Collections.singleton(NUTChannelTypeProvider.class);
116     }
117
118     public void setChannelTypeProvider(final NUTChannelTypeProvider channelTypeProvider) {
119         this.channelTypeProvider = channelTypeProvider;
120         dynamicChannelFactory = new NUTDynamicChannelFactory(channelTypeProvider);
121     }
122
123     @Override
124     public void handleCommand(final ChannelUID channelUID, final Command command) {
125         if (command instanceof RefreshType) {
126             final Channel channel = getThing().getChannel(channelUID);
127
128             if (channel == null) {
129                 logger.info("Trying to update a none existing channel: {}", channelUID);
130             } else {
131                 updateChannel(channel, variablesCache.getValue());
132             }
133         }
134     }
135
136     @Override
137     public void initialize() {
138         final NUTConfiguration config = getConfigAs(NUTConfiguration.class);
139
140         this.config = config;
141         updateStatus(ThingStatus.UNKNOWN);
142         initDynamicChannels();
143         poller = scheduler.scheduleWithFixedDelay(this::refreshStatus, 0, REFRESH_RATE_SECONDS, TimeUnit.SECONDS);
144         refreshVariablesCache = new ExpiringCache<>(Duration.ofSeconds(config.refresh), this::updateRefreshVariables);
145         nutApi = new NutApi(config.host, config.port, config.username, config.password);
146     }
147
148     @Override
149     public void dispose() {
150         if (nutApi != null) {
151             nutApi.close();
152         }
153         final ScheduledFuture<?> localPoller = poller;
154
155         if (localPoller != null && !localPoller.isCancelled()) {
156             localPoller.cancel(true);
157             poller = null;
158         }
159     }
160
161     /**
162      * Initializes any channels configured by the user by creating complementary channel types and recreate the channels
163      * of the thing.
164      */
165     private void initDynamicChannels() {
166         final NUTChannelTypeProvider localChannelTypeProvider = channelTypeProvider;
167         final NUTDynamicChannelFactory localDynamicChannelFactory = dynamicChannelFactory;
168
169         if (localChannelTypeProvider == null || localDynamicChannelFactory == null) {
170             return;
171         }
172         final List<Channel> updatedChannels = new ArrayList<>();
173         boolean rebuildChannels = false;
174
175         for (final Channel channel : thing.getChannels()) {
176             if (channel.getConfiguration().getProperties().isEmpty()) {
177                 updatedChannels.add(channel);
178             } else {
179                 // If the channel has a custom created channel type id the channel should be recreated.
180                 // This is specific for Quantity type channels created in thing files.
181                 final boolean customChannel = channel.getChannelTypeUID() == null;
182                 final NUTDynamicChannelConfiguration channelConfig = channel.getConfiguration()
183                         .as(NUTDynamicChannelConfiguration.class);
184                 final Channel dynamicChannel;
185
186                 rebuildChannels = customChannel;
187                 if (customChannel) {
188                     dynamicChannel = localDynamicChannelFactory.createChannel(channel, channelConfig);
189
190                     if (dynamicChannel == null) {
191                         logger.debug("Could not initialize the dynamic channel '{}'. This channel will be ignored ",
192                                 channel.getUID());
193                         continue;
194                     } else {
195                         logger.debug("Updating channel '{}' with dynamic channelType settings: {}", channel.getUID(),
196                                 dynamicChannel.getChannelTypeUID());
197                     }
198                 } else {
199                     logger.debug("Mapping standard dynamic channel '{}' with dynamic channelType settings: {}",
200                             channel.getUID(), channel.getChannelTypeUID());
201                     dynamicChannel = channel;
202                 }
203                 userChannelToNutMap.put(channel.getUID(), channelConfig);
204                 updatedChannels.add(dynamicChannel);
205             }
206         }
207         if (rebuildChannels) {
208             final ThingBuilder thingBuilder = editThing();
209             thingBuilder.withChannels(updatedChannels);
210             updateThing(thingBuilder.build());
211         }
212     }
213
214     /**
215      * Method called by the scheduled task that checks for the active status of the ups.
216      */
217     private void refreshStatus() {
218         try {
219             final String state = upsStatusCache.getValue();
220             final ExpiringCache<Boolean> localVariablesRefreshCache = refreshVariablesCache;
221
222             if (!lastUpsStatus.equals(state)) {
223                 if (isLinked(upsStatusChannelUID)) {
224                     updateState(upsStatusChannelUID, state == null ? UnDefType.UNDEF : new StringType(state));
225                 }
226                 lastUpsStatus = state == null ? "" : state;
227                 if (localVariablesRefreshCache != null) {
228                     localVariablesRefreshCache.invalidateValue();
229                 }
230             }
231             // Just call a get on variables. If the cache is expired it will trigger an update of the channels.
232             if (localVariablesRefreshCache != null) {
233                 localVariablesRefreshCache.getValue();
234             }
235         } catch (final RuntimeException e) {
236             logger.debug("Updating ups status failed: ", e);
237             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
238         }
239     }
240
241     /**
242      * This method is triggered when the cache {@link #refreshVariablesCache} is expired.
243      *
244      * @return returns true if success and false on error
245      */
246     private boolean updateRefreshVariables() {
247         logger.trace("Calling updateRefreshVariables {}", thing.getUID());
248         try {
249             final Map<String, String> variables = variablesCache.getValue();
250
251             if (variables == null) {
252                 logger.trace("No data from NUT server received.");
253                 return false;
254             }
255             logger.trace("Updating status of linked channels.");
256             for (final Channel channel : getThing().getChannels()) {
257                 final ChannelUID uid = channel.getUID();
258
259                 if (isLinked(uid)) {
260                     updateChannel(channel, variables);
261                 }
262             }
263             // Call getValue to trigger cache refreshing
264             refreshPropertiesCache.getValue();
265             if ((thing.getStatus() == ThingStatus.OFFLINE || thing.getStatus() == ThingStatus.UNKNOWN)
266                     && thing.getStatus() != ThingStatus.ONLINE) {
267                 updateStatus(ThingStatus.ONLINE);
268             }
269             return true;
270         } catch (final RuntimeException e) {
271             logger.debug("Refresh Network UPS Tools failed: ", e);
272             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
273             return false;
274         }
275     }
276
277     /**
278      * Method that retrieves the ups status from the NUT server.
279      *
280      * @return status of the UPS or null if it couldn't be determined
281      */
282     private @Nullable String retrieveUpsStatus() {
283         final NutApi localNutApi = nutApi;
284
285         if (localNutApi == null) {
286             return null;
287         }
288         return wrappedNutApiCall(device -> localNutApi.getVariable(device, NutName.UPS_STATUS.getName()), "UPS status");
289     }
290
291     /**
292      * Method that retrieves all variables from the NUT server.
293      *
294      * @return variables retrieved send by the NUT server or null if it couldn't be determined
295      */
296     private @Nullable Map<String, String> retrieveVariables() {
297         final NutApi localNutApi = nutApi;
298
299         if (localNutApi == null) {
300             return null;
301         }
302         return wrappedNutApiCall(localNutApi::getVariables, "NUT variables");
303     }
304
305     /**
306      * Convenience method that wraps the call to the api and handles exceptions.
307      *
308      * @param <T> Return type of the call to the api
309      * @param nutApiFunction function that will be called
310      * @return the value returned by the api call or null in case of an error
311      */
312     private @Nullable <T> T wrappedNutApiCall(final NutFunction<String, T> nutApiFunction, String logging) {
313         try {
314             final NUTConfiguration localConfig = config;
315
316             if (localConfig == null) {
317                 return null;
318             }
319             logger.trace("Get {} from server for thing: {}({})", logging, thing.getLabel(), thing.getUID());
320             return nutApiFunction.apply(localConfig.device);
321         } catch (final NutException e) {
322             logger.debug("Refresh Network UPS Tools failed: ", e);
323             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
324             return null;
325         }
326     }
327
328     /**
329      * Updates the thing properties when the cache {@link #refreshPropertiesCache} is expired.
330      *
331      * @return returns true if success and false on error
332      */
333     private Boolean updateProperties() {
334         try {
335             final Map<String, String> variables = variablesCache.getValue();
336
337             if (variables != null) {
338                 final Map<String, String> properties = editProperties();
339
340                 for (final Parameters param : NUTBindingConstants.Parameters.values()) {
341                     final String value = variables.get(param.getNutName());
342
343                     if (value == null) {
344                         logger.debug(
345                                 "Variable '{}' intented as property for thing {}({}) is not available in the NUT data.",
346                                 param.getNutName(), thing.getLabel(), thing.getUID());
347                     } else {
348                         properties.put(param.getNutName(), value);
349                     }
350                 }
351                 updateProperties(properties);
352             }
353             return Boolean.TRUE;
354         } catch (final RuntimeException e) {
355             logger.debug("Updating parameters failed: ", e);
356             return Boolean.FALSE;
357         }
358     }
359
360     private void updateChannel(final Channel channel, @Nullable final Map<String, String> variables) {
361         try {
362             if (variables == null) {
363                 return;
364             }
365             final State state;
366             final String id = channel.getUID().getId();
367             final NutName fixedChannel = NutName.channelIdToNutName(id);
368
369             if (fixedChannel == null) {
370                 state = getDynamicChannelState(channel, variables);
371             } else {
372                 state = fixedChannel.toState(variables);
373             }
374             updateState(channel.getUID(), state);
375         } catch (final NutException | RuntimeException e) {
376             logger.debug("Refresh Network UPS Tools failed: ", e);
377         }
378     }
379
380     private State getDynamicChannelState(final Channel channel, @Nullable final Map<String, String> variables)
381             throws NutException {
382         final NUTDynamicChannelConfiguration nutConfig = userChannelToNutMap.get(channel.getUID());
383         final String acceptedItemType = channel.getAcceptedItemType();
384
385         if (variables == null || acceptedItemType == null || nutConfig == null) {
386             return UnDefType.UNDEF;
387         }
388         final String value = variables.get(nutConfig.networkupstools);
389
390         if (value == null) {
391             logger.info("Variable '{}' queried for thing {}({}) is not available in the NUT data.",
392                     nutConfig.networkupstools, thing.getLabel(), thing.getUID());
393             return UnDefType.UNDEF;
394         }
395         switch (acceptedItemType) {
396             case CoreItemFactory.NUMBER:
397                 return new DecimalType(value);
398             case CoreItemFactory.STRING:
399                 return StringType.valueOf(value);
400             case CoreItemFactory.SWITCH:
401                 return OnOffType.from(value);
402             default:
403                 if (acceptedItemType.startsWith(CoreItemFactory.NUMBER + ':')) {
404                     logger.debug("nut:{}, unit:{}, value:{}", nutConfig.networkupstools, nutConfig.unit, value);
405                     return new QuantityType<>(value + nutConfig.unit);
406                 }
407                 return UnDefType.UNDEF;
408         }
409     }
410 }