TasmotaManager/console_settings.py
Mike Geppert 9c22168f79 Refactor: Split TasmotaManager into modular structure
- Created modular Python files (main, utils, discovery, configuration, console_settings, unknown_devices, reporting, unifi_client)
- Moved documentation files to docs/
- Moved data files to data/
- Removed old monolithic TasmotaManager.py and TasmotaManager_fixed.py
- Updated .gitignore and pyproject.toml
- All functionality preserved, command-line interface unchanged
Version: 2.0.0
2025-10-29 16:38:03 +00:00

249 lines
8.8 KiB
Python

"""Console settings and parameter management."""
import logging
import time
from typing import Dict, List, Optional, Tuple
from utils import send_tasmota_command, retry_command, get_hostname_base
class ConsoleSettingsManager:
"""Handles console parameter configuration for Tasmota devices."""
def __init__(self, config: dict, logger: Optional[logging.Logger] = None):
"""
Initialize console settings manager.
Args:
config: Configuration dictionary
logger: Optional logger instance
"""
self.config = config
self.logger = logger or logging.getLogger(__name__)
self.command_failures = {} # Track failed commands by device
def apply_console_settings(self, device: dict, device_details: dict) -> Tuple[bool, str]:
"""
Apply console settings to a device.
Args:
device: Device info dictionary
device_details: Detailed device information
Returns:
Tuple of (success, status_message)
"""
device_name = device.get('name', 'Unknown')
device_ip = device.get('ip', '')
if not device_ip:
return False, "No IP address"
# Get hostname base for template matching
hostname = device_details.get('StatusNET', {}).get('Hostname', device_name)
hostname_base = get_hostname_base(hostname)
# Find which console_set to use for this device
console_set_name = self._get_console_set_name(hostname_base)
if not console_set_name:
self.logger.debug(f"{device_name}: No console settings configured")
return True, "No console settings"
# Get the console command list
console_commands = self._get_console_commands(console_set_name)
if not console_commands:
self.logger.debug(f"{device_name}: Console set '{console_set_name}' is empty")
return True, "Empty console set"
self.logger.info(f"{device_name}: Applying {len(console_commands)} console settings from '{console_set_name}'")
# Apply each console command
failed_commands = []
for command in console_commands:
if not command or not command.strip():
continue # Skip empty commands
success = self._apply_single_command(device_ip, device_name, command)
if not success:
failed_commands.append(command)
# Track failures for summary
if failed_commands:
if device_name not in self.command_failures:
self.command_failures[device_name] = []
self.command_failures[device_name].extend(failed_commands)
if failed_commands:
return False, f"Failed: {len(failed_commands)} commands"
self.logger.info(f"{device_name}: All console settings applied successfully")
return True, "Applied"
def _get_console_set_name(self, hostname_base: str) -> Optional[str]:
"""
Get the console_set name for a device based on hostname.
Args:
hostname_base: Base hostname of device
Returns:
str: Console set name or None
"""
device_list = self.config.get('device_list', {})
for template_name, template_data in device_list.items():
if hostname_base.lower() in template_name.lower():
return template_data.get('console_set')
return None
def _get_console_commands(self, console_set_name: str) -> List[str]:
"""
Get console commands from a named console set.
Args:
console_set_name: Name of the console set
Returns:
list: List of console commands
"""
console_set = self.config.get('console_set', {})
if isinstance(console_set, dict):
commands = console_set.get(console_set_name, [])
if isinstance(commands, list):
return commands
return []
def _apply_single_command(self, device_ip: str, device_name: str, command: str) -> bool:
"""
Apply a single console command to a device.
Args:
device_ip: Device IP address
device_name: Device name for logging
command: Console command to apply
Returns:
bool: True if successful
"""
# Parse command into parameter and value
parts = command.split(None, 1)
if not parts:
return True # Empty command, skip
param_name = parts[0]
param_value = parts[1] if len(parts) > 1 else ""
self.logger.debug(f"{device_name}: Setting {param_name} = {param_value}")
# Handle Retain parameters - set opposite first, then desired state
if param_name.endswith('Retain'):
opposite_value = 'Off' if param_value.lower() in ['on', '1', 'true'] else 'On'
# Set opposite first
opposite_command = f"{param_name}%20{opposite_value}"
result, success = send_tasmota_command(
device_ip, opposite_command, timeout=5, logger=self.logger
)
if not success:
self.logger.warning(f"{device_name}: Failed to set {param_name} to opposite state")
time.sleep(0.5) # Brief delay between commands
# Send the actual command
escaped_command = command.replace(' ', '%20')
result, success = retry_command(
lambda: send_tasmota_command(device_ip, escaped_command, timeout=5, logger=self.logger),
max_attempts=3,
delay=1.0,
logger=self.logger,
device_name=device_name
)
if not success:
self.logger.error(f"{device_name}: Failed to set {param_name} after 3 attempts")
return False
# Verify the command was applied (if possible)
if not self._verify_command(device_ip, device_name, param_name, param_value):
self.logger.warning(f"{device_name}: Verification failed for {param_name}")
# Don't return False here - some commands can't be verified
# Check if this is a rule definition - if so, enable it
if param_name.lower().startswith('rule'):
rule_number = param_name.lower().replace('rule', '')
if rule_number.isdigit():
enable_command = f"Rule{rule_number}%201"
self.logger.debug(f"{device_name}: Enabling rule{rule_number}")
result, success = send_tasmota_command(
device_ip, enable_command, timeout=5, logger=self.logger
)
if not success:
self.logger.warning(f"{device_name}: Failed to enable rule{rule_number}")
time.sleep(0.3) # Brief delay between commands
return True
def _verify_command(self, device_ip: str, device_name: str,
param_name: str, expected_value: str) -> bool:
"""
Verify a command was applied (where possible).
Args:
device_ip: Device IP address
device_name: Device name for logging
param_name: Parameter name
expected_value: Expected value
Returns:
bool: True if verified or verification not possible
"""
# Only verify certain parameters
verifiable = ['PowerOnState', 'SetOption']
if not any(param_name.startswith(v) for v in verifiable):
return True # Can't verify, assume success
# Get current value
result, success = send_tasmota_command(
device_ip, param_name, timeout=5, logger=self.logger
)
if not success or not result:
return True # Can't verify, assume success
# Check if value matches
current_value = result.get(param_name, '')
if str(current_value) == str(expected_value):
return True
return False
def print_failure_summary(self):
"""Print summary of all command failures."""
if not self.command_failures:
return
self.logger.error("=" * 60)
self.logger.error("COMMAND FAILURE SUMMARY")
self.logger.error("=" * 60)
for device_name, failed_commands in self.command_failures.items():
self.logger.error(f"\n{device_name}:")
for cmd in failed_commands:
self.logger.error(f" - {cmd}")
self.logger.error("=" * 60)