Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 22 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,9 +219,9 @@ in the pre-2014 section. For example, this is the file for Dublin:

## Pick a Calendar provider

You can use Google Calendar or Outlook Calendar to display events.
You can use up to 5 Google Calendars, up to 5 iCalendars, CalDav and Outlook Calendar to display events. Events will be sorted across the calendars according the time of start.

### Google Calendar
### Google Calendars

The script will by default get its info from your primary Google Calendar. If you need to pick a specific calendar you will need its ID. To get its ID, open up [Google Calendar](https://calendar.google.com) and go to the settings for your preferred calendar. Under the 'Integrate Calendar' section you will see a Calendar ID which looks like `xyz12345@group.calendar.google.com`. Set that value in `env.sh`

Expand Down Expand Up @@ -250,6 +250,15 @@ Copy the URL it was trying to go to (eg: http://localhost:8080/...) and in anoth
On the first screen you should see the auth flow complete, and a new `token.pickle` file appears.
The script should now be able to run in the future without prompting required.

#### Additional Google Calendars
You can also add up to 4 additional Google calendars by setting `GOOGLE_CALENDAR_ID_2` etc. in the `env.sh` file. Additional calendars will use `token_google_2.pickle` etc. for their tokens.

```bash
export GOOGLE_CALENDAR_ID_2=second_email_address@gmail.com
export GOOGLE_CALENDAR_ID_3=some_group@group.calendar.google.com
export GOOGLE_CALENDAR_ID_4=other_group@group.calendar.google.com
export GOOGLE_CALENDAR_ID_5=third_email_address@gmail.com
```

### Outlook Calendar

Expand All @@ -265,14 +274,24 @@ Copy the ID of the calendar you want, and add it to env.sh like so:

Note that if you set an Outlook Calendar ID, the Google Calendar will be ignored.

### ICS Calendar
### ICS Calendars

ICS is simple, get the ICS URL for a calendar, and place it in `env.sh`.

export ICS_CALENDAR_URL=https://calendar.google.com/calendar/ical/xxxxxxxxxxxx/xxxxxxxxxxxxxx/basic.ics

There is no username/password support.

#### Additional ICS Calendars
You can also add up to 4 additional ICS calendars by setting `ICS_CALENDAR_URL_2` etc. in the `env.sh` file.

```bash
export ICS_CALENDAR_URL_2=https://calendar.google.com/calendar/ical/ht3jlfaac5lfd6263ulfh4tql8%40group.calendar.google.com/public/basic.ics
export ICS_CALENDAR_URL_3=https://ics.calendarlabs.com/35/72771a5e/Australia_Holidays.ics
export ICS_CALENDAR_URL_4=https://calendar.google.com/calendar/ical/xxxxxxxxxxxx/xxxxxxxxxxxxxx/basic.ics
export ICS_CALENDAR_URL_5=https://calendar.google.com/calendar/ical/xxxxxxxxxxxx/xxxxxxxxxxxxxx/basic.ics
```

### CalDav Calendar

For CalDav you will need the CalDav URL, username, and password.
Expand Down
23 changes: 14 additions & 9 deletions calendar_providers/google.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,19 @@


class GoogleCalendar(BaseCalendarProvider):
def __init__(self, google_calendar_id, max_event_results, from_date, to_date):
def __init__(self, google_calendar_id, max_event_results, from_date, to_date, index):
self.max_event_results = max_event_results
self.from_date = from_date
self.to_date = to_date
self.google_calendar_id = google_calendar_id
self.index = index

def get_google_credentials(self):

google_token_pickle = 'token.pickle'
if self.index == 1:
# for backward compatibility, the first calendar will use the old token filename
google_token_pickle = "token.pickle"
else:
google_token_pickle = "token_google_{}.pickle".format(self.index)

google_api_scopes = ['https://www.googleapis.com/auth/calendar.readonly']

Expand Down Expand Up @@ -61,19 +65,19 @@ def get_google_credentials(self):

def get_calendar_events(self) -> list[CalendarEvent]:
calendar_events = []
google_calendar_pickle = 'cache_calendar.pickle'
google_calendar_pickle = 'cache_google_{}.pickle'.format(self.index)

service = build('calendar', 'v3', credentials=self.get_google_credentials(), cache_discovery=False)

events_result = None

if is_stale(os.getcwd() + "/" + google_calendar_pickle, ttl):
logging.debug("Pickle is stale, calling the Calendar API")
logging.debug("Cache of Google calendar {} is stale, calling the Calendar API".format(self.index))

# Call the Calendar API
events_result = service.events().list(
calendarId=self.google_calendar_id,
timeMin=self.from_date.isoformat() + 'Z',
timeMin=self.from_date.isoformat(),
timeZone=google_calendar_timezone,
maxResults=self.max_event_results,
singleEvents=True,
Expand All @@ -82,8 +86,9 @@ def get_calendar_events(self) -> list[CalendarEvent]:
for event in events_result.get('items', []):
if event['start'].get('date'):
is_all_day = True
start_date = datetime.datetime.strptime(event['start'].get('date'), "%Y-%m-%d")
end_date = datetime.datetime.strptime(event['end'].get('date'), "%Y-%m-%d")
# .isoformat() produces valid RFC3339 with timezone
start_date = datetime.date.fromisoformat(event['start'].get('date'))
end_date = datetime.date.fromisoformat(event['end'].get('date'))
# Google Calendar marks the 'end' of all-day-events as
# the day _after_ the last day. eg, Today's all day event ends tomorrow!
# So subtract a day
Expand All @@ -101,7 +106,7 @@ def get_calendar_events(self) -> list[CalendarEvent]:
pickle.dump(calendar_events, cal)

else:
logging.info("Found in cache")
logging.info("Google calendar {} found in cache.".format(self.index))
with open(google_calendar_pickle, 'rb') as cal:
calendar_events = pickle.load(cal)

Expand Down
9 changes: 5 additions & 4 deletions calendar_providers/ics.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,18 @@

class ICSCalendar(BaseCalendarProvider):

def __init__(self, ics_calendar_url, max_event_results, from_date, to_date):
def __init__(self, ics_calendar_url, max_event_results, from_date, to_date, index):
self.ics_calendar_url = ics_calendar_url
self.max_event_results = max_event_results
self.from_date = from_date
self.to_date = to_date
self.index = index

def get_calendar_events(self) -> list[CalendarEvent]:
calendar_events = []
ics_calendar_pickle = 'cache_ics.pickle'
ics_calendar_pickle = "cache_ics_{}.pickle".format(self.index)
if is_stale(os.getcwd() + "/" + ics_calendar_pickle, ttl):
logging.debug("Pickle is stale, fetching ICS Calendar")
logging.debug("Pickle is stale, fetching ICS Calendar {}".format(self.index))

ics_events = icalevents.icalevents.events(self.ics_calendar_url, start=self.from_date, end=self.to_date, tzinfo=get_localzone(), strict=True, sort=True)

Expand All @@ -45,7 +46,7 @@ def get_calendar_events(self) -> list[CalendarEvent]:
with open(ics_calendar_pickle, 'wb') as cal:
pickle.dump(calendar_events, cal)
else:
logging.info("Found in cache")
logging.info("ICS calendar {} found in cache".format(self.index))
with open(ics_calendar_pickle, 'rb') as cal:
calendar_events = pickle.load(cal)

Expand Down
23 changes: 23 additions & 0 deletions env.sh.sample
100755 → 100644
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,33 @@ export WEATHER_FORMAT=CELSIUS
export GOOGLE_CALENDAR_ID=primary
# If your Google Calendar is a family calendar or doesn't allow setting timezones
# export GOOGLE_CALENDAR_TIME_ZONE_NAME=Asia/Kuala_Lumpur
# Optional second Google Calendar, name credential file as "credentials_2.json":
# export GOOGLE_CALENDAR_ID_2=second google calendar ID
# export GOOGLE_CALENDAR_TIME_ZONE_NAME_2=Asia/Kuala_Lumpur
# Optional third Google Calendar, name credential file as "credentials_3.json":
# export GOOGLE_CALENDAR_ID_3=third google calendar ID
# export GOOGLE_CALENDAR_TIME_ZONE_NAME_3=Asia/Kuala_Lumpur
# Optional fourth Google Calendar, name credential file as "credentials_4.json":
# export GOOGLE_CALENDAR_ID_4=fourth google calendar ID
# export GOOGLE_CALENDAR_TIME_ZONE_NAME_4=Asia/Kuala_Lumpur
# Optional fifth Google Calendar, name credential file as "credentials_5.json":
# export GOOGLE_CALENDAR_ID_5=fifth google calendar ID
# export GOOGLE_CALENDAR_TIME_ZONE_NAME_5=Asia/Kuala_Lumpur

# Or if you use Outlook Calendar, use python3 outlook_util.py to get available Calendar IDs
# export OUTLOOK_CALENDAR_ID=AQMkAxyz...

# Or if you use ICS Calendar,
# export ICS_CALENDAR_URL=https://calendar.google.com/calendar/ical/xxxxxxxxxxxx/xxxxxxxxxxxxxx/basic.ics
# Optional second ICS calendar: example - moon quarters:
# export ICS_CALENDAR_URL_2=https://calendar.google.com/calendar/ical/ht3jlfaac5lfd6263ulfh4tql8%40group.calendar.google.com/public/basic.ics
# Optional third ICS calendar: example - Australian hollidays:
# export ICS_CALENDAR_URL_3=https://ics.calendarlabs.com/35/72771a5e/Australia_Holidays.ics
# Optional fourth ICS calendar:
# export ICS_CALENDAR_URL_4=fourth ICS calendar URL
# Optional fifth ICS calendar:
# export ICS_CALENDAR_URL_5=fifth ICS calendar URL

# Or if you have a CalDave calendar
# export CALDAV_CALENDAR_URL=https://nextcloud.example.com/remote.php/dav/principals/users/123456/
# export CALDAV_USERNAME=username
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ tinycss2==1.2.1
tzdata==2025.2
tzlocal==4.2
uritemplate==4.1.1
urllib3==2.5.0
urllib3
vobject==0.9.6.1
webencodings==0.5.1
zope.interface==5.5.1
56 changes: 40 additions & 16 deletions screen-calendar-get.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,26 @@
# note: increasing this will require updates to the SVG template to accommodate more events
max_event_results = 10

google_calendar_id = os.getenv("GOOGLE_CALENDAR_ID", "primary")
# get data from first Google calendar:
google_calendar_ids = []
google_calendar_ids.append(os.getenv("GOOGLE_CALENDAR_ID", None))
# Get data from optional additional Google calendars, up to 5:
for id in range(2, 6):
google_calendar_ids.append(os.getenv("GOOGLE_CALENDAR_ID_{}".format(id), None))

outlook_calendar_id = os.getenv("OUTLOOK_CALENDAR_ID", None)

caldav_calendar_url = os.getenv('CALDAV_CALENDAR_URL', None)
caldav_username = os.getenv("CALDAV_USERNAME", None)
caldav_password = os.getenv("CALDAV_PASSWORD", None)
caldav_calendar_id = os.getenv("CALDAV_CALENDAR_ID", None)

ics_calendar_url = os.getenv("ICS_CALENDAR_URL", None)
# get data from first ics calendar:
ics_calendar_urls = []
ics_calendar_urls.append(os.getenv("ICS_CALENDAR_URL", None))
# Get data from optional additional ics calendars, up to 5:
for id in range(2, 6):
ics_calendar_urls.append(os.getenv("ICS_CALENDAR_URL_{}".format(id), None))

ttl = float(os.getenv("CALENDAR_TTL", 1 * 60 * 60))

Expand Down Expand Up @@ -78,28 +89,41 @@ def main():

output_svg_filename = 'screen-output-weather.svg'

today_start_time = datetime.datetime.utcnow()
today_start_time = datetime.datetime.now().astimezone()
if os.getenv("CALENDAR_INCLUDE_PAST_EVENTS_FOR_TODAY", "0") == "1":
today_start_time = datetime.datetime.combine(datetime.datetime.utcnow(), datetime.datetime.min.time())
today_start_time = datetime.datetime.combine(datetime.datetime.now().astimezone(), datetime.time.min).astimezone()
oneyearlater_iso = (datetime.datetime.now().astimezone()
+ datetime.timedelta(days=365)).astimezone()

# Initiate calendar providers array:
providers = []
if outlook_calendar_id:
logging.info("Fetching Outlook Calendar Events")
provider = OutlookCalendar(outlook_calendar_id, max_event_results, today_start_time, oneyearlater_iso)
elif caldav_calendar_url:
providers.append(OutlookCalendar(outlook_calendar_id, max_event_results, today_start_time, oneyearlater_iso,))
if caldav_calendar_url:
logging.info("Fetching Caldav Calendar Events")
provider = CalDavCalendar(caldav_calendar_url, caldav_calendar_id, max_event_results,
providers.append(CalDavCalendar(caldav_calendar_url, caldav_calendar_id, max_event_results,
today_start_time, oneyearlater_iso, caldav_username, caldav_password)
elif ics_calendar_url:
logging.info("Fetching ics Calendar Events")
today_start_time = datetime.datetime.now().astimezone()
provider = ICSCalendar(ics_calendar_url, max_event_results, today_start_time, oneyearlater_iso)
else:
logging.info("Fetching Google Calendar Events")
provider = GoogleCalendar(google_calendar_id, max_event_results, today_start_time, oneyearlater_iso)

calendar_events = provider.get_calendar_events()
)
for index, ics_calendar_url in enumerate(ics_calendar_urls):
if ics_calendar_url:
logging.info("Fetching events from ICS calendar number {}".format(index + 1))
providers.append(ICSCalendar(ics_calendar_url, max_event_results, today_start_time, oneyearlater_iso, index + 1))
for index, google_calendar_id in enumerate(google_calendar_ids):
if google_calendar_id:
logging.info("Fetching events from Google calendar number {}".format(index + 1))
providers.append(GoogleCalendar(google_calendar_id, max_event_results, today_start_time, oneyearlater_iso, index + 1))

calendar_events = []
for provider in providers:
calendar_events = calendar_events + provider.get_calendar_events()
# sort events by start date: normalise date/datetime mix so comparisons work
def sort_key(event):
s = event.start
if isinstance(s, datetime.datetime):
return s if s.tzinfo else s.replace(tzinfo=datetime.timezone.utc)
return datetime.datetime(s.year, s.month, s.day, tzinfo=datetime.timezone.utc)
calendar_events = sorted(calendar_events, key=sort_key)
output_dict = get_formatted_calendar_events(calendar_events)

# XML escape for safety
Expand Down