11 Commits

Author SHA1 Message Date
Chuck
1cdcd43662 Update README.md
Signed-off-by: Chuck <33324927+ChuckBuilds@users.noreply.github.com>
2025-05-28 19:47:21 -05:00
Chuck
ac26b819ea Update README.md
Signed-off-by: Chuck <33324927+ChuckBuilds@users.noreply.github.com>
2025-05-27 22:10:56 -05:00
Chuck
a63313e5b8 only extract favorite teams from football 2025-05-27 17:49:54 -05:00
Chuck
8b15218b1e fixed linter errors in NFL and NCAA FB 2025-05-27 17:44:34 -05:00
Chuck
5329805f0b fix time error in NFL display 2025-05-27 17:35:54 -05:00
Chuck
d8ebe6764d added team_league_map for soccer display 2025-05-27 17:31:56 -05:00
ChuckBuilds
e13620e58d fix ncaa baseball log spam 2025-05-27 15:20:37 -05:00
Chuck
d47788d93c adjust default timings of config file 2025-05-26 21:49:28 -05:00
Chuck
bdca997263 remove outdated config example 2025-05-26 21:44:45 -05:00
Chuck
509c8e6fe3 remove rgb matrix from requirements txt 2025-05-26 21:14:35 -05:00
Chuck
7cbb3c7c00 Update README.md
Signed-off-by: Chuck <33324927+ChuckBuilds@users.noreply.github.com>
2025-05-26 19:00:43 -05:00
8 changed files with 458 additions and 339 deletions

614
README.md
View File

