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.deutschebahn.internal;
15 import java.text.ParseException;
16 import java.text.SimpleDateFormat;
17 import java.time.ZoneId;
18 import java.time.ZonedDateTime;
19 import java.util.Arrays;
20 import java.util.Collections;
21 import java.util.Date;
22 import java.util.List;
23 import java.util.function.BiConsumer;
24 import java.util.function.Function;
25 import java.util.stream.Collectors;
26 import java.util.stream.Stream;
28 import org.eclipse.jdt.annotation.NonNullByDefault;
29 import org.eclipse.jdt.annotation.Nullable;
30 import org.openhab.binding.deutschebahn.internal.timetable.dto.Event;
31 import org.openhab.binding.deutschebahn.internal.timetable.dto.EventStatus;
32 import org.openhab.binding.deutschebahn.internal.timetable.dto.Message;
33 import org.openhab.core.library.types.DateTimeType;
34 import org.openhab.core.library.types.DecimalType;
35 import org.openhab.core.library.types.OnOffType;
36 import org.openhab.core.library.types.StringType;
37 import org.openhab.core.types.State;
40 * Selector for the Attribute of an {@link Event}.
42 * chapter "1.2.11 Event" in Technical Interface Description for external Developers
44 * @see <a href="https://developers.deutschebahn.com/db-api-marketplace/apis/product/timetables">DB API Marketplace</a>
46 * @author Sönke Küper - initial contribution
48 * @param <VALUE_TYPE> type of value in Bean.
49 * @param <STATE_TYPE> type of state.
52 public final class EventAttribute<VALUE_TYPE, STATE_TYPE extends State>
53 extends AbstractDtoAttributeSelector<Event, @Nullable VALUE_TYPE, STATE_TYPE> {
58 public static final EventAttribute<String, StringType> PPTH = new EventAttribute<>("planned-path", Event::getPpth,
59 Event::setPpth, StringType::new, EventAttribute::splitOnPipeToList, StringType.class);
64 public static final EventAttribute<String, StringType> CPTH = new EventAttribute<>("changed-path", Event::getCpth,
65 Event::setCpth, StringType::new, EventAttribute::splitOnPipeToList, StringType.class);
69 public static final EventAttribute<String, StringType> PP = new EventAttribute<>("planned-platform", Event::getPp,
70 Event::setPp, StringType::new, EventAttribute::singletonList, StringType.class);
74 public static final EventAttribute<String, StringType> CP = new EventAttribute<>("changed-platform", Event::getCp,
75 Event::setCp, StringType::new, EventAttribute::singletonList, StringType.class);
79 public static final EventAttribute<Date, DateTimeType> PT = new EventAttribute<>("planned-time",
80 getDate(Event::getPt), setDate(Event::setPt), EventAttribute::createDateTimeType,
81 EventAttribute::mapDateToStringList, DateTimeType.class);
85 public static final EventAttribute<Date, DateTimeType> CT = new EventAttribute<>("changed-time",
86 getDate(Event::getCt), setDate(Event::setCt), EventAttribute::createDateTimeType,
87 EventAttribute::mapDateToStringList, DateTimeType.class);
91 public static final EventAttribute<EventStatus, StringType> PS = new EventAttribute<>("planned-status",
92 Event::getPs, Event::setPs, EventAttribute::fromEventStatus, EventAttribute::listFromEventStatus,
97 public static final EventAttribute<EventStatus, StringType> CS = new EventAttribute<>("changed-status",
98 Event::getCs, Event::setCs, EventAttribute::fromEventStatus, EventAttribute::listFromEventStatus,
103 public static final EventAttribute<Integer, OnOffType> HI = new EventAttribute<>("hidden", Event::getHi,
104 Event::setHi, EventAttribute::parseHidden, EventAttribute::mapIntegerToStringList, OnOffType.class);
108 public static final EventAttribute<Date, DateTimeType> CLT = new EventAttribute<>("cancellation-time",
109 getDate(Event::getClt), setDate(Event::setClt), EventAttribute::createDateTimeType,
110 EventAttribute::mapDateToStringList, DateTimeType.class);
114 public static final EventAttribute<String, StringType> WINGS = new EventAttribute<>("wings", Event::getWings,
115 Event::setWings, StringType::new, EventAttribute::splitOnPipeToList, StringType.class);
119 public static final EventAttribute<String, StringType> TRA = new EventAttribute<>("transition", Event::getTra,
120 Event::setTra, StringType::new, EventAttribute::singletonList, StringType.class);
122 * Planned distant endpoint.
124 public static final EventAttribute<String, StringType> PDE = new EventAttribute<>("planned-distant-endpoint",
125 Event::getPde, Event::setPde, StringType::new, EventAttribute::singletonList, StringType.class);
127 * Changed distant endpoint.
129 public static final EventAttribute<String, StringType> CDE = new EventAttribute<>("changed-distant-endpoint",
130 Event::getCde, Event::setCde, StringType::new, EventAttribute::singletonList, StringType.class);
134 public static final EventAttribute<Integer, DecimalType> DC = new EventAttribute<>("distant-change", Event::getDc,
135 Event::setDc, DecimalType::new, EventAttribute::mapIntegerToStringList, DecimalType.class);
139 public static final EventAttribute<String, StringType> L = new EventAttribute<>("line", Event::getL, Event::setL,
140 StringType::new, EventAttribute::singletonList, StringType.class);
145 public static final EventAttribute<List<Message>, StringType> MESSAGES = new EventAttribute<>("messages",
146 EventAttribute.getMessages(), EventAttribute::setMessages, EventAttribute::mapMessages,
147 EventAttribute::mapMessagesToList, StringType.class);
150 * Planned Start station.
152 public static final EventAttribute<String, StringType> PLANNED_START_STATION = new EventAttribute<>(
153 "planned-start-station", EventAttribute.getSingleStationFromPath(Event::getPpth, true),
154 EventAttribute.voidSetter(), StringType::new, EventAttribute::singletonList, StringType.class);
157 * Planned Previous stations.
159 public static final EventAttribute<List<String>, StringType> PLANNED_PREVIOUS_STATIONS = new EventAttribute<>(
160 "planned-previous-stations", EventAttribute.getIntermediateStationsFromPath(Event::getPpth, true),
161 EventAttribute.voidSetter(), EventAttribute::fromStringList, EventAttribute::nullToEmptyList,
165 * Planned Target station.
167 public static final EventAttribute<String, StringType> PLANNED_TARGET_STATION = new EventAttribute<>(
168 "planned-target-station", EventAttribute.getSingleStationFromPath(Event::getPpth, false),
169 EventAttribute.voidSetter(), StringType::new, EventAttribute::singletonList, StringType.class);
172 * Planned Following stations.
174 public static final EventAttribute<List<String>, StringType> PLANNED_FOLLOWING_STATIONS = new EventAttribute<>(
175 "planned-following-stations", EventAttribute.getIntermediateStationsFromPath(Event::getPpth, false),
176 EventAttribute.voidSetter(), EventAttribute::fromStringList, EventAttribute::nullToEmptyList,
180 * Changed Start station.
182 public static final EventAttribute<String, StringType> CHANGED_START_STATION = new EventAttribute<>(
183 "changed-start-station", EventAttribute.getSingleStationFromPath(Event::getCpth, true),
184 EventAttribute.voidSetter(), StringType::new, EventAttribute::singletonList, StringType.class);
187 * Changed Previous stations.
189 public static final EventAttribute<List<String>, StringType> CHANGED_PREVIOUS_STATIONS = new EventAttribute<>(
190 "changed-previous-stations", EventAttribute.getIntermediateStationsFromPath(Event::getCpth, true),
191 EventAttribute.voidSetter(), EventAttribute::fromStringList, EventAttribute::nullToEmptyList,
195 * Changed Target station.
197 public static final EventAttribute<String, StringType> CHANGED_TARGET_STATION = new EventAttribute<>(
198 "changed-target-station", EventAttribute.getSingleStationFromPath(Event::getCpth, false),
199 EventAttribute.voidSetter(), StringType::new, EventAttribute::singletonList, StringType.class);
202 * Changed Following stations.
204 public static final EventAttribute<List<String>, StringType> CHANGED_FOLLOWING_STATIONS = new EventAttribute<>(
205 "changed-following-stations", EventAttribute.getIntermediateStationsFromPath(Event::getCpth, false),
206 EventAttribute.voidSetter(), EventAttribute::fromStringList, EventAttribute::nullToEmptyList,
210 * List containing all known {@link EventAttribute}.
212 public static final List<EventAttribute<?, ?>> ALL_ATTRIBUTES = Arrays.asList(PPTH, CPTH, PP, CP, PT, CT, PS, CS,
213 HI, CLT, WINGS, TRA, PDE, CDE, DC, L, MESSAGES);
215 private static final SimpleDateFormat DATETIME_FORMAT = new SimpleDateFormat("yyMMddHHmm");
218 * Creates a new {@link EventAttribute}.
220 * @param getter Function to get the raw value.
221 * @param setter Function to set the raw value.
222 * @param getState Function to get the Value as {@link State}.
224 private EventAttribute(final String channelTypeName, //
225 final Function<Event, @Nullable VALUE_TYPE> getter, //
226 final BiConsumer<Event, VALUE_TYPE> setter, //
227 final Function<VALUE_TYPE, @Nullable STATE_TYPE> getState, //
228 final Function<VALUE_TYPE, List<String>> valueToList, //
229 final Class<STATE_TYPE> stateType) {
230 super(channelTypeName, getter, setter, getState, valueToList, stateType);
233 private static StringType fromEventStatus(final EventStatus value) {
234 return new StringType(value.value());
237 private static List<String> listFromEventStatus(final @Nullable EventStatus value) {
239 return Collections.emptyList();
241 return Collections.singletonList(value.value());
245 private static StringType fromStringList(final List<String> value) {
246 return new StringType(value.stream().collect(Collectors.joining(" - ")));
249 private static List<String> nullToEmptyList(@Nullable final List<String> value) {
250 return value == null ? Collections.emptyList() : value;
254 * Returns a list containing only the given value or empty list if value is <code>null</code>.
256 private static List<String> singletonList(@Nullable String value) {
257 return value == null ? Collections.emptyList() : Collections.singletonList(value);
260 private static OnOffType parseHidden(@Nullable Integer value) {
261 return OnOffType.from(value != null && value == 1);
264 private static Function<Event, @Nullable Date> getDate(final Function<Event, @Nullable String> getValue) {
265 return (final Event event) -> {
266 return parseDate(getValue.apply(event));
270 private static BiConsumer<Event, Date> setDate(final BiConsumer<Event, String> setter) {
271 return (final Event event, final Date value) -> {
272 synchronized (DATETIME_FORMAT) {
273 String formattedDate = DATETIME_FORMAT.format(value);
274 setter.accept(event, formattedDate);
279 private static void setMessages(Event event, List<Message> messages) {
280 event.getM().clear();
281 event.getM().addAll(messages);
285 private static synchronized Date parseDate(@Nullable final String dateValue) {
286 if ((dateValue == null) || dateValue.isEmpty()) {
290 synchronized (DATETIME_FORMAT) {
291 return DATETIME_FORMAT.parse(dateValue);
293 } catch (final ParseException e) {
299 private static DateTimeType createDateTimeType(final @Nullable Date value) {
303 final ZonedDateTime d = ZonedDateTime.ofInstant(value.toInstant(), ZoneId.systemDefault());
304 return new DateTimeType(d);
309 * Maps the status codes from the messages into status texts.
312 private static StringType mapMessages(final @Nullable List<Message> messages) {
313 if (messages == null || messages.isEmpty()) {
314 return StringType.EMPTY;
316 final String messageTexts = messages //
318 .filter((Message message) -> message.getC() != null) //
319 .map(Message::getC) //
321 .map(MessageCodes::getMessage) //
322 .filter((String messageText) -> !messageText.isEmpty()) //
323 .collect(Collectors.joining(" - "));
325 return new StringType(messageTexts);
330 * Maps the status codes from the messages into string list.
332 private static List<String> mapMessagesToList(final @Nullable List<Message> messages) {
333 if (messages == null || messages.isEmpty()) {
334 return Collections.emptyList();
338 .filter((Message message) -> message.getC() != null) //
339 .map(Message::getC) //
341 .map(MessageCodes::getMessage) //
342 .filter((String messageText) -> !messageText.isEmpty()) //
343 .collect(Collectors.toList());
347 private static Function<Event, @Nullable List<Message>> getMessages() {
348 return new Function<Event, @Nullable List<Message>>() {
351 public @Nullable List<Message> apply(Event t) {
352 if (t.getM().isEmpty()) {
361 private static List<String> mapIntegerToStringList(@Nullable Integer value) {
363 return Collections.emptyList();
365 return Collections.singletonList(String.valueOf(value));
369 private static List<String> mapDateToStringList(@Nullable Date value) {
371 return Collections.emptyList();
373 synchronized (DATETIME_FORMAT) {
374 return Collections.singletonList(DATETIME_FORMAT.format(value));
380 * Returns a single station from a path value (i.e. pipe separated value of stations).
382 * @param getPath Getter for the path.
383 * @param returnFirst if <code>true</code> the first value will be returned, <code>false</code> will return the last
386 private static Function<Event, @Nullable String> getSingleStationFromPath(
387 final Function<Event, @Nullable String> getPath, boolean returnFirst) {
388 return (final Event event) -> {
389 String path = getPath.apply(event);
390 if (path == null || path.isEmpty()) {
394 final String[] stations = splitPath(path);
398 return stations[stations.length - 1];
404 * Returns all intermediate stations from a path. The first or last station will be omitted. The values will be
405 * separated by a single dash -.
407 * @param getPath Getter for the path.
408 * @param removeFirst if <code>true</code> the first value will be removed, <code>false</code> will remove the last
411 private static Function<Event, @Nullable List<String>> getIntermediateStationsFromPath(
412 final Function<Event, @Nullable String> getPath, boolean removeFirst) {
413 return (final Event event) -> {
414 final String path = getPath.apply(event);
415 if (path == null || path.isEmpty()) {
418 final String[] stationValues = splitPath(path);
419 Stream<String> stations = Arrays.stream(stationValues);
421 stations = stations.skip(1);
423 stations = stations.limit(stationValues.length - 1);
425 return stations.collect(Collectors.toList());
430 * Setter that does nothing.
431 * Used for derived attributes that can't be set.
433 private static <VALUE_TYPE> BiConsumer<Event, VALUE_TYPE> voidSetter() {
434 return new BiConsumer<Event, VALUE_TYPE>() {
437 public void accept(Event t, VALUE_TYPE u) {
442 private static String[] splitPath(final String path) {
443 return path.split("\\|");
446 private static List<String> splitOnPipeToList(final String value) {
447 return Arrays.asList(value.split("\\|"));
451 * Returns an {@link EventAttribute} for the given channel-type and {@link EventType}.
454 public static EventAttribute<?, ?> getByChannelName(final String channelName, EventType eventType) {
455 switch (channelName) {
460 case "planned-platform":
462 case "changed-platform":
468 case "planned-status":
470 case "changed-status":
474 case "cancellation-time":
480 case "planned-distant-endpoint":
482 case "changed-distant-endpoint":
484 case "distant-change":
490 case "planned-final-station":
491 return eventType == EventType.ARRIVAL ? PLANNED_START_STATION : PLANNED_TARGET_STATION;
492 case "planned-intermediate-stations":
493 return eventType == EventType.ARRIVAL ? PLANNED_PREVIOUS_STATIONS : PLANNED_FOLLOWING_STATIONS;
494 case "changed-final-station":
495 return eventType == EventType.ARRIVAL ? CHANGED_START_STATION : CHANGED_TARGET_STATION;
496 case "changed-intermediate-stations":
497 return eventType == EventType.ARRIVAL ? CHANGED_PREVIOUS_STATIONS : CHANGED_FOLLOWING_STATIONS;