mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-04-10 21:03:01 +00:00
add Of The Day display
This commit is contained in:
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
329
of_the_day/OF_THE_DAY_GUIDE.md
Normal file
329
of_the_day/OF_THE_DAY_GUIDE.md
Normal 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
|
||||
1827
of_the_day/slovenian_word_of_the_day.json
Normal file
1827
of_the_day/slovenian_word_of_the_day.json
Normal file
File diff suppressed because it is too large
Load Diff
1827
of_the_day/word_of_the_day.json
Normal file
1827
of_the_day/word_of_the_day.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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
281
src/of_the_day_manager.py
Normal 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
138
test/test_of_the_day.py
Normal 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()
|
||||
Reference in New Issue
Block a user