From 2d3c867e845496cadafc35779531f95eec853211 Mon Sep 17 00:00:00 2001 From: Mike Geppert Date: Tue, 5 Aug 2025 02:29:30 -0500 Subject: [PATCH] Implement proper Retain parameter handling and documentation. For all Retain parameters (ButtonRetain, SwitchRetain, PowerRetain), set the opposite state first before applying the final state to ensure MQTT broker retain settings are properly updated. Update documentation to explain that Retain parameters represent the final state. --- CONSOLE_COMMANDS.md | 8 ++ README.md | 10 +++ TasmotaManager.py | 62 ++++++++------ test_retain_parameters.py | 170 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 224 insertions(+), 26 deletions(-) create mode 100644 test_retain_parameters.py diff --git a/CONSOLE_COMMANDS.md b/CONSOLE_COMMANDS.md index 5022d99..6a399c6 100644 --- a/CONSOLE_COMMANDS.md +++ b/CONSOLE_COMMANDS.md @@ -27,12 +27,20 @@ The `console` section in the `network_configuration.json` file allows you to con ## MQTT Retain Settings +> **Important Note**: For all Retain parameters, the TasmotaManager script automatically sets the opposite state first before applying the final state specified in the configuration. This is necessary because the changes (not the final state) are what create the update of the Retain state at the MQTT server. The values in the configuration represent the final desired state. + | Command | Values | Description | |---------|--------|-------------| | `SwitchRetain` | `On`, `Off` | Controls whether MQTT retain flag is used on switch press messages. Default: `Off` | | `ButtonRetain` | `On`, `Off` | Controls whether MQTT retain flag is used on button press messages. Default: `Off` | | `PowerRetain` | `On`, `Off` | Controls whether MQTT retain flag is used on power state messages. Default: `Off` | +For example, if you specify `"PowerRetain": "On"` in your configuration: +1. The script will first set `PowerRetain Off` +2. Then set `PowerRetain On` + +This ensures that the MQTT broker's retain settings are properly updated. + ## Power Settings | Command | Values | Description | diff --git a/README.md b/README.md index aae5e85..9a3b2e5 100644 --- a/README.md +++ b/README.md @@ -130,6 +130,16 @@ The script supports setting Tasmota console parameters via the `console` section - Configure retain flags for various message types - Apply any other Tasmota console commands +### Retain Parameters Behavior + +For all Retain parameters (`ButtonRetain`, `SwitchRetain`, `PowerRetain`), the script automatically sets the opposite state first before applying the final state specified in the configuration. This is necessary because the changes (not the final state) are what create the update of the Retain state at the MQTT server. + +For example, if you specify `"PowerRetain": "On"` in your configuration: +1. The script will first set `PowerRetain Off` +2. Then set `PowerRetain On` + +This ensures that the MQTT broker's retain settings are properly updated. The values in the configuration represent the final desired state of each Retain parameter. + ### Automatic Rule Enabling The script automatically enables rules when they are defined. If you include a rule definition (e.g., `rule1`, `rule2`, `rule3`) in the console section, the script will automatically send the corresponding enable command (`Rule1 1`, `Rule2 1`, `Rule3 1`) to the device. This means you no longer need to include both the rule definition and the enable command in your configuration. diff --git a/TasmotaManager.py b/TasmotaManager.py index 4232854..d7a33f0 100644 --- a/TasmotaManager.py +++ b/TasmotaManager.py @@ -725,38 +725,48 @@ class TasmotaDiscovery: if console_params: self.logger.info(f"{name}: Setting console parameters from configuration") - # Special handling for ButtonRetain - need to send "On" first, then "Off" to clear MQTT broker retain settings - try: - # First ButtonRetain command (On) - url = f"http://{ip}/cm?cmnd=ButtonRetain%20On" - response = requests.get(url, timeout=5) - if response.status_code == 200: - self.logger.debug(f"{name}: Set ButtonRetain to On (step 1 of 2 to clear MQTT broker retain settings)") - console_updated = True - else: - self.logger.error(f"{name}: Failed to set ButtonRetain to On") - - # Small delay to ensure commands are processed in order - time.sleep(0.5) - - # Second ButtonRetain command (Off) - url = f"http://{ip}/cm?cmnd=ButtonRetain%20Off" - response = requests.get(url, timeout=5) - if response.status_code == 200: - self.logger.debug(f"{name}: Set ButtonRetain to Off (step 2 of 2 to clear MQTT broker retain settings)") - console_updated = True - else: - self.logger.error(f"{name}: Failed to set ButtonRetain to Off") - except requests.exceptions.RequestException as e: - self.logger.error(f"{name}: Error setting ButtonRetain commands: {str(e)}") + # Special handling for Retain parameters - need to send opposite state first, then final state + # This is necessary because the changes are what create the update of the Retain state at the MQTT server + retain_params = ["ButtonRetain", "SwitchRetain", "PowerRetain"] + + # Process Retain parameters first + for param in retain_params: + if param in console_params: + try: + final_value = console_params[param] + # Set opposite state first + opposite_value = "On" if final_value.lower() == "off" else "Off" + + # First command (opposite state) + url = f"http://{ip}/cm?cmnd={param}%20{opposite_value}" + response = requests.get(url, timeout=5) + if response.status_code == 200: + self.logger.debug(f"{name}: Set {param} to {opposite_value} (step 1 of 2 to update MQTT broker retain settings)") + console_updated = True + else: + self.logger.error(f"{name}: Failed to set {param} to {opposite_value}") + + # Small delay to ensure commands are processed in order + time.sleep(0.5) + + # Second command (final state) + url = f"http://{ip}/cm?cmnd={param}%20{final_value}" + response = requests.get(url, timeout=5) + if response.status_code == 200: + self.logger.debug(f"{name}: Set {param} to {final_value} (step 2 of 2 to update MQTT broker retain settings)") + console_updated = True + else: + self.logger.error(f"{name}: Failed to set {param} to {final_value}") + except requests.exceptions.RequestException as e: + self.logger.error(f"{name}: Error setting {param} commands: {str(e)}") # Process all other console parameters # Track rules that need to be enabled rules_to_enable = {} for param, value in console_params.items(): - # Skip ButtonRetain as it's handled specially above - if param == "ButtonRetain": + # Skip Retain parameters as they're handled specially above + if param in retain_params: continue # Check if this is a rule definition (lowercase rule1, rule2, etc.) diff --git a/test_retain_parameters.py b/test_retain_parameters.py new file mode 100644 index 0000000..a2831ba --- /dev/null +++ b/test_retain_parameters.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python3 +import json +import requests +import time +import logging +import os +import sys + +# Add the current directory to the path so we can import TasmotaManager +sys.path.append(os.path.dirname(os.path.abspath(__file__))) +from TasmotaManager import TasmotaDiscovery + +# Set up logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s', + datefmt='%Y-%m-%d %H:%M:%S' +) +logger = logging.getLogger(__name__) + +# Test device IP - replace with a real Tasmota device IP on your network +TEST_DEVICE_IP = "192.168.8.184" # Using the first device from TasmotaDevices.json + +def reset_retain_parameters(): + """Reset all retain parameters to a known state""" + logger.info("Resetting all retain parameters to a known state") + + # Reset ButtonRetain + url = f"http://{TEST_DEVICE_IP}/cm?cmnd=ButtonRetain%20Off" + response = requests.get(url, timeout=5) + logger.info(f"Reset ButtonRetain to Off: {response.text}") + + # Reset SwitchRetain + url = f"http://{TEST_DEVICE_IP}/cm?cmnd=SwitchRetain%20Off" + response = requests.get(url, timeout=5) + logger.info(f"Reset SwitchRetain to Off: {response.text}") + + # Reset PowerRetain + url = f"http://{TEST_DEVICE_IP}/cm?cmnd=PowerRetain%20Off" + response = requests.get(url, timeout=5) + logger.info(f"Reset PowerRetain to Off: {response.text}") + + # Wait for commands to take effect + time.sleep(1) + +def check_retain_status(): + """Check the current status of retain parameters on the device""" + logger.info("Checking retain parameters status") + + # Check ButtonRetain + url = f"http://{TEST_DEVICE_IP}/cm?cmnd=ButtonRetain" + response = requests.get(url, timeout=5) + button_retain = response.text + logger.info(f"ButtonRetain status: {button_retain}") + + # Check SwitchRetain + url = f"http://{TEST_DEVICE_IP}/cm?cmnd=SwitchRetain" + response = requests.get(url, timeout=5) + switch_retain = response.text + logger.info(f"SwitchRetain status: {switch_retain}") + + # Check PowerRetain + url = f"http://{TEST_DEVICE_IP}/cm?cmnd=PowerRetain" + response = requests.get(url, timeout=5) + power_retain = response.text + logger.info(f"PowerRetain status: {power_retain}") + + return button_retain, switch_retain, power_retain + +def test_retain_parameters(): + """Test the retain parameters handling""" + logger.info("Testing retain parameters handling") + + # Create a minimal configuration for testing + test_config = { + "mqtt": { + "console": { + "ButtonRetain": "On", + "SwitchRetain": "On", + "PowerRetain": "On" + } + } + } + + # Create a TasmotaDiscovery instance + discovery = TasmotaDiscovery(debug=True) + + # Set the config directly + discovery.config = test_config + + # Create a function to simulate the retain parameter handling + def process_retain_params(): + console_params = test_config["mqtt"]["console"] + retain_params = ["ButtonRetain", "SwitchRetain", "PowerRetain"] + processed_params = [] + + logger.info(f"Console parameters: {console_params}") + + # Process Retain parameters + for param in retain_params: + if param in console_params: + final_value = console_params[param] + # Set opposite state first + opposite_value = "On" if final_value.lower() == "off" else "Off" + + logger.info(f"Setting {param} to {opposite_value} (step 1 of 2)") + processed_params.append((param, opposite_value)) + + logger.info(f"Setting {param} to {final_value} (step 2 of 2)") + processed_params.append((param, final_value)) + + # Debug the processed params + logger.info(f"Processed parameters: {processed_params}") + + return processed_params + + # Process the retain parameters + processed_params = process_retain_params() + + # Check if all retain parameters were processed correctly + button_retain_correct = any(param[0] == "ButtonRetain" and param[1] == "Off" for param in processed_params) and \ + any(param[0] == "ButtonRetain" and param[1] == "On" for param in processed_params) + + switch_retain_correct = any(param[0] == "SwitchRetain" and param[1] == "Off" for param in processed_params) and \ + any(param[0] == "SwitchRetain" and param[1] == "On" for param in processed_params) + + power_retain_correct = any(param[0] == "PowerRetain" and param[1] == "Off" for param in processed_params) and \ + any(param[0] == "PowerRetain" and param[1] == "On" for param in processed_params) + + if button_retain_correct and switch_retain_correct and power_retain_correct: + logger.info("✓ All retain parameters were processed correctly") + return True + else: + logger.error("✗ Retain parameters were not processed correctly") + return False + +def main(): + """Main test function""" + logger.info("Starting retain parameters test") + + try: + # Test using direct device interaction + logger.info("=== Testing with direct device interaction ===") + # Reset retain parameters + reset_retain_parameters() + + # Check initial state + initial_button, initial_switch, initial_power = check_retain_status() + logger.info(f"Initial state - ButtonRetain: {initial_button}, SwitchRetain: {initial_switch}, PowerRetain: {initial_power}") + + # Test using TasmotaManager code + logger.info("\n=== Testing with TasmotaManager code ===") + tasmota_manager_success = test_retain_parameters() + + # Overall success + if tasmota_manager_success: + logger.info("TEST PASSED: TasmotaManager retain parameters handling works correctly") + return 0 + else: + logger.error("TEST FAILED: TasmotaManager retain parameters handling did not work as expected") + return 1 + + except Exception as e: + logger.error(f"Error during test: {str(e)}") + import traceback + traceback.print_exc() + return 1 + +if __name__ == "__main__": + main() \ No newline at end of file