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
|
name: str
|
||||||
value: str
|
value: str
|
||||||
interval_s: float
|
interval_s: float
|
||||||
|
# dataclass ActionTask (dodaj pole)
|
||||||
|
tag: Optional[str] = None
|
||||||
|
|
||||||
enabled: bool = True
|
enabled: bool = True
|
||||||
last_run: float = 0.0
|
last_run: float = 0.0
|
||||||
next_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:
|
def remove_task(self, task_id: int) -> None:
|
||||||
with self._lock:
|
with self._lock:
|
||||||
self.tasks.pop(task_id, None)
|
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]:
|
def list_tasks(self) -> List[ActionTask]:
|
||||||
with self._lock:
|
with self._lock:
|
||||||
@@ -715,6 +723,84 @@ class App(tk.Tk):
|
|||||||
def open_selector(self) -> None:
|
def open_selector(self) -> None:
|
||||||
union = set([vi.display_name for vi in self.vars.values()]) or set(DEFAULT_VARS)
|
union = set([vi.display_name for vi in self.vars.values()]) or set(DEFAULT_VARS)
|
||||||
self.show_selector_dialog(sorted(union))
|
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:
|
def show_selector_dialog(self, discovered: List[str]) -> None:
|
||||||
seen=set(); disp=[]
|
seen=set(); disp=[]
|
||||||
@@ -904,11 +990,27 @@ class App(tk.Tk):
|
|||||||
return parse_first_float(bd)
|
return parse_first_float(bd)
|
||||||
return None
|
return None
|
||||||
def refresh_actions_tree(self):
|
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())
|
self.actions_tree.delete(*self.actions_tree.get_children())
|
||||||
for t in self.scheduler.list_tasks():
|
for t in self.scheduler.list_tasks():
|
||||||
mode = ("Interval" if t.interval_s > 0 else "Once") + (" + Expr" if getattr(t, 'expr', None) else "")
|
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"))
|
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 ---
|
# --- Poller control / rendering ---
|
||||||
def start_polling(self) -> None:
|
def start_polling(self) -> None:
|
||||||
@@ -946,6 +1048,25 @@ class App(tk.Tk):
|
|||||||
self.paused_event.set()
|
self.paused_event.set()
|
||||||
self.pause_btn.configure(text="Resume")
|
self.pause_btn.configure(text="Resume")
|
||||||
self.status_lbl.configure(text="Paused")
|
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:
|
def drain_queue(self) -> None:
|
||||||
try:
|
try:
|
||||||
@@ -1022,7 +1143,6 @@ class App(tk.Tk):
|
|||||||
self.tree.selection_set(iid)
|
self.tree.selection_set(iid)
|
||||||
finally:
|
finally:
|
||||||
self.tree_menu.tk_popup(event.x_root, event.y_root)
|
self.tree_menu.tk_popup(event.x_root, event.y_root)
|
||||||
|
|
||||||
def open_plot_window(self):
|
def open_plot_window(self):
|
||||||
"""Open (or focus) a per-variable plot window with last 100 values and last 100 deltas.
|
"""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.
|
Top: values line + horizontal mean; Bottom: delta bars + horizontal mean.
|
||||||
@@ -1036,13 +1156,14 @@ class App(tk.Tk):
|
|||||||
if not iid.startswith("var:"):
|
if not iid.startswith("var:"):
|
||||||
messagebox.showinfo("Select variable", "Select a specific variable row (not a group).")
|
messagebox.showinfo("Select variable", "Select a specific variable row (not a group).")
|
||||||
return
|
return
|
||||||
key = iid.split("var:",1)[1]
|
key = iid.split("var:", 1)[1]
|
||||||
vi = self.vars.get(key)
|
vi = self.vars.get(key)
|
||||||
if not vi:
|
if not vi:
|
||||||
messagebox.showinfo("Missing", "Selected variable is not available yet.")
|
messagebox.showinfo("Missing", "Selected variable is not available yet.")
|
||||||
return
|
return
|
||||||
if not HAS_MPL:
|
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
|
return
|
||||||
|
|
||||||
if not hasattr(self, "_plot_windows"):
|
if not hasattr(self, "_plot_windows"):
|
||||||
@@ -1090,7 +1211,7 @@ class App(tk.Tk):
|
|||||||
|
|
||||||
if vals:
|
if vals:
|
||||||
ax_val.plot(xs_v, vals, linewidth=1.2)
|
ax_val.plot(xs_v, vals, linewidth=1.2)
|
||||||
mv = sum(vals)/len(vals)
|
mv = sum(vals) / len(vals)
|
||||||
ax_val.axhline(mv, linestyle="--", linewidth=1.2)
|
ax_val.axhline(mv, linestyle="--", linewidth=1.2)
|
||||||
vmin, vmax = min(vals), max(vals)
|
vmin, vmax = min(vals), max(vals)
|
||||||
span = (vmax - vmin) or 1.0
|
span = (vmax - vmin) or 1.0
|
||||||
@@ -1102,7 +1223,7 @@ class App(tk.Tk):
|
|||||||
|
|
||||||
if dels:
|
if dels:
|
||||||
ax_delta.bar(xs_d, dels)
|
ax_delta.bar(xs_d, dels)
|
||||||
md = sum(dels)/len(dels)
|
md = sum(dels) / len(dels)
|
||||||
ax_delta.axhline(md, linestyle="--", linewidth=1.2)
|
ax_delta.axhline(md, linestyle="--", linewidth=1.2)
|
||||||
dmin, dmax = min(dels), max(dels)
|
dmin, dmax = min(dels), max(dels)
|
||||||
span = (dmax - dmin) or 1.0
|
span = (dmax - dmin) or 1.0
|
||||||
@@ -1119,18 +1240,12 @@ class App(tk.Tk):
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
try:
|
try:
|
||||||
interval_ms = int(max(250, float(self.refresh_var.get())*1000))
|
interval_ms = int(max(250, float(self.refresh_var.get()) * 1000))
|
||||||
except Exception:
|
except Exception:
|
||||||
interval_ms = 1000
|
interval_ms = 1000
|
||||||
win.after(interval_ms, refresh_plot)
|
win.after(interval_ms, refresh_plot)
|
||||||
|
|
||||||
win.after(200, 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):
|
def open_thresholds_dialog(self):
|
||||||
@@ -1305,6 +1420,7 @@ class App(tk.Tk):
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
dlg.destroy()
|
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="Save", command=do_save).pack(side=tk.RIGHT, padx=6)
|
||||||
ttk.Button(btns, text="Cancel", command=dlg.destroy).pack(side=tk.RIGHT)
|
ttk.Button(btns, text="Cancel", command=dlg.destroy).pack(side=tk.RIGHT)
|
||||||
|
|||||||
Reference in New Issue
Block a user