Error Recovery

This commit is contained in:
Oliver 2025-09-17 19:38:17 +00:00
parent 7cbf4a3da7
commit 61132530ad

89
main.py
View File

@ -1,7 +1,11 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
Gadgetbridge MQTT Step Counter Integration Gadgetbridge MQTT Step Counter Integration
Extracts sensor data from Gadgetbridge SQLite database and publishes to Home Assistant via MQTT Extracts sensor data from Gadgetbridge SQLite database and publishes to Home Assistant via MQTT
""" """
import os import os
@ -13,7 +17,7 @@ from typing import Dict, Any
import asyncio import asyncio
import aiomqtt import aiomqtt
import re import re
import time
class GadgetbridgeMQTTPublisher: class GadgetbridgeMQTTPublisher:
def __init__(self): def __init__(self):
@ -23,6 +27,8 @@ class GadgetbridgeMQTTPublisher:
self.load_config() self.load_config()
self.mqtt_client = None self.mqtt_client = None
self.publish_interval = int(os.getenv("PUBLISH_INTERVAL_SECONDS", "300")) self.publish_interval = int(os.getenv("PUBLISH_INTERVAL_SECONDS", "300"))
self.max_retries = int(os.getenv("MAX_RETRIES", "5"))
self.retry_delay = int(os.getenv("RETRY_DELAY_SECONDS", "30"))
self.sensors = [ self.sensors = [
{ {
"name": "Daily Steps", "name": "Daily Steps",
@ -167,6 +173,7 @@ class GadgetbridgeMQTTPublisher:
self.logger.info(f"Published discovery config for {entity_id}") self.logger.info(f"Published discovery config for {entity_id}")
except Exception as e: except Exception as e:
self.logger.error(f"Failed to publish discovery config: {e}") self.logger.error(f"Failed to publish discovery config: {e}")
raise
async def setup_home_assistant_entities(self): async def setup_home_assistant_entities(self):
"""Setup Home Assistant entities via MQTT discovery""" """Setup Home Assistant entities via MQTT discovery"""
@ -176,6 +183,7 @@ class GadgetbridgeMQTTPublisher:
"model": "Fitness Tracker", "model": "Fitness Tracker",
"manufacturer": "Gadgetbridge", "manufacturer": "Gadgetbridge",
} }
for sensor in self.sensors: for sensor in self.sensors:
config = { config = {
"name": f"{self.device_name.replace('_', ' ').title()} {sensor['name']}", "name": f"{self.device_name.replace('_', ' ').title()} {sensor['name']}",
@ -183,10 +191,12 @@ class GadgetbridgeMQTTPublisher:
"state_topic": sensor["state_topic"], "state_topic": sensor["state_topic"],
"device": device_info, "device": device_info,
} }
# Add optional fields if present # Add optional fields if present
for key in ["unit_of_measurement", "icon", "state_class", "device_class"]: for key in ["unit_of_measurement", "icon", "state_class", "device_class"]:
if key in sensor: if key in sensor:
config[key] = sensor[key] config[key] = sensor[key]
await self.publish_home_assistant_discovery( await self.publish_home_assistant_discovery(
"sensor", sensor["unique_id"], config "sensor", sensor["unique_id"], config
) )
@ -295,18 +305,22 @@ class GadgetbridgeMQTTPublisher:
if not os.path.exists(self.db_path): if not os.path.exists(self.db_path):
self.logger.error(f"Database file not found: {self.db_path}") self.logger.error(f"Database file not found: {self.db_path}")
return {} return {}
try: try:
conn = sqlite3.connect(self.db_path) conn = sqlite3.connect(self.db_path, timeout=10.0)
cursor = conn.cursor() cursor = conn.cursor()
data = {} data = {}
for sensor in self.sensors: for sensor in self.sensors:
try: try:
data[sensor["unique_id"]] = sensor["query"](cursor) data[sensor["unique_id"]] = sensor["query"](cursor)
except Exception as e: except Exception as e:
self.logger.error(f"Error querying {sensor['unique_id']}: {e}") self.logger.error(f"Error querying {sensor['unique_id']}: {e}")
data[sensor["unique_id"]] = None data[sensor["unique_id"]] = None
conn.close() conn.close()
return data return data
except Exception as e: except Exception as e:
self.logger.error(f"Error querying database: {e}") self.logger.error(f"Error querying database: {e}")
return {} return {}
@ -322,43 +336,82 @@ class GadgetbridgeMQTTPublisher:
) )
except Exception as e: except Exception as e:
self.logger.error(f"Failed to publish {sensor['unique_id']}: {e}") self.logger.error(f"Failed to publish {sensor['unique_id']}: {e}")
raise
self.logger.info(f"Published sensor data: {data}") self.logger.info(f"Published sensor data: {data}")
async def run(self): async def mqtt_connect_with_retry(self):
"""Main execution method (async)""" """Connect to MQTT broker with retry logic"""
self.logger.info("Starting Gadgetbridge MQTT Publisher") for attempt in range(self.max_retries):
try: try:
async with aiomqtt.Client( self.logger.info(f"Attempting MQTT connection (attempt {attempt + 1}/{self.max_retries})")
client = aiomqtt.Client(
hostname=self.mqtt_config["broker"], hostname=self.mqtt_config["broker"],
port=self.mqtt_config["port"], port=self.mqtt_config["port"],
username=self.mqtt_config["username"] or None, username=self.mqtt_config["username"] or None,
password=self.mqtt_config["password"] or None, password=self.mqtt_config["password"] or None,
) as client: )
# Test connection
await client.connect()
self.logger.info("MQTT connection successful")
return client
except Exception as e:
self.logger.error(f"MQTT connection attempt {attempt + 1} failed: {e}")
if attempt < self.max_retries - 1:
self.logger.info(f"Retrying in {self.retry_delay} seconds...")
await asyncio.sleep(self.retry_delay)
else:
self.logger.error("All MQTT connection attempts failed")
raise
async def run_main_loop(self):
"""Main execution loop with error recovery"""
while True:
try:
async with await self.mqtt_connect_with_retry() as client:
self.mqtt_client = client self.mqtt_client = client
await self.setup_home_assistant_entities() await self.setup_home_assistant_entities()
# Publish immediately on startup # Publish immediately on startup
sensor_data = self.get_sensor_data() sensor_data = self.get_sensor_data()
await self.publish_sensor_data(sensor_data) await self.publish_sensor_data(sensor_data)
self.logger.info( self.logger.info(f"Sleeping for {self.publish_interval} seconds before next publish...")
f"Sleeping for {self.publish_interval} seconds before next publish..."
) # Main publishing loop
while True: while True:
await asyncio.sleep(self.publish_interval) await asyncio.sleep(self.publish_interval)
sensor_data = self.get_sensor_data() sensor_data = self.get_sensor_data()
await self.publish_sensor_data(sensor_data) await self.publish_sensor_data(sensor_data)
self.logger.info( self.logger.info(f"Sleeping for {self.publish_interval} seconds before next publish...")
f"Sleeping for {self.publish_interval} seconds before next publish..."
)
except Exception as e: except Exception as e:
self.logger.error(f"Failed to connect to MQTT broker: {e}") self.logger.error(f"Error in main loop: {e}")
self.logger.info(f"Restarting main loop in {self.retry_delay} seconds...")
await asyncio.sleep(self.retry_delay)
async def run(self):
"""Main execution method (async) - now with proper error recovery"""
self.logger.info("Starting Gadgetbridge MQTT Publisher")
# Initial database check
if not os.path.exists(self.db_path):
self.logger.error(f"Database file not found: {self.db_path}")
self.logger.error("Waiting for database to become available...")
while not os.path.exists(self.db_path):
await asyncio.sleep(30)
self.logger.info("Database file found, continuing...")
# Run main loop with recovery
await self.run_main_loop()
def get_device_alias(self) -> str: def get_device_alias(self) -> str:
"""Fetch ALIAS from DEVICE table for device_name where NAME contains 'band' or 'watch' (case-insensitive)""" """Fetch ALIAS from DEVICE table for device_name where NAME contains 'band' or 'watch' (case-insensitive)"""
if not os.path.exists(self.db_path): if not os.path.exists(self.db_path):
self.logger.error(f"Database file not found: {self.db_path}") self.logger.warning(f"Database file not found during init: {self.db_path}")
return "fitness_tracker" return "fitness_tracker"
try: try:
conn = sqlite3.connect(self.db_path) conn = sqlite3.connect(self.db_path, timeout=5.0)
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute( cursor.execute(
""" """
@ -369,17 +422,19 @@ class GadgetbridgeMQTTPublisher:
) )
row = cursor.fetchone() row = cursor.fetchone()
conn.close() conn.close()
if row and row[0]: if row and row[0]:
# Sanitize alias for MQTT topics # Sanitize alias for MQTT topics
return re.sub(r"\W+", "_", row[0]).lower() return re.sub(r"\W+", "_", row[0]).lower()
else: else:
return "fitness_tracker" return "fitness_tracker"
except Exception as e: except Exception as e:
self.logger.error(f"Error fetching device alias: {e}") self.logger.error(f"Error fetching device alias: {e}")
return "fitness_tracker" return "fitness_tracker"
# --- Main Entry Point --- # --- Main Entry Point ---
if __name__ == "__main__": if __name__ == "__main__":
publisher = GadgetbridgeMQTTPublisher() publisher = GadgetbridgeMQTTPublisher()
asyncio.run(publisher.run()) asyncio.run(publisher.run())