]> git.basschouten.com Git - openhab-addons.git/blob
864c103d6c8d58008eff130ffe9cc0fea969d27a
[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.generacmobilelink.internal.handler;
14
15 import java.util.Optional;
16 import java.util.concurrent.CompletableFuture;
17 import java.util.concurrent.ExecutionException;
18 import java.util.concurrent.Future;
19 import java.util.concurrent.TimeUnit;
20
21 import org.eclipse.jdt.annotation.NonNullByDefault;
22 import org.eclipse.jdt.annotation.Nullable;
23 import org.eclipse.jetty.client.HttpClient;
24 import org.eclipse.jetty.client.api.ContentProvider;
25 import org.eclipse.jetty.client.api.Request;
26 import org.eclipse.jetty.client.api.Result;
27 import org.eclipse.jetty.client.util.BufferingResponseListener;
28 import org.eclipse.jetty.client.util.StringContentProvider;
29 import org.eclipse.jetty.http.HttpMethod;
30 import org.eclipse.jetty.http.HttpStatus;
31 import org.openhab.binding.generacmobilelink.internal.GeneracMobileLinkBindingConstants;
32 import org.openhab.binding.generacmobilelink.internal.config.GeneracMobileLinkAccountConfiguration;
33 import org.openhab.binding.generacmobilelink.internal.discovery.GeneracMobileLinkDiscoveryService;
34 import org.openhab.binding.generacmobilelink.internal.dto.ErrorResponseDTO;
35 import org.openhab.binding.generacmobilelink.internal.dto.GeneratorStatusDTO;
36 import org.openhab.binding.generacmobilelink.internal.dto.GeneratorStatusResponseDTO;
37 import org.openhab.binding.generacmobilelink.internal.dto.LoginRequestDTO;
38 import org.openhab.binding.generacmobilelink.internal.dto.LoginResponseDTO;
39 import org.openhab.core.thing.Bridge;
40 import org.openhab.core.thing.ChannelUID;
41 import org.openhab.core.thing.Thing;
42 import org.openhab.core.thing.ThingStatus;
43 import org.openhab.core.thing.ThingStatusDetail;
44 import org.openhab.core.thing.ThingUID;
45 import org.openhab.core.thing.binding.BaseBridgeHandler;
46 import org.openhab.core.thing.binding.ThingHandler;
47 import org.openhab.core.types.Command;
48 import org.openhab.core.types.RefreshType;
49 import org.slf4j.Logger;
50 import org.slf4j.LoggerFactory;
51
52 import com.google.gson.FieldNamingPolicy;
53 import com.google.gson.Gson;
54 import com.google.gson.GsonBuilder;
55
56 /**
57  * The {@link GeneracMobileLinkAccountHandler} is responsible for connecting to the MobileLink cloud service and
58  * discovering generator things
59  *
60  * @author Dan Cunningham - Initial contribution
61  */
62 @NonNullByDefault
63 public class GeneracMobileLinkAccountHandler extends BaseBridgeHandler {
64     private static final String BASE_URL = "https://api.mobilelinkgen.com";
65     private static final String SHARED_KEY = "GeneseeDepot13";
66     private final Logger logger = LoggerFactory.getLogger(GeneracMobileLinkAccountHandler.class);
67     private final Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.UPPER_CAMEL_CASE).create();
68     private @Nullable Future<?> pollFuture;
69     private @Nullable String authToken;
70     private @Nullable GeneratorStatusResponseDTO generators;
71     private GeneracMobileLinkDiscoveryService discoveryService;
72     private HttpClient httpClient;
73     private int refreshIntervalSeconds = 60;
74
75     public GeneracMobileLinkAccountHandler(Bridge bridge, HttpClient httpClient,
76             GeneracMobileLinkDiscoveryService discoveryService) {
77         super(bridge);
78         this.httpClient = httpClient;
79         this.discoveryService = discoveryService;
80     }
81
82     @Override
83     public void initialize() {
84         updateStatus(ThingStatus.UNKNOWN);
85         authToken = null;
86         restartPoll();
87     }
88
89     @Override
90     public void dispose() {
91         stopPoll();
92     }
93
94     @Override
95     public void handleCommand(ChannelUID channelUID, Command command) {
96         if (command instanceof RefreshType) {
97             updateGeneratorThings();
98         }
99     }
100
101     @Override
102     public void childHandlerInitialized(ThingHandler childHandler, Thing childThing) {
103         GeneratorStatusResponseDTO generatorsLocal = generators;
104         if (generatorsLocal != null) {
105             Optional<GeneratorStatusDTO> generatorOpt = generatorsLocal.stream()
106                     .filter(g -> String.valueOf(g.gensetID).equals(childThing.getUID().getId())).findFirst();
107             if (generatorOpt.isPresent()) {
108                 ((GeneracMobileLinkGeneratorHandler) childHandler).updateGeneratorStatus(generatorOpt.get());
109             }
110         }
111     }
112
113     private void stopPoll() {
114         Future<?> localPollFuture = pollFuture;
115         if (localPollFuture != null) {
116             localPollFuture.cancel(true);
117         }
118     }
119
120     private void restartPoll() {
121         stopPoll();
122         pollFuture = scheduler.scheduleWithFixedDelay(this::poll, 0, refreshIntervalSeconds, TimeUnit.SECONDS);
123     }
124
125     private void poll() {
126         try {
127             if (authToken == null) {
128                 logger.debug("Attempting Login");
129                 login();
130             }
131             getStatuses(true);
132         } catch (InterruptedException e) {
133         }
134     }
135
136     private synchronized void login() throws InterruptedException {
137         GeneracMobileLinkAccountConfiguration config = getConfigAs(GeneracMobileLinkAccountConfiguration.class);
138         refreshIntervalSeconds = config.refreshInterval;
139         HTTPResult result = sendRequest(BASE_URL + "/Users/login", HttpMethod.POST, null,
140                 new StringContentProvider(
141                         gson.toJson(new LoginRequestDTO(SHARED_KEY, config.username, config.password))),
142                 "application/json");
143         if (result.responseCode == HttpStatus.OK_200) {
144             LoginResponseDTO loginResponse = gson.fromJson(result.content, LoginResponseDTO.class);
145             if (loginResponse != null) {
146                 authToken = loginResponse.authToken;
147                 updateStatus(ThingStatus.ONLINE);
148             }
149         } else {
150             handleErrorResponse(result);
151             if (thing.getStatusInfo().getStatusDetail() == ThingStatusDetail.CONFIGURATION_ERROR) {
152                 // bad credentials, stop trying to login
153                 stopPoll();
154             }
155         }
156     }
157
158     private void getStatuses(boolean retry) throws InterruptedException {
159         if (authToken == null) {
160             return;
161         }
162         HTTPResult result = sendRequest(BASE_URL + "/Generator/GeneratorStatus", HttpMethod.GET, authToken, null, null);
163         if (result.responseCode == HttpStatus.OK_200) {
164             generators = gson.fromJson(result.content, GeneratorStatusResponseDTO.class);
165             updateGeneratorThings();
166             if (getThing().getStatus() != ThingStatus.ONLINE) {
167                 updateStatus(ThingStatus.ONLINE);
168             }
169         } else {
170             if (retry) {
171                 logger.debug("Retrying status request");
172                 getStatuses(false);
173             } else {
174                 handleErrorResponse(result);
175             }
176         }
177     }
178
179     private HTTPResult sendRequest(String url, HttpMethod method, @Nullable String token,
180             @Nullable ContentProvider content, @Nullable String contentType) throws InterruptedException {
181         try {
182             Request request = httpClient.newRequest(url).method(method).timeout(10, TimeUnit.SECONDS);
183             if (token != null) {
184                 request = request.header("AuthToken", token);
185             }
186             if (content != null & contentType != null) {
187                 request = request.content(content, contentType);
188             }
189             logger.trace("Sending {} to {}", request.getMethod(), request.getURI());
190             final CompletableFuture<HTTPResult> futureResult = new CompletableFuture<>();
191             request.send(new BufferingResponseListener() {
192                 @NonNullByDefault({})
193                 @Override
194                 public void onComplete(Result result) {
195                     futureResult.complete(new HTTPResult(result.getResponse().getStatus(), getContentAsString()));
196                 }
197             });
198             HTTPResult result = futureResult.get();
199             logger.trace("Response - status: {} content: {}", result.responseCode, result.content);
200             return result;
201         } catch (ExecutionException e) {
202             return new HTTPResult(0, e.getMessage());
203         }
204     }
205
206     private void handleErrorResponse(HTTPResult result) {
207         switch (result.responseCode) {
208             case HttpStatus.UNAUTHORIZED_401:
209                 // the server responds with a 500 error in some cases when credentials are not correct
210             case HttpStatus.INTERNAL_SERVER_ERROR_500:
211                 // server returned a valid error response
212                 ErrorResponseDTO error = gson.fromJson(result.content, ErrorResponseDTO.class);
213                 if (error != null && error.errorCode > 0) {
214                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
215                             "Unauthorized: " + result.content);
216                     authToken = null;
217                     break;
218                 }
219             default:
220                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, result.content);
221         }
222     }
223
224     private void updateGeneratorThings() {
225         GeneratorStatusResponseDTO generatorsLocal = generators;
226         if (generatorsLocal != null) {
227             generatorsLocal.forEach(generator -> {
228                 Thing thing = getThing().getThing(new ThingUID(GeneracMobileLinkBindingConstants.THING_TYPE_GENERATOR,
229                         getThing().getUID(), String.valueOf(generator.gensetID)));
230                 if (thing == null) {
231                     discoveryService.generatorDiscovered(generator, getThing().getUID());
232                 } else {
233                     ThingHandler handler = thing.getHandler();
234                     if (handler != null) {
235                         ((GeneracMobileLinkGeneratorHandler) handler).updateGeneratorStatus(generator);
236                     }
237                 }
238             });
239         }
240     }
241
242     public static class HTTPResult {
243         public @Nullable String content;
244         public final int responseCode;
245
246         public HTTPResult(int responseCode, @Nullable String content) {
247             this.responseCode = responseCode;
248             this.content = content;
249         }
250     }
251 }