]> git.basschouten.com Git - openhab-addons.git/blob
7fb14e4dfa5e6d9c7d1799e78c4b6a1e4344e73b
[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.nest.internal.sdm.handler;
14
15 import static java.util.function.Predicate.not;
16 import static org.openhab.binding.nest.internal.sdm.dto.SDMGson.GSON;
17
18 import java.io.IOException;
19 import java.nio.charset.StandardCharsets;
20 import java.time.Duration;
21 import java.util.Base64;
22 import java.util.Collection;
23 import java.util.List;
24 import java.util.Map;
25 import java.util.Objects;
26 import java.util.concurrent.ConcurrentHashMap;
27 import java.util.concurrent.Future;
28 import java.util.concurrent.TimeUnit;
29 import java.util.stream.Collectors;
30 import java.util.stream.Stream;
31
32 import org.eclipse.jdt.annotation.NonNullByDefault;
33 import org.eclipse.jdt.annotation.Nullable;
34 import org.openhab.binding.nest.internal.sdm.api.PubSubAPI;
35 import org.openhab.binding.nest.internal.sdm.api.SDMAPI;
36 import org.openhab.binding.nest.internal.sdm.config.SDMAccountConfiguration;
37 import org.openhab.binding.nest.internal.sdm.discovery.SDMDiscoveryService;
38 import org.openhab.binding.nest.internal.sdm.dto.PubSubRequestsResponses.PubSubMessage;
39 import org.openhab.binding.nest.internal.sdm.dto.SDMEvent;
40 import org.openhab.binding.nest.internal.sdm.dto.SDMEvent.SDMResourceUpdate;
41 import org.openhab.binding.nest.internal.sdm.exception.FailedSendingPubSubDataException;
42 import org.openhab.binding.nest.internal.sdm.exception.FailedSendingSDMDataException;
43 import org.openhab.binding.nest.internal.sdm.exception.InvalidPubSubAccessTokenException;
44 import org.openhab.binding.nest.internal.sdm.exception.InvalidPubSubAuthorizationCodeException;
45 import org.openhab.binding.nest.internal.sdm.exception.InvalidSDMAccessTokenException;
46 import org.openhab.binding.nest.internal.sdm.exception.InvalidSDMAuthorizationCodeException;
47 import org.openhab.binding.nest.internal.sdm.listener.PubSubSubscriptionListener;
48 import org.openhab.binding.nest.internal.sdm.listener.SDMAPIRequestListener;
49 import org.openhab.binding.nest.internal.sdm.listener.SDMEventListener;
50 import org.openhab.core.auth.client.oauth2.OAuthFactory;
51 import org.openhab.core.config.core.Configuration;
52 import org.openhab.core.io.net.http.HttpClientFactory;
53 import org.openhab.core.thing.Bridge;
54 import org.openhab.core.thing.ChannelUID;
55 import org.openhab.core.thing.ThingStatus;
56 import org.openhab.core.thing.ThingStatusDetail;
57 import org.openhab.core.thing.binding.BaseBridgeHandler;
58 import org.openhab.core.thing.binding.ThingHandlerService;
59 import org.openhab.core.types.Command;
60 import org.slf4j.Logger;
61 import org.slf4j.LoggerFactory;
62
63 /**
64  * The {@link SDMAccountHandler} provides the {@link SDMAPI} instance used by the device handlers.
65  * The {@link SDMAPI} is used by device handlers for periodically refreshing device data and sending device commands.
66  * When Pub/Sub is properly configured, the account handler also sends received {@link SDMEvent}s from the
67  * {@link PubSubAPI} to the subscribed {@link SDMEventListener}s.
68  *
69  * @author Brian Higginbotham - Initial contribution
70  * @author Wouter Born - Initial contribution
71  */
72 @NonNullByDefault
73 public class SDMAccountHandler extends BaseBridgeHandler {
74
75     private static final String PUBSUB_TOPIC_NAME_PREFIX = "projects/sdm-prod/topics/enterprise-";
76
77     private final Logger logger = LoggerFactory.getLogger(SDMAccountHandler.class);
78
79     private HttpClientFactory httpClientFactory;
80     private OAuthFactory oAuthFactory;
81
82     private @NonNullByDefault({}) SDMAccountConfiguration config;
83     private @Nullable Future<?> initializeFuture;
84
85     private @Nullable PubSubAPI pubSubAPI;
86     private @Nullable Exception pubSubException;
87
88     private @Nullable SDMAPI sdmAPI;
89     private @Nullable Exception sdmException;
90     private @Nullable Future<?> sdmCheckFuture;
91     private final Duration sdmCheckDelay = Duration.ofMinutes(1);
92
93     private final Map<String, SDMEventListener> listeners = new ConcurrentHashMap<>();
94
95     private final SDMAPIRequestListener requestListener = new SDMAPIRequestListener() {
96         @Override
97         public void onError(Exception exception) {
98             sdmException = exception;
99             logger.debug("SDM exception occurred");
100             updateThingStatus();
101
102             Future<?> future = sdmCheckFuture;
103             if (future == null || future.isDone()) {
104                 sdmCheckFuture = scheduler.scheduleWithFixedDelay(() -> {
105                     SDMAPI localSDMAPI = sdmAPI;
106                     if (localSDMAPI != null) {
107                         try {
108                             logger.debug("Checking SDM API");
109                             localSDMAPI.listDevices();
110                         } catch (FailedSendingSDMDataException | InvalidSDMAccessTokenException e) {
111                             logger.debug("SDM API check failed");
112                         }
113                     }
114                 }, sdmCheckDelay.toNanos(), sdmCheckDelay.toNanos(), TimeUnit.NANOSECONDS);
115                 logger.debug("Scheduled SDM API check job");
116             }
117         }
118
119         @Override
120         public void onSuccess() {
121             if (sdmException != null) {
122                 sdmException = null;
123                 logger.debug("SDM exception cleared");
124                 updateThingStatus();
125             }
126
127             Future<?> future = sdmCheckFuture;
128             if (future != null) {
129                 future.cancel(true);
130                 sdmCheckFuture = null;
131                 logger.debug("Cancelled SDM API check job");
132             }
133         }
134     };
135
136     private final PubSubSubscriptionListener subscriptionListener = new PubSubSubscriptionListener() {
137         @Override
138         public void onError(Exception exception) {
139             pubSubException = exception;
140             logger.debug("Pub/Sub exception occurred");
141             updateThingStatus();
142         }
143
144         @Override
145         public void onMessage(PubSubMessage message) {
146             if (pubSubException != null) {
147                 pubSubException = null;
148                 logger.debug("Pub/Sub exception cleared");
149                 updateThingStatus();
150             }
151             handlePubSubMessage(message);
152         }
153
154         @Override
155         public void onNoNewMessages() {
156             if (pubSubException != null) {
157                 pubSubException = null;
158                 logger.debug("Pub/Sub exception cleared");
159                 updateThingStatus();
160             }
161         }
162     };
163
164     public SDMAccountHandler(Bridge bridge, HttpClientFactory httpClientFactory, OAuthFactory oAuthFactory) {
165         super(bridge);
166         this.httpClientFactory = httpClientFactory;
167         this.oAuthFactory = oAuthFactory;
168     }
169
170     @Override
171     public void handleCommand(ChannelUID channelUID, Command command) {
172     }
173
174     @Override
175     public void initialize() {
176         config = getConfigAs(SDMAccountConfiguration.class);
177
178         updateStatus(ThingStatus.UNKNOWN);
179
180         initializeFuture = scheduler.submit(() -> {
181             sdmAPI = initializeSDMAPI();
182             if (config.usePubSub()) {
183                 pubSubAPI = initializePubSubAPI();
184             }
185             updateThingStatus();
186         });
187     }
188
189     private @Nullable SDMAPI initializeSDMAPI() {
190         SDMAPI sdmAPI = new SDMAPI(httpClientFactory, oAuthFactory, getThing().getUID().getAsString(),
191                 config.sdmProjectId, config.sdmClientId, config.sdmClientSecret);
192         sdmException = null;
193
194         try {
195             if (!config.sdmAuthorizationCode.isBlank()) {
196                 sdmAPI.authorizeClient(config.sdmAuthorizationCode);
197
198                 Configuration configuration = editConfiguration();
199                 configuration.put(SDMAccountConfiguration.SDM_AUTHORIZATION_CODE, "");
200                 updateConfiguration(configuration);
201             }
202
203             sdmAPI.checkAccessTokenValidity();
204             sdmAPI.addRequestListener(requestListener);
205
206             return sdmAPI;
207         } catch (InvalidSDMAccessTokenException | InvalidSDMAuthorizationCodeException | IOException e) {
208             sdmException = e;
209             return null;
210         }
211     }
212
213     private @Nullable PubSubAPI initializePubSubAPI() {
214         PubSubAPI pubSubAPI = new PubSubAPI(httpClientFactory, oAuthFactory, getThing().getUID().getAsString(),
215                 config.pubsubProjectId, config.pubsubClientId, config.pubsubClientSecret);
216         pubSubException = null;
217
218         try {
219             if (!config.pubsubAuthorizationCode.isBlank()) {
220                 pubSubAPI.authorizeClient(config.pubsubAuthorizationCode);
221
222                 Configuration configuration = editConfiguration();
223                 configuration.put(SDMAccountConfiguration.PUBSUB_AUTHORIZATION_CODE, "");
224                 updateConfiguration(configuration);
225             }
226
227             pubSubAPI.checkAccessTokenValidity();
228             pubSubAPI.createSubscription(config.pubsubSubscriptionId, PUBSUB_TOPIC_NAME_PREFIX + config.sdmProjectId);
229             pubSubAPI.addSubscriptionListener(config.pubsubSubscriptionId, subscriptionListener);
230
231             return pubSubAPI;
232         } catch (FailedSendingPubSubDataException | InvalidPubSubAccessTokenException
233                 | InvalidPubSubAuthorizationCodeException | IOException e) {
234             pubSubException = e;
235             return null;
236         }
237     }
238
239     @Override
240     public void dispose() {
241         Future<?> localFuture = initializeFuture;
242         if (localFuture != null) {
243             localFuture.cancel(true);
244             initializeFuture = null;
245         }
246
247         localFuture = sdmCheckFuture;
248         if (localFuture != null) {
249             localFuture.cancel(true);
250             sdmCheckFuture = null;
251         }
252
253         PubSubAPI localPubSubAPI = pubSubAPI;
254         if (localPubSubAPI != null) {
255             localPubSubAPI.dispose();
256             pubSubAPI = null;
257         }
258
259         SDMAPI localSDMAPI = sdmAPI;
260         if (localSDMAPI != null) {
261             localSDMAPI.dispose();
262             sdmAPI = null;
263         }
264     }
265
266     @Override
267     public Collection<Class<? extends ThingHandlerService>> getServices() {
268         return List.of(SDMDiscoveryService.class);
269     }
270
271     public void addThingDataListener(String deviceId, SDMEventListener listener) {
272         listeners.put(deviceId, listener);
273     }
274
275     public void removeThingDataListener(String deviceId, SDMEventListener listener) {
276         listeners.remove(deviceId, listener);
277     }
278
279     public @Nullable SDMAPI getAPI() {
280         return sdmAPI;
281     }
282
283     private void handlePubSubMessage(PubSubMessage message) {
284         String messageId = message.messageId;
285         String json = new String(Base64.getDecoder().decode(message.data), StandardCharsets.UTF_8);
286
287         logger.debug("Handling messageId={} with content:", messageId);
288         logger.debug("{}", json);
289
290         SDMEvent event = GSON.fromJson(json, SDMEvent.class);
291         if (event == null) {
292             logger.debug("Ignoring messageId={} (empty)", messageId);
293             return;
294         }
295
296         SDMResourceUpdate resourceUpdate = event.resourceUpdate;
297         if (resourceUpdate == null) {
298             logger.debug("Ignoring messageId={} (no resource update)", messageId);
299             return;
300         }
301
302         String deviceId = resourceUpdate.name.deviceId;
303         SDMEventListener listener = listeners.get(deviceId);
304         if (listener != null) {
305             logger.debug("Sending messageId={} to listener with deviceId={}", messageId, deviceId);
306             listener.onEvent(event);
307         } else {
308             logger.debug("No listener for messageId={} with deviceId={}", messageId, deviceId);
309         }
310     }
311
312     private void updateThingStatus() {
313         Exception e = sdmException != null ? sdmException : pubSubException;
314         if (e != null) {
315             if (e instanceof InvalidSDMAccessTokenException || e instanceof InvalidSDMAuthorizationCodeException
316                     || e instanceof InvalidPubSubAccessTokenException
317                     || e instanceof InvalidPubSubAuthorizationCodeException) {
318                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
319             } else {
320                 Throwable cause = e.getCause();
321                 String description = Stream
322                         .of(Objects.requireNonNullElse(e.getMessage(), ""),
323                                 cause == null ? "" : Objects.requireNonNullElse(cause.getMessage(), ""))
324                         .filter(not(String::isBlank)) //
325                         .collect(Collectors.joining(": "));
326                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, description);
327             }
328         } else {
329             String description = config.usePubSub() ? "Using periodic refresh and Pub/Sub" : "Using periodic refresh";
330             updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE, description);
331         }
332     }
333 }