Merge pull request #535 from facetoe/google_calendar_modifications

Some modifications to Google Calendar:
This commit is contained in:
facetoe 2017-01-24 20:16:40 +08:00 committed by GitHub
commit 386bf55d59

View File

@ -1,4 +1,5 @@
import datetime import datetime
import threading
import httplib2 import httplib2
import oauth2client import oauth2client
@ -6,9 +7,10 @@ import pytz
from apiclient import discovery from apiclient import discovery
from dateutil import parser from dateutil import parser
from googleapiclient.errors import HttpError from googleapiclient.errors import HttpError
from i3pystatus import IntervalModule, logger from i3pystatus import IntervalModule, logger
from i3pystatus.core.color import ColorRangeModule from i3pystatus.core.color import ColorRangeModule
from i3pystatus.core.util import internet, require, user_open from i3pystatus.core.util import user_open, internet, require
class GoogleCalendar(IntervalModule, ColorRangeModule): class GoogleCalendar(IntervalModule, ColorRangeModule):
@ -36,77 +38,79 @@ class GoogleCalendar(IntervalModule, ColorRangeModule):
('format', 'format string'), ('format', 'format string'),
("credential_path", "Path to credentials"), ("credential_path", "Path to credentials"),
("skip_recurring", "Skip recurring events."), ("skip_recurring", "Skip recurring events."),
("update_interval", "How often (in seconds) to call the Goggle API and update events"),
("days", "Only show events between now and this many days in the future"), ("days", "Only show events between now and this many days in the future"),
("urgent_seconds", "Add urgent hint when this many seconds until event startTime"), ("urgent_seconds", "Add urgent hint when this many seconds until event startTime"),
("urgent_blink", "Whether or not to blink when the within urgent_seconds of event start"),
("start_color", "Hex or English name for start of color range, eg '#00FF00' or 'green'"), ("start_color", "Hex or English name for start of color range, eg '#00FF00' or 'green'"),
("end_color", "Hex or English name for end of color range, eg '#FF0000' or 'red'"), ("end_color", "Hex or English name for end of color range, eg '#FF0000' or 'red'"),
) )
required = ('credential_path',) required = ('credential_path',)
interval = 30
format = "{summary} ({remaining_time})" format = "{summary} ({remaining_time})"
credential_path = None credential_path = None
interval = 1
skip_recurring = True skip_recurring = True
update_interval = 60
days = 1 days = 1
urgent_seconds = 300 urgent_seconds = 300
urgent_blink = False
color = None color = None
service = None service = None
credentials = None credentials = None
display_event = None display_event = None
last_event_refresh = None
urgent_acknowledged = False
update_lock = threading.Lock()
def on_click(self, button, **kwargs): on_rightclick = 'acknowledge'
self.open_calendar() on_leftclick = 'open_calendar'
def init(self): def init(self):
self.colors = self.get_hex_color_range(self.end_color, self.start_color, self.urgent_seconds * 2) self.colors = self.get_hex_color_range(self.end_color, self.start_color, self.urgent_seconds * 2)
self.last_event_refresh = datetime.datetime.now(tz=pytz.UTC) - datetime.timedelta(seconds=self.update_interval)
self.connect_service()
@require(internet) @require(internet)
def run(self): def run(self):
if not self.service:
self.connect_service()
try:
self.display_event = self.get_next_event()
except ConnectionResetError as e:
logger.warn(e)
if self.display_event:
start_time = self.display_event['start_time']
now = datetime.datetime.now(tz=pytz.UTC) now = datetime.datetime.now(tz=pytz.UTC)
if self.should_update(now):
threading.Thread(target=self.update_display_event, args=(now,), daemon=True).start()
self.refresh_output(now)
alert_time = now + datetime.timedelta(seconds=self.urgent_seconds) def should_update(self, now):
self.display_event['remaining_time'] = str((start_time - now)).partition('.')[0] """
urgent = alert_time > start_time Whether or not we should update events.
color = self.get_color(now, start_time) """
wait_window = self.last_event_refresh + datetime.timedelta(seconds=self.update_interval)
self.output = { if self.display_event is None:
'full_text': self.format.format(**self.display_event), should_update = wait_window < now
'color': color, elif self.display_event['start_time'] < now:
'urgent': urgent should_update = True
} elif wait_window < now:
should_update = True
else: else:
self.display_event = None should_update = False
self.output = { return should_update and not self.update_lock.locked()
'full_text': "",
}
def connect_service(self): def update_display_event(self, now):
self.credentials = oauth2client.file.Storage(self.credential_path).get() """
self.service = discovery.build('calendar', 'v3', http=self.credentials.authorize(httplib2.Http())) Call the Google API and attempt to update the current event.
"""
def get_next_event(self): with self.update_lock:
for event in self.get_events(): logger.debug("Retrieving events...".format(threading.current_thread().name))
self.last_event_refresh = now
for event in self.get_events(now):
# If we don't have a dateTime just make do with a date. # If we don't have a dateTime just make do with a date.
if 'dateTime' not in event['start']: if 'dateTime' not in event['start']:
event['start_time'] = pytz.utc.localize(parser.parse(event['start']['date'])) event['start_time'] = pytz.utc.localize(parser.parse(event['start']['date']))
else: else:
event['start_time'] = parser.parse(event['start']['dateTime']) event['start_time'] = parser.parse(event['start']['dateTime'])
now = datetime.datetime.now(tz=pytz.UTC)
if 'recurringEventId' in event and self.skip_recurring: if 'recurringEventId' in event and self.skip_recurring:
continue continue
elif event['start_time'] < now: elif event['start_time'] < now:
@ -115,12 +119,53 @@ class GoogleCalendar(IntervalModule, ColorRangeModule):
# It is possible for there to be no title... # It is possible for there to be no title...
if 'summary' not in event: if 'summary' not in event:
event['summary'] = '(no title)' event['summary'] = '(no title)'
return event
def get_events(self): if self.display_event:
# If this is a new event, reset the urgent_acknowledged flag.
if self.display_event['id'] != event['id']:
self.urgent_acknowledged = False
self.display_event = event
return
self.display_event = None
def refresh_output(self, now):
"""
Build our output dict.
"""
if self.display_event:
start_time = self.display_event['start_time']
alert_time = now + datetime.timedelta(seconds=self.urgent_seconds)
self.display_event['remaining_time'] = str((start_time - now)).partition('.')[0]
urgent = self.is_urgent(alert_time, start_time, now)
color = self.get_color(now, start_time)
self.output = {
'full_text': self.format.format(**self.display_event),
'color': color,
'urgent': urgent
}
else:
self.output = {
'full_text': "",
}
def is_urgent(self, alert_time, start_time, now):
"""
Determine whether or not to set the urgent flag. If urgent_blink is set, toggles urgent flag
on and off every second.
"""
urgent = alert_time > start_time
if urgent and self.urgent_blink:
urgent = now.second % 2 == 0 and not self.urgent_acknowledged
return urgent
def get_events(self, now):
"""
Retrieve the next N events from Google.
"""
events = [] events = []
try: try:
now, later = self.get_timerange() now, later = self.get_timerange_formatted(now)
events_result = self.service.events().list( events_result = self.service.events().list(
calendarId='primary', calendarId='primary',
timeMin=now, timeMin=now,
@ -138,12 +183,12 @@ class GoogleCalendar(IntervalModule, ColorRangeModule):
raise raise
return events return events
def get_timerange(self): def get_timerange_formatted(self, now):
now = datetime.datetime.utcnow() """
Return two ISO8601 formatted date strings, one for timeMin, the other for timeMax (to be consumed by get_events)
"""
later = now + datetime.timedelta(days=self.days) later = now + datetime.timedelta(days=self.days)
now = now.isoformat() + 'Z' return now.isoformat(), later.isoformat()
later = later.isoformat() + 'Z'
return now, later
def get_color(self, now, start_time): def get_color(self, now, start_time):
seconds_to_event = (start_time - now).seconds seconds_to_event = (start_time - now).seconds
@ -151,8 +196,16 @@ class GoogleCalendar(IntervalModule, ColorRangeModule):
color = self.get_gradient(v, self.colors) color = self.get_gradient(v, self.colors)
return color return color
def connect_service(self):
logger.debug("Connecting Service..")
self.credentials = oauth2client.file.Storage(self.credential_path).get()
self.service = discovery.build('calendar', 'v3', http=self.credentials.authorize(httplib2.Http()))
def open_calendar(self): def open_calendar(self):
if self.display_event: if self.display_event:
calendar_url = self.display_event.get('htmlLink', None) calendar_url = self.display_event.get('htmlLink', None)
if calendar_url: if calendar_url:
user_open(calendar_url) user_open(calendar_url)
def acknowledge(self):
self.urgent_acknowledged = True