]> git.basschouten.com Git - openhab-addons.git/blob
d5faa30340333325bde5f8acac67d4aba86817a6
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 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.astro.internal.handler;
14
15 import static org.openhab.core.thing.ThingStatus.*;
16 import static org.openhab.core.thing.type.ChannelKind.TRIGGER;
17 import static org.openhab.core.types.RefreshType.REFRESH;
18
19 import java.lang.invoke.MethodHandles;
20 import java.time.ZonedDateTime;
21 import java.util.Arrays;
22 import java.util.Calendar;
23 import java.util.Collection;
24 import java.util.Collections;
25 import java.util.Date;
26 import java.util.HashSet;
27 import java.util.Iterator;
28 import java.util.List;
29 import java.util.Set;
30 import java.util.concurrent.ScheduledFuture;
31 import java.util.concurrent.TimeUnit;
32 import java.util.concurrent.locks.Lock;
33 import java.util.concurrent.locks.ReentrantLock;
34
35 import javax.measure.quantity.Angle;
36
37 import org.apache.commons.lang.time.DateFormatUtils;
38 import org.eclipse.jdt.annotation.NonNullByDefault;
39 import org.eclipse.jdt.annotation.Nullable;
40 import org.openhab.binding.astro.internal.action.AstroActions;
41 import org.openhab.binding.astro.internal.config.AstroChannelConfig;
42 import org.openhab.binding.astro.internal.config.AstroThingConfig;
43 import org.openhab.binding.astro.internal.job.Job;
44 import org.openhab.binding.astro.internal.job.PositionalJob;
45 import org.openhab.binding.astro.internal.model.Planet;
46 import org.openhab.binding.astro.internal.model.Position;
47 import org.openhab.binding.astro.internal.util.PropertyUtils;
48 import org.openhab.core.i18n.TimeZoneProvider;
49 import org.openhab.core.library.types.QuantityType;
50 import org.openhab.core.scheduler.CronScheduler;
51 import org.openhab.core.scheduler.ScheduledCompletableFuture;
52 import org.openhab.core.thing.Channel;
53 import org.openhab.core.thing.ChannelUID;
54 import org.openhab.core.thing.Thing;
55 import org.openhab.core.thing.ThingStatusDetail;
56 import org.openhab.core.thing.binding.BaseThingHandler;
57 import org.openhab.core.thing.binding.ThingHandlerService;
58 import org.openhab.core.types.Command;
59 import org.slf4j.Logger;
60 import org.slf4j.LoggerFactory;
61
62 /**
63  * Base ThingHandler for all Astro handlers.
64  *
65  * @author Gerhard Riegler - Initial contribution
66  * @author Amit Kumar Mondal - Implementation to be compliant with ESH Scheduler
67  */
68 @NonNullByDefault
69 public abstract class AstroThingHandler extends BaseThingHandler {
70
71     private static final String DAILY_MIDNIGHT = "30 0 0 * * ? *";
72
73     /** Logger Instance */
74     protected final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
75
76     /** Scheduler to schedule jobs */
77     private final CronScheduler cronScheduler;
78
79     protected final TimeZoneProvider timeZoneProvider;
80
81     private final Lock monitor = new ReentrantLock();
82
83     private final Set<ScheduledFuture<?>> scheduledFutures = new HashSet<>();
84
85     private int linkedPositionalChannels = 0;
86
87     protected AstroThingConfig thingConfig = new AstroThingConfig();
88
89     private @Nullable ScheduledCompletableFuture<?> dailyJob;
90
91     public AstroThingHandler(Thing thing, final CronScheduler scheduler, final TimeZoneProvider timeZoneProvider) {
92         super(thing);
93         this.cronScheduler = scheduler;
94         this.timeZoneProvider = timeZoneProvider;
95     }
96
97     @Override
98     public void initialize() {
99         logger.debug("Initializing thing {}", getThing().getUID());
100         String thingUid = getThing().getUID().toString();
101         thingConfig = getConfigAs(AstroThingConfig.class);
102         boolean validConfig = true;
103         String geoLocation = thingConfig.geolocation;
104         if (geoLocation == null || geoLocation.trim().isEmpty()) {
105             logger.error("Astro parameter geolocation is mandatory and must be configured, disabling thing '{}'",
106                     thingUid);
107             validConfig = false;
108         } else {
109             thingConfig.parseGeoLocation();
110         }
111
112         if (thingConfig.latitude == null || thingConfig.longitude == null) {
113             logger.error(
114                     "Astro parameters geolocation could not be split into latitude and longitude, disabling thing '{}'",
115                     thingUid);
116             validConfig = false;
117         }
118         if (thingConfig.interval < 1 || thingConfig.interval > 86400) {
119             logger.error("Astro parameter interval must be in the range of 1-86400, disabling thing '{}'", thingUid);
120             validConfig = false;
121         }
122
123         if (validConfig) {
124             logger.debug("{}", thingConfig);
125             updateStatus(ONLINE);
126             restartJobs();
127         } else {
128             updateStatus(OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR);
129         }
130         logger.debug("Thing {} initialized {}", getThing().getUID(), getThing().getStatus());
131     }
132
133     @Override
134     public void dispose() {
135         logger.debug("Disposing thing {}", getThing().getUID());
136         stopJobs();
137         logger.debug("Thing {} disposed", getThing().getUID());
138     }
139
140     @Override
141     public void handleCommand(ChannelUID channelUID, Command command) {
142         if (REFRESH == command) {
143             logger.debug("Refreshing {}", channelUID);
144             publishChannelIfLinked(channelUID);
145         } else {
146             logger.warn("The Astro-Binding is a read-only binding and can not handle commands");
147         }
148     }
149
150     /**
151      * Iterates all channels of the thing and updates their states.
152      */
153     public void publishPlanet() {
154         Planet planet = getPlanet();
155         if (planet == null) {
156             return;
157         }
158         logger.debug("Publishing planet {} for thing {}", planet.getClass().getSimpleName(), getThing().getUID());
159         for (Channel channel : getThing().getChannels()) {
160             if (channel.getKind() != TRIGGER) {
161                 publishChannelIfLinked(channel.getUID());
162             }
163         }
164     }
165
166     /**
167      * Publishes the channel with data if it's linked.
168      */
169     public void publishChannelIfLinked(ChannelUID channelUID) {
170         Planet planet = getPlanet();
171         if (isLinked(channelUID.getId()) && planet != null) {
172             final Channel channel = getThing().getChannel(channelUID.getId());
173             if (channel == null) {
174                 logger.error("Cannot find channel for {}", channelUID);
175                 return;
176             }
177             try {
178                 AstroChannelConfig config = channel.getConfiguration().as(AstroChannelConfig.class);
179                 updateState(channelUID,
180                         PropertyUtils.getState(channelUID, config, planet, timeZoneProvider.getTimeZone()));
181             } catch (Exception ex) {
182                 logger.error("Can't update state for channel {} : {}", channelUID, ex.getMessage(), ex);
183             }
184         }
185     }
186
187     /**
188      * Schedules a positional and a daily job at midnight for Astro calculation and starts it immediately too. Removes
189      * already scheduled jobs first.
190      */
191     private void restartJobs() {
192         logger.debug("Restarting jobs for thing {}", getThing().getUID());
193         monitor.lock();
194         try {
195             stopJobs();
196             if (getThing().getStatus() == ONLINE) {
197                 String thingUID = getThing().getUID().toString();
198                 // Daily Job
199                 Job runnable = getDailyJob();
200                 dailyJob = cronScheduler.schedule(runnable, DAILY_MIDNIGHT);
201                 logger.debug("Scheduled {} at midnight", dailyJob);
202                 // Execute daily startup job immediately
203                 runnable.run();
204
205                 // Repeat positional job every configured seconds
206                 // Use scheduleAtFixedRate to avoid time drift associated with scheduleWithFixedDelay
207                 if (isPositionalChannelLinked()) {
208                     Job positionalJob = new PositionalJob(thingUID);
209                     ScheduledFuture<?> future = scheduler.scheduleAtFixedRate(positionalJob, 0, thingConfig.interval,
210                             TimeUnit.SECONDS);
211                     scheduledFutures.add(future);
212                     logger.info("Scheduled {} every {} seconds", positionalJob, thingConfig.interval);
213                 }
214             }
215         } finally {
216             monitor.unlock();
217         }
218     }
219
220     /**
221      * Stops all jobs for this thing.
222      */
223     private void stopJobs() {
224         logger.debug("Stopping scheduled jobs for thing {}", getThing().getUID());
225         monitor.lock();
226         try {
227             ScheduledCompletableFuture<?> job = dailyJob;
228             if (job != null) {
229                 job.cancel(true);
230             }
231             dailyJob = null;
232             for (ScheduledFuture<?> future : scheduledFutures) {
233                 if (!future.isDone()) {
234                     future.cancel(true);
235                 }
236             }
237             scheduledFutures.clear();
238         } catch (Exception ex) {
239             logger.error("{}", ex.getMessage(), ex);
240         } finally {
241             monitor.unlock();
242         }
243     }
244
245     @Override
246     public void channelLinked(ChannelUID channelUID) {
247         linkedChannelChange(channelUID, 1);
248         publishChannelIfLinked(channelUID);
249     }
250
251     @Override
252     public void channelUnlinked(ChannelUID channelUID) {
253         linkedChannelChange(channelUID, -1);
254     }
255
256     /**
257      * Counts positional channels and restarts Astro jobs.
258      */
259     private void linkedChannelChange(ChannelUID channelUID, int step) {
260         if (Arrays.asList(getPositionalChannelIds()).contains(channelUID.getId())) {
261             int oldValue = linkedPositionalChannels;
262             linkedPositionalChannels += step;
263             if (oldValue == 0 && linkedPositionalChannels > 0 || oldValue > 0 && linkedPositionalChannels == 0) {
264                 restartJobs();
265             }
266         }
267     }
268
269     /**
270      * Returns {@code true}, if at least one positional channel is linked.
271      */
272     private boolean isPositionalChannelLinked() {
273         List<String> positionalChannels = Arrays.asList(getPositionalChannelIds());
274         for (Channel channel : getThing().getChannels()) {
275             String id = channel.getUID().getId();
276             if (isLinked(id) && positionalChannels.contains(id)) {
277                 return true;
278             }
279         }
280         return false;
281     }
282
283     /**
284      * Emits an event for the given channel.
285      */
286     public void triggerEvent(String channelId, String event) {
287         final Channel channel = getThing().getChannel(channelId);
288         if (channel == null) {
289             logger.warn("Event {} in thing {} does not exist, please recreate the thing", event, getThing().getUID());
290             return;
291         }
292         triggerChannel(channel.getUID(), event);
293     }
294
295     /**
296      * Adds the provided {@link Job} to the queue (cannot be {@code null})
297      *
298      * @return {@code true} if the {@code job} is added to the queue, otherwise {@code false}
299      */
300     public void schedule(Job job, Calendar eventAt) {
301         long sleepTime;
302         monitor.lock();
303         try {
304             tidyScheduledFutures();
305             sleepTime = eventAt.getTimeInMillis() - new Date().getTime();
306             ScheduledFuture<?> future = scheduler.schedule(job, sleepTime, TimeUnit.MILLISECONDS);
307             scheduledFutures.add(future);
308         } finally {
309             monitor.unlock();
310         }
311         if (logger.isDebugEnabled()) {
312             String formattedDate = DateFormatUtils.ISO_DATETIME_FORMAT.format(eventAt);
313             logger.debug("Scheduled {} in {}ms (at {})", job, sleepTime, formattedDate);
314         }
315     }
316
317     private void tidyScheduledFutures() {
318         for (Iterator<ScheduledFuture<?>> iterator = scheduledFutures.iterator(); iterator.hasNext();) {
319             ScheduledFuture<?> future = iterator.next();
320             if (future.isDone()) {
321                 logger.trace("Tidying up done future {}", future);
322                 iterator.remove();
323             }
324         }
325     }
326
327     /**
328      * Calculates and publishes the daily Astro data.
329      */
330     public void publishDailyInfo() {
331         publishPositionalInfo();
332     }
333
334     /**
335      * Calculates and publishes the interval Astro data.
336      */
337     public abstract void publishPositionalInfo();
338
339     /**
340      * Returns the {@link Planet} instance (cannot be {@code null})
341      */
342     public abstract @Nullable Planet getPlanet();
343
344     /**
345      * Returns the channelIds for positional calculation (cannot be {@code null})
346      */
347     protected abstract String[] getPositionalChannelIds();
348
349     /**
350      * Returns the daily calculation {@link Job} (cannot be {@code null})
351      */
352     protected abstract Job getDailyJob();
353
354     public abstract @Nullable Position getPositionAt(ZonedDateTime date);
355
356     public @Nullable QuantityType<Angle> getAzimuth(ZonedDateTime date) {
357         Position position = getPositionAt(date);
358         return position != null ? position.getAzimuth() : null;
359     }
360
361     public @Nullable QuantityType<Angle> getElevation(ZonedDateTime date) {
362         Position position = getPositionAt(date);
363         return position != null ? position.getElevation() : null;
364     }
365
366     @Override
367     public Collection<Class<? extends ThingHandlerService>> getServices() {
368         return Collections.singletonList(AstroActions.class);
369     }
370 }