2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.ahawastecollection.internal;
15 import java.io.IOException;
16 import java.time.Duration;
17 import java.time.ZonedDateTime;
18 import java.util.Collections;
19 import java.util.Date;
21 import java.util.concurrent.ScheduledExecutorService;
22 import java.util.concurrent.locks.Lock;
23 import java.util.concurrent.locks.ReentrantLock;
25 import org.eclipse.jdt.annotation.NonNullByDefault;
26 import org.eclipse.jdt.annotation.Nullable;
27 import org.openhab.binding.ahawastecollection.internal.CollectionDate.WasteType;
28 import org.openhab.core.cache.ExpiringCache;
29 import org.openhab.core.i18n.TimeZoneProvider;
30 import org.openhab.core.library.types.DateTimeType;
31 import org.openhab.core.scheduler.CronScheduler;
32 import org.openhab.core.scheduler.ScheduledCompletableFuture;
33 import org.openhab.core.thing.Channel;
34 import org.openhab.core.thing.ChannelUID;
35 import org.openhab.core.thing.Thing;
36 import org.openhab.core.thing.ThingStatus;
37 import org.openhab.core.thing.ThingStatusDetail;
38 import org.openhab.core.thing.binding.BaseThingHandler;
39 import org.openhab.core.types.Command;
40 import org.openhab.core.types.RefreshType;
41 import org.slf4j.Logger;
42 import org.slf4j.LoggerFactory;
45 * The {@link AhaWasteCollectionHandler} is responsible for handling commands, which are
46 * sent to one of the channels.
48 * @author Sönke Küper - Initial contribution
51 public class AhaWasteCollectionHandler extends BaseThingHandler {
53 private static final String DAILY_MIDNIGHT = "30 0 0 * * ? *";
55 /** Scheduler to schedule jobs */
56 private final CronScheduler cronScheduler;
57 private final Lock monitor = new ReentrantLock();
58 private final ExpiringCache<Map<WasteType, CollectionDate>> cache;
60 private final TimeZoneProvider timeZoneProvider;
61 private final Logger logger = LoggerFactory.getLogger(AhaWasteCollectionHandler.class);
63 private @Nullable AhaCollectionSchedule collectionSchedule;
65 private @Nullable ScheduledCompletableFuture<?> dailyJob;
67 private final AhaCollectionScheduleFactory scheduleFactory;
69 private final ScheduledExecutorService executorService;
71 public AhaWasteCollectionHandler(final Thing thing, final CronScheduler scheduler,
72 final TimeZoneProvider timeZoneProvider, final AhaCollectionScheduleFactory scheduleFactory,
73 @Nullable final ScheduledExecutorService executorService) {
75 this.cronScheduler = scheduler;
76 this.timeZoneProvider = timeZoneProvider;
77 this.scheduleFactory = scheduleFactory;
78 this.cache = new ExpiringCache<>(Duration.ofMinutes(5), this::loadCollectionDates);
79 this.executorService = executorService == null ? this.scheduler : executorService;
82 private Map<WasteType, CollectionDate> loadCollectionDates() {
84 final Map<WasteType, CollectionDate> collectionDates = this.collectionSchedule.getCollectionDates();
85 this.updateStatus(ThingStatus.ONLINE);
86 return collectionDates;
87 } catch (final IOException e) {
88 this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
89 return Collections.emptyMap();
94 public void handleCommand(final ChannelUID channelUID, final Command command) {
95 if (command instanceof RefreshType) {
96 this.executorService.execute(this::updateCollectionDates);
98 this.logger.warn("The AHA Abfuhrkalender is a read-only binding and can not handle commands");
103 public void initialize() {
104 final AhaWasteCollectionConfiguration config = this.getConfigAs(AhaWasteCollectionConfiguration.class);
106 final String commune = config.commune;
107 final String street = config.street;
108 final String houseNumber = config.houseNumber;
109 final String houseNumberAddon = config.houseNumberAddon;
110 final String collectionPlace = config.collectionPlace;
112 if (commune.isBlank() || street.isBlank() || houseNumber.isBlank() || collectionPlace.isBlank()) {
113 this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
114 "Parameters are mandatory and must be configured");
118 this.collectionSchedule = this.scheduleFactory.create(commune, street, houseNumber, houseNumberAddon,
121 this.updateStatus(ThingStatus.UNKNOWN);
123 this.executorService.execute(() -> {
124 final boolean online = this.updateCollectionDates();
132 * Schedules a job that updates the collection dates at midnight.
134 private void restartJob() {
135 this.logger.debug("Restarting jobs for thing {}", this.getThing().getUID());
139 if (this.getThing().getStatus() == ThingStatus.ONLINE) {
140 this.dailyJob = this.cronScheduler.schedule(this::updateCollectionDates, DAILY_MIDNIGHT);
141 this.logger.debug("Scheduled {} at midnight", this.dailyJob);
142 // Execute daily startup job immediately
143 this.updateCollectionDates();
146 this.monitor.unlock();
151 * Stops all jobs for this thing.
153 private void stopJob() {
156 final ScheduledCompletableFuture<?> job = this.dailyJob;
160 this.dailyJob = null;
162 this.monitor.unlock();
166 private boolean updateCollectionDates() {
167 final Map<WasteType, CollectionDate> collectionDates = this.cache.getValue();
168 if (collectionDates == null || collectionDates.isEmpty()) {
172 this.logger.debug("Retrieved {} collection entries.", collectionDates.size());
173 this.updateChannels(collectionDates);
178 * Refreshes the channel values with the given {@link CollectionDate}s.
180 private void updateChannels(final Map<WasteType, CollectionDate> collectionDates) {
181 for (final Channel channel : this.getThing().getChannels()) {
183 final WasteType wasteType = getWasteTypeByChannel(channel.getUID().getId());
185 final CollectionDate collectionDate = collectionDates.get(wasteType);
186 if (collectionDate == null) {
187 this.logger.debug("No collection dates found for waste type: {}", wasteType);
191 final Date nextCollectionDate = collectionDate.getDates().get(0);
193 final ZonedDateTime zonedDateTime = ZonedDateTime.ofInstant(nextCollectionDate.toInstant(),
194 this.timeZoneProvider.getTimeZone());
195 this.updateState(channel.getUID(), new DateTimeType(zonedDateTime));
199 private static WasteType getWasteTypeByChannel(final String channelId) {
201 case AhaWasteCollectionBindingConstants.BIOWASTE:
202 return WasteType.BIO_WASTE;
203 case AhaWasteCollectionBindingConstants.LEIGHTWEIGHT_PACKAGING:
204 return WasteType.LIGHT_PACKAGES;
205 case AhaWasteCollectionBindingConstants.PAPER:
206 return WasteType.PAPER;
207 case AhaWasteCollectionBindingConstants.GENERAL_WASTE:
208 return WasteType.GENERAL_WASTE;
210 throw new IllegalArgumentException("Unknown channel type: " + channelId);