feat: Add auto-listen feature with tooltip in SmartIR capture tool
This commit is contained in:
@@ -119,16 +119,24 @@ def _options_schema(platform: str, defaults: dict | None = None) -> vol.Schema:
|
||||
return vol.Optional(key, description={"suggested_value": val})
|
||||
return vol.Optional(key)
|
||||
|
||||
# Power sensors are only ever checked for on/off state (see
|
||||
# SmartIRClimate._async_power_sensor_changed and the equivalent methods in
|
||||
# fan.py, light.py, media_player.py, which compare .state to STATE_ON/STATE_OFF)
|
||||
# so restrict the picker to domains that expose a binary on/off state.
|
||||
power_sensor_selector = EntitySelector(
|
||||
EntitySelectorConfig(domain=["binary_sensor", "switch", "input_boolean"])
|
||||
)
|
||||
|
||||
if platform == "climate":
|
||||
return vol.Schema(
|
||||
{
|
||||
_opt(CONF_TEMPERATURE_SENSOR): EntitySelector(
|
||||
EntitySelectorConfig(domain="sensor")
|
||||
EntitySelectorConfig(domain="sensor", device_class="temperature")
|
||||
),
|
||||
_opt(CONF_HUMIDITY_SENSOR): EntitySelector(
|
||||
EntitySelectorConfig(domain="sensor")
|
||||
EntitySelectorConfig(domain="sensor", device_class="humidity")
|
||||
),
|
||||
_opt(CONF_POWER_SENSOR): EntitySelector(EntitySelectorConfig()),
|
||||
_opt(CONF_POWER_SENSOR): power_sensor_selector,
|
||||
vol.Optional(
|
||||
CONF_POWER_SENSOR_RESTORE_STATE,
|
||||
default=bool(d.get(CONF_POWER_SENSOR_RESTORE_STATE, False)),
|
||||
@@ -144,7 +152,7 @@ def _options_schema(platform: str, defaults: dict | None = None) -> vol.Schema:
|
||||
else ""
|
||||
)
|
||||
fields: dict = {
|
||||
_opt(CONF_POWER_SENSOR): EntitySelector(EntitySelectorConfig()),
|
||||
_opt(CONF_POWER_SENSOR): power_sensor_selector,
|
||||
vol.Optional(
|
||||
CONF_DEVICE_CLASS,
|
||||
description={"suggested_value": d.get(CONF_DEVICE_CLASS, "tv")},
|
||||
@@ -164,9 +172,7 @@ def _options_schema(platform: str, defaults: dict | None = None) -> vol.Schema:
|
||||
return vol.Schema(fields)
|
||||
|
||||
# fan and light
|
||||
return vol.Schema(
|
||||
{_opt(CONF_POWER_SENSOR): EntitySelector(EntitySelectorConfig())}
|
||||
)
|
||||
return vol.Schema({_opt(CONF_POWER_SENSOR): power_sensor_selector})
|
||||
|
||||
|
||||
def _parse_options(platform: str, user_input: dict) -> dict:
|
||||
|
||||
@@ -97,6 +97,37 @@ def combo_label(combo: dict) -> str:
|
||||
return " / ".join(parts)
|
||||
|
||||
|
||||
class _Tooltip:
|
||||
"""Simple hover tooltip for tkinter/ttk widgets."""
|
||||
|
||||
def __init__(self, widget, text: str, wraplength: int = 320):
|
||||
self.widget = widget
|
||||
self.text = text
|
||||
self.wraplength = wraplength
|
||||
self._tip: tk.Toplevel | None = None
|
||||
widget.bind("<Enter>", self._show)
|
||||
widget.bind("<Leave>", self._hide)
|
||||
|
||||
def _show(self, _event=None):
|
||||
if self._tip is not None:
|
||||
return
|
||||
x = self.widget.winfo_rootx()
|
||||
y = self.widget.winfo_rooty() + self.widget.winfo_height() + 4
|
||||
self._tip = tw = tk.Toplevel(self.widget)
|
||||
tw.wm_overrideredirect(True)
|
||||
tw.wm_geometry(f"+{x}+{y}")
|
||||
tk.Label(
|
||||
tw, text=self.text, justify="left", background="#ffffe0",
|
||||
relief="solid", borderwidth=1, wraplength=self.wraplength,
|
||||
font=("TkDefaultFont", 9), padx=6, pady=4,
|
||||
).pack()
|
||||
|
||||
def _hide(self, _event=None):
|
||||
if self._tip is not None:
|
||||
self._tip.destroy()
|
||||
self._tip = None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Main App
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -118,6 +149,7 @@ class SmartIRCapture(tk.Tk):
|
||||
self._listen_timer = None
|
||||
self.mqtt_client = None
|
||||
self._friendly_name: str = ""
|
||||
self._auto_listen_var = tk.BooleanVar(value=False)
|
||||
|
||||
self._build_ui()
|
||||
self.protocol("WM_DELETE_WINDOW", self._on_close)
|
||||
@@ -407,6 +439,19 @@ class SmartIRCapture(tk.Tk):
|
||||
command=self._do_go_back, width=14, state="disabled")
|
||||
self._btn_back.pack(side=tk.RIGHT)
|
||||
|
||||
self._auto_listen_cb = ttk.Checkbutton(
|
||||
btn_frame, text="🔁 Auto-listen for next code",
|
||||
variable=self._auto_listen_var, command=self._on_auto_listen_toggled)
|
||||
self._auto_listen_cb.pack(side=tk.RIGHT, padx=(0, 12))
|
||||
_Tooltip(
|
||||
self._auto_listen_cb,
|
||||
"When enabled, listening for the next IR code starts automatically "
|
||||
"right after you Confirm, Skip, or Copy-to-remaining — so you don't "
|
||||
"need to click Capture again between codes.\n\n"
|
||||
"Retry always re-arms listening on its own regardless of this setting. "
|
||||
"Previous and re-capturing from Overview stay manual either way."
|
||||
)
|
||||
|
||||
# MQTT log
|
||||
log_frame = ttk.LabelFrame(tab, text="MQTT Log", padding=4)
|
||||
log_frame.grid(row=4, column=0, sticky="ew")
|
||||
@@ -541,6 +586,7 @@ class SmartIRCapture(tk.Tk):
|
||||
self._sv["swing_modes"].set(", ".join(cfg.get("swingModes", [])))
|
||||
self._sv["single_code_modes"].set(", ".join(cfg.get("singleCodeModes", [])))
|
||||
self._sv["excluded_modes"].set(", ".join(cfg.get("excludedModes", [])))
|
||||
self._auto_listen_var.set(bool(data.get("auto_listen", False)))
|
||||
self.session = data
|
||||
n = len(data.get("codes", {}))
|
||||
self._setup_status_var.set(f"✓ Session loaded — {n} code(s) captured so far.")
|
||||
@@ -548,6 +594,7 @@ class SmartIRCapture(tk.Tk):
|
||||
messagebox.showerror("Load error", str(exc))
|
||||
|
||||
def _save_session(self):
|
||||
self.session["auto_listen"] = self._auto_listen_var.get()
|
||||
tmp = SESSION_FILE.with_suffix(".tmp")
|
||||
try:
|
||||
with open(tmp, "w") as f:
|
||||
@@ -556,6 +603,10 @@ class SmartIRCapture(tk.Tk):
|
||||
except Exception as exc:
|
||||
self._log(f"[WARN] Session save failed: {exc}")
|
||||
|
||||
def _on_auto_listen_toggled(self):
|
||||
"""Persist the auto-listen preference as soon as it's toggled."""
|
||||
self._save_session()
|
||||
|
||||
def _parse_config(self) -> dict | None:
|
||||
try:
|
||||
host = self._sv["mqtt_host"].get().strip()
|
||||
@@ -611,7 +662,11 @@ class SmartIRCapture(tk.Tk):
|
||||
return
|
||||
|
||||
existing_codes = self.session.get("codes", {})
|
||||
self.session = {"config": config, "codes": existing_codes}
|
||||
self.session = {
|
||||
"config": config,
|
||||
"codes": existing_codes,
|
||||
"auto_listen": self._auto_listen_var.get(),
|
||||
}
|
||||
self._friendly_name = config["device_name"]
|
||||
self.combos = build_combos(config)
|
||||
self.current_idx = 0
|
||||
@@ -705,6 +760,8 @@ class SmartIRCapture(tk.Tk):
|
||||
self.current_idx += 1
|
||||
self._update_capture_ui()
|
||||
self._update_progress()
|
||||
if self._auto_listen_var.get() and self._current_combo() is not None:
|
||||
self._do_capture()
|
||||
|
||||
# Descriptions shown under each field value in the target breakdown
|
||||
_OP_MODE_DESC = {
|
||||
|
||||
Reference in New Issue
Block a user