Console updates: add idempotent pre-checks before sending commands; skip when values already match. Retain params toggled only if needed; rule definitions compared via RuleN 5; RuleN auto-enable skipped if already ON. Add helper _get_console_param_value. Confirm config_other already conditional.

This commit is contained in:
Mike Geppert 2025-08-08 23:16:18 -05:00
parent 7d1755b44a
commit c31bcbdf85

View File

@ -1362,6 +1362,17 @@ class TasmotaDiscovery:
if param in console_params:
try:
final_value = console_params[param]
# Pre-check current value; skip if already at desired state
current_val, ok = self._get_console_param_value(ip, name, param)
if ok:
desired_cmp = str(final_value).strip().lower()
current_cmp = str(current_val or "").strip().lower()
# Map 1/0 to on/off for retain-like responses
if current_cmp in ("1", "0") and desired_cmp in ("on", "off"):
current_cmp = "on" if current_cmp == "1" else "off"
if current_cmp == desired_cmp:
self.logger.debug(f"{name}: {param} already {final_value}, skipping retain toggle")
continue
# Set opposite state first
opposite_value = "On" if final_value.lower() == "off" else "Off"
@ -1508,6 +1519,41 @@ class TasmotaDiscovery:
# If this is in the config, we'll respect it, but log that it's not needed
self.logger.debug(f"{name}: Note: {param} is not needed with auto-enable feature")
# Determine if update is needed before sending
should_send = True
try:
current_val, ok = self._get_console_param_value(ip, name, param)
if ok:
if param.lower().startswith('rule') and param.lower() == param and param[-1].isdigit():
# Compare rule definitions (whitespace-insensitive, case-insensitive)
import re as _re
def _norm_rule(s):
s = str(s or "")
s = s.strip().lower()
s = _re.sub(r"\s+", " ", s)
return s
if _norm_rule(current_val) == _norm_rule(value):
self.logger.debug(f"{name}: {param} rule already matches definition, skipping")
should_send = False
else:
# Generic comparison
desired_cmp = str(value).strip().lower()
current_cmp = str(current_val or "").strip().lower()
# Normalize common ON/OFF vs 1/0 forms
if current_cmp in ("on", "off") and desired_cmp in ("1", "0"):
current_cmp = "1" if current_cmp == "on" else "0"
if current_cmp in ("1", "0") and desired_cmp in ("on", "off"):
current_cmp = "on" if current_cmp == "1" else "off"
if current_cmp == desired_cmp:
self.logger.debug(f"{name}: {param} already set to {value}, skipping")
should_send = False
except Exception:
# If pre-check fails, fall back to sending command
pass
if not should_send:
continue
# Regular console parameter
# Special handling for rule parameters to properly encode the URL
if param.lower().startswith('rule') and param.lower() == param and param[-1].isdigit():
@ -1601,6 +1647,16 @@ class TasmotaDiscovery:
continue
# Rule auto-enabling
# Pre-check if rule already enabled
try:
current_val, ok = self._get_console_param_value(ip, name, rule_enable_param)
if ok:
state = str(current_val or "").strip().lower()
if state in ("on", "1", "enabled", "active"):
self.logger.debug(f"{name}: {rule_enable_param} already enabled, skipping")
continue
except Exception:
pass
url = f"http://{ip}/cm?cmnd={rule_enable_param}%201"
if with_retry:
@ -1655,6 +1711,81 @@ class TasmotaDiscovery:
return console_updated
def _get_console_param_value(self, ip, name, param):
"""Query the device for the current value of a console parameter.
Returns (value, True) on success, (None, False) on failure.
Special handling:
- Lowercase ruleN: returns the current rule definition text using RuleN 5
- Uppercase RuleN: returns the current enable state (ON/OFF or 1/0)
"""
try:
# Rules handling
if param.lower().startswith('rule') and param[-1].isdigit():
rule_num = param[-1]
# If lowercase 'ruleN', fetch rule definition using 'RuleN 5'
if param.islower():
url = f"http://{ip}/cm?cmnd=Rule{rule_num}%205"
response = requests.get(url, timeout=5)
# Try to parse JSON first
try:
data = response.json()
# Common keys: 'Rules' may contain the definition
if isinstance(data, dict):
for k, v in data.items():
if str(k).lower() == 'rules':
return v, True
# Fallback: first string value
for v in data.values():
if isinstance(v, str):
return v, True
return str(data), True
except Exception:
# Fallback to text parsing
text = response.text or ''
return text, True
else:
# Uppercase 'RuleN' - fetch enable state
url = f"http://{ip}/cm?cmnd=Rule{rule_num}"
response = requests.get(url, timeout=5)
try:
data = response.json()
# Expect something like {"Rule1":"ON"}
if isinstance(data, dict):
# Try exact key first
key = f"Rule{rule_num}"
if key in data:
return data[key], True
# Fallback: any ON/OFF value
for v in data.values():
if isinstance(v, (str, int)):
return v, True
return str(data), True
except Exception:
text = response.text or ''
return text, True
# Generic parameter query - send the command name without a value
url = f"http://{ip}/cm?cmnd={param}"
response = requests.get(url, timeout=5)
try:
data = response.json()
if isinstance(data, dict) and data:
# Prefer an exact key match (case-insensitive)
param_lower = param.lower()
for k, v in data.items():
if str(k).lower() == param_lower:
return v, True
# Fallback: first value
first_val = next(iter(data.values()))
return first_val, True
# If not a dict, return as string
return str(data), True
except Exception:
# Fallback to raw text
return response.text, True
except requests.exceptions.RequestException as e:
self.logger.debug(f"{name}: Failed to query current value for {param}: {e}")
return None, False
def apply_config_other(self, ip, name):
"""Wrapper for applying config_other (template) settings."""
return self.check_and_update_template(ip, name)