Updated for automatic pushing back - pulling already works via webhook
This commit is contained in:
129
app.py
129
app.py
@@ -21,6 +21,7 @@ from datetime import datetime
|
||||
|
||||
from flask import Flask, request, jsonify
|
||||
from watchdog.observers import Observer
|
||||
from watchdog.observers.polling import PollingObserver
|
||||
from watchdog.events import FileSystemEventHandler
|
||||
|
||||
# Configure logging
|
||||
@@ -35,6 +36,8 @@ app = Flask(__name__)
|
||||
# Configuration
|
||||
ESPHOME_CONFIG_DIR = os.environ.get('ESPHOME_CONFIG_DIR', '/config')
|
||||
DEBOUNCE_SECONDS = int(os.environ.get('DEBOUNCE_SECONDS', '5'))
|
||||
POLLING_INTERVAL = float(os.environ.get('POLLING_INTERVAL', '1.0')) # Seconds between polls
|
||||
USE_POLLING = os.environ.get('USE_POLLING', 'true').lower() == 'true' # Use polling for Docker compatibility
|
||||
|
||||
# Gitea configuration
|
||||
GITEA_URL = os.environ.get('GITEA_URL', '')
|
||||
@@ -58,7 +61,8 @@ class ESPHomeFileHandler(FileSystemEventHandler):
|
||||
def __init__(self):
|
||||
self.last_modified = {}
|
||||
|
||||
def on_modified(self, event):
|
||||
def _handle_file_change(self, event):
|
||||
"""Common handler for file modifications and creations"""
|
||||
if event.is_directory:
|
||||
return
|
||||
|
||||
@@ -70,6 +74,7 @@ class ESPHomeFileHandler(FileSystemEventHandler):
|
||||
now = time.time()
|
||||
if event.src_path in self.last_modified:
|
||||
if now - self.last_modified[event.src_path] < DEBOUNCE_SECONDS:
|
||||
logger.debug(f"Debouncing {event.src_path} (too soon)")
|
||||
return
|
||||
|
||||
self.last_modified[event.src_path] = now
|
||||
@@ -88,6 +93,93 @@ class ESPHomeFileHandler(FileSystemEventHandler):
|
||||
Thread(target=git_push, args=(f"Auto-sync: {device_name}.yaml changed",)).start()
|
||||
else:
|
||||
logger.info(f"AUTO_PUSH disabled, skipping push to Gitea")
|
||||
else:
|
||||
logger.debug(f"Ignoring file outside config directory: {event.src_path}")
|
||||
|
||||
def on_modified(self, event):
|
||||
"""Handle file modification events"""
|
||||
self._handle_file_change(event)
|
||||
|
||||
def on_created(self, event):
|
||||
"""Handle file creation events"""
|
||||
self._handle_file_change(event)
|
||||
|
||||
def on_deleted(self, event):
|
||||
"""Handle file deletion events"""
|
||||
if event.is_directory:
|
||||
return
|
||||
|
||||
# Only watch YAML files
|
||||
if not event.src_path.endswith(('.yaml', '.yml')):
|
||||
return
|
||||
|
||||
logger.info(f"Detected deletion of {event.src_path}")
|
||||
|
||||
# Trigger git push if AUTO_PUSH is enabled
|
||||
device_path = Path(event.src_path)
|
||||
# Check if this is a device config file directly in the config directory
|
||||
if device_path.parent == Path(ESPHOME_CONFIG_DIR):
|
||||
device_name = device_path.stem # Get filename without extension
|
||||
logger.info(f"Deletion detected for device: {device_name}")
|
||||
|
||||
if AUTO_PUSH:
|
||||
logger.info(f"AUTO_PUSH enabled, pushing deletion to Gitea")
|
||||
Thread(target=git_push, args=(f"Auto-sync: {device_name}.yaml deleted",)).start()
|
||||
else:
|
||||
logger.info(f"AUTO_PUSH disabled, skipping push to Gitea")
|
||||
else:
|
||||
logger.debug(f"Ignoring file outside config directory: {event.src_path}")
|
||||
|
||||
def on_moved(self, event):
|
||||
"""Handle file move/rename events"""
|
||||
if event.is_directory:
|
||||
return
|
||||
|
||||
# Only watch YAML files
|
||||
if not event.src_path.endswith(('.yaml', '.yml')) and not event.dest_path.endswith(('.yaml', '.yml')):
|
||||
return
|
||||
|
||||
src_path = Path(event.src_path)
|
||||
dest_path = Path(event.dest_path)
|
||||
config_dir = Path(ESPHOME_CONFIG_DIR)
|
||||
|
||||
src_in_config = src_path.parent == config_dir
|
||||
dest_in_config = dest_path.parent == config_dir
|
||||
|
||||
logger.info(f"Detected move from {event.src_path} to {event.dest_path}")
|
||||
|
||||
if src_in_config and dest_in_config:
|
||||
# Rename within config directory - treat as modification
|
||||
device_name = dest_path.stem
|
||||
logger.info(f"Rename detected: {src_path.name} -> {dest_path.name}")
|
||||
|
||||
if AUTO_PUSH:
|
||||
logger.info(f"AUTO_PUSH enabled, pushing rename to Gitea")
|
||||
Thread(target=git_push, args=(f"Auto-sync: Renamed {src_path.name} to {dest_path.name}",)).start()
|
||||
else:
|
||||
logger.info(f"AUTO_PUSH disabled, skipping push to Gitea")
|
||||
|
||||
elif src_in_config and not dest_in_config:
|
||||
# Moved OUT of config directory (e.g., to archive folder) - treat as deletion
|
||||
device_name = src_path.stem
|
||||
logger.info(f"Device moved to archive/subdirectory: {device_name}")
|
||||
|
||||
if AUTO_PUSH:
|
||||
logger.info(f"AUTO_PUSH enabled, pushing archived file as deletion to Gitea")
|
||||
Thread(target=git_push, args=(f"Auto-sync: {src_path.name} archived",)).start()
|
||||
else:
|
||||
logger.info(f"AUTO_PUSH disabled, skipping push to Gitea")
|
||||
|
||||
elif not src_in_config and dest_in_config:
|
||||
# Moved INTO config directory - treat as creation
|
||||
device_name = dest_path.stem
|
||||
logger.info(f"Device moved from subdirectory to config root: {device_name}")
|
||||
|
||||
if AUTO_PUSH:
|
||||
logger.info(f"AUTO_PUSH enabled, pushing new file to Gitea")
|
||||
Thread(target=git_push, args=(f"Auto-sync: {dest_path.name} restored from archive",)).start()
|
||||
else:
|
||||
logger.info(f"AUTO_PUSH disabled, skipping push to Gitea")
|
||||
|
||||
|
||||
def find_device_config(device_name):
|
||||
@@ -403,18 +495,49 @@ def manual_push():
|
||||
|
||||
def start_file_watcher():
|
||||
"""Start the file system watcher"""
|
||||
logger.info(f"Initializing file watcher for directory: {ESPHOME_CONFIG_DIR}")
|
||||
logger.info(f"Watching for .yaml and .yml files with {DEBOUNCE_SECONDS}s debounce")
|
||||
logger.info(f"AUTO_PUSH is {'enabled' if AUTO_PUSH else 'disabled'}")
|
||||
|
||||
# Select observer type based on configuration
|
||||
if USE_POLLING:
|
||||
logger.info(f"Using PollingObserver (interval: {POLLING_INTERVAL}s) for Docker bind mount compatibility")
|
||||
else:
|
||||
logger.info("Using native filesystem observer (inotify)")
|
||||
|
||||
# Verify the directory exists
|
||||
config_path = Path(ESPHOME_CONFIG_DIR)
|
||||
if not config_path.exists():
|
||||
logger.error(f"Config directory does not exist: {ESPHOME_CONFIG_DIR}")
|
||||
return
|
||||
if not config_path.is_dir():
|
||||
logger.error(f"Config path is not a directory: {ESPHOME_CONFIG_DIR}")
|
||||
return
|
||||
|
||||
event_handler = ESPHomeFileHandler()
|
||||
observer = Observer()
|
||||
|
||||
# Use PollingObserver for Docker compatibility, or native Observer for local development
|
||||
if USE_POLLING:
|
||||
observer = PollingObserver(timeout=POLLING_INTERVAL)
|
||||
else:
|
||||
observer = Observer()
|
||||
|
||||
observer.schedule(event_handler, ESPHOME_CONFIG_DIR, recursive=True)
|
||||
observer.start()
|
||||
logger.info(f"File watcher started on {ESPHOME_CONFIG_DIR}")
|
||||
logger.info(f"File watcher started successfully on {ESPHOME_CONFIG_DIR}")
|
||||
|
||||
# Log existing YAML files being watched
|
||||
yaml_files = list(config_path.glob('*.yaml')) + list(config_path.glob('*.yml'))
|
||||
logger.info(f"Currently watching {len(yaml_files)} YAML files: {[f.name for f in yaml_files]}")
|
||||
|
||||
try:
|
||||
while True:
|
||||
time.sleep(1)
|
||||
except KeyboardInterrupt:
|
||||
logger.info("File watcher interrupted, stopping...")
|
||||
observer.stop()
|
||||
observer.join()
|
||||
logger.info("File watcher stopped")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
Reference in New Issue
Block a user