feat: implement daily reconnection strategy with persistence

The reconnection strategy was implemented to ensure at least four connection attempts per day while conserving battery life through state persistence in a configuration file.
This commit is contained in:
jetbrains-junie[bot]
2025-06-07 10:50:08 +00:00
committed by OMGeeky
parent 25f2d9e342
commit 19c0aec290
3 changed files with 152 additions and 1 deletions

View File

@@ -20,6 +20,8 @@ The implementation provides the following features:
- Socket-based communication with MQTT brokers
- Quality of Service (QoS) support (levels 0 and 1)
- Ping/keepalive mechanism to maintain connections
- Smart reconnection strategy with exponential backoff for unreachable brokers
- Battery-efficient operation when the broker is unavailable
- Simulation mode for development on non-ESP hardware
## Classes
@@ -166,7 +168,16 @@ mqtt_config = {
"topic_data_prefix": "/homecontrol/device/data", # Prefix for data topics
"topic_config": "/homecontrol/device/config", # Topic for configuration
"load_config_from_mqtt": True, # Whether to load config from MQTT
"config_wait_time": 1.0 # Wait time for config updates in seconds
"config_wait_time": 1.0, # Wait time for config updates in seconds
"reconnect": { # Reconnection strategy configuration
"enabled": True, # Enable/disable reconnection strategy
"max_attempts": 3, # Maximum consecutive connection attempts
"attempt_count": 0, # Current number of consecutive failed attempts
"last_attempt_time": 0, # Timestamp of the last connection attempt
"backoff_factor": 2, # Exponential backoff factor
"min_interval": 3600, # Minimum interval between reconnection attempts (1 hour)
"max_interval": 21600, # Maximum interval between reconnection attempts (6 hours)
}
}
```
@@ -205,6 +216,53 @@ When running on non-ESP hardware, the implementation automatically switches to s
This is useful for development and testing without actual hardware.
## Reconnection Strategy
The MQTT implementation includes a smart reconnection strategy designed to balance connectivity needs with battery conservation, especially when the MQTT broker is unreachable. This is particularly important for ESP32 devices that use deep sleep to conserve power.
### How It Works
1. **Initial Connection Attempts**: The system will make up to `max_attempts` consecutive connection attempts (default: 3) without any delay between them.
2. **Exponential Backoff**: After reaching the maximum number of consecutive attempts, the system implements an exponential backoff strategy:
- The wait time between attempts increases exponentially based on the `backoff_factor` (default: 2)
- The formula is: `min_interval * (backoff_factor ^ (attempt_count - max_attempts))`
- This wait time is capped at `max_interval` to ensure reconnection attempts happen regularly
3. **Guaranteed Reconnection Attempts**: With default settings (min_interval: 1 hour, max_interval: 6 hours), the system will attempt to reconnect at least 4 times per day even if the broker remains unreachable.
4. **State Persistence**: The reconnection state (attempt count, last attempt time) is persisted across deep sleep cycles by saving it to the configuration file.
5. **Automatic Reset**: When a connection is successful, the attempt counter is reset to 0, returning to normal operation.
### Configuration
The reconnection strategy can be configured through the `reconnect` section of the MQTT configuration:
```python
mqtt_config = {
# ... other MQTT settings ...
"reconnect": {
"enabled": True, # Enable/disable reconnection strategy
"max_attempts": 3, # Maximum consecutive connection attempts
"attempt_count": 0, # Current number of consecutive failed attempts
"last_attempt_time": 0, # Timestamp of the last connection attempt
"backoff_factor": 2, # Exponential backoff factor
"min_interval": 3600, # Minimum interval (1 hour in seconds)
"max_interval": 21600, # Maximum interval (6 hours in seconds)
}
}
```
### Battery Efficiency
This strategy significantly reduces battery consumption when the MQTT broker is unreachable for extended periods:
- Instead of attempting to connect on every wake cycle (which would drain the battery quickly)
- The device will skip connection attempts based on the exponential backoff algorithm
- This allows the device to spend more time in deep sleep, conserving power
- While still ensuring regular reconnection attempts to restore functionality when the broker becomes available again
## Integration with Sensor Data
The `publish_sensor_data` function provides a convenient way to publish sensor data to MQTT topics:

View File

@@ -53,6 +53,15 @@ DEFAULT_CONFIG = {
"topic_data_prefix": "/homecontrol/{device_id}/data",
"ssl": False,
"keepalive": 60,
"reconnect": {
"enabled": True,
"max_attempts": 3,
"attempt_count": 0,
"last_attempt_time": 0,
"backoff_factor": 2,
"min_interval": 3600, # 1 hour in seconds
"max_interval": 21600, # 6 hours in seconds
},
},
"network": {
"ssid": "<your ssid>",

View File

@@ -260,6 +260,11 @@ def setup_mqtt(mqtt_config: dict) -> ESP32MQTTClient | MQTTClient | None:
keepalive = mqtt_config.get("keepalive", 60)
ssl = mqtt_config.get("ssl", False)
# Get reconnection configuration
reconnect_config = mqtt_config.get("reconnect", {})
reconnect_enabled = reconnect_config.get("enabled", True)
print(f"Setting up MQTT client: {client_id} -> {broker}:{port}")
# Use the new ESP32MQTTClient
@@ -267,11 +272,22 @@ def setup_mqtt(mqtt_config: dict) -> ESP32MQTTClient | MQTTClient | None:
client_id, broker, port, username, password, keepalive, ssl
)
# Check if we should attempt to connect based on reconnection strategy
if reconnect_enabled and not should_attempt_connection(reconnect_config):
print("Skipping MQTT connection attempt based on reconnection strategy")
return client
# Try to connect
if client.connect():
print("MQTT connected successfully using ESP32MQTTClient")
# Reset reconnection attempt counter on successful connection
if reconnect_enabled:
update_reconnection_state(reconnect_config, True)
else:
print("Failed to connect using ESP32MQTTClient")
# Update reconnection attempt counter
if reconnect_enabled:
update_reconnection_state(reconnect_config, False)
return client
@@ -370,6 +386,74 @@ def subscribe_to_config(
return False
def should_attempt_connection(reconnect_config: dict) -> bool:
"""
Determine if a connection attempt should be made based on the reconnection strategy.
Args:
reconnect_config: Reconnection configuration dictionary
Returns:
True if a connection attempt should be made, False otherwise
"""
# If reconnection is disabled, always attempt to connect
if not reconnect_config.get("enabled", True):
return True
# Get reconnection parameters
attempt_count = reconnect_config.get("attempt_count", 0)
max_attempts = reconnect_config.get("max_attempts", 3)
last_attempt_time = reconnect_config.get("last_attempt_time", 0)
backoff_factor = reconnect_config.get("backoff_factor", 2)
min_interval = reconnect_config.get("min_interval", 3600) # 1 hour default
max_interval = reconnect_config.get("max_interval", 21600) # 6 hours default
# If we haven't reached max attempts, always try to connect
if attempt_count < max_attempts:
return True
# Calculate the backoff interval based on attempt count
# Use exponential backoff with a maximum interval
interval = min(min_interval * (backoff_factor ** (attempt_count - max_attempts)), max_interval)
# Check if enough time has passed since the last attempt
current_time = time.time()
time_since_last_attempt = current_time - last_attempt_time
# If we've waited long enough, allow another attempt
if time_since_last_attempt >= interval:
print(f"Allowing reconnection attempt after {time_since_last_attempt:.1f}s (interval: {interval:.1f}s)")
return True
else:
print(f"Skipping reconnection attempt, next attempt in {interval - time_since_last_attempt:.1f}s")
return False
def update_reconnection_state(reconnect_config: dict, success: bool) -> None:
"""
Update the reconnection state based on the connection attempt result.
Args:
reconnect_config: Reconnection configuration dictionary
success: Whether the connection attempt was successful
"""
current_time = time.time()
if success:
# Reset attempt counter on successful connection
reconnect_config["attempt_count"] = 0
print("Connection successful, reset reconnection attempt counter")
else:
# Increment attempt counter on failed connection
attempt_count = reconnect_config.get("attempt_count", 0) + 1
reconnect_config["attempt_count"] = attempt_count
print(f"Connection failed, reconnection attempt count: {attempt_count}")
# Update last attempt time
reconnect_config["last_attempt_time"] = current_time
# Update the configuration in the parent dictionary
# This will be saved to the config file in the main application
def check_config_update(
client: ESP32MQTTClient | MQTTClient | None, mqtt_config: dict, current_config: dict
) -> dict: