/**
* Copyright (c) 2010-2017 by the respective copyright holders.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.eclipse.smarthome.binding.astro.handler;
import static org.quartz.JobBuilder.newJob;
import static org.quartz.SimpleScheduleBuilder.simpleSchedule;
import static org.quartz.TriggerBuilder.newTrigger;
import static org.quartz.impl.matchers.GroupMatcher.jobGroupEquals;
import java.util.Date;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import org.eclipse.smarthome.binding.astro.internal.config.AstroChannelConfig;
import org.eclipse.smarthome.binding.astro.internal.config.AstroThingConfig;
import org.eclipse.smarthome.binding.astro.internal.job.AbstractBaseJob;
import org.eclipse.smarthome.binding.astro.internal.job.AbstractDailyJob;
import org.eclipse.smarthome.binding.astro.internal.job.PositionalJob;
import org.eclipse.smarthome.binding.astro.internal.model.Planet;
import org.eclipse.smarthome.binding.astro.internal.util.PropertyUtils;
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.ThingStatus;
import org.eclipse.smarthome.core.thing.binding.BaseThingHandler;
import org.eclipse.smarthome.core.thing.type.ChannelKind;
import org.eclipse.smarthome.core.types.Command;
import org.eclipse.smarthome.core.types.RefreshType;
import org.eclipse.smarthome.core.types.State;
import org.quartz.CronScheduleBuilder;
import org.quartz.JobDataMap;
import org.quartz.JobDetail;
import org.quartz.JobKey;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.Trigger;
import org.quartz.impl.StdSchedulerFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Base ThingHandler for all Astro handlers.
*
* @author Gerhard Riegler - Initial contribution
*/
public abstract class AstroThingHandler extends BaseThingHandler {
private final Logger logger = LoggerFactory.getLogger(AstroThingHandler.class);
private Scheduler quartzScheduler;
private ScheduledFuture<?> schedulerFuture;
private int linkedPositionalChannels = 0;
protected AstroThingConfig thingConfig;
private Object schedulerLock = new Object();
public AstroThingHandler(Thing thing) {
super(thing);
}
/**
* {@inheritDoc}
*/
@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(ThingStatus.ONLINE);
restartJobs();
} else {
updateStatus(ThingStatus.OFFLINE);
}
logger.debug("Thing {} initialized {}", getThing().getUID(), getThing().getStatus());
}
/**
* {@inheritDoc}
*/
@Override
public void dispose() {
logger.debug("Disposing thing {}", getThing().getUID());
if (schedulerFuture != null && !schedulerFuture.isCancelled()) {
schedulerFuture.cancel(true);
schedulerFuture = null;
}
stopJobs();
quartzScheduler = null;
logger.debug("Thing {} disposed", getThing().getUID());
}
/**
* {@inheritDoc}
*/
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (RefreshType.REFRESH == command) {
logger.debug("Refreshing {}", channelUID);
publishChannelIfLinked(channelUID);
} else {
logger.warn("The Astro-Binding is a read-only binding and can not handle commands");
}
}
/**
* {@inheritDoc}
*/
@Override
public void handleUpdate(ChannelUID channelUID, State newState) {
logger.warn("The Astro-Binding is a read-only binding and can not handle channel updates");
super.handleUpdate(channelUID, newState);
}
/**
* 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() != ChannelKind.TRIGGER) {
publishChannelIfLinked(channel.getUID());
}
}
}
/**
* Publishes the channel with data if it's linked.
*/
public void publishChannelIfLinked(ChannelUID channelUID) {
if (isLinked(channelUID.getId()) && getPlanet() != null) {
try {
AstroChannelConfig config = getThing().getChannel(channelUID.getId()).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());
if (schedulerFuture != null && !schedulerFuture.isCancelled()) {
schedulerFuture.cancel(true);
}
schedulerFuture = scheduler.schedule(new Runnable() {
@Override
public void run() {
stopJobs();
try {
synchronized (schedulerLock) {
if (quartzScheduler == null) {
quartzScheduler = StdSchedulerFactory.getDefaultScheduler();
}
if (getThing().getStatus() == ThingStatus.ONLINE) {
String thingUid = getThing().getUID().toString();
String typeId = getThing().getThingTypeUID().getId();
JobDataMap jobDataMap = new JobDataMap();
jobDataMap.put(AbstractBaseJob.KEY_THING_UID, thingUid);
jobDataMap.put(AbstractBaseJob.KEY_JOB_NAME, "job-daily");
// dailyJob
Trigger trigger = newTrigger().withIdentity("trigger-daily-" + typeId, thingUid)
.withSchedule(CronScheduleBuilder.cronSchedule("0 0 0 * * ?")).build();
JobDetail jobDetail = newJob(getDailyJobClass())
.withIdentity("job-daily-" + typeId, thingUid).usingJobData(jobDataMap).build();
quartzScheduler.scheduleJob(jobDetail, trigger);
logger.info("Scheduled astro job-daily-{} at midnight for thing {}", typeId, thingUid);
// startupJob
trigger = newTrigger().withIdentity("trigger-daily-startup-" + typeId, thingUid).startNow()
.build();
jobDetail = newJob(getDailyJobClass()).withIdentity("job-daily-startup-" + typeId, thingUid)
.usingJobData(jobDataMap).build();
quartzScheduler.scheduleJob(jobDetail, trigger);
if (isPositionalChannelLinked()) {
// positional intervalJob
jobDataMap = new JobDataMap();
jobDataMap.put(AbstractBaseJob.KEY_THING_UID, thingUid);
jobDataMap.put(AbstractBaseJob.KEY_JOB_NAME, "job-positional");
Date start = new Date(System.currentTimeMillis() + (thingConfig.getInterval()) * 1000);
trigger = newTrigger().withIdentity("trigger-positional", thingUid).startAt(start)
.withSchedule(simpleSchedule().repeatForever()
.withIntervalInSeconds(thingConfig.getInterval()))
.build();
jobDetail = newJob(PositionalJob.class).withIdentity("job-positional", thingUid)
.usingJobData(jobDataMap).build();
quartzScheduler.scheduleJob(jobDetail, trigger);
logger.info("Scheduled astro job-positional with interval of {} seconds for thing {}",
thingConfig.getInterval(), thingUid);
}
}
}
} catch (SchedulerException ex) {
logger.error("{}", ex.getMessage(), ex);
}
}
}, 2000, TimeUnit.MILLISECONDS);
}
/**
* Stops all jobs for this thing.
*/
private void stopJobs() {
logger.debug("Stopping jobs for thing {}", getThing().getUID());
synchronized (schedulerLock) {
if (quartzScheduler != null) {
try {
String thingUid = getThing().getUID().toString();
for (JobKey jobKey : quartzScheduler.getJobKeys(jobGroupEquals(thingUid))) {
logger.debug("Deleting astro {} for thing '{}'", jobKey.getName(), thingUid);
quartzScheduler.deleteJob(jobKey);
}
} catch (SchedulerException ex) {
logger.error("{}", ex.getMessage(), ex);
}
}
}
}
/**
* {@inheritDoc}
*/
@Override
public void channelLinked(ChannelUID channelUID) {
linkedChannelChange(channelUID, 1);
publishChannelIfLinked(channelUID);
}
/**
* {@inheritDoc}
*/
@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 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) {
if (getThing().getChannel(channelId) != null) {
triggerChannel(getThing().getChannel(channelId).getUID(), event);
} else {
logger.warn("Event {} in thing {} does not exist, please recreate the thing", event, getThing().getUID());
}
}
/**
* Returns the scheduler for the astro jobs.
*/
public Scheduler getScheduler() {
return quartzScheduler;
}
/**
* Calculates and publishes the daily astro data.
*/
public abstract void publishDailyInfo();
/**
* Calculates and publishes the interval astro data.
*/
public abstract void publishPositionalInfo();
/**
* Returns the planet.
*/
public abstract Planet getPlanet();
/**
* Returns the channelIds for positional calculation.
*/
protected abstract String[] getPositionalChannelIds();
/**
* Returns the class for the daily calculation job.
*/
protected abstract Class<? extends AbstractDailyJob> getDailyJobClass();
}