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.ahawastecollection.internal;
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;
25 import java.util.regex.Matcher;
26 import java.util.regex.Pattern;
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;
40 * Schedule that returns the next collection dates from the aha website.
42 * @author Sönke Küper - Initial contribution
45 final class AhaCollectionScheduleImpl implements AhaCollectionSchedule {
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/";
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;
59 * Creates a new {@link AhaCollectionScheduleImpl} for the given location.
61 public AhaCollectionScheduleImpl(final String commune, final String street, final String houseNumber,
62 final String houseNumberAddon, final String collectionPlace) {
63 this.commune = commune;
65 this.houseNumber = houseNumber;
66 this.houseNumberAddon = houseNumberAddon;
67 this.collectionPlace = collectionPlace;
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") //
82 final Elements table = doc.select("table");
84 if (table.size() == 0) {
85 logger.warn("No result table found.");
86 return Collections.emptyMap();
89 final Iterator<Element> rowIt = table.get(0).getElementsByTag("tr").iterator();
90 final Map<WasteType, CollectionDate> result = new HashMap<>();
92 while (rowIt.hasNext()) {
93 final Element currentRow = rowIt.next();
94 if (!currentRow.tagName().equals("tr")) {
97 // Skip header, empty and download button rows.
98 if (isHeader(currentRow) || isDelimiterOrDownloadRow(currentRow)) {
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.");
107 final Element collectionDatesRow = rowIt.next();
109 final CollectionDate date = this.parseRows(currentRow, collectionDatesRow);
111 result.put(date.getType(), date);
118 * Parses the row with the waste type and the following row with the collection dates.
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.
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());
132 final WasteType wasteType = parseWasteType(wasteTypeElement.get(0));
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.");
141 final Element collectionDatesColumn = collectionDatesColumns.get(1);
142 final List<Date> collectionDates = parseTimes(collectionDatesColumn);
144 if (!collectionDates.isEmpty()) {
145 return new CollectionDate(wasteType, collectionDates);
152 * Returns <code>true</code> if the row is an (empty) delimiter row or if its a row that contains the download
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();
160 private boolean isHeader(Element currentRow) {
161 return !currentRow.select("th").isEmpty();
165 * Parses the waste types from the given {@link Element table cell}.
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);
173 return WasteType.parseValue(value);
177 * Parses the {@link CollectionDate} from the given {@link Element table cell}.
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);
186 synchronized (DATE_FORMAT) {
187 result.add(DATE_FORMAT.parse(dateValue));
189 } catch (final ParseException e) {
190 this.logger.warn("Could not parse date: {}", dateValue);