mirror of
https://github.com/OMGeeky/homecontrol.esp-sensors.git
synced 2026-01-06 11:34:35 +01:00
feat: enhance MQTT configuration update process with version checking
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
166
tests/test_config_update.py
Normal 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
|
||||
)
|
||||
Reference in New Issue
Block a user