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.

This commit is contained in:
Mike Geppert 2025-08-05 02:29:30 -05:00
parent daa015b4c2
commit 2d3c867e84
4 changed files with 224 additions and 26 deletions

View File

@ -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 |

View File

@ -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.

View File

@ -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
# 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:
# First ButtonRetain command (On)
url = f"http://{ip}/cm?cmnd=ButtonRetain%20On"
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 ButtonRetain to On (step 1 of 2 to clear MQTT broker retain settings)")
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 ButtonRetain to On")
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 ButtonRetain command (Off)
url = f"http://{ip}/cm?cmnd=ButtonRetain%20Off"
# 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 ButtonRetain to Off (step 2 of 2 to clear MQTT broker retain settings)")
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 ButtonRetain to Off")
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 ButtonRetain commands: {str(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.)

170
test_retain_parameters.py Normal file
View File

@ -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()