mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-10 07:02:02 +01:00
[SNCF] A binding to get French railways arrivals and departures (#11607)
* SNCF : new binding Signed-off-by: clinique <gael@lhopital.org>
This commit is contained in:
parent
83f5f01267
commit
cb0c4bbcb4
@ -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
|
||||
|
@ -1386,6 +1386,11 @@
|
||||
<artifactId>org.openhab.binding.smhi</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.sncf</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.snmp</artifactId>
|
||||
|
13
bundles/org.openhab.binding.sncf/NOTICE
Normal file
13
bundles/org.openhab.binding.sncf/NOTICE
Normal file
@ -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
|
87
bundles/org.openhab.binding.sncf/README.md
Normal file
87
bundles/org.openhab.binding.sncf/README.md
Normal file
@ -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" }
|
||||
|
||||
```
|
||||
|
17
bundles/org.openhab.binding.sncf/pom.xml
Normal file
17
bundles/org.openhab.binding.sncf/pom.xml
Normal file
@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
|
||||
<version>3.2.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>org.openhab.binding.sncf</artifactId>
|
||||
|
||||
<name>openHAB Add-ons :: Bundles :: SNCF Binding</name>
|
||||
|
||||
</project>
|
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<features name="org.openhab.binding.sncf-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
|
||||
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
|
||||
|
||||
<feature name="openhab-binding-sncf" description="SNCF Binding" version="${project.version}">
|
||||
<feature>openhab-runtime-base</feature>
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.sncf/${project.version}</bundle>
|
||||
</feature>
|
||||
</features>
|
@ -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<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(APIBRIDGE_THING_TYPE, STATION_THING_TYPE);
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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<PlaceNearby> places = handler.discoverNearby(location, searchRange);
|
||||
if (places != null && !places.isEmpty()) {
|
||||
places.forEach(place -> {
|
||||
// stop_point:SNCF:87386573:Bus
|
||||
List<String> idElts = new LinkedList<String>(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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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<Passage> passages;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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<PlaceNearby> placesNearby;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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<StopPoint> stopPoints;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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<String, @Nullable String> 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 extends SncfAnswer> T getResponseFromCache(String url, Class<T> 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<PlaceNearby> 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<StopPoint> stopPointDetail(String stopPointId) throws SncfException {
|
||||
String url = String.format("%sstop_points/%s", SERVICE_URL, stopPointId);
|
||||
List<StopPoint> points = getResponseFromCache(url, StopPoints.class).stopPoints;
|
||||
return points != null && !points.isEmpty() ? Optional.ofNullable(points.get(0)) : Optional.empty();
|
||||
}
|
||||
|
||||
public Optional<Passage> 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<Passage> 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<Class<? extends ThingHandlerService>> getServices() {
|
||||
return Set.of(SncfDiscoveryService.class);
|
||||
}
|
||||
}
|
@ -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<String, String> 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;
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<binding:binding id="sncf" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
|
||||
|
||||
<name>SNCF Binding</name>
|
||||
<description>Retrieves French railway informations</description>
|
||||
|
||||
</binding:binding>
|
@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<config-description:config-descriptions
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0
|
||||
https://openhab.org/schemas/config-description-1.0.0.xsd">
|
||||
|
||||
<config-description uri="thing-type:sncf:api">
|
||||
<parameter name="apiID" type="text" required="true">
|
||||
<label>API ID</label>
|
||||
<context>password</context>
|
||||
</parameter>
|
||||
</config-description>
|
||||
|
||||
</config-description:config-descriptions>
|
@ -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.
|
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<thing:thing-descriptions bindingId="sncf"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
|
||||
|
||||
<bridge-type id="api">
|
||||
<label>SNCF API</label>
|
||||
<config-description-ref uri="thing-type:sncf:api"/>
|
||||
</bridge-type>
|
||||
|
||||
</thing:thing-descriptions>
|
@ -0,0 +1,74 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<thing:thing-descriptions bindingId="sncf"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
|
||||
|
||||
<thing-type id="station">
|
||||
<supported-bridge-type-refs>
|
||||
<bridge-type-ref id="api"/>
|
||||
</supported-bridge-type-refs>
|
||||
|
||||
<label>Station</label>
|
||||
|
||||
<channel-groups>
|
||||
<channel-group id="arrivals" typeId="passage">
|
||||
<label>Next Arrival</label>
|
||||
</channel-group>
|
||||
<channel-group id="departures" typeId="passage">
|
||||
<label>Next Departure</label>
|
||||
</channel-group>
|
||||
</channel-groups>
|
||||
|
||||
<representation-property>stopPointId</representation-property>
|
||||
|
||||
<config-description>
|
||||
<parameter name="stopPointId" type="text" required="true">
|
||||
<label>Station ID</label>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</thing-type>
|
||||
|
||||
<channel-group-type id="passage">
|
||||
<label>Other</label>
|
||||
<channels>
|
||||
<channel id="direction" typeId="direction"/>
|
||||
<channel id="lineName" typeId="lineName"/>
|
||||
<channel id="name" typeId="name"/>
|
||||
<channel id="network" typeId="network"/>
|
||||
<channel id="timestamp" typeId="timestamp"/>
|
||||
</channels>
|
||||
</channel-group-type>
|
||||
|
||||
<channel-type id="direction">
|
||||
<item-type>String</item-type>
|
||||
<label>Direction</label>
|
||||
<state readOnly="true"></state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="lineName">
|
||||
<item-type>String</item-type>
|
||||
<label>Line</label>
|
||||
<state readOnly="true"></state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="name" advanced="true">
|
||||
<item-type>String</item-type>
|
||||
<label>Name</label>
|
||||
<state readOnly="true"></state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="network" advanced="true">
|
||||
<item-type>String</item-type>
|
||||
<label>Network</label>
|
||||
<state readOnly="true"></state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="timestamp">
|
||||
<item-type>DateTime</item-type>
|
||||
<label>Timestamp</label>
|
||||
<category>time</category>
|
||||
<state readOnly="true" pattern="%1$tH:%1$tM:%1$tS"/>
|
||||
</channel-type>
|
||||
|
||||
</thing:thing-descriptions>
|
@ -311,6 +311,7 @@
|
||||
<module>org.openhab.binding.smartmeter</module>
|
||||
<module>org.openhab.binding.smhi</module>
|
||||
<module>org.openhab.binding.smartthings</module>
|
||||
<module>org.openhab.binding.sncf</module>
|
||||
<module>org.openhab.binding.snmp</module>
|
||||
<module>org.openhab.binding.solaredge</module>
|
||||
<module>org.openhab.binding.solarlog</module>
|
||||
|
Loading…
Reference in New Issue
Block a user