fix: a Vegas static pause survives a non-numeric display duration; aliased store installs ask for a restart (#753)

* fix(vegas): a display duration that is not a number no longer cancels a static pause

The Vegas static pause compared plugin.get_display_duration() with the
clock. clock-simple, calendar and countdown return their display_duration
setting straight from config.json, so a value saved as "20" or null
reached that comparison as a string or None. The TypeError went to the
pause's broad except, which ended the pause: the plugin flashed up and
the scroll went straight on, at every one of its turns. inf held the
pause until something interrupted it, and NaN, False, 0 or a negative
number ended it at once.

The pause now reads the duration the way the rotation has since #739,
with the same helper, then the rotation's fallbacks: 30 s for anything
that is not a number or a get_display_duration() that raises, 15 s for a
number at or below zero. Logged once per plugin. test_vegas_static_mode.py's
pauses used 0 to mean "no wait"; they now use 0.01.

The helper moves from display_controller (_finite_seconds) to base_plugin
(finite_seconds), unchanged: the coordinator cannot import from
display_controller, which imports src.vegas_mode at module level, and a
new src module would turn ledmatrix-plugins' min-core table check red
until it was listed. base_plugin is already loaded whenever either one is.

Tests: test/test_vegas_static_pause_duration.py, on a fake clock,
including TestSameAsTheRotation, which runs every value through both the
pause and the rotation's _get_display_duration/_resolve_durations.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(web): a store install asks for a restart by the id it installed as

POST /plugins/install decides restart_required from whether config.json
already enables the plugin: the display loads a plugin when its enabled
flag changes, so one already enabled (a reinstall, or a config carried
over) keeps running the copy it loaded until a restart. The route read
that flag under the registry id. Weather, Music, Stocks and Leaderboard
install under the id their manifests declare (weather ->
ledmatrix-weather), which is the config section's id, so reinstalling an
enabled one never reported that a restart was needed.

Both the queued and the direct path now look up the installed id once
(#746's _installed_plugin_id) and use it for the plugin_id they answer
with and for the enabled check.

Tests: test/test_api_v3_install_restart_installed_id.py, through the
Flask test client, both paths.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(vegas): type the static pause fallback on its own (mypy ratchet)

_static_pause_duration assigned the fallback to `seconds`, which the
except branch typed as float before finite_seconds() reassigned it as
float | None. A separate `fallback` keeps both types exact; behaviour is
unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Chuck
2026-10-06 08:50:48 -04:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 7c5fa9cfdb
commit cb06124b42
8 changed files with 422 additions and 23 deletions
+2 -15
View File
@@ -25,7 +25,6 @@ import os
import inspect
import signal
import json
import math
import threading
import types
from collections import deque
@@ -64,6 +63,7 @@ from src.ipc.contract import (
PluginReloadResult,
)
from src.ipc.server import ControlServer, QueuedCommand, StateHub, start_control_server
from src.plugin_system.base_plugin import finite_seconds
from src.vegas_mode.render_pipeline import SYNC_SEND_INTERVAL
# Get logger with consistent configuration
@@ -101,19 +101,6 @@ _MIN_INITIAL_UPDATE_TIMEOUT_SECONDS = 2.0
DEFAULT_DYNAMIC_DURATION_CAP = 180.0
def _finite_seconds(value: Any) -> Optional[float]:
"""``value`` as seconds when it is a finite number or a numeric string,
else None. A bool is not a number here, though it is an int: True would
read as a one-second screen."""
if isinstance(value, bool):
return None
try:
seconds = float(value)
except (TypeError, ValueError, OverflowError):
return None
return seconds if math.isfinite(seconds) else None
class _PluginReloadJob:
"""A ``plugin.reload`` whose slow half runs off the render thread.
@@ -1569,7 +1556,7 @@ class DisplayController:
except Exception as err: # pylint: disable=broad-except
problem = f"get_display_duration() raised {type(err).__name__}: {err}"
else:
seconds = _finite_seconds(value)
seconds = finite_seconds(value)
if seconds is not None:
return seconds
problem = f"display duration {value!r} is not a number"