diff --git a/plugins.json b/plugins.json index f96d67e8..e733a6e9 100644 --- a/plugins.json +++ b/plugins.json @@ -1,6 +1,6 @@ { "version": "1.0.0", - "last_updated": "2026-05-31", + "last_updated": "2026-06-02", "plugins": [ { "id": "7-segment-clock", @@ -579,7 +579,7 @@ { "id": "soccer-scoreboard", "name": "Soccer Scoreboard", - "description": "Live, recent, and upcoming soccer games across multiple leagues including Premier League, La Liga, Bundesliga, Serie A, Ligue 1, MLS, Liga Portugal, and more", + "description": "Live, recent, and upcoming soccer games across multiple leagues including Premier League, La Liga, Bundesliga, Serie A, Ligue 1, MLS, Liga Portugal, Champions League, Europa League, and FIFA World Cup", "author": "ChuckBuilds", "category": "sports", "tags": [ @@ -591,6 +591,8 @@ "serie-a", "ligue-1", "mls", + "world-cup", + "fifa", "sports", "scoreboard", "live-scores" @@ -600,10 +602,10 @@ "plugin_path": "plugins/soccer-scoreboard", "stars": 0, "downloads": 0, - "last_updated": "2026-05-20", + "last_updated": "2026-06-02", "verified": true, "screenshot": "", - "latest_version": "1.6.3" + "latest_version": "1.7.1" }, { "id": "static-image", diff --git a/plugins/soccer-scoreboard/README.md b/plugins/soccer-scoreboard/README.md index 5deb7da4..8aac0386 100644 --- a/plugins/soccer-scoreboard/README.md +++ b/plugins/soccer-scoreboard/README.md @@ -13,9 +13,7 @@ # Soccer Scoreboard Plugin -### This version is still under development. It should be mostly functional but the custom league upload module is not working. - -A plugin for LEDMatrix that displays live, recent, and upcoming soccer games across multiple leagues including Premier League, La Liga, Bundesliga, Serie A, Ligue 1, MLS, and more. +A plugin for LEDMatrix that displays live, recent, and upcoming soccer games across multiple leagues including Premier League, La Liga, Bundesliga, Serie A, Ligue 1, MLS, and FIFA World Cup. ## Features @@ -175,8 +173,27 @@ The plugin supports the following soccer leagues: - **ita.1**: Serie A (Italy) - **fra.1**: Ligue 1 (France) - **usa.1**: MLS (USA) +- **por.1**: Liga Portugal (Portugal) - **uefa.champions**: UEFA Champions League - **uefa.europa**: UEFA Europa League +- **fifa.world**: FIFA World Cup + +Additional leagues can be added via the **Custom Leagues** setting using any ESPN soccer league code (e.g. `mex.1`, `arg.1`, `bra.1`, `ned.1`). + +## FIFA World Cup + +Enable the **FIFA World Cup** league from the plugin settings to track World Cup 2026 (June 11 – July 19, USA/Canada/Mexico). + +**To follow all games:** Enable `fifa.world` and leave `Show Favorite Teams Only` off. + +**To follow just your country:** Enable `fifa.world`, set `Favorite Teams` to your country's ESPN abbreviation (e.g. `USA`, `ENG`, `BRA`), and enable `Show Favorite Teams Only`. + +During knockout rounds, the status area shows: +- **ET1** / **ET2** — Extra Time first / second half +- **ETH** — Halftime of Extra Time +- **PEN** — Penalty Shootout in progress +- **F/ET** — Final, decided in Extra Time +- **F/Pen** — Final, decided on Penalties ## Team Names & Abbreviations diff --git a/plugins/soccer-scoreboard/TEAMS.md b/plugins/soccer-scoreboard/TEAMS.md index e2ad2fc7..83cb3582 100644 --- a/plugins/soccer-scoreboard/TEAMS.md +++ b/plugins/soccer-scoreboard/TEAMS.md @@ -212,6 +212,58 @@ Same as Champions League — use the club's domestic abbreviation from the table --- +## FIFA World Cup 2026 (fifa.world) + +> **⚠️ Verify abbreviations via debug logs.** ESPN's internal abbreviations for national teams are not always the same as FIFA country codes. Enable debug logging and check the `home_abbr`/`away_abbr` values logged for each game to confirm the exact codes the ESPN feed uses before setting `favorite_teams`. + +The table below lists high-confidence ESPN abbreviations based on prior international tournament data. Entries marked with `*` should be treated as approximate until confirmed from the live feed. + +| Country | Abbreviation | +|---------|-------------| +| Argentina | ARG | +| Australia | AUS | +| Belgium | BEL | +| Brazil | BRA | +| Cameroon | CMR | +| Canada | CAN | +| Chile | CHI | +| Colombia | COL | +| Costa Rica | CRC | +| Croatia | CRO | +| Czech Republic | CZE | +| Ecuador | ECU | +| Egypt | EGY | +| England | ENG | +| France | FRA | +| Germany | GER | +| Ghana | GHA | +| Hungary | HUN | +| Iran | IRN | +| Japan | JPN | +| Mali | MLI * | +| Mexico | MEX | +| Morocco | MAR | +| Netherlands | NED | +| New Zealand | NZL | +| Nigeria | NGA | +| Panama | PAN | +| Paraguay | PAR | +| Portugal | POR | +| Saudi Arabia | KSA | +| Senegal | SEN | +| Serbia | SRB | +| Slovakia | SVK | +| South Korea | KOR | +| Spain | ESP | +| Switzerland | SUI | +| Tunisia | TUN | +| United States | USA | +| Uruguay | URU | + +To find your team's abbreviation: enable the plugin, set `debug` log level, then look for lines like `Extracted: USA@MEX` in the logs — those are the exact codes ESPN returns. + +--- + ## Tips - **Abbreviations are case-sensitive** — use uppercase as shown (e.g. `"LIV"` not `"liv"`) diff --git a/plugins/soccer-scoreboard/config_schema.json b/plugins/soccer-scoreboard/config_schema.json index e03b1e72..99c110b1 100644 --- a/plugins/soccer-scoreboard/config_schema.json +++ b/plugins/soccer-scoreboard/config_schema.json @@ -465,7 +465,9 @@ } } }, - "additionalProperties": false + "additionalProperties": false, + "title": "Premier League", + "description": "Settings for the Premier League (England)" }, "esp.1": { "type": "object", @@ -798,7 +800,9 @@ } } }, - "additionalProperties": false + "additionalProperties": false, + "title": "La Liga", + "description": "Settings for La Liga (Spain)" }, "ger.1": { "type": "object", @@ -1131,7 +1135,9 @@ } } }, - "additionalProperties": false + "additionalProperties": false, + "title": "Bundesliga", + "description": "Settings for the Bundesliga (Germany)" }, "ita.1": { "type": "object", @@ -1464,7 +1470,9 @@ } } }, - "additionalProperties": false + "additionalProperties": false, + "title": "Serie A", + "description": "Settings for Serie A (Italy)" }, "fra.1": { "type": "object", @@ -1797,7 +1805,9 @@ } } }, - "additionalProperties": false + "additionalProperties": false, + "title": "Ligue 1", + "description": "Settings for Ligue 1 (France)" }, "usa.1": { "type": "object", @@ -2130,7 +2140,9 @@ } } }, - "additionalProperties": false + "additionalProperties": false, + "title": "MLS", + "description": "Settings for Major League Soccer (USA)" }, "uefa.champions": { "type": "object", @@ -2463,7 +2475,9 @@ } } }, - "additionalProperties": false + "additionalProperties": false, + "title": "Champions League", + "description": "Settings for the UEFA Champions League" }, "uefa.europa": { "type": "object", @@ -2796,7 +2810,9 @@ } } }, - "additionalProperties": false + "additionalProperties": false, + "title": "Europa League", + "description": "Settings for the UEFA Europa League" }, "por.1": { "type": "object", @@ -3129,7 +3145,344 @@ } } }, - "additionalProperties": false + "additionalProperties": false, + "title": "Liga Portugal", + "description": "Settings for Liga Portugal (Portugal)" + }, + "fifa.world": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false, + "description": "Enable FIFA World Cup games" + }, + "favorite_teams": { + "type": "array", + "items": { + "type": "string", + "description": "National team abbreviation (e.g. USA, BRA, ARG, ENG)" + }, + "uniqueItems": true, + "maxItems": 20, + "description": "List of favorite national team abbreviations to prioritize (e.g. USA, BRA, ARG)" + }, + "display_modes": { + "type": "object", + "title": "Display Modes", + "description": "Control which game types to show and how they are displayed", + "properties": { + "live": { + "type": "boolean", + "default": true, + "description": "Show live FIFA World Cup games" + }, + "live_display_mode": { + "type": "string", + "enum": [ + "switch", + "scroll" + ], + "default": "switch", + "description": "Display mode for live games: 'switch' shows one game at a time, 'scroll' scrolls all games horizontally" + }, + "recent": { + "type": "boolean", + "default": true, + "description": "Show recently completed FIFA World Cup games" + }, + "recent_display_mode": { + "type": "string", + "enum": [ + "switch", + "scroll" + ], + "default": "switch", + "description": "Display mode for recent games: 'switch' shows one game at a time, 'scroll' scrolls all games horizontally" + }, + "upcoming": { + "type": "boolean", + "default": true, + "description": "Show upcoming FIFA World Cup games" + }, + "upcoming_display_mode": { + "type": "string", + "enum": [ + "switch", + "scroll" + ], + "default": "switch", + "description": "Display mode for upcoming games: 'switch' shows one game at a time, 'scroll' scrolls all games horizontally" + } + }, + "additionalProperties": false + }, + "scroll_settings": { + "type": "object", + "title": "Scroll Settings", + "description": "Settings for scroll display mode (when display mode is set to 'scroll')", + "properties": { + "scroll_speed": { + "type": "number", + "default": 1.0, + "minimum": 0.01, + "maximum": 200.0, + "description": "Scroll speed in pixels per second" + }, + "scroll_delay": { + "type": "number", + "default": 0.01, + "minimum": 0.001, + "maximum": 0.1, + "description": "Delay between scroll frames in seconds (lower = smoother)" + }, + "gap_between_games": { + "type": "integer", + "default": 48, + "minimum": 8, + "maximum": 128, + "description": "Gap in pixels between game cards when scrolling" + }, + "show_league_separators": { + "type": "boolean", + "default": true, + "description": "Show league icons between different leagues" + }, + "dynamic_duration": { + "type": "boolean", + "default": true, + "description": "Automatically calculate display duration based on content width" + }, + "game_card_width": { + "type": "integer", + "default": 128, + "minimum": 32, + "maximum": 512, + "description": "Width of each game card in scroll mode (pixels). Default 128. Set lower on multi-panel chains to show more games simultaneously." + } + } + }, + "live_priority": { + "type": "boolean", + "default": true, + "description": "Give live games priority over other modes. When enabled, live games will interrupt the normal mode rotation and be displayed immediately when available." + }, + "live_game_duration": { + "type": "integer", + "default": 20, + "minimum": 10, + "maximum": 120, + "description": "Duration in seconds to display each live game before rotating to the next live game" + }, + "recent_game_duration": { + "type": "integer", + "default": 15, + "minimum": 5, + "maximum": 60, + "description": "Duration in seconds to display each recent game before rotating to the next game" + }, + "upcoming_game_duration": { + "type": "integer", + "default": 15, + "minimum": 5, + "maximum": 60, + "description": "Duration in seconds to display each upcoming game before rotating to the next game" + }, + "live_update_interval": { + "type": "integer", + "default": 30, + "minimum": 5, + "maximum": 300, + "description": "How often to update live game data (seconds)" + }, + "update_interval_seconds": { + "type": "integer", + "default": 3600, + "minimum": 30, + "maximum": 86400, + "description": "How often to fetch new data (seconds)" + }, + "game_limits": { + "type": "object", + "title": "Game Limits", + "description": "Control how many games to show", + "properties": { + "recent_games_to_show": { + "type": "integer", + "default": 1, + "minimum": 1, + "maximum": 20, + "description": "With favorites: N games per favorite team. Without favorites: N total games sorted by time." + }, + "upcoming_games_to_show": { + "type": "integer", + "default": 1, + "minimum": 1, + "maximum": 20, + "description": "With favorites: N games per favorite team. Without favorites: N total games sorted by time." + } + } + }, + "display_options": { + "type": "object", + "title": "Display Options", + "description": "Additional information to show", + "properties": { + "show_records": { + "type": "boolean", + "default": false, + "description": "Show team records (wins-losses)" + }, + "show_ranking": { + "type": "boolean", + "default": false, + "description": "Show team rankings (when available)" + }, + "show_odds": { + "type": "boolean", + "default": true, + "description": "Show betting odds" + } + } + }, + "filtering": { + "type": "object", + "title": "Filtering Options", + "description": "Control which teams are shown", + "properties": { + "show_favorite_teams_only": { + "type": "boolean", + "default": true, + "description": "Only show games from favorite teams" + }, + "show_all_live": { + "type": "boolean", + "default": false, + "description": "Show all live games, not just favorites" + } + } + }, + "dynamic_duration": { + "type": "object", + "title": "FIFA World Cup Dynamic Duration Settings", + "description": "Configure dynamic duration settings for FIFA World Cup games.", + "properties": { + "enabled": { + "type": "boolean", + "default": false, + "description": "Enable dynamic duration for FIFA World Cup games" + }, + "min_duration_seconds": { + "type": "number", + "minimum": 10, + "maximum": 300, + "default": 30, + "description": "Minimum total duration in seconds for this mode, even if few games are available" + }, + "max_duration_seconds": { + "type": "number", + "minimum": 60, + "maximum": 600, + "description": "Max duration for FIFA World Cup games" + }, + "modes": { + "type": "object", + "title": "Per-Mode Settings for FIFA World Cup", + "description": "Configure dynamic duration for specific FIFA World Cup modes", + "properties": { + "live": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false, + "description": "Enable dynamic duration for FIFA World Cup live games" + }, + "max_duration_seconds": { + "type": "number", + "minimum": 60, + "maximum": 600, + "description": "Max duration for FIFA World Cup live games" + } + } + }, + "recent": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false, + "description": "Enable dynamic duration for FIFA World Cup recent games" + }, + "max_duration_seconds": { + "type": "number", + "minimum": 60, + "maximum": 600, + "description": "Max duration for FIFA World Cup recent games" + } + } + }, + "upcoming": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false, + "description": "Enable dynamic duration for FIFA World Cup upcoming games" + }, + "max_duration_seconds": { + "type": "number", + "minimum": 60, + "maximum": 600, + "description": "Max duration for FIFA World Cup upcoming games" + } + } + } + } + } + } + }, + "mode_durations": { + "type": "object", + "title": "Mode-Level Durations", + "description": "Control total duration for each mode type. If not set, uses dynamic calculation (total_games \u00d7 per_game_duration).", + "properties": { + "recent_mode_duration": { + "type": [ + "number", + "null" + ], + "default": null, + "minimum": 10, + "maximum": 600, + "description": "Total duration in seconds for Recent mode before rotating to next mode. Default: null (uses dynamic calculation)." + }, + "upcoming_mode_duration": { + "type": [ + "number", + "null" + ], + "default": null, + "minimum": 10, + "maximum": 600, + "description": "Total duration in seconds for Upcoming mode before rotating to next mode. Default: null (uses dynamic calculation)." + }, + "live_mode_duration": { + "type": [ + "number", + "null" + ], + "default": null, + "minimum": 10, + "maximum": 600, + "description": "Total duration in seconds for Live mode before rotating to next mode. Default: null (uses dynamic calculation)." + } + } + } + }, + "additionalProperties": false, + "title": "FIFA World Cup", + "description": "Settings for the FIFA World Cup (fifa.world)" } }, "additionalProperties": false @@ -3793,4 +4146,4 @@ "custom_leagues", "leagues" ] -} +} \ No newline at end of file diff --git a/plugins/soccer-scoreboard/manager.py b/plugins/soccer-scoreboard/manager.py index bf7b2848..454ee715 100644 --- a/plugins/soccer-scoreboard/manager.py +++ b/plugins/soccer-scoreboard/manager.py @@ -60,6 +60,7 @@ create_champions_league_managers, create_europa_league_managers, create_liga_portugal_managers, + create_world_cup_managers, create_custom_league_managers, ) @@ -67,7 +68,7 @@ # Predefined league keys and display names (priority 1-8) # Custom leagues will be added dynamically with user-defined priorities -PREDEFINED_LEAGUE_KEYS = ['eng.1', 'esp.1', 'ger.1', 'ita.1', 'fra.1', 'usa.1', 'por.1', 'uefa.champions', 'uefa.europa'] +PREDEFINED_LEAGUE_KEYS = ['eng.1', 'esp.1', 'ger.1', 'ita.1', 'fra.1', 'usa.1', 'por.1', 'uefa.champions', 'uefa.europa', 'fifa.world'] PREDEFINED_LEAGUE_NAMES = { 'eng.1': 'Premier League', 'esp.1': 'La Liga', @@ -77,7 +78,8 @@ 'usa.1': 'MLS', 'por.1': 'Liga Portugal', 'uefa.champions': 'Champions League', - 'uefa.europa': 'Europa League' + 'uefa.europa': 'Europa League', + 'fifa.world': 'FIFA World Cup', } # Default priorities for predefined leagues (lower = higher priority, shows first) @@ -91,6 +93,7 @@ 'por.1': 7, 'uefa.champions': 8, 'uefa.europa': 9, + 'fifa.world': 10, } # League key -> (live_attr, recent_attr, upcoming_attr) for predefined leagues @@ -104,6 +107,7 @@ 'por.1': ('por1_live', 'por1_recent', 'por1_upcoming'), 'uefa.champions': ('champions_live', 'champions_recent', 'champions_upcoming'), 'uefa.europa': ('europa_live', 'europa_recent', 'europa_upcoming'), + 'fifa.world': ('world_cup_live', 'world_cup_recent', 'world_cup_upcoming'), } # Legacy aliases for backwards compatibility. LEAGUE_NAMES is a mutable copy that @@ -341,7 +345,11 @@ def _initialize_managers(self): self.europa_live, self.europa_recent, self.europa_upcoming = create_europa_league_managers( league_config, self.display_manager, self.cache_manager ) - + elif league_key == 'fifa.world': + self.world_cup_live, self.world_cup_recent, self.world_cup_upcoming = create_world_cup_managers( + league_config, self.display_manager, self.cache_manager + ) + self.logger.info(f"{LEAGUE_NAMES[league_key]} managers initialized") except Exception as e: @@ -1784,7 +1792,7 @@ def get_info(self) -> Dict[str, Any]: info = { "plugin_id": self.plugin_id, "name": "Soccer Scoreboard", - "version": "1.6.0", + "version": "1.7.1", "enabled": self.is_enabled, "display_size": f"{self.display_width}x{self.display_height}", "leagues": league_info, diff --git a/plugins/soccer-scoreboard/manifest.json b/plugins/soccer-scoreboard/manifest.json index a0cf3180..1bdb58cd 100644 --- a/plugins/soccer-scoreboard/manifest.json +++ b/plugins/soccer-scoreboard/manifest.json @@ -1,9 +1,9 @@ { "id": "soccer-scoreboard", "name": "Soccer Scoreboard", - "version": "1.6.3", + "version": "1.7.1", "author": "ChuckBuilds", - "description": "Live, recent, and upcoming soccer games across multiple leagues including Premier League, La Liga, Bundesliga, Serie A, Ligue 1, MLS, Liga Portugal, and more", + "description": "Live, recent, and upcoming soccer games across multiple leagues including Premier League, La Liga, Bundesliga, Serie A, Ligue 1, MLS, Liga Portugal, Champions League, Europa League, and FIFA World Cup", "category": "sports", "tags": [ "soccer", @@ -14,6 +14,8 @@ "serie-a", "ligue-1", "mls", + "world-cup", + "fifa", "sports", "scoreboard", "live-scores" @@ -24,6 +26,16 @@ "soccer_upcoming" ], "versions": [ + { + "released": "2026-06-02", + "version": "1.7.1", + "ledmatrix_min": "2.0.0" + }, + { + "released": "2026-06-02", + "version": "1.7.0", + "ledmatrix_min": "2.0.0" + }, { "released": "2026-05-20", "version": "1.6.3", @@ -90,7 +102,7 @@ "ledmatrix_min_version": "2.0.0" } ], - "last_updated": "2026-05-20", + "last_updated": "2026-06-02", "stars": 0, "downloads": 0, "verified": true, @@ -107,4 +119,4 @@ "compatible_versions": [ ">=2.0.0" ] -} +} \ No newline at end of file diff --git a/plugins/soccer-scoreboard/soccer_managers.py b/plugins/soccer-scoreboard/soccer_managers.py index 5a825b00..6ed8ebf7 100644 --- a/plugins/soccer-scoreboard/soccer_managers.py +++ b/plugins/soccer-scoreboard/soccer_managers.py @@ -26,7 +26,8 @@ 'usa.1': 'MLS', 'por.1': 'Liga Portugal', 'uefa.champions': 'Champions League', - 'uefa.europa': 'Europa League' + 'uefa.europa': 'Europa League', + 'fifa.world': 'FIFA World Cup', } @@ -207,19 +208,33 @@ def _extract_game_details(self, game_event: Dict) -> Optional[Dict]: period_text = "" status_state = status["type"]["state"] - if status_state == "in": + status_name = status["type"]["name"] + if status_state == "halftime" or status_name == "STATUS_HALFTIME": + # Check halftime first: ESPN can set state="in" AND name="STATUS_HALFTIME" + # simultaneously, so this guard must precede the generic "in" branch. + period_text = "ETH" if period >= 3 else "HALF" + elif status_state == "in": if period == 0: period_text = "Start" elif period == 1: period_text = "1H" elif period == 2: period_text = "2H" + elif period == 3: + period_text = "ET1" # Extra Time 1st half + elif period == 4: + period_text = "ET2" # Extra Time 2nd half + elif period >= 5: + period_text = "PEN" # Penalty shootout else: - period_text = f"{period}H" - elif status_state == "halftime" or status["type"]["name"] == "STATUS_HALFTIME": - period_text = "HALF" + period_text = f"P{period}" elif status_state == "post": - period_text = "Final" + if status_name in ("STATUS_FINAL_PEN", "STATUS_AFTER_PENALTIES"): + period_text = "F/Pen" + elif status_name in ("STATUS_FINAL_AET", "STATUS_AFTER_EXTRA_TIME") or period > 2: + period_text = "F/ET" + else: + period_text = "Final" elif status_state == "pre": period_text = details.get("game_time", "") @@ -403,6 +418,15 @@ def create_europa_league_managers(config, display_manager, cache_manager): ) +def create_world_cup_managers(config, display_manager, cache_manager): + """Create FIFA World Cup (fifa.world) managers.""" + return ( + SoccerLiveManager(config, display_manager, cache_manager, 'fifa.world'), + SoccerRecentManager(config, display_manager, cache_manager, 'fifa.world'), + SoccerUpcomingManager(config, display_manager, cache_manager, 'fifa.world'), + ) + + def create_custom_league_managers( league_code: str, league_name: str, diff --git a/plugins/tide-display/manager.py b/plugins/tide-display/manager.py index b2bf220a..c127455f 100644 --- a/plugins/tide-display/manager.py +++ b/plugins/tide-display/manager.py @@ -65,8 +65,10 @@ def _lerp(c1, c2, t): return tuple(int(a + (b - a) * max(0.0, min(1.0, t))) for a, b in zip(c1, c2)) def _safe_iso(s): - try: return datetime.fromisoformat(s) - except Exception: return None + try: + return datetime.fromisoformat(s) + except (TypeError, ValueError): + return None # ── Layout helper ────────────────────────────────────────────────────────────── @@ -100,8 +102,10 @@ def __init__(self, plugin_id, config, display_manager, cache_manager, plugin_man super().__init__(plugin_id, config, display_manager, cache_manager, plugin_manager) def _rgb(k, d): - try: return tuple(max(0, min(255, int(c))) for c in config.get(k, list(d))) - except Exception: return d + try: + return tuple(max(0, min(255, int(c))) for c in config.get(k, list(d))) + except (TypeError, ValueError): + return d self.station_id = str(config.get('station_id', '')).strip() self.station_name = str(config.get('station_name', '') or '').strip() @@ -192,11 +196,15 @@ def _fetch_hilo(self, u): if 'error' in d: return None out = [] for x in d.get('predictions', []): - try: out.append({'dt':datetime.strptime(x['t'],'%Y-%m-%d %H:%M').isoformat(), - 'height':float(x['v']),'type':x.get('type','?')}) - except Exception as _e: self.logger.debug("hilo entry skip: %s", _e) + try: + out.append({'dt': datetime.strptime(x['t'], '%Y-%m-%d %H:%M').isoformat(), + 'height': float(x['v']), 'type': x.get('type', '?')}) + except (KeyError, TypeError, ValueError) as _e: + self.logger.debug("hilo entry skip: %s", _e) return out or None - except Exception as e: self.logger.error("hilo: %s", e); return None + except (requests.exceptions.RequestException, OSError, ValueError) as e: + self.logger.error("hilo: %s", e, exc_info=True) + return None def _fetch_hourly(self, u): try: @@ -207,12 +215,16 @@ def _fetch_hourly(self, u): if 'error' in d: return None h = [] for x in d.get('predictions',[]): - try: h.append(float(x['v'])) - except Exception: h.append(0.0) + try: + h.append(float(x['v'])) + except (KeyError, TypeError, ValueError): + h.append(0.0) h = h[:24] while len(h) < 24: h.append(h[-1] if h else 0.0) return h - except Exception as e: self.logger.error("hourly: %s", e); return None + except (requests.exceptions.RequestException, OSError, ValueError) as e: + self.logger.error("hourly: %s", e, exc_info=True) + return None def _fetch_live(self, u): try: @@ -220,9 +232,10 @@ def _fetch_live(self, u): r = requests.get(NOAA_BASE, params=p, timeout=8); r.raise_for_status() d = r.json() if 'error' in d: return None - data = d.get('data',[]) + data = d.get('data', []) return float(data[-1]['v']) if data else None - except Exception: return None + except (requests.exceptions.RequestException, OSError, KeyError, TypeError, ValueError): + return None # ── Derived ───────────────────────────────────────────────────────────────── diff --git a/plugins/tide-display/render_preview.py b/plugins/tide-display/render_preview.py index 2e44d8a6..73ec808b 100644 --- a/plugins/tide-display/render_preview.py +++ b/plugins/tide-display/render_preview.py @@ -41,8 +41,10 @@ def _lerp(c1, c2, t): return tuple(int(a + (b - a) * t) for a, b in zip(c1, c2)) def _safe_iso(s): - try: return datetime.fromisoformat(s) - except Exception: return None + try: + return datetime.fromisoformat(s) + except (TypeError, ValueError): + return None def _layout(dw, dh): bar_w = max(8, min(32, int(dw * 0.13))) @@ -179,7 +181,8 @@ def _fmtt(iso): dt = datetime.fromisoformat(iso) hr = dt.hour % 12 or 12 return f"{hr}:{dt.minute:02d}{'a' if dt.hour<12 else 'p'}" - except Exception: return '--' + except (TypeError, ValueError): + return '--' # ── Mode renderers ─────────────────────────────────────────────────────────────