2 * Copyright (c) 2010-2021 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.foobot.internal.handler;
15 import static org.openhab.binding.foobot.internal.FoobotBindingConstants.*;
17 import java.time.Duration;
19 import java.util.concurrent.ScheduledFuture;
20 import java.util.concurrent.TimeUnit;
21 import java.util.stream.Collectors;
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;
49 * Bridge handler to manage Foobot Account
51 * @author George Katsis - Initial contribution
52 * @author Hilbrand Bouwkamp - Completed implementation
55 public class FoobotAccountHandler extends BaseBridgeHandler {
58 * Set the exact interval a little lower to compensate for the time it takes to get the new data.
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);
63 private final Logger logger = LoggerFactory.getLogger(FoobotAccountHandler.class);
65 private final FoobotApiConnector connector;
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;
73 public FoobotAccountHandler(Bridge bridge, FoobotApiConnector connector) {
75 this.connector = connector;
79 public Collection<Class<? extends ThingHandlerService>> getServices() {
80 return Collections.singleton(FoobotAccountDiscoveryService.class);
83 public List<FoobotDevice> getDeviceList() throws FoobotApiException {
84 return connector.getAssociatedDevices(username);
87 public int getRefreshInterval() {
88 return refreshInterval;
92 public void initialize() {
93 final FoobotAccountConfiguration accountConfig = getConfigAs(FoobotAccountConfiguration.class);
94 final List<String> missingParams = new ArrayList<>();
96 if (StringUtils.trimToNull(accountConfig.apiKey) == null) {
97 missingParams.add("'apikey'");
99 if (StringUtils.trimToNull(accountConfig.username) == null) {
100 missingParams.add("'username'");
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");
109 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, errorMsg);
112 username = accountConfig.username;
113 connector.setApiKey(accountConfig.apiKey);
114 refreshInterval = accountConfig.refreshInterval;
115 if (this.refreshInterval < MINIMUM_REFRESH_PERIOD_MINUTES) {
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;
121 logger.debug("Foobot Account bridge starting... user: {}, refreshInterval: {}", accountConfig.username,
124 updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, "Wait to get associated devices");
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(),
133 logger.debug("Foobot account bridge handler started.");
137 public void handleCommand(ChannelUID channelUID, Command command) {
138 logger.trace("Command '{}' received for channel '{}'", command, channelUID);
139 if (command instanceof RefreshType) {
145 public void dispose() {
146 logger.debug("Dispose {}", getThing().getUID());
148 final ScheduledFuture<?> refreshDeviceListJob = this.refreshDeviceListJob;
149 if (refreshDeviceListJob != null) {
150 refreshDeviceListJob.cancel(true);
151 this.refreshDeviceListJob = null;
153 final ScheduledFuture<?> refreshSensorsJob = this.refreshSensorsJob;
154 if (refreshSensorsJob != null) {
155 refreshSensorsJob.cancel(true);
156 this.refreshSensorsJob = null;
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.
164 * @return List of retrieved devices
166 private List<FoobotDeviceHandler> retrieveDeviceList() {
167 logger.debug("Refreshing sensors for {}", getThing().getUID());
168 final List<FoobotDeviceHandler> footbotHandlers = getFootbotHandlers();
171 getDeviceList().stream().forEach(d -> {
172 footbotHandlers.stream().filter(h -> h.getUuid().equals(d.getUuid())).findAny()
173 .ifPresent(fh -> fh.handleUpdateProperties(d));
175 } catch (FoobotApiException e) {
176 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
178 return footbotHandlers;
182 * Refreshes the devices list
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();
191 public void childHandlerInitialized(ThingHandler childHandler, Thing childThing) {
192 if (childHandler instanceof FoobotDeviceHandler) {
193 final String uuid = ((FoobotDeviceHandler) childHandler).getUuid();
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());
205 * @return Returns the list of associated footbot devices with this bridge.
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());
212 private void refreshSensors() {
213 logger.debug("Refreshing sensors for {}", getThing().getUID());
214 logger.debug("handlers: {}", getFootbotHandlers().size());
216 for (FoobotDeviceHandler handler : getFootbotHandlers()) {
217 logger.debug("handler: {}", handler.getUuid());
218 handler.refreshSensors();
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);
226 } catch (RuntimeException e) {
227 logger.debug("Error updating sensor data ", e);
228 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, e.getMessage());
232 public void updateRemainingLimitStatus() {
233 final int remaining = connector.getApiKeyLimitRemaining();
235 updateState(FoobotBindingConstants.CHANNEL_APIKEY_LIMIT_REMAINING,
236 remaining < 0 ? UnDefType.UNDEF : new DecimalType(remaining));