Java tutorial
/** * Copyright (c) 2010-2019 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License 2.0 which is available at * http://www.eclipse.org/legal/epl-2.0 * * SPDX-License-Identifier: EPL-2.0 */ package org.openhab.binding.astro.internal.handler; import static org.eclipse.smarthome.core.thing.ThingStatus.*; import static org.eclipse.smarthome.core.thing.type.ChannelKind.TRIGGER; import static org.eclipse.smarthome.core.types.RefreshType.REFRESH; import java.lang.invoke.MethodHandles; import java.util.Calendar; import java.util.Date; import java.util.HashSet; import java.util.Iterator; import java.util.Set; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import org.apache.commons.lang.ArrayUtils; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.time.DateFormatUtils; import org.eclipse.smarthome.core.scheduler.CronScheduler; import org.eclipse.smarthome.core.scheduler.ScheduledCompletableFuture; import org.eclipse.smarthome.core.thing.Channel; import org.eclipse.smarthome.core.thing.ChannelUID; import org.eclipse.smarthome.core.thing.Thing; import org.eclipse.smarthome.core.thing.binding.BaseThingHandler; import org.eclipse.smarthome.core.types.Command; import org.openhab.binding.astro.internal.config.AstroChannelConfig; import org.openhab.binding.astro.internal.config.AstroThingConfig; import org.openhab.binding.astro.internal.job.Job; import org.openhab.binding.astro.internal.job.PositionalJob; import org.openhab.binding.astro.internal.model.Planet; import org.openhab.binding.astro.internal.util.PropertyUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Base ThingHandler for all Astro handlers. * * @author Gerhard Riegler - Initial contribution * @author Amit Kumar Mondal - Implementation to be compliant with ESH Scheduler */ public abstract class AstroThingHandler extends BaseThingHandler { private static final String DAILY_MIDNIGHT = "30 0 0 * * ? *"; /** Logger Instance */ protected final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); /** Scheduler to schedule jobs */ private final CronScheduler cronScheduler; private int linkedPositionalChannels = 0; protected AstroThingConfig thingConfig; private final Lock monitor = new ReentrantLock(); private ScheduledCompletableFuture dailyJob; private final Set<ScheduledFuture<?>> scheduledFutures = new HashSet<>(); public AstroThingHandler(Thing thing, CronScheduler scheduler) { super(thing); this.cronScheduler = scheduler; } @Override public void initialize() { logger.debug("Initializing thing {}", getThing().getUID()); String thingUid = getThing().getUID().toString(); thingConfig = getConfigAs(AstroThingConfig.class); thingConfig.setThingUid(thingUid); boolean validConfig = true; if (StringUtils.trimToNull(thingConfig.getGeolocation()) == null) { logger.error("Astro parameter geolocation is mandatory and must be configured, disabling thing '{}'", thingUid); validConfig = false; } else { thingConfig.parseGeoLocation(); } if (thingConfig.getLatitude() == null || thingConfig.getLongitude() == null) { logger.error( "Astro parameters geolocation could not be split into latitude and longitude, disabling thing '{}'", thingUid); validConfig = false; } if (thingConfig.getInterval() == null || thingConfig.getInterval() < 1 || thingConfig.getInterval() > 86400) { logger.error("Astro parameter interval must be in the range of 1-86400, disabling thing '{}'", thingUid); validConfig = false; } if (validConfig) { logger.debug("{}", thingConfig); updateStatus(ONLINE); restartJobs(); } else { updateStatus(OFFLINE); } logger.debug("Thing {} initialized {}", getThing().getUID(), getThing().getStatus()); } @Override public void dispose() { logger.debug("Disposing thing {}", getThing().getUID()); stopJobs(); logger.debug("Thing {} disposed", getThing().getUID()); } @Override public void handleCommand(ChannelUID channelUID, Command command) { if (REFRESH == command) { logger.debug("Refreshing {}", channelUID); publishChannelIfLinked(channelUID); } else { logger.warn("The Astro-Binding is a read-only binding and can not handle commands"); } } /** * Iterates all channels of the thing and updates their states. */ public void publishPlanet() { logger.debug("Publishing planet {} for thing {}", getPlanet().getClass().getSimpleName(), getThing().getUID()); for (Channel channel : getThing().getChannels()) { if (channel.getKind() != TRIGGER) { publishChannelIfLinked(channel.getUID()); } } } /** * Publishes the channel with data if it's linked. */ public void publishChannelIfLinked(ChannelUID channelUID) { if (isLinked(channelUID.getId()) && getPlanet() != null) { final Channel channel = getThing().getChannel(channelUID.getId()); if (channel == null) { logger.error("Cannot find channel for {}", channelUID); return; } try { AstroChannelConfig config = channel.getConfiguration().as(AstroChannelConfig.class); updateState(channelUID, PropertyUtils.getState(channelUID, config, getPlanet())); } catch (Exception ex) { logger.error("Can't update state for channel {} : {}", channelUID, ex.getMessage(), ex); } } } /** * Schedules a positional and a daily job at midnight for Astro calculation and starts it immediately too. Removes * already scheduled jobs first. */ private void restartJobs() { logger.debug("Restarting jobs for thing {}", getThing().getUID()); monitor.lock(); try { stopJobs(); if (getThing().getStatus() == ONLINE) { String thingUID = getThing().getUID().toString(); if (cronScheduler == null) { logger.warn("Thread Pool Executor is not available"); return; } // Daily Job Job runnable = getDailyJob(); dailyJob = cronScheduler.schedule(runnable, DAILY_MIDNIGHT); logger.debug("Scheduled {} at midnight", dailyJob); // Execute daily startup job immediately runnable.run(); // Repeat positional job every configured seconds // Use scheduleAtFixedRate to avoid time drift associated with scheduleWithFixedDelay if (isPositionalChannelLinked()) { Job positionalJob = new PositionalJob(thingUID); ScheduledFuture<?> future = scheduler.scheduleAtFixedRate(positionalJob, 0, thingConfig.getInterval(), TimeUnit.SECONDS); scheduledFutures.add(future); logger.info("Scheduled {} every {} seconds", positionalJob, thingConfig.getInterval()); } } } finally { monitor.unlock(); } } /** * Stops all jobs for this thing. */ private void stopJobs() { logger.debug("Stopping scheduled jobs for thing {}", getThing().getUID()); monitor.lock(); try { if (cronScheduler != null) { if (dailyJob != null) { dailyJob.cancel(true); } dailyJob = null; } for (ScheduledFuture<?> future : scheduledFutures) { if (!future.isDone()) { future.cancel(true); } } scheduledFutures.clear(); } catch (Exception ex) { logger.error("{}", ex.getMessage(), ex); } finally { monitor.unlock(); } } @Override public void channelLinked(ChannelUID channelUID) { linkedChannelChange(channelUID, 1); publishChannelIfLinked(channelUID); } @Override public void channelUnlinked(ChannelUID channelUID) { linkedChannelChange(channelUID, -1); } /** * Counts positional channels and restarts Astro jobs. */ private void linkedChannelChange(ChannelUID channelUID, int step) { if (ArrayUtils.contains(getPositionalChannelIds(), channelUID.getId())) { int oldValue = linkedPositionalChannels; linkedPositionalChannels += step; if (oldValue == 0 && linkedPositionalChannels > 0 || oldValue > 0 && linkedPositionalChannels == 0) { restartJobs(); } } } /** * Returns {@code true}, if at least one positional channel is linked. */ private boolean isPositionalChannelLinked() { for (Channel channel : getThing().getChannels()) { if (ArrayUtils.contains(getPositionalChannelIds(), channel.getUID().getId()) && isLinked(channel.getUID().getId())) { return true; } } return false; } /** * Emits an event for the given channel. */ public void triggerEvent(String channelId, String event) { final Channel channel = getThing().getChannel(channelId); if (channel == null) { logger.warn("Event {} in thing {} does not exist, please recreate the thing", event, getThing().getUID()); return; } triggerChannel(channel.getUID(), event); } /** * Adds the provided {@link Job} to the queue (cannot be {@code null}) * * @return {@code true} if the {@code job} is added to the queue, otherwise {@code false} */ public void schedule(Job job, Calendar eventAt) { long sleepTime; monitor.lock(); try { tidyScheduledFutures(); sleepTime = eventAt.getTimeInMillis() - new Date().getTime(); ScheduledFuture<?> future = scheduler.schedule(job, sleepTime, TimeUnit.MILLISECONDS); scheduledFutures.add(future); } finally { monitor.unlock(); } if (logger.isDebugEnabled()) { String formattedDate = DateFormatUtils.ISO_DATETIME_FORMAT.format(eventAt); logger.debug("Scheduled {} in {}ms (at {})", job, sleepTime, formattedDate); } } private void tidyScheduledFutures() { for (Iterator<ScheduledFuture<?>> iterator = scheduledFutures.iterator(); iterator.hasNext();) { ScheduledFuture<?> future = iterator.next(); if (future.isDone()) { logger.trace("Tidying up done future {}", future); iterator.remove(); } } } /** * Calculates and publishes the daily Astro data. */ public abstract void publishDailyInfo(); /** * Calculates and publishes the interval Astro data. */ public abstract void publishPositionalInfo(); /** * Returns the {@link Planet} instance (cannot be {@code null}) */ public abstract Planet getPlanet(); /** * Returns the channelIds for positional calculation (cannot be {@code null}) */ protected abstract String[] getPositionalChannelIds(); /** * Returns the daily calculation {@link Job} (cannot be {@code null}) */ protected abstract Job getDailyJob(); }