add Of The Day display

This commit is contained in:
Chuck
2025-07-22 20:39:09 -05:00
parent 3ab28e8201
commit cb81bec042
7 changed files with 4432 additions and 1 deletions

View File

@@ -69,7 +69,8 @@
"ncaam_basketball_live": 30,
"ncaam_basketball_recent": 30,
"ncaam_basketball_upcoming": 30,
"music": 30
"music": 30,
"of_the_day": 20
},
"use_short_date_format": true
},
@@ -328,5 +329,22 @@
"preferred_source": "ytm",
"YTM_COMPANION_URL": "http://192.168.86.12:9863",
"POLLING_INTERVAL_SECONDS": 1
},
"of_the_day": {
"enabled": true,
"update_interval": 3600,
"category_order": ["word_of_the_day", "slovenian_word"],
"categories": {
"word_of_the_day": {
"enabled": true,
"data_file": "of_the_day/word_of_the_day.json",
"display_name": "Word of the Day"
},
"slovenian_word": {
"enabled": true,
"data_file": "of_the_day/slovenian_word_of_the_day.json",
"display_name": "Slovenian Word of the Day"
}
}
}
}

View File

@@ -0,0 +1,329 @@
# "Of The Day" Display System Guide
## Overview
The "Of The Day" display system allows you to create multiple daily displays that show different types of content each day. This system is perfect for educational content, inspirational messages, language learning, and more.
## Features
- **Multiple Categories**: Enable multiple "of the day" displays simultaneously
- **Daily Rotation**: Each day shows a different item based on the day of the year
- **Customizable Content**: Create your own categories and content
- **Configurable Display**: Control display duration, update intervals, and more
- **AI-Ready**: Designed to work with LLM AI models for content generation
## Configuration
### Basic Setup
Add the following configuration to your `config/config.json`:
```json
{
"of_the_day": {
"enabled": true,
"update_interval": 3600,
"category_order": ["word_of_the_day", "bible_verse", "spanish_word"],
"categories": {
"word_of_the_day": {
"enabled": true,
"data_file": "data/word_of_the_day.json",
"display_name": "Word of the Day"
},
"bible_verse": {
"enabled": true,
"data_file": "data/bible_verse_of_the_day.json",
"display_name": "Bible Verse of the Day"
},
"spanish_word": {
"enabled": true,
"data_file": "data/spanish_word_of_the_day.json",
"display_name": "Spanish Word of the Day"
}
}
}
}
```
### Configuration Options
- **enabled**: Enable/disable the entire "of the day" system
- **update_interval**: How often to check for updates (in seconds)
- **category_order**: The order in which categories will be displayed
- **categories**: Individual category configurations
### Category Configuration
Each category has these options:
- **enabled**: Enable/disable this specific category
- **data_file**: Path to the JSON data file (relative to project root)
- **display_name**: Human-readable name for the category
### Display Duration
Add the display duration to the `display_durations` section:
```json
"display_durations": {
"of_the_day": 20
}
```
## Data File Format
Each category uses a JSON file with the following structure:
```json
{
"1": {
"title": "SERENDIPITY",
"subtitle": "A pleasant surprise",
"description": "The occurrence and development of events by chance in a happy or beneficial way"
},
"2": {
"title": "EPHEMERAL",
"subtitle": "Short-lived",
"description": "Lasting for a very short time; transitory"
}
}
```
### Data File Structure
- **Key**: Day of the year (1-366 for leap years)
- **title**: Main text displayed in bold white font
- **subtitle**: Secondary text displayed in smaller font
- **description**: Longer description (optional, used if subtitle is empty)
## Creating Custom Categories
### Step 1: Create a Data File
Create a new JSON file in the `data/` directory:
```bash
touch data/my_custom_category.json
```
### Step 2: Add Content
Add entries for each day of the year (1-366):
```json
{
"1": {
"title": "FIRST ITEM",
"subtitle": "Brief description",
"description": "Longer explanation if needed"
},
"2": {
"title": "SECOND ITEM",
"subtitle": "Another description",
"description": "More details here"
}
}
```
### Step 3: Update Configuration
Add your category to the config:
```json
{
"of_the_day": {
"categories": {
"my_custom_category": {
"enabled": true,
"data_file": "data/my_custom_category.json",
"display_name": "My Custom Category"
}
},
"category_order": ["word_of_the_day", "my_custom_category"]
}
}
```
## Using AI to Generate Content
### Example: Word of the Day Generator
You can use an LLM to generate a full year of content. Here's a Python script example:
```python
import json
import openai
def generate_word_of_the_day():
"""Generate a full year of word of the day entries using AI."""
words = {}
for day in range(1, 367):
prompt = f"""
Generate a word of the day for day {day} of the year.
Include:
1. An interesting word (all caps)
2. A brief subtitle (2-3 words)
3. A clear definition (1-2 sentences)
Format as JSON:
{{
"title": "WORD",
"subtitle": "Brief description",
"description": "Full definition"
}}
"""
# Use your preferred AI service
response = openai.ChatCompletion.create(
model="gpt-3.5-turbo",
messages=[{"role": "user", "content": prompt}]
)
# Parse the response and add to words dict
# Implementation depends on your AI service
# Save to file
with open('data/ai_generated_words.json', 'w') as f:
json.dump(words, f, indent=4)
if __name__ == "__main__":
generate_word_of_the_day()
```
### Example: Bible Verse Generator
```python
def generate_bible_verses():
"""Generate a full year of inspirational bible verses."""
verses = {}
for day in range(1, 367):
prompt = f"""
Generate an inspirational bible verse for day {day} of the year.
Include:
1. Bible reference (e.g., "JOHN 3:16")
2. Brief theme (e.g., "God's love")
3. The verse text
Format as JSON:
{{
"title": "BIBLE REFERENCE",
"subtitle": "Brief theme",
"description": "Full verse text"
}}
"""
# Implementation with your AI service
with open('data/ai_generated_verses.json', 'w') as f:
json.dump(verses, f, indent=4)
```
## Category Ideas
Here are some ideas for custom categories:
### Educational
- **Vocabulary Word of the Day**: Expand your vocabulary
- **Math Problem of the Day**: Daily math challenges
- **Science Fact of the Day**: Interesting scientific facts
- **History Event of the Day**: Historical events that happened on this date
### Language Learning
- **Spanish Word of the Day**: Learn Spanish vocabulary
- **French Word of the Day**: Learn French vocabulary
- **German Word of the Day**: Learn German vocabulary
- **Japanese Word of the Day**: Learn Japanese vocabulary
### Inspirational
- **Bible Verse of the Day**: Daily scripture
- **Quote of the Day**: Inspirational quotes
- **Affirmation of the Day**: Positive affirmations
- **Meditation of the Day**: Daily meditation prompts
### Professional
- **Programming Tip of the Day**: Daily coding tips
- **Business Quote of the Day**: Business wisdom
- **Leadership Lesson of the Day**: Leadership insights
- **Productivity Tip of the Day**: Daily productivity advice
### Entertainment
- **Movie Quote of the Day**: Famous movie quotes
- **Song Lyric of the Day**: Inspirational song lyrics
- **Joke of the Day**: Daily humor
- **Trivia Question of the Day**: Daily trivia
## Display Layout
The display uses a layout similar to the calendar manager:
- **Title**: Bold white text at the top
- **Subtitle**: Smaller gray text below the title
- **Description**: Wrapped text if subtitle is empty
## Troubleshooting
### Common Issues
1. **No content displayed**: Check that your data file exists and has entries for the current day
2. **File not found**: Ensure the data file path is correct relative to the project root
3. **Display not showing**: Verify the category is enabled in the configuration
4. **Wrong day content**: The system uses day of year (1-366), not calendar date
### Debugging
Check the logs for error messages:
```bash
tail -f /var/log/ledmatrix.log
```
Common log messages:
- `"OfTheDayManager initialized: Object"` - Manager loaded successfully
- `"Loaded data file for category_name: X items"` - Data file loaded
- `"Displaying category_name: title"` - Content being displayed
## Advanced Configuration
### Custom Display Colors
You can modify the colors in the `OfTheDayManager` class:
```python
self.title_color = (255, 255, 255) # White
self.subtitle_color = (200, 200, 200) # Light gray
self.background_color = (0, 0, 0) # Black
```
### Custom Fonts
The system uses the same fonts as other displays:
- **Title**: `regular_font` (Press Start 2P)
- **Subtitle/Description**: `small_font` (Press Start 2P)
## Integration with Other Systems
The "Of The Day" system integrates seamlessly with:
- **Display Controller**: Automatic rotation with other displays
- **Schedule System**: Respects display schedule settings
- **Music Manager**: Properly handles music display transitions
- **Live Sports**: Prioritized over regular displays when games are live
## Performance Considerations
- Data files are loaded once at startup
- Daily content is cached and only reloaded when the date changes
- Display updates are minimal to maintain smooth performance
- Text wrapping is optimized for the LED matrix display
## Future Enhancements
Potential improvements:
- **API Integration**: Pull content from external APIs
- **User Interface**: Web interface for content management
- **Analytics**: Track which content is most engaging
- **Scheduling**: Custom schedules for different categories
- **Multi-language**: Support for different languages in the interface

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -32,6 +32,7 @@ from src.youtube_display import YouTubeDisplay
from src.calendar_manager import CalendarManager
from src.text_display import TextDisplay
from src.music_manager import MusicManager
from src.of_the_day_manager import OfTheDayManager
# Get logger without configuring
logger = logging.getLogger(__name__)
@@ -59,8 +60,10 @@ class DisplayController:
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) if self.config.get('youtube', {}).get('enabled', False) else None
self.text_display = TextDisplay(self.display_manager, self.config) if self.config.get('text_display', {}).get('enabled', False) else None
self.of_the_day = OfTheDayManager(self.display_manager, self.config) if self.config.get('of_the_day', {}).get('enabled', False) else None
logger.info(f"Calendar Manager initialized: {'Object' if self.calendar else 'None'}")
logger.info(f"Text Display initialized: {'Object' if self.text_display else 'None'}")
logger.info(f"OfTheDay Manager initialized: {'Object' if self.of_the_day else 'None'}")
logger.info("Display modes initialized in %.3f seconds", time.time() - init_time)
# Initialize Music Manager
@@ -240,6 +243,7 @@ class DisplayController:
if self.calendar: self.available_modes.append('calendar')
if self.youtube: self.available_modes.append('youtube')
if self.text_display: self.available_modes.append('text_display')
if self.of_the_day: self.available_modes.append('of_the_day')
# Add Music display mode if enabled
if self.music_manager: # Will be non-None only if successfully initialized and enabled
@@ -469,6 +473,7 @@ class DisplayController:
if self.calendar: self.calendar.update(time.time())
if self.youtube: self.youtube.update()
if self.text_display: self.text_display.update()
if self.of_the_day: self.of_the_day.update(time.time())
# Update NHL managers
if self.nhl_live: self.nhl_live.update()
@@ -939,6 +944,8 @@ class DisplayController:
logger.debug(f"Timer expired for regular mode {self.current_display_mode}. Switching.")
if self.current_display_mode == 'calendar' and self.calendar:
self.calendar.advance_event()
elif self.current_display_mode == 'of_the_day' and self.of_the_day:
self.of_the_day.advance_item()
needs_switch = True
self.current_mode_index = (self.current_mode_index + 1) % len(self.available_modes)
new_mode_after_timer = self.available_modes[self.current_mode_index]
@@ -979,6 +986,8 @@ class DisplayController:
manager_to_display = self.youtube
elif self.current_display_mode == 'text_display' and self.text_display:
manager_to_display = self.text_display
elif self.current_display_mode == 'of_the_day' and self.of_the_day:
manager_to_display = self.of_the_day
# Add other regular managers (NHL recent/upcoming, NBA, MLB, Soccer, NFL, NCAA FB)
elif self.current_display_mode == 'nhl_recent' and self.nhl_recent:
manager_to_display = self.nhl_recent
@@ -1050,6 +1059,8 @@ class DisplayController:
manager_to_display.display(force_clear=self.force_clear)
elif self.current_display_mode == 'text_display':
manager_to_display.display() # Assumes internal clearing
elif self.current_display_mode == 'of_the_day':
manager_to_display.display(force_clear=self.force_clear)
elif self.current_display_mode == 'nfl_live' and self.nfl_live:
self.nfl_live.display(force_clear=self.force_clear)
elif self.current_display_mode == 'ncaa_fb_live' and self.ncaa_fb_live:

