2 * Copyright (c) 2010-2021 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.enturno.internal;
15 import java.time.ZoneId;
16 import java.time.ZonedDateTime;
17 import java.time.format.DateTimeFormatter;
18 import java.util.ArrayList;
19 import java.util.List;
20 import java.util.concurrent.ScheduledFuture;
21 import java.util.concurrent.TimeUnit;
23 import org.apache.commons.lang.StringUtils;
24 import org.eclipse.jdt.annotation.NonNullByDefault;
25 import org.eclipse.jdt.annotation.Nullable;
26 import org.eclipse.jetty.client.HttpClient;
27 import org.openhab.binding.enturno.internal.connection.EnturCommunicationException;
28 import org.openhab.binding.enturno.internal.connection.EnturConfigurationException;
29 import org.openhab.binding.enturno.internal.connection.EnturNoConnection;
30 import org.openhab.binding.enturno.internal.model.simplified.DisplayData;
31 import org.openhab.core.library.types.DateTimeType;
32 import org.openhab.core.library.types.StringType;
33 import org.openhab.core.thing.Channel;
34 import org.openhab.core.thing.ChannelUID;
35 import org.openhab.core.thing.Thing;
36 import org.openhab.core.thing.ThingStatus;
37 import org.openhab.core.thing.ThingStatusDetail;
38 import org.openhab.core.thing.binding.BaseThingHandler;
39 import org.openhab.core.thing.type.ChannelKind;
40 import org.openhab.core.types.Command;
41 import org.openhab.core.types.RefreshType;
42 import org.openhab.core.types.State;
43 import org.openhab.core.types.UnDefType;
44 import org.slf4j.Logger;
45 import org.slf4j.LoggerFactory;
47 import com.google.gson.JsonSyntaxException;
50 * The {@link EnturNoHandler} is responsible for handling commands, which are
51 * sent to one of the channels.
53 * @author Michal Kloc - Initial contribution
56 public class EnturNoHandler extends BaseThingHandler {
58 private final Logger logger = LoggerFactory.getLogger(EnturNoHandler.class);
60 private final HttpClient httpClient;
62 private @NonNullByDefault({}) EnturNoConfiguration config;
64 private @NonNullByDefault({}) EnturNoConnection connection;
66 private static final long INITIAL_DELAY_IN_SECONDS = 15;
68 private static final long REFRESH_INTERVAL_IN_SECONDS = 30;
70 private @Nullable ScheduledFuture<?> refreshJob;
72 private @Nullable String stopId;
74 private List<DisplayData> processedData = new ArrayList<>();
76 public EnturNoHandler(Thing thing, HttpClient httpClient) {
78 this.httpClient = httpClient;
82 public void handleCommand(ChannelUID channelUID, Command command) {
83 if (command instanceof RefreshType) {
84 updateChannel(channelUID);
86 logger.debug("Entur binding is a read-only binding and cannot handle command '{}'.", command);
91 public void initialize() {
92 logger.debug("Initialize Entur EnturTimeTable API handler '{}'.", getThing().getUID());
93 config = getConfigAs(EnturNoConfiguration.class);
94 stopId = config.getStopPlaceId();
96 logger.debug("Stop place id: {}", stopId);
97 boolean configValid = true;
98 if (StringUtils.trimToNull(stopId) == null) {
99 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
100 "@text/offline.conf-error-missing-stopId");
104 logger.debug("Line code: {}", config.getLineCode());
105 if (StringUtils.trimToNull(config.getLineCode()) == null) {
106 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
107 "@text/offline.conf-error-missing-lineCode");
112 connection = new EnturNoConnection(this, httpClient);
114 updateStatus(ThingStatus.UNKNOWN);
116 if (refreshJob == null || refreshJob.isCancelled()) {
117 logger.debug("Start refresh job at interval {} sec.", REFRESH_INTERVAL_IN_SECONDS);
118 refreshJob = scheduler.scheduleWithFixedDelay(this::updateThing, INITIAL_DELAY_IN_SECONDS,
119 REFRESH_INTERVAL_IN_SECONDS, TimeUnit.SECONDS);
125 public void dispose() {
126 logger.debug("Dispose Entur real-time timetable API handler '{}'.", getThing().getUID());
127 if (refreshJob != null && !refreshJob.isCancelled()) {
128 logger.debug("Stop refresh job.");
129 if (refreshJob.cancel(true)) {
135 public EnturNoConfiguration getEnturNoConfiguration() {
139 private void updateThing() {
140 ThingStatus status = ThingStatus.OFFLINE;
141 if (connection != null) {
142 logger.trace("Updating data");
143 updateData(connection);
144 status = thing.getStatus();
146 logger.debug("Cannot update real-time data of thing '{}' as connection is null.", thing.getUID());
147 status = ThingStatus.OFFLINE;
150 updateStatus(status);
153 public void updateData(EnturNoConnection connection) {
155 if (requestData(connection)) {
157 updateStatus(ThingStatus.ONLINE);
159 } catch (EnturCommunicationException e) {
160 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getLocalizedMessage());
161 } catch (EnturConfigurationException e) {
162 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getLocalizedMessage());
166 private boolean requestData(EnturNoConnection connection)
167 throws EnturConfigurationException, EnturCommunicationException {
168 logger.debug("Update real-time data of thing '{}'.", getThing().getUID());
170 processedData = connection.getEnturTimeTable(stopId, config.getLineCode());
173 } catch (JsonSyntaxException e) {
174 logger.debug("JsonSyntaxException occurred during execution: {}", e.getLocalizedMessage(), e);
179 private void updateChannels() {
180 for (Channel channel : getThing().getChannels()) {
181 ChannelUID channelUID = channel.getUID();
182 if (ChannelKind.STATE.equals(channel.getKind()) && channelUID.isInGroup() && channelUID.getGroupId() != null
183 && isLinked(channelUID)) {
184 updateChannel(channelUID);
189 private void updateChannel(ChannelUID channelUID) {
190 String channelGroupId = channelUID.getGroupId();
191 logger.trace("Channel group id: {}", channelGroupId);
192 if (channelGroupId != null) {
193 switch (channelGroupId) {
194 case EnturNoBindingConstants.CHANNEL_GROUP_STOP_PLACE:
195 updateStopPlaceChannel(channelUID);
197 case EnturNoBindingConstants.CHANNEL_GROUP_DIRECTION_1:
198 updateDirectionChannel(channelUID, 0);
200 case EnturNoBindingConstants.CHANNEL_GROUP_DIRECTION_2:
201 updateDirectionChannel(channelUID, 1);
209 private void updateDirectionChannel(ChannelUID channelUID, int i) {
210 String channelId = channelUID.getIdWithoutGroup();
211 String channelGroupId = channelUID.getGroupId();
212 logger.trace("Channel id: {}, Channel group id: {}", channelId, channelGroupId);
213 if (processedData.size() > i) {
214 State state = UnDefType.UNDEF;
215 List<String> departures = processedData.get(i).departures;
216 List<String> estimatedFlags = processedData.get(i).estimatedFlags;
218 case EnturNoBindingConstants.CHANNEL_DEPARTURE_01:
219 state = departures.size() > 0 ? getDateTimeTypeState(departures.get(0)) : state;
221 case EnturNoBindingConstants.CHANNEL_DEPARTURE_02:
222 state = departures.size() > 1 ? getDateTimeTypeState(departures.get(1)) : state;
224 case EnturNoBindingConstants.CHANNEL_DEPARTURE_03:
225 state = departures.size() > 2 ? getDateTimeTypeState(departures.get(2)) : state;
227 case EnturNoBindingConstants.CHANNEL_DEPARTURE_04:
228 state = departures.size() > 3 ? getDateTimeTypeState(departures.get(3)) : state;
230 case EnturNoBindingConstants.CHANNEL_DEPARTURE_05:
231 state = departures.size() > 4 ? getDateTimeTypeState(departures.get(4)) : state;
233 case EnturNoBindingConstants.ESTIMATED_FLAG_01:
234 state = estimatedFlags.size() > 0 ? getStringTypeState(estimatedFlags.get(0)) : state;
236 case EnturNoBindingConstants.ESTIMATED_FLAG_02:
237 state = estimatedFlags.size() > 1 ? getStringTypeState(estimatedFlags.get(1)) : state;
239 case EnturNoBindingConstants.ESTIMATED_FLAG_03:
240 state = estimatedFlags.size() > 2 ? getStringTypeState(estimatedFlags.get(2)) : state;
242 case EnturNoBindingConstants.ESTIMATED_FLAG_04:
243 state = estimatedFlags.size() > 3 ? getStringTypeState(estimatedFlags.get(3)) : state;
245 case EnturNoBindingConstants.ESTIMATED_FLAG_05:
246 state = estimatedFlags.size() > 4 ? getStringTypeState(estimatedFlags.get(4)) : state;
248 case EnturNoBindingConstants.CHANNEL_LINE_CODE:
249 state = getStringTypeState(processedData.get(i).lineCode);
251 case EnturNoBindingConstants.CHANNEL_FRONT_DISPLAY:
252 state = getStringTypeState(processedData.get(i).frontText);
257 logger.debug("Update channel '{}' of group '{}' with new state '{}'.", channelId, channelGroupId, state);
258 updateState(channelUID, state);
260 logger.debug("No real-time data available to update channel '{}' of group '{}'.", channelId,
266 * Update the channel from the last Entur data retrieved.
268 * @param channelUID the id identifying the channel to be updated
270 private void updateStopPlaceChannel(ChannelUID channelUID) {
271 String channelId = channelUID.getIdWithoutGroup();
272 String channelGroupId = channelUID.getGroupId();
273 if (!processedData.isEmpty()) {
274 State state = UnDefType.UNDEF;
276 case EnturNoBindingConstants.CHANNEL_STOP_ID:
277 state = getStringTypeState(processedData.get(0).stopPlaceId);
279 case EnturNoBindingConstants.CHANNEL_STOP_NAME:
280 state = getStringTypeState(processedData.get(0).stopName);
282 case EnturNoBindingConstants.CHANNEL_STOP_TRANSPORT_MODE:
283 state = getStringTypeState(processedData.get(0).transportMode);
288 logger.debug("Update channel '{}' of group '{}' with new state '{}'.", channelId, channelGroupId, state);
289 updateState(channelUID, state);
291 logger.debug("No real-time data available to update channel '{}' of group '{}'.", channelId,
296 private State getDateTimeTypeState(@Nullable String value) {
297 return (value == null) ? UnDefType.UNDEF
298 : new DateTimeType(ZonedDateTime.parse(value, DateTimeFormatter.ISO_DATE_TIME)
299 .withZoneSameInstant(ZoneId.of(EnturNoBindingConstants.TIME_ZONE)));
302 private State getStringTypeState(@Nullable String value) {
303 return (value == null) ? UnDefType.UNDEF : new StringType(value);