ALARMS key stops every auto-fix too, and says so
Muted, nothing fixes itself at all: no Spotify exception (while_muted is gone) and no any-hour READY fix; holds still fix and the Sunday stream-audio failover still switches. The key reads ALARMS OFF / AUTO-FIX OFF / TIL 12AM, and its note says both. The settings page's switch says the same. The mute's end is worked out at the press and kept: a watchdog that was not running at 23:59 no longer leaves the alarms muted until the next schedule change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+78
-20
@@ -814,6 +814,13 @@ def in_windows(windows, now):
|
||||
return any(window_holds(w, day, hm, DAYS[dt.weekday() - 1]) for w in windows)
|
||||
|
||||
|
||||
def clock12(t):
|
||||
"""12AM, 6:30PM: a time on a key, the way people say it."""
|
||||
dt = datetime.fromtimestamp(t)
|
||||
h = dt.hour % 12 or 12
|
||||
return ("%d" % h if dt.minute == 0 else "%d:%02d" % (h, dt.minute)) + ("AM" if dt.hour < 12 else "PM")
|
||||
|
||||
|
||||
def windows_phrase(windows):
|
||||
"""Windows in words, for log headers and notes: "on Sundays 06:30-13:30"."""
|
||||
parts = []
|
||||
@@ -886,32 +893,71 @@ class State:
|
||||
|
||||
|
||||
class ServiceMode:
|
||||
"""On inside a scheduled window; a button press overrides until the next window edge."""
|
||||
"""On inside a scheduled window; a button press overrides until the next window edge. The
|
||||
edge is worked out at the press and kept, so the override ends there even when the watchdog
|
||||
was not running at that minute (a restart over 23:59 left the alarms muted for good)."""
|
||||
|
||||
def __init__(self, windows, state):
|
||||
self.windows = windows
|
||||
self.state = state if isinstance(state, State) else State(state)
|
||||
saved = self.state.get("service") or {}
|
||||
self.override, self.last_sched = saved.get("override"), saved.get("last_sched")
|
||||
self.edge = saved.get("edge")
|
||||
|
||||
def scheduled(self, now):
|
||||
return in_windows(self.windows, now)
|
||||
|
||||
def next_change(self, now):
|
||||
"""The first minute after `now` whose schedule differs from now's, or None (within 8 days)."""
|
||||
sched, t = self.scheduled(now), int(now // 60) * 60 + 60
|
||||
while t < now + 8 * 86400:
|
||||
if self.scheduled(t) != sched:
|
||||
return t
|
||||
t += 60
|
||||
return None
|
||||
|
||||
def on(self, now):
|
||||
sched = self.scheduled(now)
|
||||
if sched != self.last_sched:
|
||||
self.last_sched, self.override = sched, None
|
||||
if self.override is not None and self.edge is None:
|
||||
self.edge = self.next_change(now) # an override saved before edges were kept
|
||||
self._save()
|
||||
if sched != self.last_sched or (self.override is not None and self.edge and now >= self.edge):
|
||||
self.last_sched, self.override, self.edge = sched, None, None
|
||||
self._save()
|
||||
return self.override if self.override is not None else sched
|
||||
|
||||
def back_at(self, now):
|
||||
"""When a manual override (ALARMS pressed) ends by itself, as a timestamp, or None. The
|
||||
override holds until the schedule next changes; from then on the schedule rules, so the
|
||||
state flips there, or at the change after (at NA: cleared 23:59, alarms back 00:00).
|
||||
Looked up once a minute."""
|
||||
if self.override is None:
|
||||
return None
|
||||
minute = int(now // 60)
|
||||
cached = getattr(self, "_back", None)
|
||||
if cached and cached[0] == (minute, self.override):
|
||||
return cached[1]
|
||||
sched, t, found = self.scheduled(now), minute * 60 + 60, None
|
||||
while t < now + 8 * 86400:
|
||||
s = self.scheduled(t)
|
||||
if s != sched:
|
||||
sched = s
|
||||
if s != self.override:
|
||||
found = t
|
||||
break
|
||||
t += 60
|
||||
self._back = ((minute, self.override), found)
|
||||
return found
|
||||
|
||||
def set(self, value, now):
|
||||
sched = self.scheduled(now)
|
||||
self.last_sched = sched
|
||||
self.override = None if value == sched else value
|
||||
self.edge = self.next_change(now) if self.override is not None else None
|
||||
self._save()
|
||||
|
||||
def _save(self):
|
||||
self.state.set("service", {"override": self.override, "last_sched": self.last_sched})
|
||||
self.state.set("service", {"override": self.override, "last_sched": self.last_sched, "edge": self.edge})
|
||||
|
||||
|
||||
# --- Engine ------------------------------------------------------------------
|
||||
@@ -984,7 +1030,7 @@ class Engine:
|
||||
self.auto_watch = {} # "BUTTON FAULT" auto-fix went after -> when (to log when it clears)
|
||||
self.auto_paused = False
|
||||
self.auto_on = False # inside the window (for the "auto-fix on" line)
|
||||
self.auto_muted = False # ...with ALARMS muted, so only the "while_muted" fixes run
|
||||
self.auto_muted = False # ALARMS muted last round (nothing fixes itself then)
|
||||
self.auto_last = 0.0
|
||||
self.deck_ips = {} # HyperDeck connection -> last address Companion reported
|
||||
self.now = 0.0
|
||||
@@ -1498,18 +1544,27 @@ class Engine:
|
||||
if not a or now - self.auto_last < 5 or self.stopping:
|
||||
return []
|
||||
self.auto_last = now
|
||||
# ALARMS muted (maintenance) stops auto-fix, except the harmless fixes in "while_muted"
|
||||
# (playing the Spotify playlist): those keep running inside the window.
|
||||
# The ALARMS key is the on-the-fly switch: muted, nothing is fixed by itself at all (holds
|
||||
# still work). It comes back by itself with the alarms (2026-10-19: the user's choice).
|
||||
muted = not self.service.on(now)
|
||||
hours = in_windows(a.get("windows", []), now)
|
||||
# A fix marked "anytime" (writing the timecode into Planning Center: nothing in the room
|
||||
# notices) runs by itself at any hour, not only in the auto-fix hours, muted or not.
|
||||
# A fix marked "anytime" (writing the timecode into Planning Center) runs by itself at any
|
||||
# hour, not only in the auto-fix hours - but never while muted, like every other.
|
||||
anytime = {f for f in a.get("fixes", []) if self.cfg["fixes"].get(f, {}).get("anytime")
|
||||
and not self.cfg["fixes"].get(f, {}).get("never_auto") and not self.cfg["fixes"].get(f, {}).get("disabled")}
|
||||
keep = (set(a.get("while_muted", [])) | anytime) if muted else None
|
||||
if not hours or (muted and not keep):
|
||||
if muted:
|
||||
if self.autolog and (self.auto_on or (anytime and not self.auto_muted)):
|
||||
self.autolog.write(now, "PANEL", "auto-fix off (ALARMS muted)")
|
||||
self.auto_on, self.auto_muted = False, True
|
||||
self.auto_tries.clear()
|
||||
self.auto_seen.clear()
|
||||
return []
|
||||
if self.auto_muted and self.autolog and not hours and anytime:
|
||||
self.autolog.write(now, "PANEL", "ALARMS back on: the any-hour fixes (%s) run again" % ", ".join(sorted(anytime)))
|
||||
self.auto_muted = False
|
||||
if not hours:
|
||||
if self.auto_on and self.autolog:
|
||||
self.autolog.write(now, "PANEL", "auto-fix off (%s)" % ("ALARMS muted" if hours else "end of the window"))
|
||||
self.autolog.write(now, "PANEL", "auto-fix off (end of the window)")
|
||||
self.auto_on = False
|
||||
for ident in [i for i in self.auto_tries if not (i[0] == "fix" and i[1] in anytime)]:
|
||||
del self.auto_tries[ident] # the window's fixes start fresh next time
|
||||
@@ -1517,11 +1572,10 @@ class Engine:
|
||||
if not anytime:
|
||||
return []
|
||||
else:
|
||||
if (not self.auto_on or self.auto_muted != muted) and self.autolog: # opens today's file, so it can be watched from the start
|
||||
if not self.auto_on and self.autolog: # opens today's file, so it can be watched from the start
|
||||
ends = [w["end"] for w in a.get("windows", []) if in_windows([w], now)]
|
||||
self.autolog.write(now, "PANEL", "auto-fix on%s%s" % (" until " + ends[0] if ends else "",
|
||||
" (ALARMS muted: only %s)" % ", ".join(sorted(keep)) if muted else ""))
|
||||
self.auto_on, self.auto_muted = True, muted
|
||||
self.autolog.write(now, "PANEL", "auto-fix on%s" % (" until " + ends[0] if ends else ""))
|
||||
self.auto_on = True
|
||||
net_down = any(self.trackers.get("net:" + k) and self.trackers["net:" + k].confirmed
|
||||
and self.trackers["net:" + k].result.level == ERR for k in ("link", "gateway"))
|
||||
if net_down or (self.burst and (self.burst["cleared"] is None
|
||||
@@ -1560,8 +1614,6 @@ class Engine:
|
||||
for ident, kind, what, chk in self.fix_plan(t["id"], snap, now, a.get("after_seconds", 20), True, levels):
|
||||
if not ((kind == "fix" and what in gentle) or (kind == "restart" and restarts)):
|
||||
continue # not gentle: stays a hold
|
||||
if keep is not None and not (kind == "fix" and what in keep):
|
||||
continue # ALARMS muted: only the "while_muted" fixes
|
||||
self.auto_seen[ident] = now
|
||||
tries, last = self.auto_tries.get(ident, (0, 0.0))
|
||||
if tries >= a.get("max_tries", 2):
|
||||
@@ -1926,8 +1978,14 @@ def render(engine, service_on):
|
||||
out[tid] = colored(tile["label"] + "\n" + note["text"], note["level"])
|
||||
continue
|
||||
if kind == "service":
|
||||
manual = engine.service.override is not None
|
||||
text = tile["label"] + "\n" + ("ON" if service_on else "OFF") + ("\nmanual" if manual else "")
|
||||
# One key, two switches: ALARMS muted also stops every auto-fix. Says both, and when
|
||||
# they come back by themselves after a press.
|
||||
word = "ON" if service_on else "OFF"
|
||||
fix_word = "OFF" if (not service_on or (engine.cfg.get("auto_fix") or {}).get("disabled")) else "ON"
|
||||
text = "%s %s\nAUTO-FIX %s" % (tile["label"], word, fix_word)
|
||||
back = engine.service.back_at(engine.now)
|
||||
if back:
|
||||
text += "\nTIL " + clock12(back)
|
||||
out[tid] = colored(text, "service" if service_on else "off")
|
||||
continue
|
||||
if kind == "master":
|
||||
|
||||
Reference in New Issue
Block a user