mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-04-10 21:03:01 +00:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1cdcd43662 | ||
|
|
ac26b819ea | ||
|
|
a63313e5b8 | ||
|
|
8b15218b1e | ||
|
|
5329805f0b | ||
|
|
d8ebe6764d | ||
|
|
e13620e58d | ||
|
|
d47788d93c | ||
|
|
bdca997263 | ||
|
|
509c8e6fe3 | ||
|
|
7cbb3c7c00 |
632
README.md
632
README.md
@@ -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
|
||||||
- 
|

|
||||||
|
|
||||||
|
|
||||||
- Current Weather, Daily Weather, and Hourly Weather Forecasts
|
- Current Weather, Daily Weather, and Hourly Weather Forecasts
|
||||||
- 
|

|
||||||
- 
|

|
||||||
- 
|

|
||||||
|
|
||||||
|
|
||||||
- Google Calendar event display
|
- Google Calendar event display
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### 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)
|
||||||
- 
|

|
||||||
- 
|

|
||||||
- 
|

|
||||||
|
|
||||||
- NBA (Basketball)
|
- NBA (Basketball)
|
||||||
- MLB (Baseball)
|
- MLB (Baseball)
|
||||||
- 
|

|
||||||
- 
|
|
||||||
- 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
|
||||||
- 
|

|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### 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
|
||||||
- 
|

|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Custom Display Features
|
### Custom Display Features
|
||||||
- Custom Text display
|
- Custom Text display
|
||||||
|

|
||||||
|
|
||||||
- Youtube Subscriber Count Display
|
- Youtube Subscriber Count Display
|
||||||
- 
|

|
||||||
- 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
|
||||||

|

|
||||||
|
|
||||||
|
-----------------------------------------------------------------------------------
|
||||||
|
## 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.
|
||||||

|

|
||||||
|
|
||||||
|
-----------------------------------------------------------------------------------
|
||||||
|
# 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
|
||||||
|

|
||||||
|
5. Press Next then Edit Settings
|
||||||
|

|
||||||
|
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.
|
||||||
|

|
||||||
|
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.
|
||||||
|

|
||||||
|
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:
|
```bash
|
||||||
```json
|
sudo usermod -a -G gpio <your_username>
|
||||||
{
|
# Example for user 'ledpi':
|
||||||
"youtube": {
|
# sudo usermod -a -G gpio ledpi
|
||||||
"enabled": true,
|
```
|
||||||
"update_interval": 300 // Update interval in seconds (default: 300)
|
|
||||||
}
|
**Important:** You must **reboot** the Raspberry Pi after adding the user to the group for the change to take effect.
|
||||||
|
|
||||||
|
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
|
||||||
|
```
|
||||||
|
-----------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
## 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 (0–100).
|
||||||
|
- 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/...
|
||||||
|
|
||||||
|
```bash
|
||||||
|
Example: NHL Configuration"nhl_scoreboard": {
|
||||||
|
"enabled": true,
|
||||||
|
"test_mode": false,
|
||||||
|
"update_interval_seconds": 300,
|
||||||
|
"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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
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
|
|
||||||
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.
|
|
||||||
|
|
||||||
### 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,
|
|
||||||
"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.
|
|
||||||
|
|
||||||
## 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 doesn’t 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 you’ve 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
|
|
||||||
```
|
|
||||||
|
|||||||
4
assets/data/team_league_map.json
Normal file
4
assets/data/team_league_map.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"last_updated": 0,
|
||||||
|
"map": {}
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
@@ -779,8 +782,7 @@ class NCAABaseballUpcomingManager(BaseNCAABaseballManager):
|
|||||||
logger.info("[NCAABaseball] No upcoming games found for favorite teams")
|
logger.info("[NCAABaseball] No upcoming games found for favorite teams")
|
||||||
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)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -1030,4 +1059,4 @@ class NCAAFBUpcomingManager(BaseNCAAFBManager): # Renamed class
|
|||||||
# update_display() is called within _draw_scorebug_layout for upcoming
|
# update_display() is called within _draw_scorebug_layout for upcoming
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(f"[NCAAFB Upcoming] Error in display loop: {e}", exc_info=True) # Changed log prefix
|
self.logger.error(f"[NCAAFB Upcoming] Error in display loop: {e}", exc_info=True) # Changed log prefix
|
||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user