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.nest.internal.sdm.handler;
15 import static java.util.function.Predicate.not;
16 import static org.openhab.binding.nest.internal.sdm.dto.SDMGson.GSON;
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;
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;
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;
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.
69 * @author Brian Higginbotham - Initial contribution
70 * @author Wouter Born - Initial contribution
73 public class SDMAccountHandler extends BaseBridgeHandler {
75 private static final String PUBSUB_TOPIC_NAME_PREFIX = "projects/sdm-prod/topics/enterprise-";
77 private final Logger logger = LoggerFactory.getLogger(SDMAccountHandler.class);
79 private HttpClientFactory httpClientFactory;
80 private OAuthFactory oAuthFactory;
82 private @NonNullByDefault({}) SDMAccountConfiguration config;
83 private @Nullable Future<?> initializeFuture;
85 private @Nullable PubSubAPI pubSubAPI;
86 private @Nullable Exception pubSubException;
88 private @Nullable SDMAPI sdmAPI;
89 private @Nullable Exception sdmException;
90 private @Nullable Future<?> sdmCheckFuture;
91 private final Duration sdmCheckDelay = Duration.ofMinutes(1);
93 private final Map<String, SDMEventListener> listeners = new ConcurrentHashMap<>();
95 private final SDMAPIRequestListener requestListener = new SDMAPIRequestListener() {
97 public void onError(Exception exception) {
98 sdmException = exception;
99 logger.debug("SDM exception occurred");
102 Future<?> future = sdmCheckFuture;
103 if (future == null || future.isDone()) {
104 sdmCheckFuture = scheduler.scheduleWithFixedDelay(() -> {
105 SDMAPI localSDMAPI = sdmAPI;
106 if (localSDMAPI != null) {
108 logger.debug("Checking SDM API");
109 localSDMAPI.listDevices();
110 } catch (FailedSendingSDMDataException | InvalidSDMAccessTokenException e) {
111 logger.debug("SDM API check failed");
114 }, sdmCheckDelay.toNanos(), sdmCheckDelay.toNanos(), TimeUnit.NANOSECONDS);
115 logger.debug("Scheduled SDM API check job");
120 public void onSuccess() {
121 if (sdmException != null) {
123 logger.debug("SDM exception cleared");
127 Future<?> future = sdmCheckFuture;
128 if (future != null) {
130 sdmCheckFuture = null;
131 logger.debug("Cancelled SDM API check job");
136 private final PubSubSubscriptionListener subscriptionListener = new PubSubSubscriptionListener() {
138 public void onError(Exception exception) {
139 pubSubException = exception;
140 logger.debug("Pub/Sub exception occurred");
145 public void onMessage(PubSubMessage message) {
146 if (pubSubException != null) {
147 pubSubException = null;
148 logger.debug("Pub/Sub exception cleared");
151 handlePubSubMessage(message);
155 public void onNoNewMessages() {
156 if (pubSubException != null) {
157 pubSubException = null;
158 logger.debug("Pub/Sub exception cleared");
164 public SDMAccountHandler(Bridge bridge, HttpClientFactory httpClientFactory, OAuthFactory oAuthFactory) {
166 this.httpClientFactory = httpClientFactory;
167 this.oAuthFactory = oAuthFactory;
171 public void handleCommand(ChannelUID channelUID, Command command) {
175 public void initialize() {
176 config = getConfigAs(SDMAccountConfiguration.class);
178 updateStatus(ThingStatus.UNKNOWN);
180 initializeFuture = scheduler.submit(() -> {
181 sdmAPI = initializeSDMAPI();
182 if (config.usePubSub()) {
183 pubSubAPI = initializePubSubAPI();
189 private @Nullable SDMAPI initializeSDMAPI() {
190 SDMAPI sdmAPI = new SDMAPI(httpClientFactory, oAuthFactory, getThing().getUID().getAsString(),
191 config.sdmProjectId, config.sdmClientId, config.sdmClientSecret);
195 if (!config.sdmAuthorizationCode.isBlank()) {
196 sdmAPI.authorizeClient(config.sdmAuthorizationCode);
198 Configuration configuration = editConfiguration();
199 configuration.put(SDMAccountConfiguration.SDM_AUTHORIZATION_CODE, "");
200 updateConfiguration(configuration);
203 sdmAPI.checkAccessTokenValidity();
204 sdmAPI.addRequestListener(requestListener);
207 } catch (InvalidSDMAccessTokenException | InvalidSDMAuthorizationCodeException | IOException e) {
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;
219 if (!config.pubsubAuthorizationCode.isBlank()) {
220 pubSubAPI.authorizeClient(config.pubsubAuthorizationCode);
222 Configuration configuration = editConfiguration();
223 configuration.put(SDMAccountConfiguration.PUBSUB_AUTHORIZATION_CODE, "");
224 updateConfiguration(configuration);
227 pubSubAPI.checkAccessTokenValidity();
228 pubSubAPI.createSubscription(config.pubsubSubscriptionId, PUBSUB_TOPIC_NAME_PREFIX + config.sdmProjectId);
229 pubSubAPI.addSubscriptionListener(config.pubsubSubscriptionId, subscriptionListener);
232 } catch (FailedSendingPubSubDataException | InvalidPubSubAccessTokenException
233 | InvalidPubSubAuthorizationCodeException | IOException e) {
240 public void dispose() {
241 Future<?> localFuture = initializeFuture;
242 if (localFuture != null) {
243 localFuture.cancel(true);
244 initializeFuture = null;
247 localFuture = sdmCheckFuture;
248 if (localFuture != null) {
249 localFuture.cancel(true);
250 sdmCheckFuture = null;
253 PubSubAPI localPubSubAPI = pubSubAPI;
254 if (localPubSubAPI != null) {
255 localPubSubAPI.dispose();
259 SDMAPI localSDMAPI = sdmAPI;
260 if (localSDMAPI != null) {
261 localSDMAPI.dispose();
267 public Collection<Class<? extends ThingHandlerService>> getServices() {
268 return List.of(SDMDiscoveryService.class);
271 public void addThingDataListener(String deviceId, SDMEventListener listener) {
272 listeners.put(deviceId, listener);
275 public void removeThingDataListener(String deviceId, SDMEventListener listener) {
276 listeners.remove(deviceId, listener);
279 public @Nullable SDMAPI getAPI() {
283 private void handlePubSubMessage(PubSubMessage message) {
284 String messageId = message.messageId;
285 String json = new String(Base64.getDecoder().decode(message.data), StandardCharsets.UTF_8);
287 logger.debug("Handling messageId={} with content:", messageId);
288 logger.debug("{}", json);
290 SDMEvent event = GSON.fromJson(json, SDMEvent.class);
292 logger.debug("Ignoring messageId={} (empty)", messageId);
296 SDMResourceUpdate resourceUpdate = event.resourceUpdate;
297 if (resourceUpdate == null) {
298 logger.debug("Ignoring messageId={} (no resource update)", messageId);
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);
308 logger.debug("No listener for messageId={} with deviceId={}", messageId, deviceId);
312 private void updateThingStatus() {
313 Exception e = sdmException != null ? sdmException : pubSubException;
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());
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);
329 String description = config.usePubSub() ? "Using periodic refresh and Pub/Sub" : "Using periodic refresh";
330 updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE, description);