mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-04-10 13:02:59 +00:00
Add Google Calendar integration with display manager. Includes calendar manager implementation, configuration updates, and registration script.
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -6,6 +6,7 @@ __pycache__/
|
||||
# Secrets
|
||||
config/config_secrets.json
|
||||
credentials.json
|
||||
token.pickle
|
||||
|
||||
# Environment
|
||||
.env
|
||||
|
||||
75
calendar_registration.py
Normal file
75
calendar_registration.py
Normal 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()
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
129
src/calendar_manager.py
Normal 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
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user