diff --git a/CODEOWNERS b/CODEOWNERS index 454ca798a43fb..c89961092dc97 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -9,6 +9,7 @@ /bundles/org.openhab.automation.jythonscripting/ @openhab/add-ons-maintainers /bundles/org.openhab.automation.pidcontroller/ @fwolter /bundles/org.openhab.binding.adorne/ @theiding +/bundles/org.openhab.binding.ahawastecollection/ @soenkekueper /bundles/org.openhab.binding.airq/ @aurelio1 /bundles/org.openhab.binding.airquality/ @kubawolanin /bundles/org.openhab.binding.airvisualnode/ @3cky diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml index 10205adc0c26b..a26ae4965a197 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -36,6 +36,11 @@ org.openhab.binding.adorne ${project.version} + + org.openhab.addons.bundles + org.openhab.binding.ahawastecollection + ${project.version} + org.openhab.addons.bundles org.openhab.binding.airq diff --git a/bundles/org.openhab.binding.ahawastecollection/NOTICE b/bundles/org.openhab.binding.ahawastecollection/NOTICE new file mode 100644 index 0000000000000..810b7b7e4f792 --- /dev/null +++ b/bundles/org.openhab.binding.ahawastecollection/NOTICE @@ -0,0 +1,21 @@ +This content is produced and maintained by the openHAB project. + +* Project home: https://www.openhab.org + +== Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Eclipse Public License 2.0 which is available at +https://www.eclipse.org/legal/epl-2.0/. + +== Source Code + +https://github.com/openhab/openhab-addons + + +== Third-party Content + +jsoup +* License: MIT License +* Project: https://jsoup.org/ +* Source: https://github.com/jhy/jsoup diff --git a/bundles/org.openhab.binding.ahawastecollection/README.md b/bundles/org.openhab.binding.ahawastecollection/README.md new file mode 100644 index 0000000000000..a78b2a1998bda --- /dev/null +++ b/bundles/org.openhab.binding.ahawastecollection/README.md @@ -0,0 +1,139 @@ +# aha Waste Collection Binding + +This binding provides information about the upcoming waste collection dates for places, that are served by aha, the waste collection company for the region of Hannover. The values are retrieved from the online aha waste collection schedule available at: [aha Abfuhrkalender](https://www.aha-region.de/abholtermine/abfuhrkalender). + +## Supported Things + +- **collectionSchedule:** Represents the connection to the **aha Waste Collection Schedule** with four channels for the different waste types. + +## Discovery + +Discovery is not possible, due some form input values from the website above are required. + +## Thing Configuration + +For configuration of the **collectionSchedule** thing, you need the form inputs from the aha collections schedule web page. Follow the steps below to get the required configuration parameters from the form input values. + + +1. Open [aha Abfuhrkalender](https://www.aha-region.de/abholtermine/abfuhrkalender) in your favorite brower with developer-console. +2. Open the developer console and switch to network tab (for example press F12 in chrome / edge / firefox). +3. Fill in the form: Select your commune, Street and house number and hit "Suchen". +4. Select the first request to https://www.aha-region.de/abholtermine/abfuhrkalender (see first screenshot below) +5. Check the form data at the end of the request for the form values (see second screenshot below) +5. Fill in the values from the form input in thing configuration (see examples below) + +![Chrome Developer Console Top](doc/images/ChromeDevconsoleTop.png "Chrome Developer Console showing request URL") + +*Check if you've selected the correct request, that contains the form data* + +![Chrome Developer Console Bottom](doc/images/ChromeDevconsoleBottom.png "Chrome Developer Console showing form inputs") + +*Grab the values for the configuration parameters from the form data section at the end of the request* + +**collectionSchedule** parameters: + +| Property | Default | Required | Description | +|-|-|-|-| +| `commune` | | Yes | The selected commune, taken from the form field `gemeinde`. | +| `street` | | Yes | The selected street, taken from the form field `strasse`. This value must look like 67269@Rosmarinweg+/+Kirchhorst@Kirchhorst | +| `houseNumber` | | Yes | The selected house number, taken from the form field `hausnr`. | +| `houseNumberAddon` | | No | The selected house number addon, taken from the form field `hausnraddon`, may be empty. | +| `collectionPlace` | | Yes | Form value for the collection place, taken from the form field `ladeort`. This value must look like 67269-0010+ | + +## Channels + +The thing **aha Waste Collection Schedule** provides four channels for the upcoming day of waste collection for the different waste types. + + +| channel | type | description | +|----------|--------|------------------------------| +| generalWaste | DateTime | Next collection day for general waste | +| leightweightPackaging | DateTime | Next collection day for leightweight packaging | +| bioWaste | DateTime | Next collection day for bio waste | +| paper | DateTime | Next collection day for paper | + + +## Full Example + +wasteCollection.things + +``` +Thing ahawastecollection:collectionSchedule:wasteCollectionSchedule "aha Abfuhrkalender" [ commune="Isernhagen", street="67269@Rosmarinweg+/+Kirchhorst@Kirchhorst", houseNumber="10", houseNumberAddon="", collectionPlace="67269-0010+" ] +``` + +wasteCollection.items + +``` +DateTime collectionDay_generalWaste "Next general waste collection" {channel="ahawastecollection:collectionSchedule:wasteCollectionSchedule:generalWaste"} +DateTime collectionDay_leightweightPackaging "Next lightweight packaging collection" {channel="ahawastecollection:collectionSchedule:wasteCollectionSchedule:leightweightPackaging"} +DateTime collectionDay_bioWaste "Next bio waste collection" {channel="ahawastecollection:collectionSchedule:wasteCollectionSchedule:bioWaste"} +DateTime collectionDay_paper "Next paper collection" {channel="ahawastecollection:collectionSchedule:wasteCollectionSchedule:paper"} +``` + + +Example for rule that sends an notification with collected waste types on day before collection + +``` +triggers: + - id: "1" + configuration: + time: 18:00 + type: timer.TimeOfDayTrigger +conditions: [] +actions: + - inputs: {} + id: "2" + configuration: + type: application/javascript + script: >- + // Determine next day with time 00:00:00 + var today = items['LokaleZeit_DatumundZeit']; + + var tomorrow = today + .getZonedDateTime() + .plusDays(1) + .withHour(0) + .withMinute(0) + .withSecond(0) + .withNano(0); + + // Get next collection dates from items + var biomuellDate = items['collectionDay_bioWaste'].getZonedDateTime(); + var leichtverpackungDate = items['collectionDay_leightweightPackaging'].getZonedDateTime(); + var papierDate = items['collectionDay_paper'].getZonedDateTime(); + var restmuellDate = items['collectionDay_generalWaste'].getZonedDateTime(); + + + // Check which waste types are collected on the next day + var biomuellCollection = biomuellDate.equals(tomorrow); + var leichtverpackungCollection = leichtverpackungDate.equals(tomorrow); + var papierCollection = papierDate.equals(tomorrow); + var restmuellCollection = restmuellDate.equals(tomorrow); + + // Transfer booleans to waste type names + var toBeCollected = []; + + if (biomuellCollection) { + toBeCollected.push('bio waste'); + } + + if (leichtverpackungCollection) { + toBeCollected.push('leihtweight packaging'); + } + + if (papierCollection) { + toBeCollected.push('paper'); + } + + if (restmuellCollection) { + toBeCollected.push('general waste'); + } + + + // Send message (or something else) if at least one waste type is collected + if (toBeCollected.length > 0) { + var message = "Tomorrow the following waste will be collected:\n" + toBeCollected.join(', '); + events.sendCommand('SignalSmartHome_Eingabewert', message); + } + type: script.ScriptAction +``` diff --git a/bundles/org.openhab.binding.ahawastecollection/doc/images/ChromeDevconsoleBottom.png b/bundles/org.openhab.binding.ahawastecollection/doc/images/ChromeDevconsoleBottom.png new file mode 100644 index 0000000000000..dd9b87798246c Binary files /dev/null and b/bundles/org.openhab.binding.ahawastecollection/doc/images/ChromeDevconsoleBottom.png differ diff --git a/bundles/org.openhab.binding.ahawastecollection/doc/images/ChromeDevconsoleTop.png b/bundles/org.openhab.binding.ahawastecollection/doc/images/ChromeDevconsoleTop.png new file mode 100644 index 0000000000000..8e9b925963119 Binary files /dev/null and b/bundles/org.openhab.binding.ahawastecollection/doc/images/ChromeDevconsoleTop.png differ diff --git a/bundles/org.openhab.binding.ahawastecollection/pom.xml b/bundles/org.openhab.binding.ahawastecollection/pom.xml new file mode 100644 index 0000000000000..e23178b337245 --- /dev/null +++ b/bundles/org.openhab.binding.ahawastecollection/pom.xml @@ -0,0 +1,26 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 3.1.0-SNAPSHOT + + + org.openhab.binding.ahawastecollection + + openHAB Add-ons :: Bundles :: aha Waste Collection Binding + + + + org.jsoup + jsoup + 1.8.3 + provided + + + + diff --git a/bundles/org.openhab.binding.ahawastecollection/src/main/feature/feature.xml b/bundles/org.openhab.binding.ahawastecollection/src/main/feature/feature.xml new file mode 100644 index 0000000000000..b09d4bcd0b694 --- /dev/null +++ b/bundles/org.openhab.binding.ahawastecollection/src/main/feature/feature.xml @@ -0,0 +1,10 @@ + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features + + + openhab-runtime-base + mvn:org.jsoup/jsoup/1.8.3 + mvn:org.openhab.addons.bundles/org.openhab.binding.ahawastecollection/${project.version} + + diff --git a/bundles/org.openhab.binding.ahawastecollection/src/main/java/org/openhab/binding/ahawastecollection/internal/AhaCollectionSchedule.java b/bundles/org.openhab.binding.ahawastecollection/src/main/java/org/openhab/binding/ahawastecollection/internal/AhaCollectionSchedule.java new file mode 100644 index 0000000000000..6afb91fdaffc1 --- /dev/null +++ b/bundles/org.openhab.binding.ahawastecollection/src/main/java/org/openhab/binding/ahawastecollection/internal/AhaCollectionSchedule.java @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.ahawastecollection.internal; + +import java.io.IOException; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.ahawastecollection.internal.CollectionDate.WasteType; + +/** + * Schedule that returns the next collection dates from the aha website. + * + * @author Sönke Küper - Initial contribution + */ +@NonNullByDefault +public interface AhaCollectionSchedule { + + /** + * Returns the next collection dates per {@link WasteType}. + */ + public Map getCollectionDates() throws IOException; +} diff --git a/bundles/org.openhab.binding.ahawastecollection/src/main/java/org/openhab/binding/ahawastecollection/internal/AhaCollectionScheduleFactory.java b/bundles/org.openhab.binding.ahawastecollection/src/main/java/org/openhab/binding/ahawastecollection/internal/AhaCollectionScheduleFactory.java new file mode 100644 index 0000000000000..20b80d605c438 --- /dev/null +++ b/bundles/org.openhab.binding.ahawastecollection/src/main/java/org/openhab/binding/ahawastecollection/internal/AhaCollectionScheduleFactory.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.ahawastecollection.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Factory for creating an {@link AhaCollectionSchedule}. + * + * @author Sönke Küper - Initial contribution + */ +@NonNullByDefault +public interface AhaCollectionScheduleFactory { + + /** + * Creates an new {@link AhaCollectionSchedule} for the given location. + */ + public AhaCollectionSchedule create(final String commune, final String street, final String houseNumber, + final String houseNumberAddon, final String collectionPlace); +} diff --git a/bundles/org.openhab.binding.ahawastecollection/src/main/java/org/openhab/binding/ahawastecollection/internal/AhaCollectionScheduleImpl.java b/bundles/org.openhab.binding.ahawastecollection/src/main/java/org/openhab/binding/ahawastecollection/internal/AhaCollectionScheduleImpl.java new file mode 100644 index 0000000000000..d031ceb6b77db --- /dev/null +++ b/bundles/org.openhab.binding.ahawastecollection/src/main/java/org/openhab/binding/ahawastecollection/internal/AhaCollectionScheduleImpl.java @@ -0,0 +1,195 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.ahawastecollection.internal; + +import java.io.IOException; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.jsoup.Connection.Method; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.select.Elements; +import org.openhab.binding.ahawastecollection.internal.CollectionDate.WasteType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Schedule that returns the next collection dates from the aha website. + * + * @author Sönke Küper - Initial contribution + */ +@NonNullByDefault +final class AhaCollectionScheduleImpl implements AhaCollectionSchedule { + + private static final Pattern TIME_PATTERN = Pattern.compile("\\S\\S,\\s(\\d\\d.\\d\\d.\\d\\d\\d\\d)"); + private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("dd.MM.yyyy"); + private static final String WEBSITE_URL = "https://www.aha-region.de/abholtermine/abfuhrkalender/"; + + private final Logger logger = LoggerFactory.getLogger(AhaCollectionScheduleImpl.class); + private final String commune; + private final String street; + private final String houseNumber; + private final String houseNumberAddon; + private final String collectionPlace; + + /** + * Creates an new {@link AhaCollectionScheduleImpl} for the given location. + */ + public AhaCollectionScheduleImpl(final String commune, final String street, final String houseNumber, + final String houseNumberAddon, final String collectionPlace) { + this.commune = commune; + this.street = street; + this.houseNumber = houseNumber; + this.houseNumberAddon = houseNumberAddon; + this.collectionPlace = collectionPlace; + } + + @Override + public Map getCollectionDates() throws IOException { + final Document doc = Jsoup.connect(WEBSITE_URL) // + .method(Method.POST) // + .data("gemeinde", this.commune) // + .data("strasse", this.street) // + .data("hausnr", this.houseNumber) // + .data("hausnraddon", this.houseNumberAddon) // + .data("ladeort", this.collectionPlace) // + .data("anzeigen", "Suchen") // + .get(); + + final Elements table = doc.select("table"); + + if (table.size() == 0) { + logger.warn("No result table found."); + return Collections.emptyMap(); + } + + final Iterator rowIt = table.get(0).getElementsByTag("tr").iterator(); + final Map result = new HashMap<>(); + + while (rowIt.hasNext()) { + final Element currentRow = rowIt.next(); + if (!currentRow.tagName().equals("tr")) { + continue; + } + // Skip header, empty and download button rows. + if (isHeader(currentRow) || isDelimiterOrDownloadRow(currentRow)) { + continue; + } + + // If no following row is present, no collection dates can be parsed + if (!rowIt.hasNext()) { + logger.warn("No row with collection dates found."); + break; + } + final Element collectionDatesRow = rowIt.next(); + + final CollectionDate date = this.parseRows(currentRow, collectionDatesRow); + if (date != null) { + result.put(date.getType(), date); + } + } + return result; + } + + /** + * Parses the row with the waste type and the following row with the collection dates. + * + * @param wasteTypeRow Row that contains the waste type information + * @param collectionDatesRow Row that contains the collection date informations. + * @return The parsed {@link CollectionDate} or null if information could not be parsed. + */ + @Nullable + private CollectionDate parseRows(Element wasteTypeRow, Element collectionDatesRow) { + // Try to extract the waste Type from the first row + final Elements wasteTypeElement = wasteTypeRow.select("td").select("strong"); + if (wasteTypeElement.size() != 1) { + this.logger.warn("Could not parse waste type row: {}", wasteTypeRow.toString()); + return null; + } + final WasteType wasteType = parseWasteType(wasteTypeElement.get(0)); + + // Try to extract the collection dates from the second row + final Elements collectionDatesColumns = collectionDatesRow.select("td"); + if (collectionDatesColumns.size() != 3) { + this.logger.warn("collection dates row could not be parsed."); + return null; + } + + final Element collectionDatesColumn = collectionDatesColumns.get(1); + final List collectionDates = parseTimes(collectionDatesColumn); + + if (!collectionDates.isEmpty()) { + return new CollectionDate(wasteType, collectionDates); + } else { + return null; + } + } + + /** + * Returns true if the row is an (empty) delimiter row or if its an row that contains the download + * buttons for ical. + */ + private boolean isDelimiterOrDownloadRow(Element currentRow) { + final Elements columns = currentRow.select("td"); + return columns.size() == 1 && columns.get(0).text().isBlank() || !columns.select("form").isEmpty(); + } + + private boolean isHeader(Element currentRow) { + return !currentRow.select("th").isEmpty(); + } + + /** + * Parses the waste types from the given {@link Element table cell}. + */ + private static WasteType parseWasteType(final Element element) { + String value = element.text().trim(); + final int firstSpace = value.indexOf(" "); + if (firstSpace > 0) { + value = value.substring(0, firstSpace); + } + return WasteType.parseValue(value); + } + + /** + * Parses the {@link CollectionDate} from the given {@link Element table cell}. + */ + private List parseTimes(final Element element) { + final List result = new ArrayList<>(); + final String value = element.text(); + final Matcher matcher = TIME_PATTERN.matcher(value); + while (matcher.find()) { + final String dateValue = matcher.group(1); + try { + synchronized (DATE_FORMAT) { + result.add(DATE_FORMAT.parse(dateValue)); + } + } catch (final ParseException e) { + this.logger.warn("Could not parse date: {}", dateValue); + } + } + return result; + } +} diff --git a/bundles/org.openhab.binding.ahawastecollection/src/main/java/org/openhab/binding/ahawastecollection/internal/AhaWasteCollectionBindingConstants.java b/bundles/org.openhab.binding.ahawastecollection/src/main/java/org/openhab/binding/ahawastecollection/internal/AhaWasteCollectionBindingConstants.java new file mode 100644 index 0000000000000..ed12c3ef175b8 --- /dev/null +++ b/bundles/org.openhab.binding.ahawastecollection/src/main/java/org/openhab/binding/ahawastecollection/internal/AhaWasteCollectionBindingConstants.java @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.ahawastecollection.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.thing.ThingTypeUID; + +/** + * The {@link AhaWasteCollectionBindingConstants} class defines common constants, which are + * used across the whole binding. + * + * @author Sönke Küper - Initial contribution + */ +@NonNullByDefault +public class AhaWasteCollectionBindingConstants { + + private static final String BINDING_ID = "ahawastecollection"; + + // List of all Thing Type UIDs + public static final ThingTypeUID THING_TYPE_SCHEDULE = new ThingTypeUID(BINDING_ID, "collectionSchedule"); + + // List of all Channel ids + public static final String GENERAL_WASTE = "generalWaste"; + public static final String LEIGHTWEIGHT_PACKAGING = "leightweightPackaging"; + public static final String BIOWASTE = "bioWaste"; + public static final String PAPER = "paper"; +} diff --git a/bundles/org.openhab.binding.ahawastecollection/src/main/java/org/openhab/binding/ahawastecollection/internal/AhaWasteCollectionConfiguration.java b/bundles/org.openhab.binding.ahawastecollection/src/main/java/org/openhab/binding/ahawastecollection/internal/AhaWasteCollectionConfiguration.java new file mode 100644 index 0000000000000..d496ee907511c --- /dev/null +++ b/bundles/org.openhab.binding.ahawastecollection/src/main/java/org/openhab/binding/ahawastecollection/internal/AhaWasteCollectionConfiguration.java @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.ahawastecollection.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link AhaWasteCollectionConfiguration} class contains fields mapping thing configuration parameters. + * + * @author Sönke Küper - Initial contribution + */ +@NonNullByDefault +public class AhaWasteCollectionConfiguration { + + /** + * Commune. + */ + public String commune = ""; + + /** + * Street. + */ + public String street = ""; + + /** + * House number. + */ + public String houseNumber = ""; + + /** + * House number addon. + */ + public String houseNumberAddon = ""; + + /** + * Collection place. + */ + public String collectionPlace = ""; +} diff --git a/bundles/org.openhab.binding.ahawastecollection/src/main/java/org/openhab/binding/ahawastecollection/internal/AhaWasteCollectionHandler.java b/bundles/org.openhab.binding.ahawastecollection/src/main/java/org/openhab/binding/ahawastecollection/internal/AhaWasteCollectionHandler.java new file mode 100644 index 0000000000000..26bffeb6c002e --- /dev/null +++ b/bundles/org.openhab.binding.ahawastecollection/src/main/java/org/openhab/binding/ahawastecollection/internal/AhaWasteCollectionHandler.java @@ -0,0 +1,208 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.ahawastecollection.internal; + +import java.io.IOException; +import java.time.Duration; +import java.time.ZonedDateTime; +import java.util.Collections; +import java.util.Date; +import java.util.Map; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.ahawastecollection.internal.CollectionDate.WasteType; +import org.openhab.core.cache.ExpiringCache; +import org.openhab.core.i18n.TimeZoneProvider; +import org.openhab.core.library.types.DateTimeType; +import org.openhab.core.scheduler.CronScheduler; +import org.openhab.core.scheduler.ScheduledCompletableFuture; +import org.openhab.core.thing.Channel; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.binding.BaseThingHandler; +import org.openhab.core.types.Command; +import org.openhab.core.types.RefreshType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link AhaWasteCollectionHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Sönke Küper - Initial contribution + */ +@NonNullByDefault +public class AhaWasteCollectionHandler extends BaseThingHandler { + + private static final String DAILY_MIDNIGHT = "30 0 0 * * ? *"; + + /** Scheduler to schedule jobs */ + private final CronScheduler cronScheduler; + private final Lock monitor = new ReentrantLock(); + private final ExpiringCache> cache; + + private final TimeZoneProvider timeZoneProvider; + private final Logger logger = LoggerFactory.getLogger(AhaWasteCollectionHandler.class); + + private @Nullable AhaWasteCollectionConfiguration config; + private @Nullable AhaCollectionSchedule collectionSchedule; + + private @Nullable ScheduledCompletableFuture dailyJob; + + private final AhaCollectionScheduleFactory scheduleFactory; + + public AhaWasteCollectionHandler(final Thing thing, final CronScheduler scheduler, + final TimeZoneProvider timeZoneProvider, final AhaCollectionScheduleFactory scheduleFactory) { + super(thing); + this.cronScheduler = scheduler; + this.timeZoneProvider = timeZoneProvider; + this.scheduleFactory = scheduleFactory; + this.cache = new ExpiringCache<>(Duration.ofMinutes(5), this::loadCollectionDates); + } + + private Map loadCollectionDates() { + try { + final Map collectionDates = this.collectionSchedule.getCollectionDates(); + this.updateStatus(ThingStatus.ONLINE); + return collectionDates; + } catch (final IOException e) { + this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); + return Collections.emptyMap(); + } + } + + @Override + public void handleCommand(final ChannelUID channelUID, final Command command) { + if (command instanceof RefreshType) { + this.scheduler.execute(this::updateCollectionDates); + } else { + this.logger.warn("The AHA Abfuhrkalender is a read-only binding and can not handle commands"); + } + } + + @Override + public void initialize() { + this.config = this.getConfigAs(AhaWasteCollectionConfiguration.class); + + final String commune = this.config.commune; + final String street = this.config.street; + final String houseNumber = this.config.houseNumber; + final String houseNumberAddon = this.config.houseNumberAddon; + final String collectionPlace = this.config.collectionPlace; + + if (commune.isBlank() || street.isBlank() || houseNumber.isBlank() || collectionPlace.isBlank()) { + this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "Parameters are mandatory and must be configured"); + return; + } + + this.collectionSchedule = this.scheduleFactory.create(commune, street, houseNumber, houseNumberAddon, + collectionPlace); + + this.updateStatus(ThingStatus.UNKNOWN); + + this.scheduler.execute(() -> { + final boolean online = this.updateCollectionDates(); + if (online) { + this.restartJob(); + } + }); + } + + /** + * Schedules an job that updates the collection dates at midnight. + */ + private void restartJob() { + this.logger.debug("Restarting jobs for thing {}", this.getThing().getUID()); + this.monitor.lock(); + try { + this.stopJob(); + if (this.getThing().getStatus() == ThingStatus.ONLINE) { + this.dailyJob = this.cronScheduler.schedule(this::updateCollectionDates, DAILY_MIDNIGHT); + this.logger.debug("Scheduled {} at midnight", this.dailyJob); + // Execute daily startup job immediately + this.updateCollectionDates(); + } + } finally { + this.monitor.unlock(); + } + } + + /** + * Stops all jobs for this thing. + */ + private void stopJob() { + this.monitor.lock(); + try { + final ScheduledCompletableFuture job = this.dailyJob; + if (job != null) { + job.cancel(true); + } + this.dailyJob = null; + } finally { + this.monitor.unlock(); + } + } + + private boolean updateCollectionDates() { + final Map collectionDates = this.cache.getValue(); + if (collectionDates == null || collectionDates.isEmpty()) { + return false; + } + + this.logger.debug("Retrieved {} collection entries.", collectionDates.size()); + this.updateChannels(collectionDates); + return true; + } + + /** + * Refreshes the channel values with the given {@link CollectionDate}s. + */ + private void updateChannels(final Map collectionDates) { + for (final Channel channel : this.getThing().getChannels()) { + final WasteType wasteType = getWasteTypeByChannel(channel.getUID().getId()); + + final CollectionDate collectionDate = collectionDates.get(wasteType); + if (collectionDate == null) { + this.logger.warn("No collection dates found for waste type: {}", wasteType); + continue; + } + + final Date nextCollectionDate = collectionDate.getDates().get(0); + + final ZonedDateTime zonedDateTime = ZonedDateTime.ofInstant(nextCollectionDate.toInstant(), + this.timeZoneProvider.getTimeZone()); + this.updateState(channel.getUID(), new DateTimeType(zonedDateTime)); + } + } + + private static WasteType getWasteTypeByChannel(final String channelId) { + switch (channelId) { + case AhaWasteCollectionBindingConstants.BIOWASTE: + return WasteType.BIO_WASTE; + case AhaWasteCollectionBindingConstants.LEIGHTWEIGHT_PACKAGING: + return WasteType.LIGHT_PACKAGES; + case AhaWasteCollectionBindingConstants.PAPER: + return WasteType.PAPER; + case AhaWasteCollectionBindingConstants.GENERAL_WASTE: + return WasteType.GENERAL_WASTE; + default: + throw new IllegalArgumentException("Unknown channel type: " + channelId); + } + } +} diff --git a/bundles/org.openhab.binding.ahawastecollection/src/main/java/org/openhab/binding/ahawastecollection/internal/AhaWasteCollectionHandlerFactory.java b/bundles/org.openhab.binding.ahawastecollection/src/main/java/org/openhab/binding/ahawastecollection/internal/AhaWasteCollectionHandlerFactory.java new file mode 100644 index 0000000000000..f11dcd5a006e1 --- /dev/null +++ b/bundles/org.openhab.binding.ahawastecollection/src/main/java/org/openhab/binding/ahawastecollection/internal/AhaWasteCollectionHandlerFactory.java @@ -0,0 +1,75 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.ahawastecollection.internal; + +import static org.openhab.binding.ahawastecollection.internal.AhaWasteCollectionBindingConstants.THING_TYPE_SCHEDULE; + +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.i18n.TimeZoneProvider; +import org.openhab.core.scheduler.CronScheduler; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.binding.BaseThingHandlerFactory; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerFactory; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +/** + * The {@link AhaWasteCollectionHandlerFactory} is responsible for creating things and thing handlers. + * + * @author Sönke Küper - Initial contribution + */ +@NonNullByDefault +@Component(configurationPid = "binding.ahawastecollection", service = ThingHandlerFactory.class) +public class AhaWasteCollectionHandlerFactory extends BaseThingHandlerFactory { + + private static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_SCHEDULE); + private final CronScheduler scheduler; + private final TimeZoneProvider timeZoneProvider; + + @Override + public boolean supportsThingType(final ThingTypeUID thingTypeUID) { + return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID); + } + + @Activate + public AhaWasteCollectionHandlerFactory(final @Reference CronScheduler scheduler, + final @Reference TimeZoneProvider timeZoneProvider) { + this.scheduler = scheduler; + this.timeZoneProvider = timeZoneProvider; + } + + @Override + protected @Nullable ThingHandler createHandler(final Thing thing) { + final ThingTypeUID thingTypeUID = thing.getThingTypeUID(); + + if (THING_TYPE_SCHEDULE.equals(thingTypeUID)) { + final AhaCollectionScheduleFactory factory = new AhaCollectionScheduleFactory() { + + @Override + public AhaCollectionScheduleImpl create(final String commune, final String street, + final String houseNumber, final String houseNumberAddon, final String collectionPlace) { + return new AhaCollectionScheduleImpl(commune, street, houseNumber, houseNumberAddon, + collectionPlace); + } + }; + return new AhaWasteCollectionHandler(thing, this.scheduler, this.timeZoneProvider, factory); + } + return null; + } +} diff --git a/bundles/org.openhab.binding.ahawastecollection/src/main/java/org/openhab/binding/ahawastecollection/internal/CollectionDate.java b/bundles/org.openhab.binding.ahawastecollection/src/main/java/org/openhab/binding/ahawastecollection/internal/CollectionDate.java new file mode 100644 index 0000000000000..677b90538e1dc --- /dev/null +++ b/bundles/org.openhab.binding.ahawastecollection/src/main/java/org/openhab/binding/ahawastecollection/internal/CollectionDate.java @@ -0,0 +1,99 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.ahawastecollection.internal; + +import java.util.Collections; +import java.util.Date; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Contains the next collection dates for an given waste type. + * + * @author Sönke Küper - Initial contribution + */ +@NonNullByDefault +final class CollectionDate { + + /** + * Type of waste that is collected. + */ + public enum WasteType { + /** + * General waste. + */ + GENERAL_WASTE, + /** + * Bio waste. + */ + BIO_WASTE, + /** + * Paper. + */ + PAPER, + /** + * Light packaging. + */ + LIGHT_PACKAGES; + + /** + * Parses the {@link WasteType} from the given Value from the Web-page. + */ + public static WasteType parseValue(String value) { + switch (value) { + case "Restabfall": + return GENERAL_WASTE; + case "Bioabfall": + return BIO_WASTE; + case "Papier": + return PAPER; + case "Leichtverpackungen": + return LIGHT_PACKAGES; + default: + throw new IllegalArgumentException("Unknown waste type: " + value); + } + } + } + + private final WasteType type; + private final List dates; + + /** + * Creates an new {@link CollectionDate}. + */ + public CollectionDate(final WasteType type, final List dates) { + this.type = type; + this.dates = dates; + Collections.sort(this.dates); + } + + /** + * Returns the (non empty list) of next collection dates, for the given {@link WasteType}, ordered ascending. + */ + public List getDates() { + return this.dates; + } + + /** + * Returns the {@link WasteType} that is collected at the given times. + */ + public WasteType getType() { + return this.type; + } + + @Override + public String toString() { + return String.format("waste type: %s, collection dates: %s", this.type, this.dates); + } +} diff --git a/bundles/org.openhab.binding.ahawastecollection/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.ahawastecollection/src/main/resources/OH-INF/binding/binding.xml new file mode 100644 index 0000000000000..752f20958e3a6 --- /dev/null +++ b/bundles/org.openhab.binding.ahawastecollection/src/main/resources/OH-INF/binding/binding.xml @@ -0,0 +1,13 @@ + + + + aha WasteCollection Binding + This binding provides information about the upcoming waste collection dates for places, that are served by + aha, + the waste collection company of the region Hannover. The values are retrieved from the online + aha waste collection + schedule available at: https://www.aha-region.de/abholtermine/abfuhrkalender. + + diff --git a/bundles/org.openhab.binding.ahawastecollection/src/main/resources/OH-INF/i18n/ahawastecollection_de.properties b/bundles/org.openhab.binding.ahawastecollection/src/main/resources/OH-INF/i18n/ahawastecollection_de.properties new file mode 100644 index 0000000000000..47a177a70fc75 --- /dev/null +++ b/bundles/org.openhab.binding.ahawastecollection/src/main/resources/OH-INF/i18n/ahawastecollection_de.properties @@ -0,0 +1,36 @@ +binding.ahawastecollection.name = aha M�llabf�hr +binding.ahawastecollection.description = Stellt die Abfuhrtermine des aha Zweckverband Abfallwirtschaft Region Hannover zur Verf�gung. + +# thing types +thing-type.ahawastecollection.collectionSchedule.label = aha Abfuhrkalender +thing-type.ahawastecollection.collectionSchedule.description = aha Abfuhrkalender, der die n�chsten Abfuhrtermine von https://www.aha-region.de/abholtermine/abfuhrkalender abfragt. + +# thing type config description +thing-type.config.ahawastecollection.collectionSchedule.commune.label = Gemeinde +thing-type.config.ahawastecollection.collectionSchedule.commune.description = Gemeinde von der Formular-Eingabe + +thing-type.config.ahawastecollection.collectionSchedule.street.label = Stra�e +thing-type.config.ahawastecollection.collectionSchedule.street.description = Wert f�r die selektierte Stra�e: z.B. "02095@Oesterleystr.+/+S�dstadt@S�dstadt" + +thing-type.config.ahawastecollection.collectionSchedule.houseNumber.label = Hausnummer +thing-type.config.ahawastecollection.collectionSchedule.houseNumber.description = Hausnummer von der Formular-Eingabe + +thing-type.config.ahawastecollection.collectionSchedule.houseNumberAddon.label = Adresszusatz +thing-type.config.ahawastecollection.collectionSchedule.houseNumberAddon.description = Adresszusatzvon der Formular-Eingabe + +thing-type.config.ahawastecollection.collectionSchedule.collectionPlace.label = Abholplatz +thing-type.config.ahawastecollection.collectionSchedule.collectionPlace.description = Abholplatz von der Formular-Eingabe, z.B. "02095-0010+" + +# channel types +channel-type.ahawastecollection.collectionDateGeneralWaste.label = Abholtag Restabfall +channel-type.ahawastecollection.collectionDateGeneralWaste.description = N�chster Abholtag f�r Restabfall + +channel-type.ahawastecollection.collectionDateLeightweightPackaging.label = Abholtag Leichtverpackungen +channel-type.ahawastecollection.collectionDateLeightweightPackaging.description = N�chster Abholtag f�r Leichtverpackungen + +channel-type.ahawastecollection.collectionDateBioWaste.label = Abholtag Biom�ll +channel-type.ahawastecollection.collectionDateBioWaste.description = N�chster Abholtag f�r Biom�ll + +channel-type.ahawastecollection.collectionDatePaper.label = Abholtag Papier +channel-type.ahawastecollection.collectionDatePaper.description = N�chster Abholtag f�r Papier + diff --git a/bundles/org.openhab.binding.ahawastecollection/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.ahawastecollection/src/main/resources/OH-INF/thing/thing-types.xml new file mode 100644 index 0000000000000..6fe06a53f5259 --- /dev/null +++ b/bundles/org.openhab.binding.ahawastecollection/src/main/resources/OH-INF/thing/thing-types.xml @@ -0,0 +1,71 @@ + + + + + + aha Waste Collection Schedule from http://www.aha-region.de/abholtermine/abfuhrkalender + + + + + + + + + + + + The selected commune, taken from the form field 'gemeinde'. + + + + The selected street, taken from the form field 'strasse'. This value must look like + 67269@Rosmarinweg+/+Kirchhorst@Kirchhorst + + + + The selected house number, taken from the form field 'hausnr'. + + + + The selected house number addon, taken from the form field 'hausnraddon'. + + + + Form value for the collection place, taken from the form field `ladeort`. This value must look like + 67269-0010+ + + + + + + + DateTime + + Next collection day for general waste + + + + DateTime + + Next collection day for leightweight packaging + + + + DateTime + + Next collection day for bio waste + + + + DateTime + + Next collection day for paper + + + + + diff --git a/bundles/org.openhab.binding.ahawastecollection/src/test/java/org/openhab/binding/ahawastecollection/internal/AhaCollectionScheduleStub.java b/bundles/org.openhab.binding.ahawastecollection/src/test/java/org/openhab/binding/ahawastecollection/internal/AhaCollectionScheduleStub.java new file mode 100644 index 0000000000000..a88b33d8f3737 --- /dev/null +++ b/bundles/org.openhab.binding.ahawastecollection/src/test/java/org/openhab/binding/ahawastecollection/internal/AhaCollectionScheduleStub.java @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.ahawastecollection.internal; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.ahawastecollection.internal.CollectionDate.WasteType; + +/** + * @author Sönke Küper - Initial contribution + */ +@NonNullByDefault +public final class AhaCollectionScheduleStub implements AhaCollectionSchedule { + + public static final Date GENERAL_WASTE_DATE = new GregorianCalendar(2021, 2, 19).getTime(); + public static final Date LEIGHTWEIGHT_PACKAGING_DATE = new GregorianCalendar(2021, 2, 20).getTime(); + public static final Date BIO_WASTE_DATE = new GregorianCalendar(2021, 2, 21).getTime(); + public static final Date PAPER_DATE = new GregorianCalendar(2021, 2, 22).getTime(); + + @Override + public Map getCollectionDates() throws IOException { + final Map result = new LinkedHashMap<>(4); + result.put(WasteType.GENERAL_WASTE, + new CollectionDate(WasteType.GENERAL_WASTE, Arrays.asList(GENERAL_WASTE_DATE))); + result.put(WasteType.LIGHT_PACKAGES, + new CollectionDate(WasteType.GENERAL_WASTE, Arrays.asList(LEIGHTWEIGHT_PACKAGING_DATE))); + result.put(WasteType.BIO_WASTE, new CollectionDate(WasteType.GENERAL_WASTE, Arrays.asList(BIO_WASTE_DATE))); + result.put(WasteType.PAPER, new CollectionDate(WasteType.GENERAL_WASTE, Arrays.asList(PAPER_DATE))); + return result; + } +} diff --git a/bundles/org.openhab.binding.ahawastecollection/src/test/java/org/openhab/binding/ahawastecollection/internal/AhaCollectionScheduleStubFactory.java b/bundles/org.openhab.binding.ahawastecollection/src/test/java/org/openhab/binding/ahawastecollection/internal/AhaCollectionScheduleStubFactory.java new file mode 100644 index 0000000000000..2411736b008e2 --- /dev/null +++ b/bundles/org.openhab.binding.ahawastecollection/src/test/java/org/openhab/binding/ahawastecollection/internal/AhaCollectionScheduleStubFactory.java @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.ahawastecollection.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * @author Sönke Küper - Initial contribution + */ +@NonNullByDefault +public final class AhaCollectionScheduleStubFactory implements AhaCollectionScheduleFactory { + + @Override + public AhaCollectionSchedule create(final String commune, final String street, final String houseNumber, + final String houseNumberAddon, final String collectionPlace) { + return new AhaCollectionScheduleStub(); + } +} diff --git a/bundles/org.openhab.binding.ahawastecollection/src/test/java/org/openhab/binding/ahawastecollection/internal/AhaCollectionScheduleTest.java b/bundles/org.openhab.binding.ahawastecollection/src/test/java/org/openhab/binding/ahawastecollection/internal/AhaCollectionScheduleTest.java new file mode 100644 index 0000000000000..6df83c22f1bc6 --- /dev/null +++ b/bundles/org.openhab.binding.ahawastecollection/src/test/java/org/openhab/binding/ahawastecollection/internal/AhaCollectionScheduleTest.java @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.ahawastecollection.internal; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.ahawastecollection.internal.CollectionDate.WasteType; + +/** + * @author Sönke Küper - Initial contribution + */ +@NonNullByDefault +public class AhaCollectionScheduleTest { + + @Test + public void testGetValuesForHannoverCity() throws Exception { + final AhaCollectionScheduleImpl schedule = new AhaCollectionScheduleImpl("Hannover", + "02095@Oesterleystr.+/+Südstadt@Südstadt", "10", "", "02095-0010+"); + final Map dates = schedule.getCollectionDates(); + // No bio waste is collected here + assertEquals(3, dates.size()); + } + + @Test + public void testGetValuesForRegion() throws Exception { + final AhaCollectionScheduleImpl schedule = new AhaCollectionScheduleImpl("Lehrte", + "70185@Haselnussweg+/+Hämelerwald@Hämelerwald", "12", "", "70185-0012+"); + final Map dates = schedule.getCollectionDates(); + // All four waste types are collected here. + assertEquals(4, dates.size()); + } +} diff --git a/bundles/org.openhab.binding.ahawastecollection/src/test/java/org/openhab/binding/ahawastecollection/internal/AhaWasteCollectionHandlerTest.java b/bundles/org.openhab.binding.ahawastecollection/src/test/java/org/openhab/binding/ahawastecollection/internal/AhaWasteCollectionHandlerTest.java new file mode 100644 index 0000000000000..cc967353a76e3 --- /dev/null +++ b/bundles/org.openhab.binding.ahawastecollection/src/test/java/org/openhab/binding/ahawastecollection/internal/AhaWasteCollectionHandlerTest.java @@ -0,0 +1,167 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.ahawastecollection.internal; + +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.Arrays; +import java.util.Date; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.openhab.core.config.core.Configuration; +import org.openhab.core.library.types.DateTimeType; +import org.openhab.core.scheduler.CronJob; +import org.openhab.core.scheduler.CronScheduler; +import org.openhab.core.scheduler.ScheduledCompletableFuture; +import org.openhab.core.scheduler.SchedulerRunnable; +import org.openhab.core.thing.Channel; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingUID; +import org.openhab.core.thing.binding.ThingHandlerCallback; +import org.openhab.core.types.State; + +/** + * @author Sönke Küper - Initial contribution + */ +@NonNullByDefault +public class AhaWasteCollectionHandlerTest { + + private static final Configuration CONFIG = createConfig(); + + private static Configuration createConfig() { + final Configuration config = new Configuration(); + config.put("commune", "Hannover"); + config.put("collectionPlace", "02095-0010+"); + config.put("houseNumber", "10"); + config.put("houseNumberAddon", ""); + config.put("street", "02095@Oesterleystr.+/+Südstadt@Südstadt"); + return config; + } + + /** + * Exception indicating that the execution of an script within the stub-Scheduler failed. + */ + private static class SchedulerRuntimeException extends RuntimeException { + + private static final long serialVersionUID = -1262671065082256315L; + + public SchedulerRuntimeException(@Nullable final Throwable cause) { + super(cause); + } + } + + /** + * Creates an {@link CronScheduler} that executes all commands synchronous. + */ + @SuppressWarnings("unchecked") + private static CronScheduler createStubScheduler() { + return new CronScheduler() { + + @Override + public final ScheduledCompletableFuture schedule(final CronJob cronJob, + final Map config, final String cronExpression) { + try { + cronJob.run(config); + } catch (final Exception e) { + throw new SchedulerRuntimeException(e); + } + return Mockito.mock(ScheduledCompletableFuture.class); + } + + @Override + public final ScheduledCompletableFuture schedule(final SchedulerRunnable runnable, + final String cronExpression) { + try { + runnable.run(); + } catch (final Exception e) { + throw new SchedulerRuntimeException(e); + } + return Mockito.mock(ScheduledCompletableFuture.class); + } + }; + } + + private static Thing mockThing(final Configuration config) { + final Thing thing = mock(Thing.class); + when(thing.getUID()) + .thenReturn(new ThingUID(AhaWasteCollectionBindingConstants.THING_TYPE_SCHEDULE, "collectionCalendar")); + when(thing.getConfiguration()).thenReturn(config); + + final Channel channelBioWaste = mockChannel(thing.getUID(), AhaWasteCollectionBindingConstants.BIOWASTE); + final Channel channelGeneralWaste = mockChannel(thing.getUID(), + AhaWasteCollectionBindingConstants.GENERAL_WASTE); + final Channel channelPaper = mockChannel(thing.getUID(), AhaWasteCollectionBindingConstants.PAPER); + final Channel channelLeightweightPackaging = mockChannel(thing.getUID(), + AhaWasteCollectionBindingConstants.LEIGHTWEIGHT_PACKAGING); + + when(thing.getChannels()).thenReturn( + Arrays.asList(channelBioWaste, channelGeneralWaste, channelLeightweightPackaging, channelPaper)); + return thing; + } + + private static Channel mockChannel(final ThingUID thingId, final String channelId) { + final Channel channel = Mockito.mock(Channel.class); + when(channel.getUID()).thenReturn(new ChannelUID(thingId, channelId)); + return channel; + } + + private static AhaWasteCollectionHandler createAndInitHandler(final ThingHandlerCallback callback, + final Thing thing) { + final AhaWasteCollectionHandler handler = new AhaWasteCollectionHandler(thing, createStubScheduler(), + ZoneId::systemDefault, new AhaCollectionScheduleStubFactory()); + handler.setCallback(callback); + handler.initialize(); + return handler; + } + + private static State getDateTime(final Date day) { + final ZonedDateTime zonedDateTime = ZonedDateTime.ofInstant(day.toInstant(), ZoneId.systemDefault()); + return new DateTimeType(zonedDateTime); + } + + @Test + public void testUpdateChannels() { + final Thing thing = mockThing(CONFIG); + final ThingHandlerCallback callback = mock(ThingHandlerCallback.class); + final AhaWasteCollectionHandler handler = createAndInitHandler(callback, thing); + + try { + verify(callback).statusUpdated(eq(thing), argThat(arg -> arg.getStatus().equals(ThingStatus.UNKNOWN))); + verify(callback, timeout(1000)).statusUpdated(eq(thing), + argThat(arg -> arg.getStatus().equals(ThingStatus.ONLINE))); + verify(callback, timeout(1000)).stateUpdated( + new ChannelUID(thing.getUID(), AhaWasteCollectionBindingConstants.BIOWASTE), + getDateTime(AhaCollectionScheduleStub.BIO_WASTE_DATE)); + verify(callback, timeout(1000)).stateUpdated( + new ChannelUID(thing.getUID(), AhaWasteCollectionBindingConstants.GENERAL_WASTE), + getDateTime(AhaCollectionScheduleStub.GENERAL_WASTE_DATE)); + verify(callback, timeout(1000)).stateUpdated( + new ChannelUID(thing.getUID(), AhaWasteCollectionBindingConstants.LEIGHTWEIGHT_PACKAGING), + getDateTime(AhaCollectionScheduleStub.LEIGHTWEIGHT_PACKAGING_DATE)); + verify(callback, timeout(1000)).stateUpdated( + new ChannelUID(thing.getUID(), AhaWasteCollectionBindingConstants.PAPER), + getDateTime(AhaCollectionScheduleStub.PAPER_DATE)); + } finally { + handler.dispose(); + } + } +} diff --git a/bundles/pom.xml b/bundles/pom.xml index f0de300d5250a..f44d797764d6e 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -41,6 +41,7 @@ org.openhab.transform.xslt org.openhab.binding.adorne + org.openhab.binding.ahawastecollection org.openhab.binding.airq org.openhab.binding.airquality org.openhab.binding.airvisualnode