]> git.basschouten.com Git - openhab-addons.git/blob
bfc9a194a01dfa749d8ca8f5270b333d94e6f9b2
[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.foobot.internal.handler;
14
15 import static org.openhab.binding.foobot.internal.FoobotBindingConstants.*;
16
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;
25
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;
49
50 /**
51  * Bridge handler to manage Foobot Account
52  *
53  * @author George Katsis - Initial contribution
54  * @author Hilbrand Bouwkamp - Completed implementation
55  */
56 @NonNullByDefault
57 public class FoobotAccountHandler extends BaseBridgeHandler {
58
59     /*
60      * Set the exact interval a little lower to compensate for the time it takes to get the new data.
61      */
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);
64
65     private final Logger logger = LoggerFactory.getLogger(FoobotAccountHandler.class);
66
67     private final FoobotApiConnector connector;
68
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;
74
75     public FoobotAccountHandler(Bridge bridge, FoobotApiConnector connector) {
76         super(bridge);
77         this.connector = connector;
78     }
79
80     @Override
81     public Collection<Class<? extends ThingHandlerService>> getServices() {
82         return Collections.singleton(FoobotAccountDiscoveryService.class);
83     }
84
85     public List<FoobotDevice> getDeviceList() throws FoobotApiException {
86         return connector.getAssociatedDevices(username);
87     }
88
89     public int getRefreshInterval() {
90         return refreshInterval;
91     }
92
93     @Override
94     public void initialize() {
95         final FoobotAccountConfiguration accountConfig = getConfigAs(FoobotAccountConfiguration.class);
96         final List<String> missingParams = new ArrayList<>();
97
98         String apiKey = accountConfig.apiKey;
99         if (apiKey.isBlank()) {
100             missingParams.add("'apikey'");
101         }
102         String username = accountConfig.username;
103         if (username.isBlank()) {
104             missingParams.add("'username'");
105         }
106
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");
112
113             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, errorMsg);
114             return;
115         }
116         this.username = username;
117         connector.setApiKey(apiKey);
118         refreshInterval = accountConfig.refreshInterval;
119         if (this.refreshInterval < MINIMUM_REFRESH_PERIOD_MINUTES) {
120             logger.warn(
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;
124         }
125         logger.debug("Foobot Account bridge starting... user: {}, refreshInterval: {}", username, refreshInterval);
126
127         updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, "Wait to get associated devices");
128
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(),
134                 TimeUnit.SECONDS);
135
136         logger.debug("Foobot account bridge handler started.");
137     }
138
139     @Override
140     public void handleCommand(ChannelUID channelUID, Command command) {
141         logger.trace("Command '{}' received for channel '{}'", command, channelUID);
142         if (command instanceof RefreshType) {
143             refreshDeviceList();
144         }
145     }
146
147     @Override
148     public void dispose() {
149         logger.debug("Dispose {}", getThing().getUID());
150
151         final ScheduledFuture<?> refreshDeviceListJob = this.refreshDeviceListJob;
152         if (refreshDeviceListJob != null) {
153             refreshDeviceListJob.cancel(true);
154             this.refreshDeviceListJob = null;
155         }
156         final ScheduledFuture<?> refreshSensorsJob = this.refreshSensorsJob;
157         if (refreshSensorsJob != null) {
158             refreshSensorsJob.cancel(true);
159             this.refreshSensorsJob = null;
160         }
161     }
162
163     /**
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.
166      *
167      * @return List of retrieved devices
168      */
169     private List<FoobotDeviceHandler> retrieveDeviceList() {
170         logger.debug("Refreshing sensors for {}", getThing().getUID());
171         final List<FoobotDeviceHandler> footbotHandlers = getFootbotHandlers();
172
173         try {
174             getDeviceList().stream().forEach(d -> {
175                 footbotHandlers.stream().filter(h -> h.getUuid().equals(d.getUuid())).findAny()
176                         .ifPresent(fh -> fh.handleUpdateProperties(d));
177             });
178         } catch (FoobotApiException e) {
179             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
180         }
181         return footbotHandlers;
182     }
183
184     /**
185      * Refreshes the devices list
186      */
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();
191     }
192
193     @Override
194     public void childHandlerInitialized(ThingHandler childHandler, Thing childThing) {
195         if (childHandler instanceof FoobotDeviceHandler) {
196             final String uuid = ((FoobotDeviceHandler) childHandler).getUuid();
197
198             try {
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());
203             }
204         }
205     }
206
207     /**
208      * @return Returns the list of associated footbot devices with this bridge.
209      */
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());
213     }
214
215     private void refreshSensors() {
216         logger.debug("Refreshing sensors for {}", getThing().getUID());
217         logger.debug("handlers: {}", getFootbotHandlers().size());
218         try {
219             for (FoobotDeviceHandler handler : getFootbotHandlers()) {
220                 logger.debug("handler: {}", handler.getUuid());
221                 handler.refreshSensors();
222             }
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);
228             }
229         } catch (RuntimeException e) {
230             logger.debug("Error updating sensor data ", e);
231             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, e.getMessage());
232         }
233     }
234
235     public void updateRemainingLimitStatus() {
236         final int remaining = connector.getApiKeyLimitRemaining();
237
238         updateState(FoobotBindingConstants.CHANNEL_APIKEY_LIMIT_REMAINING,
239                 remaining < 0 ? UnDefType.UNDEF : new DecimalType(remaining));
240     }
241 }