]> git.basschouten.com Git - openhab-addons.git/blob
00d2e7380354ce2fa4f911cc356be9e99a88f20c
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.sncf.internal.handler;
14
15 import static org.openhab.binding.sncf.internal.SncfBindingConstants.*;
16
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;
23 import java.util.Map;
24 import java.util.concurrent.ScheduledFuture;
25 import java.util.concurrent.TimeUnit;
26
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;
53
54 /**
55  * The {@link StationHandler} is responsible for handling commands, which are sent
56  * to one of the channels.
57  *
58  * @author GaĆ«l L'hopital - Initial contribution
59  */
60 @NonNullByDefault
61 public class StationHandler extends BaseThingHandler {
62     private static final DateTimeFormatter NAVITIA_DATE_FORMAT = DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmssZ");
63
64     private final Logger logger = LoggerFactory.getLogger(StationHandler.class);
65     private final LocationProvider locationProvider;
66
67     private @Nullable ScheduledFuture<?> refreshJob;
68     private @NonNullByDefault({}) String stationId;
69     private @NonNullByDefault({}) String zoneOffset;
70
71     public StationHandler(Thing thing, LocationProvider locationProvider) {
72         super(thing);
73         this.locationProvider = locationProvider;
74     }
75
76     @Override
77     public void initialize() {
78         logger.trace("Initializing the Station handler for {}", getThing().getUID());
79
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");
83             return;
84         }
85
86         if (thing.getProperties().isEmpty() && !discoverAttributes(stationId)) {
87             return;
88         }
89
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");
93             return;
94         }
95
96         zoneOffset = ZoneId.of(timezone).getRules().getOffset(Instant.now()).getId().replace(":", "");
97         scheduleRefresh(ZonedDateTime.now().plusSeconds(2));
98         updateStatus(ThingStatus.ONLINE);
99     }
100
101     @Override
102     public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
103         super.bridgeStatusChanged(bridgeStatusInfo);
104         if (thing.getStatus() == ThingStatus.ONLINE) {
105             initialize();
106         }
107     }
108
109     private boolean discoverAttributes(String localStation) {
110         SncfBridgeHandler bridgeHandler = getBridgeHandler();
111         if (bridgeHandler != null) {
112             Map<String, String> properties = new HashMap<>();
113             try {
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());
123                     }
124                 });
125                 ThingBuilder thingBuilder = editThing();
126                 thingBuilder.withProperties(properties);
127                 updateThing(thingBuilder.build());
128                 return true;
129             } catch (SncfException e) {
130                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
131             }
132         }
133         return false;
134     }
135
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),
139                 ChronoUnit.SECONDS);
140         wishedDelay = wishedDelay < 0 ? 60 : wishedDelay;
141         logger.debug("wishedDelay is {} seconds", wishedDelay);
142         ScheduledFuture<?> job = refreshJob;
143         if (job != null) {
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");
148                 return;
149             }
150             freeRefreshJob();
151         }
152         logger.debug("Scheduling update in {} seconds.", wishedDelay);
153         refreshJob = scheduler.schedule(() -> updateThing(), wishedDelay, TimeUnit.SECONDS);
154     }
155
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);
162             });
163         }
164     }
165
166     private void updatePassage(SncfBridgeHandler bridgeHandler, String direction) {
167         try {
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);
174                         });
175                 ZonedDateTime eventTime = getEventTimestamp(passage, direction);
176                 if (eventTime != null) {
177                     scheduleRefresh(eventTime.plusSeconds(10));
178                 }
179             }, () -> {
180                 logger.debug("No {} available", direction);
181                 scheduleRefresh(ZonedDateTime.now().plusMinutes(5));
182             });
183             updateStatus(ThingStatus.ONLINE);
184         } catch (SncfException e) {
185             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
186             freeRefreshJob();
187         }
188     }
189
190     private State getValue(String channelId, Passage passage, String direction) {
191         switch (channelId) {
192             case DIRECTION:
193                 return fromNullableString(passage.displayInformations.direction);
194             case LINE_NAME:
195                 return fromNullableString(String.format("%s %s", passage.displayInformations.commercialMode,
196                         passage.displayInformations.code));
197             case NAME:
198                 return fromNullableString(passage.displayInformations.name);
199             case NETWORK:
200                 return fromNullableString(passage.displayInformations.network);
201             case TIMESTAMP:
202                 return fromNullableTime(passage, direction);
203         }
204         return UnDefType.NULL;
205     }
206
207     private State fromNullableString(@Nullable String aValue) {
208         return aValue != null ? StringType.valueOf(aValue) : UnDefType.NULL;
209     }
210
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;
215     }
216
217     private State fromNullableTime(Passage passage, String direction) {
218         ZonedDateTime timestamp = getEventTimestamp(passage, direction);
219         return timestamp != null ? new DateTimeType(timestamp) : UnDefType.NULL;
220     }
221
222     private void freeRefreshJob() {
223         ScheduledFuture<?> job = refreshJob;
224         if (job != null) {
225             job.cancel(true);
226             this.refreshJob = null;
227         }
228     }
229
230     @Override
231     public void dispose() {
232         freeRefreshJob();
233         super.dispose();
234     }
235
236     @Override
237     public void handleCommand(ChannelUID channelUID, Command command) {
238         if (command instanceof RefreshType) {
239             updateThing();
240         }
241     }
242
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;
250                 } else {
251                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
252                     return null;
253                 }
254             }
255         }
256         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
257         return null;
258     }
259 }