@@ -16,24 +16,31 @@ Modular, rotating Displays that can be individually enabled or disabled per the
### Time and Weather ### Time and Weather
- Real-time clock display - Real-time clock display
- ![DSC01342](https://github.com/user-attachments/assets/a3c9d678-b812-4977-8aa8-f0c3d1663f05) ![DSC01361](https://github.com/user-attachments/assets/c4487d40-5872-45f5-a553-debf8cea17e9)
- Current Weather, Daily Weather, and Hourly Weather Forecasts - Current Weather, Daily Weather, and Hourly Weather Forecasts
- ![DSC01332](https://github.com/user-attachments/assets/19b2182c-463c-458a-bf4e-5d04acd8d120) ![DSC01362](https://github.com/user-attachments/assets/d31df736-522f-4f61-9451-29151d69f164)
- ![DSC01335](https://github.com/user-attachments/assets/4bcae193-cbea-49da-aac4-72d6fc9a7acd) ![DSC01364](https://github.com/user-attachments/assets/eb2d16ad-6b12-49d9-ba41-e39a6a106682)
- ![DSC01333](https://github.com/user-attachments/assets/f0be5cf2-600e-4fae-a956-97327ef11d70) ![DSC01365](https://github.com/user-attachments/assets/f8a23426-e6fa-4774-8c87-19bb94cfbe73)
- Google Calendar event display - Google Calendar event display
![DSC01374-1](https://github.com/user-attachments/assets/5bc89917-876e-489d-b944-4d60274266e3)
### Sports Information ### Sports Information
The system supports live, recent, and upcoming game information for multiple sports leagues: The system supports live, recent, and upcoming game information for multiple sports leagues:
- NHL (Hockey) - NHL (Hockey)
- ![DSC01347](https://github.com/user-attachments/assets/854b3e63-43f5-4bf9-8fed-14a96cf3e7dd) ![DSC01356](https://github.com/user-attachments/assets/64c359b6-4b99-4dee-aca0-b74debda30e0)
- ![DSC01339](https://github.com/user-attachments/assets/13aacd18-c912-439b-a2f4-82a7ec7a2831) ![DSC01339](https://github.com/user-attachments/assets/2ccc52af-b4ed-4c06-a341-581506c02153)
- ![DSC01338](https://github.com/user-attachments/assets/8fbc8251-f573-4e2b-b981-428d6ff3ac61) ![DSC01337](https://github.com/user-attachments/assets/f4faf678-9f43-4d37-be56-89ecbd09acf6)
- NBA (Basketball) - NBA (Basketball)
- MLB (Baseball) - MLB (Baseball)
- ![DSC01346](https://github.com/user-attachments/assets/fb82b662-98f8-499c-aaf8-f9241dc3d634) ![DSC01359](https://github.com/user-attachments/assets/71e985f1-d2c9-4f0e-8ea1-13eaefeec01c)
- ![DSC01341](https://github.com/user-attachments/assets/f79cbf2e-f3b4-4a14-8482-01f2a3d53963)
- NFL (Football) - NFL (Football)
- NCAA Football - NCAA Football
- NCAA Men's Basketball - NCAA Men's Basketball
@@ -45,7 +52,9 @@ The system supports live, recent, and upcoming game information for multiple spo
- Near real-time stock & crypto price updates - Near real-time stock & crypto price updates
- Stock news headlines - Stock news headlines
- Customizable stock & crypto watchlists - Customizable stock & crypto watchlists
- ![DSC01317](https://github.com/user-attachments/assets/01a01ecf-bef1-4f61-a7b2-d6658622f73d) ![DSC01366](https://github.com/user-attachments/assets/95b67f50-0f69-4479-89d0-1d87c3daefd3)
![DSC01368](https://github.com/user-attachments/assets/c4b75546-388b-4d4a-8b8c-8c5a62f139f9)
### Entertainment ### Entertainment
@@ -54,15 +63,19 @@ The system supports live, recent, and upcoming game information for multiple spo
- YouTube Music integration - YouTube Music integration
- Album art display - Album art display
- Now playing information with scrolling text - Now playing information with scrolling text
- ![DSC01354](https://github.com/user-attachments/assets/41b9e45f-8946-4213-87d2-6657b7f05757) ![DSC01354](https://github.com/user-attachments/assets/7524b149-f55d-4eb7-b6c6-6e336e0d1ac1)
![DSC01389](https://github.com/user-attachments/assets/3f768651-5446-4ff5-9357-129cd8b3900d)
### Custom Display Features ### Custom Display Features
- Custom Text display - Custom Text display
![DSC01379](https://github.com/user-attachments/assets/338b7578-9d4b-4465-851c-7e6a1d999e07)
- Youtube Subscriber Count Display - Youtube Subscriber Count Display
- ![DSC01319](https://github.com/user-attachments/assets/4d80fe99-839b-4d5e-9908-149cf1cce107) ![DSC01376](https://github.com/user-attachments/assets/7ea5f42d-afce-422f-aa97-6b2a179aa7d2)
- Font testing and customization
- Configurable display modes - Font testing Display (not in rotation)
## System Architecture ## System Architecture
@@ -84,46 +97,88 @@ The system can be configured through a JSON configuration file that allows users
## Hardware Requirements ## Hardware Requirements
- Raspberry Pi 3b or 4 (NOT RPI5!) - Raspberry Pi 3b or 4 (NOT RPI5!) : Amazon Affiliate Link: Raspberry Pi 4 4GB (https://amzn.to/4dJixuX)
-- Amazon Affiliate Link: Raspberry Pi 4 4GB (https://amzn.to/4dJixuX) - Adafruit RGB Matrix Bonnet/HAT : https://www.adafruit.com/product/3211
- Adafruit RGB Matrix Bonnet/HAT - 2x LED Matrix panels (64x32) (Designed for 128x32 but has a lot of dynamic scaling elements that could work on a variety of displays, pixel pitch is user preference) : https://www.adafruit.com/product/2278
-- https://www.adafruit.com/product/3211 - 5V 4A DC Power Supply for Adafruit RGB HAT : https://www.adafruit.com/product/1466
- 2x LED Matrix panels (64x32) (Designed for 128x32 but has a lot of dynamic scaling elements that could work on a variety of displays, pixel pitch is user preference)
-- https://www.adafruit.com/product/2278
- DC Power Supply for Adafruit RGB HAT
-- https://www.adafruit.com/product/658
## Optional but recommended mod for Adafruit RGB Matrix Bonnet ## Optional but recommended mod for Adafruit RGB Matrix Bonnet
- By soldering a jumper between pins 4 and 18, you can run a specialized command for polling the matrix display. This provides better brightness, less flicker, and better color. - By soldering a jumper between pins 4 and 18, you can run a specialized command for polling the matrix display. This provides better brightness, less flicker, and better color.
- If you do the mod, we will use the command: --led-gpio-mapping=adafruit-hat-pwm, otherwise just use --led-gpio-mapping=adafruit-hat - If you do the mod, we will use the default config with led-gpio-mapping=adafruit-hat-pwm, otherwise just adjust your mapping in config.json to adafruit-hat
- More information available: https://github.com/hzeller/rpi-rgb-led-matrix/tree/master?tab=readme-ov-file - More information available: https://github.com/hzeller/rpi-rgb-led-matrix/tree/master?tab=readme-ov-file
![DSC00079](https://github.com/user-attachments/assets/4282d07d-dfa2-4546-8422-ff1f3a9c0703) ![DSC00079](https://github.com/user-attachments/assets/4282d07d-dfa2-4546-8422-ff1f3a9c0703)
-----------------------------------------------------------------------------------
## Mount/Stand
I 3D printed stands to keep the panels upright and snug. STL Files are included in the Repo but are also available at https://www.thingiverse.com/thing:5169867 Thanks to "Randomwire" for making these for the 4mm Pixel Pitch LED Matrix.
These are not required and you can probably rig up something basic with stuff you have around the house. I used these screws: https://amzn.to/4mFwNJp (Amazon Affiliate Link)
Overall 2 Matrix display with Rpi connected. -----------------------------------------------------------------------------------
2 Matrix display with Rpi connected.
![DSC00073](https://github.com/user-attachments/assets/a0e167ae-37c6-4db9-b9ce-a2b957ca1a67) ![DSC00073](https://github.com/user-attachments/assets/a0e167ae-37c6-4db9-b9ce-a2b957ca1a67)
-----------------------------------------------------------------------------------
# Preparing the Raspberry Pi
1. Create RPI Image on a Micro-SD card (I use 16gb because I have it, size is not too important but I would use 8gb or more) using [Raspberry Pi Imager](https://www.raspberrypi.com/software/)
2. Choose your Raspberry Pi (3B+ in my case)
3. For Operating System (OS), choose "Other", then choose Raspbian OS Lite (64-bit)
4. For Storage, choose your micro-sd card
![image](https://github.com/user-attachments/assets/05580e0a-86d5-4613-aadc-93207365c38f)
5. Press Next then Edit Settings
![image](https://github.com/user-attachments/assets/b392a2c9-6bf4-47d5-84b7-63a5f793a1df)
6. Inside the OS Customization Settings, choose a name for your device. I use "ledpi". Choose a password, enter your WiFi information, and set your timezone.
![image](https://github.com/user-attachments/assets/0c250e3e-ab3c-4f3c-ba60-6884121ab176)
7. Under the Services Tab, make sure that SSH is enabled. I recommend using password authentication for ease of use - it is the password you just chose above.
![image](https://github.com/user-attachments/assets/1d78d872-7bb1-466e-afb6-0ca26288673b)
8. Then Click "Save" and Agree to Overwrite the Micro-SD card.
----------------------------------------------------------------------------------- -----------------------------------------------------------------------------------
## Installation # System Setup & Installation
1. Clone this repository: 1. Open PowerShell and ssh into your Raspberry Pi with ledpi@ledpi (or Username@Hostname)
```bash
ssh ledpi@ledpi
```
2. Update repositories, upgrade raspberry pi OS, install git
```bash
sudo apt update && sudo apt upgrade -y
sudo apt install -y git python3-pip cython3 build-essential python3-dev python3-pillow scons
```
3. Clone this repository:
```bash ```bash
git clone https://github.com/ChuckBuilds/LEDMatrix.git git clone https://github.com/ChuckBuilds/LEDMatrix.git
cd LEDMatrix cd LEDMatrix
``` ```
2. Install dependencies: 4. Install dependencies:
```bash ```bash
pip3 install --break-system-packages -r requirements.txt sudo pip3 install --break-system-packages -r requirements.txt
``` ```
--break-system-packages allows us to install without a virtual environment --break-system-packages allows us to install without a virtual environment
5. Install rpi-rgb-led-matrix dependencies:
```bash
cd rpi-rgb-led-matrix-master
```
```bash
sudo make build-python PYTHON=$(which python3)
```
```bash
cd bindings/python
sudo python3 setup.py install
```
Test it with:
```bash
python3 -c 'from rgbmatrix import RGBMatrix, RGBMatrixOptions; print("Success!")'
```
## Important: Sound Module Configuration ## Important: Sound Module Configuration
1. Remove unnecessary services that might interfere with the LED matrix: 1. Remove unnecessary services that might interfere with the LED matrix:
@@ -136,7 +191,11 @@ sudo apt-get remove bluez bluez-firmware pi-bluetooth triggerhappy pigpio
cat <<EOF | sudo tee /etc/modprobe.d/blacklist-rgb-matrix.conf cat <<EOF | sudo tee /etc/modprobe.d/blacklist-rgb-matrix.conf
blacklist snd_bcm2835 blacklist snd_bcm2835
EOF EOF
```
then execute
```bash
sudo update-initramfs -u sudo update-initramfs -u
``` ```
@@ -156,23 +215,237 @@ sudo nano /boot/firmware/cmdline.txt
2. Add `isolcpus=3` at the end of the line 2. Add `isolcpus=3` at the end of the line
3. Add `dtparam=audio=off` at the end of the line 3. Ctrl + X to exit, Y to save, Enter to Confirm
4. Ctrl + X to exit, Y to save 4. Edit /boot/firmware/config.txt with
```bash
sudo nano /boot/firmware/config.txt
```
5. Save and reboot: 6. Edit the `dtparam=audio=on` section to `dtparam=audio=off`
7. Ctrl + X to exit, Y to save, Enter to Confirm
8. Save and reboot:
```bash ```bash
sudo reboot sudo reboot
``` ```
-----------------------------------------------------------------------------------
## Configuration
1.Edit `config/config.json` with your preferences via `sudo nano config/config.json`
###API Keys
For sensitive settings like API keys:
Copy the template: `cp config/config_secrets.template.json config/config_secrets.json`
Edit `config/config_secrets.json` with your API keys via `sudo nano config/config_secrets.json`
Ctrl + X to exit, Y to overwrite, Enter to Confirm
Everything is configured via `config/config.json` and `config/config_secrets.json`.
-----------------------------------------------------------------------------------
## 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: TV and Limited Input Device
- 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:
- Provide a code to enter at https://www.google.com/device 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)
## Music Display Configuration
The Music Display module shows information about the currently playing track from either Spotify or YouTube Music (via the [YouTube Music Desktop App](https://ytmdesktop.app/) companion server).
**Setup Requirements:**
1. **Spotify:**
* Requires a Spotify account (for API access).
* You need to register an application on the [Spotify Developer Dashboard](https://developer.spotify.com/dashboard/) to get API credentials.
* Go to the dashboard, log in, and click "Create App".
* Give it a name (e.g., "LEDMatrix Display") and description.
* For the "Redirect URI", enter `http://127.0.0.1:8888/callback` (or another unused port if 8888 is taken). You **must** add this exact URI in your app settings on the Spotify dashboard.
* Note down the `Client ID` and `Client Secret`.
2. **YouTube Music (YTM):**
* Requires the [YouTube Music Desktop App](https://ytmdesktop.app/) (YTMD) to be installed and running on a computer on the *same network* as the Raspberry Pi.
* In YTMD settings, enable the "Companion Server" under Integration options. Note the URL it provides (usually `http://localhost:9863` if running on the same machine, or `http://<YTMD-Computer-IP>:9863` if running on a different computer).
**`preferred_source` Options:**
* `"spotify"`: Only uses Spotify. Ignores YTM.
* `"ytm"`: Only uses the YTM Companion Server. Ignores Spotify.
### Spotify Authentication for Music Display
If you are using the Spotify integration to display currently playing music, you will need to authenticate with Spotify. This project uses an authentication flow that requires a one-time setup. Due to how the display controller script may run with specific user permissions (even when using `sudo`), the following steps are crucial:
1. **Initial Setup & Secrets:**
* Ensure you have your Spotify API Client ID, Client Secret, and Redirect URI.
* The Redirect URI should be set to `http://127.0.0.1:8888/callback` in your Spotify Developer Dashboard.
* Copy `config/config_secrets.template.json` to `config/config_secrets.json`.
* Edit `config/config_secrets.json` and fill in your Spotify credentials under the `"music"` section:
```json
{
"music": {
"SPOTIFY_CLIENT_ID": "YOUR_SPOTIFY_CLIENT_ID",
"SPOTIFY_CLIENT_SECRET": "YOUR_SPOTIFY_CLIENT_SECRET",
"SPOTIFY_REDIRECT_URI": "http://127.0.0.1:8888/callback"
}
}
```
2. **Run the Authentication Script:**
* Execute the authentication script using `sudo`. This is important because it needs to create an authentication cache file (`spotify_auth.json`) that will be owned by root.
```bash
sudo python3 src/authenticate_spotify.py
```
* The script will output a URL. Copy this URL and paste it into a web browser on any device.
* Log in to Spotify and authorize the application.
* Your browser will be redirected to a URL starting with `http://127.0.0.1:8888/callback?code=...`. It will likely show an error page like "This site can't be reached" this is expected.
* Copy the **entire** redirected URL from your browser's address bar.
* Paste this full URL back into the terminal when prompted by the script.
* If successful, it will indicate that token info has been cached.
3. **Adjust Cache File Permissions:**
* The main display script (`display_controller.py`), even when run with `sudo`, might operate with an effective User ID (e.g., UID 1 for 'daemon') that doesn't have permission to read the `spotify_auth.json` file created by `root` (which has -rw------- permissions by default).
* To allow the display script to read this cache file, change its permissions:
```bash
sudo chmod 644 config/spotify_auth.json
```
This makes the file readable by all users, including the effective user of the display script.
4. **Run the Main Application:**
* You should now be able to run your main display controller script using `sudo`:
```bash
sudo python3 display_controller.py
```
* The Spotify client should now authenticate successfully using the cached token.
**Why these specific permissions steps?**
The `authenticate_spotify.py` script, when run with `sudo`, creates `config/spotify_auth.json` owned by `root`. If the main `display_controller.py` (also run with `sudo`) effectively runs as a different user (e.g., UID 1/daemon, as observed during troubleshooting), that user won't be able to read the `root`-owned file unless its permissions are relaxed (e.g., to `644`). The `chmod 644` command allows the owner (`root`) to read/write, and everyone else (including the `daemon` user) to read.
### Youtube Music Authentication for Music Display
The system can display currently playing music information from [YouTube Music Desktop (YTMD)](https://ytmdesktop.app/) via its Companion server API.
### YouTube Display Configuration & API Key
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
**Setup:**
1. **Enable Companion Server in YTMD:**
* In the YouTube Music Desktop application, go to `Settings` -> `Integrations`.
* Enable the "Companion Server".
* Note the IP address and Port it's listening on (default is usually `http://localhost:9863`), you'll need to know the local ip address if playing music on a device other than your rpi (probably are).
2. **Configure `config/config.json`:**
* Update the `music` section in your `config/config.json`:
```json
"music": {
"enabled": true,
"preferred_source": "ytm",
"YTM_COMPANION_URL": "http://YOUR_YTMD_IP_ADDRESS:PORT", // e.g., "http://localhost:9863" or "http://192.168.1.100:9863"
"POLLING_INTERVAL_SECONDS": 1
}
```
3. **Initial Authentication & Token Storage:**
* The first time you run ` python3 src/authenticate_ytm.py` after enabling YTM, it will attempt to register itself with the YTMD Companion Server.
* You will see log messages in the terminal prompting you to **approve the "LEDMatrixController" application within the YouTube Music Desktop app.** You typically have 30 seconds to do this.
* Once approved, an authentication token is saved to your `config/ytm_auth.json`.
* This ensures the `ledpi` user owns the config directory and file, and has the necessary write permissions.
**Troubleshooting:**
* "No authorized companions" in YTMD: Ensure you've approved the `LEDMatrixController` in YTMD settings after the first run.
* Connection errors: Double-check the `YTM_COMPANION_URL` in `config.json` matches what YTMD's companion server is set to.
* Ensure your firewall (Windows Firewall) allows YTM Desktop app to access local networks.
-----------------------------------------------------------------------------------
## Before Running the Display
- To allow the script to properly access fonts, you need to set the correct permissions on your home directory:
```bash
sudo chmod o+x /home/ledpi
```
- Replace ledpi with your actual username, if different.
You can confirm your username by executing:
`whoami`
## Running the Display ## Running the Display
From the project root directory: From the project root directory:
```bash ```bash
sudo python3 display_controller.py sudo python3 display_controller.py
``` ```
This will start the display cycle but only stays active as long as your ssh session is active.
## Systemd Service Installation
-----------------------------------------------------------------------------------
## Run on Startup Automatically with Systemd Service Installation
The LEDMatrix can be installed as a systemd service to run automatically at boot and be managed easily. The service runs as root to ensure proper hardware timing access for the LED matrix. The LEDMatrix can be installed as a systemd service to run automatically at boot and be managed easily. The service runs as root to ensure proper hardware timing access for the LED matrix.
@@ -236,208 +509,72 @@ sudo ./start_display.sh
sudo ./stop_display.sh sudo ./stop_display.sh
``` ```
# Configuration
1.Edit `config/config.json` with your preferences via `sudo nano config/config.json` -----------------------------------------------------------------------------------
## Custom Fonts
You can add any font to the assets/fonts/ folder but they need to be .ttf or .btf(less support) and updated in display_manager.py
## API Keys
For sensitive settings like API keys: -----------------------------------------------------------------------------------
1. Copy the template: `cp config/config_secrets.template.json config/config_secrets.json`
2. Edit `config/config_secrets.json` with your API keys via `sudo nano config/config_secrets.json`
3. Ctrl + X to exit, Y to overwrite, Enter to save
## NHL, NBA, MLB, Soccer, NCAA FB, NCAA Men's Baseball, NCAA Men's Basketball Scoreboard Display
The LEDMatrix system includes a comprehensive scoreboard display system with three display modes:
### Display Modes
- **Live Games**: Shows currently playing games with live scores and game status
- **Recent Games**: Displays completed games from the last 48 hours (configurable)
- **Upcoming Games**: Shows scheduled games for favorite teams
### Features
- Real-time score updates from ESPN API
- Team logo display
- Game status indicators (period, time remaining)
- Configurable favorite teams
- Automatic game switching
- Built-in caching to reduce API calls
- Test mode for development
### YouTube Display Configuration ### Running the display without Sudo (Not recommended but can be useful for troubleshooting or overcoming write errors)
The YouTube display module shows channel statistics for a specified YouTube channel. To configure it: To run the display script without `sudo`, the user executing the script needs access to GPIO pins. Add the user to the `gpio` group:
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)
### Music Display Configuration
The Music Display module shows information about the currently playing track from either Spotify or YouTube Music (via the [YouTube Music Desktop App](https://ytmdesktop.app/) companion server).
**Setup Requirements:**
1. **Spotify:**
* Requires a Spotify account (for API access).
* You need to register an application on the [Spotify Developer Dashboard](https://developer.spotify.com/dashboard/) to get API credentials.
* Go to the dashboard, log in, and click "Create App".
* Give it a name (e.g., "LEDMatrix Display") and description.
* For the "Redirect URI", enter `http://127.0.0.1:8888/callback` (or another unused port if 8888 is taken). You **must** add this exact URI in your app settings on the Spotify dashboard.
* Note down the `Client ID` and `Client Secret`.
2. **YouTube Music (YTM):**
* Requires the [YouTube Music Desktop App](https://ytmdesktop.app/) (YTMD) to be installed and running on a computer on the *same network* as the Raspberry Pi.
* In YTMD settings, enable the "Companion Server" under Integration options. Note the URL it provides (usually `http://localhost:9863` if running on the same machine, or `http://<YTMD-Computer-IP>:9863` if running on a different computer).
**`preferred_source` Options:**
* `"spotify"`: Only uses Spotify. Ignores YTM.
* `"ytm"`: Only uses the YTM Companion Server. Ignores Spotify.
## Spotify Authentication for Music Display
If you are using the Spotify integration to display currently playing music, you will need to authenticate with Spotify. This project uses an authentication flow that requires a one-time setup. Due to how the display controller script may run with specific user permissions (even when using `sudo`), the following steps are crucial:
1. **Initial Setup & Secrets:**
* Ensure you have your Spotify API Client ID, Client Secret, and Redirect URI.
* The Redirect URI should be set to `http://127.0.0.1:8888/callback` in your Spotify Developer Dashboard.
* Copy `config/config_secrets.template.json` to `config/config_secrets.json`.
* Edit `config/config_secrets.json` and fill in your Spotify credentials under the `"music"` section:
```json
{
"music": {
"SPOTIFY_CLIENT_ID": "YOUR_SPOTIFY_CLIENT_ID",
"SPOTIFY_CLIENT_SECRET": "YOUR_SPOTIFY_CLIENT_SECRET",
"SPOTIFY_REDIRECT_URI": "http://127.0.0.1:8888/callback"
}
}
```
2. **Run the Authentication Script:**
* Execute the authentication script using `sudo`. This is important because it needs to create an authentication cache file (`spotify_auth.json`) that will be owned by root.
```bash ```bash
sudo python3 src/authenticate_spotify.py sudo usermod -a -G gpio <your_username>
# Example for user 'ledpi':
# sudo usermod -a -G gpio ledpi
``` ```
* The script will output a URL. Copy this URL and paste it into a web browser on any device.
* Log in to Spotify and authorize the application.
* Your browser will be redirected to a URL starting with `http://127.0.0.1:8888/callback?code=...`. It will likely show an error page like "This site can't be reached" this is expected.
* Copy the **entire** redirected URL from your browser's address bar.
* Paste this full URL back into the terminal when prompted by the script.
* If successful, it will indicate that token info has been cached.
3. **Adjust Cache File Permissions:** **Important:** You must **reboot** the Raspberry Pi after adding the user to the group for the change to take effect.
* The main display script (`display_controller.py`), even when run with `sudo`, might operate with an effective User ID (e.g., UID 1 for 'daemon') that doesn't have permission to read the `spotify_auth.json` file created by `root` (which has -rw------- permissions by default).
* To allow the display script to read this cache file, change its permissions: You also need to disable hardware pulsing in the code (see `src/display_manager.py`, set `options.disable_hardware_pulsing = True`). This may result in a flickerying display
If configured correctly, you can then run:
```bash ```bash
sudo chmod 644 config/spotify_auth.json python3 display_controller.py
``` ```
This makes the file readable by all users, including the effective user of the display script. -----------------------------------------------------------------------------------
## Display Settings
If you are copying my setup, you can likely leave this alone.
- hardware: Configures how the matrix is driven.
- rows, cols, chain_length: Physical panel configuration.
- brightness: Display brightness (0100).
- hardware_mapping: Use "adafruit-hat-pwm" for Adafruit bonnet WITH the jumper mod. Remove -pwm if you did not solder the jumper.
- pwm_bits, pwm_dither_bits, pwm_lsb_nanoseconds: Affect color fidelity.
- limit_refresh_rate_hz: Cap refresh rate for better stability.
- runtime:
- gpio_slowdown: Tweak this depending on your Pi model. Match it to the generation (e.g., Pi 3 → 3, Pi 4 -> 4).
- display_durations:
- Control how long each display module stays visible in seconds. For example, if you want more focus on stocks, increase that value.
### Modules
- Each module (weather, stocks, crypto, calendar, etc.) has enabled, update_interval, and often display_format settings.
- Sports modules also support test_mode, live_update_interval, and favorite_teams.
- Logos are loaded from the logo_dir path under assets/sports/...
4. **Run the Main Application:**
* You should now be able to run your main display controller script using `sudo`:
```bash ```bash
sudo python3 display_controller.py Example: NHL Configuration"nhl_scoreboard": {
```
* The Spotify client should now authenticate successfully using the cached token.
**Why these specific permissions steps?**
The `authenticate_spotify.py` script, when run with `sudo`, creates `config/spotify_auth.json` owned by `root`. If the main `display_controller.py` (also run with `sudo`) effectively runs as a different user (e.g., UID 1/daemon, as observed during troubleshooting), that user won't be able to read the `root`-owned file unless its permissions are relaxed (e.g., to `644`). The `chmod 644` command allows the owner (`root`) to read/write, and everyone else (including the `daemon` user) to read.
### Music Display (YouTube Music)
The system can display currently playing music information from YouTube Music Desktop (YTMD) via its Companion server API.
**Setup:**
1. **Enable Companion Server in YTMD:**
* In the YouTube Music Desktop application, go to `Settings` -> `Integrations`.
* Enable the "Companion Server".
* Note the IP address and Port it's listening on (default is usually `http://localhost:9863`), you'll need to know the local ip address if playing music on a device other than your rpi (probably are).
2. **Configure `config/config.json`:**
* Update the `music` section in your `config/config.json`:
```json
"music": {
"enabled": true, "enabled": true,
"preferred_source": "ytm", "test_mode": false,
"YTM_COMPANION_URL": "http://YOUR_YTMD_IP_ADDRESS:PORT", // e.g., "http://localhost:9863" or "http://192.168.1.100:9863" "update_interval_seconds": 300,
"POLLING_INTERVAL_SECONDS": 1 "live_update_interval": 15,
"recent_game_hours": 48,
"favorite_teams": ["TB", "DAL"],
"logo_dir": "assets/sports/nhl_logos",
"display_modes": {
"nhl_live": true,
"nhl_recent": true,
"nhl_upcoming": true
}
} }
``` ```
3. **Initial Authentication & Token Storage:**
* The first time you run ` python3 src/authenticate_ytm.py` after enabling YTM, it will attempt to register itself with the YTMD Companion Server.
* You will see log messages in the terminal prompting you to **approve the "LEDMatrixController" application within the YouTube Music Desktop app.** You typically have 30 seconds to do this.
* Once approved, an authentication token is saved to your `config/ytm_auth.json`.
* This ensures the `ledpi` user owns the config directory and file, and has the necessary write permissions.
**Troubleshooting:**
* "No authorized companions" in YTMD: Ensure you've approved the `LEDMatrixController` in YTMD settings after the first run.
* Connection errors: Double-check the `YTM_COMPANION_URL` in `config.json` matches what YTMD's companion server is set to.
* Ensure your firewall (Windows Firewall) allows YTM Desktop app to access local networks.
## Project Structure ## Project Structure
@@ -498,6 +635,22 @@ The project is organized into several key components:
Each display module in `src/` is responsible for a specific feature (weather, sports, music, etc.) and follows a consistent pattern of data fetching, processing, and display rendering. Each display module in `src/` is responsible for a specific feature (weather, sports, music, etc.) and follows a consistent pattern of data fetching, processing, and display rendering.
## NHL, NBA, MLB, Soccer, NCAA FB, NCAA Men's Baseball, NCAA Men's Basketball Scoreboard Display
The LEDMatrix system includes a comprehensive scoreboard display system with three display modes:
### Display Modes
- **Live Games**: Shows currently playing games with live scores and game status
- **Recent Games**: Displays completed games from the last 48 hours (configurable)
- **Upcoming Games**: Shows scheduled games for favorite teams
### Features
- Real-time score updates from ESPN API
- Team logo display
- Game status indicators (period, time remaining)
- Configurable favorite teams
- Automatic game switching
- Built-in caching to reduce API calls
- Test mode for development
## Caching System ## Caching System
The LEDMatrix system includes a robust caching mechanism to optimize API calls and reduce network traffic: The LEDMatrix system includes a robust caching mechanism to optimize API calls and reduce network traffic:
@@ -529,25 +682,18 @@ The LEDMatrix system includes a robust caching mechanism to optimize API calls a
- Temporary files are used for safe updates - Temporary files are used for safe updates
- JSON serialization handles all data types including timestamps - JSON serialization handles all data types including timestamps
## Fonts ## Final Notes
You can add any font to the assets/fonts/ folder but they need to be .ttf or .btf(less support) and updated in display_manager.py - Most configuration is done via config/config.json
- Refresh intervals for sports/weather/stocks are customizable
- A caching system reduces API strain and helps ensure the display doesnt hammer external services (and ruin it for everyone)
- Font files should be placed in assets/fonts/
- You can test each module individually for debugging
### Running without Sudo (Optional and not recommended)
To run the display script without `sudo`, the user executing the script needs access to GPIO pins. Add the user to the `gpio` group: ##What's Next?
- Adding MQTT/HomeAssistant integration
- Gambling odds?
- Building a user-friendly UI for easier configuration
```bash
sudo usermod -a -G gpio <your_username>
# Example for user 'ledpi':
# sudo usermod -a -G gpio ledpi
```
**Important:** You must **reboot** the Raspberry Pi after adding the user to the group for the change to take effect. ### If youve read this far — thanks!
You also need to disable hardware pulsing in the code (see `src/display_manager.py`, set `options.disable_hardware_pulsing = True`). This may result in a flickerying display
If configured correctly, you can then run:
```bash
python3 display_controller.py
```

View File

@@ -0,0 +1,4 @@
{
"last_updated": 0,
"map": {}
}

View File

@@ -1,69 +0,0 @@
{
"matrix": {
"width": 64,
"height": 32,
"brightness": 100
},
"display": {
"enabled": true,
"display_durations": {
"clock": 15,
"weather": 15,
"stocks": 45,
"calendar": 30,
"stock_news": 30
}
},
"clock": {
"enabled": true,
"format": "%I:%M:%S %p"
},
"weather": {
"enabled": true,
"api_key": "your_openweather_api_key",
"update_interval": 300,
"units": "imperial"
},
"location": {
"lat": 0.0,
"lon": 0.0,
"city": "Your City"
},
"stocks": {
"enabled": true,
"symbols": ["AAPL", "GOOGL", "MSFT"],
"update_interval": 300
},
"stock_news": {
"enabled": true,
"symbols": ["AAPL", "GOOGL", "MSFT"],
"update_interval": 900,
"max_headlines": 3
},
"calendar": {
"enabled": true,
"credentials_file": "credentials.json",
"token_file": "token.pickle",
"update_interval": 300,
"max_events": 3,
"calendars": ["primary"]
},
"mqtt": {
"broker": "homeassistant.local",
"port": 1883,
"username": "your_mqtt_username",
"password": "your_mqtt_password",
"client_id": "led_matrix",
"use_tls": false,
"topics": [
"homeassistant/sensor/#",
"homeassistant/binary_sensor/#",
"homeassistant/switch/#"
],
"display": {
"scroll_speed": 0.1,
"message_timeout": 60,
"max_messages": 5
}
}
}

View File

@@ -69,13 +69,13 @@
}, },
"weather": { "weather": {
"enabled": true, "enabled": true,
"update_interval": 600, "update_interval": 1800,
"units": "imperial", "units": "imperial",
"display_format": "{temp}°F\n{condition}" "display_format": "{temp}°F\n{condition}"
}, },
"stocks": { "stocks": {
"enabled": true, "enabled": true,
"update_interval": 300, "update_interval": 600,
"symbols": [ "symbols": [
"ASTS", "SCHD", "INTC", "NVDA", "T", "VOO", "SMCI" "ASTS", "SCHD", "INTC", "NVDA", "T", "VOO", "SMCI"
], ],
@@ -83,7 +83,7 @@
}, },
"crypto": { "crypto": {
"enabled": true, "enabled": true,
"update_interval": 300, "update_interval": 600,
"symbols": [ "symbols": [
"BTC-USD", "ETH-USD" "BTC-USD", "ETH-USD"
], ],
@@ -108,7 +108,7 @@
"nhl_scoreboard": { "nhl_scoreboard": {
"enabled": true, "enabled": true,
"test_mode": false, "test_mode": false,
"update_interval_seconds": 300, "update_interval_seconds": 3600,
"live_update_interval": 15, "live_update_interval": 15,
"recent_update_interval": 3600, "recent_update_interval": 3600,
"upcoming_update_interval": 3600, "upcoming_update_interval": 3600,
@@ -125,7 +125,7 @@
"enabled": false, "enabled": false,
"test_mode": false, "test_mode": false,
"update_interval_seconds": 3600, "update_interval_seconds": 3600,
"live_update_interval": 20, "live_update_interval": 15,
"recent_update_interval": 3600, "recent_update_interval": 3600,
"upcoming_update_interval": 3600, "upcoming_update_interval": 3600,
"recent_game_hours": 72, "recent_game_hours": 72,
@@ -204,8 +204,8 @@
"mlb": { "mlb": {
"enabled": true, "enabled": true,
"test_mode": false, "test_mode": false,
"update_interval_seconds": 300, "update_interval_seconds": 3600,
"live_update_interval": 20, "live_update_interval": 15,
"recent_update_interval": 3600, "recent_update_interval": 3600,
"upcoming_update_interval": 3600, "upcoming_update_interval": 3600,
"recent_game_hours": 48, "recent_game_hours": 48,
@@ -231,7 +231,7 @@
"soccer_scoreboard": { "soccer_scoreboard": {
"enabled": false, "enabled": false,
"test_mode": false, "test_mode": false,
"update_interval_seconds": 300, "update_interval_seconds": 3600,
"live_update_interval": 20, "live_update_interval": 20,
"recent_update_interval": 3600, "recent_update_interval": 3600,
"upcoming_update_interval": 3600, "upcoming_update_interval": 3600,

View File

@@ -3,7 +3,6 @@ pytz==2023.3
requests>=2.32.0 requests>=2.32.0
timezonefinder==6.2.0 timezonefinder==6.2.0
geopy==2.4.1 geopy==2.4.1
rgbmatrix
google-auth-oauthlib==1.0.0 google-auth-oauthlib==1.0.0
google-auth-httplib2==0.1.0 google-auth-httplib2==0.1.0
google-api-python-client==2.86.0 google-api-python-client==2.86.0

View File

@@ -643,10 +643,13 @@ class NCAABaseballRecentManager(BaseNCAABaseballManager):
current_time = time.time() current_time = time.time()
if current_time - self.last_update < self.update_interval: if current_time - self.last_update < self.update_interval:
return return
self.last_update = current_time
try: try:
games = self._fetch_ncaa_baseball_api_data() games = self._fetch_ncaa_baseball_api_data()
if not games: if not games:
logger.warning("[NCAABaseball] No games returned from API") logger.warning("[NCAABaseball] No games returned from API")
self.recent_games = []
self.current_game = None
return return
new_recent_games = [] new_recent_games = []
@@ -688,7 +691,6 @@ class NCAABaseballRecentManager(BaseNCAABaseballManager):
self.recent_games = [] self.recent_games = []
self.current_game = None self.current_game = None
self.last_update = current_time
except Exception as e: except Exception as e:
logger.error(f"[NCAABaseball] Error updating recent games: {e}", exc_info=True) logger.error(f"[NCAABaseball] Error updating recent games: {e}", exc_info=True)
@@ -740,6 +742,7 @@ class NCAABaseballUpcomingManager(BaseNCAABaseballManager):
current_time = time.time() current_time = time.time()
if current_time - self.last_update < self.update_interval: if current_time - self.last_update < self.update_interval:
return return
self.last_update = current_time
try: try:
games = self._fetch_ncaa_baseball_api_data() games = self._fetch_ncaa_baseball_api_data()
if games: if games:
@@ -780,7 +783,6 @@ class NCAABaseballUpcomingManager(BaseNCAABaseballManager):
self.upcoming_games = [] self.upcoming_games = []
self.current_game = None self.current_game = None
self.last_update = current_time
except Exception as e: except Exception as e:
logger.error(f"[NCAABaseball] Error updating upcoming games: {e}", exc_info=True) logger.error(f"[NCAABaseball] Error updating upcoming games: {e}", exc_info=True)

View File

@@ -293,6 +293,14 @@ class BaseNCAAFBManager: # Renamed class
self.logger.warning(f"[NCAAFB] Could not find home or away team in event: {game_event.get('id')}") self.logger.warning(f"[NCAAFB] Could not find home or away team in event: {game_event.get('id')}")
return None return None
home_abbr = home_team["team"]["abbreviation"]
away_abbr = away_team["team"]["abbreviation"]
# Filter by favorite teams if the list is not empty
if self.favorite_teams and not (home_abbr in self.favorite_teams or away_abbr in self.favorite_teams):
self.logger.debug(f"[NCAAFB] Skipping non-favorite game: {away_abbr}@{home_abbr} in _extract_game_details")
return None
game_time, game_date = "", "" game_time, game_date = "", ""
if start_time_utc: if start_time_utc:
local_time = start_time_utc.astimezone() local_time = start_time_utc.astimezone()
@@ -349,13 +357,13 @@ class BaseNCAAFBManager: # Renamed class
"is_final": status["type"]["state"] == "post", "is_final": status["type"]["state"] == "post",
"is_upcoming": status["type"]["state"] == "pre", "is_upcoming": status["type"]["state"] == "pre",
"is_halftime": status["type"]["state"] == "halftime", # Added halftime check "is_halftime": status["type"]["state"] == "halftime", # Added halftime check
"home_abbr": home_team["team"]["abbreviation"], "home_abbr": home_abbr,
"home_score": home_team.get("score", "0"), "home_score": home_team.get("score", "0"),
"home_logo_path": os.path.join(self.logo_dir, f"{home_team['team']['abbreviation']}.png"), "home_logo_path": os.path.join(self.logo_dir, f"{home_abbr}.png"),
"home_timeouts": home_timeouts, "home_timeouts": home_timeouts,
"away_abbr": away_team["team"]["abbreviation"], "away_abbr": away_abbr,
"away_score": away_team.get("score", "0"), "away_score": away_team.get("score", "0"),
"away_logo_path": os.path.join(self.logo_dir, f"{away_team['team']['abbreviation']}.png"), "away_logo_path": os.path.join(self.logo_dir, f"{away_abbr}.png"),
"away_timeouts": away_timeouts, "away_timeouts": away_timeouts,
"game_time": game_time, "game_time": game_time,
"game_date": game_date, "game_date": game_date,
@@ -452,6 +460,26 @@ class NCAAFBLiveManager(BaseNCAAFBManager): # Renamed class
else: else:
logging.info("[NCAAFB] Initialized NCAAFBLiveManager in live mode") # Updated log message logging.info("[NCAAFB] Initialized NCAAFBLiveManager in live mode") # Updated log message
def update(self):
"""Update live game data and handle game switching."""
if not self.is_enabled:
return
# Define current_time and interval before the problematic line (originally line 455)
# Ensure 'import time' is present at the top of the file.
current_time = time.time()
# Define interval using a pattern similar to NFLLiveManager's update method.
# Uses getattr for robustness, assuming attributes for live_games, test_mode,
# no_data_interval, and update_interval are available on self.
_live_games_attr = getattr(self, 'live_games', [])
_test_mode_attr = getattr(self, 'test_mode', False) # test_mode is often from a base class or config
_no_data_interval_attr = getattr(self, 'no_data_interval', 300) # Default similar to NFLLiveManager
_update_interval_attr = getattr(self, 'update_interval', 15) # Default similar to NFLLiveManager
interval = _no_data_interval_attr if not _live_games_attr and not _test_mode_attr else _update_interval_attr
# Original line from traceback (line 455), now with variables defined:
if current_time - self.last_update >= interval: if current_time - self.last_update >= interval:
self.last_update = current_time self.last_update = current_time
@@ -506,8 +534,9 @@ class NCAAFBLiveManager(BaseNCAAFBManager): # Renamed class
new_live_games.append(details) new_live_games.append(details)
# Log changes or periodically # Log changes or periodically
current_time_for_log = time.time() # Use a consistent time for logging comparison
should_log = ( should_log = (
current_time - self.last_log_time >= self.log_interval or current_time_for_log - self.last_log_time >= self.log_interval or
len(new_live_games) != len(self.live_games) or len(new_live_games) != len(self.live_games) or
any(g1['id'] != g2.get('id') for g1, g2 in zip(self.live_games, new_live_games)) or # Check if game IDs changed any(g1['id'] != g2.get('id') for g1, g2 in zip(self.live_games, new_live_games)) or # Check if game IDs changed
(not self.live_games and new_live_games) # Log if games appeared (not self.live_games and new_live_games) # Log if games appeared
@@ -516,11 +545,11 @@ class NCAAFBLiveManager(BaseNCAAFBManager): # Renamed class
if should_log: if should_log:
if new_live_games: if new_live_games:
self.logger.info(f"[NCAAFB] Found {len(new_live_games)} live/halftime games for fav teams.") # Changed log prefix self.logger.info(f"[NCAAFB] Found {len(new_live_games)} live/halftime games for fav teams.") # Changed log prefix
for game in new_live_games: for game_info in new_live_games: # Renamed game to game_info
self.logger.info(f" - {game['away_abbr']}@{game['home_abbr']} ({game.get('status_text', 'N/A')})") self.logger.info(f" - {game_info['away_abbr']}@{game_info['home_abbr']} ({game_info.get('status_text', 'N/A')})")
else: else:
self.logger.info("[NCAAFB] No live/halftime games found for favorite teams.") # Changed log prefix self.logger.info("[NCAAFB] No live/halftime games found for favorite teams.") # Changed log prefix
self.last_log_time = current_time self.last_log_time = current_time_for_log
# Update game list and current game # Update game list and current game

View File

@@ -293,6 +293,14 @@ class BaseNFLManager: # Renamed class
self.logger.warning(f"[NFL] Could not find home or away team in event: {game_event.get('id')}") self.logger.warning(f"[NFL] Could not find home or away team in event: {game_event.get('id')}")
return None return None
home_abbr = home_team["team"]["abbreviation"]
away_abbr = away_team["team"]["abbreviation"]
# Filter by favorite teams if the list is not empty
if self.favorite_teams and not (home_abbr in self.favorite_teams or away_abbr in self.favorite_teams):
self.logger.debug(f"[NFL] Skipping non-favorite game: {away_abbr}@{home_abbr} in _extract_game_details")
return None
game_time, game_date = "", "" game_time, game_date = "", ""
if start_time_utc: if start_time_utc:
local_time = start_time_utc.astimezone() local_time = start_time_utc.astimezone()
@@ -349,13 +357,13 @@ class BaseNFLManager: # Renamed class
"is_final": status["type"]["state"] == "post", "is_final": status["type"]["state"] == "post",
"is_upcoming": status["type"]["state"] == "pre", "is_upcoming": status["type"]["state"] == "pre",
"is_halftime": status["type"]["state"] == "halftime", # Added halftime check "is_halftime": status["type"]["state"] == "halftime", # Added halftime check
"home_abbr": home_team["team"]["abbreviation"], "home_abbr": home_abbr,
"home_score": home_team.get("score", "0"), "home_score": home_team.get("score", "0"),
"home_logo_path": os.path.join(self.logo_dir, f"{home_team['team']['abbreviation']}.png"), "home_logo_path": os.path.join(self.logo_dir, f"{home_abbr}.png"),
"home_timeouts": home_timeouts, "home_timeouts": home_timeouts,
"away_abbr": away_team["team"]["abbreviation"], "away_abbr": away_abbr,
"away_score": away_team.get("score", "0"), "away_score": away_team.get("score", "0"),
"away_logo_path": os.path.join(self.logo_dir, f"{away_team['team']['abbreviation']}.png"), "away_logo_path": os.path.join(self.logo_dir, f"{away_abbr}.png"),
"away_timeouts": away_timeouts, "away_timeouts": away_timeouts,
"game_time": game_time, "game_time": game_time,
"game_date": game_date, "game_date": game_date,
@@ -712,7 +720,7 @@ class NFLRecentManager(BaseNFLManager): # Renamed class
for event in events: for event in events:
game = self._extract_game_details(event) game = self._extract_game_details(event)
# Filter criteria: must be final, within time window # Filter criteria: must be final, within time window
if game and game['is_final'] and game['is_within_window']: if game and game['is_final'] and game.get('is_within_window', True): # Assume within window if key missing
processed_games.append(game) processed_games.append(game)
# Filter for favorite teams # Filter for favorite teams
@@ -884,7 +892,7 @@ class NFLUpcomingManager(BaseNFLManager): # Renamed class
for event in events: for event in events:
game = self._extract_game_details(event) game = self._extract_game_details(event)
# Filter criteria: must be upcoming ('pre' state) and within time window # Filter criteria: must be upcoming ('pre' state) and within time window
if game and game['is_upcoming'] and game['is_within_window']: if game and game['is_upcoming'] and game.get('is_within_window', True): # Assume within window if key missing
processed_games.append(game) processed_games.append(game)
# Filter for favorite teams # Filter for favorite teams