Add Google Calendar integration with display manager. Includes calendar manager implementation, configuration updates, and registration script.

This commit is contained in:
ChuckBuilds
2025-04-21 09:01:42 -05:00
parent be178b1465
commit d30ec921ae
7 changed files with 226 additions and 4 deletions

1
.gitignore vendored
View File

@@ -6,6 +6,7 @@ __pycache__/
# Secrets
config/config_secrets.json
credentials.json
token.pickle
# Environment
.env

75
calendar_registration.py Normal file
View File

@@ -0,0 +1,75 @@
#!/usr/bin/env python3
import os
import json
from google_auth_oauthlib.flow import InstalledAppFlow
from google.oauth2.credentials import Credentials
from google.auth.transport.requests import Request
import pickle
# If modifying these scopes, delete the file token.pickle.
SCOPES = ['https://www.googleapis.com/auth/calendar.readonly']
def load_config():
with open('config/config.json', 'r') as f:
return json.load(f)
def save_credentials(creds, token_path):
# Save the credentials for the next run
with open(token_path, 'wb') as token:
pickle.dump(creds, token)
def main():
config = load_config()
calendar_config = config.get('calendar', {})
creds_file = calendar_config.get('credentials_file', 'credentials.json')
token_file = calendar_config.get('token_file', 'token.pickle')
creds = None
# The file token.pickle stores the user's access and refresh tokens
if os.path.exists(token_file):
print("Existing token found, but you may continue to generate a new one.")
choice = input("Generate new token? (y/n): ")
if choice.lower() != 'y':
print("Keeping existing token. Exiting...")
return
# If there are no (valid) credentials available, let the user log in.
if not os.path.exists(creds_file):
print(f"Error: No credentials file found at {creds_file}")
print("Please download the credentials file from Google Cloud Console")
print("1. Go to https://console.cloud.google.com")
print("2. Create a project or select existing project")
print("3. Enable the Google Calendar API")
print("4. Configure the OAuth consent screen (select TV and Limited Input Device)")
print("5. Create OAuth 2.0 credentials")
print("6. Download the credentials and save as credentials.json")
return
# Create the flow using the client secrets file from the Google API Console
flow = InstalledAppFlow.from_client_secrets_file(
creds_file, SCOPES,
# Specify TV and Limited Input Device flow
redirect_uri='urn:ietf:wg:oauth:2.0:oob'
)
# Generate URL for authorization
auth_url, _ = flow.authorization_url(prompt='consent')
print("\nPlease visit this URL to authorize this application:")
print(auth_url)
print("\nAfter authorizing, you will receive a code. Enter that code below:")
code = input("Enter the authorization code: ")
# Exchange the authorization code for credentials
flow.fetch_token(code=code)
creds = flow.credentials
# Save the credentials
save_credentials(creds, token_file)
print(f"\nCredentials saved successfully to {token_file}")
print("You can now run the LED Matrix display with calendar integration!")
if __name__ == '__main__':
main()

View File

