diff --git a/nucleares_monitor/control_board_monitor.py b/nucleares_monitor/control_board_monitor.py index 1b8fc69..002d01f 100644 --- a/nucleares_monitor/control_board_monitor.py +++ b/nucleares_monitor/control_board_monitor.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- """ -Control Board Monitor — v1.5 +Control Board Monitor — v1.6 What’s new: - POSTs are now **exactly** like the sample app: @@ -36,7 +36,7 @@ SERVER_HOST: str = "localhost" SERVER_PORT: int = 8785 REFRESH_INTERVAL_S: int = 1 REQUEST_TIMEOUT_S: float = 5.0 -USER_AGENT: str = "ControlBoardMonitor/1.5 (+tkinter)" +USER_AGENT: str = "ControlBoardMonitor/1.6 (+tkinter)" # Fallback defaults (extracted from your HTML) DEFAULT_VARS: List[str] = ['ALARMS_ACTIVE', 'AMBIENT_TEMPERATURE', 'CHEM_BORON_DOSAGE_ACTUAL', 'CHEM_BORON_DOSAGE_ORDERED', 'CHEM_BORON_FILTER_ACTUAL', 'CHEM_BORON_FILTER_ORDERED', 'CHEM_BORON_PPM', 'CHEM_TRUCK_CONNECTED', 'CHEM_TRUCK_IN_ZONE', 'CHEMICAL_CLEANING_PUMP_DRY_STATUS', 'CHEMICAL_CLEANING_PUMP_OVERLOAD_STATUS', 'CHEMICAL_CLEANING_PUMP_STATUS', 'CHEMICAL_DOSING_PUMP_DRY_STATUS', 'CHEMICAL_DOSING_PUMP_OVERLOAD_STATUS', 'CHEMICAL_DOSING_PUMP_STATUS', 'CHEMICAL_FILTER_PUMP_DRY_STATUS', 'CHEMICAL_FILTER_PUMP_OVERLOAD_STATUS', 'CHEMICAL_FILTER_PUMP_STATUS', 'CONDENSER_CIRCULATION_PUMP_ACTIVE', 'CONDENSER_CIRCULATION_PUMP_ORDERED_SPEED', 'CONDENSER_CIRCULATION_PUMP_OVERLOAD_STATUS', 'CONDENSER_CIRCULATION_PUMP_SPEED', 'CONDENSER_CIRCULATION_PUMP_SWITCH', 'CONDENSER_CONDENSATE_FLOW_RATE', 'CONDENSER_COOLANT_EVAPORATED', 'CONDENSER_EXTRACTION_FLOW_RATE', 'CONDENSER_PRESSURE', 'CONDENSER_TEMPERATURE', 'CONDENSER_VACUUM', 'CONDENSER_VACUUM_PUMP_ACTIVE', 'CONDENSER_VACUUM_PUMP_MODE', 'CONDENSER_VACUUM_PUMP_POWER', 'CONDENSER_VACUUM_RELIEF_VALVE_OPENING', 'CONDENSER_VAPOR_VOLUME', 'CONDENSER_VOLUME', 'COOLANT_CORE_CIRCULATION_PUMP_0_CAPACITY', 'COOLANT_CORE_CIRCULATION_PUMP_0_DRY_STATUS', 'COOLANT_CORE_CIRCULATION_PUMP_0_ORDERED_SPEED', 'COOLANT_CORE_CIRCULATION_PUMP_0_OVERLOAD_STATUS', 'COOLANT_CORE_CIRCULATION_PUMP_0_SPEED', 'COOLANT_CORE_CIRCULATION_PUMP_0_STATUS', 'COOLANT_CORE_CIRCULATION_PUMP_1_CAPACITY', 'COOLANT_CORE_CIRCULATION_PUMP_1_DRY_STATUS', 'COOLANT_CORE_CIRCULATION_PUMP_1_ORDERED_SPEED', 'COOLANT_CORE_CIRCULATION_PUMP_1_OVERLOAD_STATUS', 'COOLANT_CORE_CIRCULATION_PUMP_1_SPEED', 'COOLANT_CORE_CIRCULATION_PUMP_1_STATUS', 'COOLANT_CORE_CIRCULATION_PUMP_2_CAPACITY', 'COOLANT_CORE_CIRCULATION_PUMP_2_DRY_STATUS', 'COOLANT_CORE_CIRCULATION_PUMP_2_ORDERED_SPEED', 'COOLANT_CORE_CIRCULATION_PUMP_2_OVERLOAD_STATUS', 'COOLANT_CORE_CIRCULATION_PUMP_2_SPEED', 'COOLANT_CORE_CIRCULATION_PUMP_2_STATUS', 'COOLANT_CORE_FLOW_IN', 'COOLANT_CORE_FLOW_ORDERED_SPEED', 'COOLANT_CORE_FLOW_OUT', 'COOLANT_CORE_FLOW_REACHED_SPEED', 'COOLANT_CORE_FLOW_SPEED', 'COOLANT_CORE_MAX_PRESSURE', 'COOLANT_CORE_PRESSURE', 'COOLANT_CORE_PRIMARY_LOOP_LEVEL', 'COOLANT_CORE_QUANTITY_CIRCULATION_PUMPS_PRESENT', 'COOLANT_CORE_QUANTITY_FREIGHT_PUMPS_PRESENT', 'COOLANT_CORE_QUANTITY_IN_VESSEL', 'COOLANT_CORE_STATE', 'COOLANT_CORE_VESSEL_TEMPERATURE', 'COOLANT_SEC_0_LIQUID_VOLUME', 'COOLANT_SEC_0_PRESSURE', 'COOLANT_SEC_0_TEMPERATURE', 'COOLANT_SEC_0_VOLUME', 'COOLANT_SEC_1_LIQUID_VOLUME', 'COOLANT_SEC_1_PRESSURE', 'COOLANT_SEC_1_TEMPERATURE', 'COOLANT_SEC_1_VOLUME', 'COOLANT_SEC_2_LIQUID_VOLUME', 'COOLANT_SEC_2_PRESSURE', 'COOLANT_SEC_2_TEMPERATURE', 'COOLANT_SEC_2_VOLUME', 'COOLANT_SEC_CIRCULATION_PUMP_0_CAPACITY', 'COOLANT_SEC_CIRCULATION_PUMP_0_DRY_STATUS', 'COOLANT_SEC_CIRCULATION_PUMP_0_ORDERED_SPEED', 'COOLANT_SEC_CIRCULATION_PUMP_0_OVERLOAD_STATUS', 'COOLANT_SEC_CIRCULATION_PUMP_0_SPEED', 'COOLANT_SEC_CIRCULATION_PUMP_0_STATUS', 'COOLANT_SEC_CIRCULATION_PUMP_1_CAPACITY', 'COOLANT_SEC_CIRCULATION_PUMP_1_DRY_STATUS', 'COOLANT_SEC_CIRCULATION_PUMP_1_ORDERED_SPEED', 'COOLANT_SEC_CIRCULATION_PUMP_1_OVERLOAD_STATUS', 'COOLANT_SEC_CIRCULATION_PUMP_1_SPEED', 'COOLANT_SEC_CIRCULATION_PUMP_1_STATUS', 'COOLANT_SEC_CIRCULATION_PUMP_2_CAPACITY', 'COOLANT_SEC_CIRCULATION_PUMP_2_DRY_STATUS', 'COOLANT_SEC_CIRCULATION_PUMP_2_ORDERED_SPEED', 'COOLANT_SEC_CIRCULATION_PUMP_2_OVERLOAD_STATUS', 'COOLANT_SEC_CIRCULATION_PUMP_2_SPEED', 'COOLANT_SEC_CIRCULATION_PUMP_2_STATUS', 'CORE_BAY_1_HATCH_OPEN', 'CORE_BAY_1_STATE', 'CORE_BAY_2_HATCH_OPEN', 'CORE_BAY_2_STATE', 'CORE_BAY_3_HATCH_OPEN', 'CORE_BAY_3_STATE', 'CORE_BAY_4_HATCH_OPEN', 'CORE_BAY_4_STATE', 'CORE_BAY_5_HATCH_OPEN', 'CORE_BAY_5_STATE', 'CORE_BAY_6_HATCH_OPEN', 'CORE_BAY_6_STATE', 'CORE_BAY_7_HATCH_OPEN', 'CORE_BAY_7_STATE', 'CORE_BAY_8_HATCH_OPEN', 'CORE_BAY_8_STATE', 'CORE_BAY_9_HATCH_OPEN', 'CORE_BAY_9_STATE', 'CORE_CRITICAL_MASS_REACHED', 'CORE_CRITICAL_MASS_REACHED_COUNTER', 'CORE_EXTERNAL_COOLANT_RESERVOIR_VOLUME', 'CORE_FACTOR', 'CORE_FACTOR_CHANGE', 'CORE_FUEL_1_FISSIONABLE', 'CORE_FUEL_1_POWER_FACTOR', 'CORE_FUEL_1_TEMPERATURE', 'CORE_FUEL_2_FISSIONABLE', 'CORE_FUEL_2_POWER_FACTOR', 'CORE_FUEL_2_TEMPERATURE', 'CORE_FUEL_3_FISSIONABLE', 'CORE_FUEL_3_POWER_FACTOR', 'CORE_FUEL_3_TEMPERATURE', 'CORE_FUEL_4_FISSIONABLE', 'CORE_FUEL_4_POWER_FACTOR', 'CORE_FUEL_4_TEMPERATURE', 'CORE_FUEL_5_FISSIONABLE', 'CORE_FUEL_5_POWER_FACTOR', 'CORE_FUEL_5_TEMPERATURE', 'CORE_FUEL_6_FISSIONABLE', 'CORE_FUEL_6_POWER_FACTOR', 'CORE_FUEL_6_TEMPERATURE', 'CORE_FUEL_7_FISSIONABLE', 'CORE_FUEL_7_POWER_FACTOR', 'CORE_FUEL_7_TEMPERATURE', 'CORE_FUEL_8_FISSIONABLE', 'CORE_FUEL_8_POWER_FACTOR', 'CORE_FUEL_8_TEMPERATURE', 'CORE_FUEL_9_FISSIONABLE', 'CORE_FUEL_9_POWER_FACTOR', 'CORE_FUEL_9_TEMPERATURE', 'CORE_FUEL_AVG_FISSIONABLE', 'CORE_FUEL_AVG_POWER_FACTOR', 'CORE_FUEL_AVG_TEMPERATURE', 'CORE_HIGH_STEAM_PRESENT', 'CORE_IMMINENT_FUSION', 'CORE_INTEGRITY', 'CORE_IODINE_CUMULATIVE', 'CORE_IODINE_GENERATION', 'CORE_OPERATION_MODE', 'CORE_POOL_COOLANT_TANK_VOLUME', 'CORE_POOL_PUMP', 'CORE_PRESSURE', 'CORE_PRESSURE_MAX', 'CORE_PRESSURE_OPERATIVE', 'CORE_PRIMARY_CIRCUIT_COOLING_TANK_VOLUME', 'CORE_READY_FOR_START', 'CORE_STATE', 'CORE_STATE_CRITICALITY', 'CORE_STEAM_PRESENT', 'CORE_TEMP', 'CORE_TEMP_MAX', 'CORE_TEMP_MIN', 'CORE_TEMP_OPERATIVE', 'CORE_TEMP_RESIDUAL', 'CORE_WEAR', 'CORE_XENON_CUMULATIVE', 'CORE_XENON_GENERATION', 'EMERGENCY_BATTERIES_MODE', 'EMERGENCY_BATTERIES_POWER_OUTPUT_KW', 'EMERGENCY_GENERATOR_1_FUEL', 'EMERGENCY_GENERATOR_1_MAINTENANCE_NEEDED', 'EMERGENCY_GENERATOR_1_MODE', 'EMERGENCY_GENERATOR_1_PRESSURIZER', 'EMERGENCY_GENERATOR_1_STATUS', 'EMERGENCY_GENERATOR_2_FUEL', 'EMERGENCY_GENERATOR_2_MAINTENANCE_NEEDED', 'EMERGENCY_GENERATOR_2_MODE', 'EMERGENCY_GENERATOR_2_PRESSURIZER', 'EMERGENCY_GENERATOR_2_STATUS', 'EMERGENCY_GENERATOR_POWER_OUTPUT_KW', 'FREIGHT_PUMP_CONDENSER_ACTIVE', 'FREIGHT_PUMP_CONDENSER_SWITCH', 'FREIGHT_PUMP_EXTERNAL_ACTIVE', 'FREIGHT_PUMP_EXTERNAL_SWITCH', 'FREIGHT_PUMP_FEEDWATER_ACTIVE', 'FREIGHT_PUMP_FEEDWATER_SWITCH', 'FREIGHT_PUMP_INTERNAL_ACTIVE', 'FREIGHT_PUMP_INTERNAL_SWITCH', 'FUN_IS_ENABLED', 'GAME_DIFFICULTY', 'GAME_SIM_SPEED', 'GAME_VERSION', 'GENERATOR_0_A', 'GENERATOR_0_BREAKER', 'GENERATOR_0_HERTZ', 'GENERATOR_0_KW', 'GENERATOR_0_V', 'GENERATOR_1_A', 'GENERATOR_1_BREAKER', 'GENERATOR_1_HERTZ', 'GENERATOR_1_KW', 'GENERATOR_1_V', 'GENERATOR_2_A', 'GENERATOR_2_BREAKER', 'GENERATOR_2_HERTZ', 'GENERATOR_2_KW', 'GENERATOR_2_V', 'INSTALLED_LOOPS_JSON', 'INVENTORY_HTML', 'MAINTENANCE_REPORT_HTML', 'MSCV_0_OPENING_ACTUAL', 'MSCV_1_OPENING_ACTUAL', 'MSCV_2_OPENING_ACTUAL', 'POWER_DEMAND_MW', 'POWER_FROM_EXTERNAL_KW', 'POWER_FROM_TURBINE_KW', 'POWER_MAX_THEORETICAL_FINAL_PLANT_OUTPUT_MW', 'POWER_MAX_THEORETICAL_PLANT_OUTPUT_MW', 'RES_ABSORPTION_CAPACITY_MW', 'RES_DIVERT_SURPLUS_FROM_MW', 'RES_EFFECTIVELY_DERIVED_ENERGY_MW', 'RESISTOR_BANK_01_SWITCH', 'RESISTOR_BANK_02_SWITCH', 'RESISTOR_BANK_03_SWITCH', 'RESISTOR_BANK_04_SWITCH', 'RESISTOR_BANKS_JSON', 'RESISTOR_BANKS_MAIN_SWITCH', 'ROD_BANK_POS_0_ACTUAL', 'ROD_BANK_POS_0_ORDERED', 'ROD_BANK_POS_1_ACTUAL', 'ROD_BANK_POS_1_ORDERED', 'ROD_BANK_POS_2_ACTUAL', 'ROD_BANK_POS_2_ORDERED', 'ROD_BANK_POS_3_ACTUAL', 'ROD_BANK_POS_3_ORDERED', 'ROD_BANK_POS_4_ACTUAL', 'ROD_BANK_POS_4_ORDERED', 'ROD_BANK_POS_5_ACTUAL', 'ROD_BANK_POS_5_ORDERED', 'ROD_BANK_POS_6_ACTUAL', 'ROD_BANK_POS_6_ORDERED', 'ROD_BANK_POS_7_ACTUAL', 'ROD_BANK_POS_7_ORDERED', 'ROD_BANK_POS_8_ACTUAL', 'ROD_BANK_POS_8_ORDERED', 'RODS_ALIGNED', 'RODS_DEFORMED', 'RODS_MAX_TEMPERATURE', 'RODS_MOVEMENT_SPEED', 'RODS_MOVEMENT_SPEED_DECREASED_HIGH_TEMPERATURE', 'RODS_POS_ACTUAL', 'RODS_POS_ORDERED', 'RODS_POS_REACHED', 'RODS_QUANTITY', 'RODS_STATUS', 'RODS_TEMPERATURE', 'STEAM_EJECTOR_CONDENSER_RETURN_VALVE_ACTUAL', 'STEAM_EJECTOR_CONDENSER_RETURN_VALVE_ORDERED', 'STEAM_EJECTOR_MOTIVE', 'STEAM_EJECTOR_OPERATIONAL_MOTIVE_VALVE_ACTUAL', 'STEAM_EJECTOR_OPERATIONAL_MOTIVE_VALVE_ORDERED', 'STEAM_EJECTOR_STARTUP_MOTIVE_VALVE_ACTUAL', 'STEAM_EJECTOR_STARTUP_MOTIVE_VALVE_ORDERED', 'STEAM_GEN_0_BOILING_POINT', 'STEAM_GEN_0_EVAPORATED', 'STEAM_GEN_0_INLET', 'STEAM_GEN_0_OUTLET', 'STEAM_GEN_0_RETURN_FLOW_PLUS_CONDENSED', 'STEAM_GEN_0_STATUS', 'STEAM_GEN_0_VENT_SWITCH', 'STEAM_GEN_1_BOILING_POINT', 'STEAM_GEN_1_EVAPORATED', 'STEAM_GEN_1_INLET', 'STEAM_GEN_1_OUTLET', 'STEAM_GEN_1_RETURN_FLOW_PLUS_CONDENSED', 'STEAM_GEN_1_STATUS', 'STEAM_GEN_1_VENT_SWITCH', 'STEAM_GEN_2_BOILING_POINT', 'STEAM_GEN_2_EVAPORATED', 'STEAM_GEN_2_INLET', 'STEAM_GEN_2_OUTLET', 'STEAM_GEN_2_RETURN_FLOW_PLUS_CONDENSED', 'STEAM_GEN_2_STATUS', 'STEAM_GEN_2_VENT_SWITCH', 'STEAM_TURBINE_0_BYPASS_ACTUAL', 'STEAM_TURBINE_0_INSTALLED', 'STEAM_TURBINE_0_PRESSURE', 'STEAM_TURBINE_0_RPM', 'STEAM_TURBINE_0_TEMPERATURE', 'STEAM_TURBINE_0_TORQUE', 'STEAM_TURBINE_1_BYPASS_ACTUAL', 'STEAM_TURBINE_1_INSTALLED', 'STEAM_TURBINE_1_PRESSURE', 'STEAM_TURBINE_1_RPM', 'STEAM_TURBINE_1_TEMPERATURE', 'STEAM_TURBINE_1_TORQUE', 'STEAM_TURBINE_2_BYPASS_ACTUAL', 'STEAM_TURBINE_2_INSTALLED', 'STEAM_TURBINE_2_PRESSURE', 'STEAM_TURBINE_2_RPM', 'STEAM_TURBINE_2_TEMPERATURE', 'STEAM_TURBINE_2_TORQUE', 'TIME', 'TIME_DAY', 'TIME_STAMP', 'VACUUM_RETENTION_TANK_PRESSURE', 'VACUUM_RETENTION_TANK_VOLUME', 'VALVE_M01_OPEN', 'VALVE_M02_OPEN', 'VALVE_M03_OPEN', 'VALVE_PANEL_JSON', 'WEATHER_FORECAST_JSON', 'WEBSERVER_BATCH_GET', 'WEBSERVER_LIST_VARIABLES', 'WEBSERVER_LIST_VARIABLES_JSON', 'WEBSERVER_VIEW_VARIABLES'] @@ -433,7 +433,7 @@ class Poller(threading.Thread): class App(tk.Tk): def __init__(self) -> None: super().__init__() - self.title("Control Board Monitor — v1.5") + self.title("Control Board Monitor — v1.6") self.geometry("1280x860") # Canonical registry: lower-case key -> VarInfo @@ -958,6 +958,7 @@ class App(tk.Tk): finally: self.tree_menu.tk_popup(event.x_root, event.y_root) + def open_thresholds_dialog(self): sel = self.tree.selection() if not sel: @@ -975,162 +976,159 @@ class App(tk.Tk): dlg = tk.Toplevel(self) dlg.title(f"Thresholds, actions & alarms — {vi.display_name}") - dlg.geometry("980x680") + dlg.geometry("980x700") dlg.transient(self); dlg.grab_set() - t = vi.thresholds - # Auto-defaults based on current value if thresholds are empty cur = parse_first_float(vi.last_value) auto_defaults = {} if cur is not None: - auto_defaults = { - "dead_low": round(cur * 0.8, 6), - "low": round(cur * 0.9, 6), - "high": round(cur * 1.1, 6), - "extreme_high": round(cur * 1.2, 6), - } + auto_defaults = {"dead_low": round(cur*0.8, 6), "low": round(cur*0.9, 6), "high": round(cur*1.1, 6), "extreme_high": round(cur*1.2, 6)} - grid = ttk.Frame(dlg, padding=10) - grid.pack(side=tk.TOP, fill=tk.BOTH, expand=True) + # Scrollable body + outer = ttk.Frame(dlg); outer.pack(fill=tk.BOTH, expand=True) + canvas = tk.Canvas(outer, highlightthickness=0) + vsb = ttk.Scrollbar(outer, orient="vertical", command=canvas.yview) + canvas.configure(yscrollcommand=vsb.set) + vsb.pack(side=tk.RIGHT, fill=tk.Y) + canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) + grid = ttk.Frame(canvas) + win = canvas.create_window((0,0), window=grid, anchor="nw") + def _on_cfg(event=None): + canvas.configure(scrollregion=canvas.bbox("all")) + canvas.itemconfigure(win, width=canvas.winfo_width()) + grid.bind("", _on_cfg) + canvas.bind("", _on_cfg) - entries = {} - def add_row(r, label, keyname, default): - ttk.Label(grid, text=label).grid(row=r, column=0, sticky="w", pady=4) + # thresholds + ttk.Label(grid, text="DEAD LOW").grid(row=0, column=0, sticky="w", pady=2) + ttk.Label(grid, text="LOW").grid(row=1, column=0, sticky="w", pady=2) + ttk.Label(grid, text="HIGH").grid(row=2, column=0, sticky="w", pady=2) + ttk.Label(grid, text="EXTREME HIGH").grid(row=3, column=0, sticky="w", pady=2) + + def mk_val(default, keyname, row): dv = auto_defaults.get(keyname) if default is None else default sv = tk.StringVar(value="" if dv is None else str(dv)) - e = ttk.Entry(grid, textvariable=sv, width=12) - e.grid(row=r, column=1, sticky="w", padx=6) - entries[keyname] = sv + ttk.Entry(grid, textvariable=sv, width=12).grid(row=row, column=1, sticky="w", padx=6) + return sv + dead_low_var = mk_val(t.dead_low, "dead_low", 0) + low_var = mk_val(t.low, "low", 1) + high_var = mk_val(t.high, "high", 2) + extreme_high_var = mk_val(t.extreme_high, "extreme_high", 3) - add_row(0, "DEAD LOW", "dead_low", t.dead_low) - add_row(1, "LOW", "low", t.low) - add_row(2, "HIGH", "high", t.high) - add_row(3, "EXTREME HIGH", "extreme_high", t.extreme_high) + ttk.Label(grid, text="Alarm on enter:").grid(row=0, column=3, sticky="w") + alarm_dead_low = tk.BooleanVar(value=t.alarm_dead_low) + alarm_low = tk.BooleanVar(value=t.alarm_low) + alarm_high = tk.BooleanVar(value=t.alarm_high) + alarm_ext = tk.BooleanVar(value=t.alarm_extreme_high) + ttk.Checkbutton(grid, text="Dead Low", variable=alarm_dead_low).grid(row=1, column=3, sticky="w") + ttk.Checkbutton(grid, text="Low", variable=alarm_low).grid(row=2, column=3, sticky="w") + ttk.Checkbutton(grid, text="High", variable=alarm_high).grid(row=3, column=3, sticky="w") + ttk.Checkbutton(grid, text="Extreme High", variable=alarm_ext).grid(row=4, column=3, sticky="w") - ttk.Label(grid, text="Alarm on enter:").grid(row=4, column=0, sticky="w", pady=(12,4)) - alarm_vars = {} - for i, (lbl, keyname, val) in enumerate([("Dead Low", "alarm_dead_low", t.alarm_dead_low), - ("Low", "alarm_low", t.alarm_low), - ("High", "alarm_high", t.alarm_high), - ("Extreme High", "alarm_extreme_high", t.alarm_extreme_high)]): - var = tk.BooleanVar(value=val) - ttk.Checkbutton(grid, text=lbl, variable=var).grid(row=5+i//2, column=i%2+0, sticky="w", padx=6, pady=2) - alarm_vars[keyname] = var + # actions + ttk.Label(grid, text="On enter: call function (optional)").grid(row=6, column=0, sticky="w", pady=(12,4)) + ttk.Label(grid, text="Interval(s) while in state (0 = once)").grid(row=6, column=3, sticky="w") - ttk.Label(grid, text="On enter: call function (optional)").grid(row=7, column=0, sticky="w", pady=(12,4)) rows = [("Dead Low", "action_dead_low", "value_dead_low", "action_dead_low_interval"), ("Low", "action_low", "value_low", "action_low_interval"), ("Operating", "action_operating", "value_operating", "action_operating_interval"), ("High", "action_high", "value_high", "action_high_interval"), ("Extreme High", "action_extreme_high", "value_extreme_high", "action_extreme_high_interval")] - action_vars = {} - value_vars = {} - interval_vars = {} - ttk.Label(grid, text="Interval(s) while in state (0 = once)").grid(row=7, column=3, sticky="w") + action_vars = {}; value_vars = {}; interval_vars = {} for i, (label, action_key, value_key, interval_key) in enumerate(rows): - ttk.Label(grid, text=label).grid(row=8+i, column=0, sticky="w", pady=2) + r = 7+i + ttk.Label(grid, text=label).grid(row=r, column=0, sticky="w", pady=2) s = ttk.Combobox(grid, values=self.functions_list or DEFAULT_FUNCTIONS, width=44) s.set(getattr(t, action_key) or "") - s.grid(row=8+i, column=1, sticky="w", padx=6) + s.grid(row=r, column=1, sticky="w", padx=6) self._attach_search_filter_to_combobox(s, self.functions_list) action_vars[action_key] = s sv = tk.StringVar(value=getattr(t, value_key)) - ttk.Entry(grid, textvariable=sv, width=12).grid(row=8+i, column=2, sticky="w", padx=6) + ttk.Entry(grid, textvariable=sv, width=12).grid(row=r, column=2, sticky="w", padx=6) value_vars[value_key] = sv iv = tk.DoubleVar(value=getattr(t, interval_key) if getattr(t, interval_key) is not None else 1.0) - ttk.Entry(grid, textvariable=iv, width=10).grid(row=8+i, column=3, sticky="w", padx=6) + ttk.Entry(grid, textvariable=iv, width=10).grid(row=r, column=3, sticky="w", padx=6) interval_vars[interval_key] = iv - # y/z source selectors (optional) - ttk.Label(grid, text="y source (optional)").grid(row=12, column=0, sticky="w", pady=(14,4)) + # y/z sources + ttk.Label(grid, text="y source (optional)").grid(row=13, column=0, sticky="w", pady=(14,4)) y_source_var = tk.StringVar(value=t.y_source or "") y_combo = ttk.Combobox(grid, values=(self.known_variables or [vi.display_name]), textvariable=y_source_var, width=44) - y_combo.grid(row=12, column=1, sticky="w", padx=6) + y_combo.grid(row=13, column=1, sticky="w", padx=6) self._attach_search_filter_to_combobox(y_combo, self.known_variables) - ttk.Label(grid, text="z source (optional)").grid(row=12, column=2, sticky="e") + ttk.Label(grid, text="z source (optional)").grid(row=13, column=2, sticky="e") z_source_var = tk.StringVar(value=t.z_source or "") z_combo = ttk.Combobox(grid, values=(self.known_variables or [vi.display_name]), textvariable=z_source_var, width=32) - z_combo.grid(row=12, column=3, sticky="w", padx=6) + z_combo.grid(row=13, column=3, sticky="w", padx=6) self._attach_search_filter_to_combobox(z_combo, self.known_variables) - ttk.Label(grid, text="On enter: evaluate expression/snippet (x = current value) and optionally POST to a function").grid(row=13, column=0, sticky="w", pady=(14,4), columnspan=3) - - expr_vars = { - "expr_dead_low": tk.StringVar(value=t.expr_dead_low or ""), - "expr_low": tk.StringVar(value=t.expr_low or ""), - "expr_high": tk.StringVar(value=t.expr_high or ""), - "expr_extreme_high": tk.StringVar(value=t.expr_extreme_high or ""), - } - expr_target_vars = { - "expr_target_dead_low": tk.StringVar(value=t.expr_target_dead_low or ""), - "expr_target_low": tk.StringVar(value=t.expr_target_low or ""), - "expr_target_high": tk.StringVar(value=t.expr_target_high or ""), - "expr_target_extreme_high": tk.StringVar(value=t.expr_target_extreme_high or ""), - } - - for i, (label, keyname, tkey) in enumerate([("Dead Low expr", "expr_dead_low", "expr_target_dead_low"), - ("Low expr", "expr_low", "expr_target_low"), - ("Operating expr", "expr_operating", "expr_target_operating"), - ("High expr", "expr_high", "expr_target_high"), - ("Extreme High expr", "expr_extreme_high", "expr_target_extreme_high")]): - ttk.Label(grid, text=label).grid(row=14+i, column=0, sticky="w", pady=2) - ttk.Entry(grid, textvariable=expr_vars[keyname], width=64).grid(row=14+i, column=1, sticky="w", padx=6) - ttk.Label(grid, text="→ POST to function").grid(row=14+i, column=2, sticky="e") + # expressions + ttk.Label(grid, text="On enter: evaluate expression/snippet (x, y, z available) and optionally POST to a function").grid(row=15, column=0, sticky="w", pady=(14,4), columnspan=3) + expr_rows = [("Dead Low expr", "expr_dead_low", "expr_target_dead_low"), + ("Low expr", "expr_low", "expr_target_low"), + ("Operating expr", "expr_operating", "expr_target_operating"), + ("High expr", "expr_high", "expr_target_high"), + ("Extreme High expr", "expr_extreme_high", "expr_target_extreme_high")] + expr_vars = {}; expr_target_vars = {} + for i, (label, keyname, tkey) in enumerate(expr_rows): + r = 16+i + ttk.Label(grid, text=label).grid(row=r, column=0, sticky="w", pady=2) + expr_vars[keyname] = tk.StringVar(value=getattr(t, keyname) or "") + ttk.Entry(grid, textvariable=expr_vars[keyname], width=64).grid(row=r, column=1, sticky="w", padx=6) + ttk.Label(grid, text="→ POST to function").grid(row=r, column=2, sticky="e") s = ttk.Combobox(grid, values=self.functions_list or DEFAULT_FUNCTIONS, width=32) - s.set(expr_target_vars[tkey].get()) - s.grid(row=14+i, column=3, sticky="w", padx=6) + s.set(getattr(t, tkey) or "") + s.grid(row=r, column=3, sticky="w", padx=6) self._attach_search_filter_to_combobox(s, self.functions_list) - def _on_combo_select(event, varkey=tkey, combo=s): - expr_target_vars[varkey].set(combo.get()) - s.bind("<>", _on_combo_select) + expr_target_vars[tkey] = s - def save_and_close(): - def _f(v): - v = v.strip() - return float(v) if v else None + # Bottom bar + btns = ttk.Frame(dlg); btns.pack(fill=tk.X, padx=10, pady=10) + def do_save(): nt = Thresholds( - dead_low=_f(entries["dead_low"].get()), - low=_f(entries["low"].get()), - high=_f(entries["high"].get()), - extreme_high=_f(entries["extreme_high"].get()), - alarm_dead_low=alarm_vars["alarm_dead_low"].get(), - alarm_low=alarm_vars["alarm_low"].get(), - alarm_high=alarm_vars["alarm_high"].get(), - alarm_extreme_high=alarm_vars["alarm_extreme_high"].get(), + dead_low=parse_first_float(dead_low_var.get() or None), + low=parse_first_float(low_var.get() or None), + high=parse_first_float(high_var.get() or None), + extreme_high=parse_first_float(extreme_high_var.get() or None), + alarm_dead_low=bool(alarm_dead_low.get()), + alarm_low=bool(alarm_low.get()), + alarm_high=bool(alarm_high.get()), + alarm_extreme_high=bool(alarm_ext.get()), action_dead_low=action_vars["action_dead_low"].get() or None, value_dead_low=value_vars["value_dead_low"].get(), + action_dead_low_interval=float(interval_vars["action_dead_low_interval"].get() or 1.0), action_low=action_vars["action_low"].get() or None, value_low=value_vars["value_low"].get(), + action_low_interval=float(interval_vars["action_low_interval"].get() or 1.0), + action_operating=action_vars["action_operating"].get() or None, + value_operating=value_vars["value_operating"].get(), + action_operating_interval=float(interval_vars["action_operating_interval"].get() or 1.0), action_high=action_vars["action_high"].get() or None, value_high=value_vars["value_high"].get(), + action_high_interval=float(interval_vars["action_high_interval"].get() or 1.0), action_extreme_high=action_vars["action_extreme_high"].get() or None, value_extreme_high=value_vars["value_extreme_high"].get(), - action_dead_low_interval=float(interval_vars["action_dead_low_interval"].get() or 1.0), - action_low_interval=float(interval_vars["action_low_interval"].get() or 1.0), - action_high_interval=float(interval_vars["action_high_interval"].get() or 1.0), action_extreme_high_interval=float(interval_vars["action_extreme_high_interval"].get() or 1.0), - expr_dead_low=expr_vars["expr_dead_low"].get().strip() or None, - expr_low=expr_vars["expr_low"].get().strip() or None, - expr_high=expr_vars["expr_high"].get().strip() or None, - expr_extreme_high=expr_vars["expr_extreme_high"].get().strip() or None, - expr_target_dead_low=expr_target_vars["expr_target_dead_low"].get().strip() or None, - expr_target_low=expr_target_vars["expr_target_low"].get().strip() or None, - expr_target_high=expr_target_vars["expr_target_high"].get().strip() or None, - expr_target_extreme_high=expr_target_vars["expr_target_extreme_high"].get().strip() or None, + expr_dead_low=(expr_vars["expr_dead_low"].get().strip() or None), + expr_low=(expr_vars["expr_low"].get().strip() or None), + expr_operating=(expr_vars["expr_operating"].get().strip() or None), + expr_high=(expr_vars["expr_high"].get().strip() or None), + expr_extreme_high=(expr_vars["expr_extreme_high"].get().strip() or None), + expr_target_dead_low=(expr_target_vars["expr_target_dead_low"].get().strip() or None), + expr_target_low=(expr_target_vars["expr_target_low"].get().strip() or None), + expr_target_operating=(expr_target_vars["expr_target_operating"].get().strip() or None), + expr_target_high=(expr_target_vars["expr_target_high"].get().strip() or None), + expr_target_extreme_high=(expr_target_vars["expr_target_extreme_high"].get().strip() or None), y_source=(y_source_var.get().strip() or None), z_source=(z_source_var.get().strip() or None), ) vi.thresholds = nt - self.vars[vi.key] = vi dlg.destroy() - btns = ttk.Frame(dlg, padding=10) - btns.pack(side=tk.BOTTOM, fill=tk.X) - ttk.Button(btns, text="Save", command=save_and_close).pack(side=tk.RIGHT, padx=4) - ttk.Button(btns, text="Cancel", command=dlg.destroy).pack(side=tk.RIGHT, padx=4) + 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) - # --- State evaluation per spec + color gradients --- def evaluate_thresholds(self, vi: VarInfo, value: str) -> None: x = parse_first_float(value)