]> git.basschouten.com Git - openhab-addons.git/blob
718e7339f826323283e36f2eb8a9b1a607f32707
[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.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.text.SimpleDateFormat;
21 import java.time.ZonedDateTime;
22 import java.util.Arrays;
23 import java.util.Calendar;
24 import java.util.Collection;
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.eclipse.jdt.annotation.NonNullByDefault;
38 import org.eclipse.jdt.annotation.Nullable;
39 import org.openhab.binding.astro.internal.action.AstroActions;
40 import org.openhab.binding.astro.internal.config.AstroChannelConfig;
41 import org.openhab.binding.astro.internal.config.AstroThingConfig;
42 import org.openhab.binding.astro.internal.job.Job;
43 import org.openhab.binding.astro.internal.job.PositionalJob;
44 import org.openhab.binding.astro.internal.model.Planet;
45 import org.openhab.binding.astro.internal.model.Position;
46 import org.openhab.binding.astro.internal.util.PropertyUtils;
47 import org.openhab.core.i18n.TimeZoneProvider;
48 import org.openhab.core.library.types.QuantityType;
49 import org.openhab.core.scheduler.CronScheduler;
50 import org.openhab.core.scheduler.ScheduledCompletableFuture;
51 import org.openhab.core.thing.Channel;
52 import org.openhab.core.thing.ChannelUID;
53 import org.openhab.core.thing.Thing;
54 import org.openhab.core.thing.ThingStatusDetail;
55 import org.openhab.core.thing.binding.BaseThingHandler;
56 import org.openhab.core.thing.binding.ThingHandlerService;
57 import org.openhab.core.types.Command;
58 import org.slf4j.Logger;
59 import org.slf4j.LoggerFactory;
60
61 /**
62  * Base ThingHandler for all Astro handlers.
63  *
64  * @author Gerhard Riegler - Initial contribution
65  * @author Amit Kumar Mondal - Implementation to be compliant with ESH Scheduler
66  */
67 @NonNullByDefault
68 public abstract class AstroThingHandler extends BaseThingHandler {
69     private static final String DAILY_MIDNIGHT = "30 0 0 * * ? *";
70
71     /** Logger Instance */
72     private final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
73     private final SimpleDateFormat isoFormatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
74
75     /** Scheduler to schedule jobs */
76     private final CronScheduler cronScheduler;
77
78     protected final TimeZoneProvider timeZoneProvider;
79
80     private final Lock monitor = new ReentrantLock();
81
82     private final Set<ScheduledFuture<?>> scheduledFutures = new HashSet<>();
83
84     private boolean linkedPositionalChannels;
85
86     protected AstroThingConfig thingConfig = new AstroThingConfig();
87
88     private @Nullable ScheduledCompletableFuture<?> dailyJob;
89
90     public AstroThingHandler(Thing thing, final CronScheduler scheduler, final TimeZoneProvider timeZoneProvider) {
91         super(thing);
92         this.cronScheduler = scheduler;
93         this.timeZoneProvider = timeZoneProvider;
94     }
95
96     @Override
97     public void initialize() {
98         logger.debug("Initializing thing {}", getThing().getUID());
99         String thingUid = getThing().getUID().toString();
100         thingConfig = getConfigAs(AstroThingConfig.class);
101         boolean validConfig = true;
102         String geoLocation = thingConfig.geolocation;
103         if (geoLocation == null || geoLocation.trim().isEmpty()) {
104             logger.error("Astro parameter geolocation is mandatory and must be configured, disabling thing '{}'",
105                     thingUid);
106             validConfig = false;
107         } else {
108             thingConfig.parseGeoLocation();
109         }
110
111         if (thingConfig.latitude == null || thingConfig.longitude == null) {
112             logger.error(
113                     "Astro parameters geolocation could not be split into latitude and longitude, disabling thing '{}'",
114                     thingUid);
115             validConfig = false;
116         }
117         if (thingConfig.interval < 1 || thingConfig.interval > 86400) {
118             logger.error("Astro parameter interval must be in the range of 1-86400, disabling thing '{}'", thingUid);
119             validConfig = false;
120         }
121
122         if (validConfig) {
123             logger.debug("{}", thingConfig);
124             updateStatus(ONLINE);
125             restartJobs();
126         } else {
127             updateStatus(OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR);
128         }
129         logger.debug("Thing {} initialized {}", getThing().getUID(), getThing().getStatus());
130     }
131
132     @Override
133     public void dispose() {
134         logger.debug("Disposing thing {}", getThing().getUID());
135         stopJobs();
136         logger.debug("Thing {} disposed", getThing().getUID());
137     }
138
139     @Override
140     public void handleCommand(ChannelUID channelUID, Command command) {
141         if (REFRESH == command) {
142             logger.debug("Refreshing {}", channelUID);
143             publishChannelIfLinked(channelUID);
144         } else {
145             logger.warn("The Astro-Binding is a read-only binding and can not handle commands");
146         }
147     }
148
149     /**
150      * Iterates all channels of the thing and updates their states.
151      */
152     public void publishPlanet() {
153         Planet planet = getPlanet();
154         if (planet == null) {
155             return;
156         }
157         logger.debug("Publishing planet {} for thing {}", planet.getClass().getSimpleName(), getThing().getUID());
158         for (Channel channel : getThing().getChannels()) {
159             if (channel.getKind() != TRIGGER) {
160                 publishChannelIfLinked(channel.getUID());
161             }
162         }
163     }
164
165     /**
166      * Publishes the channel with data if it's linked.
167      */
168     public void publishChannelIfLinked(ChannelUID channelUID) {
169         Planet planet = getPlanet();
170         if (isLinked(channelUID.getId()) && planet != null) {
171             final Channel channel = getThing().getChannel(channelUID.getId());
172             if (channel == null) {
173                 logger.error("Cannot find channel for {}", channelUID);
174                 return;
175             }
176             try {
177                 AstroChannelConfig config = channel.getConfiguration().as(AstroChannelConfig.class);
178                 updateState(channelUID,
179                         PropertyUtils.getState(channelUID, config, planet, timeZoneProvider.getTimeZone()));
180             } catch (Exception ex) {
181                 logger.error("Can't update state for channel {} : {}", channelUID, ex.getMessage(), ex);
182             }
183         }
184     }
185
186     /**
187      * Schedules a positional and a daily job at midnight for Astro calculation and starts it immediately too. Removes
188      * already scheduled jobs first.
189      */
190     private void restartJobs() {
191         logger.debug("Restarting jobs for thing {}", getThing().getUID());
192         monitor.lock();
193         try {
194             stopJobs();
195             if (getThing().getStatus() == ONLINE) {
196                 String thingUID = getThing().getUID().toString();
197                 // Daily Job
198                 Job runnable = getDailyJob();
199                 dailyJob = cronScheduler.schedule(runnable, DAILY_MIDNIGHT);
200                 logger.debug("Scheduled {} at midnight", dailyJob);
201                 // Execute daily startup job immediately
202                 runnable.run();
203
204                 // Repeat positional job every configured seconds
205                 // Use scheduleAtFixedRate to avoid time drift associated with scheduleWithFixedDelay
206                 linkedPositionalChannels = isPositionalChannelLinked();
207                 if (linkedPositionalChannels) {
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);
248         publishChannelIfLinked(channelUID);
249     }
250
251     @Override
252     public void channelUnlinked(ChannelUID channelUID) {
253         linkedChannelChange(channelUID);
254     }
255
256     /**
257      * Counts positional channels and restarts Astro jobs.
258      */
259     private void linkedChannelChange(ChannelUID channelUID) {
260         if (Arrays.asList(getPositionalChannelIds()).contains(channelUID.getId())) {
261             boolean oldValue = linkedPositionalChannels;
262             linkedPositionalChannels = isPositionalChannelLinked();
263             if (oldValue != linkedPositionalChannels) {
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             final String formattedDate = this.isoFormatter.format(eventAt.getTime());
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 List.of(AstroActions.class);
369     }
370 }