This commit is contained in:
2025-10-20 18:31:12 +02:00
parent 8d5aac56d1
commit e06e297540
+125 -9
View File
@@ -282,6 +282,9 @@ class ActionTask:
name: str
value: str
interval_s: float
# dataclass ActionTask (dodaj pole)
tag: Optional[str] = None
enabled: bool = True
last_run: float = 0.0
next_run: float = 0.0
@@ -317,6 +320,11 @@ class ActionScheduler(threading.Thread):
def remove_task(self, task_id: int) -> None:
with self._lock:
self.tasks.pop(task_id, None)
def remove_tasks_by_tag(self, tag: str) -> None:
with self._lock:
to_del = [tid for tid, t in self.tasks.items() if getattr(t, "tag", None) == tag]
for tid in to_del:
self.tasks.pop(tid, None)
def list_tasks(self) -> List[ActionTask]:
with self._lock:
@@ -715,6 +723,84 @@ class App(tk.Tk):
def open_selector(self) -> None:
union = set([vi.display_name for vi in self.vars.values()]) or set(DEFAULT_VARS)
self.show_selector_dialog(sorted(union))
def _reschedule_state_tasks_for(self, vi: VarInfo) -> None:
"""
Usuwa wszystkie 'while-in-state' taski dla tej zmiennej i tworzy na nowo
tylko te, które powinny działać w bieżącym stanie według aktualnych progów.
Interwał 0 => run once; >0 => cykliczne.
"""
if not hasattr(self, "scheduler") or self.scheduler is None:
return
t = vi.thresholds
# 1) Usuń istniejące zadania powiązane z tą zmienną (po tagu):
for state in ("DEAD_LOW", "LOW", "OPERATING", "HIGH", "EXTREME_HIGH"):
tag = f"state::{vi.key}::{state}"
self.scheduler.remove_tasks_by_tag(tag)
# 2) Zmapuj ustawienia dla stanów:
mapping = {
"DEAD_LOW": (
t.action_dead_low, t.value_dead_low, t.action_dead_low_interval,
getattr(t, "expr_dead_low", None), getattr(t, "expr_target_dead_low", None)
),
"LOW": (
t.action_low, t.value_low, t.action_low_interval,
getattr(t, "expr_low", None), getattr(t, "expr_target_low", None)
),
"OPERATING": (
getattr(t, "action_operating", None), getattr(t, "value_operating", "1"),
getattr(t, "action_operating_interval", 1.0),
getattr(t, "expr_operating", None), getattr(t, "expr_target_operating", None)
),
"HIGH": (
t.action_high, t.value_high, t.action_high_interval,
getattr(t, "expr_high", None), getattr(t, "expr_target_high", None)
),
"EXTREME_HIGH": (
t.action_extreme_high, t.value_extreme_high, t.action_extreme_high_interval,
getattr(t, "expr_extreme_high", None), getattr(t, "expr_target_extreme_high", None)
),
}
curr_state = vi.last_state # aktualny stan (ustawiany w evaluate_thresholds / on_state_change) :contentReference[oaicite:4]{index=4}
if curr_state not in mapping:
self.refresh_actions_tree()
return
name, value, interval_s, expr, expr_target = mapping[curr_state]
# Jeśli użytkownik ustawił 0 => 'run once' przy wejściu w stan:
# Samo przełączenie na 0 ma natychmiast skasować ew. cykliczne zadania zrobiliśmy to wyżej.
# Nie dodajemy nowego zadania, jeśli nie ma co wykonywać.
if not any([name, expr, expr_target]):
self.refresh_actions_tree()
return
tag = f"state::{vi.key}::{curr_state}"
if expr or expr_target:
# zadanie 'obliczeniowe' liczymy x/y/z (x=monitorowana)
task = ActionTask(
name=(name or expr_target or ""), # nazwa do tabeli/POST (gdy expr_target)
value=(value if name else "0"), # value nie jest używane gdy expr_target
interval_s=float(interval_s),
expr=expr,
x_src=vi.display_name,
y_src=getattr(t, "y_source", None),
z_src=getattr(t, "z_source", None),
tag=tag,
)
else:
# zwykły POST do 'name' z 'value'
task = ActionTask(
name=name or "",
value=str(value),
interval_s=float(interval_s),
tag=tag,
)
self.scheduler.add_task(task)
self.refresh_actions_tree()
def show_selector_dialog(self, discovered: List[str]) -> None:
seen=set(); disp=[]
@@ -904,11 +990,27 @@ class App(tk.Tk):
return parse_first_float(bd)
return None
def refresh_actions_tree(self):
# zapamiętaj zaznaczenie/fokus
sel_before = self.actions_tree.selection()
focus_before = self.actions_tree.focus()
# przebuduj
self.actions_tree.delete(*self.actions_tree.get_children())
for t in self.scheduler.list_tasks():
mode = ("Interval" if t.interval_s > 0 else "Once") + (" + Expr" if getattr(t, 'expr', None) else "")
next_s = ("" if t.interval_s == 0 else datetime.fromtimestamp(t.next_run).strftime("%H:%M:%S"))
self.actions_tree.insert("", "end", iid=str(t.task_id), values=(t.name, t.value, mode, t.interval_s, next_s, "Yes" if t.enabled else "No"))
self.actions_tree.insert(
"", "end", iid=str(t.task_id),
values=(t.name, t.value, mode, t.interval_s, next_s, "Yes" if t.enabled else "No")
)
# odtwórz zaznaczenie/fokus (o ile elementy wciąż istnieją)
if sel_before:
kept = [iid for iid in sel_before if self.actions_tree.exists(iid)]
if kept:
self.actions_tree.selection_set(kept)
if focus_before and self.actions_tree.exists(focus_before):
self.actions_tree.focus(focus_before)
# --- Poller control / rendering ---
def start_polling(self) -> None:
@@ -946,6 +1048,25 @@ class App(tk.Tk):
self.paused_event.set()
self.pause_btn.configure(text="Resume")
self.status_lbl.configure(text="Paused")
def _bind_mousewheel(self, widget):
# Windows / macOS
def _mw(event):
if event.delta:
widget.yview_scroll(int(-1 * (event.delta / 120)), "units")
return "break"
# Linux (X11)
def _mw_up(_e):
widget.yview_scroll(-1, "units")
return "break"
def _mw_down(_e):
widget.yview_scroll(1, "units")
return "break"
widget.bind("<MouseWheel>", _mw)
widget.bind("<Button-4>", _mw_up)
widget.bind("<Button-5>", _mw_down)
def drain_queue(self) -> None:
try:
@@ -1022,7 +1143,6 @@ class App(tk.Tk):
self.tree.selection_set(iid)
finally:
self.tree_menu.tk_popup(event.x_root, event.y_root)
def open_plot_window(self):
"""Open (or focus) a per-variable plot window with last 100 values and last 100 deltas.
Top: values line + horizontal mean; Bottom: delta bars + horizontal mean.
@@ -1042,7 +1162,8 @@ class App(tk.Tk):
messagebox.showinfo("Missing", "Selected variable is not available yet.")
return
if not HAS_MPL:
messagebox.showwarning("Plotting not available", "Matplotlib is not installed. Install 'matplotlib' to enable plots.")
messagebox.showwarning("Plotting not available",
"Matplotlib is not installed. Install 'matplotlib' to enable plots.")
return
if not hasattr(self, "_plot_windows"):
@@ -1125,12 +1246,6 @@ class App(tk.Tk):
win.after(interval_ms, refresh_plot)
win.after(200, refresh_plot)
try:
iid = self.tree.identify_row(event.y)
if iid:
self.tree.selection_set(iid)
finally:
self.tree_menu.tk_popup(event.x_root, event.y_root)
def open_thresholds_dialog(self):
@@ -1305,6 +1420,7 @@ class App(tk.Tk):
except Exception:
pass
dlg.destroy()
self._reschedule_state_tasks_for(vi)
ttk.Button(btns, text="Save", command=do_save).pack(side=tk.RIGHT, padx=6)
ttk.Button(btns, text="Cancel", command=dlg.destroy).pack(side=tk.RIGHT)