@@ -43,9 +43,10 @@
"calendar": {
"enabled": true,
"credentials_file": "credentials.json",
"token_file": "token.json",
"token_file": "token.pickle",
"update_interval": 300,
"max_events": 3
"max_events": 3,
"calendars": ["primary"]
},
"mqtt": {
"broker": "homeassistant.local",

View File

@@ -37,7 +37,8 @@
"nhl_upcoming": 20,
"nba_live": 30,
"nba_recent": 20,
"nba_upcoming": 20
"nba_upcoming": 20,
"calendar": 30
}
},
"clock": {
@@ -67,6 +68,14 @@
"max_headlines_per_symbol": 1,
"headlines_per_rotation": 2
},
"calendar": {
"enabled": true,
"credentials_file": "credentials.json",
"token_file": "token.pickle",
"update_interval": 300,
"max_events": 3,
"calendars": ["primary"]
},
"nhl_scoreboard": {
"enabled": true,
"test_mode": false,

View File

@@ -4,3 +4,6 @@ requests==2.31.0
timezonefinder==6.2.0
geopy==2.4.1
rgbmatrix
google-auth-oauthlib==1.0.0
google-auth-httplib2==0.1.0
google-api-python-client==2.86.0

129
src/calendar_manager.py Normal file
View File

@@ -0,0 +1,129 @@
from datetime import datetime, timedelta
import pickle
import os.path
from google.oauth2.credentials import Credentials
from google.auth.transport.requests import Request
from googleapiclient.discovery import build
from rgbmatrix import graphics
import pytz
class CalendarManager:
def __init__(self, matrix, canvas, config):
self.matrix = matrix
self.canvas = canvas
self.config = config.get('calendar', {})
self.enabled = self.config.get('enabled', False)
self.update_interval = self.config.get('update_interval', 300)
self.max_events = self.config.get('max_events', 3)
self.token_file = self.config.get('token_file', 'token.pickle')
# Display properties
self.font = graphics.Font()
self.font.LoadFont("assets/fonts/7x13.bdf")
self.text_color = graphics.Color(255, 255, 255)
self.date_color = graphics.Color(0, 255, 0)
# State management
self.last_update = None
self.events = []
self.service = None
self.current_event_index = 0
# Initialize the calendar service
self._initialize_service()
def _initialize_service(self):
"""Initialize the Google Calendar service with stored credentials"""
if not os.path.exists(self.token_file):
print(f"No token file found at {self.token_file}")
print("Please run calendar_registration.py first")
self.enabled = False
return
try:
with open(self.token_file, 'rb') as token:
creds = pickle.load(token)
if not creds or not creds.valid:
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
# Save the refreshed credentials
with open(self.token_file, 'wb') as token:
pickle.dump(creds, token)
else:
print("Invalid credentials. Please run calendar_registration.py")
self.enabled = False
return
self.service = build('calendar', 'v3', credentials=creds)
except Exception as e:
print(f"Error initializing calendar service: {e}")
self.enabled = False
def _fetch_events(self):
"""Fetch upcoming events from Google Calendar"""
if not self.service:
return []
try:
now = datetime.utcnow().isoformat() + 'Z'
events_result = self.service.events().list(
calendarId='primary',
timeMin=now,
maxResults=self.max_events,
singleEvents=True,
orderBy='startTime'
).execute()
return events_result.get('items', [])
except Exception as e:
print(f"Error fetching calendar events: {e}")
return []
def update(self):
"""Update calendar events if needed"""
if not self.enabled:
return
current_time = datetime.now()
if (self.last_update is None or
(current_time - self.last_update).seconds > self.update_interval):
self.events = self._fetch_events()
self.last_update = current_time
def _format_event_time(self, event):
"""Format event time for display"""
start = event['start'].get('dateTime', event['start'].get('date'))
if 'T' in start: # DateTime
dt = datetime.fromisoformat(start.replace('Z', '+00:00'))
local_dt = dt.astimezone(pytz.local)
return local_dt.strftime("%I:%M %p")
else: # All-day event
return "All Day"
def display(self):
"""Display calendar events on the matrix"""
if not self.enabled or not self.events:
return
# Clear the canvas
self.canvas.Clear()
# Get current event to display
if self.current_event_index >= len(self.events):
self.current_event_index = 0
event = self.events[self.current_event_index]
# Display event time
time_str = self._format_event_time(event)
graphics.DrawText(self.canvas, self.font, 1, 12, self.date_color, time_str)
# Display event title (with scrolling if needed)
title = event['summary']
if len(title) > 10: # Implement scrolling for long titles
# Add scrolling logic here
pass
else:
graphics.DrawText(self.canvas, self.font, 1, 25, self.text_color, title)
# Increment event index for next display
self.current_event_index += 1

View File

@@ -5,6 +5,7 @@ from typing import Dict, Any, List, Tuple
import logging
import math
from .weather_icons import WeatherIcons
from .calendar_manager import CalendarManager
import os
# Get logger without configuring
@@ -29,6 +30,9 @@ class DisplayManager:
self._load_fonts()
logger.info("Font loading completed in %.3f seconds", time.time() - font_time)
# Initialize managers
self.calendar_manager = CalendarManager(self.matrix, self.current_canvas, self.config)
def _setup_matrix(self):
"""Initialize the RGB matrix with configuration settings."""
setup_start = time.time()