281
src/of_the_day_manager.py Normal file
View File

@@ -0,0 +1,281 @@
import os
import json
import logging
from datetime import datetime, date
from PIL import Image, ImageDraw, ImageFont
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__)
logger.setLevel(logging.INFO)
class OfTheDayManager:
def __init__(self, display_manager, config):
logger.info("Initializing OfTheDayManager")
self.display_manager = display_manager
self.config = config
self.of_the_day_config = config.get('of_the_day', {})
self.enabled = self.of_the_day_config.get('enabled', False)
self.update_interval = self.of_the_day_config.get('update_interval', 3600) # 1 hour default
self.last_update = 0
self.last_display_log = 0
self.current_day = None
self.current_items = {}
self.current_item_index = 0
self.current_category_index = 0
# Load categories and their data
self.categories = self.of_the_day_config.get('categories', {})
self.category_order = self.of_the_day_config.get('category_order', [])
# Display properties
self.title_color = (255, 255, 255) # White
self.subtitle_color = (200, 200, 200) # Light gray
self.background_color = (0, 0, 0) # Black
# State management
self.force_clear = False
# Load data files
self.data_files = {}
self._load_data_files()
logger.info(f"OfTheDayManager configuration: enabled={self.enabled}, categories={list(self.categories.keys())}")
if self.enabled:
self._load_todays_items()
else:
logger.warning("OfTheDayManager is disabled in configuration")
def _load_data_files(self):
"""Load all data files for enabled categories."""
if not self.enabled:
return
for category_name, category_config in self.categories.items():
if not category_config.get('enabled', True):
continue
data_file = category_config.get('data_file')
if not data_file:
logger.warning(f"No data file specified for category: {category_name}")
continue
try:
# Try relative path first, then absolute
file_path = data_file
if not os.path.isabs(file_path):
file_path = os.path.join(os.path.dirname(__file__), '..', 'of_the_day', data_file)
if os.path.exists(file_path):
with open(file_path, 'r', encoding='utf-8') as f:
self.data_files[category_name] = json.load(f)
logger.info(f"Loaded data file for {category_name}: {len(self.data_files[category_name])} items")
else:
logger.error(f"Data file not found for {category_name}: {file_path}")
self.data_files[category_name] = {}
except Exception as e:
logger.error(f"Error loading data file for {category_name}: {e}")
self.data_files[category_name] = {}
def _load_todays_items(self):
"""Load items for today based on the current date."""
if not self.enabled:
return
today = date.today()
day_of_year = today.timetuple().tm_yday
self.current_items = {}
for category_name, category_config in self.categories.items():
if not category_config.get('enabled', True):
continue
data = self.data_files.get(category_name, {})
if not data:
continue
# Get item for today (day of year)
item = data.get(str(day_of_year))
if item:
self.current_items[category_name] = item
logger.info(f"Loaded {category_name} item for day {day_of_year}: {item.get('title', 'No title')}")
else:
logger.warning(f"No item found for {category_name} on day {day_of_year}")
self.current_day = today
self.current_category_index = 0
self.current_item_index = 0
def update(self, current_time):
"""Update items if needed (daily or on interval)."""
if not self.enabled:
logger.debug("OfTheDayManager is disabled, skipping update")
return
today = date.today()
# Check if we need to load new items (new day or first time)
if self.current_day != today:
logger.info("New day detected, loading new items")
self._load_todays_items()
# Check if we need to update based on interval
if current_time - self.last_update > self.update_interval:
logger.debug("OfTheDayManager update interval reached")
self.last_update = current_time
def draw_item(self, category_name, item):
"""Draw a single 'of the day' item."""
try:
title = item.get('title', 'No Title')
subtitle = item.get('subtitle', '')
description = item.get('description', '')
# Clear the display
self.display_manager.clear()
# Draw title in extra small font at the top for maximum text fitting
title_width = self.display_manager.get_text_width(title, self.display_manager.extra_small_font)
title_x = (self.display_manager.matrix.width - title_width) // 2
self.display_manager.draw_text(title, title_x, 2,
color=self.title_color,
font=self.display_manager.extra_small_font)
# Draw subtitle/description in extra small font below
if subtitle:
subtitle_width = self.display_manager.get_text_width(subtitle, self.display_manager.extra_small_font)
subtitle_x = (self.display_manager.matrix.width - subtitle_width) // 2
self.display_manager.draw_text(subtitle, subtitle_x, 12,
color=self.subtitle_color,
font=self.display_manager.extra_small_font)
elif description:
# Wrap description text if it's too long
available_width = self.display_manager.matrix.width - 4
wrapped_lines = self._wrap_text(description, available_width, self.display_manager.extra_small_font, max_lines=3)
for i, line in enumerate(wrapped_lines):
if line.strip():
line_width = self.display_manager.get_text_width(line, self.display_manager.extra_small_font)
line_x = (self.display_manager.matrix.width - line_width) // 2
self.display_manager.draw_text(line, line_x, 12 + (i * 8),
color=self.subtitle_color,
font=self.display_manager.extra_small_font)
return True
except Exception as e:
logger.error(f"Error drawing 'of the day' item: {e}", exc_info=True)
return False
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 [""]
lines = []
current_line = []
words = text.split()
for word in words:
# Try adding the word to the current line
test_line = ' '.join(current_line + [word]) if current_line else word
text_width = self.display_manager.get_text_width(test_line, font)
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:
# Single word too long, truncate it
truncated = word
while len(truncated) > 0:
if self.display_manager.get_text_width(truncated + "...", font) <= 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
# Handle any remaining text in current_line
if current_line and len(lines) < max_lines:
remaining_text = ' '.join(current_line)
if len(words) > len(current_line): # More words remain
# Try to fit with ellipsis
while len(remaining_text) > 0:
if self.display_manager.get_text_width(remaining_text + "...", font) <= 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[:max_lines]
def display(self, force_clear=False):
"""Display 'of the day' items on the LED matrix."""
if not self.enabled or not self.current_items:
return
try:
if force_clear:
self.display_manager.clear()
self.force_clear = True
# Get current category and item
category_names = list(self.current_items.keys())
if not category_names:
return
if self.current_category_index >= len(category_names):
self.current_category_index = 0
current_category = category_names[self.current_category_index]
current_item = self.current_items[current_category]
# Log the item being displayed, but only every 5 seconds
current_time = time.time()
if current_time - self.last_display_log > 5:
title = current_item.get('title', 'No Title')
logger.info(f"Displaying {current_category}: {title}")
self.last_display_log = current_time
# Draw the item
self.draw_item(current_category, current_item)
# Update the display
self.display_manager.update_display()
except Exception as e:
logger.error(f"Error displaying 'of the day' item: {e}", exc_info=True)
def advance_item(self):
"""Advance to the next item. Called by DisplayController when display time is up."""
if not self.enabled:
logger.debug("OfTheDayManager is disabled, skipping item advance")
return
category_names = list(self.current_items.keys())
if not category_names:
return
self.current_category_index += 1
if self.current_category_index >= len(category_names):
self.current_category_index = 0
logger.debug(f"OfTheDayManager advanced to category index {self.current_category_index}")

