]> git.basschouten.com Git - openhab-addons.git/blob
c8c8eb947e35b4cc5669f68ea98578cdabb8ee96
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 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.deutschebahn.internal;
14
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.Date;
21 import java.util.List;
22 import java.util.function.BiConsumer;
23 import java.util.function.Function;
24 import java.util.stream.Collectors;
25 import java.util.stream.Stream;
26
27 import org.eclipse.jdt.annotation.NonNullByDefault;
28 import org.eclipse.jdt.annotation.Nullable;
29 import org.openhab.binding.deutschebahn.internal.timetable.dto.Event;
30 import org.openhab.binding.deutschebahn.internal.timetable.dto.EventStatus;
31 import org.openhab.binding.deutschebahn.internal.timetable.dto.Message;
32 import org.openhab.core.library.types.DateTimeType;
33 import org.openhab.core.library.types.DecimalType;
34 import org.openhab.core.library.types.OnOffType;
35 import org.openhab.core.library.types.StringType;
36 import org.openhab.core.types.State;
37
38 /**
39  * Selector for the Attribute of an {@link Event}.
40  *
41  * chapter "1.2.11 Event" in Technical Interface Description for external Developers
42  *
43  * @see <a href="https://developers.deutschebahn.com/db-api-marketplace/apis/product/timetables">DB API Marketplace</a>
44  *
45  * @author Sönke Küper - initial contribution
46  *
47  * @param <VALUE_TYPE> type of value in Bean.
48  * @param <STATE_TYPE> type of state.
49  */
50 @NonNullByDefault
51 public final class EventAttribute<VALUE_TYPE, STATE_TYPE extends State>
52         extends AbstractDtoAttributeSelector<Event, @Nullable VALUE_TYPE, STATE_TYPE> {
53
54     /**
55      * Planned Path.
56      */
57     public static final EventAttribute<String, StringType> PPTH = new EventAttribute<>("planned-path", Event::getPpth,
58             Event::setPpth, StringType::new, EventAttribute::splitOnPipeToList, StringType.class);
59
60     /**
61      * Changed Path.
62      */
63     public static final EventAttribute<String, StringType> CPTH = new EventAttribute<>("changed-path", Event::getCpth,
64             Event::setCpth, StringType::new, EventAttribute::splitOnPipeToList, StringType.class);
65     /**
66      * Planned platform.
67      */
68     public static final EventAttribute<String, StringType> PP = new EventAttribute<>("planned-platform", Event::getPp,
69             Event::setPp, StringType::new, EventAttribute::singletonList, StringType.class);
70     /**
71      * Changed platform.
72      */
73     public static final EventAttribute<String, StringType> CP = new EventAttribute<>("changed-platform", Event::getCp,
74             Event::setCp, StringType::new, EventAttribute::singletonList, StringType.class);
75     /**
76      * Planned time.
77      */
78     public static final EventAttribute<Date, DateTimeType> PT = new EventAttribute<>("planned-time",
79             getDate(Event::getPt), setDate(Event::setPt), EventAttribute::createDateTimeType,
80             EventAttribute::mapDateToStringList, DateTimeType.class);
81     /**
82      * Changed time.
83      */
84     public static final EventAttribute<Date, DateTimeType> CT = new EventAttribute<>("changed-time",
85             getDate(Event::getCt), setDate(Event::setCt), EventAttribute::createDateTimeType,
86             EventAttribute::mapDateToStringList, DateTimeType.class);
87     /**
88      * Planned status.
89      */
90     public static final EventAttribute<EventStatus, StringType> PS = new EventAttribute<>("planned-status",
91             Event::getPs, Event::setPs, EventAttribute::fromEventStatus, EventAttribute::listFromEventStatus,
92             StringType.class);
93     /**
94      * Changed status.
95      */
96     public static final EventAttribute<EventStatus, StringType> CS = new EventAttribute<>("changed-status",
97             Event::getCs, Event::setCs, EventAttribute::fromEventStatus, EventAttribute::listFromEventStatus,
98             StringType.class);
99     /**
100      * Hidden.
101      */
102     public static final EventAttribute<Integer, OnOffType> HI = new EventAttribute<>("hidden", Event::getHi,
103             Event::setHi, EventAttribute::parseHidden, EventAttribute::mapIntegerToStringList, OnOffType.class);
104     /**
105      * Cancellation time.
106      */
107     public static final EventAttribute<Date, DateTimeType> CLT = new EventAttribute<>("cancellation-time",
108             getDate(Event::getClt), setDate(Event::setClt), EventAttribute::createDateTimeType,
109             EventAttribute::mapDateToStringList, DateTimeType.class);
110     /**
111      * Wing.
112      */
113     public static final EventAttribute<String, StringType> WINGS = new EventAttribute<>("wings", Event::getWings,
114             Event::setWings, StringType::new, EventAttribute::splitOnPipeToList, StringType.class);
115     /**
116      * Transition.
117      */
118     public static final EventAttribute<String, StringType> TRA = new EventAttribute<>("transition", Event::getTra,
119             Event::setTra, StringType::new, EventAttribute::singletonList, StringType.class);
120     /**
121      * Planned distant endpoint.
122      */
123     public static final EventAttribute<String, StringType> PDE = new EventAttribute<>("planned-distant-endpoint",
124             Event::getPde, Event::setPde, StringType::new, EventAttribute::singletonList, StringType.class);
125     /**
126      * Changed distant endpoint.
127      */
128     public static final EventAttribute<String, StringType> CDE = new EventAttribute<>("changed-distant-endpoint",
129             Event::getCde, Event::setCde, StringType::new, EventAttribute::singletonList, StringType.class);
130     /**
131      * Distant change.
132      */
133     public static final EventAttribute<Integer, DecimalType> DC = new EventAttribute<>("distant-change", Event::getDc,
134             Event::setDc, DecimalType::new, EventAttribute::mapIntegerToStringList, DecimalType.class);
135     /**
136      * Line.
137      */
138     public static final EventAttribute<String, StringType> L = new EventAttribute<>("line", Event::getL, Event::setL,
139             StringType::new, EventAttribute::singletonList, StringType.class);
140
141     /**
142      * Messages.
143      */
144     public static final EventAttribute<List<Message>, StringType> MESSAGES = new EventAttribute<>("messages",
145             EventAttribute.getMessages(), EventAttribute::setMessages, EventAttribute::mapMessages,
146             EventAttribute::mapMessagesToList, StringType.class);
147
148     /**
149      * Planned Start station.
150      */
151     public static final EventAttribute<String, StringType> PLANNED_START_STATION = new EventAttribute<>(
152             "planned-start-station", EventAttribute.getSingleStationFromPath(Event::getPpth, true),
153             EventAttribute.voidSetter(), StringType::new, EventAttribute::singletonList, StringType.class);
154
155     /**
156      * Planned Previous stations.
157      */
158     public static final EventAttribute<List<String>, StringType> PLANNED_PREVIOUS_STATIONS = new EventAttribute<>(
159             "planned-previous-stations", EventAttribute.getIntermediateStationsFromPath(Event::getPpth, true),
160             EventAttribute.voidSetter(), EventAttribute::fromStringList, EventAttribute::nullToEmptyList,
161             StringType.class);
162
163     /**
164      * Planned Target station.
165      */
166     public static final EventAttribute<String, StringType> PLANNED_TARGET_STATION = new EventAttribute<>(
167             "planned-target-station", EventAttribute.getSingleStationFromPath(Event::getPpth, false),
168             EventAttribute.voidSetter(), StringType::new, EventAttribute::singletonList, StringType.class);
169
170     /**
171      * Planned Following stations.
172      */
173     public static final EventAttribute<List<String>, StringType> PLANNED_FOLLOWING_STATIONS = new EventAttribute<>(
174             "planned-following-stations", EventAttribute.getIntermediateStationsFromPath(Event::getPpth, false),
175             EventAttribute.voidSetter(), EventAttribute::fromStringList, EventAttribute::nullToEmptyList,
176             StringType.class);
177
178     /**
179      * Changed Start station.
180      */
181     public static final EventAttribute<String, StringType> CHANGED_START_STATION = new EventAttribute<>(
182             "changed-start-station", EventAttribute.getSingleStationFromPath(Event::getCpth, true),
183             EventAttribute.voidSetter(), StringType::new, EventAttribute::singletonList, StringType.class);
184
185     /**
186      * Changed Previous stations.
187      */
188     public static final EventAttribute<List<String>, StringType> CHANGED_PREVIOUS_STATIONS = new EventAttribute<>(
189             "changed-previous-stations", EventAttribute.getIntermediateStationsFromPath(Event::getCpth, true),
190             EventAttribute.voidSetter(), EventAttribute::fromStringList, EventAttribute::nullToEmptyList,
191             StringType.class);
192
193     /**
194      * Changed Target station.
195      */
196     public static final EventAttribute<String, StringType> CHANGED_TARGET_STATION = new EventAttribute<>(
197             "changed-target-station", EventAttribute.getSingleStationFromPath(Event::getCpth, false),
198             EventAttribute.voidSetter(), StringType::new, EventAttribute::singletonList, StringType.class);
199
200     /**
201      * Changed Following stations.
202      */
203     public static final EventAttribute<List<String>, StringType> CHANGED_FOLLOWING_STATIONS = new EventAttribute<>(
204             "changed-following-stations", EventAttribute.getIntermediateStationsFromPath(Event::getCpth, false),
205             EventAttribute.voidSetter(), EventAttribute::fromStringList, EventAttribute::nullToEmptyList,
206             StringType.class);
207
208     /**
209      * List containing all known {@link EventAttribute}.
210      */
211     public static final List<EventAttribute<?, ?>> ALL_ATTRIBUTES = Arrays.asList(PPTH, CPTH, PP, CP, PT, CT, PS, CS,
212             HI, CLT, WINGS, TRA, PDE, CDE, DC, L, MESSAGES);
213
214     private static final SimpleDateFormat DATETIME_FORMAT = new SimpleDateFormat("yyMMddHHmm");
215
216     /**
217      * Creates a new {@link EventAttribute}.
218      *
219      * @param getter Function to get the raw value.
220      * @param setter Function to set the raw value.
221      * @param getState Function to get the Value as {@link State}.
222      */
223     private EventAttribute(final String channelTypeName, //
224             final Function<Event, @Nullable VALUE_TYPE> getter, //
225             final BiConsumer<Event, VALUE_TYPE> setter, //
226             final Function<VALUE_TYPE, @Nullable STATE_TYPE> getState, //
227             final Function<VALUE_TYPE, List<String>> valueToList, //
228             final Class<STATE_TYPE> stateType) {
229         super(channelTypeName, getter, setter, getState, valueToList, stateType);
230     }
231
232     private static StringType fromEventStatus(final EventStatus value) {
233         return new StringType(value.value());
234     }
235
236     private static List<String> listFromEventStatus(final @Nullable EventStatus value) {
237         if (value == null) {
238             return List.of();
239         } else {
240             return List.of(value.value());
241         }
242     }
243
244     private static StringType fromStringList(final List<String> value) {
245         return new StringType(value.stream().collect(Collectors.joining(" - ")));
246     }
247
248     private static List<String> nullToEmptyList(@Nullable final List<String> value) {
249         return value == null ? List.of() : value;
250     }
251
252     /**
253      * Returns a list containing only the given value or empty list if value is <code>null</code>.
254      */
255     private static List<String> singletonList(@Nullable String value) {
256         return value == null ? List.of() : List.of(value);
257     }
258
259     private static OnOffType parseHidden(@Nullable Integer value) {
260         return OnOffType.from(value != null && value == 1);
261     }
262
263     private static Function<Event, @Nullable Date> getDate(final Function<Event, @Nullable String> getValue) {
264         return (final Event event) -> parseDate(getValue.apply(event));
265     }
266
267     private static BiConsumer<Event, Date> setDate(final BiConsumer<Event, String> setter) {
268         return (final Event event, final Date value) -> {
269             synchronized (DATETIME_FORMAT) {
270                 String formattedDate = DATETIME_FORMAT.format(value);
271                 setter.accept(event, formattedDate);
272             }
273         };
274     }
275
276     private static void setMessages(Event event, List<Message> messages) {
277         event.getM().clear();
278         event.getM().addAll(messages);
279     }
280
281     @Nullable
282     private static synchronized Date parseDate(@Nullable final String dateValue) {
283         if ((dateValue == null) || dateValue.isEmpty()) {
284             return null;
285         }
286         try {
287             synchronized (DATETIME_FORMAT) {
288                 return DATETIME_FORMAT.parse(dateValue);
289             }
290         } catch (final ParseException e) {
291             return null;
292         }
293     }
294
295     @Nullable
296     private static DateTimeType createDateTimeType(final @Nullable Date value) {
297         if (value == null) {
298             return null;
299         } else {
300             final ZonedDateTime d = ZonedDateTime.ofInstant(value.toInstant(), ZoneId.systemDefault());
301             return new DateTimeType(d);
302         }
303     }
304
305     /**
306      * Maps the status codes from the messages into status texts.
307      */
308     @Nullable
309     private static StringType mapMessages(final @Nullable List<Message> messages) {
310         if (messages == null || messages.isEmpty()) {
311             return StringType.EMPTY;
312         } else {
313             final String messageTexts = messages //
314                     .stream()//
315                     .filter((Message message) -> message.getC() != null) //
316                     .map(Message::getC) //
317                     .distinct() //
318                     .map(MessageCodes::getMessage) //
319                     .filter((String messageText) -> !messageText.isEmpty()) //
320                     .collect(Collectors.joining(" - "));
321
322             return new StringType(messageTexts);
323         }
324     }
325
326     /**
327      * Maps the status codes from the messages into string list.
328      */
329     private static List<String> mapMessagesToList(final @Nullable List<Message> messages) {
330         if (messages == null || messages.isEmpty()) {
331             return List.of();
332         } else {
333             return messages //
334                     .stream()//
335                     .filter((Message message) -> message.getC() != null) //
336                     .map(Message::getC) //
337                     .distinct() //
338                     .map(MessageCodes::getMessage) //
339                     .filter((String messageText) -> !messageText.isEmpty()) //
340                     .collect(Collectors.toList());
341         }
342     }
343
344     private static Function<Event, @Nullable List<Message>> getMessages() {
345         return new Function<Event, @Nullable List<Message>>() {
346
347             @Override
348             public @Nullable List<Message> apply(Event t) {
349                 if (t.getM().isEmpty()) {
350                     return null;
351                 } else {
352                     return t.getM();
353                 }
354             }
355         };
356     }
357
358     private static List<String> mapIntegerToStringList(@Nullable Integer value) {
359         if (value == null) {
360             return List.of();
361         } else {
362             return List.of(String.valueOf(value));
363         }
364     }
365
366     private static List<String> mapDateToStringList(@Nullable Date value) {
367         if (value == null) {
368             return List.of();
369         } else {
370             synchronized (DATETIME_FORMAT) {
371                 return List.of(DATETIME_FORMAT.format(value));
372             }
373         }
374     }
375
376     /**
377      * Returns a single station from a path value (i.e. pipe separated value of stations).
378      * 
379      * @param getPath Getter for the path.
380      * @param returnFirst if <code>true</code> the first value will be returned, <code>false</code> will return the last
381      *            value.
382      */
383     private static Function<Event, @Nullable String> getSingleStationFromPath(
384             final Function<Event, @Nullable String> getPath, boolean returnFirst) {
385         return (final Event event) -> {
386             String path = getPath.apply(event);
387             if (path == null || path.isEmpty()) {
388                 return null;
389             }
390
391             final String[] stations = splitPath(path);
392             if (returnFirst) {
393                 return stations[0];
394             } else {
395                 return stations[stations.length - 1];
396             }
397         };
398     }
399
400     /**
401      * Returns all intermediate stations from a path. The first or last station will be omitted. The values will be
402      * separated by a single dash -.
403      * 
404      * @param getPath Getter for the path.
405      * @param removeFirst if <code>true</code> the first value will be removed, <code>false</code> will remove the last
406      *            value.
407      */
408     private static Function<Event, @Nullable List<String>> getIntermediateStationsFromPath(
409             final Function<Event, @Nullable String> getPath, boolean removeFirst) {
410         return (final Event event) -> {
411             final String path = getPath.apply(event);
412             if (path == null || path.isEmpty()) {
413                 return null;
414             }
415             final String[] stationValues = splitPath(path);
416             Stream<String> stations = Arrays.stream(stationValues);
417             if (removeFirst) {
418                 stations = stations.skip(1);
419             } else {
420                 stations = stations.limit(stationValues.length - 1);
421             }
422             return stations.collect(Collectors.toList());
423         };
424     }
425
426     /**
427      * Setter that does nothing.
428      * Used for derived attributes that can't be set.
429      */
430     private static <VALUE_TYPE> BiConsumer<Event, VALUE_TYPE> voidSetter() {
431         return new BiConsumer<Event, VALUE_TYPE>() {
432
433             @Override
434             public void accept(Event t, VALUE_TYPE u) {
435             }
436         };
437     }
438
439     private static String[] splitPath(final String path) {
440         return path.split("\\|");
441     }
442
443     private static List<String> splitOnPipeToList(final String value) {
444         return Arrays.asList(value.split("\\|"));
445     }
446
447     /**
448      * Returns an {@link EventAttribute} for the given channel-type and {@link EventType}.
449      */
450     @Nullable
451     public static EventAttribute<?, ?> getByChannelName(final String channelName, EventType eventType) {
452         switch (channelName) {
453             case "planned-path":
454                 return PPTH;
455             case "changed-path":
456                 return CPTH;
457             case "planned-platform":
458                 return PP;
459             case "changed-platform":
460                 return CP;
461             case "planned-time":
462                 return PT;
463             case "changed-time":
464                 return CT;
465             case "planned-status":
466                 return PS;
467             case "changed-status":
468                 return CS;
469             case "hidden":
470                 return HI;
471             case "cancellation-time":
472                 return CLT;
473             case "wings":
474                 return WINGS;
475             case "transition":
476                 return TRA;
477             case "planned-distant-endpoint":
478                 return PDE;
479             case "changed-distant-endpoint":
480                 return CDE;
481             case "distant-change":
482                 return DC;
483             case "line":
484                 return L;
485             case "messages":
486                 return MESSAGES;
487             case "planned-final-station":
488                 return eventType == EventType.ARRIVAL ? PLANNED_START_STATION : PLANNED_TARGET_STATION;
489             case "planned-intermediate-stations":
490                 return eventType == EventType.ARRIVAL ? PLANNED_PREVIOUS_STATIONS : PLANNED_FOLLOWING_STATIONS;
491             case "changed-final-station":
492                 return eventType == EventType.ARRIVAL ? CHANGED_START_STATION : CHANGED_TARGET_STATION;
493             case "changed-intermediate-stations":
494                 return eventType == EventType.ARRIVAL ? CHANGED_PREVIOUS_STATIONS : CHANGED_FOLLOWING_STATIONS;
495             default:
496                 return null;
497         }
498     }
499 }