summaryrefslogtreecommitdiff
path: root/ui/control_panel.py
diff options
context:
space:
mode:
Diffstat (limited to 'ui/control_panel.py')
-rw-r--r--ui/control_panel.py53
1 files changed, 47 insertions, 6 deletions
diff --git a/ui/control_panel.py b/ui/control_panel.py
index 7a3e416..6e78e89 100644
--- a/ui/control_panel.py
+++ b/ui/control_panel.py
@@ -20,6 +20,9 @@ an instance into ControlPanel.add_widget().
from __future__ import annotations
from typing import Optional
+import math as _math
+from typing import Any
+
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QPushButton, QSlider, QDoubleSpinBox, QComboBox,
@@ -44,7 +47,9 @@ class ControlWidget(QFrame):
def __init__(self, title: str, icon: str = "⚙",
device_id: str = "", channel_id: str = "",
registry: Optional[DeviceRegistry] = None,
- active_low: bool = False):
+ active_low: bool = False,
+ processor=None,
+ on_action_script: str = ""):
super().__init__()
self.title = title
self.icon = icon
@@ -52,9 +57,20 @@ class ControlWidget(QFrame):
self.channel_id = channel_id
self.registry = registry
self.active_low = active_low
+ self._processor = processor
+ self._on_action_fn = None
+ if on_action_script.strip():
+ self._compile_action(on_action_script)
self.setObjectName("controlWidget")
self._build_frame()
+ def _compile_action(self, script: str):
+ try:
+ code = compile(script, "<control_script>", "exec")
+ self._on_action_fn = code
+ except SyntaxError as e:
+ print(f"[Control '{self.title}'] script syntax error: {e}")
+
def _logic_level(self, enabled: bool) -> float:
"""Map logical ON/OFF to electrical level, respecting active-low outputs."""
if self.active_low:
@@ -112,6 +128,27 @@ class ControlWidget(QFrame):
f"check device type and channel ID")
self.value_changed.emit(self.channel_id, value)
+ if self._on_action_fn is not None:
+ try:
+ sv: dict = {}
+ channels: dict = {}
+ if self._processor is not None:
+ with self._processor._lock:
+ sv = self._processor._script_vars
+ channels = {ch_id: v
+ for (_, ch_id), (_, v) in self._processor._latest.items()}
+ ns: dict = {
+ "value": value,
+ "channel_id": self.channel_id,
+ "device_id": self.device_id,
+ "vars": sv,
+ "channels": channels,
+ "math": _math,
+ }
+ exec(self._on_action_fn, ns)
+ except Exception as e:
+ print(f"[Control '{self.title}'] script error: {e}")
+
# ══════════════════════════════════════════════════════════════════════════════
# On/Off Switch
@@ -480,9 +517,10 @@ class ControlPanel(QWidget):
controls_changed = pyqtSignal() # emitted whenever widgets are added/edited/removed
- def __init__(self, registry: DeviceRegistry):
+ def __init__(self, registry: DeviceRegistry, processor=None):
super().__init__()
- self.registry = registry
+ self.registry = registry
+ self.processor = processor
self._widgets: list[ControlWidget] = []
self._specs: list = [] # parallel list of ControlSpec
self._build()
@@ -560,7 +598,7 @@ class ControlPanel(QWidget):
def _add_widget_from_spec(self, spec):
"""Instantiate a ControlWidget from a ControlSpec and add to panel."""
from ui.control_editor import ControlSpec as CS
- widget = _build_widget_from_spec(spec, self.registry)
+ widget = _build_widget_from_spec(spec, self.registry, self.processor)
if widget is None:
return
wrapper = self._make_wrapper(widget, spec)
@@ -641,7 +679,7 @@ class ControlPanel(QWidget):
self._widgets.pop(idx); self._specs.pop(idx)
# Insert new one at same position
- new_widget = _build_widget_from_spec(new_spec, self.registry)
+ new_widget = _build_widget_from_spec(new_spec, self.registry, self.processor)
if new_widget is None:
return
new_wrapper = self._make_wrapper(new_widget, new_spec)
@@ -674,7 +712,8 @@ class ControlPanel(QWidget):
# ── Factory — build a ControlWidget from a ControlSpec ─────────────────────────
-def _build_widget_from_spec(spec, registry: DeviceRegistry) -> Optional[ControlWidget]:
+def _build_widget_from_spec(spec, registry: DeviceRegistry,
+ processor=None) -> Optional[ControlWidget]:
"""Instantiate the right ControlWidget subclass from a ControlSpec."""
# Base kwargs — icon is NOT included here; it's passed explicitly below
# so subclasses never get a double-value collision.
@@ -683,6 +722,8 @@ def _build_widget_from_spec(spec, registry: DeviceRegistry) -> Optional[ControlW
channel_id=spec.channel_id,
registry=registry,
active_low=getattr(spec, "active_low", False),
+ processor=processor,
+ on_action_script=getattr(spec, "on_action_script", ""),
)
t = spec.control_type
ttl = spec.title