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.Date;
26 import java.util.HashSet;
27 import java.util.Iterator;
28 import java.util.List;
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;
35 import javax.measure.quantity.Angle;
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;
62 * Base ThingHandler for all Astro handlers.
64 * @author Gerhard Riegler - Initial contribution
65 * @author Amit Kumar Mondal - Implementation to be compliant with ESH Scheduler
68 public abstract class AstroThingHandler extends BaseThingHandler {
69 private static final String DAILY_MIDNIGHT = "30 0 0 * * ? *";
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");
75 /** Scheduler to schedule jobs */
76 private final CronScheduler cronScheduler;
78 protected final TimeZoneProvider timeZoneProvider;
80 private final Lock monitor = new ReentrantLock();
82 private final Set<ScheduledFuture<?>> scheduledFutures = new HashSet<>();
84 private boolean linkedPositionalChannels;
86 protected AstroThingConfig thingConfig = new AstroThingConfig();
88 private @Nullable ScheduledCompletableFuture<?> dailyJob;
90 public AstroThingHandler(Thing thing, final CronScheduler scheduler, final TimeZoneProvider timeZoneProvider) {
92 this.cronScheduler = scheduler;
93 this.timeZoneProvider = timeZoneProvider;
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 '{}'",
108 thingConfig.parseGeoLocation();
111 if (thingConfig.latitude == null || thingConfig.longitude == null) {
113 "Astro parameters geolocation could not be split into latitude and longitude, disabling thing '{}'",
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);
123 logger.debug("{}", thingConfig);
124 updateStatus(ONLINE);
127 updateStatus(OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR);
129 logger.debug("Thing {} initialized {}", getThing().getUID(), getThing().getStatus());
133 public void dispose() {
134 logger.debug("Disposing thing {}", getThing().getUID());
136 logger.debug("Thing {} disposed", getThing().getUID());
140 public void handleCommand(ChannelUID channelUID, Command command) {
141 if (REFRESH == command) {
142 logger.debug("Refreshing {}", channelUID);
143 publishChannelIfLinked(channelUID);
145 logger.warn("The Astro-Binding is a read-only binding and can not handle commands");
150 * Iterates all channels of the thing and updates their states.
152 public void publishPlanet() {
153 Planet planet = getPlanet();
154 if (planet == null) {
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());
166 * Publishes the channel with data if it's linked.
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);
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);
187 * Schedules a positional and a daily job at midnight for Astro calculation and starts it immediately too. Removes
188 * already scheduled jobs first.
190 private void restartJobs() {
191 logger.debug("Restarting jobs for thing {}", getThing().getUID());
195 if (getThing().getStatus() == ONLINE) {
196 String thingUID = getThing().getUID().toString();
198 Job runnable = getDailyJob();
199 dailyJob = cronScheduler.schedule(runnable, DAILY_MIDNIGHT);
200 logger.debug("Scheduled {} at midnight", dailyJob);
201 // Execute daily startup job immediately
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,
211 scheduledFutures.add(future);
212 logger.info("Scheduled {} every {} seconds", positionalJob, thingConfig.interval);
221 * Stops all jobs for this thing.
223 private void stopJobs() {
224 logger.debug("Stopping scheduled jobs for thing {}", getThing().getUID());
227 ScheduledCompletableFuture<?> job = dailyJob;
232 for (ScheduledFuture<?> future : scheduledFutures) {
233 if (!future.isDone()) {
237 scheduledFutures.clear();
238 } catch (Exception ex) {
239 logger.error("{}", ex.getMessage(), ex);
246 public void channelLinked(ChannelUID channelUID) {
247 linkedChannelChange(channelUID);
248 publishChannelIfLinked(channelUID);
252 public void channelUnlinked(ChannelUID channelUID) {
253 linkedChannelChange(channelUID);
257 * Counts positional channels and restarts Astro jobs.
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) {
270 * Returns {@code true}, if at least one positional channel is linked.
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)) {
284 * Emits an event for the given channel.
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());
292 triggerChannel(channel.getUID(), event);
296 * Adds the provided {@link Job} to the queue (cannot be {@code null})
298 * @return {@code true} if the {@code job} is added to the queue, otherwise {@code false}
300 public void schedule(Job job, Calendar eventAt) {
304 tidyScheduledFutures();
305 sleepTime = eventAt.getTimeInMillis() - new Date().getTime();
306 ScheduledFuture<?> future = scheduler.schedule(job, sleepTime, TimeUnit.MILLISECONDS);
307 scheduledFutures.add(future);
311 if (logger.isDebugEnabled()) {
312 final String formattedDate = this.isoFormatter.format(eventAt.getTime());
313 logger.debug("Scheduled {} in {}ms (at {})", job, sleepTime, formattedDate);
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);
328 * Calculates and publishes the daily Astro data.
330 public void publishDailyInfo() {
331 publishPositionalInfo();
335 * Calculates and publishes the interval Astro data.
337 public abstract void publishPositionalInfo();
340 * Returns the {@link Planet} instance (cannot be {@code null})
342 public abstract @Nullable Planet getPlanet();
345 * Returns the channelIds for positional calculation (cannot be {@code null})
347 protected abstract String[] getPositionalChannelIds();
350 * Returns the daily calculation {@link Job} (cannot be {@code null})
352 protected abstract Job getDailyJob();
354 public abstract @Nullable Position getPositionAt(ZonedDateTime date);
356 public @Nullable QuantityType<Angle> getAzimuth(ZonedDateTime date) {
357 Position position = getPositionAt(date);
358 return position != null ? position.getAzimuth() : null;
361 public @Nullable QuantityType<Angle> getElevation(ZonedDateTime date) {
362 Position position = getPositionAt(date);
363 return position != null ? position.getElevation() : null;
367 public Collection<Class<? extends ThingHandlerService>> getServices() {
368 return List.of(AstroActions.class);