diff --git a/CODEOWNERS b/CODEOWNERS
index 3b4960540..0bb108fa7 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -279,6 +279,7 @@
/bundles/org.openhab.binding.smartmeter/ @msteigenberger
/bundles/org.openhab.binding.smartthings/ @BobRak
/bundles/org.openhab.binding.smhi/ @pacive
+/bundles/org.openhab.binding.sncf/ @clinique
/bundles/org.openhab.binding.snmp/ @openhab/add-ons-maintainers
/bundles/org.openhab.binding.solaredge/ @alexf2015
/bundles/org.openhab.binding.solarlog/ @johannrichard
diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml
index 4fe676379..c35422590 100644
--- a/bom/openhab-addons/pom.xml
+++ b/bom/openhab-addons/pom.xml
@@ -1386,6 +1386,11 @@
org.openhab.binding.smhi
${project.version}
+
+ org.openhab.addons.bundles
+ org.openhab.binding.sncf
+ ${project.version}
+
org.openhab.addons.bundles
org.openhab.binding.snmp
diff --git a/bundles/org.openhab.binding.sncf/NOTICE b/bundles/org.openhab.binding.sncf/NOTICE
new file mode 100644
index 000000000..38d625e34
--- /dev/null
+++ b/bundles/org.openhab.binding.sncf/NOTICE
@@ -0,0 +1,13 @@
+This content is produced and maintained by the openHAB project.
+
+* Project home: https://www.openhab.org
+
+== Declared Project Licenses
+
+This program and the accompanying materials are made available under the terms
+of the Eclipse Public License 2.0 which is available at
+https://www.eclipse.org/legal/epl-2.0/.
+
+== Source Code
+
+https://github.com/openhab/openhab-addons
diff --git a/bundles/org.openhab.binding.sncf/README.md b/bundles/org.openhab.binding.sncf/README.md
new file mode 100644
index 000000000..0c6ad36a7
--- /dev/null
+++ b/bundles/org.openhab.binding.sncf/README.md
@@ -0,0 +1,87 @@
+# SNCF Binding
+
+The SNCF binding provides real-time data(*) for each train, bus, tramway... station in France.
+This is based on live API provided by DIGITALSNCF.
+
+Get your API key on [DIGITALSNCF web site](https://www.digital.sncf.com/startup/api/token-developpeur)
+
+Note : SNCF Api is based on the open [API Navitia](https://doc.navitia.io/#getting-started).
+This binding uses a very small subset of it, restricted to its primary purpose.
+
+(*) According to DIGITALSNCF Transilien may only be available for schedule, maybe not real-time.
+
+## Supported Things
+
+Bridge: The binding supports a bridge to connect to the [DIGITALSNCF service](https://www.digital.sncf.com/startup/api/token developpeur).
+A bridge uses the thing ID "api".
+
+Station: Represents a given bus, train station.
+
+Of course, you can add as many stations as needed.
+
+
+## Discovery
+
+This binding takes care of auto discovery. This method is strongly recommended as it is the only way to get proper station ID depending upon transportation type.
+
+To enable auto-discovery, your location system setting must be defined.
+Once done, at first launch, discovery will search every station in a radius of 2000 m around the system, extending it by step of 500 m until it finds a first set of results.
+Every following manual successive launch will extend this radius by 500 m, increasing the number of stations discovered.
+
+
+## Binding Configuration
+
+The binding has no configuration options, all configuration is done at Thing level.
+
+## Bridge Configuration
+
+The bridge configuration only holds the api key :
+
+| Parameter | Description |
+|-----------|----------------------------------------------------------------|
+| apiID | API ID provided by the DIGITALSNCF service. Mandatory. |
+
+## Thing Configuration
+
+The 'Station' thing has only one configuration parameter:
+
+| Parameter | Description |
+|-------------|--------------------------------------------------------------|
+| stopPointId | Identifier of the station in the DIGITALSNCF network. |
+
+The thing will auto-update depending on the timestamp of the earliest event detected to trigger (arrival or departure).
+
+## Channels
+
+The Station thing holds two groups of channels (arrivals and departures) containing these channels:
+
+| Channel ID | Item Type | Description |
+|-----------------------|-----------|--------------------------------------------------|
+| direction | String | The direction of the route |
+| lineName | String | Commercial name of the line |
+| name | String | Name of the line |
+| network | String | Name of the network ruling the line |
+| timestamp | DateTime | Timestamp of the event (departure, arrival) |
+
+## Full Example
+
+sncf.things:
+
+```
+Bridge sncf:api:8901d44a68 "Bridge" [apiID="xxx-yyy-zzz"] {
+ station MyHouse "Krakow"[stopPointId="stop_point:SNCF:87561951:Bus"]
+}
+```
+
+sncf.items:
+
+```
+String Arrival_Direction { channel="sncf:station:8901d44a68:87381475_RapidTransit:arrivals#direction" }
+String Arrival_Line { channel="sncf:station:8901d44a68:87381475_RapidTransit:arrivals#lineName" }
+DateTime Arrival_Time { channel="sncf:station:8901d44a68:87381475_RapidTransit:arrivals#timestamp" }
+String Departure_Direction { channel="sncf:station:8901d44a68:87381475_RapidTransit:departures#direction" }
+String Departure_Line { channel="sncf:station:8901d44a68:87381475_RapidTransit:departures#lineName" }
+DateTime Departure_Time { channel="sncf:station:8901d44a68:87381475_RapidTransit:departures#timestamp" }
+
+```
+
diff --git a/bundles/org.openhab.binding.sncf/pom.xml b/bundles/org.openhab.binding.sncf/pom.xml
new file mode 100644
index 000000000..aefe54115
--- /dev/null
+++ b/bundles/org.openhab.binding.sncf/pom.xml
@@ -0,0 +1,17 @@
+
+
+
+ 4.0.0
+
+
+ org.openhab.addons.bundles
+ org.openhab.addons.reactor.bundles
+ 3.2.0-SNAPSHOT
+
+
+ org.openhab.binding.sncf
+
+ openHAB Add-ons :: Bundles :: SNCF Binding
+
+
diff --git a/bundles/org.openhab.binding.sncf/src/main/feature/feature.xml b/bundles/org.openhab.binding.sncf/src/main/feature/feature.xml
new file mode 100644
index 000000000..7acc033ca
--- /dev/null
+++ b/bundles/org.openhab.binding.sncf/src/main/feature/feature.xml
@@ -0,0 +1,9 @@
+
+
+ mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features
+
+
+ openhab-runtime-base
+ mvn:org.openhab.addons.bundles/org.openhab.binding.sncf/${project.version}
+
+
diff --git a/bundles/org.openhab.binding.sncf/src/main/java/org/openhab/binding/sncf/internal/SncfBindingConstants.java b/bundles/org.openhab.binding.sncf/src/main/java/org/openhab/binding/sncf/internal/SncfBindingConstants.java
new file mode 100644
index 000000000..8f68576e8
--- /dev/null
+++ b/bundles/org.openhab.binding.sncf/src/main/java/org/openhab/binding/sncf/internal/SncfBindingConstants.java
@@ -0,0 +1,54 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.sncf.internal;
+
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.thing.ThingTypeUID;
+
+/**
+ * The {@link SncfBindingConstants} class defines common constants, which are
+ * used across the whole binding.
+ *
+ * @author Gaël L'hopital - Initial contribution
+ */
+@NonNullByDefault
+public class SncfBindingConstants {
+
+ public static final String BINDING_ID = "sncf";
+
+ // Station properties
+ public static final String STOP_POINT_ID = "stopPointId";
+ public static final String DISTANCE = "Distance";
+ public static final String LOCATION = "Location";
+ public static final String TIMEZONE = "Timezone";
+
+ // List of Channel groups
+ public static final String GROUP_ARRIVAL = "arrivals";
+ public static final String GROUP_DEPARTURE = "departures";
+
+ // List of Channel id's
+ public static final String DIRECTION = "direction";
+ public static final String LINE_NAME = "lineName";
+ public static final String NAME = "name";
+ public static final String NETWORK = "network";
+ public static final String TIMESTAMP = "timestamp";
+
+ // List of Thing Type UIDs
+ public static final ThingTypeUID APIBRIDGE_THING_TYPE = new ThingTypeUID(BINDING_ID, "api");
+ public static final ThingTypeUID STATION_THING_TYPE = new ThingTypeUID(BINDING_ID, "station");
+
+ // List of all adressable things
+ public static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(APIBRIDGE_THING_TYPE, STATION_THING_TYPE);
+}
diff --git a/bundles/org.openhab.binding.sncf/src/main/java/org/openhab/binding/sncf/internal/SncfException.java b/bundles/org.openhab.binding.sncf/src/main/java/org/openhab/binding/sncf/internal/SncfException.java
new file mode 100644
index 000000000..de12a4097
--- /dev/null
+++ b/bundles/org.openhab.binding.sncf/src/main/java/org/openhab/binding/sncf/internal/SncfException.java
@@ -0,0 +1,38 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.sncf.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * Exception for errors when using the SNCF API
+ *
+ * @author Gaël L'hopital - Initial contribution
+ */
+@NonNullByDefault
+public class SncfException extends Exception {
+ private static final long serialVersionUID = -6215621577081394328L;
+
+ public SncfException(String label) {
+ super(label);
+ }
+
+ public SncfException(Throwable e) {
+ super(e);
+ }
+
+ public SncfException(@Nullable String message, @Nullable Throwable e) {
+ super(message, e);
+ }
+}
diff --git a/bundles/org.openhab.binding.sncf/src/main/java/org/openhab/binding/sncf/internal/SncfHandlerFactory.java b/bundles/org.openhab.binding.sncf/src/main/java/org/openhab/binding/sncf/internal/SncfHandlerFactory.java
new file mode 100644
index 000000000..131e5d942
--- /dev/null
+++ b/bundles/org.openhab.binding.sncf/src/main/java/org/openhab/binding/sncf/internal/SncfHandlerFactory.java
@@ -0,0 +1,78 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.sncf.internal;
+
+import static org.openhab.binding.sncf.internal.SncfBindingConstants.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
+import org.openhab.binding.sncf.internal.handler.SncfBridgeHandler;
+import org.openhab.binding.sncf.internal.handler.StationHandler;
+import org.openhab.core.i18n.LocationProvider;
+import org.openhab.core.io.net.http.HttpClientFactory;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.binding.BaseThingHandlerFactory;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.thing.binding.ThingHandlerFactory;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.FieldNamingPolicy;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+
+/**
+ * The {@link SncfHandlerFactory} is responsible for creating things and thing
+ * handlers.
+ *
+ * @author Gaël L'hopital - Initial contribution
+ */
+@NonNullByDefault
+@Component(configurationPid = "binding.sncf", service = ThingHandlerFactory.class)
+public class SncfHandlerFactory extends BaseThingHandlerFactory {
+ private final Logger logger = LoggerFactory.getLogger(SncfHandlerFactory.class);
+ private final LocationProvider locationProvider;
+ private final HttpClient httpClient;
+ private final Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
+ .create();
+
+ @Activate
+ public SncfHandlerFactory(@Reference LocationProvider locationProvider,
+ final @Reference HttpClientFactory httpClientFactory) {
+ this.locationProvider = locationProvider;
+ this.httpClient = httpClientFactory.getCommonHttpClient();
+ }
+
+ @Override
+ public boolean supportsThingType(ThingTypeUID thingTypeUID) {
+ return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
+ }
+
+ @Override
+ protected @Nullable ThingHandler createHandler(Thing thing) {
+ ThingTypeUID thingTypeUID = thing.getThingTypeUID();
+ if (APIBRIDGE_THING_TYPE.equals(thingTypeUID)) {
+ return new SncfBridgeHandler((Bridge) thing, gson, locationProvider, httpClient);
+ } else if (STATION_THING_TYPE.equals(thingTypeUID)) {
+ return new StationHandler(thing, locationProvider);
+ }
+ logger.warn("ThingHandler not found for {}", thing.getThingTypeUID());
+ return null;
+ }
+}
diff --git a/bundles/org.openhab.binding.sncf/src/main/java/org/openhab/binding/sncf/internal/discovery/SncfDiscoveryService.java b/bundles/org.openhab.binding.sncf/src/main/java/org/openhab/binding/sncf/internal/discovery/SncfDiscoveryService.java
new file mode 100644
index 000000000..73f96c6ce
--- /dev/null
+++ b/bundles/org.openhab.binding.sncf/src/main/java/org/openhab/binding/sncf/internal/discovery/SncfDiscoveryService.java
@@ -0,0 +1,115 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.sncf.internal.discovery;
+
+import static org.openhab.binding.sncf.internal.SncfBindingConstants.*;
+
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.sncf.internal.SncfException;
+import org.openhab.binding.sncf.internal.dto.PlaceNearby;
+import org.openhab.binding.sncf.internal.handler.SncfBridgeHandler;
+import org.openhab.core.config.discovery.AbstractDiscoveryService;
+import org.openhab.core.config.discovery.DiscoveryResultBuilder;
+import org.openhab.core.i18n.LocationProvider;
+import org.openhab.core.library.types.PointType;
+import org.openhab.core.thing.ThingUID;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.thing.binding.ThingHandlerService;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link SncfDiscoveryService} searches for available
+ * station discoverable through API
+ *
+ * @author Gaël L'hopital - Initial contribution
+ */
+@Component(service = ThingHandlerService.class)
+@NonNullByDefault
+public class SncfDiscoveryService extends AbstractDiscoveryService implements ThingHandlerService {
+ private static final int SEARCH_TIME = 7;
+
+ private final Logger logger = LoggerFactory.getLogger(SncfDiscoveryService.class);
+
+ private @Nullable LocationProvider locationProvider;
+ private @Nullable SncfBridgeHandler bridgeHandler;
+
+ private int searchRange = 1500;
+
+ @Activate
+ public SncfDiscoveryService() {
+ super(SUPPORTED_THING_TYPES_UIDS, SEARCH_TIME, false);
+ }
+
+ @Override
+ public void deactivate() {
+ super.deactivate();
+ }
+
+ @Override
+ public void startScan() {
+ SncfBridgeHandler handler = bridgeHandler;
+ LocationProvider provider = locationProvider;
+ if (provider != null && handler != null) {
+ PointType location = provider.getLocation();
+ if (location != null) {
+ ThingUID bridgeUID = handler.getThing().getUID();
+ searchRange += 500;
+ try {
+ List places = handler.discoverNearby(location, searchRange);
+ if (places != null && !places.isEmpty()) {
+ places.forEach(place -> {
+ // stop_point:SNCF:87386573:Bus
+ List idElts = new LinkedList(Arrays.asList(place.id.split(":")));
+ idElts.remove(0);
+ idElts.remove(0);
+ thingDiscovered(DiscoveryResultBuilder
+ .create(new ThingUID(STATION_THING_TYPE, bridgeUID, String.join("_", idElts)))
+ .withLabel(String.format("%s (%s)", place.stopPoint.name, idElts.get(1))
+ .replace("-", "_"))
+ .withBridge(bridgeUID).withRepresentationProperty(STOP_POINT_ID)
+ .withProperty(STOP_POINT_ID, place.id).build());
+ });
+ } else {
+ logger.info("No station found in a perimeter of {} m, extending search", searchRange);
+ startScan();
+ }
+ } catch (SncfException e) {
+ logger.warn("Error calling SNCF Api : {}", e.getMessage());
+ }
+ } else {
+ logger.info("Please set a system location to enable station discovery");
+ }
+ }
+ }
+
+ @Override
+ public void setThingHandler(ThingHandler handler) {
+ if (handler instanceof SncfBridgeHandler) {
+ this.bridgeHandler = (SncfBridgeHandler) handler;
+ this.locationProvider = ((SncfBridgeHandler) handler).getLocationProvider();
+ }
+ }
+
+ @Override
+ public @Nullable ThingHandler getThingHandler() {
+ return bridgeHandler;
+ }
+}
diff --git a/bundles/org.openhab.binding.sncf/src/main/java/org/openhab/binding/sncf/internal/dto/Coord.java b/bundles/org.openhab.binding.sncf/src/main/java/org/openhab/binding/sncf/internal/dto/Coord.java
new file mode 100644
index 000000000..7441c5c7f
--- /dev/null
+++ b/bundles/org.openhab.binding.sncf/src/main/java/org/openhab/binding/sncf/internal/dto/Coord.java
@@ -0,0 +1,23 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.sncf.internal.dto;
+
+/**
+ * The {@link Coord} class holds latitude and longitude of a point
+ *
+ * @author Gaël L'hopital - Initial contribution
+ */
+public class Coord {
+ public String lat;
+ public String lon;
+}
diff --git a/bundles/org.openhab.binding.sncf/src/main/java/org/openhab/binding/sncf/internal/dto/NavitiaObject.java b/bundles/org.openhab.binding.sncf/src/main/java/org/openhab/binding/sncf/internal/dto/NavitiaObject.java
new file mode 100644
index 000000000..56b514aaa
--- /dev/null
+++ b/bundles/org.openhab.binding.sncf/src/main/java/org/openhab/binding/sncf/internal/dto/NavitiaObject.java
@@ -0,0 +1,23 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.sncf.internal.dto;
+
+/**
+ * The {@link NavitiaObject} base class for API objects
+ *
+ * @author Gaël L'hopital - Initial contribution
+ */
+public class NavitiaObject {
+ public String id;
+ public String name;
+}
diff --git a/bundles/org.openhab.binding.sncf/src/main/java/org/openhab/binding/sncf/internal/dto/Passage.java b/bundles/org.openhab.binding.sncf/src/main/java/org/openhab/binding/sncf/internal/dto/Passage.java
new file mode 100644
index 000000000..0a9fbe694
--- /dev/null
+++ b/bundles/org.openhab.binding.sncf/src/main/java/org/openhab/binding/sncf/internal/dto/Passage.java
@@ -0,0 +1,24 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.sncf.internal.dto;
+
+/**
+ * The {@link Passage} holds data regarding a transportation
+ * information passing at a given station
+ *
+ * @author Gaël L'hopital - Initial contribution
+ */
+public class Passage {
+ public VJDisplayInformation displayInformations;
+ public StopDateTime stopDateTime;
+}
diff --git a/bundles/org.openhab.binding.sncf/src/main/java/org/openhab/binding/sncf/internal/dto/Passages.java b/bundles/org.openhab.binding.sncf/src/main/java/org/openhab/binding/sncf/internal/dto/Passages.java
new file mode 100644
index 000000000..f4988bf3c
--- /dev/null
+++ b/bundles/org.openhab.binding.sncf/src/main/java/org/openhab/binding/sncf/internal/dto/Passages.java
@@ -0,0 +1,30 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.sncf.internal.dto;
+
+import java.util.List;
+
+import org.eclipse.jdt.annotation.Nullable;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * The {@link Passages} is responsible for storing
+ * list of arrivals or departures depending upon called API
+ *
+ * @author Gaël L'hopital - Initial contribution
+ */
+public class Passages extends SncfAnswer {
+ @SerializedName(value = "departures", alternate = "arrivals")
+ public @Nullable List passages;
+}
diff --git a/bundles/org.openhab.binding.sncf/src/main/java/org/openhab/binding/sncf/internal/dto/PlaceNearby.java b/bundles/org.openhab.binding.sncf/src/main/java/org/openhab/binding/sncf/internal/dto/PlaceNearby.java
new file mode 100644
index 000000000..a2931351a
--- /dev/null
+++ b/bundles/org.openhab.binding.sncf/src/main/java/org/openhab/binding/sncf/internal/dto/PlaceNearby.java
@@ -0,0 +1,22 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.sncf.internal.dto;
+
+/**
+ * The {@link PlaceNearby} holds data returned by the API call
+ *
+ * @author Gaël L'hopital - Initial contribution
+ */
+public class PlaceNearby extends NavitiaObject {
+ public StopPoint stopPoint;
+}
diff --git a/bundles/org.openhab.binding.sncf/src/main/java/org/openhab/binding/sncf/internal/dto/PlacesNearby.java b/bundles/org.openhab.binding.sncf/src/main/java/org/openhab/binding/sncf/internal/dto/PlacesNearby.java
new file mode 100644
index 000000000..9141872b7
--- /dev/null
+++ b/bundles/org.openhab.binding.sncf/src/main/java/org/openhab/binding/sncf/internal/dto/PlacesNearby.java
@@ -0,0 +1,24 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.sncf.internal.dto;
+
+import java.util.List;
+
+/**
+ * The {@link PlacesNearby} holds a list or nearby places.
+ *
+ * @author Gaël L'hopital - Initial contribution
+ */
+public class PlacesNearby extends SncfAnswer {
+ public List placesNearby;
+}
diff --git a/bundles/org.openhab.binding.sncf/src/main/java/org/openhab/binding/sncf/internal/dto/SncfAnswer.java b/bundles/org.openhab.binding.sncf/src/main/java/org/openhab/binding/sncf/internal/dto/SncfAnswer.java
new file mode 100644
index 000000000..b572f8469
--- /dev/null
+++ b/bundles/org.openhab.binding.sncf/src/main/java/org/openhab/binding/sncf/internal/dto/SncfAnswer.java
@@ -0,0 +1,23 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.sncf.internal.dto;
+
+/**
+ * The {@link SncfAnswer} is the base class for all Sncf API requests
+ *
+ * @author Gaël L'hopital - Initial contribution
+ */
+public abstract class SncfAnswer {
+ public Error error;
+ public String message;
+}
diff --git a/bundles/org.openhab.binding.sncf/src/main/java/org/openhab/binding/sncf/internal/dto/StopArea.java b/bundles/org.openhab.binding.sncf/src/main/java/org/openhab/binding/sncf/internal/dto/StopArea.java
new file mode 100644
index 000000000..706874a1f
--- /dev/null
+++ b/bundles/org.openhab.binding.sncf/src/main/java/org/openhab/binding/sncf/internal/dto/StopArea.java
@@ -0,0 +1,23 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.sncf.internal.dto;
+
+/**
+ * The {@link StopArea} class holds informations for a Stop Area
+ * (usually a train station)
+ *
+ * @author Gaël L'hopital - Initial contribution
+ */
+public class StopArea extends NavitiaObject {
+ public String timezone;
+}
diff --git a/bundles/org.openhab.binding.sncf/src/main/java/org/openhab/binding/sncf/internal/dto/StopDateTime.java b/bundles/org.openhab.binding.sncf/src/main/java/org/openhab/binding/sncf/internal/dto/StopDateTime.java
new file mode 100644
index 000000000..0bcce5cd8
--- /dev/null
+++ b/bundles/org.openhab.binding.sncf/src/main/java/org/openhab/binding/sncf/internal/dto/StopDateTime.java
@@ -0,0 +1,23 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.sncf.internal.dto;
+
+/**
+ * The {@link StopDateTime} class holds informations for a transportation stop
+ *
+ * @author Gaël L'hopital - Initial contribution
+ */
+public class StopDateTime {
+ public String arrivalDateTime;
+ public String departureDateTime;
+}
diff --git a/bundles/org.openhab.binding.sncf/src/main/java/org/openhab/binding/sncf/internal/dto/StopPoint.java b/bundles/org.openhab.binding.sncf/src/main/java/org/openhab/binding/sncf/internal/dto/StopPoint.java
new file mode 100644
index 000000000..e372dc15e
--- /dev/null
+++ b/bundles/org.openhab.binding.sncf/src/main/java/org/openhab/binding/sncf/internal/dto/StopPoint.java
@@ -0,0 +1,23 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.sncf.internal.dto;
+
+/**
+ * The {@link StopPoint} class holds informations for a train station
+ *
+ * @author Gaël L'hopital - Initial contribution
+ */
+public class StopPoint extends NavitiaObject {
+ public StopArea stopArea;
+ public Coord coord;
+}
diff --git a/bundles/org.openhab.binding.sncf/src/main/java/org/openhab/binding/sncf/internal/dto/StopPoints.java b/bundles/org.openhab.binding.sncf/src/main/java/org/openhab/binding/sncf/internal/dto/StopPoints.java
new file mode 100644
index 000000000..578d33e1f
--- /dev/null
+++ b/bundles/org.openhab.binding.sncf/src/main/java/org/openhab/binding/sncf/internal/dto/StopPoints.java
@@ -0,0 +1,26 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.sncf.internal.dto;
+
+import java.util.List;
+
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * The {@link StopPoints} holds a list of Stop Points.
+ *
+ * @author Gaël L'hopital - Initial contribution
+ */
+public class StopPoints extends SncfAnswer {
+ public @Nullable List stopPoints;
+}
diff --git a/bundles/org.openhab.binding.sncf/src/main/java/org/openhab/binding/sncf/internal/dto/VJDisplayInformation.java b/bundles/org.openhab.binding.sncf/src/main/java/org/openhab/binding/sncf/internal/dto/VJDisplayInformation.java
new file mode 100644
index 000000000..7e182f6a6
--- /dev/null
+++ b/bundles/org.openhab.binding.sncf/src/main/java/org/openhab/binding/sncf/internal/dto/VJDisplayInformation.java
@@ -0,0 +1,27 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.sncf.internal.dto;
+
+/**
+ * The {@link VJDisplayInformation} class holds informations displayed
+ * to traveller regarding a stop in the station
+ *
+ * @author Gaël L'hopital - Initial contribution
+ */
+public class VJDisplayInformation {
+ public String code;
+ public String network;
+ public String name;
+ public String commercialMode;
+ public String direction;
+}
diff --git a/bundles/org.openhab.binding.sncf/src/main/java/org/openhab/binding/sncf/internal/handler/SncfBridgeHandler.java b/bundles/org.openhab.binding.sncf/src/main/java/org/openhab/binding/sncf/internal/handler/SncfBridgeHandler.java
new file mode 100644
index 000000000..0ef7fb050
--- /dev/null
+++ b/bundles/org.openhab.binding.sncf/src/main/java/org/openhab/binding/sncf/internal/handler/SncfBridgeHandler.java
@@ -0,0 +1,171 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.sncf.internal.handler;
+
+import static org.eclipse.jetty.http.HttpMethod.GET;
+import static org.eclipse.jetty.http.HttpStatus.OK_200;
+
+import java.time.Duration;
+import java.util.Collection;
+import java.util.List;
+import java.util.Locale;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.api.ContentResponse;
+import org.eclipse.jetty.http.HttpHeader;
+import org.openhab.binding.sncf.internal.SncfException;
+import org.openhab.binding.sncf.internal.discovery.SncfDiscoveryService;
+import org.openhab.binding.sncf.internal.dto.Passage;
+import org.openhab.binding.sncf.internal.dto.Passages;
+import org.openhab.binding.sncf.internal.dto.PlaceNearby;
+import org.openhab.binding.sncf.internal.dto.PlacesNearby;
+import org.openhab.binding.sncf.internal.dto.SncfAnswer;
+import org.openhab.binding.sncf.internal.dto.StopPoint;
+import org.openhab.binding.sncf.internal.dto.StopPoints;
+import org.openhab.core.cache.ExpiringCacheMap;
+import org.openhab.core.i18n.LocationProvider;
+import org.openhab.core.library.types.PointType;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.binding.BaseBridgeHandler;
+import org.openhab.core.thing.binding.ThingHandlerService;
+import org.openhab.core.types.Command;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonSyntaxException;
+
+/**
+ * The {@link SncfBridgeHandler} is handles connection and communication toward
+ * SNCF API
+ *
+ * @author Gaël L'hopital - Initial contribution
+ */
+@NonNullByDefault
+public class SncfBridgeHandler extends BaseBridgeHandler {
+ public static final String JSON_CONTENT_TYPE = "application/json";
+
+ public static final String SERVICE_URL = "https://api.sncf.com/v1/coverage/sncf/";
+
+ private final Logger logger = LoggerFactory.getLogger(SncfBridgeHandler.class);
+ private final LocationProvider locationProvider;
+ private final ExpiringCacheMap cache = new ExpiringCacheMap<>(Duration.ofMinutes(1));
+ private final HttpClient httpClient;
+
+ private final Gson gson;
+ private @NonNullByDefault({}) String apiId;
+
+ public SncfBridgeHandler(Bridge bridge, Gson gson, LocationProvider locationProvider, HttpClient httpClient) {
+ super(bridge);
+ this.locationProvider = locationProvider;
+ this.httpClient = httpClient;
+ this.gson = gson;
+ }
+
+ @Override
+ public void initialize() {
+ logger.debug("Initializing SNCF API bridge handler.");
+ apiId = (String) getConfig().get("apiID");
+ if (apiId != null && !apiId.isBlank()) {
+ updateStatus(ThingStatus.ONLINE);
+ } else {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/null-or-empty-api-key");
+ }
+ }
+
+ @Override
+ public void handleCommand(ChannelUID channelUID, Command command) {
+ logger.debug("SNCF API Bridge is read-only and does not handle commands");
+ }
+
+ private T getResponseFromCache(String url, Class objectClass) throws SncfException {
+ String answer = cache.putIfAbsentAndGet(url, () -> getResponse(url));
+ try {
+ if (answer != null) {
+ @Nullable
+ T response = gson.fromJson(answer, objectClass);
+ if (response == null) {
+ throw new SncfException("Unable to deserialize API answer");
+ }
+ if (response.message != null) {
+ throw new SncfException(response.message);
+ }
+ return response;
+ } else {
+ throw new SncfException(String.format("Unable to get api answer for url : %s", url));
+ }
+ } catch (JsonSyntaxException e) {
+ throw new SncfException(e);
+ }
+ }
+
+ private @Nullable String getResponse(String url) {
+ try {
+ logger.debug("SNCF Api request: url = '{}'", url);
+ ContentResponse contentResponse = httpClient.newRequest(url).method(GET).timeout(10, TimeUnit.SECONDS)
+ .header(HttpHeader.AUTHORIZATION, apiId).send();
+ int httpStatus = contentResponse.getStatus();
+ String content = contentResponse.getContentAsString();
+ logger.debug("SNCF Api response: status = {}, content = '{}'", httpStatus, content);
+ if (httpStatus == OK_200) {
+ return content;
+ }
+ logger.debug("SNCF Api server responded with status code {}: {}", httpStatus, content);
+ } catch (TimeoutException | ExecutionException e) {
+ logger.debug("Execution occured : {}", e.getMessage(), e);
+ } catch (InterruptedException e) {
+ logger.debug("Execution interrupted : {}", e.getMessage(), e);
+ Thread.currentThread().interrupt();
+ }
+ return null;
+ }
+
+ public @Nullable List discoverNearby(PointType location, int distance) throws SncfException {
+ String url = String.format(Locale.US, "%scoord/%.5f;%.5f/places_nearby?distance=%d&type[]=stop_point&count=100",
+ SERVICE_URL, location.getLongitude().floatValue(), location.getLatitude().floatValue(), distance);
+ PlacesNearby places = getResponseFromCache(url, PlacesNearby.class);
+ return places.placesNearby;
+ }
+
+ public Optional stopPointDetail(String stopPointId) throws SncfException {
+ String url = String.format("%sstop_points/%s", SERVICE_URL, stopPointId);
+ List points = getResponseFromCache(url, StopPoints.class).stopPoints;
+ return points != null && !points.isEmpty() ? Optional.ofNullable(points.get(0)) : Optional.empty();
+ }
+
+ public Optional getNextPassage(String stopPointId, String expected) throws SncfException {
+ String url = String.format("%sstop_points/%s/%s?disable_geojson=true&count=1", SERVICE_URL, stopPointId,
+ expected);
+ List passages = getResponseFromCache(url, Passages.class).passages;
+ return passages != null && !passages.isEmpty() ? Optional.ofNullable(passages.get(0)) : Optional.empty();
+ }
+
+ public LocationProvider getLocationProvider() {
+ return locationProvider;
+ }
+
+ @Override
+ public Collection> getServices() {
+ return Set.of(SncfDiscoveryService.class);
+ }
+}
diff --git a/bundles/org.openhab.binding.sncf/src/main/java/org/openhab/binding/sncf/internal/handler/StationHandler.java b/bundles/org.openhab.binding.sncf/src/main/java/org/openhab/binding/sncf/internal/handler/StationHandler.java
new file mode 100644
index 000000000..855199219
--- /dev/null
+++ b/bundles/org.openhab.binding.sncf/src/main/java/org/openhab/binding/sncf/internal/handler/StationHandler.java
@@ -0,0 +1,259 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.sncf.internal.handler;
+
+import static org.openhab.binding.sncf.internal.SncfBindingConstants.*;
+
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.ChronoUnit;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.sncf.internal.SncfException;
+import org.openhab.binding.sncf.internal.dto.Passage;
+import org.openhab.core.i18n.LocationProvider;
+import org.openhab.core.library.types.DateTimeType;
+import org.openhab.core.library.types.PointType;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.library.unit.SIUnits;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.Channel;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.ThingStatusInfo;
+import org.openhab.core.thing.binding.BaseThingHandler;
+import org.openhab.core.thing.binding.BridgeHandler;
+import org.openhab.core.thing.binding.builder.ThingBuilder;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.openhab.core.types.State;
+import org.openhab.core.types.UnDefType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link StationHandler} is responsible for handling commands, which are sent
+ * to one of the channels.
+ *
+ * @author Gaël L'hopital - Initial contribution
+ */
+@NonNullByDefault
+public class StationHandler extends BaseThingHandler {
+ private static final DateTimeFormatter NAVITIA_DATE_FORMAT = DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmssZ");
+
+ private final Logger logger = LoggerFactory.getLogger(StationHandler.class);
+ private final LocationProvider locationProvider;
+
+ private @Nullable ScheduledFuture> refreshJob;
+ private @NonNullByDefault({}) String stationId;
+ private @NonNullByDefault({}) String zoneOffset;
+
+ public StationHandler(Thing thing, LocationProvider locationProvider) {
+ super(thing);
+ this.locationProvider = locationProvider;
+ }
+
+ @Override
+ public void initialize() {
+ logger.trace("Initializing the Station handler for {}", getThing().getUID());
+
+ stationId = (String) getConfig().get("stopPointId");
+ if (stationId == null || stationId.isBlank()) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/null-or-empty-station-id");
+ return;
+ }
+
+ if (thing.getProperties().isEmpty() && !discoverAttributes(stationId)) {
+ return;
+ }
+
+ String timezone = thing.getProperties().get(TIMEZONE);
+ if (timezone == null || timezone.isBlank()) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/null-or-empty-timezone");
+ return;
+ }
+
+ zoneOffset = ZoneId.of(timezone).getRules().getOffset(Instant.now()).getId().replace(":", "");
+ scheduleRefresh(ZonedDateTime.now().plusSeconds(2));
+ updateStatus(ThingStatus.ONLINE);
+ }
+
+ @Override
+ public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
+ super.bridgeStatusChanged(bridgeStatusInfo);
+ if (thing.getStatus() == ThingStatus.ONLINE) {
+ initialize();
+ }
+ }
+
+ private boolean discoverAttributes(String localStation) {
+ SncfBridgeHandler bridgeHandler = getBridgeHandler();
+ if (bridgeHandler != null) {
+ Map properties = new HashMap<>();
+ try {
+ bridgeHandler.stopPointDetail(localStation).ifPresent(stopPoint -> {
+ String stationLoc = String.format("%s,%s", stopPoint.coord.lat, stopPoint.coord.lon);
+ properties.put(LOCATION, stationLoc);
+ properties.put(TIMEZONE, stopPoint.stopArea.timezone);
+ PointType serverLoc = locationProvider.getLocation();
+ if (serverLoc != null) {
+ PointType stationLocation = new PointType(stationLoc);
+ double distance = serverLoc.distanceFrom(stationLocation).doubleValue();
+ properties.put(DISTANCE, new QuantityType<>(distance, SIUnits.METRE).toString());
+ }
+ });
+ ThingBuilder thingBuilder = editThing();
+ thingBuilder.withProperties(properties);
+ updateThing(thingBuilder.build());
+ return true;
+ } catch (SncfException e) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
+ }
+ }
+ return false;
+ }
+
+ private void scheduleRefresh(@Nullable ZonedDateTime when) {
+ // Ensure we'll try to refresh in one minute if no valid timestamp is provided
+ long wishedDelay = ZonedDateTime.now().until(when != null ? when : ZonedDateTime.now().plusMinutes(1),
+ ChronoUnit.SECONDS);
+ wishedDelay = wishedDelay < 0 ? 60 : wishedDelay;
+ logger.debug("wishedDelay is {} seconds", wishedDelay);
+ ScheduledFuture> job = refreshJob;
+ if (job != null) {
+ long existingDelay = job.getDelay(TimeUnit.SECONDS);
+ logger.debug("existingDelay is {} seconds", existingDelay);
+ if (existingDelay < wishedDelay && existingDelay > 0) {
+ logger.debug("Do nothing, existingDelay earlier than wishedDelay");
+ return;
+ }
+ freeRefreshJob();
+ }
+ logger.debug("Scheduling update in {} seconds.", wishedDelay);
+ refreshJob = scheduler.schedule(() -> updateThing(), wishedDelay, TimeUnit.SECONDS);
+ }
+
+ private void updateThing() {
+ SncfBridgeHandler bridgeHandler = getBridgeHandler();
+ if (bridgeHandler != null) {
+ scheduler.submit(() -> {
+ updatePassage(bridgeHandler, GROUP_ARRIVAL);
+ updatePassage(bridgeHandler, GROUP_DEPARTURE);
+ });
+ }
+ }
+
+ private void updatePassage(SncfBridgeHandler bridgeHandler, String direction) {
+ try {
+ bridgeHandler.getNextPassage(stationId, direction).ifPresentOrElse(passage -> {
+ getThing().getChannels().stream().map(Channel::getUID)
+ .filter(channelUID -> isLinked(channelUID) && direction.equals(channelUID.getGroupId()))
+ .forEach(channelUID -> {
+ State state = getValue(channelUID.getIdWithoutGroup(), passage, direction);
+ updateState(channelUID, state);
+ });
+ ZonedDateTime eventTime = getEventTimestamp(passage, direction);
+ if (eventTime != null) {
+ scheduleRefresh(eventTime.plusSeconds(10));
+ }
+ }, () -> {
+ logger.debug("No {} available", direction);
+ scheduleRefresh(ZonedDateTime.now().plusMinutes(5));
+ });
+ updateStatus(ThingStatus.ONLINE);
+ } catch (SncfException e) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
+ freeRefreshJob();
+ }
+ }
+
+ private State getValue(String channelId, Passage passage, String direction) {
+ switch (channelId) {
+ case DIRECTION:
+ return fromNullableString(passage.displayInformations.direction);
+ case LINE_NAME:
+ return fromNullableString(String.format("%s %s", passage.displayInformations.commercialMode,
+ passage.displayInformations.code));
+ case NAME:
+ return fromNullableString(passage.displayInformations.name);
+ case NETWORK:
+ return fromNullableString(passage.displayInformations.network);
+ case TIMESTAMP:
+ return fromNullableTime(passage, direction);
+ }
+ return UnDefType.NULL;
+ }
+
+ private State fromNullableString(@Nullable String aValue) {
+ return aValue != null ? StringType.valueOf(aValue) : UnDefType.NULL;
+ }
+
+ private @Nullable ZonedDateTime getEventTimestamp(Passage passage, String direction) {
+ String eventTime = direction.equals(GROUP_ARRIVAL) ? passage.stopDateTime.arrivalDateTime
+ : passage.stopDateTime.departureDateTime;
+ return eventTime != null ? ZonedDateTime.parse(eventTime + zoneOffset, NAVITIA_DATE_FORMAT) : null;
+ }
+
+ private State fromNullableTime(Passage passage, String direction) {
+ ZonedDateTime timestamp = getEventTimestamp(passage, direction);
+ return timestamp != null ? new DateTimeType(timestamp) : UnDefType.NULL;
+ }
+
+ private void freeRefreshJob() {
+ ScheduledFuture> job = refreshJob;
+ if (job != null) {
+ job.cancel(true);
+ this.refreshJob = null;
+ }
+ }
+
+ @Override
+ public void dispose() {
+ freeRefreshJob();
+ super.dispose();
+ }
+
+ @Override
+ public void handleCommand(ChannelUID channelUID, Command command) {
+ if (command instanceof RefreshType) {
+ updateThing();
+ }
+ }
+
+ private @Nullable SncfBridgeHandler getBridgeHandler() {
+ Bridge bridge = getBridge();
+ if (bridge != null) {
+ BridgeHandler handler = bridge.getHandler();
+ if (handler != null) {
+ if (handler.getThing().getStatus() == ThingStatus.ONLINE) {
+ return (SncfBridgeHandler) handler;
+ } else {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
+ return null;
+ }
+ }
+ }
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
+ return null;
+ }
+}
diff --git a/bundles/org.openhab.binding.sncf/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.sncf/src/main/resources/OH-INF/binding/binding.xml
new file mode 100644
index 000000000..e756b395d
--- /dev/null
+++ b/bundles/org.openhab.binding.sncf/src/main/resources/OH-INF/binding/binding.xml
@@ -0,0 +1,9 @@
+
+
+
+ SNCF Binding
+ Retrieves French railway informations
+
+
diff --git a/bundles/org.openhab.binding.sncf/src/main/resources/OH-INF/config/config.xml b/bundles/org.openhab.binding.sncf/src/main/resources/OH-INF/config/config.xml
new file mode 100644
index 000000000..d96cf36e3
--- /dev/null
+++ b/bundles/org.openhab.binding.sncf/src/main/resources/OH-INF/config/config.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+ password
+
+
+
+
diff --git a/bundles/org.openhab.binding.sncf/src/main/resources/OH-INF/i18n/sncf.properties b/bundles/org.openhab.binding.sncf/src/main/resources/OH-INF/i18n/sncf.properties
new file mode 100644
index 000000000..1b02bcf82
--- /dev/null
+++ b/bundles/org.openhab.binding.sncf/src/main/resources/OH-INF/i18n/sncf.properties
@@ -0,0 +1,36 @@
+
+binding.sncf.name = SNCF Binding
+binding.sncf.description = Retrieves French railway informations
+
+config.thing-type.sncf.api.apiID.label = API ID
+config.thing-type.sncf.api.apiID.description = Your SNCF API ID
+
+thing-type.sncf.api.label = SNCF API
+thing-type.sncf.api.description = This bridge is the gateway to SNCF API.
+
+thing-type.sncf.station.label = Station
+thing-type.sncf.station.description = Represents a station hosting some transportation mode.
+thing-type.sncf.station.group.arrivals.label = Next Arrival
+thing-type.sncf.station.group.arrivals.description = Informations regarding next arrival at the station.
+thing-type.sncf.station.group.departures.label = Next Departure
+thing-type.sncf.station.group.departures.description = Informations regarding next departure from the station.
+
+thing-type.config.sncf.station.stopPointId.label = Stop Point ID
+thing-type.config.sncf.station.stopPointId.description = The stop point ID of the station as defined by DIGITALSNCF.
+
+channel-type.sncf.direction.label = Direction
+channel-type.sncf.direction.description = The direction of this route.
+channel-type.sncf.lineName.label = Line
+channel-type.sncf.lineName.description = Name of the line (network + line number/letter)
+channel-type.sncf.name.label = Name
+channel-type.sncf.name.description = Name of the line.
+channel-type.sncf.network.label = Network
+channel-type.sncf.network.description = Name of the transportation network.
+channel-type.sncf.timestamp.label = Timestamp
+channel-type.sncf.timestamp.description = Timestamp of the future event.
+
+# Error messages
+null-or-empty-api-key = Null or empty API ID
+error-invalid-apikey = Invalid API ID
+null-or-empty-station-id = Null or empty Station ID
+null-or-empty-timezone = Timezone is empty. It should have been set at first initialization.
diff --git a/bundles/org.openhab.binding.sncf/src/main/resources/OH-INF/thing/bridge.xml b/bundles/org.openhab.binding.sncf/src/main/resources/OH-INF/thing/bridge.xml
new file mode 100644
index 000000000..ab99f53c2
--- /dev/null
+++ b/bundles/org.openhab.binding.sncf/src/main/resources/OH-INF/thing/bridge.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
diff --git a/bundles/org.openhab.binding.sncf/src/main/resources/OH-INF/thing/station.xml b/bundles/org.openhab.binding.sncf/src/main/resources/OH-INF/thing/station.xml
new file mode 100644
index 000000000..495b8c1bf
--- /dev/null
+++ b/bundles/org.openhab.binding.sncf/src/main/resources/OH-INF/thing/station.xml
@@ -0,0 +1,74 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ stopPointId
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ String
+
+
+
+
+
+ String
+
+
+
+
+
+ String
+
+
+
+
+
+ String
+
+
+
+
+
+ DateTime
+
+ time
+
+
+
+
diff --git a/bundles/pom.xml b/bundles/pom.xml
index e2dcde088..711297dbc 100644
--- a/bundles/pom.xml
+++ b/bundles/pom.xml
@@ -311,6 +311,7 @@
org.openhab.binding.smartmeter
org.openhab.binding.smhi
org.openhab.binding.smartthings
+ org.openhab.binding.sncf
org.openhab.binding.snmp
org.openhab.binding.solaredge
org.openhab.binding.solarlog