]> git.basschouten.com Git - openhab-addons.git/blob
2a628d10d96ac1299a774c405e1ce2d62937719f
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 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.locks.Lock;
22 import java.util.concurrent.locks.ReentrantLock;
23
24 import org.eclipse.jdt.annotation.NonNullByDefault;
25 import org.eclipse.jdt.annotation.Nullable;
26 import org.openhab.binding.ahawastecollection.internal.CollectionDate.WasteType;
27 import org.openhab.core.cache.ExpiringCache;
28 import org.openhab.core.i18n.TimeZoneProvider;
29 import org.openhab.core.library.types.DateTimeType;
30 import org.openhab.core.scheduler.CronScheduler;
31 import org.openhab.core.scheduler.ScheduledCompletableFuture;
32 import org.openhab.core.thing.Channel;
33 import org.openhab.core.thing.ChannelUID;
34 import org.openhab.core.thing.Thing;
35 import org.openhab.core.thing.ThingStatus;
36 import org.openhab.core.thing.ThingStatusDetail;
37 import org.openhab.core.thing.binding.BaseThingHandler;
38 import org.openhab.core.types.Command;
39 import org.openhab.core.types.RefreshType;
40 import org.slf4j.Logger;
41 import org.slf4j.LoggerFactory;
42
43 /**
44  * The {@link AhaWasteCollectionHandler} is responsible for handling commands, which are
45  * sent to one of the channels.
46  *
47  * @author Sönke Küper - Initial contribution
48  */
49 @NonNullByDefault
50 public class AhaWasteCollectionHandler extends BaseThingHandler {
51
52     private static final String DAILY_MIDNIGHT = "30 0 0 * * ? *";
53
54     /** Scheduler to schedule jobs */
55     private final CronScheduler cronScheduler;
56     private final Lock monitor = new ReentrantLock();
57     private final ExpiringCache<Map<WasteType, CollectionDate>> cache;
58
59     private final TimeZoneProvider timeZoneProvider;
60     private final Logger logger = LoggerFactory.getLogger(AhaWasteCollectionHandler.class);
61
62     private @Nullable AhaWasteCollectionConfiguration config;
63     private @Nullable AhaCollectionSchedule collectionSchedule;
64
65     private @Nullable ScheduledCompletableFuture<?> dailyJob;
66
67     private final AhaCollectionScheduleFactory scheduleFactory;
68
69     public AhaWasteCollectionHandler(final Thing thing, final CronScheduler scheduler,
70             final TimeZoneProvider timeZoneProvider, final AhaCollectionScheduleFactory scheduleFactory) {
71         super(thing);
72         this.cronScheduler = scheduler;
73         this.timeZoneProvider = timeZoneProvider;
74         this.scheduleFactory = scheduleFactory;
75         this.cache = new ExpiringCache<>(Duration.ofMinutes(5), this::loadCollectionDates);
76     }
77
78     private Map<WasteType, CollectionDate> loadCollectionDates() {
79         try {
80             final Map<WasteType, CollectionDate> collectionDates = this.collectionSchedule.getCollectionDates();
81             this.updateStatus(ThingStatus.ONLINE);
82             return collectionDates;
83         } catch (final IOException e) {
84             this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
85             return Collections.emptyMap();
86         }
87     }
88
89     @Override
90     public void handleCommand(final ChannelUID channelUID, final Command command) {
91         if (command instanceof RefreshType) {
92             this.scheduler.execute(this::updateCollectionDates);
93         } else {
94             this.logger.warn("The AHA Abfuhrkalender is a read-only binding and can not handle commands");
95         }
96     }
97
98     @Override
99     public void initialize() {
100         this.config = this.getConfigAs(AhaWasteCollectionConfiguration.class);
101
102         final String commune = this.config.commune;
103         final String street = this.config.street;
104         final String houseNumber = this.config.houseNumber;
105         final String houseNumberAddon = this.config.houseNumberAddon;
106         final String collectionPlace = this.config.collectionPlace;
107
108         if (commune.isBlank() || street.isBlank() || houseNumber.isBlank() || collectionPlace.isBlank()) {
109             this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
110                     "Parameters are mandatory and must be configured");
111             return;
112         }
113
114         this.collectionSchedule = this.scheduleFactory.create(commune, street, houseNumber, houseNumberAddon,
115                 collectionPlace);
116
117         this.updateStatus(ThingStatus.UNKNOWN);
118
119         this.scheduler.execute(() -> {
120             final boolean online = this.updateCollectionDates();
121             if (online) {
122                 this.restartJob();
123             }
124         });
125     }
126
127     /**
128      * Schedules an job that updates the collection dates at midnight.
129      */
130     private void restartJob() {
131         this.logger.debug("Restarting jobs for thing {}", this.getThing().getUID());
132         this.monitor.lock();
133         try {
134             this.stopJob();
135             if (this.getThing().getStatus() == ThingStatus.ONLINE) {
136                 this.dailyJob = this.cronScheduler.schedule(this::updateCollectionDates, DAILY_MIDNIGHT);
137                 this.logger.debug("Scheduled {} at midnight", this.dailyJob);
138                 // Execute daily startup job immediately
139                 this.updateCollectionDates();
140             }
141         } finally {
142             this.monitor.unlock();
143         }
144     }
145
146     /**
147      * Stops all jobs for this thing.
148      */
149     private void stopJob() {
150         this.monitor.lock();
151         try {
152             final ScheduledCompletableFuture<?> job = this.dailyJob;
153             if (job != null) {
154                 job.cancel(true);
155             }
156             this.dailyJob = null;
157         } finally {
158             this.monitor.unlock();
159         }
160     }
161
162     private boolean updateCollectionDates() {
163         final Map<WasteType, CollectionDate> collectionDates = this.cache.getValue();
164         if (collectionDates == null || collectionDates.isEmpty()) {
165             return false;
166         }
167
168         this.logger.debug("Retrieved {} collection entries.", collectionDates.size());
169         this.updateChannels(collectionDates);
170         return true;
171     }
172
173     /**
174      * Refreshes the channel values with the given {@link CollectionDate}s.
175      */
176     private void updateChannels(final Map<WasteType, CollectionDate> collectionDates) {
177         for (final Channel channel : this.getThing().getChannels()) {
178
179             final WasteType wasteType = getWasteTypeByChannel(channel.getUID().getId());
180
181             final CollectionDate collectionDate = collectionDates.get(wasteType);
182             if (collectionDate == null) {
183                 this.logger.debug("No collection dates found for waste type: {}", wasteType);
184                 continue;
185             }
186
187             final Date nextCollectionDate = collectionDate.getDates().get(0);
188
189             final ZonedDateTime zonedDateTime = ZonedDateTime.ofInstant(nextCollectionDate.toInstant(),
190                     this.timeZoneProvider.getTimeZone());
191             this.updateState(channel.getUID(), new DateTimeType(zonedDateTime));
192         }
193     }
194
195     private static WasteType getWasteTypeByChannel(final String channelId) {
196         switch (channelId) {
197             case AhaWasteCollectionBindingConstants.BIOWASTE:
198                 return WasteType.BIO_WASTE;
199             case AhaWasteCollectionBindingConstants.LEIGHTWEIGHT_PACKAGING:
200                 return WasteType.LIGHT_PACKAGES;
201             case AhaWasteCollectionBindingConstants.PAPER:
202                 return WasteType.PAPER;
203             case AhaWasteCollectionBindingConstants.GENERAL_WASTE:
204                 return WasteType.GENERAL_WASTE;
205             default:
206                 throw new IllegalArgumentException("Unknown channel type: " + channelId);
207         }
208     }
209 }