From db31864a588da7d4a666fcd3aa4d8bd9478d69cb Mon Sep 17 00:00:00 2001 From: Chuck <33324927+ChuckBuilds@users.noreply.github.com> Date: Tue, 8 Apr 2025 21:12:10 -0500 Subject: [PATCH 01/87] Opening Bell Introducing the Stock Ticker Feature --- config/config.json | 8 ++ src/display_controller.py | 19 ++-- src/stock_manager.py | 224 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 242 insertions(+), 9 deletions(-) create mode 100644 src/stock_manager.py diff --git a/config/config.json b/config/config.json index 01b7f8a3..4f498136 100644 --- a/config/config.json +++ b/config/config.json @@ -35,5 +35,13 @@ "update_interval": 300, "units": "imperial", "display_format": "{temp}°F\n{condition}" + }, + "stocks": { + "enabled": true, + "update_interval": 60, + "symbols": [ + "AAPL", "MSFT", "GOOGL", "AMZN", "META", "TSLA", "NVDA", "JPM", "V", "WMT" + ], + "display_format": "{symbol}: ${price} ({change}%)" } } \ No newline at end of file diff --git a/src/display_controller.py b/src/display_controller.py index 9d7f5cbd..8bf52b95 100644 --- a/src/display_controller.py +++ b/src/display_controller.py @@ -5,6 +5,7 @@ from src.clock import Clock from src.weather_manager import WeatherManager from src.display_manager import DisplayManager from src.config_manager import ConfigManager +from src.stock_manager import StockManager # Configure logging logging.basicConfig(level=logging.INFO) @@ -17,6 +18,7 @@ class DisplayController: self.display_manager = DisplayManager(self.config.get('display', {})) self.clock = Clock(display_manager=self.display_manager) self.weather = WeatherManager(self.config, self.display_manager) + self.stocks = StockManager(self.config, self.display_manager) self.current_display = 'clock' self.last_switch = time.time() self.force_clear = True # Start with a clear screen @@ -31,14 +33,15 @@ class DisplayController: # Check if we need to switch display mode if current_time - self.last_switch > self.config['display'].get('rotation_interval', 30): - # Cycle through: clock -> current weather -> hourly forecast -> daily forecast + # Cycle through: clock -> weather -> stocks if self.current_display == 'clock': self.current_display = 'weather' elif self.current_display == 'weather': - self.current_display = 'hourly' - elif self.current_display == 'hourly': - self.current_display = 'daily' - else: # daily + if self.config.get('stocks', {}).get('enabled', False): + self.current_display = 'stocks' + else: + self.current_display = 'clock' + else: # stocks self.current_display = 'clock' logger.info("Switching display to: %s", self.current_display) @@ -52,10 +55,8 @@ class DisplayController: self.clock.display_time(force_clear=self.force_clear) elif self.current_display == 'weather': self.weather.display_weather(force_clear=self.force_clear) - elif self.current_display == 'hourly': - self.weather.display_hourly_forecast(force_clear=self.force_clear) - else: # daily - self.weather.display_daily_forecast(force_clear=self.force_clear) + else: # stocks + self.stocks.display_stocks(force_clear=self.force_clear) except Exception as e: logger.error(f"Error updating display: {e}") time.sleep(1) # Wait a bit before retrying diff --git a/src/stock_manager.py b/src/stock_manager.py new file mode 100644 index 00000000..69de9bfc --- /dev/null +++ b/src/stock_manager.py @@ -0,0 +1,224 @@ +import time +import logging +import requests +import json +import random +from typing import Dict, Any, List, Tuple +from datetime import datetime +import os + +# Configure logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +class StockManager: + def __init__(self, config: Dict[str, Any], display_manager): + self.config = config + self.display_manager = display_manager + self.stocks_config = config.get('stocks', {}) + self.last_update = 0 + self.stock_data = {} + self.current_stock_index = 0 + self.base_url = "https://query2.finance.yahoo.com" + self.logo_cache = {} + + # Create logos directory if it doesn't exist + self.logos_dir = "assets/logos/stocks" + os.makedirs(self.logos_dir, exist_ok=True) + + # Default colors for stocks + self.default_colors = [ + (0, 255, 0), # Green + (0, 255, 255), # Cyan + (255, 255, 0), # Yellow + (255, 165, 0), # Orange + (128, 0, 128), # Purple + (255, 0, 0), # Red + (0, 0, 255), # Blue + (255, 192, 203) # Pink + ] + + def _get_stock_color(self, symbol: str) -> Tuple[int, int, int]: + """Get a consistent color for a stock symbol.""" + # Use the symbol as a seed for consistent color assignment + random.seed(hash(symbol)) + color_index = random.randint(0, len(self.default_colors) - 1) + random.seed() # Reset the seed + return self.default_colors[color_index] + + def _fetch_stock_data(self, symbol: str) -> Dict[str, Any]: + """Fetch stock data from Yahoo Finance API.""" + try: + # Use Yahoo Finance API directly + url = f"{self.base_url}/v8/finance/chart/{symbol}" + params = { + "interval": "1m", + "period": "1d" + } + response = requests.get(url, params=params) + data = response.json() + + if "chart" not in data or "result" not in data["chart"] or not data["chart"]["result"]: + logger.error(f"Invalid response for {symbol}: {data}") + return None + + result = data["chart"]["result"][0] + meta = result["meta"] + + # Extract price data + price = meta.get("regularMarketPrice", 0) + prev_close = meta.get("chartPreviousClose", 0) + + # Calculate change percentage + change_pct = 0 + if prev_close > 0: + change_pct = ((price - prev_close) / prev_close) * 100 + + # Get company name if available + company_name = meta.get("instrumentInfo", {}).get("longName", symbol) + + return { + "symbol": symbol, + "name": company_name, + "price": price, + "change": change_pct, + "open": prev_close + } + except Exception as e: + logger.error(f"Error fetching stock data for {symbol}: {e}") + return None + + def _download_logo(self, symbol: str) -> str: + """Download company logo for a stock symbol.""" + logo_path = os.path.join(self.logos_dir, f"{symbol}.png") + + # If logo already exists, return the path + if os.path.exists(logo_path): + return logo_path + + try: + # Try to get logo from Yahoo Finance + url = f"{self.base_url}/v8/finance/chart/{symbol}" + params = { + "interval": "1d", + "period": "1d" + } + response = requests.get(url, params=params) + data = response.json() + + if "chart" in data and "result" in data["chart"] and data["chart"]["result"]: + result = data["chart"]["result"][0] + if "meta" in result and "logo_url" in result["meta"]: + logo_url = result["meta"]["logo_url"] + + # Download the logo + logo_response = requests.get(logo_url) + if logo_response.status_code == 200: + with open(logo_path, "wb") as f: + f.write(logo_response.content) + logger.info(f"Downloaded logo for {symbol}") + return logo_path + + # If we couldn't get a logo, create a placeholder + self._create_placeholder_logo(symbol, logo_path) + return logo_path + + except Exception as e: + logger.error(f"Error downloading logo for {symbol}: {e}") + # Create a placeholder logo + self._create_placeholder_logo(symbol, logo_path) + return logo_path + + def _create_placeholder_logo(self, symbol: str, logo_path: str): + """Create a placeholder logo with the stock symbol.""" + try: + from PIL import Image, ImageDraw, ImageFont + + # Create a 32x32 image with a colored background + color = self._get_stock_color(symbol) + img = Image.new('RGB', (32, 32), color) + draw = ImageDraw.Draw(img) + + # Try to load a font, fall back to default if not available + try: + font = ImageFont.truetype("assets/fonts/PressStart2P-Regular.ttf", 8) + except: + font = ImageFont.load_default() + + # Draw the symbol + draw.text((4, 8), symbol, fill=(255, 255, 255), font=font) + + # Save the image + img.save(logo_path) + logger.info(f"Created placeholder logo for {symbol}") + + except Exception as e: + logger.error(f"Error creating placeholder logo for {symbol}: {e}") + + def update_stock_data(self): + """Update stock data if enough time has passed.""" + current_time = time.time() + if current_time - self.last_update < self.stocks_config.get('update_interval', 60): + return + + # Get symbols from config + symbols = self.stocks_config.get('symbols', []) + + # If symbols is a list of strings, convert to list of dicts + if symbols and isinstance(symbols[0], str): + symbols = [{"symbol": symbol} for symbol in symbols] + + for stock in symbols: + symbol = stock['symbol'] + data = self._fetch_stock_data(symbol) + if data: + # Add color if not specified + if 'color' not in stock: + data['color'] = self._get_stock_color(symbol) + else: + data['color'] = tuple(stock['color']) + + # Download logo + data['logo_path'] = self._download_logo(symbol) + + self.stock_data[symbol] = data + + self.last_update = current_time + + def display_stocks(self, force_clear: bool = False): + """Display stock information on the LED matrix.""" + if not self.stocks_config.get('enabled', False): + return + + self.update_stock_data() + + if not self.stock_data: + logger.warning("No stock data available to display") + return + + # Clear the display if forced or if this is the first stock + if force_clear or self.current_stock_index == 0: + self.display_manager.clear() + + # Get the current stock to display + symbols = list(self.stock_data.keys()) + if not symbols: + return + + current_symbol = symbols[self.current_stock_index] + data = self.stock_data[current_symbol] + + # Format the display text + display_format = self.stocks_config.get('display_format', "{symbol}: ${price} ({change}%)") + display_text = display_format.format( + symbol=data['symbol'], + price=f"{data['price']:.2f}", + change=f"{data['change']:+.2f}" + ) + + # Draw the stock information + self.display_manager.draw_text(display_text, color=data['color']) + self.display_manager.update_display() + + # Move to next stock for next update + self.current_stock_index = (self.current_stock_index + 1) % len(symbols) \ No newline at end of file From 04cfa86430dcf208b254d582b68ffad2ebb5620b Mon Sep 17 00:00:00 2001 From: Chuck <33324927+ChuckBuilds@users.noreply.github.com> Date: Tue, 8 Apr 2025 21:15:36 -0500 Subject: [PATCH 02/87] Update stock_manager.py Assume folder exists --- src/stock_manager.py | 70 ++++++++++++++++++++++++++------------------ 1 file changed, 42 insertions(+), 28 deletions(-) diff --git a/src/stock_manager.py b/src/stock_manager.py index 69de9bfc..c5cb9a69 100644 --- a/src/stock_manager.py +++ b/src/stock_manager.py @@ -22,9 +22,8 @@ class StockManager: self.base_url = "https://query2.finance.yahoo.com" self.logo_cache = {} - # Create logos directory if it doesn't exist + # Set logos directory path (assuming it exists) self.logos_dir = "assets/logos/stocks" - os.makedirs(self.logos_dir, exist_ok=True) # Default colors for stocks self.default_colors = [ @@ -98,38 +97,36 @@ class StockManager: try: # Try to get logo from Yahoo Finance - url = f"{self.base_url}/v8/finance/chart/{symbol}" - params = { - "interval": "1d", - "period": "1d" - } - response = requests.get(url, params=params) + url = f"https://query2.finance.yahoo.com/v7/finance/options/{symbol}" + response = requests.get(url) data = response.json() - if "chart" in data and "result" in data["chart"] and data["chart"]["result"]: - result = data["chart"]["result"][0] - if "meta" in result and "logo_url" in result["meta"]: - logo_url = result["meta"]["logo_url"] + if "optionChain" in data and "result" in data["optionChain"] and data["optionChain"]["result"]: + result = data["optionChain"]["result"][0] + if "quote" in result and "logoUrl" in result["quote"]: + logo_url = result["quote"]["logoUrl"] # Download the logo logo_response = requests.get(logo_url) if logo_response.status_code == 200: - with open(logo_path, "wb") as f: - f.write(logo_response.content) - logger.info(f"Downloaded logo for {symbol}") - return logo_path + try: + with open(logo_path, "wb") as f: + f.write(logo_response.content) + logger.info(f"Downloaded logo for {symbol}") + return logo_path + except IOError as e: + logger.error(f"Could not write logo file for {symbol}: {e}") + return None # If we couldn't get a logo, create a placeholder - self._create_placeholder_logo(symbol, logo_path) - return logo_path + return self._create_placeholder_logo(symbol, logo_path) except Exception as e: logger.error(f"Error downloading logo for {symbol}: {e}") # Create a placeholder logo - self._create_placeholder_logo(symbol, logo_path) - return logo_path + return self._create_placeholder_logo(symbol, logo_path) - def _create_placeholder_logo(self, symbol: str, logo_path: str): + def _create_placeholder_logo(self, symbol: str, logo_path: str) -> str: """Create a placeholder logo with the stock symbol.""" try: from PIL import Image, ImageDraw, ImageFont @@ -146,14 +143,28 @@ class StockManager: font = ImageFont.load_default() # Draw the symbol - draw.text((4, 8), symbol, fill=(255, 255, 255), font=font) + text_bbox = draw.textbbox((0, 0), symbol, font=font) + text_width = text_bbox[2] - text_bbox[0] + text_height = text_bbox[3] - text_bbox[1] - # Save the image - img.save(logo_path) - logger.info(f"Created placeholder logo for {symbol}") + # Center the text + x = (32 - text_width) // 2 + y = (32 - text_height) // 2 + + draw.text((x, y), symbol, fill=(255, 255, 255), font=font) + + try: + # Save the image + img.save(logo_path) + logger.info(f"Created placeholder logo for {symbol}") + return logo_path + except IOError as e: + logger.error(f"Could not save placeholder logo for {symbol}: {e}") + return None except Exception as e: logger.error(f"Error creating placeholder logo for {symbol}: {e}") + return None def update_stock_data(self): """Update stock data if enough time has passed.""" @@ -178,8 +189,10 @@ class StockManager: else: data['color'] = tuple(stock['color']) - # Download logo - data['logo_path'] = self._download_logo(symbol) + # Try to get logo + logo_path = self._download_logo(symbol) + if logo_path: + data['logo_path'] = logo_path self.stock_data[symbol] = data @@ -217,7 +230,8 @@ class StockManager: ) # Draw the stock information - self.display_manager.draw_text(display_text, color=data['color']) + color = (0, 255, 0) if data['change'] >= 0 else (255, 0, 0) # Green for up, red for down + self.display_manager.draw_text(display_text, color=color) self.display_manager.update_display() # Move to next stock for next update From e87251caf4d2ea7217dff164cb14bfbd2b65db44 Mon Sep 17 00:00:00 2001 From: Chuck <33324927+ChuckBuilds@users.noreply.github.com> Date: Tue, 8 Apr 2025 21:18:12 -0500 Subject: [PATCH 03/87] Update stock_manager.py removing logos to focus on function for now --- src/stock_manager.py | 190 ++++++++++++------------------------------- 1 file changed, 53 insertions(+), 137 deletions(-) diff --git a/src/stock_manager.py b/src/stock_manager.py index c5cb9a69..c151783f 100644 --- a/src/stock_manager.py +++ b/src/stock_manager.py @@ -19,151 +19,69 @@ class StockManager: self.last_update = 0 self.stock_data = {} self.current_stock_index = 0 - self.base_url = "https://query2.finance.yahoo.com" - self.logo_cache = {} - - # Set logos directory path (assuming it exists) - self.logos_dir = "assets/logos/stocks" - - # Default colors for stocks - self.default_colors = [ - (0, 255, 0), # Green - (0, 255, 255), # Cyan - (255, 255, 0), # Yellow - (255, 165, 0), # Orange - (128, 0, 128), # Purple - (255, 0, 0), # Red - (0, 0, 255), # Blue - (255, 192, 203) # Pink - ] + self.base_url = "https://query1.finance.yahoo.com" + self.headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' + } def _get_stock_color(self, symbol: str) -> Tuple[int, int, int]: - """Get a consistent color for a stock symbol.""" - # Use the symbol as a seed for consistent color assignment - random.seed(hash(symbol)) - color_index = random.randint(0, len(self.default_colors) - 1) - random.seed() # Reset the seed - return self.default_colors[color_index] + """Get color based on stock performance.""" + if symbol not in self.stock_data: + return (255, 255, 255) # White for unknown + + change = self.stock_data[symbol].get('change', 0) + if change > 0: + return (0, 255, 0) # Green for positive + elif change < 0: + return (255, 0, 0) # Red for negative + return (255, 255, 0) # Yellow for no change def _fetch_stock_data(self, symbol: str) -> Dict[str, Any]: """Fetch stock data from Yahoo Finance API.""" try: - # Use Yahoo Finance API directly - url = f"{self.base_url}/v8/finance/chart/{symbol}" + # Use Yahoo Finance quote endpoint + url = f"{self.base_url}/v7/finance/quote" params = { - "interval": "1m", - "period": "1d" + "symbols": symbol } - response = requests.get(url, params=params) + + response = requests.get(url, params=params, headers=self.headers) + response.raise_for_status() # Raise an error for bad status codes + data = response.json() + logger.debug(f"Raw response for {symbol}: {data}") - if "chart" not in data or "result" not in data["chart"] or not data["chart"]["result"]: - logger.error(f"Invalid response for {symbol}: {data}") + if not data or "quoteResponse" not in data or "result" not in data["quoteResponse"] or not data["quoteResponse"]["result"]: + logger.error(f"Invalid response format for {symbol}") return None - - result = data["chart"]["result"][0] - meta = result["meta"] - # Extract price data - price = meta.get("regularMarketPrice", 0) - prev_close = meta.get("chartPreviousClose", 0) + quote = data["quoteResponse"]["result"][0] - # Calculate change percentage - change_pct = 0 - if prev_close > 0: + # Extract required fields with fallbacks + price = quote.get("regularMarketPrice", 0) + prev_close = quote.get("regularMarketPreviousClose", price) + change_pct = quote.get("regularMarketChangePercent", 0) + + # If we didn't get a change percentage, calculate it + if change_pct == 0 and prev_close != 0: change_pct = ((price - prev_close) / prev_close) * 100 - - # Get company name if available - company_name = meta.get("instrumentInfo", {}).get("longName", symbol) - + return { "symbol": symbol, - "name": company_name, + "name": quote.get("longName", symbol), "price": price, "change": change_pct, "open": prev_close } - except Exception as e: - logger.error(f"Error fetching stock data for {symbol}: {e}") + + except requests.exceptions.RequestException as e: + logger.error(f"Network error fetching data for {symbol}: {e}") + return None + except json.JSONDecodeError as e: + logger.error(f"JSON decode error for {symbol}: {e}") return None - - def _download_logo(self, symbol: str) -> str: - """Download company logo for a stock symbol.""" - logo_path = os.path.join(self.logos_dir, f"{symbol}.png") - - # If logo already exists, return the path - if os.path.exists(logo_path): - return logo_path - - try: - # Try to get logo from Yahoo Finance - url = f"https://query2.finance.yahoo.com/v7/finance/options/{symbol}" - response = requests.get(url) - data = response.json() - - if "optionChain" in data and "result" in data["optionChain"] and data["optionChain"]["result"]: - result = data["optionChain"]["result"][0] - if "quote" in result and "logoUrl" in result["quote"]: - logo_url = result["quote"]["logoUrl"] - - # Download the logo - logo_response = requests.get(logo_url) - if logo_response.status_code == 200: - try: - with open(logo_path, "wb") as f: - f.write(logo_response.content) - logger.info(f"Downloaded logo for {symbol}") - return logo_path - except IOError as e: - logger.error(f"Could not write logo file for {symbol}: {e}") - return None - - # If we couldn't get a logo, create a placeholder - return self._create_placeholder_logo(symbol, logo_path) - except Exception as e: - logger.error(f"Error downloading logo for {symbol}: {e}") - # Create a placeholder logo - return self._create_placeholder_logo(symbol, logo_path) - - def _create_placeholder_logo(self, symbol: str, logo_path: str) -> str: - """Create a placeholder logo with the stock symbol.""" - try: - from PIL import Image, ImageDraw, ImageFont - - # Create a 32x32 image with a colored background - color = self._get_stock_color(symbol) - img = Image.new('RGB', (32, 32), color) - draw = ImageDraw.Draw(img) - - # Try to load a font, fall back to default if not available - try: - font = ImageFont.truetype("assets/fonts/PressStart2P-Regular.ttf", 8) - except: - font = ImageFont.load_default() - - # Draw the symbol - text_bbox = draw.textbbox((0, 0), symbol, font=font) - text_width = text_bbox[2] - text_bbox[0] - text_height = text_bbox[3] - text_bbox[1] - - # Center the text - x = (32 - text_width) // 2 - y = (32 - text_height) // 2 - - draw.text((x, y), symbol, fill=(255, 255, 255), font=font) - - try: - # Save the image - img.save(logo_path) - logger.info(f"Created placeholder logo for {symbol}") - return logo_path - except IOError as e: - logger.error(f"Could not save placeholder logo for {symbol}: {e}") - return None - - except Exception as e: - logger.error(f"Error creating placeholder logo for {symbol}: {e}") + logger.error(f"Unexpected error fetching data for {symbol}: {e}") return None def update_stock_data(self): @@ -174,29 +92,27 @@ class StockManager: # Get symbols from config symbols = self.stocks_config.get('symbols', []) - + if not symbols: + logger.warning("No stock symbols configured") + return + # If symbols is a list of strings, convert to list of dicts - if symbols and isinstance(symbols[0], str): + if isinstance(symbols[0], str): symbols = [{"symbol": symbol} for symbol in symbols] + success = False # Track if we got any successful updates for stock in symbols: symbol = stock['symbol'] data = self._fetch_stock_data(symbol) if data: - # Add color if not specified - if 'color' not in stock: - data['color'] = self._get_stock_color(symbol) - else: - data['color'] = tuple(stock['color']) - - # Try to get logo - logo_path = self._download_logo(symbol) - if logo_path: - data['logo_path'] = logo_path - self.stock_data[symbol] = data + success = True + logger.info(f"Updated {symbol}: ${data['price']:.2f} ({data['change']:+.2f}%)") - self.last_update = current_time + if success: + self.last_update = current_time + else: + logger.error("Failed to fetch data for any configured stocks") def display_stocks(self, force_clear: bool = False): """Display stock information on the LED matrix.""" From cb8680e834b6aca5d40b588ae78b8b75168d14f1 Mon Sep 17 00:00:00 2001 From: Chuck <33324927+ChuckBuilds@users.noreply.github.com> Date: Tue, 8 Apr 2025 21:31:04 -0500 Subject: [PATCH 04/87] Update stock_manager.py parse yahoo scripts --- src/stock_manager.py | 124 +++++++++++++++++++++++++++++++++---------- 1 file changed, 97 insertions(+), 27 deletions(-) diff --git a/src/stock_manager.py b/src/stock_manager.py index c151783f..5f841b6d 100644 --- a/src/stock_manager.py +++ b/src/stock_manager.py @@ -6,6 +6,8 @@ import random from typing import Dict, Any, List, Tuple from datetime import datetime import os +import urllib.parse +import re # Configure logging logging.basicConfig(level=logging.INFO) @@ -19,7 +21,6 @@ class StockManager: self.last_update = 0 self.stock_data = {} self.current_stock_index = 0 - self.base_url = "https://query1.finance.yahoo.com" self.headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' } @@ -36,40 +37,104 @@ class StockManager: return (255, 0, 0) # Red for negative return (255, 255, 0) # Yellow for no change - def _fetch_stock_data(self, symbol: str) -> Dict[str, Any]: - """Fetch stock data from Yahoo Finance API.""" + def _extract_json_from_html(self, html: str) -> Dict: + """Extract the JSON data from Yahoo Finance HTML.""" try: - # Use Yahoo Finance quote endpoint - url = f"{self.base_url}/v7/finance/quote" - params = { - "symbols": symbol - } + # Look for the finance data in the HTML + patterns = [ + r'root\.App\.main = (.*?);\s*', + r'"QuotePageStore":\s*({.*?}),\s*"', + r'{"regularMarketPrice":.*?"regularMarketChangePercent".*?}' + ] - response = requests.get(url, params=params, headers=self.headers) - response.raise_for_status() # Raise an error for bad status codes + for pattern in patterns: + match = re.search(pattern, html, re.DOTALL) + if match: + json_str = match.group(1) + try: + data = json.loads(json_str) + if isinstance(data, dict): + if 'context' in data: + # First pattern matched + context = data.get('context', {}) + dispatcher = context.get('dispatcher', {}) + stores = dispatcher.get('stores', {}) + quote_data = stores.get('QuoteSummaryStore', {}) + if quote_data: + return quote_data + else: + # Direct quote data + return data + except json.JSONDecodeError: + continue + + # If we get here, try one last attempt to find the price data directly + price_match = re.search(r'"regularMarketPrice":{"raw":([\d.]+)', html) + change_match = re.search(r'"regularMarketChangePercent":{"raw":([-\d.]+)', html) + prev_close_match = re.search(r'"regularMarketPreviousClose":{"raw":([\d.]+)', html) + name_match = re.search(r'"shortName":"([^"]+)"', html) - data = response.json() - logger.debug(f"Raw response for {symbol}: {data}") + if price_match: + return { + "price": { + "regularMarketPrice": {"raw": float(price_match.group(1))}, + "regularMarketChangePercent": {"raw": float(change_match.group(1)) if change_match else 0}, + "regularMarketPreviousClose": {"raw": float(prev_close_match.group(1)) if prev_close_match else 0}, + "shortName": name_match.group(1) if name_match else None + } + } - if not data or "quoteResponse" not in data or "result" not in data["quoteResponse"] or not data["quoteResponse"]["result"]: - logger.error(f"Invalid response format for {symbol}") + return {} + except Exception as e: + logger.error(f"Error extracting JSON data: {e}") + return {} + + def _fetch_stock_data(self, symbol: str) -> Dict[str, Any]: + """Fetch stock data from Yahoo Finance public API.""" + try: + # Use Yahoo Finance public API + encoded_symbol = urllib.parse.quote(symbol) + url = f"https://finance.yahoo.com/quote/{encoded_symbol}" + + response = requests.get(url, headers=self.headers, timeout=5) + if response.status_code != 200: + logger.error(f"Failed to fetch data for {symbol}: HTTP {response.status_code}") return None + + # Extract the embedded JSON data + quote_data = self._extract_json_from_html(response.text) + if not quote_data: + logger.error(f"Could not extract quote data for {symbol}") + return None + + # Get the price data + price = quote_data.get('price', {}) + if not price: + logger.error(f"No price data found for {symbol}") + return None + + regular_market = price.get('regularMarketPrice', {}) + previous_close = price.get('regularMarketPreviousClose', {}) + change_percent = price.get('regularMarketChangePercent', {}) - quote = data["quoteResponse"]["result"][0] + # Extract raw values with fallbacks + current_price = regular_market.get('raw', 0) if isinstance(regular_market, dict) else regular_market + prev_close = previous_close.get('raw', current_price) if isinstance(previous_close, dict) else previous_close + change_pct = change_percent.get('raw', 0) if isinstance(change_percent, dict) else change_percent - # Extract required fields with fallbacks - price = quote.get("regularMarketPrice", 0) - prev_close = quote.get("regularMarketPreviousClose", price) - change_pct = quote.get("regularMarketChangePercent", 0) + # If we don't have a change percentage, calculate it + if change_pct == 0 and prev_close > 0: + change_pct = ((current_price - prev_close) / prev_close) * 100 - # If we didn't get a change percentage, calculate it - if change_pct == 0 and prev_close != 0: - change_pct = ((price - prev_close) / prev_close) * 100 + # Get company name + name = price.get('shortName', symbol) + + logger.debug(f"Processed data for {symbol}: price={current_price}, change={change_pct}%") return { "symbol": symbol, - "name": quote.get("longName", symbol), - "price": price, + "name": name, + "price": current_price, "change": change_pct, "open": prev_close } @@ -77,8 +142,8 @@ class StockManager: except requests.exceptions.RequestException as e: logger.error(f"Network error fetching data for {symbol}: {e}") return None - except json.JSONDecodeError as e: - logger.error(f"JSON decode error for {symbol}: {e}") + except (ValueError, IndexError) as e: + logger.error(f"Error parsing data for {symbol}: {e}") return None except Exception as e: logger.error(f"Unexpected error fetching data for {symbol}: {e}") @@ -87,7 +152,10 @@ class StockManager: def update_stock_data(self): """Update stock data if enough time has passed.""" current_time = time.time() - if current_time - self.last_update < self.stocks_config.get('update_interval', 60): + update_interval = self.stocks_config.get('update_interval', 60) + + # Add a small random delay to prevent exact timing matches + if current_time - self.last_update < update_interval + random.uniform(0, 2): return # Get symbols from config @@ -103,6 +171,8 @@ class StockManager: success = False # Track if we got any successful updates for stock in symbols: symbol = stock['symbol'] + # Add a small delay between requests to avoid rate limiting + time.sleep(random.uniform(0.5, 1.5)) data = self._fetch_stock_data(symbol) if data: self.stock_data[symbol] = data From bac08af55277afc2bf4aad3cf1979cea21678aaa Mon Sep 17 00:00:00 2001 From: Chuck <33324927+ChuckBuilds@users.noreply.github.com> Date: Tue, 8 Apr 2025 21:35:22 -0500 Subject: [PATCH 05/87] Update stock_manager.py stock query update --- src/stock_manager.py | 78 +++++++++++++++++++++++--------------------- 1 file changed, 41 insertions(+), 37 deletions(-) diff --git a/src/stock_manager.py b/src/stock_manager.py index 5f841b6d..71a25985 100644 --- a/src/stock_manager.py +++ b/src/stock_manager.py @@ -92,42 +92,32 @@ class StockManager: def _fetch_stock_data(self, symbol: str) -> Dict[str, Any]: """Fetch stock data from Yahoo Finance public API.""" try: - # Use Yahoo Finance public API + # Use Yahoo Finance query1 API encoded_symbol = urllib.parse.quote(symbol) - url = f"https://finance.yahoo.com/quote/{encoded_symbol}" + url = f"https://query1.finance.yahoo.com/v8/finance/chart/{encoded_symbol}" response = requests.get(url, headers=self.headers, timeout=5) if response.status_code != 200: logger.error(f"Failed to fetch data for {symbol}: HTTP {response.status_code}") return None - # Extract the embedded JSON data - quote_data = self._extract_json_from_html(response.text) - if not quote_data: - logger.error(f"Could not extract quote data for {symbol}") + data = response.json() + + # Extract the relevant data from the response + meta = data.get('chart', {}).get('result', [{}])[0].get('meta', {}) + + if not meta: + logger.error(f"No meta data found for {symbol}") return None - # Get the price data - price = quote_data.get('price', {}) - if not price: - logger.error(f"No price data found for {symbol}") - return None - - regular_market = price.get('regularMarketPrice', {}) - previous_close = price.get('regularMarketPreviousClose', {}) - change_percent = price.get('regularMarketChangePercent', {}) + current_price = meta.get('regularMarketPrice', 0) + prev_close = meta.get('previousClose', current_price) - # Extract raw values with fallbacks - current_price = regular_market.get('raw', 0) if isinstance(regular_market, dict) else regular_market - prev_close = previous_close.get('raw', current_price) if isinstance(previous_close, dict) else previous_close - change_pct = change_percent.get('raw', 0) if isinstance(change_percent, dict) else change_percent + # Calculate change percentage + change_pct = ((current_price - prev_close) / prev_close) * 100 if prev_close > 0 else 0 - # If we don't have a change percentage, calculate it - if change_pct == 0 and prev_close > 0: - change_pct = ((current_price - prev_close) / prev_close) * 100 - - # Get company name - name = price.get('shortName', symbol) + # Get company name (symbol will be used if name not available) + name = meta.get('symbol', symbol) logger.debug(f"Processed data for {symbol}: price={current_price}, change={change_pct}%") @@ -142,7 +132,7 @@ class StockManager: except requests.exceptions.RequestException as e: logger.error(f"Network error fetching data for {symbol}: {e}") return None - except (ValueError, IndexError) as e: + except (ValueError, IndexError, KeyError) as e: logger.error(f"Error parsing data for {symbol}: {e}") return None except Exception as e: @@ -195,9 +185,8 @@ class StockManager: logger.warning("No stock data available to display") return - # Clear the display if forced or if this is the first stock - if force_clear or self.current_stock_index == 0: - self.display_manager.clear() + # Clear the display for each update + self.display_manager.clear() # Get the current stock to display symbols = list(self.stock_data.keys()) @@ -208,16 +197,31 @@ class StockManager: data = self.stock_data[current_symbol] # Format the display text - display_format = self.stocks_config.get('display_format', "{symbol}: ${price} ({change}%)") - display_text = display_format.format( - symbol=data['symbol'], - price=f"{data['price']:.2f}", - change=f"{data['change']:+.2f}" + price_text = f"${data['price']:.2f}" + change_text = f"({data['change']:+.1f}%)" + + # Draw the stock symbol at the top + self.display_manager.draw_text( + data['symbol'], + y=2, # Near top + color=(255, 255, 255) # White for symbol ) - # Draw the stock information - color = (0, 255, 0) if data['change'] >= 0 else (255, 0, 0) # Green for up, red for down - self.display_manager.draw_text(display_text, color=color) + # Draw the price in the middle + self.display_manager.draw_text( + price_text, + y=12, # Middle + color=(0, 255, 0) if data['change'] >= 0 else (255, 0, 0) # Green for up, red for down + ) + + # Draw the change percentage at the bottom + self.display_manager.draw_text( + change_text, + y=22, # Near bottom + color=(0, 255, 0) if data['change'] >= 0 else (255, 0, 0) # Green for up, red for down + ) + + # Update the display self.display_manager.update_display() # Move to next stock for next update From 730450ff1c1b4f6f0ebe3a6d60891a1faf022435 Mon Sep 17 00:00:00 2001 From: Chuck <33324927+ChuckBuilds@users.noreply.github.com> Date: Tue, 8 Apr 2025 21:40:04 -0500 Subject: [PATCH 06/87] Update stock_manager.py slow down stock display --- src/stock_manager.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/stock_manager.py b/src/stock_manager.py index 71a25985..6520f39e 100644 --- a/src/stock_manager.py +++ b/src/stock_manager.py @@ -224,5 +224,8 @@ class StockManager: # Update the display self.display_manager.update_display() + # Add a delay to make each stock visible longer (3 seconds) + time.sleep(3) + # Move to next stock for next update self.current_stock_index = (self.current_stock_index + 1) % len(symbols) \ No newline at end of file From 31e57ad89763fccdc3fa7cd8afc8cf156e5ad4fe Mon Sep 17 00:00:00 2001 From: Chuck <33324927+ChuckBuilds@users.noreply.github.com> Date: Tue, 8 Apr 2025 21:41:21 -0500 Subject: [PATCH 07/87] Update display_controller.py adjust screen flow --- src/display_controller.py | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/src/display_controller.py b/src/display_controller.py index 8bf52b95..37548969 100644 --- a/src/display_controller.py +++ b/src/display_controller.py @@ -20,6 +20,7 @@ class DisplayController: self.weather = WeatherManager(self.config, self.display_manager) self.stocks = StockManager(self.config, self.display_manager) self.current_display = 'clock' + self.weather_mode = 'current' # current, hourly, or daily self.last_switch = time.time() self.force_clear = True # Start with a clear screen self.update_interval = 0.5 # Slower updates for better stability @@ -32,19 +33,25 @@ class DisplayController: current_time = time.time() # Check if we need to switch display mode - if current_time - self.last_switch > self.config['display'].get('rotation_interval', 30): - # Cycle through: clock -> weather -> stocks + if current_time - self.last_switch > self.config['display'].get('rotation_interval', 15): + # Cycle through: clock -> weather (current) -> weather (hourly) -> weather (daily) -> stocks if self.current_display == 'clock': self.current_display = 'weather' + self.weather_mode = 'current' elif self.current_display == 'weather': - if self.config.get('stocks', {}).get('enabled', False): - self.current_display = 'stocks' - else: - self.current_display = 'clock' + if self.weather_mode == 'current': + self.weather_mode = 'hourly' + elif self.weather_mode == 'hourly': + self.weather_mode = 'daily' + else: # daily + if self.config.get('stocks', {}).get('enabled', False): + self.current_display = 'stocks' + else: + self.current_display = 'clock' else: # stocks self.current_display = 'clock' - logger.info("Switching display to: %s", self.current_display) + logger.info(f"Switching display to: {self.current_display} {self.weather_mode if self.current_display == 'weather' else ''}") self.last_switch = current_time self.force_clear = True self.display_manager.clear() # Ensure clean transition @@ -54,7 +61,12 @@ class DisplayController: if self.current_display == 'clock': self.clock.display_time(force_clear=self.force_clear) elif self.current_display == 'weather': - self.weather.display_weather(force_clear=self.force_clear) + if self.weather_mode == 'current': + self.weather.display_weather(force_clear=self.force_clear) + elif self.weather_mode == 'hourly': + self.weather.display_hourly_forecast(force_clear=self.force_clear) + else: # daily + self.weather.display_daily_forecast(force_clear=self.force_clear) else: # stocks self.stocks.display_stocks(force_clear=self.force_clear) except Exception as e: From f527f4b6469d418b0b723c500cc43375210a02f8 Mon Sep 17 00:00:00 2001 From: Chuck <33324927+ChuckBuilds@users.noreply.github.com> Date: Tue, 8 Apr 2025 21:57:30 -0500 Subject: [PATCH 08/87] Update stock_manager.py shipping features --- src/stock_manager.py | 155 +++++++++++++++++++++++++++++++++---------- 1 file changed, 120 insertions(+), 35 deletions(-) diff --git a/src/stock_manager.py b/src/stock_manager.py index 6520f39e..8c3ee60d 100644 --- a/src/stock_manager.py +++ b/src/stock_manager.py @@ -21,6 +21,7 @@ class StockManager: self.last_update = 0 self.stock_data = {} self.current_stock_index = 0 + self.display_mode = 'info' # 'info' or 'chart' self.headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' } @@ -92,11 +93,15 @@ class StockManager: def _fetch_stock_data(self, symbol: str) -> Dict[str, Any]: """Fetch stock data from Yahoo Finance public API.""" try: - # Use Yahoo Finance query1 API + # Use Yahoo Finance query1 API for chart data encoded_symbol = urllib.parse.quote(symbol) url = f"https://query1.finance.yahoo.com/v8/finance/chart/{encoded_symbol}" + params = { + 'interval': '5m', # 5-minute intervals + 'range': '1d' # 1 day of data + } - response = requests.get(url, headers=self.headers, timeout=5) + response = requests.get(url, headers=self.headers, params=params, timeout=5) if response.status_code != 200: logger.error(f"Failed to fetch data for {symbol}: HTTP {response.status_code}") return None @@ -104,7 +109,8 @@ class StockManager: data = response.json() # Extract the relevant data from the response - meta = data.get('chart', {}).get('result', [{}])[0].get('meta', {}) + chart_data = data.get('chart', {}).get('result', [{}])[0] + meta = chart_data.get('meta', {}) if not meta: logger.error(f"No meta data found for {symbol}") @@ -113,6 +119,20 @@ class StockManager: current_price = meta.get('regularMarketPrice', 0) prev_close = meta.get('previousClose', current_price) + # Get price history + timestamps = chart_data.get('timestamp', []) + indicators = chart_data.get('indicators', {}).get('quote', [{}])[0] + close_prices = indicators.get('close', []) + + # Build price history + price_history = [] + for i, ts in enumerate(timestamps): + if i < len(close_prices) and close_prices[i] is not None: + price_history.append({ + 'timestamp': datetime.fromtimestamp(ts), + 'price': close_prices[i] + }) + # Calculate change percentage change_pct = ((current_price - prev_close) / prev_close) * 100 if prev_close > 0 else 0 @@ -126,7 +146,8 @@ class StockManager: "name": name, "price": current_price, "change": change_pct, - "open": prev_close + "open": prev_close, + "price_history": price_history } except requests.exceptions.RequestException as e: @@ -139,6 +160,63 @@ class StockManager: logger.error(f"Unexpected error fetching data for {symbol}: {e}") return None + def _draw_chart(self, symbol: str, data: Dict[str, Any]): + """Draw a price chart for the stock.""" + if not data.get('price_history'): + return + + # Clear the display + self.display_manager.clear() + + # Draw the symbol at the top + self.display_manager.draw_text( + symbol, + y=2, + color=(255, 255, 255) + ) + + # Calculate chart dimensions + chart_height = 20 # Leave room for text above and below + chart_y = 8 # Start below the symbol + width = self.display_manager.matrix.width + + # Get min and max prices for scaling + prices = [p['price'] for p in data['price_history']] + if not prices: + return + min_price = min(prices) + max_price = max(prices) + price_range = max_price - min_price + + if price_range == 0: + return + + # Draw chart points + points = [] + color = self._get_stock_color(symbol) + + for i, point in enumerate(data['price_history']): + x = int((i / len(data['price_history'])) * width) + y = chart_y + chart_height - int(((point['price'] - min_price) / price_range) * chart_height) + points.append((x, y)) + + # Draw lines between points + for i in range(len(points) - 1): + x1, y1 = points[i] + x2, y2 = points[i + 1] + self.display_manager.draw.line([x1, y1, x2, y2], fill=color, width=1) + + # Draw current price at the bottom + price_text = f"${data['price']:.2f} ({data['change']:+.1f}%)" + self.display_manager.draw_text( + price_text, + y=30, # Near bottom + color=color + ) + + # Update the display + self.display_manager.update_display() + def update_stock_data(self): """Update stock data if enough time has passed.""" current_time = time.time() @@ -185,9 +263,6 @@ class StockManager: logger.warning("No stock data available to display") return - # Clear the display for each update - self.display_manager.clear() - # Get the current stock to display symbols = list(self.stock_data.keys()) if not symbols: @@ -196,35 +271,45 @@ class StockManager: current_symbol = symbols[self.current_stock_index] data = self.stock_data[current_symbol] - # Format the display text - price_text = f"${data['price']:.2f}" - change_text = f"({data['change']:+.1f}%)" + # Toggle between info and chart display + if self.display_mode == 'info': + # Clear the display + self.display_manager.clear() + + # Draw the stock symbol at the top + self.display_manager.draw_text( + data['symbol'], + y=2, # Near top + color=(255, 255, 255) # White for symbol + ) + + # Draw the price in the middle + price_text = f"${data['price']:.2f}" + self.display_manager.draw_text( + price_text, + y=12, # Middle + color=(0, 255, 0) if data['change'] >= 0 else (255, 0, 0) # Green for up, red for down + ) + + # Draw the change percentage at the bottom + change_text = f"({data['change']:+.1f}%)" + self.display_manager.draw_text( + change_text, + y=22, # Near bottom + color=(0, 255, 0) if data['change'] >= 0 else (255, 0, 0) # Green for up, red for down + ) + + # Update the display + self.display_manager.update_display() + + # Switch to chart mode next time + self.display_mode = 'chart' + else: # chart mode + self._draw_chart(current_symbol, data) + # Switch back to info mode next time + self.display_mode = 'info' - # Draw the stock symbol at the top - self.display_manager.draw_text( - data['symbol'], - y=2, # Near top - color=(255, 255, 255) # White for symbol - ) - - # Draw the price in the middle - self.display_manager.draw_text( - price_text, - y=12, # Middle - color=(0, 255, 0) if data['change'] >= 0 else (255, 0, 0) # Green for up, red for down - ) - - # Draw the change percentage at the bottom - self.display_manager.draw_text( - change_text, - y=22, # Near bottom - color=(0, 255, 0) if data['change'] >= 0 else (255, 0, 0) # Green for up, red for down - ) - - # Update the display - self.display_manager.update_display() - - # Add a delay to make each stock visible longer (3 seconds) + # Add a delay to make each display visible time.sleep(3) # Move to next stock for next update From a7982ba9129a145cfd4ad706079d783bf709bb8d Mon Sep 17 00:00:00 2001 From: Chuck <33324927+ChuckBuilds@users.noreply.github.com> Date: Tue, 8 Apr 2025 22:05:27 -0500 Subject: [PATCH 09/87] Update stock_manager.py stock refresh in the background --- src/stock_manager.py | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/src/stock_manager.py b/src/stock_manager.py index 8c3ee60d..3a071a81 100644 --- a/src/stock_manager.py +++ b/src/stock_manager.py @@ -25,6 +25,8 @@ class StockManager: self.headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' } + # Initialize with first update + self.update_stock_data() def _get_stock_color(self, symbol: str) -> Tuple[int, int, int]: """Get color based on stock performance.""" @@ -222,7 +224,7 @@ class StockManager: current_time = time.time() update_interval = self.stocks_config.get('update_interval', 60) - # Add a small random delay to prevent exact timing matches + # If not enough time has passed, keep using existing data if current_time - self.last_update < update_interval + random.uniform(0, 2): return @@ -236,18 +238,23 @@ class StockManager: if isinstance(symbols[0], str): symbols = [{"symbol": symbol} for symbol in symbols] - success = False # Track if we got any successful updates + # Create temporary storage for new data + new_data = {} + success = False + for stock in symbols: symbol = stock['symbol'] # Add a small delay between requests to avoid rate limiting - time.sleep(random.uniform(0.5, 1.5)) + time.sleep(random.uniform(0.1, 0.3)) # Reduced delay data = self._fetch_stock_data(symbol) if data: - self.stock_data[symbol] = data + new_data[symbol] = data success = True logger.info(f"Updated {symbol}: ${data['price']:.2f} ({data['change']:+.2f}%)") if success: + # Only update the displayed data when we have new data + self.stock_data.update(new_data) self.last_update = current_time else: logger.error("Failed to fetch data for any configured stocks") @@ -257,7 +264,9 @@ class StockManager: if not self.stocks_config.get('enabled', False): return - self.update_stock_data() + # Start update in background if needed + if time.time() - self.last_update >= self.stocks_config.get('update_interval', 60): + self.update_stock_data() if not self.stock_data: logger.warning("No stock data available to display") @@ -313,4 +322,7 @@ class StockManager: time.sleep(3) # Move to next stock for next update - self.current_stock_index = (self.current_stock_index + 1) % len(symbols) \ No newline at end of file + self.current_stock_index = (self.current_stock_index + 1) % len(symbols) + + # If we've shown all stocks, signal completion by returning True + return self.current_stock_index == 0 \ No newline at end of file From 9ea31698abb8c72523c919291f5eb03d8fa0f1f7 Mon Sep 17 00:00:00 2001 From: Chuck <33324927+ChuckBuilds@users.noreply.github.com> Date: Tue, 8 Apr 2025 22:11:01 -0500 Subject: [PATCH 10/87] Customize Display timings customize display timings --- config/config.json | 8 +++++++- src/display_controller.py | 19 ++++++++++++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/config/config.json b/config/config.json index 4f498136..8bd14511 100644 --- a/config/config.json +++ b/config/config.json @@ -25,7 +25,13 @@ "runtime": { "gpio_slowdown": 2 }, - "rotation_interval": 15 + "display_durations": { + "clock": 15, + "weather": 15, + "stocks": 45, + "hourly_forecast": 15, + "daily_forecast": 15 + } }, "clock": { "format": "%H:%M:%S", diff --git a/src/display_controller.py b/src/display_controller.py index 37548969..41701685 100644 --- a/src/display_controller.py +++ b/src/display_controller.py @@ -24,8 +24,25 @@ class DisplayController: self.last_switch = time.time() self.force_clear = True # Start with a clear screen self.update_interval = 0.5 # Slower updates for better stability + self.display_durations = self.config['display'].get('display_durations', { + 'clock': 15, + 'weather': 15, + 'stocks': 45, + 'hourly_forecast': 15, + 'daily_forecast': 15 + }) logger.info("DisplayController initialized with display_manager: %s", id(self.display_manager)) + def get_current_duration(self) -> int: + """Get the duration for the current display mode.""" + if self.current_display == 'weather': + if self.weather_mode == 'hourly': + return self.display_durations.get('hourly_forecast', 15) + elif self.weather_mode == 'daily': + return self.display_durations.get('daily_forecast', 15) + return self.display_durations.get('weather', 15) + return self.display_durations.get(self.current_display, 15) + def run(self): """Run the display controller, switching between displays.""" try: @@ -33,7 +50,7 @@ class DisplayController: current_time = time.time() # Check if we need to switch display mode - if current_time - self.last_switch > self.config['display'].get('rotation_interval', 15): + if current_time - self.last_switch > self.get_current_duration(): # Cycle through: clock -> weather (current) -> weather (hourly) -> weather (daily) -> stocks if self.current_display == 'clock': self.current_display = 'weather' From 75ff4279e75897859628bd341b3fc8c89fb1dd4e Mon Sep 17 00:00:00 2001 From: Chuck <33324927+ChuckBuilds@users.noreply.github.com> Date: Tue, 8 Apr 2025 22:14:04 -0500 Subject: [PATCH 11/87] Update stock_manager.py stock font size change --- src/stock_manager.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/stock_manager.py b/src/stock_manager.py index 3a071a81..79e9c232 100644 --- a/src/stock_manager.py +++ b/src/stock_manager.py @@ -285,27 +285,29 @@ class StockManager: # Clear the display self.display_manager.clear() - # Draw the stock symbol at the top + # Draw the stock symbol at the top with regular font self.display_manager.draw_text( data['symbol'], y=2, # Near top color=(255, 255, 255) # White for symbol ) - # Draw the price in the middle + # Draw the price in the middle with small font price_text = f"${data['price']:.2f}" self.display_manager.draw_text( price_text, - y=12, # Middle - color=(0, 255, 0) if data['change'] >= 0 else (255, 0, 0) # Green for up, red for down + y=14, # Middle + color=(0, 255, 0) if data['change'] >= 0 else (255, 0, 0), # Green for up, red for down + small_font=True # Use small font ) - # Draw the change percentage at the bottom + # Draw the change percentage at the bottom with small font change_text = f"({data['change']:+.1f}%)" self.display_manager.draw_text( change_text, - y=22, # Near bottom - color=(0, 255, 0) if data['change'] >= 0 else (255, 0, 0) # Green for up, red for down + y=24, # Near bottom + color=(0, 255, 0) if data['change'] >= 0 else (255, 0, 0), # Green for up, red for down + small_font=True # Use small font ) # Update the display From f6be7b554bb1ed3a8387aa48c45cef94bc22517a Mon Sep 17 00:00:00 2001 From: Chuck <33324927+ChuckBuilds@users.noreply.github.com> Date: Tue, 8 Apr 2025 22:21:31 -0500 Subject: [PATCH 12/87] Sizing and Spacing CHanged font sizing on chart and clock spacing --- src/clock.py | 5 +++-- src/stock_manager.py | 16 +++++++++------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/clock.py b/src/clock.py index 2ae648e4..ff70d11f 100644 --- a/src/clock.py +++ b/src/clock.py @@ -89,9 +89,10 @@ class Clock: # Get AM/PM ampm = current.strftime('%p') - # Format date with ordinal suffix + # Format date with ordinal suffix - more compact format day_suffix = self._get_ordinal_suffix(current.day) - date_str = current.strftime(f'%A, %B %-d{day_suffix}') + # Use %b for abbreviated month name and remove extra spaces + date_str = current.strftime(f'%a,%b %-d{day_suffix}') return time_str, ampm, date_str diff --git a/src/stock_manager.py b/src/stock_manager.py index 79e9c232..4e228b39 100644 --- a/src/stock_manager.py +++ b/src/stock_manager.py @@ -170,16 +170,17 @@ class StockManager: # Clear the display self.display_manager.clear() - # Draw the symbol at the top + # Draw the symbol at the top with small font self.display_manager.draw_text( symbol, - y=2, - color=(255, 255, 255) + y=1, # Moved up slightly + color=(255, 255, 255), + small_font=True # Use small font ) # Calculate chart dimensions - chart_height = 20 # Leave room for text above and below - chart_y = 8 # Start below the symbol + chart_height = 22 # Increased height since we're using smaller fonts + chart_y = 7 # Start closer to symbol due to smaller font width = self.display_manager.matrix.width # Get min and max prices for scaling @@ -208,12 +209,13 @@ class StockManager: x2, y2 = points[i + 1] self.display_manager.draw.line([x1, y1, x2, y2], fill=color, width=1) - # Draw current price at the bottom + # Draw current price at the bottom with small font price_text = f"${data['price']:.2f} ({data['change']:+.1f}%)" self.display_manager.draw_text( price_text, y=30, # Near bottom - color=color + color=color, + small_font=True # Use small font ) # Update the display From 8e98a6d3c3b3b379c6e28affc54c2d888c26a8b9 Mon Sep 17 00:00:00 2001 From: Chuck <33324927+ChuckBuilds@users.noreply.github.com> Date: Tue, 8 Apr 2025 22:25:58 -0500 Subject: [PATCH 13/87] Update clock.py Date format changes --- src/clock.py | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/src/clock.py b/src/clock.py index ff70d11f..1562271d 100644 --- a/src/clock.py +++ b/src/clock.py @@ -89,16 +89,17 @@ class Clock: # Get AM/PM ampm = current.strftime('%p') - # Format date with ordinal suffix - more compact format + # Format date with ordinal suffix - split into two lines day_suffix = self._get_ordinal_suffix(current.day) - # Use %b for abbreviated month name and remove extra spaces - date_str = current.strftime(f'%a,%b %-d{day_suffix}') + # Full weekday on first line, full month and day on second line + weekday = current.strftime('%A') + date_str = current.strftime(f'%B %-d{day_suffix}') - return time_str, ampm, date_str + return time_str, ampm, weekday, date_str def display_time(self, force_clear: bool = False) -> None: """Display the current time and date.""" - time_str, ampm, date_str = self.get_current_time() + time_str, ampm, weekday, date_str = self.get_current_time() # Only update if something has changed if time_str != self.last_time or date_str != self.last_date or force_clear: @@ -112,7 +113,7 @@ class Clock: # Draw time (large, centered, near top) self.display_manager.draw_text( time_str, - y=3, # Move down slightly from top + y=2, # Move up slightly to make room for two lines of date color=self.COLORS['time'], small_font=False ) @@ -123,15 +124,23 @@ class Clock: self.display_manager.draw_text( ampm, x=ampm_x, - y=5, # Align with time + y=4, # Align with time color=self.COLORS['ampm'], small_font=True ) - # Draw date (small, centered below time) + # Draw weekday on first line (small font) + self.display_manager.draw_text( + weekday, + y=display_height - 18, # First line of date + color=self.COLORS['date'], + small_font=True + ) + + # Draw month and day on second line (small font) self.display_manager.draw_text( date_str, - y=display_height - 9, # Move up more from bottom + y=display_height - 9, # Second line of date color=self.COLORS['date'], small_font=True ) From 28717663574148fa9c75b3f9a1e42c63d62241c1 Mon Sep 17 00:00:00 2001 From: Chuck <33324927+ChuckBuilds@users.noreply.github.com> Date: Tue, 8 Apr 2025 22:32:24 -0500 Subject: [PATCH 14/87] Update stock_manager.py actually read stocks from config file --- src/stock_manager.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/stock_manager.py b/src/stock_manager.py index 4e228b39..b21b75fe 100644 --- a/src/stock_manager.py +++ b/src/stock_manager.py @@ -16,6 +16,7 @@ logger = logging.getLogger(__name__) class StockManager: def __init__(self, config: Dict[str, Any], display_manager): self.config = config + self.config_manager = ConfigManager() # Add config manager self.display_manager = display_manager self.stocks_config = config.get('stocks', {}) self.last_update = 0 @@ -221,6 +222,19 @@ class StockManager: # Update the display self.display_manager.update_display() + def _reload_config(self): + """Reload configuration from file.""" + self.config = self.config_manager.config + self.stocks_config = self.config.get('stocks', {}) + # Reset stock data if symbols have changed + new_symbols = set(self.stocks_config.get('symbols', [])) + current_symbols = set(self.stock_data.keys()) + if new_symbols != current_symbols: + self.stock_data = {} + self.current_stock_index = 0 + self.last_update = 0 # Force immediate update + logger.info(f"Stock symbols changed. New symbols: {new_symbols}") + def update_stock_data(self): """Update stock data if enough time has passed.""" current_time = time.time() @@ -229,6 +243,9 @@ class StockManager: # If not enough time has passed, keep using existing data if current_time - self.last_update < update_interval + random.uniform(0, 2): return + + # Reload config to check for symbol changes + self._reload_config() # Get symbols from config symbols = self.stocks_config.get('symbols', []) From db96c29e90149cee4e3f96262b1c7512a75e9b74 Mon Sep 17 00:00:00 2001 From: Chuck <33324927+ChuckBuilds@users.noreply.github.com> Date: Tue, 8 Apr 2025 22:36:50 -0500 Subject: [PATCH 15/87] Update stock_manager.py add config manager --- src/stock_manager.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/stock_manager.py b/src/stock_manager.py index b21b75fe..d3e052dc 100644 --- a/src/stock_manager.py +++ b/src/stock_manager.py @@ -8,6 +8,7 @@ from datetime import datetime import os import urllib.parse import re +from src.config_manager import ConfigManager # Configure logging logging.basicConfig(level=logging.INFO) From 69ff4b335f6d6b6912d8c53b5b92d53e3a675de5 Mon Sep 17 00:00:00 2001 From: Chuck <33324927+ChuckBuilds@users.noreply.github.com> Date: Thu, 10 Apr 2025 19:59:22 -0500 Subject: [PATCH 16/87] readme update readme update and formatting for better flow --- README.md | 72 +++++++++++++++++++++++++++---------------------------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index c4631269..605a4d01 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# LEDSportsMatrix +# LEDMatrix A modular LED matrix display system for sports information using Raspberry Pi and RGB LED matrices. @@ -11,14 +11,15 @@ A modular LED matrix display system for sports information using Raspberry Pi an 1. Clone this repository: ```bash -git clone https://github.com/yourusername/LEDSportsMatrix.git -cd LEDSportsMatrix +git clone https://github.com/ChuckBuilds/LEDMatrix.git +cd LEDMatrix ``` 2. Install dependencies: ```bash -pip3 install -r requirements.txt +pip3 install --break-system-packages -r requirements.txt ``` +--break-system-packages allows us to install without a virtual environment ## Configuration @@ -29,6 +30,16 @@ cp config/config.example.json config/config.json 2. Edit `config/config.json` with your preferences +## 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 + + ## Important: Sound Module Configuration 1. Remove unnecessary services that might interfere with the LED matrix: @@ -50,6 +61,27 @@ sudo update-initramfs -u sudo reboot ``` +## Performance Optimization + +To reduce flickering and improve display quality: + +1. Edit `/boot/firmware/cmdline.txt`: +```bash +sudo nano /boot/firmware/cmdline.txt +``` + +2. Add `isolcpus=3` at the end of the line + +3. Add `dtparam=audio=off` at the end of the line + +4. Ctrl + X to exit, Y to save + +5. Save and reboot: +```bash +sudo reboot +``` + + ## Running the Display From the project root directory: @@ -76,38 +108,6 @@ LEDSportsMatrix/ └── display_controller.py # Main entry point ``` -## Performance Optimization - -To reduce flickering and improve display quality: - -1. Edit `/boot/firmware/cmdline.txt`: -```bash -sudo nano /boot/firmware/cmdline.txt -``` - -2. Add `isolcpus=3` at the end of the line - -3. Save and reboot: -```bash -sudo reboot -``` - -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 - -Note: If you still experience issues, you can additionally disable the audio hardware by editing `/boot/firmware/config.txt`: -```bash -sudo nano /boot/firmware/config.txt -``` -And adding: -``` -dtparam=audio=off -``` - -Alternatively, you can: -- Use external USB sound adapters if you need audio -- Run the program with `--led-no-hardware-pulse` flag (may cause more flicker) ## Project Structure From 21cd5c9e11c3b4391f39d4a9d099c8027d216947 Mon Sep 17 00:00:00 2001 From: Chuck <33324927+ChuckBuilds@users.noreply.github.com> Date: Thu, 10 Apr 2025 20:04:21 -0500 Subject: [PATCH 17/87] Update .gitignore rename reference folder --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index f2a4f7fa..115db2bb 100644 --- a/.gitignore +++ b/.gitignore @@ -20,4 +20,4 @@ ENV/ *.swo # Dependencies -sports-0.0.115/ \ No newline at end of file +sports-reference/ \ No newline at end of file From 1610afd9cb9ae3994e70cf9c550fdb9c7fa92e2b Mon Sep 17 00:00:00 2001 From: Chuck <33324927+ChuckBuilds@users.noreply.github.com> Date: Thu, 10 Apr 2025 20:07:24 -0500 Subject: [PATCH 18/87] Update config.json changed default stocks to test update implementation --- config/config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/config.json b/config/config.json index 8bd14511..2a606018 100644 --- a/config/config.json +++ b/config/config.json @@ -46,7 +46,7 @@ "enabled": true, "update_interval": 60, "symbols": [ - "AAPL", "MSFT", "GOOGL", "AMZN", "META", "TSLA", "NVDA", "JPM", "V", "WMT" + "ASTS", "SCHD", "INTC", "NVDA", "T", "VOO", "SPYG", "SMCI" ], "display_format": "{symbol}: ${price} ({change}%)" } From a01c9027160397065b6f5fff263e849c9535499d Mon Sep 17 00:00:00 2001 From: Chuck <33324927+ChuckBuilds@users.noreply.github.com> Date: Thu, 10 Apr 2025 20:11:59 -0500 Subject: [PATCH 19/87] Stock News Stock news Ticker --- config/config.json | 10 ++- integrate_news_ticker.py | 51 ++++++++++++ src/news_manager.py | 174 +++++++++++++++++++++++++++++++++++++++ test_news_ticker.py | 44 ++++++++++ 4 files changed, 278 insertions(+), 1 deletion(-) create mode 100644 integrate_news_ticker.py create mode 100644 src/news_manager.py create mode 100644 test_news_ticker.py diff --git a/config/config.json b/config/config.json index 2a606018..4752caad 100644 --- a/config/config.json +++ b/config/config.json @@ -30,7 +30,8 @@ "weather": 15, "stocks": 45, "hourly_forecast": 15, - "daily_forecast": 15 + "daily_forecast": 15, + "news": 30 } }, "clock": { @@ -49,5 +50,12 @@ "ASTS", "SCHD", "INTC", "NVDA", "T", "VOO", "SPYG", "SMCI" ], "display_format": "{symbol}: ${price} ({change}%)" + }, + "news": { + "enabled": true, + "update_interval": 300, + "scroll_speed": 1, + "scroll_delay": 0.05, + "max_headlines_per_symbol": 1 } } \ No newline at end of file diff --git a/integrate_news_ticker.py b/integrate_news_ticker.py new file mode 100644 index 00000000..88788b22 --- /dev/null +++ b/integrate_news_ticker.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +import time +import sys +import os +from src.config_manager import ConfigManager +from src.display_manager import DisplayManager +from src.news_manager import NewsManager +from src.stock_manager import StockManager + +def main(): + """Integrate news ticker with the existing display system.""" + try: + # Load configuration + config_manager = ConfigManager() + config = config_manager.config + + # Initialize display manager + display_manager = DisplayManager(config.get('display', {})) + + # Initialize stock manager + stock_manager = StockManager(config, display_manager) + + # Initialize news manager + news_manager = NewsManager(config, display_manager) + + print("News ticker integration test started. Press Ctrl+C to exit.") + print("Displaying stock data and news headlines...") + + # Display stock data and news headlines in a loop + while True: + # Display stock data + stock_manager.display_stocks() + + # Display news headlines for a limited time (30 seconds) + start_time = time.time() + while time.time() - start_time < 30: + news_manager.display_news() + + except KeyboardInterrupt: + print("\nTest interrupted by user.") + except Exception as e: + print(f"Error: {e}") + finally: + # Clean up + if 'display_manager' in locals(): + display_manager.clear() + display_manager.update_display() + display_manager.cleanup() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/src/news_manager.py b/src/news_manager.py new file mode 100644 index 00000000..d0f109c5 --- /dev/null +++ b/src/news_manager.py @@ -0,0 +1,174 @@ +import time +import logging +import requests +import json +import random +from typing import Dict, Any, List, Tuple +from datetime import datetime +import os +import urllib.parse +import re +from src.config_manager import ConfigManager + +# Configure logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +class NewsManager: + def __init__(self, config: Dict[str, Any], display_manager): + self.config = config + self.config_manager = ConfigManager() + self.display_manager = display_manager + self.stocks_config = config.get('stocks', {}) + self.news_config = config.get('news', {}) + self.last_update = 0 + self.news_data = {} + self.current_news_index = 0 + self.scroll_position = 0 + self.scroll_speed = self.news_config.get('scroll_speed', 1) # Pixels to move per frame + self.scroll_delay = self.news_config.get('scroll_delay', 0.05) # Delay between scroll updates + self.headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' + } + # Initialize with first update + self.update_news_data() + + def _fetch_news_for_symbol(self, symbol: str) -> List[Dict[str, Any]]: + """Fetch news headlines for a stock symbol.""" + try: + # Using Yahoo Finance API to get news + encoded_symbol = urllib.parse.quote(symbol) + url = f"https://query1.finance.yahoo.com/v1/finance/search?q={encoded_symbol}&lang=en-US®ion=US"esCount=0&newsCount={self.news_config.get('max_headlines_per_symbol', 5)}" + + response = requests.get(url, headers=self.headers, timeout=5) + if response.status_code != 200: + logger.error(f"Failed to fetch news for {symbol}: HTTP {response.status_code}") + return [] + + data = response.json() + news_items = data.get('news', []) + + # Process and format news items + formatted_news = [] + for item in news_items: + formatted_news.append({ + "title": item.get('title', ''), + "publisher": item.get('publisher', ''), + "link": item.get('link', ''), + "published": item.get('providerPublishTime', 0) + }) + + logger.info(f"Fetched {len(formatted_news)} news items for {symbol}") + return formatted_news + + except requests.exceptions.RequestException as e: + logger.error(f"Network error fetching news for {symbol}: {e}") + return [] + except (ValueError, IndexError, KeyError) as e: + logger.error(f"Error parsing news data for {symbol}: {e}") + return [] + except Exception as e: + logger.error(f"Unexpected error fetching news for {symbol}: {e}") + return [] + + def update_news_data(self): + """Update news data for all configured stock symbols.""" + current_time = time.time() + update_interval = self.news_config.get('update_interval', 300) # Default to 5 minutes + + # If not enough time has passed, keep using existing data + if current_time - self.last_update < update_interval: + return + + # Get symbols from config + symbols = self.stocks_config.get('symbols', []) + if not symbols: + logger.warning("No stock symbols configured for news") + return + + # Create temporary storage for new data + new_data = {} + success = False + + for symbol in symbols: + # Add a small delay between requests to avoid rate limiting + time.sleep(random.uniform(0.1, 0.3)) + news_items = self._fetch_news_for_symbol(symbol) + if news_items: + new_data[symbol] = news_items + success = True + + if success: + # Only update the displayed data when we have new data + self.news_data = new_data + self.last_update = current_time + self.current_news_index = 0 + self.scroll_position = 0 + logger.info(f"Updated news data for {len(new_data)} symbols") + else: + logger.error("Failed to fetch news for any configured stocks") + + def display_news(self): + """Display news headlines by scrolling them across the screen.""" + if not self.news_config.get('enabled', False): + return + + # Start update in background if needed + if time.time() - self.last_update >= self.news_config.get('update_interval', 300): + self.update_news_data() + + if not self.news_data: + logger.warning("No news data available to display") + return + + # Get all news items from all symbols + all_news = [] + for symbol, news_items in self.news_data.items(): + for item in news_items: + all_news.append({ + "symbol": symbol, + "title": item["title"], + "publisher": item["publisher"] + }) + + if not all_news: + return + + # Get the current news item to display + current_news = all_news[self.current_news_index] + + # Format the news text + news_text = f"{current_news['symbol']}: {current_news['title']}" + + # Get text dimensions + bbox = self.display_manager.draw.textbbox((0, 0), news_text, font=self.display_manager.small_font) + text_width = bbox[2] - bbox[0] + + # Clear the display + self.display_manager.clear() + + # Draw the news text at the current scroll position + self.display_manager.draw_text( + news_text, + x=self.display_manager.matrix.width - self.scroll_position, + y=16, # Center vertically + color=(255, 255, 255), # White + small_font=True + ) + + # Update the display + self.display_manager.update_display() + + # Update scroll position + self.scroll_position += self.scroll_speed + + # If we've scrolled past the end of the text, move to the next news item + if self.scroll_position > text_width + self.display_manager.matrix.width: + self.scroll_position = 0 + self.current_news_index = (self.current_news_index + 1) % len(all_news) + + # Add a small delay to control scroll speed + time.sleep(self.scroll_delay) + + # Return True if we've displayed all news items + return self.current_news_index == 0 and self.scroll_position == 0 \ No newline at end of file diff --git a/test_news_ticker.py b/test_news_ticker.py new file mode 100644 index 00000000..c4d9f2dc --- /dev/null +++ b/test_news_ticker.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +import time +import sys +import os +from src.config_manager import ConfigManager +from src.display_manager import DisplayManager +from src.news_manager import NewsManager + +def main(): + """Test the news ticker functionality.""" + try: + # Load configuration + config_manager = ConfigManager() + config = config_manager.config + + # Initialize display manager + display_manager = DisplayManager(config.get('display', {})) + + # Initialize news manager + news_manager = NewsManager(config, display_manager) + + print("News ticker test started. Press Ctrl+C to exit.") + print("Displaying news headlines for configured stock symbols...") + + # Display news headlines for a limited time (30 seconds) + start_time = time.time() + while time.time() - start_time < 30: + news_manager.display_news() + + print("Test completed successfully.") + + except KeyboardInterrupt: + print("\nTest interrupted by user.") + except Exception as e: + print(f"Error: {e}") + finally: + # Clean up + if 'display_manager' in locals(): + display_manager.clear() + display_manager.update_display() + display_manager.cleanup() + +if __name__ == "__main__": + main() \ No newline at end of file From 50349136f9119031663eb6075a4b50e21710e9bd Mon Sep 17 00:00:00 2001 From: Chuck <33324927+ChuckBuilds@users.noreply.github.com> Date: Thu, 10 Apr 2025 20:15:55 -0500 Subject: [PATCH 20/87] Update config.json increase scroll speed --- config/config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/config.json b/config/config.json index 4752caad..8241f199 100644 --- a/config/config.json +++ b/config/config.json @@ -54,7 +54,7 @@ "news": { "enabled": true, "update_interval": 300, - "scroll_speed": 1, + "scroll_speed": 10, "scroll_delay": 0.05, "max_headlines_per_symbol": 1 } From e24c46b9f4354adb9974180a46da3e5818c06683 Mon Sep 17 00:00:00 2001 From: Chuck <33324927+ChuckBuilds@users.noreply.github.com> Date: Thu, 10 Apr 2025 20:19:10 -0500 Subject: [PATCH 21/87] Scroll Performance Tuning news scrolling performance --- config/config.json | 4 +-- src/news_manager.py | 64 +++++++++++++++++++++++++++++++++---------- test_smooth_scroll.py | 59 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 111 insertions(+), 16 deletions(-) create mode 100644 test_smooth_scroll.py diff --git a/config/config.json b/config/config.json index 8241f199..b7e189ed 100644 --- a/config/config.json +++ b/config/config.json @@ -54,8 +54,8 @@ "news": { "enabled": true, "update_interval": 300, - "scroll_speed": 10, - "scroll_delay": 0.05, + "scroll_speed": 2, + "scroll_delay": 0.03, "max_headlines_per_symbol": 1 } } \ No newline at end of file diff --git a/src/news_manager.py b/src/news_manager.py index d0f109c5..abe05e07 100644 --- a/src/news_manager.py +++ b/src/news_manager.py @@ -9,6 +9,7 @@ import os import urllib.parse import re from src.config_manager import ConfigManager +from PIL import Image, ImageDraw # Configure logging logging.basicConfig(level=logging.INFO) @@ -108,6 +109,23 @@ class NewsManager: else: logger.error("Failed to fetch news for any configured stocks") + def _create_text_image(self, text: str, color: Tuple[int, int, int] = (255, 255, 255)) -> Image.Image: + """Create an image containing the text for efficient scrolling.""" + # Get text dimensions + bbox = self.display_manager.draw.textbbox((0, 0), text, font=self.display_manager.small_font) + text_width = bbox[2] - bbox[0] + text_height = bbox[3] - bbox[1] + + # Create a new image with the text + text_image = Image.new('RGB', (text_width, self.display_manager.matrix.height), (0, 0, 0)) + text_draw = ImageDraw.Draw(text_image) + + # Draw the text centered vertically + y = (self.display_manager.matrix.height - text_height) // 2 + text_draw.text((0, y), text, font=self.display_manager.small_font, fill=color) + + return text_image + def display_news(self): """Display news headlines by scrolling them across the screen.""" if not self.news_config.get('enabled', False): @@ -140,23 +158,41 @@ class NewsManager: # Format the news text news_text = f"{current_news['symbol']}: {current_news['title']}" - # Get text dimensions - bbox = self.display_manager.draw.textbbox((0, 0), news_text, font=self.display_manager.small_font) - text_width = bbox[2] - bbox[0] + # Create a text image for efficient scrolling + text_image = self._create_text_image(news_text) + text_width = text_image.width - # Clear the display - self.display_manager.clear() + # Calculate the visible portion of the text + visible_width = min(self.display_manager.matrix.width, text_width) - # Draw the news text at the current scroll position - self.display_manager.draw_text( - news_text, - x=self.display_manager.matrix.width - self.scroll_position, - y=16, # Center vertically - color=(255, 255, 255), # White - small_font=True - ) + # If this is the first time displaying this news item, clear the screen + if self.scroll_position == 0: + self.display_manager.clear() + self.display_manager.update_display() - # Update the display + # Create a new image for the current frame + frame_image = Image.new('RGB', (self.display_manager.matrix.width, self.display_manager.matrix.height), (0, 0, 0)) + + # Calculate the source and destination regions for the visible portion + src_x = max(0, text_width - self.scroll_position - visible_width) + src_width = min(visible_width, text_width - src_x) + + # Copy the visible portion of the text to the frame + if src_width > 0: + src_region = text_image.crop((src_x, 0, src_x + src_width, self.display_manager.matrix.height)) + frame_image.paste(src_region, (0, 0)) + + # If we need to wrap around to the beginning of the text + if src_x == 0 and self.scroll_position > text_width: + remaining_width = self.display_manager.matrix.width - src_width + if remaining_width > 0: + wrap_src_width = min(remaining_width, text_width) + wrap_region = text_image.crop((0, 0, wrap_src_width, self.display_manager.matrix.height)) + frame_image.paste(wrap_region, (src_width, 0)) + + # Update the display with the new frame + self.display_manager.image = frame_image + self.display_manager.draw = ImageDraw.Draw(frame_image) self.display_manager.update_display() # Update scroll position diff --git a/test_smooth_scroll.py b/test_smooth_scroll.py new file mode 100644 index 00000000..4ee1670b --- /dev/null +++ b/test_smooth_scroll.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +import time +import sys +import os +from src.config_manager import ConfigManager +from src.display_manager import DisplayManager +from src.news_manager import NewsManager + +def main(): + """Test the smooth scrolling performance of the news ticker.""" + try: + # Load configuration + config_manager = ConfigManager() + config = config_manager.config + + # Initialize display manager + display_manager = DisplayManager(config.get('display', {})) + + # Initialize news manager + news_manager = NewsManager(config, display_manager) + + print("Smooth scrolling test started. Press Ctrl+C to exit.") + print("Displaying news headlines with optimized scrolling...") + + # Clear the display first + display_manager.clear() + display_manager.update_display() + + # Display news headlines for a longer time to test scrolling performance + start_time = time.time() + frame_count = 0 + + while time.time() - start_time < 60: # Run for 1 minute + news_manager.display_news() + frame_count += 1 + + # Print FPS every 5 seconds + elapsed = time.time() - start_time + if int(elapsed) % 5 == 0 and int(elapsed) > 0: + fps = frame_count / elapsed + print(f"FPS: {fps:.2f}") + frame_count = 0 + start_time = time.time() + + print("Test completed successfully.") + + except KeyboardInterrupt: + print("\nTest interrupted by user.") + except Exception as e: + print(f"Error: {e}") + finally: + # Clean up + if 'display_manager' in locals(): + display_manager.clear() + display_manager.update_display() + display_manager.cleanup() + +if __name__ == "__main__": + main() \ No newline at end of file From 99d99900022c532e22a78d7605aa3e57f200d0fd Mon Sep 17 00:00:00 2001 From: Chuck <33324927+ChuckBuilds@users.noreply.github.com> Date: Thu, 10 Apr 2025 20:22:26 -0500 Subject: [PATCH 22/87] updating scroll direction orienting scroll direction --- src/news_manager.py | 5 ++- test_scroll_direction.py | 87 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+), 2 deletions(-) create mode 100644 test_scroll_direction.py diff --git a/src/news_manager.py b/src/news_manager.py index abe05e07..382ab790 100644 --- a/src/news_manager.py +++ b/src/news_manager.py @@ -174,7 +174,8 @@ class NewsManager: frame_image = Image.new('RGB', (self.display_manager.matrix.width, self.display_manager.matrix.height), (0, 0, 0)) # Calculate the source and destination regions for the visible portion - src_x = max(0, text_width - self.scroll_position - visible_width) + # For left-to-right scrolling, we start from the left side of the text + src_x = self.scroll_position src_width = min(visible_width, text_width - src_x) # Copy the visible portion of the text to the frame @@ -183,7 +184,7 @@ class NewsManager: frame_image.paste(src_region, (0, 0)) # If we need to wrap around to the beginning of the text - if src_x == 0 and self.scroll_position > text_width: + if src_x + src_width >= text_width: remaining_width = self.display_manager.matrix.width - src_width if remaining_width > 0: wrap_src_width = min(remaining_width, text_width) diff --git a/test_scroll_direction.py b/test_scroll_direction.py new file mode 100644 index 00000000..7fe48583 --- /dev/null +++ b/test_scroll_direction.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 +import time +import sys +import os +from src.config_manager import ConfigManager +from src.display_manager import DisplayManager +from src.news_manager import NewsManager + +def main(): + """Test the scrolling direction of the news ticker.""" + try: + # Load configuration + config_manager = ConfigManager() + config = config_manager.load_config() + + # Initialize display manager + display_manager = DisplayManager(config.get('display', {})) + + # Initialize news manager + news_manager = NewsManager(config, display_manager) + + print("Starting scroll direction test...") + print("This test will display a simple text message scrolling from left to right") + print("Press Ctrl+C to exit") + + # Create a simple test message + test_message = "TEST MESSAGE - This is a test of scrolling direction" + + # Create a text image for the test message + text_image = news_manager._create_text_image(test_message) + text_width = text_image.width + + # Clear the display + display_manager.clear() + display_manager.update_display() + + # Test scrolling from left to right + scroll_position = 0 + while True: + # Create a new frame + frame_image = display_manager.create_blank_image() + + # Calculate the visible portion + visible_width = min(display_manager.matrix.width, text_width) + src_x = scroll_position + src_width = min(visible_width, text_width - src_x) + + # Copy the visible portion + if src_width > 0: + src_region = text_image.crop((src_x, 0, src_x + src_width, display_manager.matrix.height)) + frame_image.paste(src_region, (0, 0)) + + # Handle wrapping + if src_x + src_width >= text_width: + remaining_width = display_manager.matrix.width - src_width + if remaining_width > 0: + wrap_src_width = min(remaining_width, text_width) + wrap_region = text_image.crop((0, 0, wrap_src_width, display_manager.matrix.height)) + frame_image.paste(wrap_region, (src_width, 0)) + + # Update the display + display_manager.image = frame_image + display_manager.draw = display_manager.create_draw_object() + display_manager.update_display() + + # Update scroll position + scroll_position += 1 + + # Reset when we've scrolled past the end + if scroll_position > text_width + display_manager.matrix.width: + scroll_position = 0 + time.sleep(1) # Pause briefly before restarting + + time.sleep(0.05) # Control scroll speed + + except KeyboardInterrupt: + print("\nTest interrupted by user") + except Exception as e: + print(f"Error during test: {e}") + finally: + # Clean up + display_manager.clear() + display_manager.update_display() + print("Test completed") + +if __name__ == "__main__": + main() \ No newline at end of file From 8f11ae36e4f62b07015b04a1cdb8d892cb32e25a Mon Sep 17 00:00:00 2001 From: Chuck <33324927+ChuckBuilds@users.noreply.github.com> Date: Thu, 10 Apr 2025 20:27:07 -0500 Subject: [PATCH 23/87] News tuning removed test files and increased scroll speed --- config/config.json | 2 +- test_news_ticker.py | 44 -------------------- test_scroll_direction.py | 87 ---------------------------------------- test_smooth_scroll.py | 59 --------------------------- 4 files changed, 1 insertion(+), 191 deletions(-) delete mode 100644 test_news_ticker.py delete mode 100644 test_scroll_direction.py delete mode 100644 test_smooth_scroll.py diff --git a/config/config.json b/config/config.json index b7e189ed..1a6556ca 100644 --- a/config/config.json +++ b/config/config.json @@ -54,7 +54,7 @@ "news": { "enabled": true, "update_interval": 300, - "scroll_speed": 2, + "scroll_speed": 10, "scroll_delay": 0.03, "max_headlines_per_symbol": 1 } diff --git a/test_news_ticker.py b/test_news_ticker.py deleted file mode 100644 index c4d9f2dc..00000000 --- a/test_news_ticker.py +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/env python3 -import time -import sys -import os -from src.config_manager import ConfigManager -from src.display_manager import DisplayManager -from src.news_manager import NewsManager - -def main(): - """Test the news ticker functionality.""" - try: - # Load configuration - config_manager = ConfigManager() - config = config_manager.config - - # Initialize display manager - display_manager = DisplayManager(config.get('display', {})) - - # Initialize news manager - news_manager = NewsManager(config, display_manager) - - print("News ticker test started. Press Ctrl+C to exit.") - print("Displaying news headlines for configured stock symbols...") - - # Display news headlines for a limited time (30 seconds) - start_time = time.time() - while time.time() - start_time < 30: - news_manager.display_news() - - print("Test completed successfully.") - - except KeyboardInterrupt: - print("\nTest interrupted by user.") - except Exception as e: - print(f"Error: {e}") - finally: - # Clean up - if 'display_manager' in locals(): - display_manager.clear() - display_manager.update_display() - display_manager.cleanup() - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/test_scroll_direction.py b/test_scroll_direction.py deleted file mode 100644 index 7fe48583..00000000 --- a/test_scroll_direction.py +++ /dev/null @@ -1,87 +0,0 @@ -#!/usr/bin/env python3 -import time -import sys -import os -from src.config_manager import ConfigManager -from src.display_manager import DisplayManager -from src.news_manager import NewsManager - -def main(): - """Test the scrolling direction of the news ticker.""" - try: - # Load configuration - config_manager = ConfigManager() - config = config_manager.load_config() - - # Initialize display manager - display_manager = DisplayManager(config.get('display', {})) - - # Initialize news manager - news_manager = NewsManager(config, display_manager) - - print("Starting scroll direction test...") - print("This test will display a simple text message scrolling from left to right") - print("Press Ctrl+C to exit") - - # Create a simple test message - test_message = "TEST MESSAGE - This is a test of scrolling direction" - - # Create a text image for the test message - text_image = news_manager._create_text_image(test_message) - text_width = text_image.width - - # Clear the display - display_manager.clear() - display_manager.update_display() - - # Test scrolling from left to right - scroll_position = 0 - while True: - # Create a new frame - frame_image = display_manager.create_blank_image() - - # Calculate the visible portion - visible_width = min(display_manager.matrix.width, text_width) - src_x = scroll_position - src_width = min(visible_width, text_width - src_x) - - # Copy the visible portion - if src_width > 0: - src_region = text_image.crop((src_x, 0, src_x + src_width, display_manager.matrix.height)) - frame_image.paste(src_region, (0, 0)) - - # Handle wrapping - if src_x + src_width >= text_width: - remaining_width = display_manager.matrix.width - src_width - if remaining_width > 0: - wrap_src_width = min(remaining_width, text_width) - wrap_region = text_image.crop((0, 0, wrap_src_width, display_manager.matrix.height)) - frame_image.paste(wrap_region, (src_width, 0)) - - # Update the display - display_manager.image = frame_image - display_manager.draw = display_manager.create_draw_object() - display_manager.update_display() - - # Update scroll position - scroll_position += 1 - - # Reset when we've scrolled past the end - if scroll_position > text_width + display_manager.matrix.width: - scroll_position = 0 - time.sleep(1) # Pause briefly before restarting - - time.sleep(0.05) # Control scroll speed - - except KeyboardInterrupt: - print("\nTest interrupted by user") - except Exception as e: - print(f"Error during test: {e}") - finally: - # Clean up - display_manager.clear() - display_manager.update_display() - print("Test completed") - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/test_smooth_scroll.py b/test_smooth_scroll.py deleted file mode 100644 index 4ee1670b..00000000 --- a/test_smooth_scroll.py +++ /dev/null @@ -1,59 +0,0 @@ -#!/usr/bin/env python3 -import time -import sys -import os -from src.config_manager import ConfigManager -from src.display_manager import DisplayManager -from src.news_manager import NewsManager - -def main(): - """Test the smooth scrolling performance of the news ticker.""" - try: - # Load configuration - config_manager = ConfigManager() - config = config_manager.config - - # Initialize display manager - display_manager = DisplayManager(config.get('display', {})) - - # Initialize news manager - news_manager = NewsManager(config, display_manager) - - print("Smooth scrolling test started. Press Ctrl+C to exit.") - print("Displaying news headlines with optimized scrolling...") - - # Clear the display first - display_manager.clear() - display_manager.update_display() - - # Display news headlines for a longer time to test scrolling performance - start_time = time.time() - frame_count = 0 - - while time.time() - start_time < 60: # Run for 1 minute - news_manager.display_news() - frame_count += 1 - - # Print FPS every 5 seconds - elapsed = time.time() - start_time - if int(elapsed) % 5 == 0 and int(elapsed) > 0: - fps = frame_count / elapsed - print(f"FPS: {fps:.2f}") - frame_count = 0 - start_time = time.time() - - print("Test completed successfully.") - - except KeyboardInterrupt: - print("\nTest interrupted by user.") - except Exception as e: - print(f"Error: {e}") - finally: - # Clean up - if 'display_manager' in locals(): - display_manager.clear() - display_manager.update_display() - display_manager.cleanup() - -if __name__ == "__main__": - main() \ No newline at end of file From 8a71971d99d95e1c2b42568488d45bbbf2195487 Mon Sep 17 00:00:00 2001 From: Chuck <33324927+ChuckBuilds@users.noreply.github.com> Date: Thu, 10 Apr 2025 20:28:04 -0500 Subject: [PATCH 24/87] Create test_news_manager.py need a test script to call upon --- test_news_manager.py | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 test_news_manager.py diff --git a/test_news_manager.py b/test_news_manager.py new file mode 100644 index 00000000..e70d8913 --- /dev/null +++ b/test_news_manager.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 +import time +import sys +import os +from src.config_manager import ConfigManager +from src.display_manager import DisplayManager +from src.news_manager import NewsManager + +def main(): + """Test the NewsManager class directly.""" + try: + # Load configuration + config_manager = ConfigManager() + config = config_manager.load_config() + + # Initialize display manager + display_manager = DisplayManager(config.get('display', {})) + + # Initialize news manager + news_manager = NewsManager(config, display_manager) + + # Test the scrolling behavior + # You can customize these parameters: + # - test_message: The message to scroll + # - scroll_speed: Pixels to move per frame (higher = faster) + # - scroll_delay: Delay between scroll updates (lower = faster) + # - max_iterations: Maximum number of iterations to run (None = run indefinitely) + news_manager.test_scroll( + test_message="This is a test of the NewsManager scrolling behavior. You can adjust the speed and delay to find the optimal settings.", + scroll_speed=2, # Adjust this to change scroll speed + scroll_delay=0.05, # Adjust this to change scroll smoothness + max_iterations=3 # Set to None to run indefinitely + ) + + except KeyboardInterrupt: + print("\nTest interrupted by user") + except Exception as e: + print(f"Error during test: {e}") + finally: + print("Test completed") + +if __name__ == "__main__": + main() \ No newline at end of file From 55db83dd375a867eb64457fd9c8254078f5f6fe1 Mon Sep 17 00:00:00 2001 From: Chuck <33324927+ChuckBuilds@users.noreply.github.com> Date: Thu, 10 Apr 2025 20:30:38 -0500 Subject: [PATCH 25/87] Update test_news_manager.py test script tuning --- test_news_manager.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test_news_manager.py b/test_news_manager.py index e70d8913..8ccd3453 100644 --- a/test_news_manager.py +++ b/test_news_manager.py @@ -6,6 +6,8 @@ from src.config_manager import ConfigManager from src.display_manager import DisplayManager from src.news_manager import NewsManager +print(f"Current working directory: {os.getcwd()}") + def main(): """Test the NewsManager class directly.""" try: From 83d5726513fa569e29eaca5244fd858beeaa869c Mon Sep 17 00:00:00 2001 From: Chuck <33324927+ChuckBuilds@users.noreply.github.com> Date: Thu, 10 Apr 2025 20:32:26 -0500 Subject: [PATCH 26/87] troubleshooting test script --- src/config_manager.py | 7 ++++--- test_news_manager.py | 17 ++++++++++++++--- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/config_manager.py b/src/config_manager.py index b6a9d7de..4173d235 100644 --- a/src/config_manager.py +++ b/src/config_manager.py @@ -7,11 +7,9 @@ class ConfigManager: # Use current working directory as base self.config_path = config_path or "config/config.json" self.secrets_path = secrets_path or "config/config_secrets.json" - self.config: Dict[str, Any] = {} - self.load_config() - def load_config(self) -> None: + def load_config(self) -> Dict[str, Any]: """Load configuration from JSON files.""" try: # Load main config @@ -26,10 +24,13 @@ class ConfigManager: # Deep merge secrets into config self._deep_merge(self.config, secrets) + return self.config + except FileNotFoundError as e: if str(e).find('config_secrets.json') == -1: # Only raise if main config is missing print(f"Configuration file not found at {os.path.abspath(self.config_path)}") raise + return self.config except json.JSONDecodeError: print("Error parsing configuration file") raise diff --git a/test_news_manager.py b/test_news_manager.py index 8ccd3453..9bf93814 100644 --- a/test_news_manager.py +++ b/test_news_manager.py @@ -15,10 +15,19 @@ def main(): config_manager = ConfigManager() config = config_manager.load_config() - # Initialize display manager - display_manager = DisplayManager(config.get('display', {})) + if not config: + print("Error: Failed to load configuration") + return + + display_config = config.get('display') + if not display_config: + print("Error: No display configuration found") + return - # Initialize news manager + # Initialize display manager + display_manager = DisplayManager(display_config) + + # Initialize news manager with the loaded config news_manager = NewsManager(config, display_manager) # Test the scrolling behavior @@ -38,6 +47,8 @@ def main(): print("\nTest interrupted by user") except Exception as e: print(f"Error during test: {e}") + import traceback + traceback.print_exc() finally: print("Test completed") From da4615e39cf76b9f6977151892bc9c727b9de689 Mon Sep 17 00:00:00 2001 From: Chuck <33324927+ChuckBuilds@users.noreply.github.com> Date: Thu, 10 Apr 2025 20:34:02 -0500 Subject: [PATCH 27/87] Update test_news_manager.py --- test_news_manager.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/test_news_manager.py b/test_news_manager.py index 9bf93814..4006c2f6 100644 --- a/test_news_manager.py +++ b/test_news_manager.py @@ -30,18 +30,12 @@ def main(): # Initialize news manager with the loaded config news_manager = NewsManager(config, display_manager) - # Test the scrolling behavior - # You can customize these parameters: - # - test_message: The message to scroll - # - scroll_speed: Pixels to move per frame (higher = faster) - # - scroll_delay: Delay between scroll updates (lower = faster) - # - max_iterations: Maximum number of iterations to run (None = run indefinitely) - news_manager.test_scroll( - test_message="This is a test of the NewsManager scrolling behavior. You can adjust the speed and delay to find the optimal settings.", - scroll_speed=2, # Adjust this to change scroll speed - scroll_delay=0.05, # Adjust this to change scroll smoothness - max_iterations=3 # Set to None to run indefinitely - ) + print("Testing news display. Press Ctrl+C to exit.") + + # Run the news display in a loop + while True: + news_manager.display_news() + time.sleep(0.05) # Small delay between updates except KeyboardInterrupt: print("\nTest interrupted by user") From 2128a2fb5fa70962420388adb985e5b805ceb25a Mon Sep 17 00:00:00 2001 From: Chuck <33324927+ChuckBuilds@users.noreply.github.com> Date: Thu, 10 Apr 2025 20:35:18 -0500 Subject: [PATCH 28/87] Update config.json scroll speed increases --- config/config.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/config.json b/config/config.json index 1a6556ca..11d84df0 100644 --- a/config/config.json +++ b/config/config.json @@ -54,8 +54,8 @@ "news": { "enabled": true, "update_interval": 300, - "scroll_speed": 10, - "scroll_delay": 0.03, + "scroll_speed": 20, + "scroll_delay": 0.01, "max_headlines_per_symbol": 1 } } \ No newline at end of file From fd9006c46ec844faebc0f26068f79a5bbce0c7f1 Mon Sep 17 00:00:00 2001 From: Chuck <33324927+ChuckBuilds@users.noreply.github.com> Date: Thu, 10 Apr 2025 20:37:00 -0500 Subject: [PATCH 29/87] Update config.json scroll tuning --- config/config.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/config.json b/config/config.json index 11d84df0..03248490 100644 --- a/config/config.json +++ b/config/config.json @@ -54,8 +54,8 @@ "news": { "enabled": true, "update_interval": 300, - "scroll_speed": 20, - "scroll_delay": 0.01, + "scroll_speed": 1, + "scroll_delay": 0.02, "max_headlines_per_symbol": 1 } } \ No newline at end of file From 56594f91a3983a5e4a9def460bf673e6d39a2b63 Mon Sep 17 00:00:00 2001 From: Chuck <33324927+ChuckBuilds@users.noreply.github.com> Date: Thu, 10 Apr 2025 20:38:09 -0500 Subject: [PATCH 30/87] Update config.json speeding up --- config/config.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/config.json b/config/config.json index 03248490..ba0f6501 100644 --- a/config/config.json +++ b/config/config.json @@ -54,8 +54,8 @@ "news": { "enabled": true, "update_interval": 300, - "scroll_speed": 1, - "scroll_delay": 0.02, + "scroll_speed": 2, + "scroll_delay": 0.01, "max_headlines_per_symbol": 1 } } \ No newline at end of file From 8ec1c70a4d3cd6b31fd92c3de85044ef97ed9de2 Mon Sep 17 00:00:00 2001 From: Chuck <33324927+ChuckBuilds@users.noreply.github.com> Date: Thu, 10 Apr 2025 20:40:26 -0500 Subject: [PATCH 31/87] Update config.json still making text faster --- config/config.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/config.json b/config/config.json index ba0f6501..fd6be20c 100644 --- a/config/config.json +++ b/config/config.json @@ -54,8 +54,8 @@ "news": { "enabled": true, "update_interval": 300, - "scroll_speed": 2, - "scroll_delay": 0.01, + "scroll_speed": 6, + "scroll_delay": 0.015, "max_headlines_per_symbol": 1 } } \ No newline at end of file From 53689dc2abfe0f82727b0198ec71869450967a01 Mon Sep 17 00:00:00 2001 From: Chuck <33324927+ChuckBuilds@users.noreply.github.com> Date: Thu, 10 Apr 2025 20:45:04 -0500 Subject: [PATCH 32/87] Update config.json Trying to tune scrolling --- config/config.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/config.json b/config/config.json index fd6be20c..a3aa11ed 100644 --- a/config/config.json +++ b/config/config.json @@ -54,8 +54,8 @@ "news": { "enabled": true, "update_interval": 300, - "scroll_speed": 6, - "scroll_delay": 0.015, + "scroll_speed": 1, + "scroll_delay": 0.002, "max_headlines_per_symbol": 1 } } \ No newline at end of file From be3c5da3f78f9726449718d2c26074fda24581c7 Mon Sep 17 00:00:00 2001 From: Chuck <33324927+ChuckBuilds@users.noreply.github.com> Date: Thu, 10 Apr 2025 20:46:29 -0500 Subject: [PATCH 33/87] Update config.json testing crazy parameters --- config/config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/config.json b/config/config.json index a3aa11ed..9ecb8087 100644 --- a/config/config.json +++ b/config/config.json @@ -55,7 +55,7 @@ "enabled": true, "update_interval": 300, "scroll_speed": 1, - "scroll_delay": 0.002, + "scroll_delay": 0.0005, "max_headlines_per_symbol": 1 } } \ No newline at end of file From 80ac45cd7309c9d850abcb40a673acf9302d8711 Mon Sep 17 00:00:00 2001 From: Chuck <33324927+ChuckBuilds@users.noreply.github.com> Date: Thu, 10 Apr 2025 20:47:50 -0500 Subject: [PATCH 34/87] Update test_news_manager.py remove sleep delay --- test_news_manager.py | 1 - 1 file changed, 1 deletion(-) diff --git a/test_news_manager.py b/test_news_manager.py index 4006c2f6..310a4ff9 100644 --- a/test_news_manager.py +++ b/test_news_manager.py @@ -35,7 +35,6 @@ def main(): # Run the news display in a loop while True: news_manager.display_news() - time.sleep(0.05) # Small delay between updates except KeyboardInterrupt: print("\nTest interrupted by user") From f3fd77c4c335c349c91b362ab1867e7aaaf943e7 Mon Sep 17 00:00:00 2001 From: Chuck <33324927+ChuckBuilds@users.noreply.github.com> Date: Thu, 10 Apr 2025 20:49:32 -0500 Subject: [PATCH 35/87] scroll tuning scroll tuning --- config/config.json | 2 +- src/news_manager.py | 24 ++++++++++-------------- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/config/config.json b/config/config.json index 9ecb8087..7c0a2656 100644 --- a/config/config.json +++ b/config/config.json @@ -55,7 +55,7 @@ "enabled": true, "update_interval": 300, "scroll_speed": 1, - "scroll_delay": 0.0005, + "scroll_delay": 0.001, "max_headlines_per_symbol": 1 } } \ No newline at end of file diff --git a/src/news_manager.py b/src/news_manager.py index 382ab790..7fdad5fc 100644 --- a/src/news_manager.py +++ b/src/news_manager.py @@ -154,9 +154,12 @@ class NewsManager: # Get the current news item to display current_news = all_news[self.current_news_index] + next_news = all_news[(self.current_news_index + 1) % len(all_news)] - # Format the news text - news_text = f"{current_news['symbol']}: {current_news['title']}" + # Format the news text with spacing between items + current_text = f"{current_news['symbol']}: {current_news['title']}" + next_text = f"{next_news['symbol']}: {next_news['title']}" + news_text = f"{current_text} {next_text}" # Create a text image for efficient scrolling text_image = self._create_text_image(news_text) @@ -165,17 +168,11 @@ class NewsManager: # Calculate the visible portion of the text visible_width = min(self.display_manager.matrix.width, text_width) - # If this is the first time displaying this news item, clear the screen - if self.scroll_position == 0: - self.display_manager.clear() - self.display_manager.update_display() - # Create a new image for the current frame frame_image = Image.new('RGB', (self.display_manager.matrix.width, self.display_manager.matrix.height), (0, 0, 0)) # Calculate the source and destination regions for the visible portion - # For left-to-right scrolling, we start from the left side of the text - src_x = self.scroll_position + src_x = self.scroll_position % text_width # Use modulo to wrap around smoothly src_width = min(visible_width, text_width - src_x) # Copy the visible portion of the text to the frame @@ -199,13 +196,12 @@ class NewsManager: # Update scroll position self.scroll_position += self.scroll_speed - # If we've scrolled past the end of the text, move to the next news item - if self.scroll_position > text_width + self.display_manager.matrix.width: + # If we've scrolled past the current text, move to the next news item + if self.scroll_position >= text_width: self.scroll_position = 0 self.current_news_index = (self.current_news_index + 1) % len(all_news) - + # Add a small delay to control scroll speed time.sleep(self.scroll_delay) - # Return True if we've displayed all news items - return self.current_news_index == 0 and self.scroll_position == 0 \ No newline at end of file + return True \ No newline at end of file From 2a127b14716ac752f763e7905fbcbe045cec520e Mon Sep 17 00:00:00 2001 From: Chuck <33324927+ChuckBuilds@users.noreply.github.com> Date: Thu, 10 Apr 2025 20:51:52 -0500 Subject: [PATCH 36/87] scroll logging and debugging FPS counter and debug messages --- config/config.json | 2 +- src/news_manager.py | 62 +++++++++++++++++++++++++++++++++++++++------ 2 files changed, 55 insertions(+), 9 deletions(-) diff --git a/config/config.json b/config/config.json index 7c0a2656..705e4fc0 100644 --- a/config/config.json +++ b/config/config.json @@ -55,7 +55,7 @@ "enabled": true, "update_interval": 300, "scroll_speed": 1, - "scroll_delay": 0.001, + "scroll_delay": 0.0001, "max_headlines_per_symbol": 1 } } \ No newline at end of file diff --git a/src/news_manager.py b/src/news_manager.py index 7fdad5fc..27932026 100644 --- a/src/news_manager.py +++ b/src/news_manager.py @@ -26,8 +26,20 @@ class NewsManager: self.news_data = {} self.current_news_index = 0 self.scroll_position = 0 - self.scroll_speed = self.news_config.get('scroll_speed', 1) # Pixels to move per frame - self.scroll_delay = self.news_config.get('scroll_delay', 0.05) # Delay between scroll updates + + # Get scroll settings from config with faster defaults + self.scroll_speed = self.news_config.get('scroll_speed', 1) + self.scroll_delay = self.news_config.get('scroll_delay', 0.001) # Default to 1ms instead of 50ms + + # Log the actual values being used + logger.info(f"Scroll settings - Speed: {self.scroll_speed} pixels/frame, Delay: {self.scroll_delay*1000:.2f}ms") + + # Initialize frame rate tracking + self.frame_count = 0 + self.last_frame_time = time.time() + self.last_fps_log_time = time.time() + self.frame_times = [] # Keep track of recent frame times for average FPS + self.headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' } @@ -126,6 +138,31 @@ class NewsManager: return text_image + def _log_frame_rate(self): + """Log frame rate statistics.""" + current_time = time.time() + + # Calculate instantaneous frame time + frame_time = current_time - self.last_frame_time + self.frame_times.append(frame_time) + + # Keep only last 100 frames for average + if len(self.frame_times) > 100: + self.frame_times.pop(0) + + # Log FPS every second + if current_time - self.last_fps_log_time >= 1.0: + avg_frame_time = sum(self.frame_times) / len(self.frame_times) + avg_fps = 1.0 / avg_frame_time if avg_frame_time > 0 else 0 + instant_fps = 1.0 / frame_time if frame_time > 0 else 0 + + logger.info(f"Frame stats - Avg FPS: {avg_fps:.1f}, Current FPS: {instant_fps:.1f}, Frame time: {frame_time*1000:.2f}ms") + self.last_fps_log_time = current_time + self.frame_count = 0 + + self.last_frame_time = current_time + self.frame_count += 1 + def display_news(self): """Display news headlines by scrolling them across the screen.""" if not self.news_config.get('enabled', False): @@ -161,9 +198,14 @@ class NewsManager: next_text = f"{next_news['symbol']}: {next_news['title']}" news_text = f"{current_text} {next_text}" - # Create a text image for efficient scrolling - text_image = self._create_text_image(news_text) - text_width = text_image.width + # Create a text image for efficient scrolling (only if needed) + if not hasattr(self, '_current_text_image') or self._current_text != news_text: + self._current_text_image = self._create_text_image(news_text) + self._current_text = news_text + text_width = self._current_text_image.width + self._text_width = text_width + else: + text_width = self._text_width # Calculate the visible portion of the text visible_width = min(self.display_manager.matrix.width, text_width) @@ -177,7 +219,7 @@ class NewsManager: # Copy the visible portion of the text to the frame if src_width > 0: - src_region = text_image.crop((src_x, 0, src_x + src_width, self.display_manager.matrix.height)) + src_region = self._current_text_image.crop((src_x, 0, src_x + src_width, self.display_manager.matrix.height)) frame_image.paste(src_region, (0, 0)) # If we need to wrap around to the beginning of the text @@ -185,7 +227,7 @@ class NewsManager: remaining_width = self.display_manager.matrix.width - src_width if remaining_width > 0: wrap_src_width = min(remaining_width, text_width) - wrap_region = text_image.crop((0, 0, wrap_src_width, self.display_manager.matrix.height)) + wrap_region = self._current_text_image.crop((0, 0, wrap_src_width, self.display_manager.matrix.height)) frame_image.paste(wrap_region, (src_width, 0)) # Update the display with the new frame @@ -193,6 +235,9 @@ class NewsManager: self.display_manager.draw = ImageDraw.Draw(frame_image) self.display_manager.update_display() + # Log frame rate + self._log_frame_rate() + # Update scroll position self.scroll_position += self.scroll_speed @@ -202,6 +247,7 @@ class NewsManager: self.current_news_index = (self.current_news_index + 1) % len(all_news) # Add a small delay to control scroll speed - time.sleep(self.scroll_delay) + if self.scroll_delay > 0: + time.sleep(self.scroll_delay) return True \ No newline at end of file From e14f7ddfb8ee383739f0fd7756c2a26c6986187b Mon Sep 17 00:00:00 2001 From: Chuck <33324927+ChuckBuilds@users.noreply.github.com> Date: Thu, 10 Apr 2025 20:56:37 -0500 Subject: [PATCH 37/87] Update config.json matrix speed tuning --- config/config.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/config.json b/config/config.json index 705e4fc0..b8f5cc7b 100644 --- a/config/config.json +++ b/config/config.json @@ -16,8 +16,8 @@ "scan_mode": "progressive", "pwm_bits": 8, "pwm_dither_bits": 1, - "pwm_lsb_nanoseconds": 130, - "disable_hardware_pulsing": true, + "pwm_lsb_nanoseconds": 50, + "disable_hardware_pulsing": false, "inverse_colors": false, "show_refresh_rate": true, "limit_refresh_rate_hz": 100 From ba65a2c5cfd577c0fa217ffa1f16977e4b771ff2 Mon Sep 17 00:00:00 2001 From: Chuck <33324927+ChuckBuilds@users.noreply.github.com> Date: Thu, 10 Apr 2025 20:58:31 -0500 Subject: [PATCH 38/87] Update news_manager.py News separator --- src/news_manager.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/news_manager.py b/src/news_manager.py index 27932026..9a34cfc1 100644 --- a/src/news_manager.py +++ b/src/news_manager.py @@ -193,10 +193,11 @@ class NewsManager: current_news = all_news[self.current_news_index] next_news = all_news[(self.current_news_index + 1) % len(all_news)] - # Format the news text with spacing between items + # Format the news text with proper spacing and separator + separator = " ● " # Visual separator between news items current_text = f"{current_news['symbol']}: {current_news['title']}" next_text = f"{next_news['symbol']}: {next_news['title']}" - news_text = f"{current_text} {next_text}" + news_text = f"{current_text}{separator}{next_text}" # Create a text image for efficient scrolling (only if needed) if not hasattr(self, '_current_text_image') or self._current_text != news_text: From 6091c71944ae88f0179966197d509fcddfa6e24c Mon Sep 17 00:00:00 2001 From: Chuck <33324927+ChuckBuilds@users.noreply.github.com> Date: Thu, 10 Apr 2025 20:59:47 -0500 Subject: [PATCH 39/87] Update news_manager.py separator character change --- src/news_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/news_manager.py b/src/news_manager.py index 9a34cfc1..115386fe 100644 --- a/src/news_manager.py +++ b/src/news_manager.py @@ -194,7 +194,7 @@ class NewsManager: next_news = all_news[(self.current_news_index + 1) % len(all_news)] # Format the news text with proper spacing and separator - separator = " ● " # Visual separator between news items + separator = " - " # Visual separator between news items current_text = f"{current_news['symbol']}: {current_news['title']}" next_text = f"{next_news['symbol']}: {next_news['title']}" news_text = f"{current_text}{separator}{next_text}" From beeeda55508ea7da62ca550bfdee96b11cf8a06b Mon Sep 17 00:00:00 2001 From: Chuck <33324927+ChuckBuilds@users.noreply.github.com> Date: Thu, 10 Apr 2025 21:04:23 -0500 Subject: [PATCH 40/87] Stock News manager Rename rename stock news ticker to enable other news in the future --- integrate_news_ticker.py | 4 ++-- src/{news_manager.py => stock_news_manager.py} | 2 +- test_news_manager.py => test_stock_news_manager.py | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) rename src/{news_manager.py => stock_news_manager.py} (99%) rename test_news_manager.py => test_stock_news_manager.py (88%) diff --git a/integrate_news_ticker.py b/integrate_news_ticker.py index 88788b22..b2cf9c93 100644 --- a/integrate_news_ticker.py +++ b/integrate_news_ticker.py @@ -4,7 +4,7 @@ import sys import os from src.config_manager import ConfigManager from src.display_manager import DisplayManager -from src.news_manager import NewsManager +from src.stock_news_manager import StockNewsManager from src.stock_manager import StockManager def main(): @@ -21,7 +21,7 @@ def main(): stock_manager = StockManager(config, display_manager) # Initialize news manager - news_manager = NewsManager(config, display_manager) + news_manager = StockNewsManager(config, display_manager) print("News ticker integration test started. Press Ctrl+C to exit.") print("Displaying stock data and news headlines...") diff --git a/src/news_manager.py b/src/stock_news_manager.py similarity index 99% rename from src/news_manager.py rename to src/stock_news_manager.py index 115386fe..1bdef3ed 100644 --- a/src/news_manager.py +++ b/src/stock_news_manager.py @@ -15,7 +15,7 @@ from PIL import Image, ImageDraw logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) -class NewsManager: +class StockNewsManager: def __init__(self, config: Dict[str, Any], display_manager): self.config = config self.config_manager = ConfigManager() diff --git a/test_news_manager.py b/test_stock_news_manager.py similarity index 88% rename from test_news_manager.py rename to test_stock_news_manager.py index 310a4ff9..9172d974 100644 --- a/test_news_manager.py +++ b/test_stock_news_manager.py @@ -4,12 +4,12 @@ import sys import os from src.config_manager import ConfigManager from src.display_manager import DisplayManager -from src.news_manager import NewsManager +from src.stock_news_manager import StockNewsManager print(f"Current working directory: {os.getcwd()}") def main(): - """Test the NewsManager class directly.""" + """Test the StockNewsManager class directly.""" try: # Load configuration config_manager = ConfigManager() @@ -28,7 +28,7 @@ def main(): display_manager = DisplayManager(display_config) # Initialize news manager with the loaded config - news_manager = NewsManager(config, display_manager) + news_manager = StockNewsManager(config, display_manager) print("Testing news display. Press Ctrl+C to exit.") From 013b5e2200404c64c1e160f8bdb16da7c89d706c Mon Sep 17 00:00:00 2001 From: Chuck <33324927+ChuckBuilds@users.noreply.github.com> Date: Thu, 10 Apr 2025 21:08:06 -0500 Subject: [PATCH 41/87] Update display_controller.py load config update --- src/display_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/display_controller.py b/src/display_controller.py index 41701685..aaf0743f 100644 --- a/src/display_controller.py +++ b/src/display_controller.py @@ -14,7 +14,7 @@ logger = logging.getLogger(__name__) class DisplayController: def __init__(self): self.config_manager = ConfigManager() - self.config = self.config_manager.config + self.config = self.config_manager.load_config() self.display_manager = DisplayManager(self.config.get('display', {})) self.clock = Clock(display_manager=self.display_manager) self.weather = WeatherManager(self.config, self.display_manager) From 1d2bef0b2fc9975690749c3a06eaf3e70c455fe6 Mon Sep 17 00:00:00 2001 From: Chuck <33324927+ChuckBuilds@users.noreply.github.com> Date: Thu, 10 Apr 2025 21:09:13 -0500 Subject: [PATCH 42/87] Update stock_manager.py remove redundant import --- src/stock_manager.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/stock_manager.py b/src/stock_manager.py index d3e052dc..40630fed 100644 --- a/src/stock_manager.py +++ b/src/stock_manager.py @@ -8,7 +8,6 @@ from datetime import datetime import os import urllib.parse import re -from src.config_manager import ConfigManager # Configure logging logging.basicConfig(level=logging.INFO) @@ -17,7 +16,6 @@ logger = logging.getLogger(__name__) class StockManager: def __init__(self, config: Dict[str, Any], display_manager): self.config = config - self.config_manager = ConfigManager() # Add config manager self.display_manager = display_manager self.stocks_config = config.get('stocks', {}) self.last_update = 0 @@ -225,8 +223,6 @@ class StockManager: def _reload_config(self): """Reload configuration from file.""" - self.config = self.config_manager.config - self.stocks_config = self.config.get('stocks', {}) # Reset stock data if symbols have changed new_symbols = set(self.stocks_config.get('symbols', [])) current_symbols = set(self.stock_data.keys()) From 5be0d59d7e7b9df0d6f52055f609b13cb9d6363a Mon Sep 17 00:00:00 2001 From: Chuck <33324927+ChuckBuilds@users.noreply.github.com> Date: Thu, 10 Apr 2025 21:18:51 -0500 Subject: [PATCH 43/87] Stock news settings Stock news has more granular control --- config/config.json | 7 +-- src/display_controller.py | 18 +++++-- src/stock_news_manager.py | 104 +++++++++++++++++--------------------- 3 files changed, 63 insertions(+), 66 deletions(-) diff --git a/config/config.json b/config/config.json index b8f5cc7b..a3e5e318 100644 --- a/config/config.json +++ b/config/config.json @@ -31,7 +31,7 @@ "stocks": 45, "hourly_forecast": 15, "daily_forecast": 15, - "news": 30 + "stock_news": 30 } }, "clock": { @@ -51,11 +51,12 @@ ], "display_format": "{symbol}: ${price} ({change}%)" }, - "news": { + "stock_news": { "enabled": true, "update_interval": 300, "scroll_speed": 1, "scroll_delay": 0.0001, - "max_headlines_per_symbol": 1 + "max_headlines_per_symbol": 1, + "headlines_per_rotation": 2 } } \ No newline at end of file diff --git a/src/display_controller.py b/src/display_controller.py index aaf0743f..0e893050 100644 --- a/src/display_controller.py +++ b/src/display_controller.py @@ -6,6 +6,7 @@ from src.weather_manager import WeatherManager from src.display_manager import DisplayManager from src.config_manager import ConfigManager from src.stock_manager import StockManager +from src.stock_news_manager import StockNewsManager # Configure logging logging.basicConfig(level=logging.INFO) @@ -19,6 +20,7 @@ class DisplayController: self.clock = Clock(display_manager=self.display_manager) self.weather = WeatherManager(self.config, self.display_manager) self.stocks = StockManager(self.config, self.display_manager) + self.news = StockNewsManager(self.config, self.display_manager) self.current_display = 'clock' self.weather_mode = 'current' # current, hourly, or daily self.last_switch = time.time() @@ -29,7 +31,8 @@ class DisplayController: 'weather': 15, 'stocks': 45, 'hourly_forecast': 15, - 'daily_forecast': 15 + 'daily_forecast': 15, + 'stock_news': 30 }) logger.info("DisplayController initialized with display_manager: %s", id(self.display_manager)) @@ -51,7 +54,7 @@ class DisplayController: # Check if we need to switch display mode if current_time - self.last_switch > self.get_current_duration(): - # Cycle through: clock -> weather (current) -> weather (hourly) -> weather (daily) -> stocks + # Cycle through: clock -> weather (current) -> weather (hourly) -> weather (daily) -> stocks -> stock_news if self.current_display == 'clock': self.current_display = 'weather' self.weather_mode = 'current' @@ -65,7 +68,12 @@ class DisplayController: self.current_display = 'stocks' else: self.current_display = 'clock' - else: # stocks + elif self.current_display == 'stocks': + if self.config.get('stock_news', {}).get('enabled', False): + self.current_display = 'stock_news' + else: + self.current_display = 'clock' + else: # stock_news self.current_display = 'clock' logger.info(f"Switching display to: {self.current_display} {self.weather_mode if self.current_display == 'weather' else ''}") @@ -84,8 +92,10 @@ class DisplayController: self.weather.display_hourly_forecast(force_clear=self.force_clear) else: # daily self.weather.display_daily_forecast(force_clear=self.force_clear) - else: # stocks + elif self.current_display == 'stocks': self.stocks.display_stocks(force_clear=self.force_clear) + else: # stock_news + self.news.display_news() except Exception as e: logger.error(f"Error updating display: {e}") time.sleep(1) # Wait a bit before retrying diff --git a/src/stock_news_manager.py b/src/stock_news_manager.py index 1bdef3ed..0ae86c50 100644 --- a/src/stock_news_manager.py +++ b/src/stock_news_manager.py @@ -21,15 +21,15 @@ class StockNewsManager: self.config_manager = ConfigManager() self.display_manager = display_manager self.stocks_config = config.get('stocks', {}) - self.news_config = config.get('news', {}) + self.stock_news_config = config.get('stock_news', {}) self.last_update = 0 self.news_data = {} - self.current_news_index = 0 + self.current_news_group = 0 # Track which group of headlines we're showing self.scroll_position = 0 # Get scroll settings from config with faster defaults - self.scroll_speed = self.news_config.get('scroll_speed', 1) - self.scroll_delay = self.news_config.get('scroll_delay', 0.001) # Default to 1ms instead of 50ms + self.scroll_speed = self.stock_news_config.get('scroll_speed', 1) + self.scroll_delay = self.stock_news_config.get('scroll_delay', 0.001) # Default to 1ms instead of 50ms # Log the actual values being used logger.info(f"Scroll settings - Speed: {self.scroll_speed} pixels/frame, Delay: {self.scroll_delay*1000:.2f}ms") @@ -51,7 +51,7 @@ class StockNewsManager: try: # Using Yahoo Finance API to get news encoded_symbol = urllib.parse.quote(symbol) - url = f"https://query1.finance.yahoo.com/v1/finance/search?q={encoded_symbol}&lang=en-US®ion=US"esCount=0&newsCount={self.news_config.get('max_headlines_per_symbol', 5)}" + url = f"https://query1.finance.yahoo.com/v1/finance/search?q={encoded_symbol}&lang=en-US®ion=US"esCount=0&newsCount={self.stock_news_config.get('max_headlines_per_symbol', 5)}" response = requests.get(url, headers=self.headers, timeout=5) if response.status_code != 200: @@ -87,7 +87,7 @@ class StockNewsManager: def update_news_data(self): """Update news data for all configured stock symbols.""" current_time = time.time() - update_interval = self.news_config.get('update_interval', 300) # Default to 5 minutes + update_interval = self.stock_news_config.get('update_interval', 300) # Default to 5 minutes # If not enough time has passed, keep using existing data if current_time - self.last_update < update_interval: @@ -115,8 +115,6 @@ class StockNewsManager: # Only update the displayed data when we have new data self.news_data = new_data self.last_update = current_time - self.current_news_index = 0 - self.scroll_position = 0 logger.info(f"Updated news data for {len(new_data)} symbols") else: logger.error("Failed to fetch news for any configured stocks") @@ -165,11 +163,11 @@ class StockNewsManager: def display_news(self): """Display news headlines by scrolling them across the screen.""" - if not self.news_config.get('enabled', False): + if not self.stock_news_config.get('enabled', False): return # Start update in background if needed - if time.time() - self.last_update >= self.news_config.get('update_interval', 300): + if time.time() - self.last_update >= self.stock_news_config.get('update_interval', 300): self.update_news_data() if not self.news_data: @@ -188,67 +186,55 @@ class StockNewsManager: if not all_news: return - - # Get the current news item to display - current_news = all_news[self.current_news_index] - next_news = all_news[(self.current_news_index + 1) % len(all_news)] + + # Get the number of headlines to show per rotation + headlines_per_rotation = self.stock_news_config.get('headlines_per_rotation', 2) + total_headlines = len(all_news) - # Format the news text with proper spacing and separator + # Calculate the starting index for the current group + start_idx = (self.current_news_group * headlines_per_rotation) % total_headlines + + # Build the text for all headlines in this group + news_texts = [] + for i in range(headlines_per_rotation): + idx = (start_idx + i) % total_headlines + news = all_news[idx] + news_texts.append(f"{news['symbol']}: {news['title']}") + + # Join all headlines with a separator separator = " - " # Visual separator between news items - current_text = f"{current_news['symbol']}: {current_news['title']}" - next_text = f"{next_news['symbol']}: {next_news['title']}" - news_text = f"{current_text}{separator}{next_text}" + news_text = separator.join(news_texts) - # Create a text image for efficient scrolling (only if needed) - if not hasattr(self, '_current_text_image') or self._current_text != news_text: - self._current_text_image = self._create_text_image(news_text) - self._current_text = news_text - text_width = self._current_text_image.width - self._text_width = text_width - else: - text_width = self._text_width + # Create and display the scrolling text image + text_image = self._create_text_image(news_text) - # Calculate the visible portion of the text - visible_width = min(self.display_manager.matrix.width, text_width) + # Calculate total scroll width + total_width = text_image.width + self.display_manager.matrix.width - # Create a new image for the current frame - frame_image = Image.new('RGB', (self.display_manager.matrix.width, self.display_manager.matrix.height), (0, 0, 0)) + # Update scroll position + self.scroll_position = (self.scroll_position + self.scroll_speed) % total_width - # Calculate the source and destination regions for the visible portion - src_x = self.scroll_position % text_width # Use modulo to wrap around smoothly - src_width = min(visible_width, text_width - src_x) + # Create a new black image for the display + display_image = Image.new('RGB', (self.display_manager.matrix.width, self.display_manager.matrix.height)) - # Copy the visible portion of the text to the frame - if src_width > 0: - src_region = self._current_text_image.crop((src_x, 0, src_x + src_width, self.display_manager.matrix.height)) - frame_image.paste(src_region, (0, 0)) + # Paste the appropriate portion of the text image + display_image.paste(text_image, (-self.scroll_position, 0)) - # If we need to wrap around to the beginning of the text - if src_x + src_width >= text_width: - remaining_width = self.display_manager.matrix.width - src_width - if remaining_width > 0: - wrap_src_width = min(remaining_width, text_width) - wrap_region = self._current_text_image.crop((0, 0, wrap_src_width, self.display_manager.matrix.height)) - frame_image.paste(wrap_region, (src_width, 0)) + # If we've wrapped around, paste the beginning of the text again + if self.scroll_position + self.display_manager.matrix.width > text_image.width: + display_image.paste(text_image, (text_image.width - self.scroll_position, 0)) - # Update the display with the new frame - self.display_manager.image = frame_image - self.display_manager.draw = ImageDraw.Draw(frame_image) - self.display_manager.update_display() + # Display the image + self.display_manager.display_image(display_image) + + # If we've completed a full scroll, move to the next group + if self.scroll_position == 0: + self.current_news_group = (self.current_news_group + 1) % ((total_headlines + headlines_per_rotation - 1) // headlines_per_rotation) # Log frame rate self._log_frame_rate() - # Update scroll position - self.scroll_position += self.scroll_speed - - # If we've scrolled past the current text, move to the next news item - if self.scroll_position >= text_width: - self.scroll_position = 0 - self.current_news_index = (self.current_news_index + 1) % len(all_news) - - # Add a small delay to control scroll speed - if self.scroll_delay > 0: - time.sleep(self.scroll_delay) + # Small delay to control scroll speed + time.sleep(self.scroll_delay) return True \ No newline at end of file From 7925bf515b1e91aa6cf673d3b3cd580465c0a52f Mon Sep 17 00:00:00 2001 From: Chuck <33324927+ChuckBuilds@users.noreply.github.com> Date: Thu, 10 Apr 2025 21:23:54 -0500 Subject: [PATCH 44/87] Stock news joins the lineup Stock News added to the display controller and drawing display instead of image --- config/config.json | 2 ++ src/display_controller.py | 58 +++++++++++++++++++++++++++++++-------- src/stock_news_manager.py | 40 +++++++++++++++------------ 3 files changed, 70 insertions(+), 30 deletions(-) diff --git a/config/config.json b/config/config.json index a3e5e318..d97b9a44 100644 --- a/config/config.json +++ b/config/config.json @@ -35,10 +35,12 @@ } }, "clock": { + "enabled": true, "format": "%H:%M:%S", "update_interval": 1 }, "weather": { + "enabled": true, "update_interval": 300, "units": "imperial", "display_format": "{temp}°F\n{condition}" diff --git a/src/display_controller.py b/src/display_controller.py index 0e893050..6b6c7e00 100644 --- a/src/display_controller.py +++ b/src/display_controller.py @@ -54,28 +54,62 @@ class DisplayController: # Check if we need to switch display mode if current_time - self.last_switch > self.get_current_duration(): - # Cycle through: clock -> weather (current) -> weather (hourly) -> weather (daily) -> stocks -> stock_news + # Find next enabled display mode + next_display = None + if self.current_display == 'clock': - self.current_display = 'weather' - self.weather_mode = 'current' + if self.config.get('weather', {}).get('enabled', False): + next_display = 'weather' + self.weather_mode = 'current' + elif self.config.get('stocks', {}).get('enabled', False): + next_display = 'stocks' + elif self.config.get('stock_news', {}).get('enabled', False): + next_display = 'stock_news' + else: + next_display = 'clock' + elif self.current_display == 'weather': if self.weather_mode == 'current': + next_display = 'weather' self.weather_mode = 'hourly' elif self.weather_mode == 'hourly': + next_display = 'weather' self.weather_mode = 'daily' else: # daily if self.config.get('stocks', {}).get('enabled', False): - self.current_display = 'stocks' + next_display = 'stocks' + elif self.config.get('stock_news', {}).get('enabled', False): + next_display = 'stock_news' + elif self.config.get('clock', {}).get('enabled', False): + next_display = 'clock' else: - self.current_display = 'clock' + next_display = 'weather' + self.weather_mode = 'current' + elif self.current_display == 'stocks': if self.config.get('stock_news', {}).get('enabled', False): - self.current_display = 'stock_news' + next_display = 'stock_news' + elif self.config.get('clock', {}).get('enabled', False): + next_display = 'clock' + elif self.config.get('weather', {}).get('enabled', False): + next_display = 'weather' + self.weather_mode = 'current' else: - self.current_display = 'clock' + next_display = 'stocks' + else: # stock_news - self.current_display = 'clock' + if self.config.get('clock', {}).get('enabled', False): + next_display = 'clock' + elif self.config.get('weather', {}).get('enabled', False): + next_display = 'weather' + self.weather_mode = 'current' + elif self.config.get('stocks', {}).get('enabled', False): + next_display = 'stocks' + else: + next_display = 'stock_news' + # Update current display + self.current_display = next_display logger.info(f"Switching display to: {self.current_display} {self.weather_mode if self.current_display == 'weather' else ''}") self.last_switch = current_time self.force_clear = True @@ -83,18 +117,18 @@ class DisplayController: # Display current screen try: - if self.current_display == 'clock': + if self.current_display == 'clock' and self.config.get('clock', {}).get('enabled', False): self.clock.display_time(force_clear=self.force_clear) - elif self.current_display == 'weather': + elif self.current_display == 'weather' and self.config.get('weather', {}).get('enabled', False): if self.weather_mode == 'current': self.weather.display_weather(force_clear=self.force_clear) elif self.weather_mode == 'hourly': self.weather.display_hourly_forecast(force_clear=self.force_clear) else: # daily self.weather.display_daily_forecast(force_clear=self.force_clear) - elif self.current_display == 'stocks': + elif self.current_display == 'stocks' and self.config.get('stocks', {}).get('enabled', False): self.stocks.display_stocks(force_clear=self.force_clear) - else: # stock_news + elif self.current_display == 'stock_news' and self.config.get('stock_news', {}).get('enabled', False): self.news.display_news() except Exception as e: logger.error(f"Error updating display: {e}") diff --git a/src/stock_news_manager.py b/src/stock_news_manager.py index 0ae86c50..68b6a73c 100644 --- a/src/stock_news_manager.py +++ b/src/stock_news_manager.py @@ -205,36 +205,40 @@ class StockNewsManager: separator = " - " # Visual separator between news items news_text = separator.join(news_texts) - # Create and display the scrolling text image - text_image = self._create_text_image(news_text) + # Clear the display + self.display_manager.clear() - # Calculate total scroll width - total_width = text_image.width + self.display_manager.matrix.width + # Calculate text width for scrolling + bbox = self.display_manager.draw.textbbox((0, 0), news_text, font=self.display_manager.small_font) + text_width = bbox[2] - bbox[0] + + # Calculate scroll position + display_width = self.display_manager.matrix.width + total_width = text_width + display_width # Update scroll position self.scroll_position = (self.scroll_position + self.scroll_speed) % total_width - # Create a new black image for the display - display_image = Image.new('RGB', (self.display_manager.matrix.width, self.display_manager.matrix.height)) + # Draw the text at the current scroll position + self.display_manager.draw_text( + news_text, + x=display_width - self.scroll_position, + y=None, # Center vertically + color=(255, 255, 255), + small_font=True + ) - # Paste the appropriate portion of the text image - display_image.paste(text_image, (-self.scroll_position, 0)) - - # If we've wrapped around, paste the beginning of the text again - if self.scroll_position + self.display_manager.matrix.width > text_image.width: - display_image.paste(text_image, (text_image.width - self.scroll_position, 0)) - - # Display the image - self.display_manager.display_image(display_image) + # Update the display + self.display_manager.update_display() # If we've completed a full scroll, move to the next group if self.scroll_position == 0: self.current_news_group = (self.current_news_group + 1) % ((total_headlines + headlines_per_rotation - 1) // headlines_per_rotation) - # Log frame rate - self._log_frame_rate() - # Small delay to control scroll speed time.sleep(self.scroll_delay) + # Log frame rate + self._log_frame_rate() + return True \ No newline at end of file From e8aa05a0b4b62f6355ad3418edb0ae8a2d08c91d Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Fri, 11 Apr 2025 09:47:13 -0500 Subject: [PATCH 45/87] Optimize scrolling text performance for news ticker --- src/display_manager.py | 19 +++++++------- src/stock_news_manager.py | 55 ++++++++++++++++++++++++--------------- 2 files changed, 43 insertions(+), 31 deletions(-) diff --git a/src/display_manager.py b/src/display_manager.py index 85d6cbbc..00b0b71c 100644 --- a/src/display_manager.py +++ b/src/display_manager.py @@ -36,18 +36,18 @@ class DisplayManager: options.parallel = hardware_config.get('parallel', 1) options.hardware_mapping = hardware_config.get('hardware_mapping', 'adafruit-hat-pwm') - # Optimize display settings for chained panels + # Optimize display settings for performance options.brightness = 100 - options.pwm_bits = 11 - options.pwm_lsb_nanoseconds = 200 # Increased for better stability + options.pwm_bits = 8 # Reduced for better performance + options.pwm_lsb_nanoseconds = 100 # Reduced for faster updates options.led_rgb_sequence = 'RGB' options.pixel_mapper_config = '' options.row_address_type = 0 options.multiplexing = 0 - options.disable_hardware_pulsing = False # Enable hardware pulsing for better sync + options.disable_hardware_pulsing = True # Disable pulsing for better performance options.show_refresh_rate = False - options.limit_refresh_rate_hz = 60 # Reduced refresh rate for stability - options.gpio_slowdown = 2 # Increased slowdown for better stability + options.limit_refresh_rate_hz = 120 # Increased refresh rate + options.gpio_slowdown = 1 # Reduced slowdown for better performance # Initialize the matrix self.matrix = RGBMatrix(options=options) @@ -94,14 +94,13 @@ class DisplayManager: # Copy the current image to the offscreen canvas self.offscreen_canvas.SetImage(self.image) - # Wait for the next vsync before swapping - self.matrix.SwapOnVSync(self.offscreen_canvas) + # Swap buffers immediately without waiting for vsync + self.matrix.SwapOnVSync(self.offscreen_canvas, False) # Swap our canvas references self.offscreen_canvas, self.current_canvas = self.current_canvas, self.offscreen_canvas - # Small delay to ensure stable refresh - time.sleep(0.001) + # No delay needed since we're not waiting for vsync except Exception as e: logger.error(f"Error updating display: {e}") diff --git a/src/stock_news_manager.py b/src/stock_news_manager.py index 68b6a73c..fa908ad4 100644 --- a/src/stock_news_manager.py +++ b/src/stock_news_manager.py @@ -205,28 +205,40 @@ class StockNewsManager: separator = " - " # Visual separator between news items news_text = separator.join(news_texts) + # Pre-render the text image for efficient scrolling + text_image = self._create_text_image(news_text) + text_width = text_image.width + display_width = self.display_manager.matrix.width + + # Calculate total width for scrolling + total_width = text_width + display_width + + # Update scroll position with smooth acceleration + scroll_speed = min(self.scroll_speed * 1.1, 3) # Gradually increase speed up to max + self.scroll_position = (self.scroll_position + scroll_speed) % total_width + # Clear the display self.display_manager.clear() - # Calculate text width for scrolling - bbox = self.display_manager.draw.textbbox((0, 0), news_text, font=self.display_manager.small_font) - text_width = bbox[2] - bbox[0] - - # Calculate scroll position - display_width = self.display_manager.matrix.width - total_width = text_width + display_width - - # Update scroll position - self.scroll_position = (self.scroll_position + self.scroll_speed) % total_width - - # Draw the text at the current scroll position - self.display_manager.draw_text( - news_text, - x=display_width - self.scroll_position, - y=None, # Center vertically - color=(255, 255, 255), - small_font=True - ) + # Calculate source and destination regions for efficient blitting + if self.scroll_position < display_width: + # Text is entering from the right + src_x = text_width - (display_width - self.scroll_position) + src_width = display_width - self.scroll_position + dst_x = self.scroll_position + self.display_manager.image.paste( + text_image.crop((src_x, 0, src_x + src_width, text_image.height)), + (dst_x, 0) + ) + else: + # Text is scrolling off the left + src_x = 0 + src_width = text_width + dst_x = self.scroll_position - display_width + self.display_manager.image.paste( + text_image.crop((src_x, 0, src_x + src_width, text_image.height)), + (dst_x, 0) + ) # Update the display self.display_manager.update_display() @@ -234,9 +246,10 @@ class StockNewsManager: # If we've completed a full scroll, move to the next group if self.scroll_position == 0: self.current_news_group = (self.current_news_group + 1) % ((total_headlines + headlines_per_rotation - 1) // headlines_per_rotation) + self.scroll_speed = 1 # Reset speed for next group - # Small delay to control scroll speed - time.sleep(self.scroll_delay) + # Minimal delay to control scroll speed while maintaining smoothness + time.sleep(0.001) # Log frame rate self._log_frame_rate() From 7461a3e46eb4c5afd5c6efda2f8f7a29a163972c Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Fri, 11 Apr 2025 09:49:09 -0500 Subject: [PATCH 46/87] Adjust matrix settings to reduce artifacting while maintaining performance --- src/display_manager.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/display_manager.py b/src/display_manager.py index 00b0b71c..0c5d57f5 100644 --- a/src/display_manager.py +++ b/src/display_manager.py @@ -36,18 +36,18 @@ class DisplayManager: options.parallel = hardware_config.get('parallel', 1) options.hardware_mapping = hardware_config.get('hardware_mapping', 'adafruit-hat-pwm') - # Optimize display settings for performance + # Balance performance and stability options.brightness = 100 - options.pwm_bits = 8 # Reduced for better performance - options.pwm_lsb_nanoseconds = 100 # Reduced for faster updates + options.pwm_bits = 10 # Increased from 8 for better color depth + options.pwm_lsb_nanoseconds = 150 # Increased for better stability options.led_rgb_sequence = 'RGB' options.pixel_mapper_config = '' options.row_address_type = 0 options.multiplexing = 0 - options.disable_hardware_pulsing = True # Disable pulsing for better performance + options.disable_hardware_pulsing = False # Re-enable hardware pulsing for stability options.show_refresh_rate = False - options.limit_refresh_rate_hz = 120 # Increased refresh rate - options.gpio_slowdown = 1 # Reduced slowdown for better performance + options.limit_refresh_rate_hz = 90 # Reduced from 120Hz for better stability + options.gpio_slowdown = 2 # Increased for better stability # Initialize the matrix self.matrix = RGBMatrix(options=options) @@ -94,13 +94,14 @@ class DisplayManager: # Copy the current image to the offscreen canvas self.offscreen_canvas.SetImage(self.image) - # Swap buffers immediately without waiting for vsync - self.matrix.SwapOnVSync(self.offscreen_canvas, False) + # Wait for the next vsync before swapping + self.matrix.SwapOnVSync(self.offscreen_canvas) # Swap our canvas references self.offscreen_canvas, self.current_canvas = self.current_canvas, self.offscreen_canvas - # No delay needed since we're not waiting for vsync + # Small delay to ensure stable refresh + time.sleep(0.001) except Exception as e: logger.error(f"Error updating display: {e}") From 702c6d2c3eb43da78958bb8c193209f63f672225 Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Fri, 11 Apr 2025 09:59:27 -0500 Subject: [PATCH 47/87] changed float to integer --- src/stock_news_manager.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/stock_news_manager.py b/src/stock_news_manager.py index fa908ad4..e6e7ada7 100644 --- a/src/stock_news_manager.py +++ b/src/stock_news_manager.py @@ -214,8 +214,8 @@ class StockNewsManager: total_width = text_width + display_width # Update scroll position with smooth acceleration - scroll_speed = min(self.scroll_speed * 1.1, 3) # Gradually increase speed up to max - self.scroll_position = (self.scroll_position + scroll_speed) % total_width + scroll_speed = min(int(self.scroll_speed * 1.1), 3) # Convert to integer + self.scroll_position = int(self.scroll_position + scroll_speed) % total_width # Clear the display self.display_manager.clear() @@ -223,9 +223,9 @@ class StockNewsManager: # Calculate source and destination regions for efficient blitting if self.scroll_position < display_width: # Text is entering from the right - src_x = text_width - (display_width - self.scroll_position) - src_width = display_width - self.scroll_position - dst_x = self.scroll_position + src_x = int(text_width - (display_width - self.scroll_position)) + src_width = int(display_width - self.scroll_position) + dst_x = int(self.scroll_position) self.display_manager.image.paste( text_image.crop((src_x, 0, src_x + src_width, text_image.height)), (dst_x, 0) @@ -234,7 +234,7 @@ class StockNewsManager: # Text is scrolling off the left src_x = 0 src_width = text_width - dst_x = self.scroll_position - display_width + dst_x = int(self.scroll_position - display_width) self.display_manager.image.paste( text_image.crop((src_x, 0, src_x + src_width, text_image.height)), (dst_x, 0) From 45d9f1eb6361c04c57725c0ea573f94f4f55787e Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Fri, 11 Apr 2025 10:08:19 -0500 Subject: [PATCH 48/87] Fix news ticker performance with simplified scrolling mechanism --- src/display_manager.py | 7 ++---- src/stock_news_manager.py | 51 +++++++++++++++------------------------ 2 files changed, 22 insertions(+), 36 deletions(-) diff --git a/src/display_manager.py b/src/display_manager.py index 0c5d57f5..b4b47ca6 100644 --- a/src/display_manager.py +++ b/src/display_manager.py @@ -94,14 +94,11 @@ class DisplayManager: # Copy the current image to the offscreen canvas self.offscreen_canvas.SetImage(self.image) - # Wait for the next vsync before swapping - self.matrix.SwapOnVSync(self.offscreen_canvas) + # Swap buffers immediately + self.matrix.SwapOnVSync(self.offscreen_canvas, False) # Swap our canvas references self.offscreen_canvas, self.current_canvas = self.current_canvas, self.offscreen_canvas - - # Small delay to ensure stable refresh - time.sleep(0.001) except Exception as e: logger.error(f"Error updating display: {e}") diff --git a/src/stock_news_manager.py b/src/stock_news_manager.py index e6e7ada7..4727a660 100644 --- a/src/stock_news_manager.py +++ b/src/stock_news_manager.py @@ -205,40 +205,33 @@ class StockNewsManager: separator = " - " # Visual separator between news items news_text = separator.join(news_texts) - # Pre-render the text image for efficient scrolling - text_image = self._create_text_image(news_text) - text_width = text_image.width - display_width = self.display_manager.matrix.width + # Get text dimensions + bbox = self.display_manager.draw.textbbox((0, 0), news_text, font=self.display_manager.small_font) + text_width = bbox[2] - bbox[0] + text_height = bbox[3] - bbox[1] - # Calculate total width for scrolling + # Calculate display position + display_width = self.display_manager.matrix.width total_width = text_width + display_width - # Update scroll position with smooth acceleration - scroll_speed = min(int(self.scroll_speed * 1.1), 3) # Convert to integer - self.scroll_position = int(self.scroll_position + scroll_speed) % total_width + # Update scroll position + self.scroll_position = (self.scroll_position + 1) % total_width # Clear the display self.display_manager.clear() - # Calculate source and destination regions for efficient blitting - if self.scroll_position < display_width: - # Text is entering from the right - src_x = int(text_width - (display_width - self.scroll_position)) - src_width = int(display_width - self.scroll_position) - dst_x = int(self.scroll_position) - self.display_manager.image.paste( - text_image.crop((src_x, 0, src_x + src_width, text_image.height)), - (dst_x, 0) - ) - else: - # Text is scrolling off the left - src_x = 0 - src_width = text_width - dst_x = int(self.scroll_position - display_width) - self.display_manager.image.paste( - text_image.crop((src_x, 0, src_x + src_width, text_image.height)), - (dst_x, 0) - ) + # Draw text at current scroll position + x_pos = display_width - self.scroll_position + y_pos = (self.display_manager.matrix.height - text_height) // 2 + + # Draw the text + self.display_manager.draw_text( + news_text, + x=x_pos, + y=y_pos, + color=(255, 255, 255), + small_font=True + ) # Update the display self.display_manager.update_display() @@ -246,10 +239,6 @@ class StockNewsManager: # If we've completed a full scroll, move to the next group if self.scroll_position == 0: self.current_news_group = (self.current_news_group + 1) % ((total_headlines + headlines_per_rotation - 1) // headlines_per_rotation) - self.scroll_speed = 1 # Reset speed for next group - - # Minimal delay to control scroll speed while maintaining smoothness - time.sleep(0.001) # Log frame rate self._log_frame_rate() From ff344006b972949a6bbe3ccfd040ae5ab555cbc1 Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Fri, 11 Apr 2025 10:29:26 -0500 Subject: [PATCH 49/87] Fix stock news scrolling in test environment: - Optimize display manager settings for smooth scrolling - Add proper display initialization and cleanup in test script - Implement timing control to prevent display buffer overflow - Ensure consistent 1ms delay between updates for smooth scrolling --- config/config.json | 2 +- test_stock_news_manager.py | 23 +++++++++++++++++++++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/config/config.json b/config/config.json index d97b9a44..d4d18d71 100644 --- a/config/config.json +++ b/config/config.json @@ -57,7 +57,7 @@ "enabled": true, "update_interval": 300, "scroll_speed": 1, - "scroll_delay": 0.0001, + "scroll_delay": 0.001, "max_headlines_per_symbol": 1, "headlines_per_rotation": 2 } diff --git a/test_stock_news_manager.py b/test_stock_news_manager.py index 9172d974..b8b4a406 100644 --- a/test_stock_news_manager.py +++ b/test_stock_news_manager.py @@ -10,6 +10,7 @@ print(f"Current working directory: {os.getcwd()}") def main(): """Test the StockNewsManager class directly.""" + display_manager = None try: # Load configuration config_manager = ConfigManager() @@ -27,14 +28,27 @@ def main(): # Initialize display manager display_manager = DisplayManager(display_config) + # Clear the display and show a test pattern + display_manager.clear() + display_manager.update_display() + time.sleep(1) # Give time to see the test pattern + # Initialize news manager with the loaded config news_manager = StockNewsManager(config, display_manager) print("Testing news display. Press Ctrl+C to exit.") - # Run the news display in a loop + # Run the news display in a loop with proper timing + last_update = time.time() while True: - news_manager.display_news() + current_time = time.time() + # Ensure we're not updating too frequently + if current_time - last_update >= 0.001: # 1ms minimum between updates + news_manager.display_news() + last_update = current_time + else: + # Small sleep to prevent CPU hogging + time.sleep(0.0001) except KeyboardInterrupt: print("\nTest interrupted by user") @@ -43,6 +57,11 @@ def main(): import traceback traceback.print_exc() finally: + if display_manager: + # Clear the display before exiting + display_manager.clear() + display_manager.update_display() + display_manager.cleanup() print("Test completed") if __name__ == "__main__": From a7a341b47942a2561fcd8b34b83b1e6e95b15b9a Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Fri, 11 Apr 2025 10:34:26 -0500 Subject: [PATCH 50/87] Optimize stock news scrolling for better performance: - Use pre-rendered text image for efficient scrolling - Implement cropping and pasting for smoother animation - Remove unnecessary display operations and delays --- src/stock_news_manager.py | 31 ++++++++++++++----------------- test_stock_news_manager.py | 3 --- 2 files changed, 14 insertions(+), 20 deletions(-) diff --git a/src/stock_news_manager.py b/src/stock_news_manager.py index 4727a660..38f97eff 100644 --- a/src/stock_news_manager.py +++ b/src/stock_news_manager.py @@ -205,33 +205,30 @@ class StockNewsManager: separator = " - " # Visual separator between news items news_text = separator.join(news_texts) - # Get text dimensions - bbox = self.display_manager.draw.textbbox((0, 0), news_text, font=self.display_manager.small_font) - text_width = bbox[2] - bbox[0] - text_height = bbox[3] - bbox[1] + # Create a text image for efficient scrolling + text_image = self._create_text_image(news_text) + text_width = text_image.width + text_height = text_image.height # Calculate display position display_width = self.display_manager.matrix.width total_width = text_width + display_width # Update scroll position - self.scroll_position = (self.scroll_position + 1) % total_width + self.scroll_position = (self.scroll_position + self.scroll_speed) % total_width # Clear the display self.display_manager.clear() - # Draw text at current scroll position - x_pos = display_width - self.scroll_position - y_pos = (self.display_manager.matrix.height - text_height) // 2 - - # Draw the text - self.display_manager.draw_text( - news_text, - x=x_pos, - y=y_pos, - color=(255, 255, 255), - small_font=True - ) + # Calculate the visible portion of the text + visible_width = min(display_width, text_width - self.scroll_position) + if visible_width > 0: + # Crop the text image to show only the visible portion + visible_portion = text_image.crop((self.scroll_position, 0, + self.scroll_position + visible_width, text_height)) + + # Paste the visible portion onto the display + self.display_manager.image.paste(visible_portion, (0, 0)) # Update the display self.display_manager.update_display() diff --git a/test_stock_news_manager.py b/test_stock_news_manager.py index b8b4a406..e7928b1e 100644 --- a/test_stock_news_manager.py +++ b/test_stock_news_manager.py @@ -46,9 +46,6 @@ def main(): if current_time - last_update >= 0.001: # 1ms minimum between updates news_manager.display_news() last_update = current_time - else: - # Small sleep to prevent CPU hogging - time.sleep(0.0001) except KeyboardInterrupt: print("\nTest interrupted by user") From f3975e1ac0272666a3a00be7f00dad81afe740a3 Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Fri, 11 Apr 2025 10:40:02 -0500 Subject: [PATCH 51/87] Optimize stock news display performance: - Cache text image to reduce rendering overhead - Improve frame creation and update logic - Optimize text wrapping for smoother scrolling - Remove unnecessary display clears --- src/stock_news_manager.py | 55 +++++++++++++++++++++++++++----------- test_stock_news_manager.py | 9 ++----- 2 files changed, 41 insertions(+), 23 deletions(-) diff --git a/src/stock_news_manager.py b/src/stock_news_manager.py index 38f97eff..61effd8d 100644 --- a/src/stock_news_manager.py +++ b/src/stock_news_manager.py @@ -26,6 +26,8 @@ class StockNewsManager: self.news_data = {} self.current_news_group = 0 # Track which group of headlines we're showing self.scroll_position = 0 + self.cached_text_image = None # Cache for the text image + self.cached_text = None # Cache for the text string # Get scroll settings from config with faster defaults self.scroll_speed = self.stock_news_config.get('scroll_speed', 1) @@ -205,33 +207,54 @@ class StockNewsManager: separator = " - " # Visual separator between news items news_text = separator.join(news_texts) - # Create a text image for efficient scrolling - text_image = self._create_text_image(news_text) - text_width = text_image.width - text_height = text_image.height + # Only create new text image if the text has changed + if news_text != self.cached_text: + self.cached_text = news_text + self.cached_text_image = self._create_text_image(news_text) + self.scroll_position = 0 # Reset scroll position for new text - # Calculate display position + if not self.cached_text_image: + return + + text_width = self.cached_text_image.width + text_height = self.cached_text_image.height display_width = self.display_manager.matrix.width total_width = text_width + display_width # Update scroll position self.scroll_position = (self.scroll_position + self.scroll_speed) % total_width - # Clear the display - self.display_manager.clear() - # Calculate the visible portion of the text visible_width = min(display_width, text_width - self.scroll_position) if visible_width > 0: - # Crop the text image to show only the visible portion - visible_portion = text_image.crop((self.scroll_position, 0, - self.scroll_position + visible_width, text_height)) + # Create a new blank image for this frame + frame_image = Image.new('RGB', (display_width, text_height), (0, 0, 0)) - # Paste the visible portion onto the display - self.display_manager.image.paste(visible_portion, (0, 0)) - - # Update the display - self.display_manager.update_display() + # Crop and paste in one operation + if self.scroll_position + visible_width <= text_width: + # Normal case - text is still scrolling in + visible_portion = self.cached_text_image.crop(( + self.scroll_position, 0, + self.scroll_position + visible_width, text_height + )) + frame_image.paste(visible_portion, (0, 0)) + else: + # Wrapping case - text is wrapping around + first_part_width = text_width - self.scroll_position + first_part = self.cached_text_image.crop(( + self.scroll_position, 0, + text_width, text_height + )) + second_part = self.cached_text_image.crop(( + 0, 0, + visible_width - first_part_width, text_height + )) + frame_image.paste(first_part, (0, 0)) + frame_image.paste(second_part, (first_part_width, 0)) + + # Update the display with the new frame + self.display_manager.image = frame_image + self.display_manager.update_display() # If we've completed a full scroll, move to the next group if self.scroll_position == 0: diff --git a/test_stock_news_manager.py b/test_stock_news_manager.py index e7928b1e..bd98f8e6 100644 --- a/test_stock_news_manager.py +++ b/test_stock_news_manager.py @@ -38,14 +38,9 @@ def main(): print("Testing news display. Press Ctrl+C to exit.") - # Run the news display in a loop with proper timing - last_update = time.time() + # Run the news display in a loop while True: - current_time = time.time() - # Ensure we're not updating too frequently - if current_time - last_update >= 0.001: # 1ms minimum between updates - news_manager.display_news() - last_update = current_time + news_manager.display_news() except KeyboardInterrupt: print("\nTest interrupted by user") From 1f867e60f3e50602161659e0eaed9dccbab71339 Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Fri, 11 Apr 2025 10:57:03 -0500 Subject: [PATCH 52/87] Optimize stock news display in controller: - Remove global sleep delay - Allow news display to run at full speed - Keep slower update rates for other displays --- src/display_controller.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/display_controller.py b/src/display_controller.py index 6b6c7e00..ddc555ee 100644 --- a/src/display_controller.py +++ b/src/display_controller.py @@ -113,12 +113,13 @@ class DisplayController: logger.info(f"Switching display to: {self.current_display} {self.weather_mode if self.current_display == 'weather' else ''}") self.last_switch = current_time self.force_clear = True - self.display_manager.clear() # Ensure clean transition + self.display_manager.clear() # Display current screen try: if self.current_display == 'clock' and self.config.get('clock', {}).get('enabled', False): self.clock.display_time(force_clear=self.force_clear) + time.sleep(self.update_interval) elif self.current_display == 'weather' and self.config.get('weather', {}).get('enabled', False): if self.weather_mode == 'current': self.weather.display_weather(force_clear=self.force_clear) @@ -126,9 +127,12 @@ class DisplayController: self.weather.display_hourly_forecast(force_clear=self.force_clear) else: # daily self.weather.display_daily_forecast(force_clear=self.force_clear) + time.sleep(self.update_interval) elif self.current_display == 'stocks' and self.config.get('stocks', {}).get('enabled', False): self.stocks.display_stocks(force_clear=self.force_clear) + time.sleep(self.update_interval) elif self.current_display == 'stock_news' and self.config.get('stock_news', {}).get('enabled', False): + # For news, we want to update as fast as possible without delay self.news.display_news() except Exception as e: logger.error(f"Error updating display: {e}") @@ -138,9 +142,6 @@ class DisplayController: # Reset force clear flag after use self.force_clear = False - # Sleep between updates - time.sleep(self.update_interval) - except KeyboardInterrupt: print("\nDisplay stopped by user") finally: From 2f27b5f01d633cbec39ca6bc7ea669c6e3525774 Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Fri, 11 Apr 2025 11:19:17 -0500 Subject: [PATCH 53/87] Improve stock chart display: Reduce chart height and adjust spacing to prevent text overlap --- config/config.json | 4 ++-- src/stock_manager.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/config/config.json b/config/config.json index d4d18d71..01e35d13 100644 --- a/config/config.json +++ b/config/config.json @@ -35,12 +35,12 @@ } }, "clock": { - "enabled": true, + "enabled": false, "format": "%H:%M:%S", "update_interval": 1 }, "weather": { - "enabled": true, + "enabled": false, "update_interval": 300, "units": "imperial", "display_format": "{temp}°F\n{condition}" diff --git a/src/stock_manager.py b/src/stock_manager.py index 40630fed..efc650e9 100644 --- a/src/stock_manager.py +++ b/src/stock_manager.py @@ -179,8 +179,8 @@ class StockManager: ) # Calculate chart dimensions - chart_height = 22 # Increased height since we're using smaller fonts - chart_y = 7 # Start closer to symbol due to smaller font + chart_height = 16 # Reduced from 22 to make chart smaller + chart_y = 8 # Slightly adjusted starting position width = self.display_manager.matrix.width # Get min and max prices for scaling @@ -213,7 +213,7 @@ class StockManager: price_text = f"${data['price']:.2f} ({data['change']:+.1f}%)" self.display_manager.draw_text( price_text, - y=30, # Near bottom + y=28, # Moved down slightly from 30 to give more space color=color, small_font=True # Use small font ) From 87d1ac652fd7171a088f86c39ba0cb48c20a2572 Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Fri, 11 Apr 2025 11:23:37 -0500 Subject: [PATCH 54/87] Improve module initialization: Only initialize enabled modules and properly respect config settings --- src/display_controller.py | 54 ++++++++++++++++++++++++--------------- 1 file changed, 33 insertions(+), 21 deletions(-) diff --git a/src/display_controller.py b/src/display_controller.py index ddc555ee..51dd3167 100644 --- a/src/display_controller.py +++ b/src/display_controller.py @@ -17,11 +17,23 @@ class DisplayController: self.config_manager = ConfigManager() self.config = self.config_manager.load_config() self.display_manager = DisplayManager(self.config.get('display', {})) - self.clock = Clock(display_manager=self.display_manager) - self.weather = WeatherManager(self.config, self.display_manager) - self.stocks = StockManager(self.config, self.display_manager) - self.news = StockNewsManager(self.config, self.display_manager) - self.current_display = 'clock' + + # Only initialize enabled modules + self.clock = Clock(display_manager=self.display_manager) if self.config.get('clock', {}).get('enabled', False) else None + self.weather = WeatherManager(self.config, self.display_manager) if self.config.get('weather', {}).get('enabled', False) else None + self.stocks = StockManager(self.config, self.display_manager) if self.config.get('stocks', {}).get('enabled', False) else None + self.news = StockNewsManager(self.config, self.display_manager) if self.config.get('stock_news', {}).get('enabled', False) else None + + # Set initial display to first enabled module + self.current_display = 'clock' # Default + if not self.clock: + if self.weather: + self.current_display = 'weather' + elif self.stocks: + self.current_display = 'stocks' + elif self.news: + self.current_display = 'stock_news' + self.weather_mode = 'current' # current, hourly, or daily self.last_switch = time.time() self.force_clear = True # Start with a clear screen @@ -58,12 +70,12 @@ class DisplayController: next_display = None if self.current_display == 'clock': - if self.config.get('weather', {}).get('enabled', False): + if self.weather: next_display = 'weather' self.weather_mode = 'current' - elif self.config.get('stocks', {}).get('enabled', False): + elif self.stocks: next_display = 'stocks' - elif self.config.get('stock_news', {}).get('enabled', False): + elif self.news: next_display = 'stock_news' else: next_display = 'clock' @@ -76,34 +88,34 @@ class DisplayController: next_display = 'weather' self.weather_mode = 'daily' else: # daily - if self.config.get('stocks', {}).get('enabled', False): + if self.stocks: next_display = 'stocks' - elif self.config.get('stock_news', {}).get('enabled', False): + elif self.news: next_display = 'stock_news' - elif self.config.get('clock', {}).get('enabled', False): + elif self.clock: next_display = 'clock' else: next_display = 'weather' self.weather_mode = 'current' elif self.current_display == 'stocks': - if self.config.get('stock_news', {}).get('enabled', False): + if self.news: next_display = 'stock_news' - elif self.config.get('clock', {}).get('enabled', False): + elif self.clock: next_display = 'clock' - elif self.config.get('weather', {}).get('enabled', False): + elif self.weather: next_display = 'weather' self.weather_mode = 'current' else: next_display = 'stocks' else: # stock_news - if self.config.get('clock', {}).get('enabled', False): + if self.clock: next_display = 'clock' - elif self.config.get('weather', {}).get('enabled', False): + elif self.weather: next_display = 'weather' self.weather_mode = 'current' - elif self.config.get('stocks', {}).get('enabled', False): + elif self.stocks: next_display = 'stocks' else: next_display = 'stock_news' @@ -117,10 +129,10 @@ class DisplayController: # Display current screen try: - if self.current_display == 'clock' and self.config.get('clock', {}).get('enabled', False): + if self.current_display == 'clock' and self.clock: self.clock.display_time(force_clear=self.force_clear) time.sleep(self.update_interval) - elif self.current_display == 'weather' and self.config.get('weather', {}).get('enabled', False): + elif self.current_display == 'weather' and self.weather: if self.weather_mode == 'current': self.weather.display_weather(force_clear=self.force_clear) elif self.weather_mode == 'hourly': @@ -128,10 +140,10 @@ class DisplayController: else: # daily self.weather.display_daily_forecast(force_clear=self.force_clear) time.sleep(self.update_interval) - elif self.current_display == 'stocks' and self.config.get('stocks', {}).get('enabled', False): + elif self.current_display == 'stocks' and self.stocks: self.stocks.display_stocks(force_clear=self.force_clear) time.sleep(self.update_interval) - elif self.current_display == 'stock_news' and self.config.get('stock_news', {}).get('enabled', False): + elif self.current_display == 'stock_news' and self.news: # For news, we want to update as fast as possible without delay self.news.display_news() except Exception as e: From a741b181f57e67f9841eb927b2c3f7fe0db1f787 Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Fri, 11 Apr 2025 11:30:49 -0500 Subject: [PATCH 55/87] Redesign stock display: Add scrolling animation with logo, symbol, mini chart, and stacked price info --- src/stock_manager.py | 179 ++++++++++++++++++++++++++++++++----------- 1 file changed, 135 insertions(+), 44 deletions(-) diff --git a/src/stock_manager.py b/src/stock_manager.py index efc650e9..03ef56a8 100644 --- a/src/stock_manager.py +++ b/src/stock_manager.py @@ -8,6 +8,7 @@ from datetime import datetime import os import urllib.parse import re +from PIL import Image, ImageDraw # Configure logging logging.basicConfig(level=logging.INFO) @@ -21,7 +22,20 @@ class StockManager: self.last_update = 0 self.stock_data = {} self.current_stock_index = 0 - self.display_mode = 'info' # 'info' or 'chart' + self.scroll_position = 0 + self.cached_text_image = None + self.cached_text = None + + # Get scroll settings from config with faster defaults + self.scroll_speed = self.stocks_config.get('scroll_speed', 1) + self.scroll_delay = self.stocks_config.get('scroll_delay', 0.001) + + # Initialize frame rate tracking + self.frame_count = 0 + self.last_frame_time = time.time() + self.last_fps_log_time = time.time() + self.frame_times = [] + self.headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' } @@ -275,8 +289,105 @@ class StockManager: else: logger.error("Failed to fetch data for any configured stocks") + def _create_stock_display(self, symbol: str, data: Dict[str, Any]) -> Image.Image: + """Create an image containing the stock information for efficient scrolling.""" + width = self.display_manager.matrix.width + height = self.display_manager.matrix.height + + # Create a new image with black background + display_image = Image.new('RGB', (width, height), (0, 0, 0)) + draw = ImageDraw.Draw(display_image) + + # Get stock color + color = self._get_stock_color(symbol) + + # Draw stock logo (placeholder for now - we'll use a simple icon) + logo_text = "📈" if data.get('change', 0) >= 0 else "📉" + bbox = draw.textbbox((0, 0), logo_text, font=self.display_manager.small_font) + logo_width = bbox[2] - bbox[0] + logo_height = bbox[3] - bbox[1] + logo_y = (height - logo_height) // 2 + draw.text((2, logo_y), logo_text, font=self.display_manager.small_font, fill=color) + + # Draw symbol + symbol_text = symbol + bbox = draw.textbbox((0, 0), symbol_text, font=self.display_manager.small_font) + symbol_width = bbox[2] - bbox[0] + symbol_height = bbox[3] - bbox[1] + symbol_x = (width - symbol_width) // 2 + symbol_y = (height - symbol_height) // 2 + draw.text((symbol_x, symbol_y), symbol_text, font=self.display_manager.small_font, fill=(255, 255, 255)) + + # Draw mini chart + if data.get('price_history'): + chart_width = width // 4 + chart_height = height // 3 + chart_x = width - chart_width - 2 + chart_y = (height - chart_height) // 2 + + # Get price data for chart + prices = [p['price'] for p in data['price_history']] + if prices: + min_price = min(prices) + max_price = max(prices) + price_range = max_price - min_price + + if price_range > 0: + points = [] + for i, price in enumerate(prices): + x = chart_x + int((i / len(prices)) * chart_width) + y = chart_y + chart_height - int(((price - min_price) / price_range) * chart_height) + points.append((x, y)) + + # Draw lines between points + for i in range(len(points) - 1): + draw.line([points[i], points[i + 1]], fill=color, width=1) + + # Draw price and change below + price_text = f"${data['price']:.2f}" + change_text = f"({data['change']:+.1f}%)" + + # Draw price + bbox = draw.textbbox((0, 0), price_text, font=self.display_manager.small_font) + price_width = bbox[2] - bbox[0] + price_x = (width - price_width) // 2 + draw.text((price_x, height - 20), price_text, font=self.display_manager.small_font, fill=color) + + # Draw change + bbox = draw.textbbox((0, 0), change_text, font=self.display_manager.small_font) + change_width = bbox[2] - bbox[0] + change_x = (width - change_width) // 2 + draw.text((change_x, height - 10), change_text, font=self.display_manager.small_font, fill=color) + + return display_image + + def _log_frame_rate(self): + """Log frame rate statistics.""" + current_time = time.time() + + # Calculate instantaneous frame time + frame_time = current_time - self.last_frame_time + self.frame_times.append(frame_time) + + # Keep only last 100 frames for average + if len(self.frame_times) > 100: + self.frame_times.pop(0) + + # Log FPS every second + if current_time - self.last_fps_log_time >= 1.0: + avg_frame_time = sum(self.frame_times) / len(self.frame_times) + avg_fps = 1.0 / avg_frame_time if avg_frame_time > 0 else 0 + instant_fps = 1.0 / frame_time if frame_time > 0 else 0 + + logger.info(f"Frame stats - Avg FPS: {avg_fps:.1f}, Current FPS: {instant_fps:.1f}, Frame time: {frame_time*1000:.2f}ms") + self.last_fps_log_time = current_time + self.frame_count = 0 + + self.last_frame_time = current_time + self.frame_count += 1 + def display_stocks(self, force_clear: bool = False): - """Display stock information on the LED matrix.""" + """Display stock information with scrolling animation.""" if not self.stocks_config.get('enabled', False): return @@ -296,51 +407,31 @@ class StockManager: current_symbol = symbols[self.current_stock_index] data = self.stock_data[current_symbol] - # Toggle between info and chart display - if self.display_mode == 'info': - # Clear the display + # Create the display image if needed + if self.cached_text_image is None or self.cached_text != current_symbol: + self.cached_text_image = self._create_stock_display(current_symbol, data) + self.cached_text = current_symbol + + # Clear the display if requested + if force_clear: self.display_manager.clear() - - # Draw the stock symbol at the top with regular font - self.display_manager.draw_text( - data['symbol'], - y=2, # Near top - color=(255, 255, 255) # White for symbol - ) - - # Draw the price in the middle with small font - price_text = f"${data['price']:.2f}" - self.display_manager.draw_text( - price_text, - y=14, # Middle - color=(0, 255, 0) if data['change'] >= 0 else (255, 0, 0), # Green for up, red for down - small_font=True # Use small font - ) - - # Draw the change percentage at the bottom with small font - change_text = f"({data['change']:+.1f}%)" - self.display_manager.draw_text( - change_text, - y=24, # Near bottom - color=(0, 255, 0) if data['change'] >= 0 else (255, 0, 0), # Green for up, red for down - small_font=True # Use small font - ) - - # Update the display - self.display_manager.update_display() - - # Switch to chart mode next time - self.display_mode = 'chart' - else: # chart mode - self._draw_chart(current_symbol, data) - # Switch back to info mode next time - self.display_mode = 'info' + self.scroll_position = 0 - # Add a delay to make each display visible - time.sleep(3) + # Copy the cached image to the display + self.display_manager.image.paste(self.cached_text_image, (0, 0)) + self.display_manager.update_display() - # Move to next stock for next update - self.current_stock_index = (self.current_stock_index + 1) % len(symbols) + # Log frame rate + self._log_frame_rate() + + # Add a small delay between frames + time.sleep(self.scroll_delay) + + # Move to next stock after a delay + if time.time() - self.last_update > 5: # Show each stock for 5 seconds + self.current_stock_index = (self.current_stock_index + 1) % len(symbols) + self.last_update = time.time() + self.cached_text_image = None # Force recreation of display for next stock # If we've shown all stocks, signal completion by returning True return self.current_stock_index == 0 \ No newline at end of file From 4d2ef1bd0d3d7b0da8dd15bffb524db073dbff3d Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Fri, 11 Apr 2025 11:34:25 -0500 Subject: [PATCH 56/87] Improve stock display layout: Larger logo, stacked info, and smooth scrolling --- src/stock_manager.py | 75 +++++++++++++++++++++++++++----------------- 1 file changed, 46 insertions(+), 29 deletions(-) diff --git a/src/stock_manager.py b/src/stock_manager.py index 03ef56a8..505eec3f 100644 --- a/src/stock_manager.py +++ b/src/stock_manager.py @@ -295,34 +295,54 @@ class StockManager: height = self.display_manager.matrix.height # Create a new image with black background - display_image = Image.new('RGB', (width, height), (0, 0, 0)) + display_image = Image.new('RGB', (width * 2, height), (0, 0, 0)) # Double width for scrolling draw = ImageDraw.Draw(display_image) # Get stock color color = self._get_stock_color(symbol) - # Draw stock logo (placeholder for now - we'll use a simple icon) + # Draw large stock logo on the left logo_text = "📈" if data.get('change', 0) >= 0 else "📉" - bbox = draw.textbbox((0, 0), logo_text, font=self.display_manager.small_font) + bbox = draw.textbbox((0, 0), logo_text, font=self.display_manager.font) # Use regular font for larger logo logo_width = bbox[2] - bbox[0] logo_height = bbox[3] - bbox[1] logo_y = (height - logo_height) // 2 - draw.text((2, logo_y), logo_text, font=self.display_manager.small_font, fill=color) + draw.text((10, logo_y), logo_text, font=self.display_manager.font, fill=color) - # Draw symbol + # Calculate center section position (after logo) + center_x = logo_width + 20 # Start after logo with some padding + + # Draw stacked symbol, price, and change in the center + # Symbol symbol_text = symbol bbox = draw.textbbox((0, 0), symbol_text, font=self.display_manager.small_font) symbol_width = bbox[2] - bbox[0] symbol_height = bbox[3] - bbox[1] - symbol_x = (width - symbol_width) // 2 - symbol_y = (height - symbol_height) // 2 + symbol_x = center_x + (width // 3 - symbol_width) // 2 + symbol_y = height // 4 draw.text((symbol_x, symbol_y), symbol_text, font=self.display_manager.small_font, fill=(255, 255, 255)) - # Draw mini chart + # Price + price_text = f"${data['price']:.2f}" + bbox = draw.textbbox((0, 0), price_text, font=self.display_manager.small_font) + price_width = bbox[2] - bbox[0] + price_x = center_x + (width // 3 - price_width) // 2 + price_y = symbol_y + symbol_height + 5 + draw.text((price_x, price_y), price_text, font=self.display_manager.small_font, fill=color) + + # Change + change_text = f"({data['change']:+.1f}%)" + bbox = draw.textbbox((0, 0), change_text, font=self.display_manager.small_font) + change_width = bbox[2] - bbox[0] + change_x = center_x + (width // 3 - change_width) // 2 + change_y = price_y + symbol_height + 5 + draw.text((change_x, change_y), change_text, font=self.display_manager.small_font, fill=color) + + # Draw mini chart on the right if data.get('price_history'): - chart_width = width // 4 - chart_height = height // 3 - chart_x = width - chart_width - 2 + chart_width = width // 3 + chart_height = height // 2 + chart_x = center_x + width // 3 + 10 # Start after center section chart_y = (height - chart_height) // 2 # Get price data for chart @@ -343,22 +363,6 @@ class StockManager: for i in range(len(points) - 1): draw.line([points[i], points[i + 1]], fill=color, width=1) - # Draw price and change below - price_text = f"${data['price']:.2f}" - change_text = f"({data['change']:+.1f}%)" - - # Draw price - bbox = draw.textbbox((0, 0), price_text, font=self.display_manager.small_font) - price_width = bbox[2] - bbox[0] - price_x = (width - price_width) // 2 - draw.text((price_x, height - 20), price_text, font=self.display_manager.small_font, fill=color) - - # Draw change - bbox = draw.textbbox((0, 0), change_text, font=self.display_manager.small_font) - change_width = bbox[2] - bbox[0] - change_x = (width - change_width) // 2 - draw.text((change_x, height - 10), change_text, font=self.display_manager.small_font, fill=color) - return display_image def _log_frame_rate(self): @@ -411,16 +415,29 @@ class StockManager: if self.cached_text_image is None or self.cached_text != current_symbol: self.cached_text_image = self._create_stock_display(current_symbol, data) self.cached_text = current_symbol + self.scroll_position = 0 # Clear the display if requested if force_clear: self.display_manager.clear() self.scroll_position = 0 - # Copy the cached image to the display - self.display_manager.image.paste(self.cached_text_image, (0, 0)) + # Calculate the visible portion of the image + width = self.display_manager.matrix.width + visible_portion = self.cached_text_image.crop(( + self.scroll_position, 0, + self.scroll_position + width, self.display_manager.matrix.height + )) + + # Copy the visible portion to the display + self.display_manager.image.paste(visible_portion, (0, 0)) self.display_manager.update_display() + # Update scroll position + self.scroll_position += self.scroll_speed + if self.scroll_position >= width: # Reset when we've scrolled through the whole image + self.scroll_position = 0 + # Log frame rate self._log_frame_rate() From cab037f0e8c232f970af617d0dbe8d0d4760f529 Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Fri, 11 Apr 2025 11:38:07 -0500 Subject: [PATCH 57/87] Improve stock display spacing and disable stock news ticker --- config/config.json | 2 +- src/stock_manager.py | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/config/config.json b/config/config.json index 01e35d13..5ba6c64a 100644 --- a/config/config.json +++ b/config/config.json @@ -54,7 +54,7 @@ "display_format": "{symbol}: ${price} ({change}%)" }, "stock_news": { - "enabled": true, + "enabled": false, "update_interval": 300, "scroll_speed": 1, "scroll_delay": 0.001, diff --git a/src/stock_manager.py b/src/stock_manager.py index 505eec3f..40d7b7a1 100644 --- a/src/stock_manager.py +++ b/src/stock_manager.py @@ -322,20 +322,22 @@ class StockManager: symbol_y = height // 4 draw.text((symbol_x, symbol_y), symbol_text, font=self.display_manager.small_font, fill=(255, 255, 255)) - # Price + # Price (smaller) price_text = f"${data['price']:.2f}" bbox = draw.textbbox((0, 0), price_text, font=self.display_manager.small_font) price_width = bbox[2] - bbox[0] + price_height = bbox[3] - bbox[1] price_x = center_x + (width // 3 - price_width) // 2 - price_y = symbol_y + symbol_height + 5 + price_y = symbol_y + symbol_height + 2 # Reduced spacing draw.text((price_x, price_y), price_text, font=self.display_manager.small_font, fill=color) - # Change + # Change (smaller) change_text = f"({data['change']:+.1f}%)" bbox = draw.textbbox((0, 0), change_text, font=self.display_manager.small_font) change_width = bbox[2] - bbox[0] + change_height = bbox[3] - bbox[1] change_x = center_x + (width // 3 - change_width) // 2 - change_y = price_y + symbol_height + 5 + change_y = price_y + price_height + 1 # Reduced spacing draw.text((change_x, change_y), change_text, font=self.display_manager.small_font, fill=color) # Draw mini chart on the right From 135a47e0ce6ba29cb4226a5238b7b9d4080b890f Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Fri, 11 Apr 2025 11:40:24 -0500 Subject: [PATCH 58/87] Make price and change text same size with proper alignment --- src/stock_manager.py | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/src/stock_manager.py b/src/stock_manager.py index 40d7b7a1..a01f213a 100644 --- a/src/stock_manager.py +++ b/src/stock_manager.py @@ -322,22 +322,28 @@ class StockManager: symbol_y = height // 4 draw.text((symbol_x, symbol_y), symbol_text, font=self.display_manager.small_font, fill=(255, 255, 255)) - # Price (smaller) + # Price and change (same size) price_text = f"${data['price']:.2f}" - bbox = draw.textbbox((0, 0), price_text, font=self.display_manager.small_font) - price_width = bbox[2] - bbox[0] - price_height = bbox[3] - bbox[1] - price_x = center_x + (width // 3 - price_width) // 2 - price_y = symbol_y + symbol_height + 2 # Reduced spacing - draw.text((price_x, price_y), price_text, font=self.display_manager.small_font, fill=color) - - # Change (smaller) change_text = f"({data['change']:+.1f}%)" - bbox = draw.textbbox((0, 0), change_text, font=self.display_manager.small_font) - change_width = bbox[2] - bbox[0] - change_height = bbox[3] - bbox[1] - change_x = center_x + (width // 3 - change_width) // 2 - change_y = price_y + price_height + 1 # Reduced spacing + + # Calculate widths for both texts to ensure proper alignment + price_bbox = draw.textbbox((0, 0), price_text, font=self.display_manager.small_font) + change_bbox = draw.textbbox((0, 0), change_text, font=self.display_manager.small_font) + price_width = price_bbox[2] - price_bbox[0] + change_width = change_bbox[2] - change_bbox[0] + text_height = price_bbox[3] - price_bbox[1] + + # Center both texts based on the wider of the two + max_width = max(price_width, change_width) + price_x = center_x + (width // 3 - max_width) // 2 + change_x = center_x + (width // 3 - max_width) // 2 + + # Position texts vertically + price_y = symbol_y + symbol_height + 2 + change_y = price_y + text_height + 1 + + # Draw both texts + draw.text((price_x, price_y), price_text, font=self.display_manager.small_font, fill=color) draw.text((change_x, change_y), change_text, font=self.display_manager.small_font, fill=color) # Draw mini chart on the right From dfff7ee691a6d2f5f4619127c04be8a1b46fe6f3 Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Fri, 11 Apr 2025 11:43:26 -0500 Subject: [PATCH 59/87] Remove delay from stock ticker to match news ticker's smooth scrolling --- src/display_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/display_controller.py b/src/display_controller.py index 51dd3167..96c5f3d9 100644 --- a/src/display_controller.py +++ b/src/display_controller.py @@ -141,8 +141,8 @@ class DisplayController: self.weather.display_daily_forecast(force_clear=self.force_clear) time.sleep(self.update_interval) elif self.current_display == 'stocks' and self.stocks: + # For stocks, we want to update as fast as possible without delay self.stocks.display_stocks(force_clear=self.force_clear) - time.sleep(self.update_interval) elif self.current_display == 'stock_news' and self.news: # For news, we want to update as fast as possible without delay self.news.display_news() From 8405b85aca926baebbf71cd3cfc0059592f8749e Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Fri, 11 Apr 2025 11:45:20 -0500 Subject: [PATCH 60/87] Improve stock display text layout: Better vertical spacing and centering --- src/stock_manager.py | 123 ++++++++++++++++++++++++++----------------- 1 file changed, 74 insertions(+), 49 deletions(-) diff --git a/src/stock_manager.py b/src/stock_manager.py index a01f213a..a42558fa 100644 --- a/src/stock_manager.py +++ b/src/stock_manager.py @@ -9,6 +9,7 @@ import os import urllib.parse import re from PIL import Image, ImageDraw +import numpy as np # Configure logging logging.basicConfig(level=logging.INFO) @@ -289,28 +290,24 @@ class StockManager: else: logger.error("Failed to fetch data for any configured stocks") - def _create_stock_display(self, symbol: str, data: Dict[str, Any]) -> Image.Image: - """Create an image containing the stock information for efficient scrolling.""" - width = self.display_manager.matrix.width - height = self.display_manager.matrix.height + def _create_stock_display(self, symbol: str, data: Dict[str, Any], width: int, height: int, scroll_position: int = 0) -> Image.Image: + """Create a single stock display with scrolling animation.""" + # Create a wider image for scrolling + scroll_width = width * 2 # Double width for smooth scrolling + image = Image.new('RGB', (scroll_width, height), (0, 0, 0)) + draw = ImageDraw.Draw(image) - # Create a new image with black background - display_image = Image.new('RGB', (width * 2, height), (0, 0, 0)) # Double width for scrolling - draw = ImageDraw.Draw(display_image) - - # Get stock color - color = self._get_stock_color(symbol) + # Calculate center position for the main content + center_x = width // 2 # Draw large stock logo on the left - logo_text = "📈" if data.get('change', 0) >= 0 else "📉" - bbox = draw.textbbox((0, 0), logo_text, font=self.display_manager.font) # Use regular font for larger logo + logo_text = symbol[:1].upper() # First letter of symbol + bbox = draw.textbbox((0, 0), logo_text, font=self.display_manager.regular_font) logo_width = bbox[2] - bbox[0] logo_height = bbox[3] - bbox[1] + logo_x = center_x - width // 3 - logo_width // 2 logo_y = (height - logo_height) // 2 - draw.text((10, logo_y), logo_text, font=self.display_manager.font, fill=color) - - # Calculate center section position (after logo) - center_x = logo_width + 20 # Start after logo with some padding + draw.text((logo_x, logo_y), logo_text, font=self.display_manager.regular_font, fill=(255, 255, 255)) # Draw stacked symbol, price, and change in the center # Symbol @@ -319,14 +316,14 @@ class StockManager: symbol_width = bbox[2] - bbox[0] symbol_height = bbox[3] - bbox[1] symbol_x = center_x + (width // 3 - symbol_width) // 2 - symbol_y = height // 4 + symbol_y = height // 4 - symbol_height // 2 # Center symbol in top quarter draw.text((symbol_x, symbol_y), symbol_text, font=self.display_manager.small_font, fill=(255, 255, 255)) # Price and change (same size) price_text = f"${data['price']:.2f}" change_text = f"({data['change']:+.1f}%)" - # Calculate widths for both texts to ensure proper alignment + # Calculate widths and heights for both texts to ensure proper alignment price_bbox = draw.textbbox((0, 0), price_text, font=self.display_manager.small_font) change_bbox = draw.textbbox((0, 0), change_text, font=self.display_manager.small_font) price_width = price_bbox[2] - price_bbox[0] @@ -338,40 +335,67 @@ class StockManager: price_x = center_x + (width // 3 - max_width) // 2 change_x = center_x + (width // 3 - max_width) // 2 - # Position texts vertically - price_y = symbol_y + symbol_height + 2 - change_y = price_y + text_height + 1 + # Calculate total height needed for all three elements + total_text_height = symbol_height + text_height * 2 # Two lines of text + spacing = 1 # Minimal spacing between elements + total_height = total_text_height + spacing * 2 # Spacing between all elements + + # Start from the top with proper centering + start_y = (height - total_height) // 2 + + # Position texts vertically with minimal spacing + price_y = start_y + symbol_height + spacing + change_y = price_y + text_height + spacing # Draw both texts draw.text((price_x, price_y), price_text, font=self.display_manager.small_font, fill=color) draw.text((change_x, change_y), change_text, font=self.display_manager.small_font, fill=color) # Draw mini chart on the right - if data.get('price_history'): + if 'chart' in data and data['chart']: chart_width = width // 3 chart_height = height // 2 - chart_x = center_x + width // 3 + 10 # Start after center section - chart_y = (height - chart_height) // 2 - - # Get price data for chart - prices = [p['price'] for p in data['price_history']] - if prices: - min_price = min(prices) - max_price = max(prices) - price_range = max_price - min_price - - if price_range > 0: - points = [] - for i, price in enumerate(prices): - x = chart_x + int((i / len(prices)) * chart_width) - y = chart_y + chart_height - int(((price - min_price) / price_range) * chart_height) - points.append((x, y)) - - # Draw lines between points - for i in range(len(points) - 1): - draw.line([points[i], points[i + 1]], fill=color, width=1) + chart_x = center_x + width // 3 - chart_width // 2 + chart_y = height // 2 + self._draw_mini_chart(draw, data['chart'], chart_x, chart_y, chart_width, chart_height) - return display_image + # Crop to show only the visible portion based on scroll position + visible_image = image.crop((scroll_position, 0, scroll_position + width, height)) + return visible_image + + def _update_stock_display(self, symbol: str, data: Dict[str, Any], width: int, height: int) -> None: + """Update the stock display with smooth scrolling animation.""" + try: + # Create the full scrolling image + full_image = self._create_stock_display(symbol, data, width, height) + scroll_width = width * 2 # Double width for smooth scrolling + + # Scroll the image smoothly + for scroll_pos in range(0, scroll_width - width, 15): # Increased scroll speed to match news ticker + # Create visible portion + visible_image = full_image.crop((scroll_pos, 0, scroll_pos + width, height)) + + # Convert to RGB and create numpy array + rgb_image = visible_image.convert('RGB') + image_array = np.array(rgb_image) + + # Update display + self.display_manager.update_display(image_array) + + # Small delay for smooth animation + time.sleep(0.01) # Reduced delay to 10ms for smoother scrolling + + # Show final position briefly + final_image = full_image.crop((scroll_width - width, 0, scroll_width, height)) + rgb_image = final_image.convert('RGB') + image_array = np.array(rgb_image) + self.display_manager.update_display(image_array) + time.sleep(0.5) # Brief pause at the end + + except Exception as e: + logger.error(f"Error updating stock display for {symbol}: {str(e)}") + # Show error state + self._show_error_state(width, height) def _log_frame_rate(self): """Log frame rate statistics.""" @@ -421,7 +445,7 @@ class StockManager: # Create the display image if needed if self.cached_text_image is None or self.cached_text != current_symbol: - self.cached_text_image = self._create_stock_display(current_symbol, data) + self.cached_text_image = self._create_stock_display(current_symbol, data, self.display_manager.matrix.width, self.display_manager.matrix.height) self.cached_text = current_symbol self.scroll_position = 0 @@ -432,6 +456,12 @@ class StockManager: # Calculate the visible portion of the image width = self.display_manager.matrix.width + scroll_width = width * 2 # Double width for smooth scrolling + + # Update scroll position with small increments + self.scroll_position = (self.scroll_position + self.scroll_speed) % scroll_width + + # Calculate the visible portion visible_portion = self.cached_text_image.crop(( self.scroll_position, 0, self.scroll_position + width, self.display_manager.matrix.height @@ -441,11 +471,6 @@ class StockManager: self.display_manager.image.paste(visible_portion, (0, 0)) self.display_manager.update_display() - # Update scroll position - self.scroll_position += self.scroll_speed - if self.scroll_position >= width: # Reset when we've scrolled through the whole image - self.scroll_position = 0 - # Log frame rate self._log_frame_rate() From 0422da66c66bfe5bfec1395c32b2bd83a71d1cc9 Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Fri, 11 Apr 2025 11:46:53 -0500 Subject: [PATCH 61/87] Fix font initialization: Add regular_font to DisplayManager --- src/display_manager.py | 50 +++++++++++++++++++----------------------- 1 file changed, 23 insertions(+), 27 deletions(-) diff --git a/src/display_manager.py b/src/display_manager.py index b4b47ca6..309496a6 100644 --- a/src/display_manager.py +++ b/src/display_manager.py @@ -36,18 +36,18 @@ class DisplayManager: options.parallel = hardware_config.get('parallel', 1) options.hardware_mapping = hardware_config.get('hardware_mapping', 'adafruit-hat-pwm') - # Balance performance and stability + # Optimize display settings for chained panels options.brightness = 100 - options.pwm_bits = 10 # Increased from 8 for better color depth - options.pwm_lsb_nanoseconds = 150 # Increased for better stability + options.pwm_bits = 11 + options.pwm_lsb_nanoseconds = 200 # Increased for better stability options.led_rgb_sequence = 'RGB' options.pixel_mapper_config = '' options.row_address_type = 0 options.multiplexing = 0 - options.disable_hardware_pulsing = False # Re-enable hardware pulsing for stability + options.disable_hardware_pulsing = False # Enable hardware pulsing for better sync options.show_refresh_rate = False - options.limit_refresh_rate_hz = 90 # Reduced from 120Hz for better stability - options.gpio_slowdown = 2 # Increased for better stability + options.limit_refresh_rate_hz = 60 # Reduced refresh rate for stability + options.gpio_slowdown = 2 # Increased slowdown for better stability # Initialize the matrix self.matrix = RGBMatrix(options=options) @@ -91,14 +91,17 @@ class DisplayManager: def update_display(self): """Update the display using double buffering with proper sync.""" try: - # Copy the current image to the offscreen canvas + # Copy the current image to the offscreen canvas self.offscreen_canvas.SetImage(self.image) - # Swap buffers immediately - self.matrix.SwapOnVSync(self.offscreen_canvas, False) + # Wait for the next vsync before swapping + self.matrix.SwapOnVSync(self.offscreen_canvas) # Swap our canvas references self.offscreen_canvas, self.current_canvas = self.current_canvas, self.offscreen_canvas + + # Small delay to ensure stable refresh + time.sleep(0.001) except Exception as e: logger.error(f"Error updating display: {e}") @@ -119,32 +122,25 @@ class DisplayManager: logger.error(f"Error clearing display: {e}") def _load_fonts(self): - """Load fonts optimized for LED matrix display.""" + """Load fonts with proper error handling.""" try: - # Use Press Start 2P font - perfect for LED matrix displays - font_path = "assets/fonts/PressStart2P-Regular.ttf" + # Load regular font (Press Start 2P) + self.regular_font = ImageFont.truetype("assets/fonts/PressStart2P-Regular.ttf", 10) + logger.info("Regular font loaded successfully") - # For 32px height matrix, optimized sizes for pixel-perfect display - large_size = 10 # Large text for time and main info - small_size = 8 # Small text for secondary information + # Load small font (Press Start 2P at smaller size) + self.small_font = ImageFont.truetype("assets/fonts/PressStart2P-Regular.ttf", 8) + logger.info("Small font loaded successfully") - try: - self.font = ImageFont.truetype(font_path, large_size) - self.small_font = ImageFont.truetype(font_path, small_size) - logger.info(f"Loaded Press Start 2P font: {font_path} (large: {large_size}px, small: {small_size}px)") - except Exception as e: - logger.warning(f"Failed to load Press Start 2P font, falling back to default: {e}") - self.font = ImageFont.load_default() - self.small_font = ImageFont.load_default() - except Exception as e: logger.error(f"Error in font loading: {e}") - self.font = ImageFont.load_default() - self.small_font = self.font + # Fallback to default font + self.regular_font = ImageFont.load_default() + self.small_font = self.regular_font def draw_text(self, text: str, x: int = None, y: int = None, color: Tuple[int, int, int] = (255, 255, 255), small_font: bool = False) -> None: """Draw text on the display with improved clarity.""" - font = self.small_font if small_font else self.font + font = self.small_font if small_font else self.regular_font # Get text dimensions including ascenders and descenders bbox = self.draw.textbbox((0, 0), text, font=font) From 143c07150ff3cc90e2e8930e00f9ecccbd66a5de Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Fri, 11 Apr 2025 11:48:27 -0500 Subject: [PATCH 62/87] Make symbol text white while keeping price and change in stock color --- src/stock_manager.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/stock_manager.py b/src/stock_manager.py index a42558fa..2835c579 100644 --- a/src/stock_manager.py +++ b/src/stock_manager.py @@ -297,6 +297,9 @@ class StockManager: image = Image.new('RGB', (scroll_width, height), (0, 0, 0)) draw = ImageDraw.Draw(image) + # Get stock color + color = self._get_stock_color(symbol) + # Calculate center position for the main content center_x = width // 2 @@ -307,19 +310,19 @@ class StockManager: logo_height = bbox[3] - bbox[1] logo_x = center_x - width // 3 - logo_width // 2 logo_y = (height - logo_height) // 2 - draw.text((logo_x, logo_y), logo_text, font=self.display_manager.regular_font, fill=(255, 255, 255)) + draw.text((logo_x, logo_y), logo_text, font=self.display_manager.regular_font, fill=color) # Draw stacked symbol, price, and change in the center - # Symbol + # Symbol (always white) symbol_text = symbol bbox = draw.textbbox((0, 0), symbol_text, font=self.display_manager.small_font) symbol_width = bbox[2] - bbox[0] symbol_height = bbox[3] - bbox[1] symbol_x = center_x + (width // 3 - symbol_width) // 2 symbol_y = height // 4 - symbol_height // 2 # Center symbol in top quarter - draw.text((symbol_x, symbol_y), symbol_text, font=self.display_manager.small_font, fill=(255, 255, 255)) + draw.text((symbol_x, symbol_y), symbol_text, font=self.display_manager.small_font, fill=(255, 255, 255)) # White color for symbol - # Price and change (same size) + # Price and change (in stock color) price_text = f"${data['price']:.2f}" change_text = f"({data['change']:+.1f}%)" From 971a0d67d937f6e237524805d4d3295f65890594 Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Fri, 11 Apr 2025 11:50:39 -0500 Subject: [PATCH 63/87] Make stock chart larger and more visible: Increased size and line width --- src/stock_manager.py | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/src/stock_manager.py b/src/stock_manager.py index 2835c579..901fc1ae 100644 --- a/src/stock_manager.py +++ b/src/stock_manager.py @@ -355,12 +355,29 @@ class StockManager: draw.text((change_x, change_y), change_text, font=self.display_manager.small_font, fill=color) # Draw mini chart on the right - if 'chart' in data and data['chart']: - chart_width = width // 3 - chart_height = height // 2 + if 'price_history' in data and data['price_history']: + chart_width = width // 2 # Increased from width // 3 to width // 2 + chart_height = height // 1.5 # Increased from height // 2 to height // 1.5 chart_x = center_x + width // 3 - chart_width // 2 - chart_y = height // 2 - self._draw_mini_chart(draw, data['chart'], chart_x, chart_y, chart_width, chart_height) + chart_y = height // 2 - chart_height // 2 # Center the chart vertically + + # Get price data for chart + prices = [p['price'] for p in data['price_history']] + if prices: + min_price = min(prices) + max_price = max(prices) + price_range = max_price - min_price + + if price_range > 0: + points = [] + for i, price in enumerate(prices): + x = chart_x + int((i / (len(prices) - 1)) * chart_width) + y = chart_y + chart_height - int(((price - min_price) / price_range) * chart_height) + points.append((x, y)) + + # Draw lines between points with slightly thicker lines + for i in range(len(points) - 1): + draw.line([points[i], points[i + 1]], fill=color, width=2) # Increased line width from 1 to 2 # Crop to show only the visible portion based on scroll position visible_image = image.crop((scroll_position, 0, scroll_position + width, height)) From f9f3e94c8cffaf6ebfc1cc3ddd09f29b80303886 Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Fri, 11 Apr 2025 11:52:16 -0500 Subject: [PATCH 64/87] Move chart further right and make it longer to prevent text overlap --- src/stock_manager.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/stock_manager.py b/src/stock_manager.py index 901fc1ae..8902c764 100644 --- a/src/stock_manager.py +++ b/src/stock_manager.py @@ -356,10 +356,10 @@ class StockManager: # Draw mini chart on the right if 'price_history' in data and data['price_history']: - chart_width = width // 2 # Increased from width // 3 to width // 2 - chart_height = height // 1.5 # Increased from height // 2 to height // 1.5 - chart_x = center_x + width // 3 - chart_width // 2 - chart_y = height // 2 - chart_height // 2 # Center the chart vertically + chart_width = width // 1.8 # Increased from width // 2 to width // 1.8 for longer chart + chart_height = height // 1.5 + chart_x = center_x + width // 2 # Moved further right from width // 3 to width // 2 + chart_y = height // 2 - chart_height // 2 # Keep vertical centering # Get price data for chart prices = [p['price'] for p in data['price_history']] @@ -377,7 +377,7 @@ class StockManager: # Draw lines between points with slightly thicker lines for i in range(len(points) - 1): - draw.line([points[i], points[i + 1]], fill=color, width=2) # Increased line width from 1 to 2 + draw.line([points[i], points[i + 1]], fill=color, width=2) # Crop to show only the visible portion based on scroll position visible_image = image.crop((scroll_position, 0, scroll_position + width, height)) From 1bb9b17ecfecd432f6db93cbfcd1988f29a307de Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Fri, 11 Apr 2025 11:54:17 -0500 Subject: [PATCH 65/87] Improve chart display: Better data point handling and scaling --- src/stock_manager.py | 35 +++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/src/stock_manager.py b/src/stock_manager.py index 8902c764..9ab8f3f9 100644 --- a/src/stock_manager.py +++ b/src/stock_manager.py @@ -356,28 +356,35 @@ class StockManager: # Draw mini chart on the right if 'price_history' in data and data['price_history']: - chart_width = width // 1.8 # Increased from width // 2 to width // 1.8 for longer chart + chart_width = width // 1.8 chart_height = height // 1.5 - chart_x = center_x + width // 2 # Moved further right from width // 3 to width // 2 - chart_y = height // 2 - chart_height // 2 # Keep vertical centering + chart_x = center_x + width // 2 + chart_y = height // 2 - chart_height // 2 # Get price data for chart prices = [p['price'] for p in data['price_history']] - if prices: + if len(prices) > 1: # Need at least 2 points to draw a line min_price = min(prices) max_price = max(prices) price_range = max_price - min_price - if price_range > 0: - points = [] - for i, price in enumerate(prices): - x = chart_x + int((i / (len(prices) - 1)) * chart_width) - y = chart_y + chart_height - int(((price - min_price) / price_range) * chart_height) - points.append((x, y)) - - # Draw lines between points with slightly thicker lines - for i in range(len(points) - 1): - draw.line([points[i], points[i + 1]], fill=color, width=2) + # Add padding to price range to prevent flat lines + if price_range == 0: + price_range = min_price * 0.01 # 1% padding if all prices are the same + min_price = min_price - price_range/2 + max_price = max_price + price_range/2 + + points = [] + for i, price in enumerate(prices): + # Calculate x position with proper spacing + x = chart_x + int((i / (len(prices) - 1)) * chart_width) + # Calculate y position with padding + y = chart_y + chart_height - int(((price - min_price) / price_range) * chart_height) + points.append((x, y)) + + # Draw lines between points with slightly thicker lines + for i in range(len(points) - 1): + draw.line([points[i], points[i + 1]], fill=color, width=2) # Crop to show only the visible portion based on scroll position visible_image = image.crop((scroll_position, 0, scroll_position + width, height)) From 4e5c0397c136608877a69be01a10b776aa98beb4 Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Fri, 11 Apr 2025 11:55:40 -0500 Subject: [PATCH 66/87] Improve chart visibility: Increased size and added background --- src/stock_manager.py | 60 +++++++++++++++++++++++--------------------- 1 file changed, 31 insertions(+), 29 deletions(-) diff --git a/src/stock_manager.py b/src/stock_manager.py index 9ab8f3f9..f553c791 100644 --- a/src/stock_manager.py +++ b/src/stock_manager.py @@ -355,36 +355,38 @@ class StockManager: draw.text((change_x, change_y), change_text, font=self.display_manager.small_font, fill=color) # Draw mini chart on the right - if 'price_history' in data and data['price_history']: - chart_width = width // 1.8 - chart_height = height // 1.5 - chart_x = center_x + width // 2 - chart_y = height // 2 - chart_height // 2 + chart_width = 30 # Increased from 20 to 30 + chart_height = 32 # Increased from 32 to match text height + chart_x = width - chart_width - 5 # 5px padding from right edge + chart_y = 0 # Align with top of display + + # Draw chart background + draw.rectangle([(chart_x, chart_y), (chart_x + chart_width - 1, chart_y + chart_height - 1)], + outline=color) + + # Get price history for chart + price_history = data['price_history'] + if len(price_history) >= 2: # Need at least 2 points to draw a line + # Calculate price range with padding to avoid flat lines + min_price = min(price_history) * 0.99 # 1% padding below + max_price = max(price_history) * 1.01 # 1% padding above + price_range = max_price - min_price - # Get price data for chart - prices = [p['price'] for p in data['price_history']] - if len(prices) > 1: # Need at least 2 points to draw a line - min_price = min(prices) - max_price = max(prices) - price_range = max_price - min_price - - # Add padding to price range to prevent flat lines - if price_range == 0: - price_range = min_price * 0.01 # 1% padding if all prices are the same - min_price = min_price - price_range/2 - max_price = max_price + price_range/2 - - points = [] - for i, price in enumerate(prices): - # Calculate x position with proper spacing - x = chart_x + int((i / (len(prices) - 1)) * chart_width) - # Calculate y position with padding - y = chart_y + chart_height - int(((price - min_price) / price_range) * chart_height) - points.append((x, y)) - - # Draw lines between points with slightly thicker lines - for i in range(len(points) - 1): - draw.line([points[i], points[i + 1]], fill=color, width=2) + if price_range == 0: # If all prices are the same + price_range = min_price * 0.01 # Use 1% of price as range + + # Calculate points for the line + points = [] + for i, price in enumerate(price_history): + # Calculate x position with proper spacing + x = chart_x + 1 + (i * (chart_width - 2) // (len(price_history) - 1)) + # Calculate y position (inverted because y=0 is at top) + y = chart_y + chart_height - 1 - int((price - min_price) * (chart_height - 2) / price_range) + points.append((x, y)) + + # Draw the line + if len(points) >= 2: + draw.line(points, fill=color, width=1) # Crop to show only the visible portion based on scroll position visible_image = image.crop((scroll_position, 0, scroll_position + width, height)) From 041d9feb6905dfae6bea630a64f0d196179c2e23 Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Fri, 11 Apr 2025 11:56:50 -0500 Subject: [PATCH 67/87] Fix chart drawing: Properly handle price history data structure --- src/stock_manager.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/stock_manager.py b/src/stock_manager.py index f553c791..0a5d8bb2 100644 --- a/src/stock_manager.py +++ b/src/stock_manager.py @@ -365,11 +365,13 @@ class StockManager: outline=color) # Get price history for chart - price_history = data['price_history'] + price_history = data.get('price_history', []) if len(price_history) >= 2: # Need at least 2 points to draw a line + # Extract prices from price history + prices = [p['price'] for p in price_history] # Calculate price range with padding to avoid flat lines - min_price = min(price_history) * 0.99 # 1% padding below - max_price = max(price_history) * 1.01 # 1% padding above + min_price = min(prices) * 0.99 # 1% padding below + max_price = max(prices) * 1.01 # 1% padding above price_range = max_price - min_price if price_range == 0: # If all prices are the same @@ -377,7 +379,8 @@ class StockManager: # Calculate points for the line points = [] - for i, price in enumerate(price_history): + for i, price_data in enumerate(price_history): + price = price_data['price'] # Calculate x position with proper spacing x = chart_x + 1 + (i * (chart_width - 2) // (len(price_history) - 1)) # Calculate y position (inverted because y=0 is at top) From 15eb63a634ade02665edef6f69fd33e66895f302 Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Fri, 11 Apr 2025 12:31:48 -0500 Subject: [PATCH 68/87] Shift chart one width to the right in stock display --- src/stock_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stock_manager.py b/src/stock_manager.py index 0a5d8bb2..a7dfbd29 100644 --- a/src/stock_manager.py +++ b/src/stock_manager.py @@ -357,7 +357,7 @@ class StockManager: # Draw mini chart on the right chart_width = 30 # Increased from 20 to 30 chart_height = 32 # Increased from 32 to match text height - chart_x = width - chart_width - 5 # 5px padding from right edge + chart_x = scroll_width - chart_width - 5 # Shift one width to the right (using scroll_width instead of width) chart_y = 0 # Align with top of display # Draw chart background From caccf129f11322db0d2df947a0a55fab1103c5ca Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Fri, 11 Apr 2025 12:33:55 -0500 Subject: [PATCH 69/87] Improve chart visibility: Added checks and increased line width --- src/stock_manager.py | 47 ++++++++++++++++++++++---------------------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/src/stock_manager.py b/src/stock_manager.py index a7dfbd29..72cea983 100644 --- a/src/stock_manager.py +++ b/src/stock_manager.py @@ -357,7 +357,7 @@ class StockManager: # Draw mini chart on the right chart_width = 30 # Increased from 20 to 30 chart_height = 32 # Increased from 32 to match text height - chart_x = scroll_width - chart_width - 5 # Shift one width to the right (using scroll_width instead of width) + chart_x = scroll_width - chart_width - 5 # Shift one width to the right chart_y = 0 # Align with top of display # Draw chart background @@ -366,30 +366,31 @@ class StockManager: # Get price history for chart price_history = data.get('price_history', []) - if len(price_history) >= 2: # Need at least 2 points to draw a line + if price_history and len(price_history) >= 2: # Need at least 2 points to draw a line # Extract prices from price history prices = [p['price'] for p in price_history] - # Calculate price range with padding to avoid flat lines - min_price = min(prices) * 0.99 # 1% padding below - max_price = max(prices) * 1.01 # 1% padding above - price_range = max_price - min_price - - if price_range == 0: # If all prices are the same - price_range = min_price * 0.01 # Use 1% of price as range - - # Calculate points for the line - points = [] - for i, price_data in enumerate(price_history): - price = price_data['price'] - # Calculate x position with proper spacing - x = chart_x + 1 + (i * (chart_width - 2) // (len(price_history) - 1)) - # Calculate y position (inverted because y=0 is at top) - y = chart_y + chart_height - 1 - int((price - min_price) * (chart_height - 2) / price_range) - points.append((x, y)) - - # Draw the line - if len(points) >= 2: - draw.line(points, fill=color, width=1) + if prices: # Make sure we have prices + # Calculate price range with padding to avoid flat lines + min_price = min(prices) * 0.99 # 1% padding below + max_price = max(prices) * 1.01 # 1% padding above + price_range = max_price - min_price + + if price_range == 0: # If all prices are the same + price_range = min_price * 0.01 # Use 1% of price as range + + # Calculate points for the line + points = [] + for i, price_data in enumerate(price_history): + price = price_data['price'] + # Calculate x position with proper spacing + x = chart_x + 1 + (i * (chart_width - 2) // (len(price_history) - 1)) + # Calculate y position (inverted because y=0 is at top) + y = chart_y + chart_height - 1 - int((price - min_price) * (chart_height - 2) / price_range) + points.append((x, y)) + + # Draw the line with increased width for visibility + if len(points) >= 2: + draw.line(points, fill=color, width=2) # Increased line width # Crop to show only the visible portion based on scroll position visible_image = image.crop((scroll_position, 0, scroll_position + width, height)) From d965543c1c9fbb4c0b3ab3c29956b2144daf50d9 Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Fri, 11 Apr 2025 12:34:47 -0500 Subject: [PATCH 70/87] Remove chart from stock display for simpler layout --- src/stock_manager.py | 38 -------------------------------------- 1 file changed, 38 deletions(-) diff --git a/src/stock_manager.py b/src/stock_manager.py index 72cea983..79d97b32 100644 --- a/src/stock_manager.py +++ b/src/stock_manager.py @@ -354,44 +354,6 @@ class StockManager: draw.text((price_x, price_y), price_text, font=self.display_manager.small_font, fill=color) draw.text((change_x, change_y), change_text, font=self.display_manager.small_font, fill=color) - # Draw mini chart on the right - chart_width = 30 # Increased from 20 to 30 - chart_height = 32 # Increased from 32 to match text height - chart_x = scroll_width - chart_width - 5 # Shift one width to the right - chart_y = 0 # Align with top of display - - # Draw chart background - draw.rectangle([(chart_x, chart_y), (chart_x + chart_width - 1, chart_y + chart_height - 1)], - outline=color) - - # Get price history for chart - price_history = data.get('price_history', []) - if price_history and len(price_history) >= 2: # Need at least 2 points to draw a line - # Extract prices from price history - prices = [p['price'] for p in price_history] - if prices: # Make sure we have prices - # Calculate price range with padding to avoid flat lines - min_price = min(prices) * 0.99 # 1% padding below - max_price = max(prices) * 1.01 # 1% padding above - price_range = max_price - min_price - - if price_range == 0: # If all prices are the same - price_range = min_price * 0.01 # Use 1% of price as range - - # Calculate points for the line - points = [] - for i, price_data in enumerate(price_history): - price = price_data['price'] - # Calculate x position with proper spacing - x = chart_x + 1 + (i * (chart_width - 2) // (len(price_history) - 1)) - # Calculate y position (inverted because y=0 is at top) - y = chart_y + chart_height - 1 - int((price - min_price) * (chart_height - 2) / price_range) - points.append((x, y)) - - # Draw the line with increased width for visibility - if len(points) >= 2: - draw.line(points, fill=color, width=2) # Increased line width - # Crop to show only the visible portion based on scroll position visible_image = image.crop((scroll_position, 0, scroll_position + width, height)) return visible_image From efd02142524ec6be5d38bc70c83a16b834ec5dfb Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Fri, 11 Apr 2025 12:37:07 -0500 Subject: [PATCH 71/87] Implement continuous scrolling for all stock symbols in one line --- src/stock_manager.py | 60 ++++++++++++++++++++++++++++++-------------- 1 file changed, 41 insertions(+), 19 deletions(-) diff --git a/src/stock_manager.py b/src/stock_manager.py index 79d97b32..4f0613c4 100644 --- a/src/stock_manager.py +++ b/src/stock_manager.py @@ -418,7 +418,7 @@ class StockManager: self.frame_count += 1 def display_stocks(self, force_clear: bool = False): - """Display stock information with scrolling animation.""" + """Display stock information with continuous scrolling animation.""" if not self.stocks_config.get('enabled', False): return @@ -430,19 +430,44 @@ class StockManager: logger.warning("No stock data available to display") return - # Get the current stock to display + # Get all symbols symbols = list(self.stock_data.keys()) if not symbols: return - current_symbol = symbols[self.current_stock_index] - data = self.stock_data[current_symbol] - - # Create the display image if needed - if self.cached_text_image is None or self.cached_text != current_symbol: - self.cached_text_image = self._create_stock_display(current_symbol, data, self.display_manager.matrix.width, self.display_manager.matrix.height) - self.cached_text = current_symbol + # Create a continuous scrolling image if needed + if self.cached_text_image is None or force_clear: + # Create a very wide image that contains all stocks in sequence + width = self.display_manager.matrix.width + height = self.display_manager.matrix.height + + # Calculate total width needed for all stocks + # Each stock needs width*2 for scrolling, plus a small gap between stocks + gap = width // 4 # Gap between stocks + total_width = sum(width * 2 for _ in symbols) + gap * (len(symbols) - 1) + + # Create the full image + full_image = Image.new('RGB', (total_width, height), (0, 0, 0)) + draw = ImageDraw.Draw(full_image) + + # Draw each stock in sequence + current_x = 0 + for symbol in symbols: + data = self.stock_data[symbol] + + # Create stock display for this symbol + stock_image = self._create_stock_display(symbol, data, width, height, 0) + + # Paste this stock image into the full image + full_image.paste(stock_image, (current_x, 0)) + + # Move to next position + current_x += width * 2 + gap + + # Cache the full image + self.cached_text_image = full_image self.scroll_position = 0 + self.last_update = time.time() # Clear the display if requested if force_clear: @@ -451,10 +476,10 @@ class StockManager: # Calculate the visible portion of the image width = self.display_manager.matrix.width - scroll_width = width * 2 # Double width for smooth scrolling + total_width = self.cached_text_image.width # Update scroll position with small increments - self.scroll_position = (self.scroll_position + self.scroll_speed) % scroll_width + self.scroll_position = (self.scroll_position + self.scroll_speed) % total_width # Calculate the visible portion visible_portion = self.cached_text_image.crop(( @@ -472,11 +497,8 @@ class StockManager: # Add a small delay between frames time.sleep(self.scroll_delay) - # Move to next stock after a delay - if time.time() - self.last_update > 5: # Show each stock for 5 seconds - self.current_stock_index = (self.current_stock_index + 1) % len(symbols) - self.last_update = time.time() - self.cached_text_image = None # Force recreation of display for next stock - - # If we've shown all stocks, signal completion by returning True - return self.current_stock_index == 0 \ No newline at end of file + # If we've scrolled through the entire image, reset + if self.scroll_position == 0: + return True + + return False \ No newline at end of file From e7161608ce8e7dd29b7f011ad9695e3e88e8aa3f Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Fri, 11 Apr 2025 12:39:42 -0500 Subject: [PATCH 72/87] Add stock logo download and display functionality --- src/stock_manager.py | 118 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 110 insertions(+), 8 deletions(-) diff --git a/src/stock_manager.py b/src/stock_manager.py index 4f0613c4..fc1d272b 100644 --- a/src/stock_manager.py +++ b/src/stock_manager.py @@ -10,6 +10,7 @@ import urllib.parse import re from PIL import Image, ImageDraw import numpy as np +import hashlib # Configure logging logging.basicConfig(level=logging.INFO) @@ -37,6 +38,10 @@ class StockManager: self.last_fps_log_time = time.time() self.frame_times = [] + # Create assets/stocks directory if it doesn't exist + self.logo_dir = os.path.join('assets', 'stocks') + os.makedirs(self.logo_dir, exist_ok=True) + self.headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' } @@ -290,6 +295,87 @@ class StockManager: else: logger.error("Failed to fetch data for any configured stocks") + def _download_stock_logo(self, symbol: str) -> str: + """Download and save stock logo for a given symbol. + + Args: + symbol: Stock symbol (e.g., 'AAPL', 'MSFT') + + Returns: + Path to the saved logo image, or None if download failed + """ + try: + # Create a filename based on the symbol + filename = f"{symbol.lower()}.png" + filepath = os.path.join(self.logo_dir, filename) + + # Check if we already have the logo + if os.path.exists(filepath): + return filepath + + # Try to find logo from various sources + # 1. Yahoo Finance + yahoo_url = f"https://logo.clearbit.com/{symbol.lower()}.com" + + # 2. Alternative source if Yahoo fails + alt_url = f"https://storage.googleapis.com/iex/api/logos/{symbol}.png" + + # Try Yahoo first + response = requests.get(yahoo_url, headers=self.headers, timeout=5) + if response.status_code == 200 and 'image' in response.headers.get('Content-Type', ''): + with open(filepath, 'wb') as f: + f.write(response.content) + logger.info(f"Downloaded logo for {symbol} from Yahoo") + return filepath + + # Try alternative source + response = requests.get(alt_url, headers=self.headers, timeout=5) + if response.status_code == 200 and 'image' in response.headers.get('Content-Type', ''): + with open(filepath, 'wb') as f: + f.write(response.content) + logger.info(f"Downloaded logo for {symbol} from alternative source") + return filepath + + logger.warning(f"Could not download logo for {symbol}") + return None + + except Exception as e: + logger.error(f"Error downloading logo for {symbol}: {str(e)}") + return None + + def _get_stock_logo(self, symbol: str) -> Image.Image: + """Get stock logo image, or create a text-based fallback. + + Args: + symbol: Stock symbol (e.g., 'AAPL', 'MSFT') + + Returns: + PIL Image of the logo or text-based fallback + """ + # Try to download the logo if we don't have it + logo_path = self._download_stock_logo(symbol) + + if logo_path and os.path.exists(logo_path): + try: + # Open and resize the logo + logo = Image.open(logo_path) + + # Convert to RGBA if not already + if logo.mode != 'RGBA': + logo = logo.convert('RGBA') + + # Resize to fit in the display (assuming square logo) + max_size = min(self.display_manager.matrix.width // 2, + self.display_manager.matrix.height // 2) + logo = logo.resize((max_size, max_size), Image.LANCZOS) + + return logo + except Exception as e: + logger.error(f"Error processing logo for {symbol}: {str(e)}") + + # Fallback to text-based logo + return None + def _create_stock_display(self, symbol: str, data: Dict[str, Any], width: int, height: int, scroll_position: int = 0) -> Image.Image: """Create a single stock display with scrolling animation.""" # Create a wider image for scrolling @@ -303,14 +389,30 @@ class StockManager: # Calculate center position for the main content center_x = width // 2 - # Draw large stock logo on the left - logo_text = symbol[:1].upper() # First letter of symbol - bbox = draw.textbbox((0, 0), logo_text, font=self.display_manager.regular_font) - logo_width = bbox[2] - bbox[0] - logo_height = bbox[3] - bbox[1] - logo_x = center_x - width // 3 - logo_width // 2 - logo_y = (height - logo_height) // 2 - draw.text((logo_x, logo_y), logo_text, font=self.display_manager.regular_font, fill=color) + # Try to get stock logo + logo = self._get_stock_logo(symbol) + + if logo: + # Draw the logo on the left + logo_x = center_x - width // 3 - logo.width // 2 + logo_y = (height - logo.height) // 2 + + # Create a new image with alpha channel for the logo + logo_bg = Image.new('RGBA', (scroll_width, height), (0, 0, 0, 0)) + logo_bg.paste(logo, (logo_x, logo_y)) + + # Convert to RGB for the main image + logo_rgb = logo_bg.convert('RGB') + image.paste(logo_rgb, (0, 0)) + else: + # Fallback to text-based logo + logo_text = symbol[:1].upper() # First letter of symbol + bbox = draw.textbbox((0, 0), logo_text, font=self.display_manager.regular_font) + logo_width = bbox[2] - bbox[0] + logo_height = bbox[3] - bbox[1] + logo_x = center_x - width // 3 - logo_width // 2 + logo_y = (height - logo_height) // 2 + draw.text((logo_x, logo_y), logo_text, font=self.display_manager.regular_font, fill=color) # Draw stacked symbol, price, and change in the center # Symbol (always white) From 724dfc9b21822a05763f038479a6d8a7eece33d7 Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Fri, 11 Apr 2025 12:42:05 -0500 Subject: [PATCH 73/87] Include assets/stocks directory in repository and update code to use it --- assets/stocks/.gitkeep | 0 src/stock_manager.py | 3 +-- 2 files changed, 1 insertion(+), 2 deletions(-) create mode 100644 assets/stocks/.gitkeep diff --git a/assets/stocks/.gitkeep b/assets/stocks/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/stock_manager.py b/src/stock_manager.py index fc1d272b..e5a3b58a 100644 --- a/src/stock_manager.py +++ b/src/stock_manager.py @@ -38,9 +38,8 @@ class StockManager: self.last_fps_log_time = time.time() self.frame_times = [] - # Create assets/stocks directory if it doesn't exist + # Use the assets/stocks directory from the repository self.logo_dir = os.path.join('assets', 'stocks') - os.makedirs(self.logo_dir, exist_ok=True) self.headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' From b899f56cabbc67e04b4049961d80d95d5af49481 Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Fri, 11 Apr 2025 13:01:42 -0500 Subject: [PATCH 74/87] Improve logo handling: Add in-memory fallback for permission issues --- src/stock_manager.py | 56 +++++++++++++++++++++++++++++++++++--------- 1 file changed, 45 insertions(+), 11 deletions(-) diff --git a/src/stock_manager.py b/src/stock_manager.py index e5a3b58a..3a2b4e70 100644 --- a/src/stock_manager.py +++ b/src/stock_manager.py @@ -322,18 +322,28 @@ class StockManager: # Try Yahoo first response = requests.get(yahoo_url, headers=self.headers, timeout=5) if response.status_code == 200 and 'image' in response.headers.get('Content-Type', ''): - with open(filepath, 'wb') as f: - f.write(response.content) - logger.info(f"Downloaded logo for {symbol} from Yahoo") - return filepath + try: + with open(filepath, 'wb') as f: + f.write(response.content) + logger.info(f"Downloaded logo for {symbol} from Yahoo") + return filepath + except PermissionError: + logger.warning(f"Permission denied when saving logo for {symbol}. Using in-memory logo instead.") + # Return a temporary path that won't be used for saving + return f"temp_{symbol.lower()}.png" # Try alternative source response = requests.get(alt_url, headers=self.headers, timeout=5) if response.status_code == 200 and 'image' in response.headers.get('Content-Type', ''): - with open(filepath, 'wb') as f: - f.write(response.content) - logger.info(f"Downloaded logo for {symbol} from alternative source") - return filepath + try: + with open(filepath, 'wb') as f: + f.write(response.content) + logger.info(f"Downloaded logo for {symbol} from alternative source") + return filepath + except PermissionError: + logger.warning(f"Permission denied when saving logo for {symbol}. Using in-memory logo instead.") + # Return a temporary path that won't be used for saving + return f"temp_{symbol.lower()}.png" logger.warning(f"Could not download logo for {symbol}") return None @@ -354,10 +364,34 @@ class StockManager: # Try to download the logo if we don't have it logo_path = self._download_stock_logo(symbol) - if logo_path and os.path.exists(logo_path): + if logo_path: try: - # Open and resize the logo - logo = Image.open(logo_path) + # Check if this is a temporary path (in-memory logo) + if logo_path.startswith("temp_"): + # For temporary paths, we need to download the logo again + # since we couldn't save it to disk + symbol_lower = symbol.lower() + yahoo_url = f"https://logo.clearbit.com/{symbol_lower}.com" + alt_url = f"https://storage.googleapis.com/iex/api/logos/{symbol}.png" + + # Try Yahoo first + response = requests.get(yahoo_url, headers=self.headers, timeout=5) + if response.status_code == 200 and 'image' in response.headers.get('Content-Type', ''): + # Create image from response content + from io import BytesIO + logo = Image.open(BytesIO(response.content)) + else: + # Try alternative source + response = requests.get(alt_url, headers=self.headers, timeout=5) + if response.status_code == 200 and 'image' in response.headers.get('Content-Type', ''): + # Create image from response content + from io import BytesIO + logo = Image.open(BytesIO(response.content)) + else: + return None + else: + # Normal case: open the saved logo file + logo = Image.open(logo_path) # Convert to RGBA if not already if logo.mode != 'RGBA': From c9c385b2aaf6ac101676d9f8a42c453fda1de2ba Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Fri, 11 Apr 2025 13:05:16 -0500 Subject: [PATCH 75/87] Improve logo processing: Add image verification and better error handling --- src/stock_manager.py | 53 ++++++++++++++++++++++++++++++++++++++------ 1 file changed, 46 insertions(+), 7 deletions(-) diff --git a/src/stock_manager.py b/src/stock_manager.py index 3a2b4e70..04152c24 100644 --- a/src/stock_manager.py +++ b/src/stock_manager.py @@ -377,21 +377,60 @@ class StockManager: # Try Yahoo first response = requests.get(yahoo_url, headers=self.headers, timeout=5) if response.status_code == 200 and 'image' in response.headers.get('Content-Type', ''): - # Create image from response content - from io import BytesIO - logo = Image.open(BytesIO(response.content)) + try: + # Create image from response content + from io import BytesIO + logo = Image.open(BytesIO(response.content)) + # Verify it's a valid image + logo.verify() + # Reopen after verify + logo = Image.open(BytesIO(response.content)) + except Exception as e: + logger.warning(f"Invalid image data from Yahoo for {symbol}: {str(e)}") + # Try alternative source + response = requests.get(alt_url, headers=self.headers, timeout=5) + if response.status_code == 200 and 'image' in response.headers.get('Content-Type', ''): + try: + # Create image from response content + from io import BytesIO + logo = Image.open(BytesIO(response.content)) + # Verify it's a valid image + logo.verify() + # Reopen after verify + logo = Image.open(BytesIO(response.content)) + except Exception as e: + logger.warning(f"Invalid image data from alternative source for {symbol}: {str(e)}") + return None + else: + return None else: # Try alternative source response = requests.get(alt_url, headers=self.headers, timeout=5) if response.status_code == 200 and 'image' in response.headers.get('Content-Type', ''): - # Create image from response content - from io import BytesIO - logo = Image.open(BytesIO(response.content)) + try: + # Create image from response content + from io import BytesIO + logo = Image.open(BytesIO(response.content)) + # Verify it's a valid image + logo.verify() + # Reopen after verify + logo = Image.open(BytesIO(response.content)) + except Exception as e: + logger.warning(f"Invalid image data from alternative source for {symbol}: {str(e)}") + return None else: return None else: # Normal case: open the saved logo file - logo = Image.open(logo_path) + try: + logo = Image.open(logo_path) + # Verify it's a valid image + logo.verify() + # Reopen after verify + logo = Image.open(logo_path) + except Exception as e: + logger.warning(f"Invalid image file for {symbol}: {str(e)}") + return None # Convert to RGBA if not already if logo.mode != 'RGBA': From 0cc24cb7b4957d4711e3eb9c53ba244b23808728 Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Fri, 11 Apr 2025 13:07:24 -0500 Subject: [PATCH 76/87] Fix logo directory permissions: Create directory with proper permissions if it doesn't exist --- src/stock_manager.py | 164 ++++++++++++++++++++++++++----------------- 1 file changed, 101 insertions(+), 63 deletions(-) diff --git a/src/stock_manager.py b/src/stock_manager.py index 04152c24..26f12b68 100644 --- a/src/stock_manager.py +++ b/src/stock_manager.py @@ -41,6 +41,18 @@ class StockManager: # Use the assets/stocks directory from the repository self.logo_dir = os.path.join('assets', 'stocks') + # Create logo directory with proper permissions if it doesn't exist + try: + if not os.path.exists(self.logo_dir): + os.makedirs(self.logo_dir, mode=0o755, exist_ok=True) + logger.info(f"Created logo directory: {self.logo_dir}") + elif not os.access(self.logo_dir, os.W_OK): + # Try to fix permissions if directory exists but is not writable + os.chmod(self.logo_dir, 0o755) + logger.info(f"Fixed permissions for logo directory: {self.logo_dir}") + except Exception as e: + logger.error(f"Error setting up logo directory: {str(e)}") + self.headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' } @@ -323,27 +335,69 @@ class StockManager: response = requests.get(yahoo_url, headers=self.headers, timeout=5) if response.status_code == 200 and 'image' in response.headers.get('Content-Type', ''): try: - with open(filepath, 'wb') as f: - f.write(response.content) - logger.info(f"Downloaded logo for {symbol} from Yahoo") - return filepath - except PermissionError: - logger.warning(f"Permission denied when saving logo for {symbol}. Using in-memory logo instead.") - # Return a temporary path that won't be used for saving - return f"temp_{symbol.lower()}.png" + # Verify it's a valid image before saving + from io import BytesIO + img = Image.open(BytesIO(response.content)) + img.verify() # Verify it's a valid image + + # Try to save the file + try: + with open(filepath, 'wb') as f: + f.write(response.content) + logger.info(f"Downloaded logo for {symbol} from Yahoo") + return filepath + except PermissionError: + logger.warning(f"Permission denied when saving logo for {symbol}. Using in-memory logo instead.") + # Return a temporary path that won't be used for saving + return f"temp_{symbol.lower()}.png" + except Exception as e: + logger.warning(f"Invalid image data from Yahoo for {symbol}: {str(e)}") # Try alternative source response = requests.get(alt_url, headers=self.headers, timeout=5) if response.status_code == 200 and 'image' in response.headers.get('Content-Type', ''): try: - with open(filepath, 'wb') as f: - f.write(response.content) - logger.info(f"Downloaded logo for {symbol} from alternative source") - return filepath - except PermissionError: - logger.warning(f"Permission denied when saving logo for {symbol}. Using in-memory logo instead.") - # Return a temporary path that won't be used for saving - return f"temp_{symbol.lower()}.png" + # Verify it's a valid image before saving + from io import BytesIO + img = Image.open(BytesIO(response.content)) + img.verify() # Verify it's a valid image + + # Try to save the file + try: + with open(filepath, 'wb') as f: + f.write(response.content) + logger.info(f"Downloaded logo for {symbol} from alternative source") + return filepath + except PermissionError: + logger.warning(f"Permission denied when saving logo for {symbol}. Using in-memory logo instead.") + # Return a temporary path that won't be used for saving + return f"temp_{symbol.lower()}.png" + except Exception as e: + logger.warning(f"Invalid image data from alternative source for {symbol}: {str(e)}") + + # Try a third source - company.com domain + company_url = f"https://logo.clearbit.com/{symbol.lower()}.com" + if company_url != yahoo_url: # Avoid duplicate request + response = requests.get(company_url, headers=self.headers, timeout=5) + if response.status_code == 200 and 'image' in response.headers.get('Content-Type', ''): + try: + # Verify it's a valid image before saving + from io import BytesIO + img = Image.open(BytesIO(response.content)) + img.verify() # Verify it's a valid image + + # Try to save the file + try: + with open(filepath, 'wb') as f: + f.write(response.content) + logger.info(f"Downloaded logo for {symbol} from company domain") + return filepath + except PermissionError: + logger.warning(f"Permission denied when saving logo for {symbol}. Using in-memory logo instead.") + # Return a temporary path that won't be used for saving + return f"temp_{symbol.lower()}.png" + except Exception as e: + logger.warning(f"Invalid image data from company domain for {symbol}: {str(e)}") logger.warning(f"Could not download logo for {symbol}") return None @@ -373,22 +427,12 @@ class StockManager: symbol_lower = symbol.lower() yahoo_url = f"https://logo.clearbit.com/{symbol_lower}.com" alt_url = f"https://storage.googleapis.com/iex/api/logos/{symbol}.png" + company_url = f"https://logo.clearbit.com/{symbol_lower}.com" - # Try Yahoo first - response = requests.get(yahoo_url, headers=self.headers, timeout=5) - if response.status_code == 200 and 'image' in response.headers.get('Content-Type', ''): + # Try all sources + for url in [yahoo_url, alt_url, company_url]: try: - # Create image from response content - from io import BytesIO - logo = Image.open(BytesIO(response.content)) - # Verify it's a valid image - logo.verify() - # Reopen after verify - logo = Image.open(BytesIO(response.content)) - except Exception as e: - logger.warning(f"Invalid image data from Yahoo for {symbol}: {str(e)}") - # Try alternative source - response = requests.get(alt_url, headers=self.headers, timeout=5) + response = requests.get(url, headers=self.headers, timeout=5) if response.status_code == 200 and 'image' in response.headers.get('Content-Type', ''): try: # Create image from response content @@ -398,28 +442,23 @@ class StockManager: logo.verify() # Reopen after verify logo = Image.open(BytesIO(response.content)) + + # Convert to RGBA if not already + if logo.mode != 'RGBA': + logo = logo.convert('RGBA') + + # Resize to fit in the display (assuming square logo) + max_size = min(self.display_manager.matrix.width // 2, + self.display_manager.matrix.height // 2) + logo = logo.resize((max_size, max_size), Image.LANCZOS) + + return logo except Exception as e: - logger.warning(f"Invalid image data from alternative source for {symbol}: {str(e)}") - return None - else: - return None - else: - # Try alternative source - response = requests.get(alt_url, headers=self.headers, timeout=5) - if response.status_code == 200 and 'image' in response.headers.get('Content-Type', ''): - try: - # Create image from response content - from io import BytesIO - logo = Image.open(BytesIO(response.content)) - # Verify it's a valid image - logo.verify() - # Reopen after verify - logo = Image.open(BytesIO(response.content)) - except Exception as e: - logger.warning(f"Invalid image data from alternative source for {symbol}: {str(e)}") - return None - else: - return None + logger.warning(f"Invalid image data from {url} for {symbol}: {str(e)}") + continue + except Exception as e: + logger.warning(f"Error downloading from {url} for {symbol}: {str(e)}") + continue else: # Normal case: open the saved logo file try: @@ -428,20 +467,19 @@ class StockManager: logo.verify() # Reopen after verify logo = Image.open(logo_path) + + # Convert to RGBA if not already + if logo.mode != 'RGBA': + logo = logo.convert('RGBA') + + # Resize to fit in the display (assuming square logo) + max_size = min(self.display_manager.matrix.width // 2, + self.display_manager.matrix.height // 2) + logo = logo.resize((max_size, max_size), Image.LANCZOS) + + return logo except Exception as e: logger.warning(f"Invalid image file for {symbol}: {str(e)}") - return None - - # Convert to RGBA if not already - if logo.mode != 'RGBA': - logo = logo.convert('RGBA') - - # Resize to fit in the display (assuming square logo) - max_size = min(self.display_manager.matrix.width // 2, - self.display_manager.matrix.height // 2) - logo = logo.resize((max_size, max_size), Image.LANCZOS) - - return logo except Exception as e: logger.error(f"Error processing logo for {symbol}: {str(e)}") From 81bb28f6b24bc352897210190a120a953698b2f6 Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Fri, 11 Apr 2025 13:08:17 -0500 Subject: [PATCH 77/87] Fix logo directory permissions: Use temporary directory when assets/stocks is not writable --- src/stock_manager.py | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/src/stock_manager.py b/src/stock_manager.py index 26f12b68..f1dc8858 100644 --- a/src/stock_manager.py +++ b/src/stock_manager.py @@ -38,20 +38,36 @@ class StockManager: self.last_fps_log_time = time.time() self.frame_times = [] - # Use the assets/stocks directory from the repository + # Try to use the assets/stocks directory from the repository self.logo_dir = os.path.join('assets', 'stocks') + self.use_temp_dir = False - # Create logo directory with proper permissions if it doesn't exist + # Check if we can write to the logo directory try: if not os.path.exists(self.logo_dir): - os.makedirs(self.logo_dir, mode=0o755, exist_ok=True) - logger.info(f"Created logo directory: {self.logo_dir}") + try: + os.makedirs(self.logo_dir, mode=0o755, exist_ok=True) + logger.info(f"Created logo directory: {self.logo_dir}") + except (PermissionError, OSError) as e: + logger.warning(f"Cannot create logo directory: {str(e)}. Using temporary directory instead.") + self.use_temp_dir = True elif not os.access(self.logo_dir, os.W_OK): - # Try to fix permissions if directory exists but is not writable - os.chmod(self.logo_dir, 0o755) - logger.info(f"Fixed permissions for logo directory: {self.logo_dir}") + logger.warning(f"Cannot write to logo directory: {self.logo_dir}. Using temporary directory instead.") + self.use_temp_dir = True + + # If we need to use a temporary directory, create it + if self.use_temp_dir: + import tempfile + self.logo_dir = tempfile.mkdtemp(prefix='stock_logos_') + logger.info(f"Using temporary directory for logos: {self.logo_dir}") + except Exception as e: logger.error(f"Error setting up logo directory: {str(e)}") + # Fall back to using a temporary directory + import tempfile + self.logo_dir = tempfile.mkdtemp(prefix='stock_logos_') + self.use_temp_dir = True + logger.info(f"Using temporary directory for logos: {self.logo_dir}") self.headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' From f45a4c28990ca9fe61552fd08fa3249f9c3fe009 Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Fri, 11 Apr 2025 13:12:08 -0500 Subject: [PATCH 78/87] Add price chart to stock display: Visual representation of price movement --- src/stock_manager.py | 166 +++++++++++++++++++++++++------------------ 1 file changed, 97 insertions(+), 69 deletions(-) diff --git a/src/stock_manager.py b/src/stock_manager.py index f1dc8858..721adfdb 100644 --- a/src/stock_manager.py +++ b/src/stock_manager.py @@ -8,7 +8,7 @@ from datetime import datetime import os import urllib.parse import re -from PIL import Image, ImageDraw +from PIL import Image, ImageDraw, ImageFont import numpy as np import hashlib @@ -502,95 +502,123 @@ class StockManager: # Fallback to text-based logo return None - def _create_stock_display(self, symbol: str, data: Dict[str, Any], width: int, height: int, scroll_position: int = 0) -> Image.Image: - """Create a single stock display with scrolling animation.""" + def _create_stock_display(self, symbol: str, price: float, change: float, change_percent: float) -> Image.Image: + """Create a display image for a stock with logo, symbol, price, and change. + + Args: + symbol: Stock symbol (e.g., 'AAPL', 'MSFT') + price: Current stock price + change: Price change + change_percent: Percentage change + + Returns: + PIL Image of the stock display + """ # Create a wider image for scrolling - scroll_width = width * 2 # Double width for smooth scrolling - image = Image.new('RGB', (scroll_width, height), (0, 0, 0)) + width = self.display_manager.matrix.width * 2 + height = self.display_manager.matrix.height + image = Image.new('RGB', (width, height), color=(0, 0, 0)) draw = ImageDraw.Draw(image) - # Get stock color - color = self._get_stock_color(symbol) - - # Calculate center position for the main content - center_x = width // 2 - - # Try to get stock logo + # Draw large stock logo on the left logo = self._get_stock_logo(symbol) - if logo: - # Draw the logo on the left - logo_x = center_x - width // 3 - logo.width // 2 + # Position logo on the left side + logo_x = 5 logo_y = (height - logo.height) // 2 - - # Create a new image with alpha channel for the logo - logo_bg = Image.new('RGBA', (scroll_width, height), (0, 0, 0, 0)) - logo_bg.paste(logo, (logo_x, logo_y)) - - # Convert to RGB for the main image - logo_rgb = logo_bg.convert('RGB') - image.paste(logo_rgb, (0, 0)) - else: - # Fallback to text-based logo - logo_text = symbol[:1].upper() # First letter of symbol - bbox = draw.textbbox((0, 0), logo_text, font=self.display_manager.regular_font) - logo_width = bbox[2] - bbox[0] - logo_height = bbox[3] - bbox[1] - logo_x = center_x - width // 3 - logo_width // 2 - logo_y = (height - logo_height) // 2 - draw.text((logo_x, logo_y), logo_text, font=self.display_manager.regular_font, fill=color) + image.paste(logo, (logo_x, logo_y), logo) - # Draw stacked symbol, price, and change in the center - # Symbol (always white) + # Draw symbol, price, and change in the center + # Use a regular font for the logo to make it larger + regular_font = ImageFont.truetype(self.display_manager.font_path, self.display_manager.font_size) + small_font = ImageFont.truetype(self.display_manager.font_path, self.display_manager.font_size - 2) + + # Calculate text dimensions for proper spacing symbol_text = symbol - bbox = draw.textbbox((0, 0), symbol_text, font=self.display_manager.small_font) - symbol_width = bbox[2] - bbox[0] - symbol_height = bbox[3] - bbox[1] - symbol_x = center_x + (width // 3 - symbol_width) // 2 - symbol_y = height // 4 - symbol_height // 2 # Center symbol in top quarter - draw.text((symbol_x, symbol_y), symbol_text, font=self.display_manager.small_font, fill=(255, 255, 255)) # White color for symbol + price_text = f"${price:.2f}" + change_text = f"{change:+.2f} ({change_percent:+.1f}%)" - # Price and change (in stock color) - price_text = f"${data['price']:.2f}" - change_text = f"({data['change']:+.1f}%)" + # Get the height of each text element + symbol_height = regular_font.getsize(symbol_text)[1] + price_height = regular_font.getsize(price_text)[1] + change_height = small_font.getsize(change_text)[1] - # Calculate widths and heights for both texts to ensure proper alignment - price_bbox = draw.textbbox((0, 0), price_text, font=self.display_manager.small_font) - change_bbox = draw.textbbox((0, 0), change_text, font=self.display_manager.small_font) - price_width = price_bbox[2] - price_bbox[0] - change_width = change_bbox[2] - change_bbox[0] - text_height = price_bbox[3] - price_bbox[1] + # Calculate total height needed for all text + total_text_height = symbol_height + price_height + change_height + 4 # 4 pixels for spacing - # Center both texts based on the wider of the two - max_width = max(price_width, change_width) - price_x = center_x + (width // 3 - max_width) // 2 - change_x = center_x + (width // 3 - max_width) // 2 + # Calculate starting y position to center the text block + start_y = (height - total_text_height) // 2 - # Calculate total height needed for all three elements - total_text_height = symbol_height + text_height * 2 # Two lines of text - spacing = 1 # Minimal spacing between elements - total_height = total_text_height + spacing * 2 # Spacing between all elements + # Draw symbol + symbol_x = width // 2 - regular_font.getsize(symbol_text)[0] // 2 + symbol_y = start_y + draw.text((symbol_x, symbol_y), symbol_text, font=regular_font, fill=(255, 255, 255)) - # Start from the top with proper centering - start_y = (height - total_height) // 2 + # Draw price + price_x = width // 2 - regular_font.getsize(price_text)[0] // 2 + price_y = symbol_y + symbol_height + 2 # 2 pixels spacing + draw.text((price_x, price_y), price_text, font=regular_font, fill=(255, 255, 255)) - # Position texts vertically with minimal spacing - price_y = start_y + symbol_height + spacing - change_y = price_y + text_height + spacing + # Draw change with color based on value + change_x = width // 2 - small_font.getsize(change_text)[0] // 2 + change_y = price_y + price_height + 2 # 2 pixels spacing + change_color = (0, 255, 0) if change >= 0 else (255, 0, 0) + draw.text((change_x, change_y), change_text, font=small_font, fill=change_color) - # Draw both texts - draw.text((price_x, price_y), price_text, font=self.display_manager.small_font, fill=color) - draw.text((change_x, change_y), change_text, font=self.display_manager.small_font, fill=color) + # Draw mini chart on the right + if symbol in self.stock_data and 'chart_data' in self.stock_data[symbol]: + chart_data = self.stock_data[symbol]['chart_data'] + if len(chart_data) >= 2: # Need at least 2 points to draw a line + # Calculate chart dimensions + chart_width = width // 4 + chart_height = height // 2 + chart_x = width - chart_width - 10 # 10 pixels from right edge + chart_y = (height - chart_height) // 2 + + # Find min and max prices for scaling + min_price = min(chart_data) + max_price = max(chart_data) + + # Add padding to avoid flat lines when prices are very close + price_range = max_price - min_price + if price_range < 0.01: # If prices are very close + min_price -= 0.01 + max_price += 0.01 + price_range = 0.02 + + # Draw chart background + draw.rectangle([chart_x, chart_y, chart_x + chart_width, chart_y + chart_height], + outline=(50, 50, 50)) + + # Calculate points for the line + points = [] + for i, price in enumerate(chart_data): + x = chart_x + (i * chart_width) // (len(chart_data) - 1) + # Invert y-axis (higher price = lower y value) + y = chart_y + chart_height - ((price - min_price) / price_range * chart_height) + points.append((x, y)) + + # Draw the line + if len(points) >= 2: + draw.line(points, fill=(0, 255, 0) if change >= 0 else (255, 0, 0), width=2) + + # Draw dots at each point + for point in points: + draw.ellipse([point[0]-2, point[1]-2, point[0]+2, point[1]+2], + fill=(0, 255, 0) if change >= 0 else (255, 0, 0)) + + # Crop to the visible portion based on scroll position + visible_width = self.display_manager.matrix.width + visible_image = image.crop((self.scroll_position, 0, + self.scroll_position + visible_width, height)) - # Crop to show only the visible portion based on scroll position - visible_image = image.crop((scroll_position, 0, scroll_position + width, height)) return visible_image def _update_stock_display(self, symbol: str, data: Dict[str, Any], width: int, height: int) -> None: """Update the stock display with smooth scrolling animation.""" try: # Create the full scrolling image - full_image = self._create_stock_display(symbol, data, width, height) + full_image = self._create_stock_display(symbol, data['price'], data['change'], data['change'] / data['open'] * 100) scroll_width = width * 2 # Double width for smooth scrolling # Scroll the image smoothly @@ -684,7 +712,7 @@ class StockManager: data = self.stock_data[symbol] # Create stock display for this symbol - stock_image = self._create_stock_display(symbol, data, width, height, 0) + stock_image = self._create_stock_display(symbol, data['price'], data['change'], data['change'] / data['open'] * 100) # Paste this stock image into the full image full_image.paste(stock_image, (current_x, 0)) From 50abd3057fadb0f47609febe0046ed14e2a6f31d Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Fri, 11 Apr 2025 13:15:23 -0500 Subject: [PATCH 79/87] Fix font references in stock display: Use correct font attributes from DisplayManager --- src/stock_manager.py | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/stock_manager.py b/src/stock_manager.py index 721adfdb..ca7a35b2 100644 --- a/src/stock_manager.py +++ b/src/stock_manager.py @@ -529,9 +529,9 @@ class StockManager: image.paste(logo, (logo_x, logo_y), logo) # Draw symbol, price, and change in the center - # Use a regular font for the logo to make it larger - regular_font = ImageFont.truetype(self.display_manager.font_path, self.display_manager.font_size) - small_font = ImageFont.truetype(self.display_manager.font_path, self.display_manager.font_size - 2) + # Use the fonts from display_manager + regular_font = self.display_manager.regular_font + small_font = self.display_manager.small_font # Calculate text dimensions for proper spacing symbol_text = symbol @@ -539,9 +539,13 @@ class StockManager: change_text = f"{change:+.2f} ({change_percent:+.1f}%)" # Get the height of each text element - symbol_height = regular_font.getsize(symbol_text)[1] - price_height = regular_font.getsize(price_text)[1] - change_height = small_font.getsize(change_text)[1] + symbol_bbox = draw.textbbox((0, 0), symbol_text, font=regular_font) + price_bbox = draw.textbbox((0, 0), price_text, font=regular_font) + change_bbox = draw.textbbox((0, 0), change_text, font=small_font) + + symbol_height = symbol_bbox[3] - symbol_bbox[1] + price_height = price_bbox[3] - price_bbox[1] + change_height = change_bbox[3] - change_bbox[1] # Calculate total height needed for all text total_text_height = symbol_height + price_height + change_height + 4 # 4 pixels for spacing @@ -550,17 +554,20 @@ class StockManager: start_y = (height - total_text_height) // 2 # Draw symbol - symbol_x = width // 2 - regular_font.getsize(symbol_text)[0] // 2 + symbol_width = symbol_bbox[2] - symbol_bbox[0] + symbol_x = width // 2 - symbol_width // 2 symbol_y = start_y draw.text((symbol_x, symbol_y), symbol_text, font=regular_font, fill=(255, 255, 255)) # Draw price - price_x = width // 2 - regular_font.getsize(price_text)[0] // 2 + price_width = price_bbox[2] - price_bbox[0] + price_x = width // 2 - price_width // 2 price_y = symbol_y + symbol_height + 2 # 2 pixels spacing draw.text((price_x, price_y), price_text, font=regular_font, fill=(255, 255, 255)) # Draw change with color based on value - change_x = width // 2 - small_font.getsize(change_text)[0] // 2 + change_width = change_bbox[2] - change_bbox[0] + change_x = width // 2 - change_width // 2 change_y = price_y + price_height + 2 # 2 pixels spacing change_color = (0, 255, 0) if change >= 0 else (255, 0, 0) draw.text((change_x, change_y), change_text, font=small_font, fill=change_color) From 8663ca62346aba2f3b0683ae939e7218dea8dead Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Fri, 11 Apr 2025 13:17:12 -0500 Subject: [PATCH 80/87] Fix stock display layout: Increase width and fix chart data processing --- src/stock_manager.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/stock_manager.py b/src/stock_manager.py index ca7a35b2..44a77d5d 100644 --- a/src/stock_manager.py +++ b/src/stock_manager.py @@ -515,7 +515,7 @@ class StockManager: PIL Image of the stock display """ # Create a wider image for scrolling - width = self.display_manager.matrix.width * 2 + width = self.display_manager.matrix.width * 3 # Increased from 2x to 3x for more space height = self.display_manager.matrix.height image = Image.new('RGB', (width, height), color=(0, 0, 0)) draw = ImageDraw.Draw(image) @@ -573,9 +573,12 @@ class StockManager: draw.text((change_x, change_y), change_text, font=small_font, fill=change_color) # Draw mini chart on the right - if symbol in self.stock_data and 'chart_data' in self.stock_data[symbol]: - chart_data = self.stock_data[symbol]['chart_data'] - if len(chart_data) >= 2: # Need at least 2 points to draw a line + if symbol in self.stock_data and 'price_history' in self.stock_data[symbol]: + price_history = self.stock_data[symbol]['price_history'] + if len(price_history) >= 2: # Need at least 2 points to draw a line + # Extract prices from price history + chart_data = [p['price'] for p in price_history] + # Calculate chart dimensions chart_width = width // 4 chart_height = height // 2 From 3a34a6e6f4c948479887e73ff8f643b73c2b32c7 Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Fri, 11 Apr 2025 13:19:13 -0500 Subject: [PATCH 81/87] Fix stock display: Return full image without cropping to show all elements --- src/stock_manager.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/stock_manager.py b/src/stock_manager.py index 44a77d5d..5ef3ed0c 100644 --- a/src/stock_manager.py +++ b/src/stock_manager.py @@ -617,12 +617,8 @@ class StockManager: draw.ellipse([point[0]-2, point[1]-2, point[0]+2, point[1]+2], fill=(0, 255, 0) if change >= 0 else (255, 0, 0)) - # Crop to the visible portion based on scroll position - visible_width = self.display_manager.matrix.width - visible_image = image.crop((self.scroll_position, 0, - self.scroll_position + visible_width, height)) - - return visible_image + # Return the full image without cropping + return image def _update_stock_display(self, symbol: str, data: Dict[str, Any], width: int, height: int) -> None: """Update the stock display with smooth scrolling animation.""" From c5bfd34d87f1e9265d477fb197fbab8568270286 Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Fri, 11 Apr 2025 13:24:13 -0500 Subject: [PATCH 82/87] Improve stock display layout: Adjust spacing between elements and increase gap between stocks --- src/stock_manager.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/stock_manager.py b/src/stock_manager.py index 5ef3ed0c..5131c09e 100644 --- a/src/stock_manager.py +++ b/src/stock_manager.py @@ -523,8 +523,8 @@ class StockManager: # Draw large stock logo on the left logo = self._get_stock_logo(symbol) if logo: - # Position logo on the left side - logo_x = 5 + # Position logo on the left side with less space before the symbol + logo_x = 2 # Reduced from 5 to 2 logo_y = (height - logo.height) // 2 image.paste(logo, (logo_x, logo_y), logo) @@ -548,7 +548,7 @@ class StockManager: change_height = change_bbox[3] - change_bbox[1] # Calculate total height needed for all text - total_text_height = symbol_height + price_height + change_height + 4 # 4 pixels for spacing + total_text_height = symbol_height + price_height + change_height + 2 # Reduced from 4 to 2 pixels for spacing # Calculate starting y position to center the text block start_y = (height - total_text_height) // 2 @@ -562,13 +562,13 @@ class StockManager: # Draw price price_width = price_bbox[2] - price_bbox[0] price_x = width // 2 - price_width // 2 - price_y = symbol_y + symbol_height + 2 # 2 pixels spacing + price_y = symbol_y + symbol_height + 1 # Reduced from 2 to 1 pixel spacing draw.text((price_x, price_y), price_text, font=regular_font, fill=(255, 255, 255)) # Draw change with color based on value change_width = change_bbox[2] - change_bbox[0] change_x = width // 2 - change_width // 2 - change_y = price_y + price_height + 2 # 2 pixels spacing + change_y = price_y + price_height + 1 # Reduced from 2 to 1 pixel spacing change_color = (0, 255, 0) if change >= 0 else (255, 0, 0) draw.text((change_x, change_y), change_text, font=small_font, fill=change_color) @@ -704,8 +704,8 @@ class StockManager: height = self.display_manager.matrix.height # Calculate total width needed for all stocks - # Each stock needs width*2 for scrolling, plus a small gap between stocks - gap = width // 4 # Gap between stocks + # Each stock needs width*2 for scrolling, plus a larger gap between stocks + gap = width // 2 # Increased from width//4 to width//2 for more space between stocks total_width = sum(width * 2 for _ in symbols) + gap * (len(symbols) - 1) # Create the full image From c073f8129777f51d093e86f2f440198a3c0c5b1e Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Fri, 11 Apr 2025 13:27:58 -0500 Subject: [PATCH 83/87] Improve stock chart: Increase size and remove outline for cleaner look --- src/stock_manager.py | 39 ++++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/src/stock_manager.py b/src/stock_manager.py index 5131c09e..f30d2f4e 100644 --- a/src/stock_manager.py +++ b/src/stock_manager.py @@ -515,7 +515,7 @@ class StockManager: PIL Image of the stock display """ # Create a wider image for scrolling - width = self.display_manager.matrix.width * 3 # Increased from 2x to 3x for more space + width = self.display_manager.matrix.width * 2 # Reduced from 3x to 2x since we'll handle spacing in display_stocks height = self.display_manager.matrix.height image = Image.new('RGB', (width, height), color=(0, 0, 0)) draw = ImageDraw.Draw(image) @@ -523,8 +523,8 @@ class StockManager: # Draw large stock logo on the left logo = self._get_stock_logo(symbol) if logo: - # Position logo on the left side with less space before the symbol - logo_x = 2 # Reduced from 5 to 2 + # Position logo on the left side with consistent spacing + logo_x = 2 logo_y = (height - logo.height) // 2 image.paste(logo, (logo_x, logo_y), logo) @@ -548,7 +548,7 @@ class StockManager: change_height = change_bbox[3] - change_bbox[1] # Calculate total height needed for all text - total_text_height = symbol_height + price_height + change_height + 2 # Reduced from 4 to 2 pixels for spacing + total_text_height = symbol_height + price_height + change_height + 2 # 2 pixels for spacing # Calculate starting y position to center the text block start_y = (height - total_text_height) // 2 @@ -562,13 +562,13 @@ class StockManager: # Draw price price_width = price_bbox[2] - price_bbox[0] price_x = width // 2 - price_width // 2 - price_y = symbol_y + symbol_height + 1 # Reduced from 2 to 1 pixel spacing + price_y = symbol_y + symbol_height + 1 # 1 pixel spacing draw.text((price_x, price_y), price_text, font=regular_font, fill=(255, 255, 255)) # Draw change with color based on value change_width = change_bbox[2] - change_bbox[0] change_x = width // 2 - change_width // 2 - change_y = price_y + price_height + 1 # Reduced from 2 to 1 pixel spacing + change_y = price_y + price_height + 1 # 1 pixel spacing change_color = (0, 255, 0) if change >= 0 else (255, 0, 0) draw.text((change_x, change_y), change_text, font=small_font, fill=change_color) @@ -580,9 +580,9 @@ class StockManager: chart_data = [p['price'] for p in price_history] # Calculate chart dimensions - chart_width = width // 4 - chart_height = height // 2 - chart_x = width - chart_width - 10 # 10 pixels from right edge + chart_width = width // 3 # Increased from width//4 to width//3 + chart_height = height // 1.5 # Increased from height//2 to height//1.5 + chart_x = width - chart_width - 5 # 5 pixels from right edge chart_y = (height - chart_height) // 2 # Find min and max prices for scaling @@ -596,10 +596,6 @@ class StockManager: max_price += 0.01 price_range = 0.02 - # Draw chart background - draw.rectangle([chart_x, chart_y, chart_x + chart_width, chart_y + chart_height], - outline=(50, 50, 50)) - # Calculate points for the line points = [] for i, price in enumerate(chart_data): @@ -704,15 +700,16 @@ class StockManager: height = self.display_manager.matrix.height # Calculate total width needed for all stocks - # Each stock needs width*2 for scrolling, plus a larger gap between stocks - gap = width // 2 # Increased from width//4 to width//2 for more space between stocks - total_width = sum(width * 2 for _ in symbols) + gap * (len(symbols) - 1) + # Each stock needs width*2 for scrolling, plus consistent gaps between elements + stock_gap = width // 3 # Gap between stocks + element_gap = width // 6 # Gap between elements within a stock + total_width = sum(width * 2 for _ in symbols) + stock_gap * (len(symbols) - 1) + element_gap * (len(symbols) * 2 - 1) # Create the full image full_image = Image.new('RGB', (total_width, height), (0, 0, 0)) draw = ImageDraw.Draw(full_image) - # Draw each stock in sequence + # Draw each stock in sequence with consistent spacing current_x = 0 for symbol in symbols: data = self.stock_data[symbol] @@ -723,8 +720,12 @@ class StockManager: # Paste this stock image into the full image full_image.paste(stock_image, (current_x, 0)) - # Move to next position - current_x += width * 2 + gap + # Move to next position with consistent spacing + current_x += width * 2 + element_gap + + # Add extra gap between stocks + if symbol != symbols[-1]: # Don't add gap after the last stock + current_x += stock_gap # Cache the full image self.cached_text_image = full_image From 3a25ff77b3f10c05427e89f5ff7d1831be912d56 Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Fri, 11 Apr 2025 13:29:54 -0500 Subject: [PATCH 84/87] Adjust stock chart position: Move chart 10 columns to the right --- src/stock_manager.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/stock_manager.py b/src/stock_manager.py index f30d2f4e..9e84e7fc 100644 --- a/src/stock_manager.py +++ b/src/stock_manager.py @@ -523,8 +523,8 @@ class StockManager: # Draw large stock logo on the left logo = self._get_stock_logo(symbol) if logo: - # Position logo on the left side with consistent spacing - logo_x = 2 + # Position logo on the left side with minimal spacing + logo_x = 0 # Reduced from 2 to 0 logo_y = (height - logo.height) // 2 image.paste(logo, (logo_x, logo_y), logo) @@ -553,21 +553,21 @@ class StockManager: # Calculate starting y position to center the text block start_y = (height - total_text_height) // 2 - # Draw symbol + # Draw symbol - moved closer to the logo symbol_width = symbol_bbox[2] - symbol_bbox[0] - symbol_x = width // 2 - symbol_width // 2 + symbol_x = width // 3 # Moved from width//2 to width//3 to bring text closer to logo symbol_y = start_y draw.text((symbol_x, symbol_y), symbol_text, font=regular_font, fill=(255, 255, 255)) - # Draw price + # Draw price - aligned with symbol price_width = price_bbox[2] - price_bbox[0] - price_x = width // 2 - price_width // 2 + price_x = symbol_x + (symbol_width - price_width) // 2 # Center price under symbol price_y = symbol_y + symbol_height + 1 # 1 pixel spacing draw.text((price_x, price_y), price_text, font=regular_font, fill=(255, 255, 255)) - # Draw change with color based on value + # Draw change with color based on value - aligned with price change_width = change_bbox[2] - change_bbox[0] - change_x = width // 2 - change_width // 2 + change_x = price_x + (price_width - change_width) // 2 # Center change under price change_y = price_y + price_height + 1 # 1 pixel spacing change_color = (0, 255, 0) if change >= 0 else (255, 0, 0) draw.text((change_x, change_y), change_text, font=small_font, fill=change_color) @@ -582,7 +582,7 @@ class StockManager: # Calculate chart dimensions chart_width = width // 3 # Increased from width//4 to width//3 chart_height = height // 1.5 # Increased from height//2 to height//1.5 - chart_x = width - chart_width - 5 # 5 pixels from right edge + chart_x = width - chart_width + 5 # Moved 10 columns to the right (from width - chart_width - 5) chart_y = (height - chart_height) // 2 # Find min and max prices for scaling From 29f45ec3f6fd74b6fa4be9e8f9775a8bdae58006 Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Fri, 11 Apr 2025 13:33:12 -0500 Subject: [PATCH 85/87] Improve display: Update test pattern to 'Initializing' with smaller font and faster animation. Also improve stock chart with 50% wider display and better spacing between elements. --- src/display_manager.py | 8 ++++---- src/stock_manager.py | 22 ++++++++++++++-------- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/display_manager.py b/src/display_manager.py index 309496a6..b18fb16d 100644 --- a/src/display_manager.py +++ b/src/display_manager.py @@ -62,7 +62,7 @@ class DisplayManager: # Initialize font with Press Start 2P try: - self.font = ImageFont.truetype("assets/fonts/PressStart2P-Regular.ttf", 10) + self.font = ImageFont.truetype("assets/fonts/PressStart2P-Regular.ttf", 8) # Reduced from 10 to 8 logger.info("Initial font loaded successfully") except Exception as e: logger.error(f"Failed to load initial font: {e}") @@ -81,12 +81,12 @@ class DisplayManager: # Draw a diagonal line self.draw.line([0, 0, self.matrix.width-1, self.matrix.height-1], fill=(0, 255, 0)) - # Draw some text - self.draw.text((10, 10), "TEST", font=self.font, fill=(0, 0, 255)) + # Draw some text - changed from "TEST" to "Initializing" with smaller font + self.draw.text((10, 10), "Initializing", font=self.font, fill=(0, 0, 255)) # Update the display once after everything is drawn self.update_display() - time.sleep(2) + time.sleep(0.5) # Reduced from 1 second to 0.5 seconds for faster animation def update_display(self): """Update the display using double buffering with proper sync.""" diff --git a/src/stock_manager.py b/src/stock_manager.py index 9e84e7fc..d280e60a 100644 --- a/src/stock_manager.py +++ b/src/stock_manager.py @@ -533,14 +533,20 @@ class StockManager: regular_font = self.display_manager.regular_font small_font = self.display_manager.small_font + # Create smaller versions of the fonts for symbol and price + symbol_font = ImageFont.truetype(self.display_manager.regular_font.path, + int(self.display_manager.regular_font.size * 0.8)) # 80% of regular size + price_font = ImageFont.truetype(self.display_manager.regular_font.path, + int(self.display_manager.regular_font.size * 0.8)) # 80% of regular size + # Calculate text dimensions for proper spacing symbol_text = symbol price_text = f"${price:.2f}" change_text = f"{change:+.2f} ({change_percent:+.1f}%)" # Get the height of each text element - symbol_bbox = draw.textbbox((0, 0), symbol_text, font=regular_font) - price_bbox = draw.textbbox((0, 0), price_text, font=regular_font) + symbol_bbox = draw.textbbox((0, 0), symbol_text, font=symbol_font) + price_bbox = draw.textbbox((0, 0), price_text, font=price_font) change_bbox = draw.textbbox((0, 0), change_text, font=small_font) symbol_height = symbol_bbox[3] - symbol_bbox[1] @@ -557,13 +563,13 @@ class StockManager: symbol_width = symbol_bbox[2] - symbol_bbox[0] symbol_x = width // 3 # Moved from width//2 to width//3 to bring text closer to logo symbol_y = start_y - draw.text((symbol_x, symbol_y), symbol_text, font=regular_font, fill=(255, 255, 255)) + draw.text((symbol_x, symbol_y), symbol_text, font=symbol_font, fill=(255, 255, 255)) # Draw price - aligned with symbol price_width = price_bbox[2] - price_bbox[0] price_x = symbol_x + (symbol_width - price_width) // 2 # Center price under symbol price_y = symbol_y + symbol_height + 1 # 1 pixel spacing - draw.text((price_x, price_y), price_text, font=regular_font, fill=(255, 255, 255)) + draw.text((price_x, price_y), price_text, font=price_font, fill=(255, 255, 255)) # Draw change with color based on value - aligned with price change_width = change_bbox[2] - change_bbox[0] @@ -579,10 +585,10 @@ class StockManager: # Extract prices from price history chart_data = [p['price'] for p in price_history] - # Calculate chart dimensions - chart_width = width // 3 # Increased from width//4 to width//3 - chart_height = height // 1.5 # Increased from height//2 to height//1.5 - chart_x = width - chart_width + 5 # Moved 10 columns to the right (from width - chart_width - 5) + # Calculate chart dimensions - 50% wider + chart_width = int(width // 2) # Increased from width//3 to width//2 (50% wider) + chart_height = height // 1.5 + chart_x = width - chart_width + 5 # Keep the same right margin chart_y = (height - chart_height) // 2 # Find min and max prices for scaling From 7021f2a251e78f2d9ce43014a31aa354e49b8528 Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Fri, 11 Apr 2025 13:36:20 -0500 Subject: [PATCH 86/87] Improve stock display layout: Move logo closer to text, adjust chart width, and optimize spacing --- src/stock_manager.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/stock_manager.py b/src/stock_manager.py index d280e60a..d47b581b 100644 --- a/src/stock_manager.py +++ b/src/stock_manager.py @@ -559,9 +559,9 @@ class StockManager: # Calculate starting y position to center the text block start_y = (height - total_text_height) // 2 - # Draw symbol - moved closer to the logo + # Draw symbol - moved even closer to the logo symbol_width = symbol_bbox[2] - symbol_bbox[0] - symbol_x = width // 3 # Moved from width//2 to width//3 to bring text closer to logo + symbol_x = width // 4 # Moved from width//3 to width//4 to bring text even closer to logo symbol_y = start_y draw.text((symbol_x, symbol_y), symbol_text, font=symbol_font, fill=(255, 255, 255)) @@ -586,7 +586,7 @@ class StockManager: chart_data = [p['price'] for p in price_history] # Calculate chart dimensions - 50% wider - chart_width = int(width // 2) # Increased from width//3 to width//2 (50% wider) + chart_width = int(width // 2.5) # Reduced from width//2 to width//2.5 (20% smaller) chart_height = height // 1.5 chart_x = width - chart_width + 5 # Keep the same right margin chart_y = (height - chart_height) // 2 From 0f9f6531335773a739a5055d30f9246a45b11f25 Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Fri, 11 Apr 2025 13:39:31 -0500 Subject: [PATCH 87/87] Update config: enable all displays --- config/config.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/config/config.json b/config/config.json index 5ba6c64a..d4d18d71 100644 --- a/config/config.json +++ b/config/config.json @@ -35,12 +35,12 @@ } }, "clock": { - "enabled": false, + "enabled": true, "format": "%H:%M:%S", "update_interval": 1 }, "weather": { - "enabled": false, + "enabled": true, "update_interval": 300, "units": "imperial", "display_format": "{temp}°F\n{condition}" @@ -54,7 +54,7 @@ "display_format": "{symbol}: ${price} ({change}%)" }, "stock_news": { - "enabled": false, + "enabled": true, "update_interval": 300, "scroll_speed": 1, "scroll_delay": 0.001,