mirror of
https://github.com/migatu/vtt_work.git
synced 2026-07-14 13:34:42 +00:00
1.11
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user