Merge pull request #3 from OMGeeky/jetbrains-junie/issue-2-run-75eae3ea-54e4-40a5-bf5b-51361fd684d8

[Junie]: feat: implement daily reconnection strategy with persistence
This commit is contained in:
OMGeeky
2025-06-07 14:28:29 +02:00
committed by GitHub
4 changed files with 193 additions and 35 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,10 @@ 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 +271,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
@@ -321,9 +336,7 @@ def publish_sensor_data(
# Publish the data and check the result
publish_success = client.publish(data_topic, data_payload)
if publish_success:
print(
f"Published sensor data to MQTT: '{data_topic}'"
)
print(f"Published sensor data to MQTT: '{data_topic}'")
return True
else:
print("Failed to publish sensor data to MQTT")
@@ -370,6 +383,82 @@ 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:

View File

@@ -149,36 +149,38 @@ def main():
mqtt_client = None
if mqtt_client and mqtt_client.connected and load_config_from_mqtt:
display.set_status("Checking MQTT config...")
print("Checking for configuration updates before publishing...")
updated_config = check_config_update(
mqtt_client, config.mqtt_config, config.config
)
display.set_status("Checking MQTT config...")
print("Checking for configuration updates before publishing...")
updated_config = check_config_update(
mqtt_client, config.mqtt_config, config.config
)
# If we got an updated configuration with a newer version, save it
if (
updated_config != config.config
and updated_config.get("version", 0) > config.current_version
):
display.set_status("Updating config...")
print(
f"Found newer configuration (version {updated_config.get('version')}), updating..."
)
config.save_config(updated_config)
publish_success = mqtt_client.publish(
get_data_topic(config.mqtt_config) + "/config_status",
"Configuration updated",
)
if not publish_success:
print("Failed to publish configuration update status")
# Note: We continue with the current config for this cycle
# The updated config will be used after the next reboot
else:
print(
f"No configuration updates found or no newer version available (local version: {config.current_version})"
)
# If we got an updated configuration with a newer version, save it
if (
updated_config != config.config
and updated_config.get("version", 0) > config.current_version
):
display.set_status("Updating config...")
print(
f"Found newer configuration (version {updated_config.get('version')}), updating..."
)
config.save_config(updated_config)
publish_success = mqtt_client.publish(
get_data_topic(config.mqtt_config) + "/config_status",
"Configuration updated",
)
if not publish_success:
print("Failed to publish configuration update status")
# Note: We continue with the current config for this cycle
# The updated config will be used after the next reboot
else:
print(
f"No configuration updates found or no newer version available (local version: {config.current_version})"
)
else:
print("MQTT client not connected or not configured to load config from broker, skipping config check")
print(
"MQTT client not connected or not configured to load config from broker, skipping config check"
)
display.set_status("MQTT not loading config")
if mqtt_client and mqtt_client.connected:
@@ -204,9 +206,9 @@ def main():
print("MQTT client disconnected")
except Exception as e:
print(f"Error disconnecting MQTT client: {e}")
else:
print("MQTT client not connected, skipping publish")
display.set_status("MQTT not connected")
if mqtt_client:
# Save the updated reconnection state to the configuration
config.save_config()
else:
print("MQTT is disabled, not publishing data")