2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.astro.internal.handler;
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;
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;
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;
36 import javax.measure.quantity.Angle;
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;
63 * Base ThingHandler for all Astro handlers.
65 * @author Gerhard Riegler - Initial contribution
66 * @author Amit Kumar Mondal - Implementation to be compliant with ESH Scheduler
69 public abstract class AstroThingHandler extends BaseThingHandler {
70 private static final String DAILY_MIDNIGHT = "30 0 0 * * ? *";
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");
76 /** Scheduler to schedule jobs */
77 private final CronScheduler cronScheduler;
79 protected final TimeZoneProvider timeZoneProvider;
81 private final Lock monitor = new ReentrantLock();
83 private final Set<ScheduledFuture<?>> scheduledFutures = new HashSet<>();
85 private boolean linkedPositionalChannels;
87 protected AstroThingConfig thingConfig = new AstroThingConfig();
89 private @Nullable ScheduledCompletableFuture<?> dailyJob;
91 public AstroThingHandler(Thing thing, final CronScheduler scheduler, final TimeZoneProvider timeZoneProvider) {
93 this.cronScheduler = scheduler;
94 this.timeZoneProvider = timeZoneProvider;
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 '{}'",
109 thingConfig.parseGeoLocation();
112 if (thingConfig.latitude == null || thingConfig.longitude == null) {
114 "Astro parameters geolocation could not be split into latitude and longitude, disabling thing '{}'",
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);
124 logger.debug("{}", thingConfig);
125 updateStatus(ONLINE);
128 updateStatus(OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR);
130 logger.debug("Thing {} initialized {}", getThing().getUID(), getThing().getStatus());
134 public void dispose() {
135 logger.debug("Disposing thing {}", getThing().getUID());
137 logger.debug("Thing {} disposed", getThing().getUID());
141 public void handleCommand(ChannelUID channelUID, Command command) {
142 if (REFRESH == command) {
143 logger.debug("Refreshing {}", channelUID);
144 publishChannelIfLinked(channelUID);
146 logger.warn("The Astro-Binding is a read-only binding and can not handle commands");
151 * Iterates all channels of the thing and updates their states.
153 public void publishPlanet() {
154 Planet planet = getPlanet();
155 if (planet == null) {
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());
167 * Publishes the channel with data if it's linked.
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);
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);
188 * Schedules a positional and a daily job at midnight for Astro calculation and starts it immediately too. Removes
189 * already scheduled jobs first.
191 private void restartJobs() {
192 logger.debug("Restarting jobs for thing {}", getThing().getUID());
196 if (getThing().getStatus() == ONLINE) {
197 String thingUID = getThing().getUID().toString();
199 Job runnable = getDailyJob();
200 dailyJob = cronScheduler.schedule(runnable, DAILY_MIDNIGHT);
201 logger.debug("Scheduled {} at midnight", dailyJob);
202 // Execute daily startup job immediately
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,
212 scheduledFutures.add(future);
213 logger.info("Scheduled {} every {} seconds", positionalJob, thingConfig.interval);
222 * Stops all jobs for this thing.
224 private void stopJobs() {
225 logger.debug("Stopping scheduled jobs for thing {}", getThing().getUID());
228 ScheduledCompletableFuture<?> job = dailyJob;
233 for (ScheduledFuture<?> future : scheduledFutures) {
234 if (!future.isDone()) {
238 scheduledFutures.clear();
239 } catch (Exception ex) {
240 logger.error("{}", ex.getMessage(), ex);
247 public void channelLinked(ChannelUID channelUID) {
248 linkedChannelChange(channelUID);
249 publishChannelIfLinked(channelUID);
253 public void channelUnlinked(ChannelUID channelUID) {
254 linkedChannelChange(channelUID);
258 * Counts positional channels and restarts Astro jobs.
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) {
271 * Returns {@code true}, if at least one positional channel is linked.
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)) {
285 * Emits an event for the given channel.
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());
293 triggerChannel(channel.getUID(), event);
297 * Adds the provided {@link Job} to the queue (cannot be {@code null})
299 * @return {@code true} if the {@code job} is added to the queue, otherwise {@code false}
301 public void schedule(Job job, Calendar eventAt) {
305 tidyScheduledFutures();
306 sleepTime = eventAt.getTimeInMillis() - new Date().getTime();
307 ScheduledFuture<?> future = scheduler.schedule(job, sleepTime, TimeUnit.MILLISECONDS);
308 scheduledFutures.add(future);
312 if (logger.isDebugEnabled()) {
313 final String formattedDate = this.isoFormatter.format(eventAt.getTime());
314 logger.debug("Scheduled {} in {}ms (at {})", job, sleepTime, formattedDate);
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);
329 * Calculates and publishes the daily Astro data.
331 public void publishDailyInfo() {
332 publishPositionalInfo();
336 * Calculates and publishes the interval Astro data.
338 public abstract void publishPositionalInfo();
341 * Returns the {@link Planet} instance (cannot be {@code null})
343 public abstract @Nullable Planet getPlanet();
346 * Returns the channelIds for positional calculation (cannot be {@code null})
348 protected abstract String[] getPositionalChannelIds();
351 * Returns the daily calculation {@link Job} (cannot be {@code null})
353 protected abstract Job getDailyJob();
355 public abstract @Nullable Position getPositionAt(ZonedDateTime date);
357 public @Nullable QuantityType<Angle> getAzimuth(ZonedDateTime date) {
358 Position position = getPositionAt(date);
359 return position != null ? position.getAzimuth() : null;
362 public @Nullable QuantityType<Angle> getElevation(ZonedDateTime date) {
363 Position position = getPositionAt(date);
364 return position != null ? position.getElevation() : null;
368 public Collection<Class<? extends ThingHandlerService>> getServices() {
369 return Collections.singletonList(AstroActions.class);