From 727141198e0c7a843b0da943c77e3cf3352109b2 Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Mon, 21 Apr 2025 21:51:11 -0500 Subject: [PATCH 01/40] Add YouTube display feature with channel stats and logo. Move sensitive credentials to secrets file. --- config/config.json | 7 +- config/config_secrets.template.json | 4 + src/youtube_display.py | 115 ++++++++++++++++++++++++++++ 3 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 src/youtube_display.py diff --git a/config/config.json b/config/config.json index e100938f..cdcfdc66 100644 --- a/config/config.json +++ b/config/config.json @@ -38,7 +38,8 @@ "nba_live": 30, "nba_recent": 20, "nba_upcoming": 20, - "calendar": 30 + "calendar": 30, + "youtube": 20 } }, "clock": { @@ -109,5 +110,9 @@ "nba_upcoming": true }, "live_game_duration": 30 + }, + "youtube": { + "enabled": true, + "update_interval": 3600 } } \ No newline at end of file diff --git a/config/config_secrets.template.json b/config/config_secrets.template.json index 6153736f..0a454c14 100644 --- a/config/config_secrets.template.json +++ b/config/config_secrets.template.json @@ -1,5 +1,9 @@ { "weather": { "api_key": "YOUR_OPENWEATHERMAP_API_KEY" + }, + "youtube": { + "api_key": "YOUR_YOUTUBE_API_KEY", + "channel_id": "YOUR_YOUTUBE_CHANNEL_ID" } } \ No newline at end of file diff --git a/src/youtube_display.py b/src/youtube_display.py new file mode 100644 index 00000000..d9a799a9 --- /dev/null +++ b/src/youtube_display.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python3 +import json +import time +from PIL import Image, ImageDraw, ImageFont +import requests +from rgbmatrix import RGBMatrix, RGBMatrixOptions +import os + +class YouTubeDisplay: + def __init__(self, config_path='config/config.json', secrets_path='config/config_secrets.json'): + self.config = self._load_config(config_path) + self.secrets = self._load_config(secrets_path) + self.matrix = self._setup_matrix() + self.canvas = self.matrix.CreateFrameCanvas() + self.font = ImageFont.truetype("assets/fonts/PressStart2P-Regular.ttf", 8) + self.youtube_logo = Image.open("assets/youtube_logo.png") + + def _load_config(self, config_path): + with open(config_path, 'r') as f: + return json.load(f) + + def _setup_matrix(self): + options = RGBMatrixOptions() + display_config = self.config['display']['hardware'] + + options.rows = display_config['rows'] + options.cols = display_config['cols'] + options.chain_length = display_config['chain_length'] + options.parallel = display_config['parallel'] + options.hardware_mapping = display_config['hardware_mapping'] + options.brightness = display_config['brightness'] + options.pwm_bits = display_config['pwm_bits'] + options.pwm_lsb_nanoseconds = display_config['pwm_lsb_nanoseconds'] + options.disable_hardware_pulsing = display_config['disable_hardware_pulsing'] + options.show_refresh_rate = display_config['show_refresh_rate'] + options.limit_refresh_rate_hz = display_config['limit_refresh_rate_hz'] + options.gpio_slowdown = self.config['display']['runtime']['gpio_slowdown'] + + return RGBMatrix(options=options) + + def _get_channel_stats(self, channel_id): + api_key = self.secrets['youtube']['api_key'] + url = f"https://www.googleapis.com/youtube/v3/channels?part=statistics,snippet&id={channel_id}&key={api_key}" + + try: + response = requests.get(url) + data = response.json() + if data['items']: + channel = data['items'][0] + return { + 'title': channel['snippet']['title'], + 'subscribers': int(channel['statistics']['subscriberCount']), + 'views': int(channel['statistics']['viewCount']) + } + except Exception as e: + print(f"Error fetching YouTube stats: {e}") + return None + + def _create_display(self, channel_stats): + # Create a new image with the matrix dimensions + image = Image.new('RGB', (self.matrix.width, self.matrix.height)) + draw = ImageDraw.Draw(image) + + # Resize YouTube logo to fit + logo_height = self.matrix.height // 3 + logo_width = int(self.youtube_logo.width * (logo_height / self.youtube_logo.height)) + resized_logo = self.youtube_logo.resize((logo_width, logo_height)) + + # Calculate positions + logo_x = (self.matrix.width - logo_width) // 2 + logo_y = 0 + + # Paste the logo + image.paste(resized_logo, (logo_x, logo_y)) + + # Draw channel name + channel_name = channel_stats['title'] + name_bbox = draw.textbbox((0, 0), channel_name, font=self.font) + name_width = name_bbox[2] - name_bbox[0] + name_x = (self.matrix.width - name_width) // 2 + name_y = logo_height + 5 + draw.text((name_x, name_y), channel_name, font=self.font, fill=(255, 255, 255)) + + # Draw subscriber count + subs_text = f"{channel_stats['subscribers']:,} subscribers" + subs_bbox = draw.textbbox((0, 0), subs_text, font=self.font) + subs_width = subs_bbox[2] - subs_bbox[0] + subs_x = (self.matrix.width - subs_width) // 2 + subs_y = name_y + 15 + draw.text((subs_x, subs_y), subs_text, font=self.font, fill=(255, 255, 255)) + + return image + + def run(self): + if not self.config.get('youtube', {}).get('enabled', False): + return + + channel_id = self.secrets['youtube']['channel_id'] + duration = self.config['display']['display_durations']['youtube'] + channel_stats = self._get_channel_stats(channel_id) + + if channel_stats: + display_image = self._create_display(channel_stats) + self.canvas.SetImage(display_image) + self.matrix.SwapOnVSync(self.canvas) + time.sleep(duration) + + def cleanup(self): + self.matrix.Clear() + +if __name__ == "__main__": + # Example usage + youtube_display = YouTubeDisplay() + youtube_display.run() + youtube_display.cleanup() \ No newline at end of file From 4334f533668da3d086ba3a4767fbff43a5dcfc4a Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Mon, 21 Apr 2025 21:52:57 -0500 Subject: [PATCH 02/40] Add YouTube display to display controller rotation --- src/display_controller.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/display_controller.py b/src/display_controller.py index 863e2e59..ff4c09d2 100644 --- a/src/display_controller.py +++ b/src/display_controller.py @@ -9,6 +9,7 @@ from src.stock_manager import StockManager from src.stock_news_manager import StockNewsManager from src.nhl_managers import NHLLiveManager, NHLRecentManager, NHLUpcomingManager from src.nba_managers import NBALiveManager, NBARecentManager, NBAUpcomingManager +from src.youtube_display import YouTubeDisplay # Get logger without configuring logger = logging.getLogger(__name__) @@ -33,6 +34,7 @@ class DisplayController: self.stocks = StockManager(self.config, self.display_manager) if self.config.get('stocks', {}).get('enabled', False) else None self.news = StockNewsManager(self.config, self.display_manager) if self.config.get('stock_news', {}).get('enabled', False) else None self.calendar = self.display_manager.calendar_manager if self.config.get('calendar', {}).get('enabled', False) else None + self.youtube = YouTubeDisplay() if self.config.get('youtube', {}).get('enabled', False) else None logger.info(f"Calendar Manager initialized: {'Object' if self.calendar else 'None'}") logger.info("Display modes initialized in %.3f seconds", time.time() - init_time) @@ -70,6 +72,7 @@ class DisplayController: if self.stocks: self.available_modes.append('stocks') if self.news: self.available_modes.append('stock_news') if self.calendar: self.available_modes.append('calendar') + if self.youtube: self.available_modes.append('youtube') # Add NHL display modes if enabled if nhl_enabled: @@ -110,7 +113,8 @@ class DisplayController: 'weather_daily': 15, 'stocks': 45, 'stock_news': 30, - 'calendar': 30 + 'calendar': 30, + 'youtube': 30 }) logger.info("DisplayController initialized with display_manager: %s", id(self.display_manager)) logger.info(f"Available display modes: {self.available_modes}") @@ -137,6 +141,7 @@ class DisplayController: if self.stocks: self.stocks.update_stock_data() if self.news: self.news.update_news_data() if self.calendar: self.calendar.update(time.time()) + if self.youtube: self.youtube.run() # Update NHL managers if self.nhl_live: self.nhl_live.update() @@ -380,6 +385,9 @@ class DisplayController: elif self.current_display_mode == 'nba_upcoming' and self.nba_upcoming: self.nba_upcoming.display(force_clear=self.force_clear) + elif self.current_display_mode == 'youtube' and self.youtube: + self.youtube.display() + except Exception as e: logger.error(f"Error updating display for mode {self.current_display_mode}: {e}", exc_info=True) continue From 3f1e5d0a731ea0f6df7d67d3fa615c50bf67aa3d Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Mon, 21 Apr 2025 21:59:56 -0500 Subject: [PATCH 03/40] Add text outline feature to NHL scoreboard for improved readability --- src/nhl_managers.py | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/src/nhl_managers.py b/src/nhl_managers.py index acc077bb..ee6650ee 100644 --- a/src/nhl_managers.py +++ b/src/nhl_managers.py @@ -241,6 +241,27 @@ class BaseNHLManager: fonts['status'] = ImageFont.load_default() return fonts + def _draw_text_with_outline(self, draw, text, position, font, fill=(255, 255, 255), outline_color=(0, 0, 0)): + """ + Draw text with a black outline for better readability. + + Args: + draw: ImageDraw object + text: Text to draw + position: (x, y) position to draw the text + font: Font to use + fill: Text color (default: white) + outline_color: Outline color (default: black) + """ + x, y = position + + # Draw the outline by drawing the text in black at 8 positions around the text + for dx, dy in [(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)]: + draw.text((x + dx, y + dy), text, font=font, fill=outline_color) + + # Draw the text in the specified color + draw.text((x, y), text, font=font, fill=fill) + def _load_and_resize_logo(self, team_abbrev: str) -> Optional[Image.Image]: """Load and resize a team logo, with caching.""" self.logger.debug(f"Loading logo for {team_abbrev}") @@ -430,19 +451,19 @@ class BaseNHLManager: status_width = draw.textlength(status_text, font=self.fonts['status']) status_x = (self.display_width - status_width) // 2 status_y = 2 - draw.text((status_x, status_y), status_text, font=self.fonts['status'], fill=(255, 255, 255)) + self._draw_text_with_outline(draw, status_text, (status_x, status_y), self.fonts['status']) # Calculate position for the date text (centered horizontally, below "Next Game") date_width = draw.textlength(game_date, font=self.fonts['time']) date_x = (self.display_width - date_width) // 2 date_y = center_y - 5 # Position in center - draw.text((date_x, date_y), game_date, font=self.fonts['time'], fill=(255, 255, 255)) + self._draw_text_with_outline(draw, game_date, (date_x, date_y), self.fonts['time']) # Calculate position for the time text (centered horizontally, in center) time_width = draw.textlength(game_time, font=self.fonts['time']) time_x = (self.display_width - time_width) // 2 time_y = date_y + 10 # Position below date - draw.text((time_x, time_y), game_time, font=self.fonts['time'], fill=(255, 255, 255)) + self._draw_text_with_outline(draw, game_time, (time_x, time_y), self.fonts['time']) else: # For live/final games, show scores and period/time home_score = str(game.get("home_score", "0")) @@ -453,7 +474,7 @@ class BaseNHLManager: score_width = draw.textlength(score_text, font=self.fonts['score']) score_x = (self.display_width - score_width) // 2 score_y = self.display_height - 15 - draw.text((score_x, score_y), score_text, font=self.fonts['score'], fill=(255, 255, 255)) + self._draw_text_with_outline(draw, score_text, (score_x, score_y), self.fonts['score']) # Draw period and time or Final if game.get("is_final", False): @@ -474,7 +495,7 @@ class BaseNHLManager: status_width = draw.textlength(status_text, font=self.fonts['time']) status_x = (self.display_width - status_width) // 2 status_y = 5 - draw.text((status_x, status_y), status_text, font=self.fonts['time'], fill=(255, 255, 255)) + self._draw_text_with_outline(draw, status_text, (status_x, status_y), self.fonts['time']) # Display the image self.display_manager.image.paste(main_img, (0, 0)) From 9d4d5227178618b2e749f6494a48d76e2b5e854f Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Mon, 21 Apr 2025 22:02:25 -0500 Subject: [PATCH 04/40] turned off youtube until ready to test --- config/config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/config.json b/config/config.json index cdcfdc66..ccfd3463 100644 --- a/config/config.json +++ b/config/config.json @@ -112,7 +112,7 @@ "live_game_duration": 30 }, "youtube": { - "enabled": true, + "enabled": false, "update_interval": 3600 } } \ No newline at end of file From 9f07b235ed395ba14b1f169eddd22de07c748d32 Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Mon, 21 Apr 2025 22:08:00 -0500 Subject: [PATCH 05/40] Refactor calendar manager to work like other display managers --- src/display_controller.py | 3 ++- src/display_manager.py | 3 +-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/display_controller.py b/src/display_controller.py index ff4c09d2..6fec0712 100644 --- a/src/display_controller.py +++ b/src/display_controller.py @@ -10,6 +10,7 @@ from src.stock_news_manager import StockNewsManager from src.nhl_managers import NHLLiveManager, NHLRecentManager, NHLUpcomingManager from src.nba_managers import NBALiveManager, NBARecentManager, NBAUpcomingManager from src.youtube_display import YouTubeDisplay +from src.calendar_manager import CalendarManager # Get logger without configuring logger = logging.getLogger(__name__) @@ -33,7 +34,7 @@ class DisplayController: self.weather = WeatherManager(self.config, self.display_manager) if self.config.get('weather', {}).get('enabled', False) else None self.stocks = StockManager(self.config, self.display_manager) if self.config.get('stocks', {}).get('enabled', False) else None self.news = StockNewsManager(self.config, self.display_manager) if self.config.get('stock_news', {}).get('enabled', False) else None - self.calendar = self.display_manager.calendar_manager if self.config.get('calendar', {}).get('enabled', False) else None + self.calendar = CalendarManager(self.display_manager.matrix, self.display_manager.current_canvas, self.config) if self.config.get('calendar', {}).get('enabled', False) else None self.youtube = YouTubeDisplay() if self.config.get('youtube', {}).get('enabled', False) else None logger.info(f"Calendar Manager initialized: {'Object' if self.calendar else 'None'}") logger.info("Display modes initialized in %.3f seconds", time.time() - init_time) diff --git a/src/display_manager.py b/src/display_manager.py index 357cedbb..a39a6fda 100644 --- a/src/display_manager.py +++ b/src/display_manager.py @@ -5,7 +5,6 @@ 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 @@ -31,7 +30,7 @@ class DisplayManager: 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) + # Calendar manager is now initialized by DisplayController def _setup_matrix(self): """Initialize the RGB matrix with configuration settings.""" From df0af3794804acfc28121ea04330670fba0a17fc Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Tue, 22 Apr 2025 10:01:12 -0500 Subject: [PATCH 06/40] Add comprehensive logging to calendar manager for debugging --- src/calendar_manager.py | 67 +++++++++++++++++++++++++---------------- 1 file changed, 41 insertions(+), 26 deletions(-) diff --git a/src/calendar_manager.py b/src/calendar_manager.py index 9479a27f..85d16647 100644 --- a/src/calendar_manager.py +++ b/src/calendar_manager.py @@ -13,8 +13,12 @@ from rgbmatrix import graphics import pytz from src.config_manager import ConfigManager +# Get logger without configuring +logger = logging.getLogger(__name__) + class CalendarManager: def __init__(self, matrix, canvas, config): + logger.info("Initializing CalendarManager") self.matrix = matrix self.canvas = canvas self.config = config @@ -27,6 +31,8 @@ class CalendarManager: self.events = [] self.service = None + logger.info(f"Calendar configuration: enabled={self.enabled}, update_interval={self.update_interval}, max_events={self.max_events}, calendars={self.calendars}") + # Get display manager instance from src.display_manager import DisplayManager self.display_manager = DisplayManager._instance @@ -53,27 +59,33 @@ class CalendarManager: def authenticate(self): """Authenticate with Google Calendar API.""" + logger.info("Starting calendar authentication") creds = None token_file = self.calendar_config.get('token_file', 'token.pickle') if os.path.exists(token_file): + logger.info(f"Loading credentials from {token_file}") with open(token_file, 'rb') as token: creds = pickle.load(token) - + if not creds or not creds.valid: + logger.info("Credentials not found or invalid") if creds and creds.expired and creds.refresh_token: + logger.info("Refreshing expired credentials") 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 + logger.info("Requesting new credentials") + flow = InstalledAppFlow.from_client_secrets_file( + self.calendar_config.get('credentials_file', 'credentials.json'), + ['https://www.googleapis.com/auth/calendar.readonly']) + creds = flow.run_local_server(port=0) + + logger.info(f"Saving credentials to {token_file}") + with open(token_file, 'wb') as token: + pickle.dump(creds, token) + + self.service = build('calendar', 'v3', credentials=creds) + logger.info("Calendar service built successfully") def get_events(self): """Fetch upcoming calendar events.""" @@ -99,6 +111,7 @@ class CalendarManager: def draw_event(self, event, y_start=1): """Draw a single calendar event on the canvas. Returns True on success, False on error.""" try: + logger.debug(f"Drawing event: {event.get('summary', 'No title')}") # Get event details summary = event.get('summary', 'No Title') time_str = self._format_event_time(event) @@ -139,7 +152,7 @@ class CalendarManager: return True # Return True on successful drawing except Exception as e: - logging.error(f"Error drawing calendar event: {str(e)}", exc_info=True) + logger.error(f"Error drawing calendar event: {str(e)}", exc_info=True) return False # Return False on error def _wrap_text(self, text, max_width, font): @@ -178,19 +191,21 @@ class CalendarManager: def update(self, current_time): """Update calendar events if needed.""" if not self.enabled: + logger.debug("Calendar manager is disabled, skipping update") return - # Only fetch new events if the update interval has passed - if current_time - self.last_update >= self.update_interval: - logging.info("Fetching new calendar events...") + if current_time - self.last_update > self.update_interval: + logger.info("Updating calendar events") self.events = self.get_events() self.last_update = current_time if not self.events: - logging.info("No upcoming calendar events found.") + logger.info("No upcoming calendar events found.") else: - logging.info(f"Fetched {len(self.events)} calendar events.") + logger.info(f"Fetched {len(self.events)} calendar events.") # Reset index if events change self.current_event_index = 0 + else: + logger.debug("Skipping calendar update - not enough time has passed") def _format_event_date(self, event): """Format event date for display""" @@ -229,14 +244,13 @@ class CalendarManager: def display(self): """Display the current calendar event on the matrix""" - logging.debug(f"CalendarManager display called. Enabled: {self.enabled}, Events count: {len(self.events) if self.events is not None else 'None'}") if not self.enabled: - logging.debug("CalendarManager display returning because not enabled.") + logger.debug("Calendar manager is disabled, skipping display") return if not self.events: # Display "No Events" message if the list is empty - logging.info("--> CalendarManager: Attempting to draw DEBUG (no events).") + logger.info("--> CalendarManager: Attempting to draw DEBUG (no events).") self.display_manager.clear() self.display_manager.draw_text("Calendar DEBUG", small_font=True, color=self.text_color) self.display_manager.update_display() @@ -246,10 +260,10 @@ class CalendarManager: if self.current_event_index >= len(self.events): self.current_event_index = 0 # Wrap around event_to_display = self.events[self.current_event_index] - logging.debug(f"CalendarManager displaying event index {self.current_event_index}: {event_to_display.get('summary')}") + logger.debug(f"CalendarManager displaying event index {self.current_event_index}: {event_to_display.get('summary')}") # Clear the display before drawing the current event - logging.debug("CalendarManager clearing display for event.") + logger.debug("CalendarManager clearing display for event.") self.display_manager.clear() # Draw the event @@ -258,19 +272,20 @@ class CalendarManager: if draw_successful: # Update the display self.display_manager.update_display() - logging.debug("CalendarManager event display updated.") + logger.debug("CalendarManager event display updated.") else: # Draw failed (error logged in draw_event), show debug message - logging.info("--> CalendarManager: Attempting to draw DEBUG (draw_event failed).") + logger.info("--> CalendarManager: Attempting to draw DEBUG (draw_event failed).") self.display_manager.clear() # Clear any partial drawing self.display_manager.draw_text("Calendar DEBUG", small_font=True, color=self.text_color) self.display_manager.update_display() def advance_event(self): """Advance to the next event. Called by DisplayManager when calendar display time is up.""" - if not self.events: + if not self.enabled: + logger.debug("Calendar manager is disabled, skipping event advance") return self.current_event_index += 1 if self.current_event_index >= len(self.events): self.current_event_index = 0 - logging.debug(f"CalendarManager advanced to event index {self.current_event_index}") \ No newline at end of file + logger.debug(f"CalendarManager advanced to event index {self.current_event_index}") \ No newline at end of file From d1456329cd65c00c439543d14bf2908df95a360b Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Tue, 22 Apr 2025 10:05:18 -0500 Subject: [PATCH 07/40] Configure calendar manager logger to show DEBUG messages --- src/calendar_manager.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/calendar_manager.py b/src/calendar_manager.py index 85d16647..dd081fc7 100644 --- a/src/calendar_manager.py +++ b/src/calendar_manager.py @@ -13,8 +13,9 @@ from rgbmatrix import graphics import pytz from src.config_manager import ConfigManager -# Get logger without configuring +# Configure logger for this module logger = logging.getLogger(__name__) +logger.setLevel(logging.DEBUG) # Set to DEBUG to see all messages class CalendarManager: def __init__(self, matrix, canvas, config): @@ -43,11 +44,13 @@ class CalendarManager: try: self.timezone = pytz.timezone(timezone_str) except pytz.UnknownTimeZoneError: - logging.warning(f"Unknown timezone '{timezone_str}' in config, defaulting to UTC.") + logger.warning(f"Unknown timezone '{timezone_str}' in config, defaulting to UTC.") self.timezone = pytz.utc if self.enabled: self.authenticate() + else: + logger.warning("Calendar manager is disabled in configuration") # Display properties self.text_color = (255, 255, 255) # White From 969b8ceb7becad5e770723d8e3ed701fcaa08952 Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Tue, 22 Apr 2025 10:09:21 -0500 Subject: [PATCH 08/40] Reduce frequency of calendar manager debug messages to every 5 seconds --- src/calendar_manager.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/calendar_manager.py b/src/calendar_manager.py index dd081fc7..1fa655bd 100644 --- a/src/calendar_manager.py +++ b/src/calendar_manager.py @@ -29,6 +29,7 @@ class CalendarManager: self.max_events = self.calendar_config.get('max_events', 3) self.calendars = self.calendar_config.get('calendars', ['birthdays']) self.last_update = 0 + self.last_debug_log = 0 # Add timestamp for debug message throttling self.events = [] self.service = None @@ -208,7 +209,10 @@ class CalendarManager: # Reset index if events change self.current_event_index = 0 else: - logger.debug("Skipping calendar update - not enough time has passed") + # Only log debug message every 5 seconds + if current_time - self.last_debug_log > 5: + logger.debug("Skipping calendar update - not enough time has passed") + self.last_debug_log = current_time def _format_event_date(self, event): """Format event date for display""" From 3ee59b915ca4ba23476ed5d3a847792e0e20f7d2 Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Tue, 22 Apr 2025 10:17:17 -0500 Subject: [PATCH 09/40] Add detailed logging to calendar draw_event and add display delay to prevent immediate clearing --- src/calendar_manager.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/calendar_manager.py b/src/calendar_manager.py index 1fa655bd..72a9fbcd 100644 --- a/src/calendar_manager.py +++ b/src/calendar_manager.py @@ -12,6 +12,7 @@ import numpy as np from rgbmatrix import graphics import pytz from src.config_manager import ConfigManager +import time # Configure logger for this module logger = logging.getLogger(__name__) @@ -121,12 +122,15 @@ class CalendarManager: time_str = self._format_event_time(event) date_str = self._format_event_date(event) + logger.debug(f"Event details - Date: {date_str}, Time: {time_str}, Summary: {summary}") + # Use display manager's font for wrapping font = self.display_manager.small_font available_width = self.display_manager.matrix.width - 4 # Leave 2 pixel margin on each side # Wrap title text title_lines = self._wrap_text(summary, available_width, font) + logger.debug(f"Wrapped title into {len(title_lines)} lines: {title_lines}") # Calculate total height needed date_height = 8 # Approximate height for date string @@ -138,18 +142,23 @@ class CalendarManager: # Calculate starting y position to center vertically y_pos = (self.display_manager.matrix.height - total_height) // 2 y_pos = max(1, y_pos) # Ensure it doesn't start above the top edge + logger.debug(f"Starting y position: {y_pos}, Total height: {total_height}") # Draw date in grey + logger.debug(f"Drawing date at y={y_pos}: {date_str}") self.display_manager.draw_text(date_str, y=y_pos, color=self.date_color, small_font=True) y_pos += date_height + 2 # Move down for the time # Draw time in green + logger.debug(f"Drawing time at y={y_pos}: {time_str}") self.display_manager.draw_text(time_str, y=y_pos, color=self.time_color, small_font=True) y_pos += time_height + 2 # Move down for the title # Draw title lines - for line in title_lines: + for i, line in enumerate(title_lines): + logger.debug(f"Drawing title line {i+1} at y={y_pos}: {line}") if y_pos >= self.display_manager.matrix.height - 8: # Stop if we run out of space + logger.debug("Stopping title drawing - reached bottom of display") break self.display_manager.draw_text(line, y=y_pos, color=self.text_color, small_font=True) y_pos += 8 + 2 # Move down for the next line, add 2px spacing @@ -277,6 +286,8 @@ class CalendarManager: draw_successful = self.draw_event(event_to_display) if draw_successful: + # Add a small delay to ensure the content stays visible + time.sleep(0.1) # 100ms delay # Update the display self.display_manager.update_display() logger.debug("CalendarManager event display updated.") From 2a70361ded82497821a52aa6acf2dfcfbe81d6f4 Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Tue, 22 Apr 2025 10:24:12 -0500 Subject: [PATCH 10/40] Add display delay to prevent rapid cycling and ensure content stays visible --- src/display_controller.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/display_controller.py b/src/display_controller.py index 6fec0712..eec30f77 100644 --- a/src/display_controller.py +++ b/src/display_controller.py @@ -342,6 +342,7 @@ class DisplayController: self.nba_live.display(force_clear=self.force_clear) self.force_clear = False + time.sleep(0.1) # Small delay to prevent CPU overuse continue # Skip the rest of the loop to stay on live games # Only proceed with mode switching if no live games @@ -352,8 +353,6 @@ class DisplayController: logger.info(f"Switching to: {self.current_display_mode}") self.force_clear = True self.last_switch = current_time - if self.current_display_mode != 'calendar' and self.calendar: - self.calendar.advance_event() # Display current mode frame (only for non-live modes) try: @@ -394,6 +393,8 @@ class DisplayController: continue self.force_clear = False + # Add a small delay to prevent CPU overuse and ensure display stays visible + time.sleep(0.1) except KeyboardInterrupt: logger.info("Display controller stopped by user") From d8a9ac5222755204a4bdc7311cfc937f0a8210ab Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Tue, 22 Apr 2025 10:27:57 -0500 Subject: [PATCH 11/40] remove .1s sleep that slows down the scrolling --- src/display_controller.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/display_controller.py b/src/display_controller.py index eec30f77..55097617 100644 --- a/src/display_controller.py +++ b/src/display_controller.py @@ -342,7 +342,6 @@ class DisplayController: self.nba_live.display(force_clear=self.force_clear) self.force_clear = False - time.sleep(0.1) # Small delay to prevent CPU overuse continue # Skip the rest of the loop to stay on live games # Only proceed with mode switching if no live games From 792bca625fd74e9cb495c801af2e088f690bda5d Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Tue, 22 Apr 2025 10:30:43 -0500 Subject: [PATCH 12/40] remove .1s sleep that slows down the scrolling --- src/display_controller.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/display_controller.py b/src/display_controller.py index 55097617..c24de9b3 100644 --- a/src/display_controller.py +++ b/src/display_controller.py @@ -392,8 +392,7 @@ class DisplayController: continue self.force_clear = False - # Add a small delay to prevent CPU overuse and ensure display stays visible - time.sleep(0.1) + except KeyboardInterrupt: logger.info("Display controller stopped by user") From aa4ccf5bb39d39c2448c31650e1723095a09d62d Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Tue, 22 Apr 2025 10:35:29 -0500 Subject: [PATCH 13/40] fix: prevent calendar display flashing by removing unnecessary delay and improving display update logic --- src/calendar_manager.py | 24 ++++++++++++------------ src/display_controller.py | 2 +- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/calendar_manager.py b/src/calendar_manager.py index 72a9fbcd..84a83537 100644 --- a/src/calendar_manager.py +++ b/src/calendar_manager.py @@ -258,7 +258,7 @@ class CalendarManager: logging.error(f"Could not parse time string: {start} - {e}") return "Invalid Time" - def display(self): + def display(self, force_clear=False): """Display the current calendar event on the matrix""" if not self.enabled: logger.debug("Calendar manager is disabled, skipping display") @@ -266,9 +266,10 @@ class CalendarManager: if not self.events: # Display "No Events" message if the list is empty - logger.info("--> CalendarManager: Attempting to draw DEBUG (no events).") - self.display_manager.clear() - self.display_manager.draw_text("Calendar DEBUG", small_font=True, color=self.text_color) + logger.debug("No calendar events to display") + if force_clear: + self.display_manager.clear() + self.display_manager.draw_text("No Events", small_font=True, color=self.text_color) self.display_manager.update_display() return @@ -278,24 +279,23 @@ class CalendarManager: event_to_display = self.events[self.current_event_index] logger.debug(f"CalendarManager displaying event index {self.current_event_index}: {event_to_display.get('summary')}") - # Clear the display before drawing the current event - logger.debug("CalendarManager clearing display for event.") - self.display_manager.clear() + # Only clear if forced or if this is a new event + if force_clear: + self.display_manager.clear() # Draw the event draw_successful = self.draw_event(event_to_display) if draw_successful: - # Add a small delay to ensure the content stays visible - time.sleep(0.1) # 100ms delay # Update the display self.display_manager.update_display() logger.debug("CalendarManager event display updated.") else: # Draw failed (error logged in draw_event), show debug message - logger.info("--> CalendarManager: Attempting to draw DEBUG (draw_event failed).") - self.display_manager.clear() # Clear any partial drawing - self.display_manager.draw_text("Calendar DEBUG", small_font=True, color=self.text_color) + logger.warning("Failed to draw calendar event") + if force_clear: + self.display_manager.clear() + self.display_manager.draw_text("Calendar Error", small_font=True, color=self.text_color) self.display_manager.update_display() def advance_event(self): diff --git a/src/display_controller.py b/src/display_controller.py index c24de9b3..a6983482 100644 --- a/src/display_controller.py +++ b/src/display_controller.py @@ -372,7 +372,7 @@ class DisplayController: self.news.display_news() elif self.current_display_mode == 'calendar' and self.calendar: - self.calendar.display() + self.calendar.display(force_clear=self.force_clear) elif self.current_display_mode == 'nhl_recent' and self.nhl_recent: self.nhl_recent.display(force_clear=self.force_clear) From a33adf6338029f67ca9f8b9d9abecbbdd7f59587 Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Tue, 22 Apr 2025 10:44:01 -0500 Subject: [PATCH 14/40] refactor: reduce calendar manager logging spam while maintaining important display info --- src/calendar_manager.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/calendar_manager.py b/src/calendar_manager.py index 84a83537..8a6769a6 100644 --- a/src/calendar_manager.py +++ b/src/calendar_manager.py @@ -116,14 +116,18 @@ class CalendarManager: def draw_event(self, event, y_start=1): """Draw a single calendar event on the canvas. Returns True on success, False on error.""" try: - logger.debug(f"Drawing event: {event.get('summary', 'No title')}") + # Only log event details at INFO level when first drawing + if self.current_event_index == 0: + logger.info(f"Drawing event: {event.get('summary', 'No title')}") + logger.info(f"Event details - Date: {self._format_event_date(event)}, Time: {self._format_event_time(event)}, Summary: {event.get('summary', 'No Title')}") + else: + logger.debug(f"Drawing event: {event.get('summary', 'No title')}") + # Get event details summary = event.get('summary', 'No Title') time_str = self._format_event_time(event) date_str = self._format_event_date(event) - logger.debug(f"Event details - Date: {date_str}, Time: {time_str}, Summary: {summary}") - # Use display manager's font for wrapping font = self.display_manager.small_font available_width = self.display_manager.matrix.width - 4 # Leave 2 pixel margin on each side @@ -277,7 +281,12 @@ class CalendarManager: if self.current_event_index >= len(self.events): self.current_event_index = 0 # Wrap around event_to_display = self.events[self.current_event_index] - logger.debug(f"CalendarManager displaying event index {self.current_event_index}: {event_to_display.get('summary')}") + + # Only log at INFO level when switching to calendar or when force_clear is True + if force_clear: + logger.info(f"CalendarManager displaying event index {self.current_event_index}: {event_to_display.get('summary')}") + else: + logger.debug(f"CalendarManager displaying event index {self.current_event_index}: {event_to_display.get('summary')}") # Only clear if forced or if this is a new event if force_clear: From f303ea69b5d976fcaa379ad3b99c68f2c8f66580 Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Tue, 22 Apr 2025 10:47:54 -0500 Subject: [PATCH 15/40] Refactor YouTubeDisplay to use shared DisplayManager and follow consistent display pattern --- src/display_controller.py | 6 +++--- src/youtube_display.py | 42 +++++++++++++++++++++++++++++---------- 2 files changed, 34 insertions(+), 14 deletions(-) diff --git a/src/display_controller.py b/src/display_controller.py index a6983482..c7aa0ed0 100644 --- a/src/display_controller.py +++ b/src/display_controller.py @@ -35,7 +35,7 @@ class DisplayController: self.stocks = StockManager(self.config, self.display_manager) if self.config.get('stocks', {}).get('enabled', False) else None self.news = StockNewsManager(self.config, self.display_manager) if self.config.get('stock_news', {}).get('enabled', False) else None self.calendar = CalendarManager(self.display_manager.matrix, self.display_manager.current_canvas, self.config) if self.config.get('calendar', {}).get('enabled', False) else None - self.youtube = YouTubeDisplay() if self.config.get('youtube', {}).get('enabled', False) else None + self.youtube = YouTubeDisplay(self.display_manager, self.config_manager.config_path, self.config_manager.secrets_path) if self.config.get('youtube', {}).get('enabled', False) else None logger.info(f"Calendar Manager initialized: {'Object' if self.calendar else 'None'}") logger.info("Display modes initialized in %.3f seconds", time.time() - init_time) @@ -142,7 +142,7 @@ class DisplayController: if self.stocks: self.stocks.update_stock_data() if self.news: self.news.update_news_data() if self.calendar: self.calendar.update(time.time()) - if self.youtube: self.youtube.run() + if self.youtube: self.youtube.update() # Update NHL managers if self.nhl_live: self.nhl_live.update() @@ -385,7 +385,7 @@ class DisplayController: self.nba_upcoming.display(force_clear=self.force_clear) elif self.current_display_mode == 'youtube' and self.youtube: - self.youtube.display() + self.youtube.display(force_clear=self.force_clear) except Exception as e: logger.error(f"Error updating display for mode {self.current_display_mode}: {e}", exc_info=True) diff --git a/src/youtube_display.py b/src/youtube_display.py index d9a799a9..e2abbc27 100644 --- a/src/youtube_display.py +++ b/src/youtube_display.py @@ -1,19 +1,28 @@ #!/usr/bin/env python3 import json import time +import logging from PIL import Image, ImageDraw, ImageFont import requests from rgbmatrix import RGBMatrix, RGBMatrixOptions import os +from typing import Dict, Any + +# Get logger without configuring +logger = logging.getLogger(__name__) class YouTubeDisplay: - def __init__(self, config_path='config/config.json', secrets_path='config/config_secrets.json'): + def __init__(self, display_manager, config_path='config/config.json', secrets_path='config/config_secrets.json'): self.config = self._load_config(config_path) self.secrets = self._load_config(secrets_path) self.matrix = self._setup_matrix() self.canvas = self.matrix.CreateFrameCanvas() self.font = ImageFont.truetype("assets/fonts/PressStart2P-Regular.ttf", 8) self.youtube_logo = Image.open("assets/youtube_logo.png") + self.display_manager = display_manager + self.last_update = 0 + self.update_interval = self.config.get('youtube', {}).get('update_interval', 300) # Default 5 minutes + self.channel_stats = None def _load_config(self, config_path): with open(config_path, 'r') as f: @@ -53,7 +62,7 @@ class YouTubeDisplay: 'views': int(channel['statistics']['viewCount']) } except Exception as e: - print(f"Error fetching YouTube stats: {e}") + logger.error(f"Error fetching YouTube stats: {e}") return None def _create_display(self, channel_stats): @@ -91,19 +100,30 @@ class YouTubeDisplay: return image - def run(self): + def update(self): + """Update YouTube channel stats if needed.""" + current_time = time.time() + if current_time - self.last_update >= self.update_interval: + channel_id = self.secrets['youtube']['channel_id'] + self.channel_stats = self._get_channel_stats(channel_id) + self.last_update = current_time + + def display(self, force_clear: bool = False): + """Display YouTube channel stats.""" if not self.config.get('youtube', {}).get('enabled', False): return - channel_id = self.secrets['youtube']['channel_id'] - duration = self.config['display']['display_durations']['youtube'] - channel_stats = self._get_channel_stats(channel_id) - - if channel_stats: - display_image = self._create_display(channel_stats) + if not self.channel_stats: + self.update() + + if self.channel_stats: + if force_clear: + self.matrix.Clear() + + display_image = self._create_display(self.channel_stats) self.canvas.SetImage(display_image) self.matrix.SwapOnVSync(self.canvas) - time.sleep(duration) + time.sleep(self.update_interval) def cleanup(self): self.matrix.Clear() @@ -111,5 +131,5 @@ class YouTubeDisplay: if __name__ == "__main__": # Example usage youtube_display = YouTubeDisplay() - youtube_display.run() + youtube_display.display() youtube_display.cleanup() \ No newline at end of file From 0410374dcdaa22bd2724e061a0f330ffeeddaeac Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Tue, 22 Apr 2025 10:49:14 -0500 Subject: [PATCH 16/40] Reduce logging noise by changing calendar manager log level to INFO --- src/calendar_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calendar_manager.py b/src/calendar_manager.py index 8a6769a6..f0ef43de 100644 --- a/src/calendar_manager.py +++ b/src/calendar_manager.py @@ -16,7 +16,7 @@ import time # Configure logger for this module logger = logging.getLogger(__name__) -logger.setLevel(logging.DEBUG) # Set to DEBUG to see all messages +logger.setLevel(logging.INFO) # Set to INFO to reduce noise class CalendarManager: def __init__(self, matrix, canvas, config): From eed1389b7d3b7a8b7fea5f9fd029b3ff6b789e96 Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Tue, 22 Apr 2025 10:52:00 -0500 Subject: [PATCH 17/40] Reduce calendar manager logging noise by only logging event details on changes --- src/calendar_manager.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/calendar_manager.py b/src/calendar_manager.py index f0ef43de..62b465d2 100644 --- a/src/calendar_manager.py +++ b/src/calendar_manager.py @@ -61,6 +61,7 @@ class CalendarManager: # State management self.current_event_index = 0 + self.force_clear = False def authenticate(self): """Authenticate with Google Calendar API.""" @@ -116,8 +117,8 @@ class CalendarManager: def draw_event(self, event, y_start=1): """Draw a single calendar event on the canvas. Returns True on success, False on error.""" try: - # Only log event details at INFO level when first drawing - if self.current_event_index == 0: + # Only log event details at INFO level when first drawing or when force_clear is True + if self.current_event_index == 0 or self.force_clear: logger.info(f"Drawing event: {event.get('summary', 'No title')}") logger.info(f"Event details - Date: {self._format_event_date(event)}, Time: {self._format_event_time(event)}, Summary: {event.get('summary', 'No Title')}") else: @@ -282,6 +283,9 @@ class CalendarManager: self.current_event_index = 0 # Wrap around event_to_display = self.events[self.current_event_index] + # Set force_clear flag for logging + self.force_clear = force_clear + # Only log at INFO level when switching to calendar or when force_clear is True if force_clear: logger.info(f"CalendarManager displaying event index {self.current_event_index}: {event_to_display.get('summary')}") From f4662086ddaac45305b5eecc28aa73a5eda84fab Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Tue, 22 Apr 2025 10:54:54 -0500 Subject: [PATCH 18/40] Fix calendar event iteration by advancing to next event after display --- src/calendar_manager.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/calendar_manager.py b/src/calendar_manager.py index 62b465d2..0b521c54 100644 --- a/src/calendar_manager.py +++ b/src/calendar_manager.py @@ -303,6 +303,9 @@ class CalendarManager: # Update the display self.display_manager.update_display() logger.debug("CalendarManager event display updated.") + + # Advance to next event for next display + self.advance_event() else: # Draw failed (error logged in draw_event), show debug message logger.warning("Failed to draw calendar event") From 27d004ff231a5ff1684337498f43aa6e93e284ec Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Tue, 22 Apr 2025 10:58:32 -0500 Subject: [PATCH 19/40] Further reduce calendar manager logging noise by only logging at INFO level when switching to calendar display --- src/calendar_manager.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/calendar_manager.py b/src/calendar_manager.py index 0b521c54..a94136a4 100644 --- a/src/calendar_manager.py +++ b/src/calendar_manager.py @@ -117,9 +117,9 @@ class CalendarManager: def draw_event(self, event, y_start=1): """Draw a single calendar event on the canvas. Returns True on success, False on error.""" try: - # Only log event details at INFO level when first drawing or when force_clear is True - if self.current_event_index == 0 or self.force_clear: - logger.info(f"Drawing event: {event.get('summary', 'No title')}") + # Only log event details at INFO level when first switching to calendar display + if self.force_clear: + logger.info(f"CalendarManager displaying event: {event.get('summary', 'No title')}") logger.info(f"Event details - Date: {self._format_event_date(event)}, Time: {self._format_event_time(event)}, Summary: {event.get('summary', 'No Title')}") else: logger.debug(f"Drawing event: {event.get('summary', 'No title')}") From 8a550cf5b1211f4531966811e427b792a00c13ea Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Tue, 22 Apr 2025 11:06:50 -0500 Subject: [PATCH 20/40] Fix calendar display overlapping by always clearing display before drawing new content --- src/calendar_manager.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/calendar_manager.py b/src/calendar_manager.py index a94136a4..cbf852e2 100644 --- a/src/calendar_manager.py +++ b/src/calendar_manager.py @@ -269,11 +269,12 @@ class CalendarManager: logger.debug("Calendar manager is disabled, skipping display") return + # Always clear the display before drawing new content + self.display_manager.clear() + if not self.events: # Display "No Events" message if the list is empty logger.debug("No calendar events to display") - if force_clear: - self.display_manager.clear() self.display_manager.draw_text("No Events", small_font=True, color=self.text_color) self.display_manager.update_display() return @@ -291,10 +292,6 @@ class CalendarManager: logger.info(f"CalendarManager displaying event index {self.current_event_index}: {event_to_display.get('summary')}") else: logger.debug(f"CalendarManager displaying event index {self.current_event_index}: {event_to_display.get('summary')}") - - # Only clear if forced or if this is a new event - if force_clear: - self.display_manager.clear() # Draw the event draw_successful = self.draw_event(event_to_display) @@ -309,8 +306,6 @@ class CalendarManager: else: # Draw failed (error logged in draw_event), show debug message logger.warning("Failed to draw calendar event") - if force_clear: - self.display_manager.clear() self.display_manager.draw_text("Calendar Error", small_font=True, color=self.text_color) self.display_manager.update_display() From 4b2270c80fc452244162d1c4434543ec51cfce1a Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Tue, 22 Apr 2025 11:11:52 -0500 Subject: [PATCH 21/40] Fix calendar text wrapping by using textbbox instead of textlength --- src/calendar_manager.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/calendar_manager.py b/src/calendar_manager.py index cbf852e2..102496c0 100644 --- a/src/calendar_manager.py +++ b/src/calendar_manager.py @@ -184,8 +184,9 @@ class CalendarManager: for word in words: test_line = ' '.join(current_line + [word]) - # Use textlength for accurate width calculation - text_width = self.display_manager.draw.textlength(test_line, font=font) + # Use textbbox for accurate width calculation + bbox = self.display_manager.draw.textbbox((0, 0), test_line, font=font) + text_width = bbox[2] - bbox[0] if text_width <= max_width: current_line.append(word) @@ -197,7 +198,8 @@ class CalendarManager: lines.append(' '.join(current_line)) current_line = [word] # Recheck if the new line with just this word is too long - if self.display_manager.draw.textlength(word, font=font) > max_width: + bbox = self.display_manager.draw.textbbox((0, 0), word, font=font) + if bbox[2] - bbox[0] > max_width: # Handle very long words if necessary (e.g., truncate) pass From 718eb27cbb7765d07bf1f7a6537d8fe80707a6a9 Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Tue, 22 Apr 2025 11:19:51 -0500 Subject: [PATCH 22/40] Refactor calendar manager initialization to use display_manager directly instead of matrix and canvas --- src/calendar_manager.py | 9 ++------- src/display_controller.py | 2 +- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/src/calendar_manager.py b/src/calendar_manager.py index 102496c0..7c3803fa 100644 --- a/src/calendar_manager.py +++ b/src/calendar_manager.py @@ -19,10 +19,9 @@ logger = logging.getLogger(__name__) logger.setLevel(logging.INFO) # Set to INFO to reduce noise class CalendarManager: - def __init__(self, matrix, canvas, config): + def __init__(self, display_manager, config): logger.info("Initializing CalendarManager") - self.matrix = matrix - self.canvas = canvas + self.display_manager = display_manager self.config = config self.calendar_config = config.get('calendar', {}) self.enabled = self.calendar_config.get('enabled', False) @@ -36,10 +35,6 @@ class CalendarManager: logger.info(f"Calendar configuration: enabled={self.enabled}, update_interval={self.update_interval}, max_events={self.max_events}, calendars={self.calendars}") - # Get display manager instance - from src.display_manager import DisplayManager - self.display_manager = DisplayManager._instance - # Get timezone from config self.config_manager = ConfigManager() timezone_str = self.config_manager.get_timezone() diff --git a/src/display_controller.py b/src/display_controller.py index c7aa0ed0..83f20ca2 100644 --- a/src/display_controller.py +++ b/src/display_controller.py @@ -34,7 +34,7 @@ class DisplayController: self.weather = WeatherManager(self.config, self.display_manager) if self.config.get('weather', {}).get('enabled', False) else None self.stocks = StockManager(self.config, self.display_manager) if self.config.get('stocks', {}).get('enabled', False) else None self.news = StockNewsManager(self.config, self.display_manager) if self.config.get('stock_news', {}).get('enabled', False) else None - self.calendar = CalendarManager(self.display_manager.matrix, self.display_manager.current_canvas, self.config) if self.config.get('calendar', {}).get('enabled', False) else None + self.calendar = CalendarManager(self.display_manager, self.config) if self.config.get('calendar', {}).get('enabled', False) else None self.youtube = YouTubeDisplay(self.display_manager, self.config_manager.config_path, self.config_manager.secrets_path) if self.config.get('youtube', {}).get('enabled', False) else None logger.info(f"Calendar Manager initialized: {'Object' if self.calendar else 'None'}") logger.info("Display modes initialized in %.3f seconds", time.time() - init_time) From b0c58b5ef203ec2bb30edfaba839816a58edc842 Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Tue, 22 Apr 2025 11:20:07 -0500 Subject: [PATCH 23/40] slight readme changes --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5655ee85..b8eb3b3a 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,7 @@ The LEDMatrix system includes a robust caching mechanism to optimize API calls a - Stock prices and market data - Stock news headlines - NHL game information +- NBA game information ### Cache Behavior - Data is cached based on update intervals defined in `config.json` @@ -113,9 +114,9 @@ The LEDMatrix system includes a robust caching mechanism to optimize API calls a - Temporary files are used for safe updates - JSON serialization handles all data types including timestamps -## NHL Scoreboard Display +## NHL, NBA Scoreboard Display -The LEDMatrix system includes a comprehensive NHL scoreboard display system with three display modes: +The LEDMatrix system includes a comprehensive NHL, NBA scoreboard display system with three display modes: ### Display Modes - **Live Games**: Shows currently playing games with live scores and game status From 0e1f7c1a76e8b4dd96c5b8afaf58bcb04cf22aa6 Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Tue, 22 Apr 2025 11:22:31 -0500 Subject: [PATCH 24/40] slight readme changes --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b8eb3b3a..19c1505d 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,10 @@ A modular LED matrix display system for sports information using Raspberry Pi and RGB LED matrices. ## Hardware Requirements -- Raspberry Pi 3 or newer +- Raspberry Pi 4 or older - Adafruit RGB Matrix Bonnet/HAT -- LED Matrix panels (64x32) +- 2x LED Matrix panels (64x32) +- DC Power Supply for Adafruit RGB HAT ## Installation From cfd3ea69289db4d44ca398af8d6796fb8d24688b Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Tue, 22 Apr 2025 11:33:08 -0500 Subject: [PATCH 25/40] Fix calendar display timing to show events for full duration --- src/calendar_manager.py | 3 --- src/display_controller.py | 4 ++++ 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/calendar_manager.py b/src/calendar_manager.py index 7c3803fa..caea6fac 100644 --- a/src/calendar_manager.py +++ b/src/calendar_manager.py @@ -297,9 +297,6 @@ class CalendarManager: # Update the display self.display_manager.update_display() logger.debug("CalendarManager event display updated.") - - # Advance to next event for next display - self.advance_event() else: # Draw failed (error logged in draw_event), show debug message logger.warning("Failed to draw calendar event") diff --git a/src/display_controller.py b/src/display_controller.py index 83f20ca2..bb26d712 100644 --- a/src/display_controller.py +++ b/src/display_controller.py @@ -347,6 +347,10 @@ class DisplayController: # Only proceed with mode switching if no live games if current_time - self.last_switch > self.get_current_duration(): # No live games, continue with regular rotation + # If we're currently on calendar, advance to next event before switching modes + if self.current_display_mode == 'calendar' and self.calendar: + self.calendar.advance_event() + self.current_mode_index = (self.current_mode_index + 1) % len(self.available_modes) self.current_display_mode = self.available_modes[self.current_mode_index] logger.info(f"Switching to: {self.current_display_mode}") From 6ad1af191991cd74d02a9095e805ac68310b89c9 Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Tue, 22 Apr 2025 12:26:08 -0500 Subject: [PATCH 26/40] Fix blank calendar display by optimizing clear/redraw logic --- src/calendar_manager.py | 7 +++++-- src/display_controller.py | 3 +++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/calendar_manager.py b/src/calendar_manager.py index caea6fac..236ea1d6 100644 --- a/src/calendar_manager.py +++ b/src/calendar_manager.py @@ -266,8 +266,9 @@ class CalendarManager: logger.debug("Calendar manager is disabled, skipping display") return - # Always clear the display before drawing new content - self.display_manager.clear() + # Only clear if force_clear is True (mode switch) or no events are drawn + if force_clear: + self.display_manager.clear() if not self.events: # Display "No Events" message if the list is empty @@ -287,6 +288,8 @@ class CalendarManager: # Only log at INFO level when switching to calendar or when force_clear is True if force_clear: logger.info(f"CalendarManager displaying event index {self.current_event_index}: {event_to_display.get('summary')}") + logger.info(f"CalendarManager displaying event: {event_to_display.get('summary')}") + logger.info(f"Event details - Date: {self._format_event_date(event_to_display)}, Time: {self._format_event_time(event_to_display)}, Summary: {event_to_display.get('summary', 'No Title')}") else: logger.debug(f"CalendarManager displaying event index {self.current_event_index}: {event_to_display.get('summary')}") diff --git a/src/display_controller.py b/src/display_controller.py index bb26d712..71586ad4 100644 --- a/src/display_controller.py +++ b/src/display_controller.py @@ -376,6 +376,9 @@ class DisplayController: self.news.display_news() elif self.current_display_mode == 'calendar' and self.calendar: + # Update calendar data if needed + self.calendar.update(current_time) + # Always display the calendar, with force_clear only on mode switch self.calendar.display(force_clear=self.force_clear) elif self.current_display_mode == 'nhl_recent' and self.nhl_recent: From fa0afa7546672e9e56d4e68a8b76aa45bf5f3bb1 Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Tue, 22 Apr 2025 14:30:22 -0500 Subject: [PATCH 27/40] Add scrolling functionality to calendar events for long summaries --- src/calendar_manager.py | 87 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 79 insertions(+), 8 deletions(-) diff --git a/src/calendar_manager.py b/src/calendar_manager.py index 236ea1d6..53b8c0cd 100644 --- a/src/calendar_manager.py +++ b/src/calendar_manager.py @@ -33,6 +33,15 @@ class CalendarManager: self.events = [] self.service = None + # Scrolling state + self.scroll_position = 0 + self.scroll_direction = 1 # 1 for down, -1 for up + self.scroll_speed = 1 # pixels per frame + self.scroll_delay = 0.1 # seconds between scroll updates + self.last_scroll_time = 0 + self.scroll_enabled = False + self.scroll_reset_time = 3 # seconds to wait before resetting scroll position + logger.info(f"Calendar configuration: enabled={self.enabled}, update_interval={self.update_interval}, max_events={self.max_events}, calendars={self.calendars}") # Get timezone from config @@ -142,6 +151,17 @@ class CalendarManager: # Calculate starting y position to center vertically y_pos = (self.display_manager.matrix.height - total_height) // 2 y_pos = max(1, y_pos) # Ensure it doesn't start above the top edge + + # Apply scroll offset + y_pos -= self.scroll_position + + # Check if scrolling is needed + if total_height > self.display_manager.matrix.height: + self.scroll_enabled = True + else: + self.scroll_enabled = False + self.scroll_position = 0 + logger.debug(f"Starting y position: {y_pos}, Total height: {total_height}") # Draw date in grey @@ -176,6 +196,7 @@ class CalendarManager: lines = [] words = text.split() current_line = [] + max_lines = 3 # Maximum number of lines to display for word in words: test_line = ' '.join(current_line + [word]) @@ -186,19 +207,51 @@ class CalendarManager: if text_width <= max_width: current_line.append(word) else: - # If the word itself is too long, add it on its own line (or handle differently if needed) + # If the word itself is too long, split it if not current_line: - lines.append(word) + # Check if the word itself is too long + bbox = self.display_manager.draw.textbbox((0, 0), word, font=font) + if bbox[2] - bbox[0] > max_width: + # Split long word into chunks that fit + chunks = [] + current_chunk = "" + for char in word: + test_chunk = current_chunk + char + bbox = self.display_manager.draw.textbbox((0, 0), test_chunk, font=font) + if bbox[2] - bbox[0] <= max_width: + current_chunk = test_chunk + else: + chunks.append(current_chunk) + current_chunk = char + if current_chunk: + chunks.append(current_chunk) + lines.extend(chunks) + else: + lines.append(word) else: lines.append(' '.join(current_line)) current_line = [word] - # Recheck if the new line with just this word is too long - bbox = self.display_manager.draw.textbbox((0, 0), word, font=font) - if bbox[2] - bbox[0] > max_width: - # Handle very long words if necessary (e.g., truncate) - pass + + # If we've reached the maximum number of lines, add ellipsis to the last line + if len(lines) >= max_lines: + last_line = lines[-1] + # Add ellipsis if there's room + bbox = self.display_manager.draw.textbbox((0, 0), last_line + "...", font=font) + if bbox[2] - bbox[0] <= max_width: + lines[-1] = last_line + "..." + else: + # If no room for ellipsis, remove last word and add ellipsis + words = last_line.split() + while words: + test_line = ' '.join(words[:-1]) + "..." + bbox = self.display_manager.draw.textbbox((0, 0), test_line, font=font) + if bbox[2] - bbox[0] <= max_width: + lines[-1] = test_line + break + words = words[:-1] + break - if current_line: + if current_line and len(lines) < max_lines: lines.append(' '.join(current_line)) return lines @@ -269,6 +322,8 @@ class CalendarManager: # Only clear if force_clear is True (mode switch) or no events are drawn if force_clear: self.display_manager.clear() + self.scroll_position = 0 + self.last_scroll_time = time.time() if not self.events: # Display "No Events" message if the list is empty @@ -293,6 +348,22 @@ class CalendarManager: else: logger.debug(f"CalendarManager displaying event index {self.current_event_index}: {event_to_display.get('summary')}") + # Handle scrolling if enabled + current_time = time.time() + if self.scroll_enabled: + if current_time - self.last_scroll_time >= self.scroll_delay: + self.scroll_position += self.scroll_speed * self.scroll_direction + + # Check if we need to reverse direction + if self.scroll_position <= 0: + self.scroll_direction = 1 + self.scroll_position = 0 + elif self.scroll_position >= self.display_manager.matrix.height: + self.scroll_direction = -1 + self.scroll_position = self.display_manager.matrix.height + + self.last_scroll_time = current_time + # Draw the event draw_successful = self.draw_event(event_to_display) From d4c1f49bd01aeb8fbfe1a0ba1759dd7d4a94a5e8 Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Tue, 22 Apr 2025 14:40:14 -0500 Subject: [PATCH 28/40] Fix calendar text scrolling to prevent visual distortion --- src/calendar_manager.py | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/src/calendar_manager.py b/src/calendar_manager.py index 53b8c0cd..dd3b5c09 100644 --- a/src/calendar_manager.py +++ b/src/calendar_manager.py @@ -35,12 +35,12 @@ class CalendarManager: # Scrolling state self.scroll_position = 0 - self.scroll_direction = 1 # 1 for down, -1 for up self.scroll_speed = 1 # pixels per frame self.scroll_delay = 0.1 # seconds between scroll updates self.last_scroll_time = 0 self.scroll_enabled = False - self.scroll_reset_time = 3 # seconds to wait before resetting scroll position + self.scroll_pause_time = 2.0 # seconds to pause at top before starting scroll + self.last_reset_time = 0 logger.info(f"Calendar configuration: enabled={self.enabled}, update_interval={self.update_interval}, max_events={self.max_events}, calendars={self.calendars}") @@ -324,6 +324,7 @@ class CalendarManager: self.display_manager.clear() self.scroll_position = 0 self.last_scroll_time = time.time() + self.last_reset_time = time.time() if not self.events: # Display "No Events" message if the list is empty @@ -351,18 +352,16 @@ class CalendarManager: # Handle scrolling if enabled current_time = time.time() if self.scroll_enabled: - if current_time - self.last_scroll_time >= self.scroll_delay: - self.scroll_position += self.scroll_speed * self.scroll_direction - - # Check if we need to reverse direction - if self.scroll_position <= 0: - self.scroll_direction = 1 - self.scroll_position = 0 - elif self.scroll_position >= self.display_manager.matrix.height: - self.scroll_direction = -1 - self.scroll_position = self.display_manager.matrix.height - - self.last_scroll_time = current_time + # Check if we should start scrolling (after pause at top) + if current_time - self.last_reset_time >= self.scroll_pause_time: + if current_time - self.last_scroll_time >= self.scroll_delay: + self.scroll_position += self.scroll_speed + self.last_scroll_time = current_time + + # If we've scrolled past the bottom, reset to top with pause + if self.scroll_position >= self.display_manager.matrix.height: + self.scroll_position = 0 + self.last_reset_time = current_time # Draw the event draw_successful = self.draw_event(event_to_display) From 9d01a876f19617238152135214ba93ce4a99fe57 Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Tue, 22 Apr 2025 14:50:55 -0500 Subject: [PATCH 29/40] Redesign calendar display with fixed two-line layout --- src/calendar_manager.py | 162 ++++++++++------------------------------ 1 file changed, 41 insertions(+), 121 deletions(-) diff --git a/src/calendar_manager.py b/src/calendar_manager.py index dd3b5c09..e0b5dbb7 100644 --- a/src/calendar_manager.py +++ b/src/calendar_manager.py @@ -33,15 +33,6 @@ class CalendarManager: self.events = [] self.service = None - # Scrolling state - self.scroll_position = 0 - self.scroll_speed = 1 # pixels per frame - self.scroll_delay = 0.1 # seconds between scroll updates - self.last_scroll_time = 0 - self.scroll_enabled = False - self.scroll_pause_time = 2.0 # seconds to pause at top before starting scroll - self.last_reset_time = 0 - logger.info(f"Calendar configuration: enabled={self.enabled}, update_interval={self.update_interval}, max_events={self.max_events}, calendars={self.calendars}") # Get timezone from config @@ -137,58 +128,26 @@ class CalendarManager: font = self.display_manager.small_font available_width = self.display_manager.matrix.width - 4 # Leave 2 pixel margin on each side - # Wrap title text - title_lines = self._wrap_text(summary, available_width, font) - logger.debug(f"Wrapped title into {len(title_lines)} lines: {title_lines}") - - # Calculate total height needed - date_height = 8 # Approximate height for date string - time_height = 8 # Approximate height for time string - title_height = len(title_lines) * 8 # Approximate height for title lines - # Height = date + time + title + spacing between each - total_height = date_height + time_height + title_height + ( (1 + len(title_lines)) * 2 ) + # Draw date and time on top line + datetime_str = f"{date_str} {time_str}" + self.display_manager.draw_text(datetime_str, y=2, color=self.time_color, small_font=True) - # Calculate starting y position to center vertically - y_pos = (self.display_manager.matrix.height - total_height) // 2 - y_pos = max(1, y_pos) # Ensure it doesn't start above the top edge + # Wrap summary text for two lines + title_lines = self._wrap_text(summary, available_width, font, max_lines=2) - # Apply scroll offset - y_pos -= self.scroll_position - - # Check if scrolling is needed - if total_height > self.display_manager.matrix.height: - self.scroll_enabled = True - else: - self.scroll_enabled = False - self.scroll_position = 0 - - logger.debug(f"Starting y position: {y_pos}, Total height: {total_height}") - - # Draw date in grey - logger.debug(f"Drawing date at y={y_pos}: {date_str}") - self.display_manager.draw_text(date_str, y=y_pos, color=self.date_color, small_font=True) - y_pos += date_height + 2 # Move down for the time - - # Draw time in green - logger.debug(f"Drawing time at y={y_pos}: {time_str}") - self.display_manager.draw_text(time_str, y=y_pos, color=self.time_color, small_font=True) - y_pos += time_height + 2 # Move down for the title - - # Draw title lines - for i, line in enumerate(title_lines): - logger.debug(f"Drawing title line {i+1} at y={y_pos}: {line}") - if y_pos >= self.display_manager.matrix.height - 8: # Stop if we run out of space - logger.debug("Stopping title drawing - reached bottom of display") - break + # Draw summary lines + y_pos = 12 # Start position for summary (below date/time) + for line in title_lines: self.display_manager.draw_text(line, y=y_pos, color=self.text_color, small_font=True) - y_pos += 8 + 2 # Move down for the next line, add 2px spacing - return True # Return True on successful drawing + y_pos += 8 # Move down for next line + + return True except Exception as e: logger.error(f"Error drawing calendar event: {str(e)}", exc_info=True) - return False # Return False on error + return False - def _wrap_text(self, text, max_width, font): + def _wrap_text(self, text, max_width, font, max_lines=2): """Wrap text to fit within max_width using the provided font.""" if not text: return [""] @@ -196,7 +155,6 @@ class CalendarManager: lines = [] words = text.split() current_line = [] - max_lines = 3 # Maximum number of lines to display for word in words: test_line = ' '.join(current_line + [word]) @@ -207,53 +165,40 @@ class CalendarManager: if text_width <= max_width: current_line.append(word) else: - # If the word itself is too long, split it - if not current_line: - # Check if the word itself is too long - bbox = self.display_manager.draw.textbbox((0, 0), word, font=font) - if bbox[2] - bbox[0] > max_width: - # Split long word into chunks that fit - chunks = [] - current_chunk = "" - for char in word: - test_chunk = current_chunk + char - bbox = self.display_manager.draw.textbbox((0, 0), test_chunk, font=font) - if bbox[2] - bbox[0] <= max_width: - current_chunk = test_chunk - else: - chunks.append(current_chunk) - current_chunk = char - if current_chunk: - chunks.append(current_chunk) - lines.extend(chunks) - else: - lines.append(word) - else: + if current_line: lines.append(' '.join(current_line)) - current_line = [word] + current_line = [word] + else: + # Word is too long for the line, truncate it + lines.append(word[:10] + "...") - # If we've reached the maximum number of lines, add ellipsis to the last line - if len(lines) >= max_lines: - last_line = lines[-1] - # Add ellipsis if there's room - bbox = self.display_manager.draw.textbbox((0, 0), last_line + "...", font=font) - if bbox[2] - bbox[0] <= max_width: - lines[-1] = last_line + "..." - else: - # If no room for ellipsis, remove last word and add ellipsis - words = last_line.split() - while words: - test_line = ' '.join(words[:-1]) + "..." - bbox = self.display_manager.draw.textbbox((0, 0), test_line, font=font) - if bbox[2] - bbox[0] <= max_width: - lines[-1] = test_line - break - words = words[:-1] - break + # Check if we've reached max lines + if len(lines) >= max_lines - 1 and current_line: + # For the last line, add ellipsis if there are more words + test_line = ' '.join(current_line + [word]) + if len(words) > words.index(word) + 1: + test_line += "..." + + # Check if the line with ellipsis fits + bbox = self.display_manager.draw.textbbox((0, 0), test_line, font=font) + if bbox[2] - bbox[0] <= max_width: + lines.append(test_line) + else: + # If it doesn't fit, truncate the last line + last_line = ' '.join(current_line) + if len(last_line) > 10: + last_line = last_line[:10] + "..." + lines.append(last_line) + break + # Add the last line if we haven't hit max_lines if current_line and len(lines) < max_lines: lines.append(' '.join(current_line)) + # If we only have one line, pad with an empty line + if len(lines) == 1: + lines.append("") + return lines def update(self, current_time): @@ -322,9 +267,6 @@ class CalendarManager: # Only clear if force_clear is True (mode switch) or no events are drawn if force_clear: self.display_manager.clear() - self.scroll_position = 0 - self.last_scroll_time = time.time() - self.last_reset_time = time.time() if not self.events: # Display "No Events" message if the list is empty @@ -341,28 +283,6 @@ class CalendarManager: # Set force_clear flag for logging self.force_clear = force_clear - # Only log at INFO level when switching to calendar or when force_clear is True - if force_clear: - logger.info(f"CalendarManager displaying event index {self.current_event_index}: {event_to_display.get('summary')}") - logger.info(f"CalendarManager displaying event: {event_to_display.get('summary')}") - logger.info(f"Event details - Date: {self._format_event_date(event_to_display)}, Time: {self._format_event_time(event_to_display)}, Summary: {event_to_display.get('summary', 'No Title')}") - else: - logger.debug(f"CalendarManager displaying event index {self.current_event_index}: {event_to_display.get('summary')}") - - # Handle scrolling if enabled - current_time = time.time() - if self.scroll_enabled: - # Check if we should start scrolling (after pause at top) - if current_time - self.last_reset_time >= self.scroll_pause_time: - if current_time - self.last_scroll_time >= self.scroll_delay: - self.scroll_position += self.scroll_speed - self.last_scroll_time = current_time - - # If we've scrolled past the bottom, reset to top with pause - if self.scroll_position >= self.display_manager.matrix.height: - self.scroll_position = 0 - self.last_reset_time = current_time - # Draw the event draw_successful = self.draw_event(event_to_display) From 367d014757fa80b134a1814fa8bfb86b58d3351e Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Tue, 22 Apr 2025 14:58:28 -0500 Subject: [PATCH 30/40] Change calendar date/time color to white for better readability --- src/calendar_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calendar_manager.py b/src/calendar_manager.py index e0b5dbb7..25620070 100644 --- a/src/calendar_manager.py +++ b/src/calendar_manager.py @@ -130,7 +130,7 @@ class CalendarManager: # Draw date and time on top line datetime_str = f"{date_str} {time_str}" - self.display_manager.draw_text(datetime_str, y=2, color=self.time_color, small_font=True) + self.display_manager.draw_text(datetime_str, y=2, color=self.text_color, small_font=True) # Wrap summary text for two lines title_lines = self._wrap_text(summary, available_width, font, max_lines=2) From 5391f60f0c004a37ff8e18aa9e7430a3be957652 Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Tue, 22 Apr 2025 15:03:46 -0500 Subject: [PATCH 31/40] Improve calendar text wrapping and use smaller font for summaries --- src/calendar_manager.py | 78 ++++++++++++++++++++++------------------- 1 file changed, 42 insertions(+), 36 deletions(-) diff --git a/src/calendar_manager.py b/src/calendar_manager.py index 25620070..e468f825 100644 --- a/src/calendar_manager.py +++ b/src/calendar_manager.py @@ -124,22 +124,23 @@ class CalendarManager: time_str = self._format_event_time(event) date_str = self._format_event_date(event) - # Use display manager's font for wrapping - font = self.display_manager.small_font + # Use different fonts for header and summary + header_font = self.display_manager.small_font + summary_font = self.display_manager.extra_small_font available_width = self.display_manager.matrix.width - 4 # Leave 2 pixel margin on each side # Draw date and time on top line datetime_str = f"{date_str} {time_str}" self.display_manager.draw_text(datetime_str, y=2, color=self.text_color, small_font=True) - # Wrap summary text for two lines - title_lines = self._wrap_text(summary, available_width, font, max_lines=2) + # Wrap summary text for two lines using extra small font + title_lines = self._wrap_text(summary, available_width, summary_font, max_lines=2) # Draw summary lines y_pos = 12 # Start position for summary (below date/time) for line in title_lines: - self.display_manager.draw_text(line, y=y_pos, color=self.text_color, small_font=True) - y_pos += 8 # Move down for next line + self.display_manager.draw_text(line, y=y_pos, color=self.text_color, extra_small_font=True) + y_pos += 6 # Move down less for extra small font return True @@ -153,53 +154,58 @@ class CalendarManager: return [""] lines = [] - words = text.split() current_line = [] - + words = text.split() + for word in words: - test_line = ' '.join(current_line + [word]) - # Use textbbox for accurate width calculation + # Try adding the word to the current line + test_line = ' '.join(current_line + [word]) if current_line else word bbox = self.display_manager.draw.textbbox((0, 0), test_line, font=font) text_width = bbox[2] - bbox[0] if text_width <= max_width: + # Word fits, add it to current line current_line.append(word) else: + # Word doesn't fit, start a new line if current_line: lines.append(' '.join(current_line)) current_line = [word] else: - # Word is too long for the line, truncate it - lines.append(word[:10] + "...") - - # Check if we've reached max lines - if len(lines) >= max_lines - 1 and current_line: - # For the last line, add ellipsis if there are more words - test_line = ' '.join(current_line + [word]) - if len(words) > words.index(word) + 1: - test_line += "..." - - # Check if the line with ellipsis fits - bbox = self.display_manager.draw.textbbox((0, 0), test_line, font=font) - if bbox[2] - bbox[0] <= max_width: - lines.append(test_line) - else: - # If it doesn't fit, truncate the last line - last_line = ' '.join(current_line) - if len(last_line) > 10: - last_line = last_line[:10] + "..." - lines.append(last_line) + # Single word too long, truncate it + truncated = word + while len(truncated) > 0: + bbox = self.display_manager.draw.textbbox((0, 0), truncated + "...", font=font) + if bbox[2] - bbox[0] <= max_width: + lines.append(truncated + "...") + break + truncated = truncated[:-1] + if not truncated: + lines.append(word[:10] + "...") + + # Check if we've filled all lines + if len(lines) >= max_lines: break - # Add the last line if we haven't hit max_lines + # Handle any remaining text in current_line if current_line and len(lines) < max_lines: - lines.append(' '.join(current_line)) - - # If we only have one line, pad with an empty line - if len(lines) == 1: + remaining_text = ' '.join(current_line) + if len(words) > len(current_line): # More words remain + # Try to fit with ellipsis + while len(remaining_text) > 0: + bbox = self.display_manager.draw.textbbox((0, 0), remaining_text + "...", font=font) + if bbox[2] - bbox[0] <= max_width: + lines.append(remaining_text + "...") + break + remaining_text = remaining_text[:-1] + else: + lines.append(remaining_text) + + # Ensure we have exactly max_lines + while len(lines) < max_lines: lines.append("") - return lines + return lines[:max_lines] def update(self, current_time): """Update calendar events if needed.""" From c3549584c87f47f9fab0874610f39229088edf61 Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Tue, 22 Apr 2025 15:10:02 -0500 Subject: [PATCH 32/40] Fix font usage in calendar display to use available small_font --- src/calendar_manager.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/calendar_manager.py b/src/calendar_manager.py index e468f825..816f657e 100644 --- a/src/calendar_manager.py +++ b/src/calendar_manager.py @@ -124,23 +124,22 @@ class CalendarManager: time_str = self._format_event_time(event) date_str = self._format_event_date(event) - # Use different fonts for header and summary - header_font = self.display_manager.small_font - summary_font = self.display_manager.extra_small_font + # Use display manager's font for wrapping + font = self.display_manager.small_font available_width = self.display_manager.matrix.width - 4 # Leave 2 pixel margin on each side # Draw date and time on top line datetime_str = f"{date_str} {time_str}" self.display_manager.draw_text(datetime_str, y=2, color=self.text_color, small_font=True) - # Wrap summary text for two lines using extra small font - title_lines = self._wrap_text(summary, available_width, summary_font, max_lines=2) + # Wrap summary text for two lines + title_lines = self._wrap_text(summary, available_width, font, max_lines=2) # Draw summary lines y_pos = 12 # Start position for summary (below date/time) for line in title_lines: - self.display_manager.draw_text(line, y=y_pos, color=self.text_color, extra_small_font=True) - y_pos += 6 # Move down less for extra small font + self.display_manager.draw_text(line, y=y_pos, color=self.text_color, small_font=True) + y_pos += 8 # Move down for next line return True From 8aba1ad1de82b7af6a1e5030b28077ca47378c21 Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Tue, 22 Apr 2025 15:21:44 -0500 Subject: [PATCH 33/40] Switch to One Call API for accurate daily temperature extremes - Use daily.temp.min/max for current weather display - Update forecast processing for new API format --- src/weather_manager.py | 72 ++++++++++++++++++------------------------ 1 file changed, 31 insertions(+), 41 deletions(-) diff --git a/src/weather_manager.py b/src/weather_manager.py index b3baee37..63a248bb 100644 --- a/src/weather_manager.py +++ b/src/weather_manager.py @@ -99,20 +99,30 @@ class WeatherManager: lat = geo_data[0]['lat'] lon = geo_data[0]['lon'] - # Get current weather and forecast using coordinates - weather_url = f"https://api.openweathermap.org/data/2.5/weather?lat={lat}&lon={lon}&appid={api_key}&units={units}" - forecast_url = f"https://api.openweathermap.org/data/2.5/forecast?lat={lat}&lon={lon}&appid={api_key}&units={units}" + # Get current weather and daily forecast using One Call API + one_call_url = f"https://api.openweathermap.org/data/3.0/onecall?lat={lat}&lon={lon}&exclude=minutely,hourly,alerts&appid={api_key}&units={units}" - # Fetch current weather - response = requests.get(weather_url) + # Fetch current weather and daily forecast + response = requests.get(one_call_url) response.raise_for_status() - self.weather_data = response.json() - - # Fetch forecast - response = requests.get(forecast_url) - response.raise_for_status() - self.forecast_data = response.json() + one_call_data = response.json() + + # Store current weather data + self.weather_data = { + 'main': { + 'temp': one_call_data['current']['temp'], + 'temp_max': one_call_data['daily'][0]['temp']['max'], + 'temp_min': one_call_data['daily'][0]['temp']['min'], + 'humidity': one_call_data['current']['humidity'], + 'pressure': one_call_data['current']['pressure'] + }, + 'weather': one_call_data['current']['weather'], + 'wind': one_call_data['current'].get('wind', {}) + } + # Store forecast data (for hourly and daily forecasts) + self.forecast_data = one_call_data + # Process forecast data self._process_forecast_data(self.forecast_data) @@ -144,12 +154,12 @@ class WeatherManager: return # Process hourly forecast (next 5 hours) - hourly_list = forecast_data.get('list', [])[:5] # Changed from 6 to 5 to match image + hourly_list = forecast_data.get('hourly', [])[:5] # Get next 5 hours self.hourly_forecast = [] for hour_data in hourly_list: dt = datetime.fromtimestamp(hour_data['dt']) - temp = round(hour_data['main']['temp']) + temp = round(hour_data['temp']) condition = hour_data['weather'][0]['main'] self.hourly_forecast.append({ 'hour': dt.strftime('%I:00 %p').lstrip('0'), # Format as "2:00 PM" @@ -158,38 +168,18 @@ class WeatherManager: }) # Process daily forecast - daily_data = {} - full_forecast_list = forecast_data.get('list', []) # Use the full list - for item in full_forecast_list: # Iterate over the full list - date = datetime.fromtimestamp(item['dt']).strftime('%Y-%m-%d') - if date not in daily_data: - daily_data[date] = { - 'temps': [], - 'conditions': [], - 'date': datetime.fromtimestamp(item['dt']) - } - daily_data[date]['temps'].append(item['main']['temp']) - daily_data[date]['conditions'].append(item['weather'][0]['main']) - - # Calculate daily summaries, excluding today + daily_list = forecast_data.get('daily', [])[1:4] # Skip today (index 0) and get next 3 days self.daily_forecast = [] - today_str = datetime.now().strftime('%Y-%m-%d') - # Sort data by date to ensure chronological order - sorted_daily_items = sorted(daily_data.items(), key=lambda item: item[1]['date']) - - # Filter out today's data and take the next 3 days - future_days_data = [item for item in sorted_daily_items if item[0] != today_str][:3] - - for date_str, data in future_days_data: - temps = data['temps'] - temp_high = round(max(temps)) - temp_low = round(min(temps)) - condition = max(set(data['conditions']), key=data['conditions'].count) + for day_data in daily_list: + dt = datetime.fromtimestamp(day_data['dt']) + temp_high = round(day_data['temp']['max']) + temp_low = round(day_data['temp']['min']) + condition = day_data['weather'][0]['main'] self.daily_forecast.append({ - 'date': data['date'].strftime('%a'), # Day name (Mon, Tue, etc.) - 'date_str': data['date'].strftime('%m/%d'), # Date (4/8, 4/9, etc.) + 'date': dt.strftime('%a'), # Day name (Mon, Tue, etc.) + 'date_str': dt.strftime('%m/%d'), # Date (4/8, 4/9, etc.) 'temp_high': temp_high, 'temp_low': temp_low, 'condition': condition From f125e244d7be1bdb415f1e495415d6f1891ec73b Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Tue, 22 Apr 2025 15:23:23 -0500 Subject: [PATCH 34/40] Fix wind data handling for One Call API - Map wind_speed and wind_deg to expected format --- src/weather_manager.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/weather_manager.py b/src/weather_manager.py index 63a248bb..39be21ff 100644 --- a/src/weather_manager.py +++ b/src/weather_manager.py @@ -117,7 +117,10 @@ class WeatherManager: 'pressure': one_call_data['current']['pressure'] }, 'weather': one_call_data['current']['weather'], - 'wind': one_call_data['current'].get('wind', {}) + 'wind': { + 'speed': one_call_data['current'].get('wind_speed', 0), + 'deg': one_call_data['current'].get('wind_deg', 0) + } } # Store forecast data (for hourly and daily forecasts) @@ -330,7 +333,7 @@ class WeatherManager: # --- Wind (Section 3) --- wind_speed = weather_data['wind']['speed'] - wind_deg = weather_data.get('wind', {}).get('deg', 0) + wind_deg = weather_data['wind']['deg'] wind_dir = self._get_wind_direction(wind_deg) wind_text = f"W:{wind_speed:.0f}{wind_dir}" wind_width = draw.textlength(wind_text, font=font) From 73e36aedf848bc04b7ad7f239bf096d529e393bd Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Tue, 22 Apr 2025 15:24:52 -0500 Subject: [PATCH 35/40] Include hourly forecast data in One Call API request - Remove hourly from exclude parameter --- src/weather_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/weather_manager.py b/src/weather_manager.py index 39be21ff..0409d488 100644 --- a/src/weather_manager.py +++ b/src/weather_manager.py @@ -100,7 +100,7 @@ class WeatherManager: lon = geo_data[0]['lon'] # Get current weather and daily forecast using One Call API - one_call_url = f"https://api.openweathermap.org/data/3.0/onecall?lat={lat}&lon={lon}&exclude=minutely,hourly,alerts&appid={api_key}&units={units}" + one_call_url = f"https://api.openweathermap.org/data/3.0/onecall?lat={lat}&lon={lon}&exclude=minutely,alerts&appid={api_key}&units={units}" # Fetch current weather and daily forecast response = requests.get(one_call_url) From e9681c3fe7439fa337334ae1132c81f87dd6fb3a Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Tue, 22 Apr 2025 15:28:49 -0500 Subject: [PATCH 36/40] docs: add configuration instructions for YouTube and Calendar display modules --- README.md | 66 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/README.md b/README.md index 19c1505d..bbd9e277 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,72 @@ cp config/config.example.json config/config.json 2. Edit `config/config.json` with your preferences +### YouTube Display Configuration + +The YouTube display module shows channel statistics for a specified YouTube channel. To configure it: + +1. In `config/config.json`, add the following section: +```json +{ + "youtube": { + "enabled": true, + "update_interval": 300 // Update interval in seconds (default: 300) + } +} +``` + +2. In `config/config_secrets.json`, add your YouTube API credentials: +```json +{ + "youtube": { + "api_key": "YOUR_YOUTUBE_API_KEY", + "channel_id": "YOUR_CHANNEL_ID" + } +} +``` + +To get these credentials: +1. Go to the [Google Cloud Console](https://console.cloud.google.com/) +2. Create a new project or select an existing one +3. Enable the YouTube Data API v3 +4. Create credentials (API key) +5. For the channel ID, you can find it in your YouTube channel URL or use the YouTube Data API to look it up + +### Calendar Display Configuration + +The calendar display module shows upcoming events from your Google Calendar. To configure it: + +1. In `config/config.json`, add the following section: +```json +{ + "calendar": { + "enabled": true, + "update_interval": 300, // Update interval in seconds (default: 300) + "max_events": 3, // Maximum number of events to display + "calendars": ["primary"] // List of calendar IDs to display + } +} +``` + +2. Set up Google Calendar API access: + 1. Go to the [Google Cloud Console](https://console.cloud.google.com/) + 2. Create a new project or select an existing one + 3. Enable the Google Calendar API + 4. Create OAuth 2.0 credentials: + - Application type: Desktop app + - Download the credentials file as `credentials.json` + 5. Place the `credentials.json` file in your project root directory + +3. On first run, the application will: + - Open a browser window for Google authentication + - Request calendar read-only access + - Save the authentication token as `token.pickle` + +The calendar display will show: +- Event date and time +- Event title (wrapped to fit the display) +- Up to 3 upcoming events (configurable) + ## API Keys For sensitive settings like API keys: From c090473854f09e5c8199195765867882c58eb674 Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Tue, 22 Apr 2025 15:38:07 -0500 Subject: [PATCH 37/40] turned on youtube display --- config/config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/config.json b/config/config.json index ccfd3463..cdcfdc66 100644 --- a/config/config.json +++ b/config/config.json @@ -112,7 +112,7 @@ "live_game_duration": 30 }, "youtube": { - "enabled": false, + "enabled": true, "update_interval": 3600 } } \ No newline at end of file From c223d629d42ac967d9f13d4d50d85d38904e7661 Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Tue, 22 Apr 2025 16:56:44 -0500 Subject: [PATCH 38/40] Move YouTube API key and channel ID to secrets file for better security --- src/youtube_display.py | 127 +++++++++++++++++++++++++---------------- 1 file changed, 77 insertions(+), 50 deletions(-) diff --git a/src/youtube_display.py b/src/youtube_display.py index e2abbc27..ef43c26d 100644 --- a/src/youtube_display.py +++ b/src/youtube_display.py @@ -12,43 +12,46 @@ from typing import Dict, Any logger = logging.getLogger(__name__) class YouTubeDisplay: - def __init__(self, display_manager, config_path='config/config.json', secrets_path='config/config_secrets.json'): - self.config = self._load_config(config_path) - self.secrets = self._load_config(secrets_path) - self.matrix = self._setup_matrix() - self.canvas = self.matrix.CreateFrameCanvas() - self.font = ImageFont.truetype("assets/fonts/PressStart2P-Regular.ttf", 8) - self.youtube_logo = Image.open("assets/youtube_logo.png") + def __init__(self, display_manager, config: Dict[str, Any]): self.display_manager = display_manager + self.config = config + self.youtube_config = config.get('youtube', {}) + self.enabled = self.youtube_config.get('enabled', False) + self.update_interval = self.youtube_config.get('update_interval', 300) self.last_update = 0 - self.update_interval = self.config.get('youtube', {}).get('update_interval', 300) # Default 5 minutes self.channel_stats = None - def _load_config(self, config_path): - with open(config_path, 'r') as f: - return json.load(f) + # Load secrets file + try: + with open('config/config_secrets.json', 'r') as f: + self.secrets = json.load(f) + except Exception as e: + logger.error(f"Error loading secrets file: {e}") + self.secrets = {} + self.enabled = False + + if self.enabled: + logger.info("YouTube display enabled") + self._initialize_display() + else: + logger.info("YouTube display disabled") + + def _initialize_display(self): + """Initialize display components.""" + self.font = ImageFont.truetype("assets/fonts/PressStart2P-Regular.ttf", 8) + try: + self.youtube_logo = Image.open("assets/youtube_logo.png") + except Exception as e: + logger.error(f"Error loading YouTube logo: {e}") + self.enabled = False - def _setup_matrix(self): - options = RGBMatrixOptions() - display_config = self.config['display']['hardware'] - - options.rows = display_config['rows'] - options.cols = display_config['cols'] - options.chain_length = display_config['chain_length'] - options.parallel = display_config['parallel'] - options.hardware_mapping = display_config['hardware_mapping'] - options.brightness = display_config['brightness'] - options.pwm_bits = display_config['pwm_bits'] - options.pwm_lsb_nanoseconds = display_config['pwm_lsb_nanoseconds'] - options.disable_hardware_pulsing = display_config['disable_hardware_pulsing'] - options.show_refresh_rate = display_config['show_refresh_rate'] - options.limit_refresh_rate_hz = display_config['limit_refresh_rate_hz'] - options.gpio_slowdown = self.config['display']['runtime']['gpio_slowdown'] - - return RGBMatrix(options=options) - def _get_channel_stats(self, channel_id): - api_key = self.secrets['youtube']['api_key'] + """Fetch channel statistics from YouTube API.""" + api_key = self.secrets.get('youtube', {}).get('api_key') + if not api_key: + logger.error("YouTube API key not configured in secrets file") + return None + url = f"https://www.googleapis.com/youtube/v3/channels?part=statistics,snippet&id={channel_id}&key={api_key}" try: @@ -66,51 +69,73 @@ class YouTubeDisplay: return None def _create_display(self, channel_stats): + """Create the display image with channel statistics.""" + if not channel_stats: + return None + # Create a new image with the matrix dimensions - image = Image.new('RGB', (self.matrix.width, self.matrix.height)) + image = Image.new('RGB', (self.display_manager.matrix.width, self.display_manager.matrix.height)) draw = ImageDraw.Draw(image) - # Resize YouTube logo to fit - logo_height = self.matrix.height // 3 + # Resize YouTube logo to fill 75% of display height + logo_height = int(self.display_manager.matrix.height * 0.75) logo_width = int(self.youtube_logo.width * (logo_height / self.youtube_logo.height)) resized_logo = self.youtube_logo.resize((logo_width, logo_height)) - # Calculate positions - logo_x = (self.matrix.width - logo_width) // 2 - logo_y = 0 + # Position logo on the left + logo_x = 2 # Small padding from left edge + logo_y = (self.display_manager.matrix.height - logo_height) // 2 # Center vertically # Paste the logo image.paste(resized_logo, (logo_x, logo_y)) - # Draw channel name + # Calculate right section width (remaining space after logo) + right_section_x = logo_x + logo_width + 5 # Start after logo with some padding + + # Draw channel name (top right) channel_name = channel_stats['title'] name_bbox = draw.textbbox((0, 0), channel_name, font=self.font) name_width = name_bbox[2] - name_bbox[0] - name_x = (self.matrix.width - name_width) // 2 - name_y = logo_height + 5 + name_x = right_section_x + ((self.display_manager.matrix.width - right_section_x - name_width) // 2) + name_y = 5 # Small padding from top draw.text((name_x, name_y), channel_name, font=self.font, fill=(255, 255, 255)) - # Draw subscriber count + # Draw subscriber count (middle right) subs_text = f"{channel_stats['subscribers']:,} subscribers" subs_bbox = draw.textbbox((0, 0), subs_text, font=self.font) subs_width = subs_bbox[2] - subs_bbox[0] - subs_x = (self.matrix.width - subs_width) // 2 - subs_y = name_y + 15 + subs_x = right_section_x + ((self.display_manager.matrix.width - right_section_x - subs_width) // 2) + subs_y = name_y + 15 # Position below channel name draw.text((subs_x, subs_y), subs_text, font=self.font, fill=(255, 255, 255)) + # Draw view count (bottom right) + views_text = f"{channel_stats['views']:,} views" + views_bbox = draw.textbbox((0, 0), views_text, font=self.font) + views_width = views_bbox[2] - views_bbox[0] + views_x = right_section_x + ((self.display_manager.matrix.width - right_section_x - views_width) // 2) + views_y = subs_y + 15 # Position below subscriber count + draw.text((views_x, views_y), views_text, font=self.font, fill=(255, 255, 255)) + return image def update(self): """Update YouTube channel stats if needed.""" + if not self.enabled: + return + current_time = time.time() if current_time - self.last_update >= self.update_interval: - channel_id = self.secrets['youtube']['channel_id'] + channel_id = self.config.get('youtube', {}).get('channel_id') + if not channel_id: + logger.error("YouTube channel ID not configured") + return + self.channel_stats = self._get_channel_stats(channel_id) self.last_update = current_time def display(self, force_clear: bool = False): """Display YouTube channel stats.""" - if not self.config.get('youtube', {}).get('enabled', False): + if not self.enabled: return if not self.channel_stats: @@ -118,15 +143,17 @@ class YouTubeDisplay: if self.channel_stats: if force_clear: - self.matrix.Clear() + self.display_manager.clear() display_image = self._create_display(self.channel_stats) - self.canvas.SetImage(display_image) - self.matrix.SwapOnVSync(self.canvas) - time.sleep(self.update_interval) + if display_image: + self.display_manager.image = display_image + self.display_manager.update_display() def cleanup(self): - self.matrix.Clear() + """Clean up resources.""" + if self.enabled: + self.display_manager.clear() if __name__ == "__main__": # Example usage From 1e86df1d0c93969bcd26cc61932f81c658a99a14 Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Tue, 22 Apr 2025 17:25:01 -0500 Subject: [PATCH 39/40] update youtube display call logic --- src/display_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/display_controller.py b/src/display_controller.py index 71586ad4..ac46001a 100644 --- a/src/display_controller.py +++ b/src/display_controller.py @@ -35,7 +35,7 @@ class DisplayController: self.stocks = StockManager(self.config, self.display_manager) if self.config.get('stocks', {}).get('enabled', False) else None self.news = StockNewsManager(self.config, self.display_manager) if self.config.get('stock_news', {}).get('enabled', False) else None self.calendar = CalendarManager(self.display_manager, self.config) if self.config.get('calendar', {}).get('enabled', False) else None - self.youtube = YouTubeDisplay(self.display_manager, self.config_manager.config_path, self.config_manager.secrets_path) if self.config.get('youtube', {}).get('enabled', False) else None + self.youtube = YouTubeDisplay(self.display_manager, self.config) if self.config.get('youtube', {}).get('enabled', False) else None logger.info(f"Calendar Manager initialized: {'Object' if self.calendar else 'None'}") logger.info("Display modes initialized in %.3f seconds", time.time() - init_time) From 125bb9a1ea94cfcd854a6cbed67986c4cf05cbf9 Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Tue, 22 Apr 2025 17:34:06 -0500 Subject: [PATCH 40/40] Improve YouTube display layout: adjust logo size, text spacing, and add truncation for long channel names --- src/youtube_display.py | 36 +++++++++++++++++++++--------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/src/youtube_display.py b/src/youtube_display.py index ef43c26d..10915666 100644 --- a/src/youtube_display.py +++ b/src/youtube_display.py @@ -77,44 +77,50 @@ class YouTubeDisplay: image = Image.new('RGB', (self.display_manager.matrix.width, self.display_manager.matrix.height)) draw = ImageDraw.Draw(image) - # Resize YouTube logo to fill 75% of display height - logo_height = int(self.display_manager.matrix.height * 0.75) + # Calculate logo dimensions - 60% of display height to ensure text fits + logo_height = int(self.display_manager.matrix.height * 0.6) logo_width = int(self.youtube_logo.width * (logo_height / self.youtube_logo.height)) resized_logo = self.youtube_logo.resize((logo_width, logo_height)) - # Position logo on the left + # Position logo on the left with padding logo_x = 2 # Small padding from left edge logo_y = (self.display_manager.matrix.height - logo_height) // 2 # Center vertically # Paste the logo image.paste(resized_logo, (logo_x, logo_y)) - # Calculate right section width (remaining space after logo) - right_section_x = logo_x + logo_width + 5 # Start after logo with some padding + # Calculate right section width and starting position + right_section_x = logo_x + logo_width + 4 # Start after logo with some padding - # Draw channel name (top right) + # Calculate text positions + line_height = 10 # Approximate line height for PressStart2P font at size 8 + total_text_height = line_height * 3 # 3 lines of text + start_y = (self.display_manager.matrix.height - total_text_height) // 2 + + # Draw channel name (top) channel_name = channel_stats['title'] + # Truncate channel name if too long + max_chars = (self.display_manager.matrix.width - right_section_x - 4) // 8 # 8 pixels per character + if len(channel_name) > max_chars: + channel_name = channel_name[:max_chars-3] + "..." name_bbox = draw.textbbox((0, 0), channel_name, font=self.font) name_width = name_bbox[2] - name_bbox[0] name_x = right_section_x + ((self.display_manager.matrix.width - right_section_x - name_width) // 2) - name_y = 5 # Small padding from top - draw.text((name_x, name_y), channel_name, font=self.font, fill=(255, 255, 255)) + draw.text((name_x, start_y), channel_name, font=self.font, fill=(255, 255, 255)) - # Draw subscriber count (middle right) - subs_text = f"{channel_stats['subscribers']:,} subscribers" + # Draw subscriber count (middle) + subs_text = f"{channel_stats['subscribers']:,} subs" subs_bbox = draw.textbbox((0, 0), subs_text, font=self.font) subs_width = subs_bbox[2] - subs_bbox[0] subs_x = right_section_x + ((self.display_manager.matrix.width - right_section_x - subs_width) // 2) - subs_y = name_y + 15 # Position below channel name - draw.text((subs_x, subs_y), subs_text, font=self.font, fill=(255, 255, 255)) + draw.text((subs_x, start_y + line_height), subs_text, font=self.font, fill=(255, 255, 255)) - # Draw view count (bottom right) + # Draw view count (bottom) views_text = f"{channel_stats['views']:,} views" views_bbox = draw.textbbox((0, 0), views_text, font=self.font) views_width = views_bbox[2] - views_bbox[0] views_x = right_section_x + ((self.display_manager.matrix.width - right_section_x - views_width) // 2) - views_y = subs_y + 15 # Position below subscriber count - draw.text((views_x, views_y), views_text, font=self.font, fill=(255, 255, 255)) + draw.text((views_x, start_y + (line_height * 2)), views_text, font=self.font, fill=(255, 255, 255)) return image