]> git.basschouten.com Git - openhab-addons.git/blob
894d3aae0224a369e2d090de51a78cf9ed4da259
[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.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.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;
27
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;
38
39 /**
40  * Selector for the Attribute of an {@link Event}.
41  *
42  * chapter "1.2.11 Event" in Technical Interface Description for external Developers
43  *
44  * @see <a href="https://developers.deutschebahn.com/db-api-marketplace/apis/product/timetables">DB API Marketplace</a>
45  *
46  * @author Sönke Küper - initial contribution
47  *
48  * @param <VALUE_TYPE> type of value in Bean.
49  * @param <STATE_TYPE> type of state.
50  */
51 @NonNullByDefault
52 public final class EventAttribute<VALUE_TYPE, STATE_TYPE extends State>
53         extends AbstractDtoAttributeSelector<Event, @Nullable VALUE_TYPE, STATE_TYPE> {
54
55     /**
56      * Planned Path.
57      */
58     public static final EventAttribute<String, StringType> PPTH = new EventAttribute<>("planned-path", Event::getPpth,
59             Event::setPpth, StringType::new, EventAttribute::splitOnPipeToList, StringType.class);
60
61     /**
62      * Changed Path.
63      */
64     public static final EventAttribute<String, StringType> CPTH = new EventAttribute<>("changed-path", Event::getCpth,
65             Event::setCpth, StringType::new, EventAttribute::splitOnPipeToList, StringType.class);
66     /**
67      * Planned platform.
68      */
69     public static final EventAttribute<String, StringType> PP = new EventAttribute<>("planned-platform", Event::getPp,
70             Event::setPp, StringType::new, EventAttribute::singletonList, StringType.class);
71     /**
72      * Changed platform.
73      */
74     public static final EventAttribute<String, StringType> CP = new EventAttribute<>("changed-platform", Event::getCp,
75             Event::setCp, StringType::new, EventAttribute::singletonList, StringType.class);
76     /**
77      * Planned time.
78      */
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);
82     /**
83      * Changed time.
84      */
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);
88     /**
89      * Planned status.
90      */
91     public static final EventAttribute<EventStatus, StringType> PS = new EventAttribute<>("planned-status",
92             Event::getPs, Event::setPs, EventAttribute::fromEventStatus, EventAttribute::listFromEventStatus,
93             StringType.class);
94     /**
95      * Changed status.
96      */
97     public static final EventAttribute<EventStatus, StringType> CS = new EventAttribute<>("changed-status",
98             Event::getCs, Event::setCs, EventAttribute::fromEventStatus, EventAttribute::listFromEventStatus,
99             StringType.class);
100     /**
101      * Hidden.
102      */
103     public static final EventAttribute<Integer, OnOffType> HI = new EventAttribute<>("hidden", Event::getHi,
104             Event::setHi, EventAttribute::parseHidden, EventAttribute::mapIntegerToStringList, OnOffType.class);
105     /**
106      * Cancellation time.
107      */
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);
111     /**
112      * Wing.
113      */
114     public static final EventAttribute<String, StringType> WINGS = new EventAttribute<>("wings", Event::getWings,
115             Event::setWings, StringType::new, EventAttribute::splitOnPipeToList, StringType.class);
116     /**
117      * Transition.
118      */
119     public static final EventAttribute<String, StringType> TRA = new EventAttribute<>("transition", Event::getTra,
120             Event::setTra, StringType::new, EventAttribute::singletonList, StringType.class);
121     /**
122      * Planned distant endpoint.
123      */
124     public static final EventAttribute<String, StringType> PDE = new EventAttribute<>("planned-distant-endpoint",
125             Event::getPde, Event::setPde, StringType::new, EventAttribute::singletonList, StringType.class);
126     /**
127      * Changed distant endpoint.
128      */
129     public static final EventAttribute<String, StringType> CDE = new EventAttribute<>("changed-distant-endpoint",
130             Event::getCde, Event::setCde, StringType::new, EventAttribute::singletonList, StringType.class);
131     /**
132      * Distant change.
133      */
134     public static final EventAttribute<Integer, DecimalType> DC = new EventAttribute<>("distant-change", Event::getDc,
135             Event::setDc, DecimalType::new, EventAttribute::mapIntegerToStringList, DecimalType.class);
136     /**
137      * Line.
138      */
139     public static final EventAttribute<String, StringType> L = new EventAttribute<>("line", Event::getL, Event::setL,
140             StringType::new, EventAttribute::singletonList, StringType.class);
141
142     /**
143      * Messages.
144      */
145     public static final EventAttribute<List<Message>, StringType> MESSAGES = new EventAttribute<>("messages",
146             EventAttribute.getMessages(), EventAttribute::setMessages, EventAttribute::mapMessages,
147             EventAttribute::mapMessagesToList, StringType.class);
148
149     /**
150      * Planned Start station.
151      */
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);
155
156     /**
157      * Planned Previous stations.
158      */
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,
162             StringType.class);
163
164     /**
165      * Planned Target station.
166      */
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);
170
171     /**
172      * Planned Following stations.
173      */
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,
177             StringType.class);
178
179     /**
180      * Changed Start station.
181      */
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);
185
186     /**
187      * Changed Previous stations.
188      */
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,
192             StringType.class);
193
194     /**
195      * Changed Target station.
196      */
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);
200
201     /**
202      * Changed Following stations.
203      */
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,
207             StringType.class);
208
209     /**
210      * List containing all known {@link EventAttribute}.
211      */
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);
214
215     private static final SimpleDateFormat DATETIME_FORMAT = new SimpleDateFormat("yyMMddHHmm");
216
217     /**
218      * Creates a new {@link EventAttribute}.
219      *
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}.
223      */
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);
231     }
232
233     private static StringType fromEventStatus(final EventStatus value) {
234         return new StringType(value.value());
235     }
236
237     private static List<String> listFromEventStatus(final @Nullable EventStatus value) {
238         if (value == null) {
239             return Collections.emptyList();
240         } else {
241             return Collections.singletonList(value.value());
242         }
243     }
244
245     private static StringType fromStringList(final List<String> value) {
246         return new StringType(value.stream().collect(Collectors.joining(" - ")));
247     }
248
249     private static List<String> nullToEmptyList(@Nullable final List<String> value) {
250         return value == null ? Collections.emptyList() : value;
251     }
252
253     /**
254      * Returns a list containing only the given value or empty list if value is <code>null</code>.
255      */
256     private static List<String> singletonList(@Nullable String value) {
257         return value == null ? Collections.emptyList() : Collections.singletonList(value);
258     }
259
260     private static OnOffType parseHidden(@Nullable Integer value) {
261         return OnOffType.from(value != null && value == 1);
262     }
263
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));
267         };
268     }
269
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);
275             }
276         };
277     }
278
279     private static void setMessages(Event event, List<Message> messages) {
280         event.getM().clear();
281         event.getM().addAll(messages);
282     }
283
284     @Nullable
285     private static synchronized Date parseDate(@Nullable final String dateValue) {
286         if ((dateValue == null) || dateValue.isEmpty()) {
287             return null;
288         }
289         try {
290             synchronized (DATETIME_FORMAT) {
291                 return DATETIME_FORMAT.parse(dateValue);
292             }
293         } catch (final ParseException e) {
294             return null;
295         }
296     }
297
298     @Nullable
299     private static DateTimeType createDateTimeType(final @Nullable Date value) {
300         if (value == null) {
301             return null;
302         } else {
303             final ZonedDateTime d = ZonedDateTime.ofInstant(value.toInstant(), ZoneId.systemDefault());
304             return new DateTimeType(d);
305         }
306     }
307
308     /**
309      * Maps the status codes from the messages into status texts.
310      */
311     @Nullable
312     private static StringType mapMessages(final @Nullable List<Message> messages) {
313         if (messages == null || messages.isEmpty()) {
314             return StringType.EMPTY;
315         } else {
316             final String messageTexts = messages //
317                     .stream()//
318                     .filter((Message message) -> message.getC() != null) //
319                     .map(Message::getC) //
320                     .distinct() //
321                     .map(MessageCodes::getMessage) //
322                     .filter((String messageText) -> !messageText.isEmpty()) //
323                     .collect(Collectors.joining(" - "));
324
325             return new StringType(messageTexts);
326         }
327     }
328
329     /**
330      * Maps the status codes from the messages into string list.
331      */
332     private static List<String> mapMessagesToList(final @Nullable List<Message> messages) {
333         if (messages == null || messages.isEmpty()) {
334             return Collections.emptyList();
335         } else {
336             return messages //
337                     .stream()//
338                     .filter((Message message) -> message.getC() != null) //
339                     .map(Message::getC) //
340                     .distinct() //
341                     .map(MessageCodes::getMessage) //
342                     .filter((String messageText) -> !messageText.isEmpty()) //
343                     .collect(Collectors.toList());
344         }
345     }
346
347     private static Function<Event, @Nullable List<Message>> getMessages() {
348         return new Function<Event, @Nullable List<Message>>() {
349
350             @Override
351             public @Nullable List<Message> apply(Event t) {
352                 if (t.getM().isEmpty()) {
353                     return null;
354                 } else {
355                     return t.getM();
356                 }
357             }
358         };
359     }
360
361     private static List<String> mapIntegerToStringList(@Nullable Integer value) {
362         if (value == null) {
363             return Collections.emptyList();
364         } else {
365             return Collections.singletonList(String.valueOf(value));
366         }
367     }
368
369     private static List<String> mapDateToStringList(@Nullable Date value) {
370         if (value == null) {
371             return Collections.emptyList();
372         } else {
373             synchronized (DATETIME_FORMAT) {
374                 return Collections.singletonList(DATETIME_FORMAT.format(value));
375             }
376         }
377     }
378
379     /**
380      * Returns a single station from a path value (i.e. pipe separated value of stations).
381      * 
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
384      *            value.
385      */
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()) {
391                 return null;
392             }
393
394             final String[] stations = splitPath(path);
395             if (returnFirst) {
396                 return stations[0];
397             } else {
398                 return stations[stations.length - 1];
399             }
400         };
401     }
402
403     /**
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 -.
406      * 
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
409      *            value.
410      */
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()) {
416                 return null;
417             }
418             final String[] stationValues = splitPath(path);
419             Stream<String> stations = Arrays.stream(stationValues);
420             if (removeFirst) {
421                 stations = stations.skip(1);
422             } else {
423                 stations = stations.limit(stationValues.length - 1);
424             }
425             return stations.collect(Collectors.toList());
426         };
427     }
428
429     /**
430      * Setter that does nothing.
431      * Used for derived attributes that can't be set.
432      */
433     private static <VALUE_TYPE> BiConsumer<Event, VALUE_TYPE> voidSetter() {
434         return new BiConsumer<Event, VALUE_TYPE>() {
435
436             @Override
437             public void accept(Event t, VALUE_TYPE u) {
438             }
439         };
440     }
441
442     private static String[] splitPath(final String path) {
443         return path.split("\\|");
444     }
445
446     private static List<String> splitOnPipeToList(final String value) {
447         return Arrays.asList(value.split("\\|"));
448     }
449
450     /**
451      * Returns an {@link EventAttribute} for the given channel-type and {@link EventType}.
452      */
453     @Nullable
454     public static EventAttribute<?, ?> getByChannelName(final String channelName, EventType eventType) {
455         switch (channelName) {
456             case "planned-path":
457                 return PPTH;
458             case "changed-path":
459                 return CPTH;
460             case "planned-platform":
461                 return PP;
462             case "changed-platform":
463                 return CP;
464             case "planned-time":
465                 return PT;
466             case "changed-time":
467                 return CT;
468             case "planned-status":
469                 return PS;
470             case "changed-status":
471                 return CS;
472             case "hidden":
473                 return HI;
474             case "cancellation-time":
475                 return CLT;
476             case "wings":
477                 return WINGS;
478             case "transition":
479                 return TRA;
480             case "planned-distant-endpoint":
481                 return PDE;
482             case "changed-distant-endpoint":
483                 return CDE;
484             case "distant-change":
485                 return DC;
486             case "line":
487                 return L;
488             case "messages":
489                 return MESSAGES;
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;
498             default:
499                 return null;
500         }
501     }
502 }