2 * Copyright (c) 2010-2024 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.sncf.internal.handler;
15 import static org.eclipse.jetty.http.HttpMethod.GET;
16 import static org.eclipse.jetty.http.HttpStatus.OK_200;
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;
24 import java.util.concurrent.ExecutionException;
25 import java.util.concurrent.TimeUnit;
26 import java.util.concurrent.TimeoutException;
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;
55 import com.google.gson.Gson;
56 import com.google.gson.JsonSyntaxException;
59 * The {@link SncfBridgeHandler} is handles connection and communication toward
62 * @author Gaƫl L'hopital - Initial contribution
65 public class SncfBridgeHandler extends BaseBridgeHandler {
66 public static final String JSON_CONTENT_TYPE = "application/json";
68 public static final String SERVICE_URL = "https://api.sncf.com/v1/coverage/sncf/";
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;
75 private final Gson gson;
76 private @NonNullByDefault({}) String apiId;
78 public SncfBridgeHandler(Bridge bridge, Gson gson, LocationProvider locationProvider, HttpClient httpClient) {
80 this.locationProvider = locationProvider;
81 this.httpClient = httpClient;
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);
92 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/null-or-empty-api-key");
97 public void handleCommand(ChannelUID channelUID, Command command) {
98 logger.debug("SNCF API Bridge is read-only and does not handle commands");
101 private <T extends SncfAnswer> T getResponseFromCache(String url, Class<T> objectClass) throws SncfException {
102 String answer = cache.putIfAbsentAndGet(url, () -> getResponse(url));
104 if (answer != null) {
106 T response = gson.fromJson(answer, objectClass);
107 if (response == null) {
108 throw new SncfException("Unable to deserialize API answer");
110 if (response.message != null) {
111 throw new SncfException(response.message);
115 throw new SncfException(String.format("Unable to get api answer for url : %s", url));
117 } catch (JsonSyntaxException e) {
118 throw new SncfException(e);
122 private @Nullable String getResponse(String url) {
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) {
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();
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;
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();
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,
159 List<Passage> passages = getResponseFromCache(url, Passages.class).passages;
160 return passages != null && !passages.isEmpty() ? Optional.ofNullable(passages.get(0)) : Optional.empty();
163 public LocationProvider getLocationProvider() {
164 return locationProvider;
168 public Collection<Class<? extends ThingHandlerService>> getServices() {
169 return Set.of(SncfDiscoveryService.class);