]> git.basschouten.com Git - openhab-addons.git/blob
4684b7456749d81a7da8c664a4c7bf50bbf0e62a
[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.binding.foobot.internal.handler;
14
15 import static org.openhab.binding.foobot.internal.FoobotBindingConstants.*;
16
17 import java.time.Duration;
18 import java.util.*;
19 import java.util.concurrent.ScheduledFuture;
20 import java.util.concurrent.TimeUnit;
21 import java.util.stream.Collectors;
22
23 import org.apache.commons.lang.StringUtils;
24 import org.eclipse.jdt.annotation.NonNullByDefault;
25 import org.eclipse.jdt.annotation.Nullable;
26 import org.openhab.binding.foobot.internal.FoobotApiConnector;
27 import org.openhab.binding.foobot.internal.FoobotApiException;
28 import org.openhab.binding.foobot.internal.FoobotBindingConstants;
29 import org.openhab.binding.foobot.internal.config.FoobotAccountConfiguration;
30 import org.openhab.binding.foobot.internal.discovery.FoobotAccountDiscoveryService;
31 import org.openhab.binding.foobot.internal.json.FoobotDevice;
32 import org.openhab.core.cache.ExpiringCache;
33 import org.openhab.core.library.types.DecimalType;
34 import org.openhab.core.thing.Bridge;
35 import org.openhab.core.thing.ChannelUID;
36 import org.openhab.core.thing.Thing;
37 import org.openhab.core.thing.ThingStatus;
38 import org.openhab.core.thing.ThingStatusDetail;
39 import org.openhab.core.thing.binding.BaseBridgeHandler;
40 import org.openhab.core.thing.binding.ThingHandler;
41 import org.openhab.core.thing.binding.ThingHandlerService;
42 import org.openhab.core.types.Command;
43 import org.openhab.core.types.RefreshType;
44 import org.openhab.core.types.UnDefType;
45 import org.slf4j.Logger;
46 import org.slf4j.LoggerFactory;
47
48 /**
49  * Bridge handler to manage Foobot Account
50  *
51  * @author George Katsis - Initial contribution
52  * @author Hilbrand Bouwkamp - Completed implementation
53  */
54 @NonNullByDefault
55 public class FoobotAccountHandler extends BaseBridgeHandler {
56
57     /*
58      * Set the exact interval a little lower to compensate for the time it takes to get the new data.
59      */
60     private static final long DEVICES_INTERVAL_MINUTES = Duration.ofDays(1).minus(Duration.ofMinutes(1)).toMinutes();
61     private static final Duration SENSOR_INTERVAL_OFFSET_SECONDS = Duration.ofSeconds(15);
62
63     private final Logger logger = LoggerFactory.getLogger(FoobotAccountHandler.class);
64
65     private final FoobotApiConnector connector;
66
67     private String username = "";
68     private int refreshInterval;
69     private @Nullable ScheduledFuture<?> refreshDeviceListJob;
70     private @Nullable ScheduledFuture<?> refreshSensorsJob;
71     private @NonNullByDefault({}) ExpiringCache<List<FoobotDeviceHandler>> dataCache;
72
73     public FoobotAccountHandler(Bridge bridge, FoobotApiConnector connector) {
74         super(bridge);
75         this.connector = connector;
76     }
77
78     @Override
79     public Collection<Class<? extends ThingHandlerService>> getServices() {
80         return Collections.singleton(FoobotAccountDiscoveryService.class);
81     }
82
83     public List<FoobotDevice> getDeviceList() throws FoobotApiException {
84         return connector.getAssociatedDevices(username);
85     }
86
87     public int getRefreshInterval() {
88         return refreshInterval;
89     }
90
91     @Override
92     public void initialize() {
93         final FoobotAccountConfiguration accountConfig = getConfigAs(FoobotAccountConfiguration.class);
94         final List<String> missingParams = new ArrayList<>();
95
96         if (StringUtils.trimToNull(accountConfig.apiKey) == null) {
97             missingParams.add("'apikey'");
98         }
99         if (StringUtils.trimToNull(accountConfig.username) == null) {
100             missingParams.add("'username'");
101         }
102
103         if (!missingParams.isEmpty()) {
104             final boolean oneParam = missingParams.size() == 1;
105             final String errorMsg = String.format(
106                     "Parameter%s [%s] %s mandatory and must be configured and not be empty", oneParam ? "" : "s",
107                     StringUtils.join(missingParams, ", "), oneParam ? "is" : "are");
108
109             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, errorMsg);
110             return;
111         }
112         username = accountConfig.username;
113         connector.setApiKey(accountConfig.apiKey);
114         refreshInterval = accountConfig.refreshInterval;
115         if (this.refreshInterval < MINIMUM_REFRESH_PERIOD_MINUTES) {
116             logger.warn(
117                     "Refresh interval time [{}] is not valid. Refresh interval time must be at least {} minutes. Setting to {} minutes",
118                     accountConfig.refreshInterval, MINIMUM_REFRESH_PERIOD_MINUTES, DEFAULT_REFRESH_PERIOD_MINUTES);
119             refreshInterval = DEFAULT_REFRESH_PERIOD_MINUTES;
120         }
121         logger.debug("Foobot Account bridge starting... user: {}, refreshInterval: {}", accountConfig.username,
122                 refreshInterval);
123
124         updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, "Wait to get associated devices");
125
126         dataCache = new ExpiringCache<>(Duration.ofMinutes(refreshInterval), this::retrieveDeviceList);
127         this.refreshDeviceListJob = scheduler.scheduleWithFixedDelay(this::refreshDeviceList, 0,
128                 DEVICES_INTERVAL_MINUTES, TimeUnit.MINUTES);
129         this.refreshSensorsJob = scheduler.scheduleWithFixedDelay(this::refreshSensors, 0,
130                 Duration.ofMinutes(refreshInterval).minus(SENSOR_INTERVAL_OFFSET_SECONDS).getSeconds(),
131                 TimeUnit.SECONDS);
132
133         logger.debug("Foobot account bridge handler started.");
134     }
135
136     @Override
137     public void handleCommand(ChannelUID channelUID, Command command) {
138         logger.trace("Command '{}' received for channel '{}'", command, channelUID);
139         if (command instanceof RefreshType) {
140             refreshDeviceList();
141         }
142     }
143
144     @Override
145     public void dispose() {
146         logger.debug("Dispose {}", getThing().getUID());
147
148         final ScheduledFuture<?> refreshDeviceListJob = this.refreshDeviceListJob;
149         if (refreshDeviceListJob != null) {
150             refreshDeviceListJob.cancel(true);
151             this.refreshDeviceListJob = null;
152         }
153         final ScheduledFuture<?> refreshSensorsJob = this.refreshSensorsJob;
154         if (refreshSensorsJob != null) {
155             refreshSensorsJob.cancel(true);
156             this.refreshSensorsJob = null;
157         }
158     }
159
160     /**
161      * Retrieves the list of devices and updates the properties of the devices. This method is called by the cache to
162      * update the cache data.
163      *
164      * @return List of retrieved devices
165      */
166     private List<FoobotDeviceHandler> retrieveDeviceList() {
167         logger.debug("Refreshing sensors for {}", getThing().getUID());
168         final List<FoobotDeviceHandler> footbotHandlers = getFootbotHandlers();
169
170         try {
171             getDeviceList().stream().forEach(d -> {
172                 footbotHandlers.stream().filter(h -> h.getUuid().equals(d.getUuid())).findAny()
173                         .ifPresent(fh -> fh.handleUpdateProperties(d));
174             });
175         } catch (FoobotApiException e) {
176             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
177         }
178         return footbotHandlers;
179     }
180
181     /**
182      * Refreshes the devices list
183      */
184     private void refreshDeviceList() {
185         // This getValue() return value not used here. But if the cache is expired it refreshes the cache.
186         dataCache.getValue();
187         updateRemainingLimitStatus();
188     }
189
190     @Override
191     public void childHandlerInitialized(ThingHandler childHandler, Thing childThing) {
192         if (childHandler instanceof FoobotDeviceHandler) {
193             final String uuid = ((FoobotDeviceHandler) childHandler).getUuid();
194
195             try {
196                 getDeviceList().stream().filter(d -> d.getUuid().equals(uuid)).findAny()
197                         .ifPresent(fd -> ((FoobotDeviceHandler) childHandler).handleUpdateProperties(fd));
198             } catch (FoobotApiException e) {
199                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
200             }
201         }
202     }
203
204     /**
205      * @return Returns the list of associated footbot devices with this bridge.
206      */
207     public List<FoobotDeviceHandler> getFootbotHandlers() {
208         return getThing().getThings().stream().map(Thing::getHandler).filter(FoobotDeviceHandler.class::isInstance)
209                 .map(FoobotDeviceHandler.class::cast).collect(Collectors.toList());
210     }
211
212     private void refreshSensors() {
213         logger.debug("Refreshing sensors for {}", getThing().getUID());
214         logger.debug("handlers: {}", getFootbotHandlers().size());
215         try {
216             for (FoobotDeviceHandler handler : getFootbotHandlers()) {
217                 logger.debug("handler: {}", handler.getUuid());
218                 handler.refreshSensors();
219             }
220             if (connector.getApiKeyLimitRemaining() == FoobotApiConnector.API_RATE_LIMIT_EXCEEDED) {
221                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE,
222                         FoobotApiConnector.API_RATE_LIMIT_EXCEEDED_MESSAGE);
223             } else if (getThing().getStatus() != ThingStatus.ONLINE) {
224                 updateStatus(ThingStatus.ONLINE);
225             }
226         } catch (RuntimeException e) {
227             logger.debug("Error updating sensor data ", e);
228             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, e.getMessage());
229         }
230     }
231
232     public void updateRemainingLimitStatus() {
233         final int remaining = connector.getApiKeyLimitRemaining();
234
235         updateState(FoobotBindingConstants.CHANNEL_APIKEY_LIMIT_REMAINING,
236                 remaining < 0 ? UnDefType.UNDEF : new DecimalType(remaining));
237     }
238 }