/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
<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>
--- /dev/null
+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
--- /dev/null
+# 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" }
+
+```
+
--- /dev/null
+<?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>
--- /dev/null
+<?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>
--- /dev/null
+/**
+ * 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);
+}
--- /dev/null
+/**
+ * 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);
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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;
+}
--- /dev/null
+/**
+ * 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;
+}
--- /dev/null
+/**
+ * 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;
+}
--- /dev/null
+/**
+ * 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;
+}
--- /dev/null
+/**
+ * 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;
+}
--- /dev/null
+/**
+ * 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;
+}
--- /dev/null
+/**
+ * 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;
+}
--- /dev/null
+/**
+ * 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;
+}
--- /dev/null
+/**
+ * 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;
+}
--- /dev/null
+/**
+ * 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;
+}
--- /dev/null
+/**
+ * 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;
+}
--- /dev/null
+/**
+ * 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;
+}
--- /dev/null
+/**
+ * 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);
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+<?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>
--- /dev/null
+<?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>
--- /dev/null
+
+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.
--- /dev/null
+<?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>
--- /dev/null
+<?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>
<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>