]> git.basschouten.com Git - openhab-addons.git/blob
85ecf51517674618a218ffcb7db37f1304531f0f
[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.sncf.internal.handler;
14
15 import static org.eclipse.jetty.http.HttpMethod.GET;
16 import static org.eclipse.jetty.http.HttpStatus.OK_200;
17
18 import java.time.Duration;
19 import java.util.Collection;
20 import java.util.List;
21 import java.util.Locale;
22 import java.util.Optional;
23 import java.util.Set;
24 import java.util.concurrent.ExecutionException;
25 import java.util.concurrent.TimeUnit;
26 import java.util.concurrent.TimeoutException;
27
28 import org.eclipse.jdt.annotation.NonNullByDefault;
29 import org.eclipse.jdt.annotation.Nullable;
30 import org.eclipse.jetty.client.HttpClient;
31 import org.eclipse.jetty.client.api.ContentResponse;
32 import org.eclipse.jetty.http.HttpHeader;
33 import org.openhab.binding.sncf.internal.SncfException;
34 import org.openhab.binding.sncf.internal.discovery.SncfDiscoveryService;
35 import org.openhab.binding.sncf.internal.dto.Passage;
36 import org.openhab.binding.sncf.internal.dto.Passages;
37 import org.openhab.binding.sncf.internal.dto.PlaceNearby;
38 import org.openhab.binding.sncf.internal.dto.PlacesNearby;
39 import org.openhab.binding.sncf.internal.dto.SncfAnswer;
40 import org.openhab.binding.sncf.internal.dto.StopPoint;
41 import org.openhab.binding.sncf.internal.dto.StopPoints;
42 import org.openhab.core.cache.ExpiringCacheMap;
43 import org.openhab.core.i18n.LocationProvider;
44 import org.openhab.core.library.types.PointType;
45 import org.openhab.core.thing.Bridge;
46 import org.openhab.core.thing.ChannelUID;
47 import org.openhab.core.thing.ThingStatus;
48 import org.openhab.core.thing.ThingStatusDetail;
49 import org.openhab.core.thing.binding.BaseBridgeHandler;
50 import org.openhab.core.thing.binding.ThingHandlerService;
51 import org.openhab.core.types.Command;
52 import org.slf4j.Logger;
53 import org.slf4j.LoggerFactory;
54
55 import com.google.gson.Gson;
56 import com.google.gson.JsonSyntaxException;
57
58 /**
59  * The {@link SncfBridgeHandler} is handles connection and communication toward
60  * SNCF API
61  *
62  * @author GaĆ«l L'hopital - Initial contribution
63  */
64 @NonNullByDefault
65 public class SncfBridgeHandler extends BaseBridgeHandler {
66     public static final String JSON_CONTENT_TYPE = "application/json";
67
68     public static final String SERVICE_URL = "https://api.sncf.com/v1/coverage/sncf/";
69
70     private final Logger logger = LoggerFactory.getLogger(SncfBridgeHandler.class);
71     private final LocationProvider locationProvider;
72     private final ExpiringCacheMap<String, @Nullable String> cache = new ExpiringCacheMap<>(Duration.ofMinutes(1));
73     private final HttpClient httpClient;
74
75     private final Gson gson;
76     private @NonNullByDefault({}) String apiId;
77
78     public SncfBridgeHandler(Bridge bridge, Gson gson, LocationProvider locationProvider, HttpClient httpClient) {
79         super(bridge);
80         this.locationProvider = locationProvider;
81         this.httpClient = httpClient;
82         this.gson = gson;
83     }
84
85     @Override
86     public void initialize() {
87         logger.debug("Initializing SNCF API bridge handler.");
88         apiId = (String) getConfig().get("apiID");
89         if (apiId != null && !apiId.isBlank()) {
90             updateStatus(ThingStatus.ONLINE);
91         } else {
92             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/null-or-empty-api-key");
93         }
94     }
95
96     @Override
97     public void handleCommand(ChannelUID channelUID, Command command) {
98         logger.debug("SNCF API Bridge is read-only and does not handle commands");
99     }
100
101     private <T extends SncfAnswer> T getResponseFromCache(String url, Class<T> objectClass) throws SncfException {
102         String answer = cache.putIfAbsentAndGet(url, () -> getResponse(url));
103         try {
104             if (answer != null) {
105                 @Nullable
106                 T response = gson.fromJson(answer, objectClass);
107                 if (response == null) {
108                     throw new SncfException("Unable to deserialize API answer");
109                 }
110                 if (response.message != null) {
111                     throw new SncfException(response.message);
112                 }
113                 return response;
114             } else {
115                 throw new SncfException(String.format("Unable to get api answer for url : %s", url));
116             }
117         } catch (JsonSyntaxException e) {
118             throw new SncfException(e);
119         }
120     }
121
122     private @Nullable String getResponse(String url) {
123         try {
124             logger.debug("SNCF Api request: url = '{}'", url);
125             ContentResponse contentResponse = httpClient.newRequest(url).method(GET).timeout(10, TimeUnit.SECONDS)
126                     .header(HttpHeader.AUTHORIZATION, apiId).send();
127             int httpStatus = contentResponse.getStatus();
128             String content = contentResponse.getContentAsString();
129             logger.debug("SNCF Api response: status = {}, content = '{}'", httpStatus, content);
130             if (httpStatus == OK_200) {
131                 return content;
132             }
133             logger.debug("SNCF Api server responded with status code {}: {}", httpStatus, content);
134         } catch (TimeoutException | ExecutionException e) {
135             logger.debug("Execution occured : {}", e.getMessage(), e);
136         } catch (InterruptedException e) {
137             logger.debug("Execution interrupted : {}", e.getMessage(), e);
138             Thread.currentThread().interrupt();
139         }
140         return null;
141     }
142
143     public @Nullable List<PlaceNearby> discoverNearby(PointType location, int distance) throws SncfException {
144         String url = String.format(Locale.US, "%scoord/%.5f;%.5f/places_nearby?distance=%d&type[]=stop_point&count=100",
145                 SERVICE_URL, location.getLongitude().floatValue(), location.getLatitude().floatValue(), distance);
146         PlacesNearby places = getResponseFromCache(url, PlacesNearby.class);
147         return places.placesNearby;
148     }
149
150     public Optional<StopPoint> stopPointDetail(String stopPointId) throws SncfException {
151         String url = String.format("%sstop_points/%s", SERVICE_URL, stopPointId);
152         List<StopPoint> points = getResponseFromCache(url, StopPoints.class).stopPoints;
153         return points != null && !points.isEmpty() ? Optional.ofNullable(points.get(0)) : Optional.empty();
154     }
155
156     public Optional<Passage> getNextPassage(String stopPointId, String expected) throws SncfException {
157         String url = String.format("%sstop_points/%s/%s?disable_geojson=true&count=1", SERVICE_URL, stopPointId,
158                 expected);
159         List<Passage> passages = getResponseFromCache(url, Passages.class).passages;
160         return passages != null && !passages.isEmpty() ? Optional.ofNullable(passages.get(0)) : Optional.empty();
161     }
162
163     public LocationProvider getLocationProvider() {
164         return locationProvider;
165     }
166
167     @Override
168     public Collection<Class<? extends ThingHandlerService>> getServices() {
169         return Set.of(SncfDiscoveryService.class);
170     }
171 }