]> git.basschouten.com Git - openhab-addons.git/blob
a224ff25a485f1fa7158f85c9e982657c3b2c2e4
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
7  * This program and the accompanying materials are made available under the
8  * terms of the Eclipse Public License 2.0 which is available at
9  * http://www.eclipse.org/legal/epl-2.0
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.ahawastecollection.internal;
14
15 import java.io.IOException;
16 import java.time.Duration;
17 import java.time.ZonedDateTime;
18 import java.util.Collections;
19 import java.util.Date;
20 import java.util.Map;
21 import java.util.concurrent.ScheduledExecutorService;
22 import java.util.concurrent.locks.Lock;
23 import java.util.concurrent.locks.ReentrantLock;
24
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;
43
44 /**
45  * The {@link AhaWasteCollectionHandler} is responsible for handling commands, which are
46  * sent to one of the channels.
47  *
48  * @author Sönke Küper - Initial contribution
49  */
50 @NonNullByDefault
51 public class AhaWasteCollectionHandler extends BaseThingHandler {
52
53     private static final String DAILY_MIDNIGHT = "30 0 0 * * ? *";
54
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;
59
60     private final TimeZoneProvider timeZoneProvider;
61     private final Logger logger = LoggerFactory.getLogger(AhaWasteCollectionHandler.class);
62
63     private @Nullable AhaCollectionSchedule collectionSchedule;
64
65     private @Nullable ScheduledCompletableFuture<?> dailyJob;
66
67     private final AhaCollectionScheduleFactory scheduleFactory;
68
69     private final ScheduledExecutorService executorService;
70
71     public AhaWasteCollectionHandler(final Thing thing, final CronScheduler scheduler,
72             final TimeZoneProvider timeZoneProvider, final AhaCollectionScheduleFactory scheduleFactory,
73             @Nullable final ScheduledExecutorService executorService) {
74         super(thing);
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;
80     }
81
82     private Map<WasteType, CollectionDate> loadCollectionDates() {
83         try {
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();
90         }
91     }
92
93     @Override
94     public void handleCommand(final ChannelUID channelUID, final Command command) {
95         if (command instanceof RefreshType) {
96             this.executorService.execute(this::updateCollectionDates);
97         } else {
98             this.logger.warn("The AHA Abfuhrkalender is a read-only binding and can not handle commands");
99         }
100     }
101
102     @Override
103     public void initialize() {
104         final AhaWasteCollectionConfiguration config = this.getConfigAs(AhaWasteCollectionConfiguration.class);
105
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;
111
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");
115             return;
116         }
117
118         this.collectionSchedule = this.scheduleFactory.create(commune, street, houseNumber, houseNumberAddon,
119                 collectionPlace);
120
121         this.updateStatus(ThingStatus.UNKNOWN);
122
123         this.executorService.execute(() -> {
124             final boolean online = this.updateCollectionDates();
125             if (online) {
126                 this.restartJob();
127             }
128         });
129     }
130
131     /**
132      * Schedules a job that updates the collection dates at midnight.
133      */
134     private void restartJob() {
135         this.logger.debug("Restarting jobs for thing {}", this.getThing().getUID());
136         this.monitor.lock();
137         try {
138             this.stopJob();
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();
144             }
145         } finally {
146             this.monitor.unlock();
147         }
148     }
149
150     /**
151      * Stops all jobs for this thing.
152      */
153     private void stopJob() {
154         this.monitor.lock();
155         try {
156             final ScheduledCompletableFuture<?> job = this.dailyJob;
157             if (job != null) {
158                 job.cancel(true);
159             }
160             this.dailyJob = null;
161         } finally {
162             this.monitor.unlock();
163         }
164     }
165
166     private boolean updateCollectionDates() {
167         final Map<WasteType, CollectionDate> collectionDates = this.cache.getValue();
168         if (collectionDates == null || collectionDates.isEmpty()) {
169             return false;
170         }
171
172         this.logger.debug("Retrieved {} collection entries.", collectionDates.size());
173         this.updateChannels(collectionDates);
174         return true;
175     }
176
177     /**
178      * Refreshes the channel values with the given {@link CollectionDate}s.
179      */
180     private void updateChannels(final Map<WasteType, CollectionDate> collectionDates) {
181         for (final Channel channel : this.getThing().getChannels()) {
182
183             final WasteType wasteType = getWasteTypeByChannel(channel.getUID().getId());
184
185             final CollectionDate collectionDate = collectionDates.get(wasteType);
186             if (collectionDate == null) {
187                 this.logger.debug("No collection dates found for waste type: {}", wasteType);
188                 continue;
189             }
190
191             final Date nextCollectionDate = collectionDate.getDates().get(0);
192
193             final ZonedDateTime zonedDateTime = ZonedDateTime.ofInstant(nextCollectionDate.toInstant(),
194                     this.timeZoneProvider.getTimeZone());
195             this.updateState(channel.getUID(), new DateTimeType(zonedDateTime));
196         }
197     }
198
199     private static WasteType getWasteTypeByChannel(final String channelId) {
200         switch (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;
209             default:
210                 throw new IllegalArgumentException("Unknown channel type: " + channelId);
211         }
212     }
213 }