]> git.basschouten.com Git - openhab-addons.git/blob
767bf11125eadb357a0f7f25ebd4f564b724e01f
[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.Collections;
26 import java.util.Date;
27 import java.util.HashSet;
28 import java.util.Iterator;
29 import java.util.List;
30 import java.util.Set;
31 import java.util.concurrent.ScheduledFuture;
32 import java.util.concurrent.TimeUnit;
33 import java.util.concurrent.locks.Lock;
34 import java.util.concurrent.locks.ReentrantLock;
35
36 import javax.measure.quantity.Angle;
37
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     private static final String DAILY_MIDNIGHT = "30 0 0 * * ? *";
71
72     /** Logger Instance */
73     private final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
74     private final SimpleDateFormat isoFormatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
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 boolean linkedPositionalChannels;
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                 linkedPositionalChannels = isPositionalChannelLinked();
208                 if (linkedPositionalChannels) {
209                     Job positionalJob = new PositionalJob(thingUID);
210                     ScheduledFuture<?> future = scheduler.scheduleAtFixedRate(positionalJob, 0, thingConfig.interval,
211                             TimeUnit.SECONDS);
212                     scheduledFutures.add(future);
213                     logger.info("Scheduled {} every {} seconds", positionalJob, thingConfig.interval);
214                 }
215             }
216         } finally {
217             monitor.unlock();
218         }
219     }
220
221     /**
222      * Stops all jobs for this thing.
223      */
224     private void stopJobs() {
225         logger.debug("Stopping scheduled jobs for thing {}", getThing().getUID());
226         monitor.lock();
227         try {
228             ScheduledCompletableFuture<?> job = dailyJob;
229             if (job != null) {
230                 job.cancel(true);
231             }
232             dailyJob = null;
233             for (ScheduledFuture<?> future : scheduledFutures) {
234                 if (!future.isDone()) {
235                     future.cancel(true);
236                 }
237             }
238             scheduledFutures.clear();
239         } catch (Exception ex) {
240             logger.error("{}", ex.getMessage(), ex);
241         } finally {
242             monitor.unlock();
243         }
244     }
245
246     @Override
247     public void channelLinked(ChannelUID channelUID) {
248         linkedChannelChange(channelUID);
249         publishChannelIfLinked(channelUID);
250     }
251
252     @Override
253     public void channelUnlinked(ChannelUID channelUID) {
254         linkedChannelChange(channelUID);
255     }
256
257     /**
258      * Counts positional channels and restarts Astro jobs.
259      */
260     private void linkedChannelChange(ChannelUID channelUID) {
261         if (Arrays.asList(getPositionalChannelIds()).contains(channelUID.getId())) {
262             boolean oldValue = linkedPositionalChannels;
263             linkedPositionalChannels = isPositionalChannelLinked();
264             if (oldValue != linkedPositionalChannels) {
265                 restartJobs();
266             }
267         }
268     }
269
270     /**
271      * Returns {@code true}, if at least one positional channel is linked.
272      */
273     private boolean isPositionalChannelLinked() {
274         List<String> positionalChannels = Arrays.asList(getPositionalChannelIds());
275         for (Channel channel : getThing().getChannels()) {
276             String id = channel.getUID().getId();
277             if (isLinked(id) && positionalChannels.contains(id)) {
278                 return true;
279             }
280         }
281         return false;
282     }
283
284     /**
285      * Emits an event for the given channel.
286      */
287     public void triggerEvent(String channelId, String event) {
288         final Channel channel = getThing().getChannel(channelId);
289         if (channel == null) {
290             logger.warn("Event {} in thing {} does not exist, please recreate the thing", event, getThing().getUID());
291             return;
292         }
293         triggerChannel(channel.getUID(), event);
294     }
295
296     /**
297      * Adds the provided {@link Job} to the queue (cannot be {@code null})
298      *
299      * @return {@code true} if the {@code job} is added to the queue, otherwise {@code false}
300      */
301     public void schedule(Job job, Calendar eventAt) {
302         long sleepTime;
303         monitor.lock();
304         try {
305             tidyScheduledFutures();
306             sleepTime = eventAt.getTimeInMillis() - new Date().getTime();
307             ScheduledFuture<?> future = scheduler.schedule(job, sleepTime, TimeUnit.MILLISECONDS);
308             scheduledFutures.add(future);
309         } finally {
310             monitor.unlock();
311         }
312         if (logger.isDebugEnabled()) {
313             final String formattedDate = this.isoFormatter.format(eventAt.getTime());
314             logger.debug("Scheduled {} in {}ms (at {})", job, sleepTime, formattedDate);
315         }
316     }
317
318     private void tidyScheduledFutures() {
319         for (Iterator<ScheduledFuture<?>> iterator = scheduledFutures.iterator(); iterator.hasNext();) {
320             ScheduledFuture<?> future = iterator.next();
321             if (future.isDone()) {
322                 logger.trace("Tidying up done future {}", future);
323                 iterator.remove();
324             }
325         }
326     }
327
328     /**
329      * Calculates and publishes the daily Astro data.
330      */
331     public void publishDailyInfo() {
332         publishPositionalInfo();
333     }
334
335     /**
336      * Calculates and publishes the interval Astro data.
337      */
338     public abstract void publishPositionalInfo();
339
340     /**
341      * Returns the {@link Planet} instance (cannot be {@code null})
342      */
343     public abstract @Nullable Planet getPlanet();
344
345     /**
346      * Returns the channelIds for positional calculation (cannot be {@code null})
347      */
348     protected abstract String[] getPositionalChannelIds();
349
350     /**
351      * Returns the daily calculation {@link Job} (cannot be {@code null})
352      */
353     protected abstract Job getDailyJob();
354
355     public abstract @Nullable Position getPositionAt(ZonedDateTime date);
356
357     public @Nullable QuantityType<Angle> getAzimuth(ZonedDateTime date) {
358         Position position = getPositionAt(date);
359         return position != null ? position.getAzimuth() : null;
360     }
361
362     public @Nullable QuantityType<Angle> getElevation(ZonedDateTime date) {
363         Position position = getPositionAt(date);
364         return position != null ? position.getElevation() : null;
365     }
366
367     @Override
368     public Collection<Class<? extends ThingHandlerService>> getServices() {
369         return Collections.singletonList(AstroActions.class);
370     }
371 }