feat: enhance MQTT configuration update process with version checking

This commit is contained in:
OMGeeky
2025-06-07 15:31:15 +02:00
parent a9ee843059
commit bc5ebca13e
3 changed files with 238 additions and 85 deletions

View File

@@ -302,6 +302,10 @@ def check_and_update_config_from_mqtt(
"""
Check for configuration updates from MQTT and update local configuration if needed.
This function uses a two-step process:
1. Check the version topic to see if a new version is available
2. If a new version is detected, fetch the full configuration from the data topic
Args:
mqtt_client: MQTT client instance
mqtt_config: MQTT configuration dictionary
@@ -315,28 +319,22 @@ def check_and_update_config_from_mqtt(
return current_config
try:
# Get the configuration topic
topic_config = mqtt_config.get("topic_config")
if not topic_config:
print("No configuration topic specified")
# Get the version and data topics
topic_config_version = mqtt_config.get("topic_config_version")
topic_config_data = mqtt_config.get("topic_config_data")
if not topic_config_version or not topic_config_data:
print("Configuration version or data topic not specified")
return current_config
# Subscribe to the configuration topic
print(f"Subscribing to configuration topic: {topic_config}")
# This function is now implemented in the mqtt.py module
# We'll import and use that implementation
from .mqtt import check_config_update
# This is a simplified implementation - in a real implementation, we would
# set up a callback to handle the message and wait for it to be received
# For now, we'll just return the current configuration
# Use the implementation from mqtt.py to check for updates
updated_config = check_config_update(mqtt_client, mqtt_config, current_config)
# In a real implementation, we would:
# 1. Subscribe to the topic
# 2. Wait for a message (with timeout)
# 3. Parse the message as JSON
# 4. Check if the version is newer than the current version
# 5. If it is, update the local configuration and save it
print("MQTT configuration update check not implemented yet")
return current_config
return updated_config
except Exception as e:
print(f"Error checking for configuration updates: {e}")
return current_config

View File

@@ -354,7 +354,7 @@ def subscribe_to_config(
client: ESP32MQTTClient | MQTTClient | None, mqtt_config: dict
) -> bool:
"""
Subscribe to the configuration topic.
Subscribe to the configuration version topic.
Args:
client: ESP32MQTTClient or MQTTClient instance
@@ -368,18 +368,18 @@ def subscribe_to_config(
return False
try:
topic_config = mqtt_config.get("topic_config")
if not topic_config:
print("No configuration topic specified")
topic_config_version = mqtt_config.get("topic_config_version")
if not topic_config_version:
print("No configuration version topic specified")
return False
print(f"Subscribing to configuration topic: {topic_config}")
print(f"Subscribing to configuration version topic: {topic_config_version}")
# Both client types have compatible subscribe methods
client.subscribe(topic_config.encode())
client.subscribe(topic_config_version.encode())
return True
except Exception as e:
print(f"Failed to subscribe to configuration topic: {e}")
print(f"Failed to subscribe to configuration version topic: {e}")
return False
@@ -465,6 +465,9 @@ def check_config_update(
"""
Check for configuration updates from MQTT.
First checks the version topic to see if a new version is available.
If a new version is detected, fetches the full configuration from the data topic.
Args:
client: ESP32MQTTClient or MQTTClient instance
mqtt_config: MQTT configuration dictionary
@@ -477,19 +480,49 @@ def check_config_update(
return current_config
try:
# Variable to store the received configuration
# Get the version and data topics
topic_config_version = mqtt_config.get("topic_config_version")
topic_config_data = mqtt_config.get("topic_config_data")
if not topic_config_version or not topic_config_data:
print("Configuration version or data topic not specified")
return current_config
# Variable to store the received version and configuration
received_version = None
received_config = None
wait_time = mqtt_config.get("config_wait_time", 1.0)
if isinstance(client, ESP32MQTTClient):
print("Using ESP32MQTTClient to check for configuration updates")
# Step 1: Check the version topic for updates
print(
f"Reading from version topic: {topic_config_version} with wait time: {wait_time}s"
)
version_msg = client.read_topic(topic_config_version, wait_time)
topic_config = mqtt_config.get("topic_config")
wait_time = mqtt_config.get("config_wait_time", 5.0)
if version_msg:
try:
msg_str = (
version_msg.decode("utf-8")
if isinstance(version_msg, bytes)
else version_msg
)
received_version = int(msg_str.strip())
print(f"Received version: {received_version}")
except Exception as e:
print(f"Error parsing version message: {e}")
# Step 2: If we received a version and it's newer, fetch the full config from the data topic
current_version = current_config.get("version", 0)
if received_version is not None and received_version > current_version:
print(
f"Found newer version ({received_version} > {current_version}), fetching full configuration"
)
print(
f"Using ESP32MQTTClient to read from config topic with wait time: {wait_time}s"
f"Reading from data topic: {topic_config_data} with wait time: {wait_time}s"
)
config_msg = client.read_topic(topic_config, wait_time)
config_msg = client.read_topic(topic_config_data, wait_time)
if config_msg:
try:
@@ -501,60 +534,16 @@ def check_config_update(
received_config = json.loads(msg_str)
except Exception as e:
print(f"Error parsing configuration message: {e}")
else:
# Define callback function to handle incoming messages
def config_callback(topic, msg):
nonlocal received_config
try:
# Verify that the topic matches our expected topic
expected_topic = mqtt_config.get("topic_config")
topic_str = (
topic.decode("utf-8") if isinstance(topic, bytes) else topic
)
if topic_str != expected_topic:
print(
f"Ignoring message from topic {topic_str} - not matching our config topic {expected_topic}"
)
return
# Parse the message as JSON
msg_str = msg.decode("utf-8") if isinstance(msg, bytes) else msg
config_data = json.loads(msg_str)
print(
f"Received configuration from MQTT: version {config_data.get('version', 0)}"
)
received_config = config_data
except Exception as e:
print(f"Error parsing configuration message: {e}")
# Set the callback
client.set_callback(config_callback)
# Subscribe to the configuration topic
if not subscribe_to_config(client, mqtt_config):
print("Failed to subscribe to configuration topic")
return current_config
# Check for retained messages (will be processed by the callback)
print("Checking for retained configuration messages...")
client.check_msg()
# For basic MQTTClient, use the original approach
print("Waiting for configuration updates...")
# Wait a short time for any retained messages to be processed
time.sleep(0.5)
client.check_msg()
print("done waiting for configuration updates")
# If we received a configuration and its version is newer, return it
if received_config and received_config.get("version", 0) > current_config.get(
"version", 0
):
print(
f"Found newer configuration (version {received_config.get('version')})"
)
return received_config
# If we received a configuration and its version matches what we expected, return it
if (
received_config
and received_config.get("version", 0) == received_version
):
print(f"Successfully fetched configuration version {received_version}")
return received_config
else:
print("Failed to fetch the full configuration or version mismatch")
return current_config
except Exception as e:

166
tests/test_config_update.py Normal file
View File

@@ -0,0 +1,166 @@
"""
Tests for the configuration update functionality.
"""
import json
from unittest.mock import patch, MagicMock
import pytest
from src.esp_sensors.mqtt import check_config_update, ESP32MQTTClient
@pytest.fixture
def mqtt_config():
"""Fixture providing a sample MQTT configuration with version and data topics."""
return {
"enabled": True,
"broker": "test.mosquitto.org",
"port": 1883,
"client_id": "test_client",
"username": "test_user",
"password": "test_pass",
"load_config_from_mqtt": True,
"topic_config_version": "test/config/version",
"topic_config_data": "test/config/data",
"topic_data_prefix": "test/sensors",
"publish_interval": 30,
"ssl": False,
"keepalive": 60,
"use_esp32_client": True,
}
@pytest.fixture
def current_config():
"""Fixture providing a sample current configuration."""
return {
"device_id": "test_device",
"device_name": "Test Device",
"version": 5,
"sensors": {
"dht22": {
"id": "test-dht22",
"name": "Test Sensor",
"pin": 16,
"interval": 60,
}
},
}
@pytest.fixture
def new_config():
"""Fixture providing a sample new configuration."""
return {
"device_id": "test_device",
"device_name": "Test Device Updated",
"version": 6,
"sensors": {
"dht22": {
"id": "test-dht22",
"name": "Test Sensor Updated",
"pin": 16,
"interval": 30,
}
},
}
def test_check_config_update_no_client(mqtt_config, current_config):
"""Test that check_config_update returns the current config when client is None."""
result = check_config_update(None, mqtt_config, current_config)
assert result == current_config
def test_check_config_update_disabled(mqtt_config, current_config):
"""Test that check_config_update returns the current config when load_config_from_mqtt is False."""
mqtt_config["load_config_from_mqtt"] = False
mock_client = MagicMock()
result = check_config_update(mock_client, mqtt_config, current_config)
assert result == current_config
def test_check_config_update_esp32_newer_version(
mqtt_config, current_config, new_config
):
"""Test that check_config_update returns the new config when a newer version is available (ESP32MQTTClient)."""
# Create a mock ESP32MQTTClient
mock_client = MagicMock(spec=ESP32MQTTClient)
# Configure the mock to return a version and then the new config
mock_client.read_topic.side_effect = [
str(new_config["version"]), # First call returns the version
json.dumps(new_config), # Second call returns the config data
]
# Call the function
result = check_config_update(mock_client, mqtt_config, current_config)
# Verify the result
assert result == new_config
# Verify read_topic was called for both topics
assert mock_client.read_topic.call_count == 2
mock_client.read_topic.assert_any_call(mqtt_config["topic_config_version"], 5.0)
mock_client.read_topic.assert_any_call(mqtt_config["topic_config_data"], 5.0)
def test_check_config_update_esp32_same_version(mqtt_config, current_config):
"""Test that check_config_update returns the current config when the version is the same (ESP32MQTTClient)."""
# Create a mock ESP32MQTTClient
mock_client = MagicMock(spec=ESP32MQTTClient)
# Configure the mock to return the same version
mock_client.read_topic.return_value = str(current_config["version"])
# Call the function
result = check_config_update(mock_client, mqtt_config, current_config)
# Verify the result
assert result == current_config
# Verify read_topic was called only for the version topic
mock_client.read_topic.assert_called_once_with(
mqtt_config["topic_config_version"], 5.0
)
def test_check_config_update_esp32_older_version(mqtt_config, current_config):
"""Test that check_config_update returns the current config when the version is older (ESP32MQTTClient)."""
# Create a mock ESP32MQTTClient
mock_client = MagicMock(spec=ESP32MQTTClient)
# Configure the mock to return an older version
mock_client.read_topic.return_value = str(current_config["version"] - 1)
# Call the function
result = check_config_update(mock_client, mqtt_config, current_config)
# Verify the result
assert result == current_config
# Verify read_topic was called only for the version topic
mock_client.read_topic.assert_called_once_with(
mqtt_config["topic_config_version"], 5.0
)
def test_check_config_update_esp32_no_version(mqtt_config, current_config):
"""Test that check_config_update returns the current config when no version is available (ESP32MQTTClient)."""
# Create a mock ESP32MQTTClient
mock_client = MagicMock(spec=ESP32MQTTClient)
# Configure the mock to return None for the version
mock_client.read_topic.return_value = None
# Call the function
result = check_config_update(mock_client, mqtt_config, current_config)
# Verify the result
assert result == current_config
# Verify read_topic was called only for the version topic
mock_client.read_topic.assert_called_once_with(
mqtt_config["topic_config_version"], 5.0
)