/** * Copyright (c) 2010-2016 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.openhab.persistence.gcal.internal; import static org.quartz.JobBuilder.newJob; import static org.quartz.SimpleScheduleBuilder.repeatSecondlyForever; import static org.quartz.TriggerBuilder.newTrigger; import static org.quartz.impl.matchers.GroupMatcher.jobGroupEquals; import java.io.IOException; import java.util.ArrayList; import java.util.Date; import java.util.Map; import java.util.Queue; import java.util.Set; import java.util.concurrent.ConcurrentLinkedQueue; import org.apache.commons.lang.StringUtils; import org.openhab.core.items.Item; import org.openhab.core.persistence.PersistenceService; import org.openhab.io.gcal.auth.GCalGoogleOAuth; import org.osgi.framework.BundleContext; import org.quartz.DisallowConcurrentExecution; import org.quartz.Job; import org.quartz.JobDetail; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; import org.quartz.JobKey; import org.quartz.Scheduler; import org.quartz.SchedulerException; import org.quartz.SimpleTrigger; import org.quartz.impl.StdSchedulerFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.api.client.auth.oauth2.Credential; import com.google.api.client.http.HttpTransport; import com.google.api.client.http.javanet.NetHttpTransport; import com.google.api.client.json.JsonFactory; import com.google.api.client.json.jackson2.JacksonFactory; import com.google.api.client.util.DateTime; import com.google.api.services.calendar.Calendar; import com.google.api.services.calendar.model.CalendarListEntry; import com.google.api.services.calendar.model.Event; import com.google.api.services.calendar.model.EventDateTime; /** * This implementation of the {@link PersistenceService} provides Presence * Simulation features based on the Google Calendar Service. * * @author Thomas.Eichstaedt-Engelen * @since 1.0.0 */ public class GCalPersistenceService implements PersistenceService { private static final Logger logger = LoggerFactory.getLogger(GCalPersistenceService.class); private static final String GCAL_SCHEDULER_GROUP = "GoogleCalendar"; private static String calendar_name = ""; /** the upload interval (optional, defaults to 10 seconds) */ private static int uploadInterval = 10; /** the offset (in days) which will used to store future events */ private static int offset = 14; /** * the base script which is written to the newly created Calendar-Events by * the GCal based presence simulation. It must contain two format markers * <code>%s</code>. The first marker represents the Item to send the command * to and the second represents the State. */ private static String executeScript = "send %s %s"; /** indicated whether this service was properly initialized */ private boolean initialized = false; public static final HttpTransport HTTP_TRANSPORT = new NetHttpTransport(); /** * Define a global instance of the JSON factory. */ public static final JsonFactory JSON_FACTORY = new JacksonFactory(); /** holds the local quartz scheduler instance */ private Scheduler scheduler; /** holds the Google Calendar entries to upload to Google */ private static Queue<Event> entries = new ConcurrentLinkedQueue<Event>(); public void activate(final BundleContext bundleContext, final Map<String, Object> config) { String offsetString = (String) config.get("offset"); if (StringUtils.isNotBlank(offsetString)) { try { offset = Integer.valueOf(offsetString); } catch (IllegalArgumentException iae) { logger.warn("couldn't parse '{}' to an integer"); } } String urlString = (String) config.get("calendar_name"); if (!StringUtils.isBlank(urlString)) { calendar_name = urlString; } else { logger.warn( "gcal-persistence:calendar_name must be configured in openhab.cfg. Calendar name or word \"primary\" MUST be specified"); } String executeScriptString = (String) config.get("executescript"); if (StringUtils.isNotBlank(executeScriptString)) { executeScript = executeScriptString; } initialized = true; scheduleUploadJob(); } public void deactivate(final int reason) { cancelAllJobs(); } /** * @{inheritDoc} */ @Override public String getName() { return "gcal"; } /** * @{inheritDoc} */ @Override public void store(Item item) { store(item, item.getName()); } /** * Creates a new Google Calendar Entry for each <code>item</code> and adds * it to the processing queue. The entries' title will either be the items * name or <code>alias</code> if it is <code>!= null</code>. * * The new Calendar Entry will contain a single command to be executed e.g.<br> * <p> * <code>send <item.name> <item.state></code> * </p> * * @param item the item which state should be persisted. * @param alias the alias under which the item should be persisted. */ @Override public void store(final Item item, final String alias) { if (initialized) { String newAlias = alias != null ? alias : item.getName(); Event event = new Event(); event.setSummary("[PresenceSimulation] " + newAlias); event.setDescription(String.format(executeScript, item.getName(), item.getState().toString())); Date now = new Date(); Date startDate = new Date(now.getTime() + 3600000L * 24 * offset); Date endDate = startDate; DateTime start = new DateTime(startDate); event.setStart(new EventDateTime().setDateTime(start)); DateTime end = new DateTime(endDate); event.setEnd(new EventDateTime().setDateTime(end)); entries.offer(event); logger.trace("added new entry '{}' for item '{}' to upload queue", event.getSummary(), item.getName()); } else { logger.debug( "GCal PresenceSimulation Service isn't initialized properly! No entries will be uploaded to your Google Calendar"); } } /** * Schedules new quartz scheduler job for uploading calendar entries to Google */ private void scheduleUploadJob() { try { scheduler = StdSchedulerFactory.getDefaultScheduler(); JobDetail job = newJob(SynchronizationJob.class).withIdentity("Upload_GCal-Entries", GCAL_SCHEDULER_GROUP) .build(); SimpleTrigger trigger = newTrigger().withIdentity("Upload_GCal-Entries", GCAL_SCHEDULER_GROUP) .withSchedule(repeatSecondlyForever(uploadInterval)).build(); scheduler.scheduleJob(job, trigger); logger.debug("Scheduled Google Calendar Upload-Job with interval '{}'", uploadInterval); } catch (SchedulerException e) { logger.warn("Could not create Google Calendar Upload-Job: {}", e.getMessage()); } } /** * Delete all quartz scheduler jobs of the group <code>Dropbox</code>. */ private void cancelAllJobs() { try { Set<JobKey> jobKeys = scheduler.getJobKeys(jobGroupEquals(GCAL_SCHEDULER_GROUP)); if (jobKeys.size() > 0) { scheduler.deleteJobs(new ArrayList<JobKey>(jobKeys)); logger.debug("Found {} Google Calendar Upload-Jobs to delete from DefaulScheduler (keys={})", jobKeys.size(), jobKeys); } else { logger.debug("Not found Google Calendar Upload to remove"); } } catch (SchedulerException e) { logger.warn("Couldn't remove Google Calendar Upload-Job: {}", e.getMessage()); } } /** * A quartz scheduler job to upload {@link Event}s to * the remote Calendar. There can be only one instance of a specific job * type running at the same time. * * @author Thomas.Eichstaedt-Engelen * @since 1.0.0 */ @DisallowConcurrentExecution public static class SynchronizationJob implements Job { @Override public void execute(JobExecutionContext context) throws JobExecutionException { logger.trace("going to upload {} calendar entries to Google now ...", entries.size()); Calendar calendarClient = null; if (entries.size() > 0) { Credential credential = GCalGoogleOAuth.getCredential(false); if (credential == null) { logger.error( "Please configure gcal:client_id/gcal:client_secret in openhab.cfg. Refer to wiki how to create client_id/client_secret pair"); } else { // set up global Calendar instance calendarClient = new com.google.api.services.calendar.Calendar.Builder(HTTP_TRANSPORT, JSON_FACTORY, credential).setApplicationName("openHABpersistence").build(); } } for (Event entry : entries) { upload(calendarClient, entry); entries.remove(entry); } } private void upload(Calendar calendarClient, Event entry) { try { long startTime = System.currentTimeMillis(); Event createdEvent = createCalendarEvent(calendarClient, entry); logger.debug("succesfully created new calendar event (title='{}', date='{}', content='{}') in {}ms", new Object[] { createdEvent.getSummary(), createdEvent.getStart().toString(), createdEvent.getDescription(), System.currentTimeMillis() - startTime }); } catch (Exception e) { logger.error("creating a new calendar entry throws an exception: {}", e.getMessage()); } } /** * Creates a new calendar entry. * * @param event the event to create in the remote calendar identified by the * full calendar feed configured in </code>openhab.cfg</code> * @return the newly created entry * @throws IOException */ private Event createCalendarEvent(Calendar calendarClient, Event event) throws IOException { if (calendarClient == null) { logger.error( "Please configure gcal:client_id/gcal:client_secret in openhab.cfg. Refer to wiki how to create client_id/client_secret pair"); } else { // set up global Calendar instance CalendarListEntry calendarID = GCalGoogleOAuth.getCalendarId(calendar_name); if (calendarID != null) { return calendarClient.events().insert(calendarID.getId(), event).execute(); } } return null; } } }