make sure web ui is pulling existing config options

This commit is contained in:
Chuck
2025-09-15 13:59:33 -04:00
parent 652461a819
commit b20c3880b2
4 changed files with 310 additions and 29 deletions

View File

@@ -119,6 +119,7 @@ class OddsTickerManager:
self.current_game_index = 0
self.ticker_image = None # This will hold the single, wide image
self.last_display_time = 0
self._end_reached_logged = False # Track if we've already logged reaching the end
# Font setup
self.fonts = self._load_fonts()
@@ -1715,6 +1716,8 @@ class OddsTickerManager:
logger.debug(f"Reset/initialized display start time: {self._display_start_time}")
# Also reset scroll position for clean start
self.scroll_position = 0
# Reset the end reached logging flag
self._end_reached_logged = False
else:
# Check if the display start time is too old (more than 2x the dynamic duration)
current_time = time.time()
@@ -1723,6 +1726,8 @@ class OddsTickerManager:
logger.debug(f"Display start time is too old ({elapsed_time:.1f}s), resetting")
self._display_start_time = current_time
self.scroll_position = 0
# Reset the end reached logging flag
self._end_reached_logged = False
logger.debug(f"Number of games in data at start of display method: {len(self.games_data)}")
if not self.games_data:
@@ -1826,11 +1831,13 @@ class OddsTickerManager:
else:
# Stop scrolling when we reach the end
if self.scroll_position >= self.ticker_image.width - width:
logger.info(f"Odds ticker reached end: scroll_position {self.scroll_position} >= {self.ticker_image.width - width}")
if not self._end_reached_logged:
logger.info(f"Odds ticker reached end: scroll_position {self.scroll_position} >= {self.ticker_image.width - width}")
logger.info("Odds ticker scrolling stopped - reached end of content")
self._end_reached_logged = True
self.scroll_position = self.ticker_image.width - width
# Signal that scrolling has stopped
self.display_manager.set_scrolling_state(False)
logger.info("Odds ticker scrolling stopped - reached end of content")
# Check if we're at a natural break point for mode switching
# If we're near the end of the display duration and not at a clean break point,

View File