138
test/test_of_the_day.py Normal file
View File

@@ -0,0 +1,138 @@
#!/usr/bin/env python3
import sys
import os
import json
from datetime import date
# Add the project root to the path
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from src.of_the_day_manager import OfTheDayManager
from src.display_manager import DisplayManager
from src.config_manager import ConfigManager
def test_of_the_day_manager():
"""Test the OfTheDayManager functionality."""
print("Testing OfTheDayManager...")
# Load config
config_manager = ConfigManager()
config = config_manager.load_config()
# Create a mock display manager (we won't actually display)
display_manager = DisplayManager(config)
# Create the OfTheDayManager
of_the_day = OfTheDayManager(display_manager, config)
print(f"OfTheDayManager enabled: {of_the_day.enabled}")
print(f"Categories loaded: {list(of_the_day.categories.keys())}")
print(f"Data files loaded: {list(of_the_day.data_files.keys())}")
# Test loading today's items
today = date.today()
day_of_year = today.timetuple().tm_yday
print(f"Today is day {day_of_year} of the year")
of_the_day._load_todays_items()
print(f"Today's items: {list(of_the_day.current_items.keys())}")
# Test data file loading
for category_name, data in of_the_day.data_files.items():
print(f"Category '{category_name}': {len(data)} items loaded")
if str(day_of_year) in data:
item = data[str(day_of_year)]
print(f" Today's item: {item.get('title', 'No title')}")
else:
print(f" No item found for day {day_of_year}")
# Test text wrapping
test_text = "This is a very long text that should be wrapped to fit on the LED matrix display"
wrapped = of_the_day._wrap_text(test_text, 60, display_manager.extra_small_font, max_lines=3)
print(f"Text wrapping test: {wrapped}")
print("OfTheDayManager test completed successfully!")
def test_data_files():
"""Test that all data files are valid JSON."""
print("\nTesting data files...")
data_dir = "of_the_day"
if not os.path.exists(data_dir):
print(f"Data directory {data_dir} not found!")
return
for filename in os.listdir(data_dir):
if filename.endswith('.json'):
filepath = os.path.join(data_dir, filename)
try:
with open(filepath, 'r', encoding='utf-8') as f:
data = json.load(f)
print(f"{filename}: {len(data)} items")
# Check for today's entry
today = date.today()
day_of_year = today.timetuple().tm_yday
if str(day_of_year) in data:
item = data[str(day_of_year)]
print(f" Today's item: {item.get('title', 'No title')}")
else:
print(f" No item for day {day_of_year}")
except Exception as e:
print(f"{filename}: Error - {e}")
print("Data files test completed!")
def test_config():
"""Test the configuration is valid."""
print("\nTesting configuration...")
config_manager = ConfigManager()
config = config_manager.load_config()
of_the_day_config = config.get('of_the_day', {})
if not of_the_day_config:
print("✗ No 'of_the_day' configuration found in config.json")
return
print(f"✓ OfTheDay configuration found")
print(f" Enabled: {of_the_day_config.get('enabled', False)}")
print(f" Update interval: {of_the_day_config.get('update_interval', 'Not set')}")
categories = of_the_day_config.get('categories', {})
print(f" Categories: {list(categories.keys())}")
for category_name, category_config in categories.items():
enabled = category_config.get('enabled', False)
data_file = category_config.get('data_file', 'Not set')
print(f" {category_name}: enabled={enabled}, data_file={data_file}")
# Check display duration
display_durations = config.get('display', {}).get('display_durations', {})
of_the_day_duration = display_durations.get('of_the_day', 'Not set')
print(f" Display duration: {of_the_day_duration} seconds")
print("Configuration test completed!")
if __name__ == "__main__":
print("=== OfTheDay System Test ===\n")
try:
test_config()
test_data_files()
test_of_the_day_manager()
print("\n=== All tests completed successfully! ===")
print("\nTo test the display on the Raspberry Pi, run:")
print("python3 run.py")
except Exception as e:
print(f"\n✗ Test failed with error: {e}")
import traceback
traceback.print_exc()