Error Recovery
This commit is contained in:
parent
7cbf4a3da7
commit
61132530ad
89
main.py
89
main.py
@ -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())
|
||||||
Loading…
Reference in New Issue
Block a user