@@ -992,36 +992,36 @@
<div>
<div class="form-group">
<label for="rows">Rows:</label>
<input type="number" class="form-control" id="rows" name="rows" value="{{ main_config.display.hardware.rows }}" min="1" max="64">
<input type="number" class="form-control" id="rows" name="rows" value="{{ safe_config_get(main_config, 'display', 'hardware', 'rows', default=32) }}" min="1" max="64">
<div class="description">Number of LED rows</div>
</div>
<div class="form-group">
<label for="cols">Columns:</label>
<input type="number" class="form-control" id="cols" name="cols" value="{{ main_config.display.hardware.cols }}" min="1" max="128">
<input type="number" class="form-control" id="cols" name="cols" value="{{ safe_config_get(main_config, 'display', 'hardware', 'cols', default=64) }}" min="1" max="128">
<div class="description">Number of LED columns</div>
</div>
<div class="form-group">
<label for="chain_length">Chain Length:</label>
<input type="number" class="form-control" id="chain_length" name="chain_length" value="{{ main_config.display.hardware.chain_length }}" min="1" max="8">
<input type="number" class="form-control" id="chain_length" name="chain_length" value="{{ safe_config_get(main_config, 'display', 'hardware', 'chain_length', default=2) }}" min="1" max="8">
<div class="description">Number of LED panels chained together</div>
</div>
<div class="form-group">
<label for="parallel">Parallel:</label>
<input type="number" class="form-control" id="parallel" name="parallel" value="{{ main_config.display.hardware.parallel }}" min="1" max="4">
<input type="number" class="form-control" id="parallel" name="parallel" value="{{ safe_config_get(main_config, 'display', 'hardware', 'parallel', default=1) }}" min="1" max="4">
<div class="description">Number of parallel chains</div>
</div>
<div class="form-group">
<label for="brightness">Brightness:</label>
<input type="range" class="form-control" id="brightness" name="brightness" value="{{ main_config.display.hardware.brightness }}" min="1" max="100" oninput="updateBrightnessDisplay(this.value)">
<div class="description">LED brightness: <span id="brightness-value">{{ main_config.display.hardware.brightness }}</span>%</div>
<input type="range" class="form-control" id="brightness" name="brightness" value="{{ safe_config_get(main_config, 'display', 'hardware', 'brightness', default=95) }}" min="1" max="100" oninput="updateBrightnessDisplay(this.value)">
<div class="description">LED brightness: <span id="brightness-value">{{ safe_config_get(main_config, 'display', 'hardware', 'brightness', default=95) }}</span>%</div>
</div>
<div class="form-group">
<label for="hardware_mapping">Hardware Mapping:</label>
<select class="form-control" id="hardware_mapping" name="hardware_mapping">
<option value="adafruit-hat-pwm" {% if main_config.display.hardware.hardware_mapping == "adafruit-hat-pwm" %}selected{% endif %}>Adafruit HAT PWM</option>
<option value="adafruit-hat" {% if main_config.display.hardware.hardware_mapping == "adafruit-hat" %}selected{% endif %}>Adafruit HAT</option>
<option value="regular" {% if main_config.display.hardware.hardware_mapping == "regular" %}selected{% endif %}>Regular</option>
<option value="regular-pi1" {% if main_config.display.hardware.hardware_mapping == "regular-pi1" %}selected{% endif %}>Regular Pi1</option>
<option value="adafruit-hat-pwm" {% if safe_config_get(main_config, 'display', 'hardware', 'hardware_mapping', default='adafruit-hat-pwm') == "adafruit-hat-pwm" %}selected{% endif %}>Adafruit HAT PWM</option>
<option value="adafruit-hat" {% if safe_config_get(main_config, 'display', 'hardware', 'hardware_mapping', default='adafruit-hat-pwm') == "adafruit-hat" %}selected{% endif %}>Adafruit HAT</option>
<option value="regular" {% if safe_config_get(main_config, 'display', 'hardware', 'hardware_mapping', default='adafruit-hat-pwm') == "regular" %}selected{% endif %}>Regular</option>
<option value="regular-pi1" {% if safe_config_get(main_config, 'display', 'hardware', 'hardware_mapping', default='adafruit-hat-pwm') == "regular-pi1" %}selected{% endif %}>Regular Pi1</option>
</select>
<div class="description">Hardware mapping type</div>
</div>
@@ -1029,32 +1029,32 @@
<div>
<div class="form-group">
<label for="gpio_slowdown">GPIO Slowdown:</label>
<input type="number" class="form-control" id="gpio_slowdown" name="gpio_slowdown" value="{{ main_config.display.runtime.gpio_slowdown }}" min="0" max="5">
<input type="number" class="form-control" id="gpio_slowdown" name="gpio_slowdown" value="{{ safe_config_get(main_config, 'display', 'runtime', 'gpio_slowdown', default=3) }}" min="0" max="5">
<div class="description">GPIO slowdown factor (0-5)</div>
</div>
<div class="form-group">
<label for="scan_mode">Scan Mode:</label>
<input type="number" class="form-control" id="scan_mode" name="scan_mode" value="{{ main_config.display.hardware.scan_mode }}" min="0" max="1">
<input type="number" class="form-control" id="scan_mode" name="scan_mode" value="{{ safe_config_get(main_config, 'display', 'hardware', 'scan_mode', default=0) }}" min="0" max="1">
<div class="description">Scan mode for LED matrix (0-1)</div>
</div>
<div class="form-group">
<label for="pwm_bits">PWM Bits:</label>
<input type="number" class="form-control" id="pwm_bits" name="pwm_bits" value="{{ main_config.display.hardware.pwm_bits }}" min="1" max="11">
<input type="number" class="form-control" id="pwm_bits" name="pwm_bits" value="{{ safe_config_get(main_config, 'display', 'hardware', 'pwm_bits', default=9) }}" min="1" max="11">
<div class="description">PWM bits for brightness control (1-11)</div>
</div>
<div class="form-group">
<label for="pwm_dither_bits">PWM Dither Bits:</label>
<input type="number" class="form-control" id="pwm_dither_bits" name="pwm_dither_bits" value="{{ main_config.display.hardware.pwm_dither_bits }}" min="0" max="4">
<input type="number" class="form-control" id="pwm_dither_bits" name="pwm_dither_bits" value="{{ safe_config_get(main_config, 'display', 'hardware', 'pwm_dither_bits', default=1) }}" min="0" max="4">
<div class="description">PWM dither bits (0-4)</div>
</div>
<div class="form-group">
<label for="pwm_lsb_nanoseconds">PWM LSB Nanoseconds:</label>
<input type="number" class="form-control" id="pwm_lsb_nanoseconds" name="pwm_lsb_nanoseconds" value="{{ main_config.display.hardware.pwm_lsb_nanoseconds }}" min="50" max="500">
<input type="number" class="form-control" id="pwm_lsb_nanoseconds" name="pwm_lsb_nanoseconds" value="{{ safe_config_get(main_config, 'display', 'hardware', 'pwm_lsb_nanoseconds', default=130) }}" min="50" max="500">
<div class="description">PWM LSB nanoseconds (50-500)</div>
</div>
<div class="form-group">
<label for="limit_refresh_rate_hz">Limit Refresh Rate (Hz):</label>
<input type="number" class="form-control" id="limit_refresh_rate_hz" name="limit_refresh_rate_hz" value="{{ main_config.display.hardware.limit_refresh_rate_hz }}" min="1" max="1000">
<input type="number" class="form-control" id="limit_refresh_rate_hz" name="limit_refresh_rate_hz" value="{{ safe_config_get(main_config, 'display', 'hardware', 'limit_refresh_rate_hz', default=120) }}" min="1" max="1000">
<div class="description">Limit refresh rate in Hz (1-1000)</div>
</div>
</div>
@@ -1063,28 +1063,28 @@
<div class="form-row">
<div class="form-group">
<label>
<input type="checkbox" id="disable_hardware_pulsing" name="disable_hardware_pulsing" {% if main_config.display.hardware.disable_hardware_pulsing %}checked{% endif %}>
<input type="checkbox" id="disable_hardware_pulsing" name="disable_hardware_pulsing" {% if safe_config_get(main_config, 'display', 'hardware', 'disable_hardware_pulsing', default=False) %}checked{% endif %}>
Disable Hardware Pulsing
</label>
<div class="description">Disable hardware pulsing</div>
</div>
<div class="form-group">
<label>
<input type="checkbox" id="inverse_colors" name="inverse_colors" {% if main_config.display.hardware.inverse_colors %}checked{% endif %}>
<input type="checkbox" id="inverse_colors" name="inverse_colors" {% if safe_config_get(main_config, 'display', 'hardware', 'inverse_colors', default=False) %}checked{% endif %}>
Inverse Colors
</label>
<div class="description">Inverse color display</div>
</div>
<div class="form-group">
<label>
<input type="checkbox" id="show_refresh_rate" name="show_refresh_rate" {% if main_config.display.hardware.show_refresh_rate %}checked{% endif %}>
<input type="checkbox" id="show_refresh_rate" name="show_refresh_rate" {% if safe_config_get(main_config, 'display', 'hardware', 'show_refresh_rate', default=False) %}checked{% endif %}>
Show Refresh Rate
</label>
<div class="description">Show refresh rate on display</div>
</div>
<div class="form-group">
<label>
<input type="checkbox" id="use_short_date_format" name="use_short_date_format" {% if main_config.display.use_short_date_format %}checked{% endif %}>
<input type="checkbox" id="use_short_date_format" name="use_short_date_format" {% if safe_config_get(main_config, 'display', 'use_short_date_format', default=True) %}checked{% endif %}>
Use Short Date Format
</label>
<div class="description">Use short date format for display</div>

