feat(scheduler): airtime guards for {cmd:...}, plus missed docs

Two guards on command placeholders in scheduled messages, neither
configurable, because both exist to protect a shared medium:

A 15-minute floor. A schedule containing {cmd:...} that fires more often
is rejected at startup with an error rather than quietly running slower.
The interval is sampled and measured by the tightest gap between
firings, so "0,1 * * * *" is correctly treated as every 60 seconds and
not as hourly. Schedules without a command placeholder are unaffected.

The command's own cooldown_seconds now applies to a render. Scheduling
is not a way around the rate a command was configured to run at. The
execution is recorded before the command runs, matching execute_commands,
so a slow or failing render cannot be retried straight past the cooldown.

Also fills documentation gaps from the preceding commits:

- --install-extras was only in the script's own --help; documented in
  service-installation.md (including alongside --update-venv) and
  upgrade.md.
- weather-service.md documented weather_alarm's once-a-day scheduling
  with no route to more than one forecast a day, which is exactly what
  people go there looking for. It now points at {cmd:wx ...} and notes
  that sunrise/sunset still belong to weather_alarm.
This commit is contained in:
agessaman committed 2026-08-21 21:33:54 -07:00
1 parent b0148506a6
commit a764186eb2
9 files changed
+257 -4

No files matched your search

+13
View File
@@ -1920,6 +1920,19 @@ class CommandManager:
)
return None
# The command's own cooldown still governs it. A schedule is not a licence to
# run something more often than the operator configured it to run.
allowed, remaining = command.check_cooldown()
if not allowed:
self.logger.warning(
"Scheduled {cmd:...} placeholder: %r is on cooldown for another %.0fs; skipped",
command_name, remaining,
)
return None
# Recorded before execution, matching execute_commands, so a slow or failing
# render cannot be retried straight past the cooldown.
command.record_execution()
sink: list[str] = []
synthetic = MeshMessage(
content=spec,