Updated for automatic pushing back - pulling already works via webhook

This commit is contained in:
2026-01-13 20:03:36 +01:00
parent e5ba624aa9
commit 424c20923b
5 changed files with 404 additions and 125 deletions

129
app.py
View File

@@ -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__':