197
test_config_display.py Normal file
View File

@@ -0,0 +1,197 @@
#!/usr/bin/env python3
"""
Test script to verify the safe_config_get function and template logic works correctly.
"""
import json
import sys
import os
# Add the src directory to Python path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
class DictWrapper:
"""Wrapper to make dictionary accessible via dot notation for Jinja2 templates."""
def __init__(self, data=None):
# Store the original data
object.__setattr__(self, '_data', data if isinstance(data, dict) else {})
# Set attributes from the dictionary
if isinstance(data, dict):
for key, value in data.items():
if isinstance(value, dict):
object.__setattr__(self, key, DictWrapper(value))
elif isinstance(value, list):
object.__setattr__(self, key, value)
else:
object.__setattr__(self, key, value)
def __getattr__(self, name):
# Return a new empty DictWrapper for missing attributes
# This allows chaining like main_config.display.hardware.rows
return DictWrapper({})
def __str__(self):
# Return empty string for missing values to avoid template errors
data = object.__getattribute__(self, '_data')
if not data:
return ''
return str(data)
def __int__(self):
# Return 0 for missing numeric values
data = object.__getattribute__(self, '_data')
if not data:
return 0
try:
return int(data)
except (ValueError, TypeError):
return 0
def __bool__(self):
# Return False for missing boolean values
data = object.__getattribute__(self, '_data')
if not data:
return False
return bool(data)
def get(self, key, default=None):
# Support .get() method like dictionaries
data = object.__getattribute__(self, '_data')
if data and key in data:
return data[key]
return default
def safe_config_get(config, *keys, default=''):
"""Safely get nested config values with fallback."""
try:
current = config
for key in keys:
if hasattr(current, key):
current = getattr(current, key)
# Check if we got an empty DictWrapper
if isinstance(current, DictWrapper):
data = object.__getattribute__(current, '_data')
if not data: # Empty DictWrapper means missing config
return default
elif isinstance(current, dict) and key in current:
current = current[key]
else:
return default
# Final check for empty values
if current is None or (hasattr(current, '_data') and not object.__getattribute__(current, '_data')):
return default
return current
except (AttributeError, KeyError, TypeError):
return default
def test_config_access():
"""Test the safe config access with actual config data."""
print("Testing safe_config_get function...")
# Load the actual config
try:
with open('config/config.json', 'r') as f:
config_data = json.load(f)
print("✓ Successfully loaded config.json")
except Exception as e:
print(f"✗ Failed to load config.json: {e}")
return False
# Wrap the config
main_config = DictWrapper(config_data)
print("✓ Successfully wrapped config in DictWrapper")
# Test critical configuration values
test_cases = [
('display.hardware.rows', 32),
('display.hardware.cols', 64),
('display.hardware.brightness', 95),
('display.hardware.chain_length', 2),
('display.hardware.parallel', 1),
('display.hardware.hardware_mapping', 'adafruit-hat-pwm'),
('display.runtime.gpio_slowdown', 3),
('display.hardware.scan_mode', 0),
('display.hardware.pwm_bits', 9),
('display.hardware.pwm_dither_bits', 1),
('display.hardware.pwm_lsb_nanoseconds', 130),
('display.hardware.limit_refresh_rate_hz', 120),
('display.hardware.disable_hardware_pulsing', False),
('display.hardware.inverse_colors', False),
('display.hardware.show_refresh_rate', False),
('display.use_short_date_format', True),
]
print("\nTesting configuration value access:")
all_passed = True
for key_path, expected_default in test_cases:
keys = key_path.split('.')
# Test safe_config_get function
result = safe_config_get(main_config, *keys, default=expected_default)
# Test direct access (old way) for comparison
try:
direct_result = main_config
for key in keys:
direct_result = getattr(direct_result, key)
direct_success = True
except AttributeError:
direct_result = None
direct_success = False
status = "" if result is not None else ""
print(f" {status} {key_path}: {result} (direct: {direct_result if direct_success else 'FAILED'})")
if result is None:
all_passed = False
return all_passed
def test_missing_config():
"""Test behavior with missing configuration sections."""
print("\nTesting with missing configuration sections...")
# Create a config with missing sections
incomplete_config = {
"timezone": "America/Chicago",
# Missing display section entirely
}
main_config = DictWrapper(incomplete_config)
# Test that safe_config_get returns defaults for missing sections
test_cases = [
('display.hardware.rows', 32),
('display.hardware.cols', 64),
('display.hardware.brightness', 95),
]
all_passed = True
for key_path, expected_default in test_cases:
keys = key_path.split('.')
result = safe_config_get(main_config, *keys, default=expected_default)
status = "" if result == expected_default else ""
print(f" {status} {key_path}: {result} (expected default: {expected_default})")
if result != expected_default:
all_passed = False
return all_passed
if __name__ == "__main__":
print("=" * 60)
print("Testing Web Interface Configuration Display")
print("=" * 60)
success1 = test_config_access()
success2 = test_missing_config()
print("\n" + "=" * 60)
if success1 and success2:
print("✓ ALL TESTS PASSED - Web interface should display config correctly!")
else:
print("✗ SOME TESTS FAILED - There may be issues with config display")
print("=" * 60)

