From 1639b7cc27dafd5e5df8a244b00d6bc6346e3e80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20L=27hopital?= Date: Sat, 4 Dec 2021 18:33:50 +0100 Subject: [PATCH] [SNCF] A binding to get French railways arrivals and departures (#11607) * SNCF : new binding Signed-off-by: clinique Signed-off-by: Andras Uhrin --- CODEOWNERS | 1 + bom/openhab-addons/pom.xml | 5 + bundles/org.openhab.binding.sncf/NOTICE | 13 + bundles/org.openhab.binding.sncf/README.md | 87 ++++++ bundles/org.openhab.binding.sncf/pom.xml | 17 ++ .../src/main/feature/feature.xml | 9 + .../sncf/internal/SncfBindingConstants.java | 54 ++++ .../binding/sncf/internal/SncfException.java | 38 +++ .../sncf/internal/SncfHandlerFactory.java | 78 ++++++ .../discovery/SncfDiscoveryService.java | 115 ++++++++ .../binding/sncf/internal/dto/Coord.java | 23 ++ .../sncf/internal/dto/NavitiaObject.java | 23 ++ .../binding/sncf/internal/dto/Passage.java | 24 ++ .../binding/sncf/internal/dto/Passages.java | 30 ++ .../sncf/internal/dto/PlaceNearby.java | 22 ++ .../sncf/internal/dto/PlacesNearby.java | 24 ++ .../binding/sncf/internal/dto/SncfAnswer.java | 23 ++ .../binding/sncf/internal/dto/StopArea.java | 23 ++ .../sncf/internal/dto/StopDateTime.java | 23 ++ .../binding/sncf/internal/dto/StopPoint.java | 23 ++ .../binding/sncf/internal/dto/StopPoints.java | 26 ++ .../internal/dto/VJDisplayInformation.java | 27 ++ .../internal/handler/SncfBridgeHandler.java | 171 ++++++++++++ .../sncf/internal/handler/StationHandler.java | 259 ++++++++++++++++++ .../main/resources/OH-INF/binding/binding.xml | 9 + .../main/resources/OH-INF/config/config.xml | 15 + .../resources/OH-INF/i18n/sncf.properties | 36 +++ .../main/resources/OH-INF/thing/bridge.xml | 12 + .../main/resources/OH-INF/thing/station.xml | 74 +++++ bundles/pom.xml | 1 + 30 files changed, 1285 insertions(+) create mode 100644 bundles/org.openhab.binding.sncf/NOTICE create mode 100644 bundles/org.openhab.binding.sncf/README.md create mode 100644 bundles/org.openhab.binding.sncf/pom.xml create mode 100644 bundles/org.openhab.binding.sncf/src/main/feature/feature.xml create mode 100644 bundles/org.openhab.binding.sncf/src/main/java/org/openhab/binding/sncf/internal/SncfBindingConstants.java create mode 100644 bundles/org.openhab.binding.sncf/src/main/java/org/openhab/binding/sncf/internal/SncfException.java create mode 100644 bundles/org.openhab.binding.sncf/src/main/java/org/openhab/binding/sncf/internal/SncfHandlerFactory.java create mode 100644 bundles/org.openhab.binding.sncf/src/main/java/org/openhab/binding/sncf/internal/discovery/SncfDiscoveryService.java create mode 100644 bundles/org.openhab.binding.sncf/src/main/java/org/openhab/binding/sncf/internal/dto/Coord.java create mode 100644 bundles/org.openhab.binding.sncf/src/main/java/org/openhab/binding/sncf/internal/dto/NavitiaObject.java create mode 100644 bundles/org.openhab.binding.sncf/src/main/java/org/openhab/binding/sncf/internal/dto/Passage.java create mode 100644 bundles/org.openhab.binding.sncf/src/main/java/org/openhab/binding/sncf/internal/dto/Passages.java create mode 100644 bundles/org.openhab.binding.sncf/src/main/java/org/openhab/binding/sncf/internal/dto/PlaceNearby.java create mode 100644 bundles/org.openhab.binding.sncf/src/main/java/org/openhab/binding/sncf/internal/dto/PlacesNearby.java create mode 100644 bundles/org.openhab.binding.sncf/src/main/java/org/openhab/binding/sncf/internal/dto/SncfAnswer.java create mode 100644 bundles/org.openhab.binding.sncf/src/main/java/org/openhab/binding/sncf/internal/dto/StopArea.java create mode 100644 bundles/org.openhab.binding.sncf/src/main/java/org/openhab/binding/sncf/internal/dto/StopDateTime.java create mode 100644 bundles/org.openhab.binding.sncf/src/main/java/org/openhab/binding/sncf/internal/dto/StopPoint.java create mode 100644 bundles/org.openhab.binding.sncf/src/main/java/org/openhab/binding/sncf/internal/dto/StopPoints.java create mode 100644 bundles/org.openhab.binding.sncf/src/main/java/org/openhab/binding/sncf/internal/dto/VJDisplayInformation.java create mode 100644 bundles/org.openhab.binding.sncf/src/main/java/org/openhab/binding/sncf/internal/handler/SncfBridgeHandler.java create mode 100644 bundles/org.openhab.binding.sncf/src/main/java/org/openhab/binding/sncf/internal/handler/StationHandler.java create mode 100644 bundles/org.openhab.binding.sncf/src/main/resources/OH-INF/binding/binding.xml create mode 100644 bundles/org.openhab.binding.sncf/src/main/resources/OH-INF/config/config.xml create mode 100644 bundles/org.openhab.binding.sncf/src/main/resources/OH-INF/i18n/sncf.properties create mode 100644 bundles/org.openhab.binding.sncf/src/main/resources/OH-INF/thing/bridge.xml create mode 100644 bundles/org.openhab.binding.sncf/src/main/resources/OH-INF/thing/station.xml diff --git a/CODEOWNERS b/CODEOWNERS index 2bd54c65fd43b..76c5453aed0de 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -280,6 +280,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 8027e6bdedff5..fdaa5add0e736 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -1391,6 +1391,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 0000000000000..38d625e349232 --- /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 0000000000000..0c6ad36a733f1 --- /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 0000000000000..aefe5411519fe --- /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 0000000000000..7acc033cab63f --- /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 0000000000000..8f68576e8e508 --- /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 0000000000000..de12a40973385 --- /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 0000000000000..131e5d942ca5a --- /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 0000000000000..73f96c6ce70d5 --- /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 0000000000000..7441c5c7f7b6d --- /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 0000000000000..56b514aaa9b16 --- /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 0000000000000..0a9fbe6945301 --- /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 0000000000000..f4988bf3c2e9f --- /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 0000000000000..a2931351a83b3 --- /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 0000000000000..9141872b77d0f --- /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 0000000000000..b572f84693103 --- /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 0000000000000..706874a1f8536 --- /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 0000000000000..0bcce5cd81bbf --- /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 0000000000000..e372dc15e5629 --- /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 0000000000000..578d33e1fcf12 --- /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 0000000000000..7e182f6a6dc9e --- /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 0000000000000..0ef7fb05001a1 --- /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 0000000000000..855199219aad8 --- /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 0000000000000..e756b395d0d29 --- /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 0000000000000..d96cf36e38e8e --- /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 0000000000000..1b02bcf8288c8 --- /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 0000000000000..ab99f53c25924 --- /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 0000000000000..495b8c1bf6192 --- /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 0002e2cad3561..3c0f756994396 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -312,6 +312,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