2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.networkupstools.internal;
15 import java.time.Duration;
16 import java.util.ArrayList;
17 import java.util.Collection;
18 import java.util.HashMap;
19 import java.util.List;
22 import java.util.concurrent.ScheduledFuture;
23 import java.util.concurrent.TimeUnit;
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;
53 * The {@link NUTHandler} is responsible for handling commands, which are
54 * sent to one of the channels.
56 * @author Hilbrand Bouwkamp - Initial contribution
59 public class NUTHandler extends BaseThingHandler {
60 private static final int REFRESH_RATE_SECONDS = 3;
62 private final Logger logger = LoggerFactory.getLogger(NUTHandler.class);
64 * Map to cache user configured channels with their configuration. Channels are dynamically created at
65 * initialization phase of the thing.
67 private final Map<ChannelUID, NUTDynamicChannelConfiguration> userChannelToNutMap = new HashMap<>();
69 * Cache of the UPS status. When expired makes a call to the NUT server is done to get the actual status. Expires at
71 * short time refresh rate. Used to avoid triggering multiple calls to the server in a short time frame.
73 private final ExpiringCache<String> upsStatusCache = new ExpiringCache<>(Duration.ofSeconds(REFRESH_RATE_SECONDS),
74 this::retrieveUpsStatus);
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.
79 private final ExpiringCache<Map<String, String>> variablesCache = new ExpiringCache<>(
80 Duration.ofSeconds(REFRESH_RATE_SECONDS), this::retrieveVariables);
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.
86 private final ExpiringCache<Boolean> refreshPropertiesCache = new ExpiringCache<>(Duration.ofHours(1),
87 this::updateProperties);
88 private final ChannelUID upsStatusChannelUID;
90 private @Nullable NUTChannelTypeProvider channelTypeProvider;
91 private @Nullable NUTDynamicChannelFactory dynamicChannelFactory;
92 private @Nullable NUTConfiguration config;
93 private @Nullable ScheduledFuture<?> poller;
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
100 private @Nullable ExpiringCache<Boolean> refreshVariablesCache;
101 private @Nullable NutApi nutApi;
104 * Keep track of the last ups status to avoid updating the status every 3 seconds when nothing changed.
106 private String lastUpsStatus = "";
108 public NUTHandler(final Thing thing) {
110 upsStatusChannelUID = new ChannelUID(getThing().getUID(), NutName.UPS_STATUS.getChannelId());
114 public Collection<Class<? extends ThingHandlerService>> getServices() {
115 return Set.of(NUTChannelTypeProvider.class);
118 public void setChannelTypeProvider(final NUTChannelTypeProvider channelTypeProvider) {
119 this.channelTypeProvider = channelTypeProvider;
120 dynamicChannelFactory = new NUTDynamicChannelFactory(channelTypeProvider);
124 public void handleCommand(final ChannelUID channelUID, final Command command) {
125 if (command instanceof RefreshType) {
126 final Channel channel = getThing().getChannel(channelUID);
128 if (channel == null) {
129 logger.info("Trying to update a none existing channel: {}", channelUID);
131 updateChannel(channel, variablesCache.getValue());
137 public void initialize() {
138 final NUTConfiguration config = getConfigAs(NUTConfiguration.class);
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);
149 public void dispose() {
150 if (nutApi != null) {
153 final ScheduledFuture<?> localPoller = poller;
155 if (localPoller != null && !localPoller.isCancelled()) {
156 localPoller.cancel(true);
162 * Initializes any channels configured by the user by creating complementary channel types and recreate the channels
165 private void initDynamicChannels() {
166 final NUTChannelTypeProvider localChannelTypeProvider = channelTypeProvider;
167 final NUTDynamicChannelFactory localDynamicChannelFactory = dynamicChannelFactory;
169 if (localChannelTypeProvider == null || localDynamicChannelFactory == null) {
172 final List<Channel> updatedChannels = new ArrayList<>();
173 boolean rebuildChannels = false;
175 for (final Channel channel : thing.getChannels()) {
176 if (channel.getConfiguration().getProperties().isEmpty()) {
177 updatedChannels.add(channel);
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;
186 rebuildChannels = customChannel;
188 dynamicChannel = localDynamicChannelFactory.createChannel(channel, channelConfig);
190 if (dynamicChannel == null) {
191 logger.debug("Could not initialize the dynamic channel '{}'. This channel will be ignored ",
195 logger.debug("Updating channel '{}' with dynamic channelType settings: {}", channel.getUID(),
196 dynamicChannel.getChannelTypeUID());
199 logger.debug("Mapping standard dynamic channel '{}' with dynamic channelType settings: {}",
200 channel.getUID(), channel.getChannelTypeUID());
201 dynamicChannel = channel;
203 userChannelToNutMap.put(channel.getUID(), channelConfig);
204 updatedChannels.add(dynamicChannel);
207 if (rebuildChannels) {
208 final ThingBuilder thingBuilder = editThing();
209 thingBuilder.withChannels(updatedChannels);
210 updateThing(thingBuilder.build());
215 * Method called by the scheduled task that checks for the active status of the ups.
217 private void refreshStatus() {
219 final String state = upsStatusCache.getValue();
220 final ExpiringCache<Boolean> localVariablesRefreshCache = refreshVariablesCache;
222 if (!lastUpsStatus.equals(state)) {
223 if (isLinked(upsStatusChannelUID)) {
224 updateState(upsStatusChannelUID, state == null ? UnDefType.UNDEF : new StringType(state));
226 lastUpsStatus = state == null ? "" : state;
227 if (localVariablesRefreshCache != null) {
228 localVariablesRefreshCache.invalidateValue();
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();
235 } catch (final RuntimeException e) {
236 logger.debug("Updating ups status failed: ", e);
237 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
242 * This method is triggered when the cache {@link #refreshVariablesCache} is expired.
244 * @return returns true if success and false on error
246 private boolean updateRefreshVariables() {
247 logger.trace("Calling updateRefreshVariables {}", thing.getUID());
249 final Map<String, String> variables = variablesCache.getValue();
251 if (variables == null) {
252 logger.trace("No data from NUT server received.");
255 logger.trace("Updating status of linked channels.");
256 for (final Channel channel : getThing().getChannels()) {
257 final ChannelUID uid = channel.getUID();
260 updateChannel(channel, variables);
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);
270 } catch (final RuntimeException e) {
271 logger.debug("Refresh Network UPS Tools failed: ", e);
272 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
278 * Method that retrieves the ups status from the NUT server.
280 * @return status of the UPS or null if it couldn't be determined
282 private @Nullable String retrieveUpsStatus() {
283 final NutApi localNutApi = nutApi;
285 if (localNutApi == null) {
288 return wrappedNutApiCall(device -> localNutApi.getVariable(device, NutName.UPS_STATUS.getName()), "UPS status");
292 * Method that retrieves all variables from the NUT server.
294 * @return variables retrieved send by the NUT server or null if it couldn't be determined
296 private @Nullable Map<String, String> retrieveVariables() {
297 final NutApi localNutApi = nutApi;
299 if (localNutApi == null) {
302 return wrappedNutApiCall(localNutApi::getVariables, "NUT variables");
306 * Convenience method that wraps the call to the api and handles exceptions.
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
312 private @Nullable <T> T wrappedNutApiCall(final NutFunction<String, T> nutApiFunction, String logging) {
314 final NUTConfiguration localConfig = config;
316 if (localConfig == null) {
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());
329 * Updates the thing properties when the cache {@link #refreshPropertiesCache} is expired.
331 * @return returns true if success and false on error
333 private Boolean updateProperties() {
335 final Map<String, String> variables = variablesCache.getValue();
337 if (variables != null) {
338 final Map<String, String> properties = editProperties();
340 for (final Parameters param : NUTBindingConstants.Parameters.values()) {
341 final String value = variables.get(param.getNutName());
345 "Variable '{}' intented as property for thing {}({}) is not available in the NUT data.",
346 param.getNutName(), thing.getLabel(), thing.getUID());
348 properties.put(param.getNutName(), value);
351 updateProperties(properties);
354 } catch (final RuntimeException e) {
355 logger.debug("Updating parameters failed: ", e);
356 return Boolean.FALSE;
360 private void updateChannel(final Channel channel, @Nullable final Map<String, String> variables) {
362 if (variables == null) {
366 final String id = channel.getUID().getId();
367 final NutName fixedChannel = NutName.channelIdToNutName(id);
369 if (fixedChannel == null) {
370 state = getDynamicChannelState(channel, variables);
372 state = fixedChannel.toState(variables);
374 updateState(channel.getUID(), state);
375 } catch (final NutException | RuntimeException e) {
376 logger.debug("Refresh Network UPS Tools failed: ", e);
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();
385 if (variables == null || acceptedItemType == null || nutConfig == null) {
386 return UnDefType.UNDEF;
388 final String value = variables.get(nutConfig.networkupstools);
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;
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);
403 if (acceptedItemType.startsWith(CoreItemFactory.NUMBER + ':')) {
404 logger.debug("nut:{}, unit:{}, value:{}", nutConfig.networkupstools, nutConfig.unit, value);
405 return new QuantityType<>(value + nutConfig.unit);
407 return UnDefType.UNDEF;