View File

@@ -38,6 +38,57 @@ import logging
app = Flask(__name__)
app.secret_key = os.urandom(24)
# Custom Jinja2 filter for safe nested dictionary access
@app.template_filter('safe_get')
def safe_get(obj, key_path, default=''):
"""Safely access nested dictionary values using dot notation.
Usage: {{ main_config|safe_get('display.hardware.brightness', 95) }}
"""
try:
keys = key_path.split('.')
current = obj
for key in keys:
if hasattr(current, key):
current = getattr(current, key)
elif isinstance(current, dict) and key in current:
current = current[key]
else:
return default
return current if current is not None else default
except (AttributeError, KeyError, TypeError):
return default
# Template context processor to provide safe access methods
@app.context_processor
def inject_safe_access():
"""Inject safe access methods into template context."""
def safe_config_get(config, *keys, default=''):
"""Safely get nested config values with fallback."""
try:
current = config
for key in keys:
if hasattr(current, key):
current = getattr(current, key)
# Check if we got an empty DictWrapper
if isinstance(current, DictWrapper):
data = object.__getattribute__(current, '_data')
if not data: # Empty DictWrapper means missing config
return default
elif isinstance(current, dict) and key in current:
current = current[key]
else:
return default
# Final check for empty values
if current is None or (hasattr(current, '_data') and not object.__getattribute__(current, '_data')):
return default
return current
except (AttributeError, KeyError, TypeError):
return default
return dict(safe_config_get=safe_config_get)
# Prefer eventlet when available, but allow forcing threading via env for troubleshooting
force_threading = os.getenv('USE_THREADING', '0') == '1' or os.getenv('FORCE_THREADING', '0') == '1'
if force_threading:
@@ -83,6 +134,30 @@ class DictWrapper:
# This allows chaining like main_config.display.hardware.rows
return DictWrapper({})
def __str__(self):
# Return empty string for missing values to avoid template errors
data = object.__getattribute__(self, '_data')
if not data:
return ''
return str(data)
def __int__(self):
# Return 0 for missing numeric values
data = object.__getattribute__(self, '_data')
if not data:
return 0
try:
return int(data)
except (ValueError, TypeError):
return 0
def __bool__(self):
# Return False for missing boolean values
data = object.__getattribute__(self, '_data')
if not data:
return False
return bool(data)
def __getitem__(self, key):
# Support bracket notation
return getattr(self, key, DictWrapper({}))
@@ -95,24 +170,26 @@ class DictWrapper:
return {}.items()
def get(self, key, default=None):
# Support .get() method
# Support .get() method like dictionaries
data = object.__getattribute__(self, '_data')
if data and key in data:
value = data[key]
if isinstance(value, dict):
return DictWrapper(value)
return value
return data[key]
return default
def has_key(self, key):
# Check if key exists
data = object.__getattribute__(self, '_data')
return data and key in data
def keys(self):
# Support .keys() method
data = object.__getattribute__(self, '_data')
return data.keys() if data else [].keys()
return data.keys() if data else []
def values(self):
# Support .values() method
data = object.__getattribute__(self, '_data')
return data.values() if data else [].values()
return data.values() if data else []
def __str__(self):
# Return empty string for missing values to avoid template errors