2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.sncf.internal.handler;
15 import static org.openhab.binding.sncf.internal.SncfBindingConstants.*;
17 import java.time.Instant;
18 import java.time.ZoneId;
19 import java.time.ZonedDateTime;
20 import java.time.format.DateTimeFormatter;
21 import java.time.temporal.ChronoUnit;
22 import java.util.HashMap;
24 import java.util.concurrent.ScheduledFuture;
25 import java.util.concurrent.TimeUnit;
27 import org.eclipse.jdt.annotation.NonNullByDefault;
28 import org.eclipse.jdt.annotation.Nullable;
29 import org.openhab.binding.sncf.internal.SncfException;
30 import org.openhab.binding.sncf.internal.dto.Passage;
31 import org.openhab.core.i18n.LocationProvider;
32 import org.openhab.core.library.types.DateTimeType;
33 import org.openhab.core.library.types.PointType;
34 import org.openhab.core.library.types.QuantityType;
35 import org.openhab.core.library.types.StringType;
36 import org.openhab.core.library.unit.SIUnits;
37 import org.openhab.core.thing.Bridge;
38 import org.openhab.core.thing.Channel;
39 import org.openhab.core.thing.ChannelUID;
40 import org.openhab.core.thing.Thing;
41 import org.openhab.core.thing.ThingStatus;
42 import org.openhab.core.thing.ThingStatusDetail;
43 import org.openhab.core.thing.ThingStatusInfo;
44 import org.openhab.core.thing.binding.BaseThingHandler;
45 import org.openhab.core.thing.binding.BridgeHandler;
46 import org.openhab.core.thing.binding.builder.ThingBuilder;
47 import org.openhab.core.types.Command;
48 import org.openhab.core.types.RefreshType;
49 import org.openhab.core.types.State;
50 import org.openhab.core.types.UnDefType;
51 import org.slf4j.Logger;
52 import org.slf4j.LoggerFactory;
55 * The {@link StationHandler} is responsible for handling commands, which are sent
56 * to one of the channels.
58 * @author Gaƫl L'hopital - Initial contribution
61 public class StationHandler extends BaseThingHandler {
62 private static final DateTimeFormatter NAVITIA_DATE_FORMAT = DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmssZ");
64 private final Logger logger = LoggerFactory.getLogger(StationHandler.class);
65 private final LocationProvider locationProvider;
67 private @Nullable ScheduledFuture<?> refreshJob;
68 private @NonNullByDefault({}) String stationId;
69 private @NonNullByDefault({}) String zoneOffset;
71 public StationHandler(Thing thing, LocationProvider locationProvider) {
73 this.locationProvider = locationProvider;
77 public void initialize() {
78 logger.trace("Initializing the Station handler for {}", getThing().getUID());
80 stationId = (String) getConfig().get("stopPointId");
81 if (stationId == null || stationId.isBlank()) {
82 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/null-or-empty-station-id");
86 if (thing.getProperties().isEmpty() && !discoverAttributes(stationId)) {
90 String timezone = thing.getProperties().get(TIMEZONE);
91 if (timezone == null || timezone.isBlank()) {
92 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/null-or-empty-timezone");
96 zoneOffset = ZoneId.of(timezone).getRules().getOffset(Instant.now()).getId().replace(":", "");
97 scheduleRefresh(ZonedDateTime.now().plusSeconds(2));
98 updateStatus(ThingStatus.ONLINE);
102 public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
103 super.bridgeStatusChanged(bridgeStatusInfo);
104 if (thing.getStatus() == ThingStatus.ONLINE) {
109 private boolean discoverAttributes(String localStation) {
110 SncfBridgeHandler bridgeHandler = getBridgeHandler();
111 if (bridgeHandler != null) {
112 Map<String, String> properties = new HashMap<>();
114 bridgeHandler.stopPointDetail(localStation).ifPresent(stopPoint -> {
115 String stationLoc = String.format("%s,%s", stopPoint.coord.lat, stopPoint.coord.lon);
116 properties.put(LOCATION, stationLoc);
117 properties.put(TIMEZONE, stopPoint.stopArea.timezone);
118 PointType serverLoc = locationProvider.getLocation();
119 if (serverLoc != null) {
120 PointType stationLocation = new PointType(stationLoc);
121 double distance = serverLoc.distanceFrom(stationLocation).doubleValue();
122 properties.put(DISTANCE, new QuantityType<>(distance, SIUnits.METRE).toString());
125 ThingBuilder thingBuilder = editThing();
126 thingBuilder.withProperties(properties);
127 updateThing(thingBuilder.build());
129 } catch (SncfException e) {
130 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
136 private void scheduleRefresh(@Nullable ZonedDateTime when) {
137 // Ensure we'll try to refresh in one minute if no valid timestamp is provided
138 long wishedDelay = ZonedDateTime.now().until(when != null ? when : ZonedDateTime.now().plusMinutes(1),
140 wishedDelay = wishedDelay < 0 ? 60 : wishedDelay;
141 logger.debug("wishedDelay is {} seconds", wishedDelay);
142 ScheduledFuture<?> job = refreshJob;
144 long existingDelay = job.getDelay(TimeUnit.SECONDS);
145 logger.debug("existingDelay is {} seconds", existingDelay);
146 if (existingDelay < wishedDelay && existingDelay > 0) {
147 logger.debug("Do nothing, existingDelay earlier than wishedDelay");
152 logger.debug("Scheduling update in {} seconds.", wishedDelay);
153 refreshJob = scheduler.schedule(() -> updateThing(), wishedDelay, TimeUnit.SECONDS);
156 private void updateThing() {
157 SncfBridgeHandler bridgeHandler = getBridgeHandler();
158 if (bridgeHandler != null) {
159 scheduler.submit(() -> {
160 updatePassage(bridgeHandler, GROUP_ARRIVAL);
161 updatePassage(bridgeHandler, GROUP_DEPARTURE);
166 private void updatePassage(SncfBridgeHandler bridgeHandler, String direction) {
168 bridgeHandler.getNextPassage(stationId, direction).ifPresentOrElse(passage -> {
169 getThing().getChannels().stream().map(Channel::getUID)
170 .filter(channelUID -> isLinked(channelUID) && direction.equals(channelUID.getGroupId()))
171 .forEach(channelUID -> {
172 State state = getValue(channelUID.getIdWithoutGroup(), passage, direction);
173 updateState(channelUID, state);
175 ZonedDateTime eventTime = getEventTimestamp(passage, direction);
176 if (eventTime != null) {
177 scheduleRefresh(eventTime.plusSeconds(10));
180 logger.debug("No {} available", direction);
181 scheduleRefresh(ZonedDateTime.now().plusMinutes(5));
183 updateStatus(ThingStatus.ONLINE);
184 } catch (SncfException e) {
185 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
190 private State getValue(String channelId, Passage passage, String direction) {
193 return fromNullableString(passage.displayInformations.direction);
195 return fromNullableString(String.format("%s %s", passage.displayInformations.commercialMode,
196 passage.displayInformations.code));
198 return fromNullableString(passage.displayInformations.name);
200 return fromNullableString(passage.displayInformations.network);
202 return fromNullableTime(passage, direction);
204 return UnDefType.NULL;
207 private State fromNullableString(@Nullable String aValue) {
208 return aValue != null ? StringType.valueOf(aValue) : UnDefType.NULL;
211 private @Nullable ZonedDateTime getEventTimestamp(Passage passage, String direction) {
212 String eventTime = direction.equals(GROUP_ARRIVAL) ? passage.stopDateTime.arrivalDateTime
213 : passage.stopDateTime.departureDateTime;
214 return eventTime != null ? ZonedDateTime.parse(eventTime + zoneOffset, NAVITIA_DATE_FORMAT) : null;
217 private State fromNullableTime(Passage passage, String direction) {
218 ZonedDateTime timestamp = getEventTimestamp(passage, direction);
219 return timestamp != null ? new DateTimeType(timestamp) : UnDefType.NULL;
222 private void freeRefreshJob() {
223 ScheduledFuture<?> job = refreshJob;
226 this.refreshJob = null;
231 public void dispose() {
237 public void handleCommand(ChannelUID channelUID, Command command) {
238 if (command instanceof RefreshType) {
243 private @Nullable SncfBridgeHandler getBridgeHandler() {
244 Bridge bridge = getBridge();
245 if (bridge != null) {
246 BridgeHandler handler = bridge.getHandler();
247 if (handler != null) {
248 if (handler.getThing().getStatus() == ThingStatus.ONLINE) {
249 return (SncfBridgeHandler) handler;
251 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
256 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);