]> git.basschouten.com Git - openhab-addons.git/blob
6e888a3d53d50ae82af9fc1bdfc284698cdfc8b9
[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.ahawastecollection.internal;
14
15 import java.io.IOException;
16 import java.text.ParseException;
17 import java.text.SimpleDateFormat;
18 import java.util.ArrayList;
19 import java.util.Collections;
20 import java.util.Date;
21 import java.util.HashMap;
22 import java.util.Iterator;
23 import java.util.List;
24 import java.util.Map;
25 import java.util.regex.Matcher;
26 import java.util.regex.Pattern;
27
28 import org.eclipse.jdt.annotation.NonNullByDefault;
29 import org.eclipse.jdt.annotation.Nullable;
30 import org.jsoup.Connection.Method;
31 import org.jsoup.Jsoup;
32 import org.jsoup.nodes.Document;
33 import org.jsoup.nodes.Element;
34 import org.jsoup.select.Elements;
35 import org.openhab.binding.ahawastecollection.internal.CollectionDate.WasteType;
36 import org.slf4j.Logger;
37 import org.slf4j.LoggerFactory;
38
39 /**
40  * Schedule that returns the next collection dates from the aha website.
41  *
42  * @author Sönke Küper - Initial contribution
43  */
44 @NonNullByDefault
45 final class AhaCollectionScheduleImpl implements AhaCollectionSchedule {
46
47     private static final Pattern TIME_PATTERN = Pattern.compile("\\S\\S,\\s(\\d\\d.\\d\\d.\\d\\d\\d\\d)");
48     private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("dd.MM.yyyy");
49     private static final String WEBSITE_URL = "https://www.aha-region.de/abholtermine/abfuhrkalender/";
50
51     private final Logger logger = LoggerFactory.getLogger(AhaCollectionScheduleImpl.class);
52     private final String commune;
53     private final String street;
54     private final String houseNumber;
55     private final String houseNumberAddon;
56     private final String collectionPlace;
57
58     /**
59      * Creates a new {@link AhaCollectionScheduleImpl} for the given location.
60      */
61     public AhaCollectionScheduleImpl(final String commune, final String street, final String houseNumber,
62             final String houseNumberAddon, final String collectionPlace) {
63         this.commune = commune;
64         this.street = street;
65         this.houseNumber = houseNumber;
66         this.houseNumberAddon = houseNumberAddon;
67         this.collectionPlace = collectionPlace;
68     }
69
70     @Override
71     public Map<WasteType, CollectionDate> getCollectionDates() throws IOException {
72         final Document doc = Jsoup.connect(WEBSITE_URL) //
73                 .method(Method.POST) //
74                 .data("gemeinde", this.commune) //
75                 .data("strasse", this.street) //
76                 .data("hausnr", this.houseNumber) //
77                 .data("hausnraddon", this.houseNumberAddon) //
78                 .data("ladeort", this.collectionPlace) //
79                 .data("anzeigen", "Suchen") //
80                 .get();
81
82         final Elements table = doc.select("table");
83
84         if (table.size() == 0) {
85             logger.warn("No result table found.");
86             return Collections.emptyMap();
87         }
88
89         final Iterator<Element> rowIt = table.get(0).getElementsByTag("tr").iterator();
90         final Map<WasteType, CollectionDate> result = new HashMap<>();
91
92         while (rowIt.hasNext()) {
93             final Element currentRow = rowIt.next();
94             if (!currentRow.tagName().equals("tr")) {
95                 continue;
96             }
97             // Skip header, empty and download button rows.
98             if (isHeader(currentRow) || isDelimiterOrDownloadRow(currentRow)) {
99                 continue;
100             }
101
102             // If no following row is present, no collection dates can be parsed
103             if (!rowIt.hasNext()) {
104                 logger.warn("No row with collection dates found.");
105                 break;
106             }
107             final Element collectionDatesRow = rowIt.next();
108
109             final CollectionDate date = this.parseRows(currentRow, collectionDatesRow);
110             if (date != null) {
111                 result.put(date.getType(), date);
112             }
113         }
114         return result;
115     }
116
117     /**
118      * Parses the row with the waste type and the following row with the collection dates.
119      * 
120      * @param wasteTypeRow Row that contains the waste type information
121      * @param collectionDatesRow Row that contains the collection date informations.
122      * @return The parsed {@link CollectionDate} or <code>null</code> if information could not be parsed.
123      */
124     @Nullable
125     private CollectionDate parseRows(Element wasteTypeRow, Element collectionDatesRow) {
126         // Try to extract the waste Type from the first row
127         final Elements wasteTypeElement = wasteTypeRow.select("td").select("strong");
128         if (wasteTypeElement.size() != 1) {
129             this.logger.warn("Could not parse waste type row: {}", wasteTypeRow.toString());
130             return null;
131         }
132         final WasteType wasteType = parseWasteType(wasteTypeElement.get(0));
133
134         // Try to extract the collection dates from the second row
135         final Elements collectionDatesColumns = collectionDatesRow.select("td");
136         if (collectionDatesColumns.size() != 3) {
137             this.logger.warn("collection dates row could not be parsed.");
138             return null;
139         }
140
141         final Element collectionDatesColumn = collectionDatesColumns.get(1);
142         final List<Date> collectionDates = parseTimes(collectionDatesColumn);
143
144         if (!collectionDates.isEmpty()) {
145             return new CollectionDate(wasteType, collectionDates);
146         } else {
147             return null;
148         }
149     }
150
151     /**
152      * Returns <code>true</code> if the row is an (empty) delimiter row or if its a row that contains the download
153      * buttons for ical.
154      */
155     private boolean isDelimiterOrDownloadRow(Element currentRow) {
156         final Elements columns = currentRow.select("td");
157         return columns.size() == 1 && columns.get(0).text().isBlank() || !columns.select("form").isEmpty();
158     }
159
160     private boolean isHeader(Element currentRow) {
161         return !currentRow.select("th").isEmpty();
162     }
163
164     /**
165      * Parses the waste types from the given {@link Element table cell}.
166      */
167     private static WasteType parseWasteType(final Element element) {
168         String value = element.text().trim();
169         final int firstSpace = value.indexOf(" ");
170         if (firstSpace > 0) {
171             value = value.substring(0, firstSpace);
172         }
173         return WasteType.parseValue(value);
174     }
175
176     /**
177      * Parses the {@link CollectionDate} from the given {@link Element table cell}.
178      */
179     private List<Date> parseTimes(final Element element) {
180         final List<Date> result = new ArrayList<>();
181         final String value = element.text();
182         final Matcher matcher = TIME_PATTERN.matcher(value);
183         while (matcher.find()) {
184             final String dateValue = matcher.group(1);
185             try {
186                 synchronized (DATE_FORMAT) {
187                     result.add(DATE_FORMAT.parse(dateValue));
188                 }
189             } catch (final ParseException e) {
190                 this.logger.warn("Could not parse date: {}", dateValue);
191             }
192         }
193         return result;
194     }
195 }