[ahawastecollection] Initial Contribution (#10387)

Signed-off-by: Sönke Küper <soenkekueper@gmx.de>
This commit is contained in:
Sönke Küper 2021-04-30 15:59:05 +02:00 committed by GitHub
parent 7fb7c65306
commit 710aa760d0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 1337 additions and 0 deletions

View File

@ -9,6 +9,7 @@
/bundles/org.openhab.automation.jythonscripting/ @openhab/add-ons-maintainers /bundles/org.openhab.automation.jythonscripting/ @openhab/add-ons-maintainers
/bundles/org.openhab.automation.pidcontroller/ @fwolter /bundles/org.openhab.automation.pidcontroller/ @fwolter
/bundles/org.openhab.binding.adorne/ @theiding /bundles/org.openhab.binding.adorne/ @theiding
/bundles/org.openhab.binding.ahawastecollection/ @soenkekueper
/bundles/org.openhab.binding.airq/ @aurelio1 /bundles/org.openhab.binding.airq/ @aurelio1
/bundles/org.openhab.binding.airquality/ @kubawolanin /bundles/org.openhab.binding.airquality/ @kubawolanin
/bundles/org.openhab.binding.airvisualnode/ @3cky /bundles/org.openhab.binding.airvisualnode/ @3cky

View File

@ -36,6 +36,11 @@
<artifactId>org.openhab.binding.adorne</artifactId> <artifactId>org.openhab.binding.adorne</artifactId>
<version>${project.version}</version> <version>${project.version}</version>
</dependency> </dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.ahawastecollection</artifactId>
<version>${project.version}</version>
</dependency>
<dependency> <dependency>
<groupId>org.openhab.addons.bundles</groupId> <groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.airq</artifactId> <artifactId>org.openhab.binding.airq</artifactId>

View File

@ -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

View File

@ -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
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
<version>3.1.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.binding.ahawastecollection</artifactId>
<name>openHAB Add-ons :: Bundles :: aha Waste Collection Binding</name>
<dependencies>
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.8.3</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.ahawastecollection-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
<feature name="openhab-binding-ahawastecollection" description="aha Waste Collection Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<bundle dependency="true">mvn:org.jsoup/jsoup/1.8.3</bundle>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.ahawastecollection/${project.version}</bundle>
</feature>
</features>

View File

@ -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<WasteType, CollectionDate> getCollectionDates() throws IOException;
}

View File

@ -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);
}

View File

@ -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<WasteType, CollectionDate> 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<Element> rowIt = table.get(0).getElementsByTag("tr").iterator();
final Map<WasteType, CollectionDate> 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 <code>null</code> 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<Date> collectionDates = parseTimes(collectionDatesColumn);
if (!collectionDates.isEmpty()) {
return new CollectionDate(wasteType, collectionDates);
} else {
return null;
}
}
/**
* Returns <code>true</code> 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<Date> parseTimes(final Element element) {
final List<Date> 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;
}
}

View File

@ -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";
}

View File

@ -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 = "";
}

View File

@ -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<Map<WasteType, CollectionDate>> 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<WasteType, CollectionDate> loadCollectionDates() {
try {
final Map<WasteType, CollectionDate> 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<WasteType, CollectionDate> 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<WasteType, CollectionDate> 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);
}
}
}

View File

@ -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<ThingTypeUID> 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;
}
}

View File

@ -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<Date> dates;
/**
* Creates an new {@link CollectionDate}.
*/
public CollectionDate(final WasteType type, final List<Date> 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<Date> 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);
}
}

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="ahawastecollection" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
<name>aha WasteCollection Binding</name>
<description>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.</description>
</binding:binding>

View File

@ -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

View File

@ -0,0 +1,71 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="ahawastecollection"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="collectionSchedule">
<label>aha Waste Collection Schedule</label>
<description>aha Waste Collection Schedule from http://www.aha-region.de/abholtermine/abfuhrkalender</description>
<channels>
<channel id="generalWaste" typeId="collectionDateGeneralWaste"/>
<channel id="leightweightPackaging" typeId="collectionDateLeightweightPackaging"/>
<channel id="bioWaste" typeId="collectionDateBioWaste"/>
<channel id="paper" typeId="collectionDatePaper"/>
</channels>
<config-description>
<parameter name="commune" type="text" required="true">
<label>Commune</label>
<description>The selected commune, taken from the form field 'gemeinde'. </description>
</parameter>
<parameter name="street" type="text" required="true">
<label>Street</label>
<description>The selected street, taken from the form field 'strasse'. This value must look like
67269@Rosmarinweg+/+Kirchhorst@Kirchhorst</description>
</parameter>
<parameter name="houseNumber" type="text" required="true">
<label>House Number</label>
<description>The selected house number, taken from the form field 'hausnr'.</description>
</parameter>
<parameter name="houseNumberAddon" type="text" required="false">
<label>House Number Addon</label>
<description>The selected house number addon, taken from the form field 'hausnraddon'.</description>
</parameter>
<parameter name="collectionPlace" type="text" required="true">
<label>Collection Place</label>
<description>Form value for the collection place, taken from the form field `ladeort`. This value must look like
67269-0010+</description>
</parameter>
</config-description>
</thing-type>
<!-- Channel type that represents the collection time of an waste type. -->
<channel-type id="collectionDateGeneralWaste">
<item-type>DateTime</item-type>
<label>General Waste</label>
<description>Next collection day for general waste</description>
<state readOnly="true" pattern="%1$tF"/>
</channel-type>
<channel-type id="collectionDateLeightweightPackaging">
<item-type>DateTime</item-type>
<label>Leightweight Packaging</label>
<description>Next collection day for leightweight packaging</description>
<state readOnly="true" pattern="%1$tF"/>
</channel-type>
<channel-type id="collectionDateBioWaste">
<item-type>DateTime</item-type>
<label>Bio Waste</label>
<description>Next collection day for bio waste</description>
<state readOnly="true" pattern="%1$tF"/>
</channel-type>
<channel-type id="collectionDatePaper">
<item-type>DateTime</item-type>
<label>Paper</label>
<description>Next collection day for paper</description>
<state readOnly="true" pattern="%1$tF"/>
</channel-type>
</thing:thing-descriptions>

View File

@ -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<WasteType, CollectionDate> getCollectionDates() throws IOException {
final Map<WasteType, CollectionDate> 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;
}
}

View File

@ -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();
}
}

View File

@ -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<WasteType, CollectionDate> 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<WasteType, CollectionDate> dates = schedule.getCollectionDates();
// All four waste types are collected here.
assertEquals(4, dates.size());
}
}

View File

@ -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<Void> schedule(final CronJob cronJob,
final Map<String, Object> 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<Void> 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();
}
}
}

View File

@ -41,6 +41,7 @@
<module>org.openhab.transform.xslt</module> <module>org.openhab.transform.xslt</module>
<!-- bindings --> <!-- bindings -->
<module>org.openhab.binding.adorne</module> <module>org.openhab.binding.adorne</module>
<module>org.openhab.binding.ahawastecollection</module>
<module>org.openhab.binding.airq</module> <module>org.openhab.binding.airq</module>
<module>org.openhab.binding.airquality</module> <module>org.openhab.binding.airquality</module>
<module>org.openhab.binding.airvisualnode</module> <module>org.openhab.binding.airvisualnode</module>