Files
LEDMatrix/src/calendar_manager.py

196 lines
7.1 KiB
Python

import os
import json
import logging
from datetime import datetime, timedelta
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from google.auth.transport.requests import Request
from googleapiclient.discovery import build
import pickle
from PIL import Image, ImageDraw, ImageFont
import numpy as np
from rgbmatrix import graphics
import pytz
class CalendarManager:
def __init__(self, matrix, canvas, config):
self.matrix = matrix
self.canvas = canvas
self.config = config
self.calendar_config = config.get('calendar', {})
self.enabled = self.calendar_config.get('enabled', False)
self.update_interval = self.calendar_config.get('update_interval', 300)
self.max_events = self.calendar_config.get('max_events', 3)
self.calendars = self.calendar_config.get('calendars', ['primary'])
self.last_update = 0
self.events = []
self.service = None
# Get display manager instance
from src.display_manager import DisplayManager
self.display_manager = DisplayManager._instance
if self.enabled:
self.authenticate()
# Display properties
self.text_color = (255, 255, 255) # White
self.date_color = (0, 255, 0) # Green
# State management
self.current_event_index = 0
def authenticate(self):
"""Authenticate with Google Calendar API."""
creds = None
token_file = self.calendar_config.get('token_file', 'token.pickle')
if os.path.exists(token_file):
with open(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())
else:
logging.error("Calendar credentials not found or invalid. Please run calendar_registration.py first.")
self.enabled = False
return
try:
self.service = build('calendar', 'v3', credentials=creds)
logging.info("Successfully authenticated with Google Calendar")
except Exception as e:
logging.error(f"Error building calendar service: {str(e)}")
self.enabled = False
def get_events(self):
"""Fetch upcoming calendar events."""
if not self.enabled or 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()
events = events_result.get('items', [])
return events
except Exception as e:
logging.error(f"Error fetching calendar events: {str(e)}")
return []
def draw_event(self, event, y_position):
"""Draw a single calendar event on the canvas."""
try:
# Get event details
summary = event.get('summary', 'No Title')
start = event.get('start', {}).get('dateTime', event.get('start', {}).get('date'))
if start:
start_time = datetime.fromisoformat(start.replace('Z', '+00:00'))
time_str = start_time.strftime('%H:%M')
else:
time_str = 'All Day'
# Calculate available width (assuming 32x32 matrix)
available_width = self.matrix.width - 2 # Leave 1 pixel margin on each side
# Draw time in green
self.display_manager.draw_text(time_str, y=y_position, color=self.date_color, small_font=True)
# Draw title, wrapping if needed
title_lines = self._wrap_text(summary, available_width)
for i, line in enumerate(title_lines):
line_y = y_position + 8 + (i * 8) # 8 pixels between lines
if line_y >= self.matrix.height - 8: # Leave space at bottom
break
self.display_manager.draw_text(line, y=line_y, color=self.text_color, small_font=True)
# Return the next available y position
return y_position + 8 + (len(title_lines) * 8)
except Exception as e:
logging.error(f"Error drawing calendar event: {str(e)}")
return y_position
def _wrap_text(self, text, max_width):
"""Wrap text to fit within max_width using small font."""
if not text:
return [""]
words = text.split()
lines = []
current_line = []
for word in words:
# Test if adding this word would exceed max_width
test_line = ' '.join(current_line + [word])
if len(test_line) * 4 <= max_width: # Assuming small font is ~4 pixels wide
current_line.append(word)
else:
if current_line:
lines.append(' '.join(current_line))
current_line = [word]
if current_line:
lines.append(' '.join(current_line))
return lines
def update(self, current_time):
"""Update calendar display if needed."""
if not self.enabled:
return
if current_time - self.last_update >= self.update_interval:
self.events = self.get_events()
self.last_update = current_time
# Clear the display
self.display_manager.clear()
# Draw each event
y_pos = 1
for event in self.events:
y_pos = self.draw_event(event, y_pos)
if y_pos >= self.matrix.height - 8: # Leave some space at the bottom
break
# Update the display
self.display_manager.update_display()
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 display
self.display_manager.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]
# Draw the event starting from the top with proper spacing
self.draw_event(event, 1) # Start 1 pixel from top
# Update the display
self.display_manager.update_display()
# Increment event index for next display
self.current_event_index += 1