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