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.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.eclipse.jdt.annotation.NonNullByDefault;
24 import org.eclipse.jdt.annotation.Nullable;
25 import org.eclipse.jetty.client.HttpClient;
26 import org.openhab.binding.enturno.internal.connection.EnturCommunicationException;
27 import org.openhab.binding.enturno.internal.connection.EnturConfigurationException;
28 import org.openhab.binding.enturno.internal.connection.EnturNoConnection;
29 import org.openhab.binding.enturno.internal.model.simplified.DisplayData;
30 import org.openhab.core.library.types.DateTimeType;
31 import org.openhab.core.library.types.StringType;
32 import org.openhab.core.thing.Channel;
33 import org.openhab.core.thing.ChannelUID;
34 import org.openhab.core.thing.Thing;
35 import org.openhab.core.thing.ThingStatus;
36 import org.openhab.core.thing.ThingStatusDetail;
37 import org.openhab.core.thing.binding.BaseThingHandler;
38 import org.openhab.core.thing.type.ChannelKind;
39 import org.openhab.core.types.Command;
40 import org.openhab.core.types.RefreshType;
41 import org.openhab.core.types.State;
42 import org.openhab.core.types.UnDefType;
43 import org.slf4j.Logger;
44 import org.slf4j.LoggerFactory;
46 import com.google.gson.JsonSyntaxException;
49 * The {@link EnturNoHandler} is responsible for handling commands, which are
50 * sent to one of the channels.
52 * @author Michal Kloc - Initial contribution
55 public class EnturNoHandler extends BaseThingHandler {
57 private final Logger logger = LoggerFactory.getLogger(EnturNoHandler.class);
59 private final HttpClient httpClient;
61 private @NonNullByDefault({}) EnturNoConfiguration config;
63 private @NonNullByDefault({}) EnturNoConnection connection;
65 private static final long INITIAL_DELAY_IN_SECONDS = 15;
67 private static final long REFRESH_INTERVAL_IN_SECONDS = 30;
69 private @Nullable ScheduledFuture<?> refreshJob;
71 private @Nullable String stopId;
73 private List<DisplayData> processedData = new ArrayList<>();
75 public EnturNoHandler(Thing thing, HttpClient httpClient) {
77 this.httpClient = httpClient;
81 public void handleCommand(ChannelUID channelUID, Command command) {
82 if (command instanceof RefreshType) {
83 updateChannel(channelUID);
85 logger.debug("Entur binding is a read-only binding and cannot handle command '{}'.", command);
90 public void initialize() {
91 logger.debug("Initialize Entur EnturTimeTable API handler '{}'.", getThing().getUID());
92 config = getConfigAs(EnturNoConfiguration.class);
93 stopId = config.getStopPlaceId();
95 logger.debug("Stop place id: {}", stopId);
96 boolean configValid = true;
97 if (stopId == null || stopId.isBlank()) {
98 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
99 "@text/offline.conf-error-missing-stopId");
103 String lineCode = config.getLineCode();
104 logger.debug("Line code: {}", lineCode);
105 if (lineCode == null || lineCode.isBlank()) {
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 int departuresCount = departures.size();
217 List<String> estimatedFlags = processedData.get(i).estimatedFlags;
218 int esitmatedFlagsCount = estimatedFlags.size();
220 case EnturNoBindingConstants.CHANNEL_DEPARTURE_01:
221 state = departuresCount > 0 ? getDateTimeTypeState(departures.get(0)) : state;
223 case EnturNoBindingConstants.CHANNEL_DEPARTURE_02:
224 state = departuresCount > 1 ? getDateTimeTypeState(departures.get(1)) : state;
226 case EnturNoBindingConstants.CHANNEL_DEPARTURE_03:
227 state = departuresCount > 2 ? getDateTimeTypeState(departures.get(2)) : state;
229 case EnturNoBindingConstants.CHANNEL_DEPARTURE_04:
230 state = departuresCount > 3 ? getDateTimeTypeState(departures.get(3)) : state;
232 case EnturNoBindingConstants.CHANNEL_DEPARTURE_05:
233 state = departuresCount > 4 ? getDateTimeTypeState(departures.get(4)) : state;
235 case EnturNoBindingConstants.ESTIMATED_FLAG_01:
236 state = esitmatedFlagsCount > 0 ? getStringTypeState(estimatedFlags.get(0)) : state;
238 case EnturNoBindingConstants.ESTIMATED_FLAG_02:
239 state = esitmatedFlagsCount > 1 ? getStringTypeState(estimatedFlags.get(1)) : state;
241 case EnturNoBindingConstants.ESTIMATED_FLAG_03:
242 state = esitmatedFlagsCount > 2 ? getStringTypeState(estimatedFlags.get(2)) : state;
244 case EnturNoBindingConstants.ESTIMATED_FLAG_04:
245 state = esitmatedFlagsCount > 3 ? getStringTypeState(estimatedFlags.get(3)) : state;
247 case EnturNoBindingConstants.ESTIMATED_FLAG_05:
248 state = esitmatedFlagsCount > 4 ? getStringTypeState(estimatedFlags.get(4)) : state;
250 case EnturNoBindingConstants.CHANNEL_LINE_CODE:
251 state = getStringTypeState(processedData.get(i).lineCode);
253 case EnturNoBindingConstants.CHANNEL_FRONT_DISPLAY:
254 state = getStringTypeState(processedData.get(i).frontText);
259 logger.debug("Update channel '{}' of group '{}' with new state '{}'.", channelId, channelGroupId, state);
260 updateState(channelUID, state);
262 logger.debug("No real-time data available to update channel '{}' of group '{}'.", channelId,
268 * Update the channel from the last Entur data retrieved.
270 * @param channelUID the id identifying the channel to be updated
272 private void updateStopPlaceChannel(ChannelUID channelUID) {
273 String channelId = channelUID.getIdWithoutGroup();
274 String channelGroupId = channelUID.getGroupId();
275 if (!processedData.isEmpty()) {
276 State state = UnDefType.UNDEF;
278 case EnturNoBindingConstants.CHANNEL_STOP_ID:
279 state = getStringTypeState(processedData.get(0).stopPlaceId);
281 case EnturNoBindingConstants.CHANNEL_STOP_NAME:
282 state = getStringTypeState(processedData.get(0).stopName);
284 case EnturNoBindingConstants.CHANNEL_STOP_TRANSPORT_MODE:
285 state = getStringTypeState(processedData.get(0).transportMode);
290 logger.debug("Update channel '{}' of group '{}' with new state '{}'.", channelId, channelGroupId, state);
291 updateState(channelUID, state);
293 logger.debug("No real-time data available to update channel '{}' of group '{}'.", channelId,
298 private State getDateTimeTypeState(@Nullable String value) {
299 return (value == null) ? UnDefType.UNDEF
300 : new DateTimeType(ZonedDateTime.parse(value, DateTimeFormatter.ISO_DATE_TIME)
301 .withZoneSameInstant(ZoneId.of(EnturNoBindingConstants.TIME_ZONE)));
304 private State getStringTypeState(@Nullable String value) {
305 return (value == null) ? UnDefType.UNDEF : new StringType(value);