initial commit
This commit is contained in:
291
CLAUDE.md
Normal file
291
CLAUDE.md
Normal file
@@ -0,0 +1,291 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
This is an ESPHome Gitea Sync Service - a Flask-based synchronization tool that bridges Gitea repositories with ESPHome device configurations. The service:
|
||||||
|
- Clones and syncs a Gitea repository containing ESPHome configs
|
||||||
|
- Watches for local file changes and can auto-push to Gitea
|
||||||
|
- Receives webhooks from Gitea to pull changes
|
||||||
|
- Provides manual sync endpoints
|
||||||
|
|
||||||
|
The ESPHome container remains immutable and handles all compilation/deployment operations. This service only manages git synchronization.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Core Components
|
||||||
|
|
||||||
|
1. **Flask REST API** (app.py)
|
||||||
|
- Health check and device listing endpoints
|
||||||
|
- Gitea webhook endpoint (`/webhook/gitea`) for receiving push events
|
||||||
|
- Manual sync endpoints (`/sync/pull`, `/sync/push`)
|
||||||
|
- Thread-safe operation management using locks
|
||||||
|
|
||||||
|
2. **File Watcher System** (app.py)
|
||||||
|
- Uses watchdog library to monitor YAML file changes
|
||||||
|
- Implements debouncing (default 5 seconds) to prevent duplicate triggers
|
||||||
|
- Tracks last modification time per file in `ESPHomeFileHandler.last_modified`
|
||||||
|
- Monitors all `.yaml`/`.yml` files directly in the config directory
|
||||||
|
- Optionally triggers `git push` when `AUTO_PUSH=true`
|
||||||
|
|
||||||
|
3. **Git Operations** (app.py)
|
||||||
|
- `git_clone()` - Initial clone of Gitea repository on startup
|
||||||
|
- `git_pull()` - Pull changes from Gitea (triggered by webhook or manual)
|
||||||
|
- `git_push()` - Push local changes to Gitea (auto or manual)
|
||||||
|
- `initialize_git_repo()` - Called on startup to clone or sync repo
|
||||||
|
- Authentication via Gitea token injected into remote URL
|
||||||
|
|
||||||
|
4. **Synchronization Flow**
|
||||||
|
- **Gitea → ESPHome**: Gitea webhook triggers `/webhook/gitea` → `git pull` → ESPHome sees updated files
|
||||||
|
- **ESPHome → Gitea**: File change detected → debounced → optionally `git push` (if AUTO_PUSH enabled)
|
||||||
|
- Compilation/upload handled entirely by ESPHome container
|
||||||
|
|
||||||
|
### Expected Directory Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
config/
|
||||||
|
├── device-name-1.yaml
|
||||||
|
├── device-name-2.yaml
|
||||||
|
├── device-name-3.yaml
|
||||||
|
└── .git/
|
||||||
|
```
|
||||||
|
|
||||||
|
The service expects a flat structure with each device having its own `.yaml` file directly in the config directory. The device name is the filename without the extension.
|
||||||
|
|
||||||
|
### Thread Safety
|
||||||
|
|
||||||
|
- `operation_lock` (Lock) protects the `pending_operations` dictionary
|
||||||
|
- Prevents concurrent operations on the same device
|
||||||
|
- Operations run in separate threads spawned from file watcher or webhook handlers
|
||||||
|
|
||||||
|
## Development Commands
|
||||||
|
|
||||||
|
### Local Development (Without Docker)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install dependencies
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
# Set environment variables
|
||||||
|
export ESPHOME_CONFIG_DIR=/path/to/config
|
||||||
|
export DEBOUNCE_SECONDS=5
|
||||||
|
export GITEA_URL=https://gitea.example.com
|
||||||
|
export GITEA_REPO=username/esphome-configs
|
||||||
|
export GITEA_TOKEN=your_gitea_token
|
||||||
|
export GITEA_BRANCH=main
|
||||||
|
export AUTO_PUSH=false
|
||||||
|
export GIT_USER_NAME="ESPHome Sync Service"
|
||||||
|
export GIT_USER_EMAIL="esphome-sync@localhost"
|
||||||
|
|
||||||
|
# Run the service directly
|
||||||
|
python app.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start services
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
docker-compose logs webhook
|
||||||
|
docker-compose logs esphome
|
||||||
|
|
||||||
|
# Stop services
|
||||||
|
docker-compose down
|
||||||
|
|
||||||
|
# Rebuild webhook service after code changes
|
||||||
|
docker-compose build webhook
|
||||||
|
docker-compose up -d webhook
|
||||||
|
```
|
||||||
|
|
||||||
|
### Building Custom Image
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t esphome-webhook:custom .
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
- `GET /health` - Health check with configuration info
|
||||||
|
- `GET /devices` - List all devices (scans for `.yaml`/`.yml` files in config directory)
|
||||||
|
- Returns device name (filename without extension), config path, and last modified time
|
||||||
|
- Example: `bedroom-light.yaml` → device name: `bedroom-light`
|
||||||
|
- `POST /webhook/gitea` - Gitea webhook endpoint (handles push events, triggers git pull)
|
||||||
|
- `POST /sync/pull` - Manually trigger git pull from Gitea
|
||||||
|
- `POST /sync/push` - Manually trigger git push to Gitea (optional JSON body: `{"message": "commit message"}`)
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Environment variables (set in docker-compose.yml or locally):
|
||||||
|
|
||||||
|
**Core Settings:**
|
||||||
|
- `ESPHOME_CONFIG_DIR` - Path to device configurations (default: `/config`)
|
||||||
|
- `DEBOUNCE_SECONDS` - Delay before triggering after file change (default: `5`)
|
||||||
|
|
||||||
|
**Gitea Configuration:**
|
||||||
|
- `GITEA_URL` - URL of Gitea instance (e.g., `https://gitea.example.com`)
|
||||||
|
- `GITEA_REPO` - Repository path (e.g., `username/esphome-configs`)
|
||||||
|
- `GITEA_TOKEN` - Authentication token for Gitea API (generate in Gitea user settings)
|
||||||
|
- `GITEA_BRANCH` - Git branch to use (default: `main`)
|
||||||
|
- `AUTO_PUSH` - Auto-push local changes to Gitea (default: `false`)
|
||||||
|
|
||||||
|
**Git User Configuration:**
|
||||||
|
- `GIT_USER_NAME` - Git commit author name (default: `ESPHome Sync Service`)
|
||||||
|
- `GIT_USER_EMAIL` - Git commit author email (default: `esphome-sync@localhost`)
|
||||||
|
|
||||||
|
## Setup Guide
|
||||||
|
|
||||||
|
### Initial Setup
|
||||||
|
|
||||||
|
1. **Create Gitea Repository**
|
||||||
|
- Create a new repository in Gitea for your ESPHome configs
|
||||||
|
- Generate an access token in Gitea: Settings → Applications → Generate New Token
|
||||||
|
- Give token appropriate permissions (read/write repository)
|
||||||
|
|
||||||
|
2. **Configure Environment Variables**
|
||||||
|
- Update `docker-compose.yml` with your Gitea details:
|
||||||
|
- `GITEA_URL`: Your Gitea instance URL
|
||||||
|
- `GITEA_REPO`: Your repository path (username/repo)
|
||||||
|
- `GITEA_TOKEN`: Your generated token
|
||||||
|
- Set `AUTO_PUSH=true` if you want local changes auto-pushed to Gitea
|
||||||
|
|
||||||
|
3. **Start Services**
|
||||||
|
```bash
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
- On first startup, service will clone the Gitea repository to `/config`
|
||||||
|
- If `/config` already has files, manually initialize git first
|
||||||
|
|
||||||
|
4. **Configure Gitea Webhook** (Optional but recommended)
|
||||||
|
- Go to repository Settings → Webhooks → Add Webhook
|
||||||
|
- Set URL: `http://your-server:5000/webhook/gitea`
|
||||||
|
- Set Content Type: `application/json`
|
||||||
|
- Select events: `Push` events
|
||||||
|
- When you push to Gitea, service will automatically pull changes
|
||||||
|
|
||||||
|
### Migration from Old Structure
|
||||||
|
|
||||||
|
If you have existing configs in the old `device-name/main.yaml` structure, you need to migrate to the flat structure:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Move all device configs to the flat structure
|
||||||
|
cd config
|
||||||
|
for dir in */; do
|
||||||
|
if [ -f "${dir}main.yaml" ]; then
|
||||||
|
device_name="${dir%/}"
|
||||||
|
mv "${dir}main.yaml" "${device_name}.yaml"
|
||||||
|
rmdir "${dir}" 2>/dev/null || echo "Directory ${dir} not empty, manual cleanup needed"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Commit the changes
|
||||||
|
git add -A
|
||||||
|
git commit -m "Migrate to flat YAML structure"
|
||||||
|
git push origin main
|
||||||
|
```
|
||||||
|
|
||||||
|
After migration, your structure should be:
|
||||||
|
```
|
||||||
|
config/
|
||||||
|
├── device-1.yaml (was device-1/main.yaml)
|
||||||
|
├── device-2.yaml (was device-2/main.yaml)
|
||||||
|
└── .git/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Workflows
|
||||||
|
|
||||||
|
**Editing in ESPHome Dashboard:**
|
||||||
|
1. Edit YAML files in ESPHome dashboard
|
||||||
|
2. If `AUTO_PUSH=true`, changes automatically pushed to Gitea
|
||||||
|
3. If `AUTO_PUSH=false`, manually push: `curl -X POST http://localhost:5000/sync/push`
|
||||||
|
|
||||||
|
**Editing in Gitea (or git client):**
|
||||||
|
1. Commit and push changes to Gitea repository
|
||||||
|
2. Gitea webhook triggers `/webhook/gitea`
|
||||||
|
3. Service runs `git pull`
|
||||||
|
4. ESPHome sees updated files
|
||||||
|
|
||||||
|
**Manual Sync:**
|
||||||
|
- Pull from Gitea: `curl -X POST http://localhost:5000/sync/pull`
|
||||||
|
- Push to Gitea: `curl -X POST http://localhost:5000/sync/push -H "Content-Type: application/json" -d '{"message": "My commit message"}'`
|
||||||
|
|
||||||
|
## Key Implementation Details
|
||||||
|
|
||||||
|
### Device Config Resolution
|
||||||
|
|
||||||
|
The `find_device_config()` function looks for device configs at:
|
||||||
|
- `{ESPHOME_CONFIG_DIR}/{device_name}.yaml`
|
||||||
|
|
||||||
|
If the device name doesn't include an extension, `.yaml` is automatically appended.
|
||||||
|
|
||||||
|
### File Watching Behavior
|
||||||
|
|
||||||
|
The file watcher monitors all `.yaml` and `.yml` files directly in the config directory:
|
||||||
|
- **Watched**: Any `*.yaml` or `*.yml` file in the root of the config directory
|
||||||
|
- **Ignored**: Subdirectories, non-YAML files, and hidden files (like `.git`)
|
||||||
|
- **Debouncing**: Changes are debounced for `DEBOUNCE_SECONDS` (default 5) to prevent duplicate triggers
|
||||||
|
- **Auto-push**: If `AUTO_PUSH=true`, any watched file change triggers a git commit and push
|
||||||
|
- **Commit message format**: `Auto-sync: {device_name}.yaml changed`
|
||||||
|
|
||||||
|
Example watched files in `/config`:
|
||||||
|
- `bedroom-light.yaml` ✓
|
||||||
|
- `kitchen-sensor.yaml` ✓
|
||||||
|
- `garage-door.yml` ✓
|
||||||
|
- `README.md` ✗ (not YAML)
|
||||||
|
- `.gitignore` ✗ (hidden file)
|
||||||
|
- `backup/old-config.yaml` ✗ (in subdirectory)
|
||||||
|
|
||||||
|
### Git Authentication
|
||||||
|
|
||||||
|
Authentication is handled by injecting the Gitea token into the remote URL:
|
||||||
|
- Input: `GITEA_URL=https://gitea.com`, `GITEA_TOKEN=abc123`, `GITEA_REPO=user/repo`
|
||||||
|
- Remote URL: `https://abc123@gitea.com/user/repo.git`
|
||||||
|
- Token is only stored in environment variables, never committed to git
|
||||||
|
|
||||||
|
### Operation Flow
|
||||||
|
|
||||||
|
1. **Startup**: `initialize_git_repo()` → Clone if not exists OR Pull latest changes
|
||||||
|
2. **File Change Detected** → Debounced → Check if YAML file in config directory → Optionally `git push` (threaded)
|
||||||
|
3. **Gitea Webhook** → Parse payload → `git pull` → ESPHome sees updated files
|
||||||
|
4. **Thread Safety** → Operations use `operation_lock` to prevent concurrent access to `pending_operations` dict
|
||||||
|
|
||||||
|
### Git Repository Initialization
|
||||||
|
|
||||||
|
On startup, the service:
|
||||||
|
1. Checks if `/config/.git` exists
|
||||||
|
2. If not, clones the repository to a temp directory, then moves contents to `/config`
|
||||||
|
3. If yes, configures git user and pulls latest changes
|
||||||
|
4. All git operations have 60-second timeout
|
||||||
|
|
||||||
|
### Gunicorn Configuration (Dockerfile)
|
||||||
|
|
||||||
|
- 2 workers
|
||||||
|
- 600 second timeout
|
||||||
|
- Binds to 0.0.0.0:5000
|
||||||
|
|
||||||
|
## Docker Compose Architecture
|
||||||
|
|
||||||
|
Two services work together:
|
||||||
|
|
||||||
|
1. **esphome** - Official ESPHome dashboard (host network mode for mDNS/OTA)
|
||||||
|
- Dashboard: http://localhost:6052
|
||||||
|
- Handles compilation and device deployment
|
||||||
|
- Shares `./config` volume with sync service (read-only)
|
||||||
|
- Uses persistent `esphome-cache` volume
|
||||||
|
- **Immutable** - never modified, only reads configs
|
||||||
|
|
||||||
|
2. **webhook** (esphome-gitea-sync) - This service (bridged network)
|
||||||
|
- API: http://localhost:5000
|
||||||
|
- Synchronizes configurations between Gitea and ESPHome
|
||||||
|
- Read-write mount of `./config` for git operations
|
||||||
|
- Has git installed and manages the git repository
|
||||||
|
- ESPHome container handles all compilation/upload operations
|
||||||
|
|
||||||
|
### Volume Architecture
|
||||||
|
|
||||||
|
- `./config` is shared between both containers
|
||||||
|
- Sync service has read-write access (manages git repo)
|
||||||
|
- ESPHome has read access (consumes configs)
|
||||||
|
- On first startup, if `./config` is empty, sync service clones Gitea repo
|
||||||
35
Dockerfile
Normal file
35
Dockerfile
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# Dockerfile for ESPHome Webhook Service
|
||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
# Install system dependencies
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
git \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Install ESPHome
|
||||||
|
RUN pip install --no-cache-dir esphome
|
||||||
|
|
||||||
|
# Set working directory
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy requirements and install Python dependencies
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# Copy application code
|
||||||
|
COPY app.py .
|
||||||
|
|
||||||
|
# Create config directory
|
||||||
|
RUN mkdir -p /config
|
||||||
|
|
||||||
|
# Expose webhook service port
|
||||||
|
EXPOSE 5000
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
ENV ESPHOME_CONFIG_DIR=/config
|
||||||
|
ENV AUTO_COMPILE=true
|
||||||
|
ENV AUTO_UPLOAD=false
|
||||||
|
ENV DEBOUNCE_SECONDS=5
|
||||||
|
|
||||||
|
# Run the webhook service
|
||||||
|
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "2", "--timeout", "600", "app:app"]
|
||||||
223
README.md
Normal file
223
README.md
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
# ESPHome Webhook Service
|
||||||
|
|
||||||
|
A Docker-based webhook service for automating ESPHome device compilation and deployment with file watching capabilities.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Webhook Endpoints**: Trigger ESPHome operations via HTTP POST requests
|
||||||
|
- **File Watching**: Automatically detect YAML changes and trigger compilation/upload
|
||||||
|
- **REST API**: List devices, check health, and manage operations
|
||||||
|
- **Docker Compose**: Easy deployment with ESPHome dashboard
|
||||||
|
- **Debouncing**: Prevent duplicate operations from rapid file changes
|
||||||
|
- **Thread-Safe**: Concurrent operation handling with locks
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### 1. Start the Services
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd esphome-webhook-service
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
This starts two services:
|
||||||
|
- **ESPHome Dashboard**: `http://localhost:6052` (host network mode)
|
||||||
|
- **Webhook Service**: `http://localhost:5000`
|
||||||
|
|
||||||
|
### 2. Access the Services
|
||||||
|
|
||||||
|
- ESPHome Dashboard: http://localhost:6052
|
||||||
|
- Webhook API: http://localhost:5000/health
|
||||||
|
|
||||||
|
### 3. Stop the Services
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose down
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
Configure the webhook service behavior in `docker-compose.yml`:
|
||||||
|
|
||||||
|
| Variable | Default | Description |
|
||||||
|
|----------|---------|-------------|
|
||||||
|
| `ESPHOME_CONFIG_DIR` | `/config` | Directory containing device configs |
|
||||||
|
| `AUTO_COMPILE` | `true` | Auto-compile on YAML file changes |
|
||||||
|
| `AUTO_UPLOAD` | `false` | Auto-upload on YAML file changes (⚠️ use with caution) |
|
||||||
|
| `DEBOUNCE_SECONDS` | `5` | Delay before triggering operations after file change |
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
### Health Check
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:5000/health
|
||||||
|
```
|
||||||
|
|
||||||
|
### List Devices
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:5000/devices
|
||||||
|
```
|
||||||
|
|
||||||
|
### Validate Configuration
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:5000/webhook/validate/ades-office-control-panel
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compile Device
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:5000/webhook/compile/ades-office-control-panel
|
||||||
|
```
|
||||||
|
|
||||||
|
### Upload Device (OTA)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:5000/webhook/upload/ades-office-control-panel
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compile and Upload
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:5000/webhook/run/ades-office-control-panel
|
||||||
|
```
|
||||||
|
|
||||||
|
## File Watching
|
||||||
|
|
||||||
|
The service automatically watches all YAML files in the config directory. When a `main.yaml` file is modified:
|
||||||
|
|
||||||
|
1. **Auto-Compile Enabled**: Automatically compiles the device configuration
|
||||||
|
2. **Auto-Upload Enabled**: Automatically uploads firmware to the device (OTA)
|
||||||
|
|
||||||
|
**⚠️ WARNING**: Only enable `AUTO_UPLOAD=true` if you're confident in your changes and have physical access to devices in case of failures.
|
||||||
|
|
||||||
|
## Integration Examples
|
||||||
|
|
||||||
|
### GitHub Actions Webhook
|
||||||
|
|
||||||
|
Trigger compilation after pushing changes:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: ESPHome Compile
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- '**/*.yaml'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
compile:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Trigger Webhook
|
||||||
|
run: |
|
||||||
|
curl -X POST http://your-server:5000/webhook/compile/ades-office-control-panel
|
||||||
|
```
|
||||||
|
|
||||||
|
### Home Assistant Automation
|
||||||
|
|
||||||
|
Create an automation to compile devices:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
automation:
|
||||||
|
- alias: "Compile ESPHome Device"
|
||||||
|
trigger:
|
||||||
|
platform: webhook
|
||||||
|
webhook_id: esphome_compile
|
||||||
|
action:
|
||||||
|
service: rest_command.compile_device
|
||||||
|
data:
|
||||||
|
device: "{{ trigger.data.device }}"
|
||||||
|
|
||||||
|
rest_command:
|
||||||
|
compile_device:
|
||||||
|
url: "http://localhost:5000/webhook/compile/{{ device }}"
|
||||||
|
method: POST
|
||||||
|
```
|
||||||
|
|
||||||
|
## Directory Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
esphome-webhook-service/
|
||||||
|
├── app.py # Webhook service with file watching
|
||||||
|
├── Dockerfile # Container image definition
|
||||||
|
├── requirements.txt # Python dependencies
|
||||||
|
├── docker-compose.yml # Service orchestration
|
||||||
|
├── README.md # This file
|
||||||
|
└── config/ # Device configurations (mounted as /config)
|
||||||
|
├── ades-office-control-panel/
|
||||||
|
│ └── main.yaml
|
||||||
|
├── Old_Phone_Doorbell/
|
||||||
|
│ └── main.yaml
|
||||||
|
└── Oekoboiler/
|
||||||
|
└── main.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Service Won't Start
|
||||||
|
|
||||||
|
Check logs:
|
||||||
|
```bash
|
||||||
|
docker-compose logs webhook
|
||||||
|
docker-compose logs esphome
|
||||||
|
```
|
||||||
|
|
||||||
|
### Webhook Returns 404
|
||||||
|
|
||||||
|
Ensure the device name matches the directory name:
|
||||||
|
```bash
|
||||||
|
curl http://localhost:5000/devices
|
||||||
|
```
|
||||||
|
|
||||||
|
### OTA Upload Fails
|
||||||
|
|
||||||
|
1. Verify device is on the same network
|
||||||
|
2. Check ESPHome logs: `docker-compose logs esphome`
|
||||||
|
3. Ensure OTA password is correct in secrets.yaml
|
||||||
|
4. Try using host network mode for webhook service (uncomment in docker-compose.yml)
|
||||||
|
|
||||||
|
### File Watcher Not Triggering
|
||||||
|
|
||||||
|
1. Check that `AUTO_COMPILE` is set to `true`
|
||||||
|
2. Verify the file path is correct
|
||||||
|
3. Check webhook logs for file change events
|
||||||
|
4. Ensure Docker has permission to watch the mounted volume
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Run Locally (Without Docker)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install dependencies
|
||||||
|
pip install -r requirements.txt
|
||||||
|
pip install esphome
|
||||||
|
|
||||||
|
# Set environment variables
|
||||||
|
export ESPHOME_CONFIG_DIR=/path/to/esphome2/config
|
||||||
|
export AUTO_COMPILE=true
|
||||||
|
export AUTO_UPLOAD=false
|
||||||
|
|
||||||
|
# Run the service
|
||||||
|
python app.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build Custom Image
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t esphome-webhook:custom .
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
- The webhook service has no authentication by default
|
||||||
|
- Only expose port 5000 on trusted networks
|
||||||
|
- Use a reverse proxy (nginx, Traefik) with authentication for external access
|
||||||
|
- Keep `AUTO_UPLOAD=false` unless absolutely necessary
|
||||||
|
- Review changes before enabling auto-upload
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Same as parent repository (see LICENSE file).
|
||||||
434
app.py
Normal file
434
app.py
Normal file
@@ -0,0 +1,434 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
ESPHome Gitea Sync Service
|
||||||
|
|
||||||
|
This service synchronizes ESPHome device configurations between Gitea and ESPHome.
|
||||||
|
The actual compilation and upload operations are handled by the ESPHome container.
|
||||||
|
|
||||||
|
Provides:
|
||||||
|
1. File watching for detecting YAML changes
|
||||||
|
2. RESTful API for device management and sync operations
|
||||||
|
3. Webhook endpoints for Gitea integration
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import logging
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
from threading import Thread, Lock
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from flask import Flask, request, jsonify
|
||||||
|
from watchdog.observers import Observer
|
||||||
|
from watchdog.events import FileSystemEventHandler
|
||||||
|
|
||||||
|
# Configure logging
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||||
|
)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
ESPHOME_CONFIG_DIR = os.environ.get('ESPHOME_CONFIG_DIR', '/config')
|
||||||
|
DEBOUNCE_SECONDS = int(os.environ.get('DEBOUNCE_SECONDS', '5'))
|
||||||
|
|
||||||
|
# Gitea configuration
|
||||||
|
GITEA_URL = os.environ.get('GITEA_URL', '')
|
||||||
|
GITEA_REPO = os.environ.get('GITEA_REPO', '')
|
||||||
|
GITEA_TOKEN = os.environ.get('GITEA_TOKEN', '')
|
||||||
|
GITEA_BRANCH = os.environ.get('GITEA_BRANCH', 'main')
|
||||||
|
AUTO_PUSH = os.environ.get('AUTO_PUSH', 'false').lower() == 'true'
|
||||||
|
|
||||||
|
# Git user configuration
|
||||||
|
GIT_USER_NAME = os.environ.get('GIT_USER_NAME', 'ESPHome Sync Service')
|
||||||
|
GIT_USER_EMAIL = os.environ.get('GIT_USER_EMAIL', 'esphome-sync@localhost')
|
||||||
|
|
||||||
|
# Track ongoing operations to prevent duplicate triggers
|
||||||
|
operation_lock = Lock()
|
||||||
|
pending_operations = {}
|
||||||
|
|
||||||
|
|
||||||
|
class ESPHomeFileHandler(FileSystemEventHandler):
|
||||||
|
"""Handles file system events for ESPHome YAML files"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.last_modified = {}
|
||||||
|
|
||||||
|
def on_modified(self, event):
|
||||||
|
if event.is_directory:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Only watch YAML files
|
||||||
|
if not event.src_path.endswith(('.yaml', '.yml')):
|
||||||
|
return
|
||||||
|
|
||||||
|
# Debounce - ignore rapid repeated events
|
||||||
|
now = time.time()
|
||||||
|
if event.src_path in self.last_modified:
|
||||||
|
if now - self.last_modified[event.src_path] < DEBOUNCE_SECONDS:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.last_modified[event.src_path] = now
|
||||||
|
|
||||||
|
logger.info(f"Detected change in {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"Change detected in device: {device_name}")
|
||||||
|
|
||||||
|
if AUTO_PUSH:
|
||||||
|
logger.info(f"AUTO_PUSH enabled, pushing changes to Gitea")
|
||||||
|
Thread(target=git_push, args=(f"Auto-sync: {device_name}.yaml changed",)).start()
|
||||||
|
else:
|
||||||
|
logger.info(f"AUTO_PUSH disabled, skipping push to Gitea")
|
||||||
|
|
||||||
|
|
||||||
|
def find_device_config(device_name):
|
||||||
|
"""Find the YAML config for a given device"""
|
||||||
|
# Look for <device>.yaml directly in the config directory
|
||||||
|
config_path = Path(ESPHOME_CONFIG_DIR) / device_name
|
||||||
|
if config_path.suffix not in ['.yaml', '.yml']:
|
||||||
|
config_path = config_path.with_suffix('.yaml')
|
||||||
|
|
||||||
|
if not config_path.exists():
|
||||||
|
raise FileNotFoundError(f"Config not found for device: {device_name}")
|
||||||
|
|
||||||
|
return str(config_path)
|
||||||
|
|
||||||
|
|
||||||
|
# Git Operations
|
||||||
|
|
||||||
|
def run_git_command(args, cwd=None):
|
||||||
|
"""Execute a git command and return the result"""
|
||||||
|
if cwd is None:
|
||||||
|
cwd = ESPHOME_CONFIG_DIR
|
||||||
|
|
||||||
|
cmd = ['git'] + args
|
||||||
|
logger.info(f"Running git command in {cwd}: {' '.join(cmd)}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
cmd,
|
||||||
|
cwd=cwd,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=60
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.returncode != 0:
|
||||||
|
logger.error(f"Git command failed: {result.stderr}")
|
||||||
|
return {'success': False, 'error': result.stderr, 'stdout': result.stdout}
|
||||||
|
|
||||||
|
return {'success': True, 'stdout': result.stdout, 'stderr': result.stderr}
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
return {'success': False, 'error': 'Git command timed out'}
|
||||||
|
except Exception as e:
|
||||||
|
return {'success': False, 'error': str(e)}
|
||||||
|
|
||||||
|
|
||||||
|
def is_git_repo():
|
||||||
|
"""Check if the config directory is a git repository"""
|
||||||
|
git_dir = Path(ESPHOME_CONFIG_DIR) / '.git'
|
||||||
|
return git_dir.exists() and git_dir.is_dir()
|
||||||
|
|
||||||
|
|
||||||
|
def get_git_remote_url():
|
||||||
|
"""Build the git remote URL with authentication token"""
|
||||||
|
if not GITEA_URL or not GITEA_REPO:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Parse URL and inject token
|
||||||
|
# Format: https://token@gitea.com/user/repo.git
|
||||||
|
url = GITEA_URL.rstrip('/')
|
||||||
|
if url.startswith('https://'):
|
||||||
|
url = url.replace('https://', f'https://{GITEA_TOKEN}@', 1)
|
||||||
|
elif url.startswith('http://'):
|
||||||
|
url = url.replace('http://', f'http://{GITEA_TOKEN}@', 1)
|
||||||
|
|
||||||
|
return f"{url}/{GITEA_REPO}.git"
|
||||||
|
|
||||||
|
|
||||||
|
def git_clone():
|
||||||
|
"""Clone the Gitea repository to the config directory"""
|
||||||
|
if is_git_repo():
|
||||||
|
logger.info("Config directory is already a git repository")
|
||||||
|
return {'success': True, 'message': 'Already a git repository'}
|
||||||
|
|
||||||
|
remote_url = get_git_remote_url()
|
||||||
|
if not remote_url:
|
||||||
|
return {'success': False, 'error': 'Gitea URL, repo, or token not configured'}
|
||||||
|
|
||||||
|
logger.info(f"Cloning repository from {GITEA_URL}/{GITEA_REPO}")
|
||||||
|
|
||||||
|
# Clone into a temporary directory, then move contents
|
||||||
|
parent_dir = Path(ESPHOME_CONFIG_DIR).parent
|
||||||
|
temp_dir = parent_dir / 'temp_clone'
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Clone to temp directory
|
||||||
|
result = run_git_command(['clone', '-b', GITEA_BRANCH, remote_url, str(temp_dir)], cwd=parent_dir)
|
||||||
|
if not result['success']:
|
||||||
|
return result
|
||||||
|
|
||||||
|
# Move contents to config directory
|
||||||
|
import shutil
|
||||||
|
for item in temp_dir.iterdir():
|
||||||
|
dest = Path(ESPHOME_CONFIG_DIR) / item.name
|
||||||
|
if dest.exists():
|
||||||
|
if dest.is_dir():
|
||||||
|
shutil.rmtree(dest)
|
||||||
|
else:
|
||||||
|
dest.unlink()
|
||||||
|
shutil.move(str(item), str(dest))
|
||||||
|
|
||||||
|
# Remove temp directory
|
||||||
|
temp_dir.rmdir()
|
||||||
|
|
||||||
|
# Configure git user
|
||||||
|
run_git_command(['config', 'user.name', GIT_USER_NAME])
|
||||||
|
run_git_command(['config', 'user.email', GIT_USER_EMAIL])
|
||||||
|
|
||||||
|
logger.info("Repository cloned successfully")
|
||||||
|
return {'success': True, 'message': 'Repository cloned'}
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(f"Error during git clone: {e}")
|
||||||
|
return {'success': False, 'error': str(e)}
|
||||||
|
|
||||||
|
|
||||||
|
def git_pull():
|
||||||
|
"""Pull latest changes from Gitea"""
|
||||||
|
# Ensure safe.directory is set before git operations
|
||||||
|
run_git_command(['config', '--global', '--add', 'safe.directory', ESPHOME_CONFIG_DIR], cwd='/tmp')
|
||||||
|
|
||||||
|
# Ensure git user is configured (needed for merge commits)
|
||||||
|
run_git_command(['config', 'user.name', GIT_USER_NAME])
|
||||||
|
run_git_command(['config', 'user.email', GIT_USER_EMAIL])
|
||||||
|
|
||||||
|
if not is_git_repo():
|
||||||
|
logger.warning("Not a git repository, attempting to clone")
|
||||||
|
return git_clone()
|
||||||
|
|
||||||
|
logger.info("Pulling latest changes from Gitea")
|
||||||
|
|
||||||
|
# Fetch and pull
|
||||||
|
result = run_git_command(['fetch', 'origin', GITEA_BRANCH])
|
||||||
|
if not result['success']:
|
||||||
|
return result
|
||||||
|
|
||||||
|
result = run_git_command(['pull', 'origin', GITEA_BRANCH])
|
||||||
|
if result['success']:
|
||||||
|
logger.info("Git pull completed successfully")
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def git_push(commit_message="Auto-sync from ESPHome"):
|
||||||
|
"""Push local changes to Gitea"""
|
||||||
|
# Ensure safe.directory is set before git operations
|
||||||
|
run_git_command(['config', '--global', '--add', 'safe.directory', ESPHOME_CONFIG_DIR], cwd='/tmp')
|
||||||
|
|
||||||
|
# Ensure git user is configured
|
||||||
|
run_git_command(['config', 'user.name', GIT_USER_NAME])
|
||||||
|
run_git_command(['config', 'user.email', GIT_USER_EMAIL])
|
||||||
|
|
||||||
|
if not is_git_repo():
|
||||||
|
return {'success': False, 'error': 'Not a git repository'}
|
||||||
|
|
||||||
|
logger.info("Pushing changes to Gitea")
|
||||||
|
|
||||||
|
# Debug: Check status before adding
|
||||||
|
status_before = run_git_command(['status', '--porcelain'])
|
||||||
|
logger.info(f"Git status BEFORE add: {status_before.get('stdout', '').strip()}")
|
||||||
|
|
||||||
|
# Add all changes
|
||||||
|
result = run_git_command(['add', '-A'])
|
||||||
|
if not result['success']:
|
||||||
|
logger.error(f"Git add failed: {result.get('error')}")
|
||||||
|
return result
|
||||||
|
logger.info(f"Git add output: {result.get('stdout', '').strip()}")
|
||||||
|
|
||||||
|
# Check if there are changes to commit
|
||||||
|
status_result = run_git_command(['status', '--porcelain'])
|
||||||
|
logger.info(f"Git status AFTER add: {status_result.get('stdout', '').strip()}")
|
||||||
|
if status_result['success'] and not status_result['stdout'].strip():
|
||||||
|
logger.info("No changes to commit")
|
||||||
|
return {'success': True, 'message': 'No changes to commit'}
|
||||||
|
|
||||||
|
# Commit
|
||||||
|
result = run_git_command(['commit', '-m', commit_message])
|
||||||
|
if not result['success']:
|
||||||
|
return result
|
||||||
|
|
||||||
|
# Push
|
||||||
|
result = run_git_command(['push', 'origin', GITEA_BRANCH])
|
||||||
|
if result['success']:
|
||||||
|
logger.info("Changes pushed to Gitea successfully")
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def initialize_git_repo():
|
||||||
|
"""Initialize or verify git repository on startup"""
|
||||||
|
logger.info("Initializing git repository...")
|
||||||
|
|
||||||
|
# Mark the config directory as safe to prevent "dubious ownership" errors
|
||||||
|
# Run from /tmp to avoid git checking the dubious repository
|
||||||
|
run_git_command(['config', '--global', '--add', 'safe.directory', ESPHOME_CONFIG_DIR], cwd='/tmp')
|
||||||
|
|
||||||
|
if not GITEA_URL or not GITEA_REPO or not GITEA_TOKEN:
|
||||||
|
logger.warning("Gitea configuration incomplete, skipping git initialization")
|
||||||
|
return
|
||||||
|
|
||||||
|
if not is_git_repo():
|
||||||
|
logger.info("Config directory is not a git repository, cloning...")
|
||||||
|
result = git_clone()
|
||||||
|
if not result['success']:
|
||||||
|
logger.error(f"Failed to clone repository: {result.get('error')}")
|
||||||
|
else:
|
||||||
|
logger.info("Config directory is already a git repository")
|
||||||
|
# Ensure git user is configured
|
||||||
|
run_git_command(['config', 'user.name', GIT_USER_NAME])
|
||||||
|
run_git_command(['config', 'user.email', GIT_USER_EMAIL])
|
||||||
|
|
||||||
|
# Pull latest changes
|
||||||
|
logger.info("Pulling latest changes on startup...")
|
||||||
|
result = git_pull()
|
||||||
|
if not result['success']:
|
||||||
|
logger.error(f"Failed to pull changes: {result.get('error')}")
|
||||||
|
|
||||||
|
|
||||||
|
# REST API Endpoints
|
||||||
|
|
||||||
|
@app.route('/health', methods=['GET'])
|
||||||
|
def health_check():
|
||||||
|
"""Health check endpoint"""
|
||||||
|
return jsonify({
|
||||||
|
'status': 'healthy',
|
||||||
|
'timestamp': datetime.utcnow().isoformat(),
|
||||||
|
'config_dir': ESPHOME_CONFIG_DIR,
|
||||||
|
'service': 'gitea-sync'
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/devices', methods=['GET'])
|
||||||
|
def list_devices():
|
||||||
|
"""List all available devices"""
|
||||||
|
config_dir = Path(ESPHOME_CONFIG_DIR)
|
||||||
|
devices = []
|
||||||
|
|
||||||
|
# Find all .yaml/.yml files directly in the config directory
|
||||||
|
for item in config_dir.iterdir():
|
||||||
|
if item.is_file() and item.suffix in ['.yaml', '.yml']:
|
||||||
|
devices.append({
|
||||||
|
'name': item.stem, # Device name is the filename without extension
|
||||||
|
'config_path': str(item),
|
||||||
|
'last_modified': datetime.fromtimestamp(item.stat().st_mtime).isoformat()
|
||||||
|
})
|
||||||
|
|
||||||
|
return jsonify({'devices': devices})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/webhook/gitea', methods=['POST'])
|
||||||
|
def gitea_webhook():
|
||||||
|
"""Webhook endpoint for Gitea push events"""
|
||||||
|
logger.info("Received Gitea webhook")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Parse Gitea webhook payload
|
||||||
|
payload = request.json
|
||||||
|
if not payload:
|
||||||
|
return jsonify({'error': 'No payload received'}), 400
|
||||||
|
|
||||||
|
# Log the event
|
||||||
|
event_type = request.headers.get('X-Gitea-Event', 'unknown')
|
||||||
|
logger.info(f"Gitea event type: {event_type}")
|
||||||
|
|
||||||
|
# For push events, pull the changes
|
||||||
|
if event_type == 'push':
|
||||||
|
result = git_pull()
|
||||||
|
if result['success']:
|
||||||
|
return jsonify({'status': 'success', 'message': 'Changes pulled successfully'}), 200
|
||||||
|
else:
|
||||||
|
return jsonify({'status': 'error', 'error': result.get('error')}), 500
|
||||||
|
|
||||||
|
return jsonify({'status': 'ignored', 'message': f'Event type {event_type} not handled'}), 200
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(f"Error processing Gitea webhook: {e}")
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/sync/pull', methods=['POST'])
|
||||||
|
def manual_pull():
|
||||||
|
"""Manually trigger a git pull"""
|
||||||
|
logger.info("Manual git pull triggered")
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = git_pull()
|
||||||
|
if result['success']:
|
||||||
|
return jsonify({'status': 'success', 'message': 'Changes pulled successfully'}), 200
|
||||||
|
else:
|
||||||
|
return jsonify({'status': 'error', 'error': result.get('error')}), 500
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(f"Error during manual pull: {e}")
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/sync/push', methods=['POST'])
|
||||||
|
def manual_push():
|
||||||
|
"""Manually trigger a git push"""
|
||||||
|
logger.info("Manual git push triggered")
|
||||||
|
|
||||||
|
try:
|
||||||
|
commit_message = request.json.get('message', 'Manual sync from ESPHome') if request.json else 'Manual sync from ESPHome'
|
||||||
|
result = git_push(commit_message)
|
||||||
|
|
||||||
|
if result['success']:
|
||||||
|
return jsonify({'status': 'success', 'message': result.get('message', 'Changes pushed successfully')}), 200
|
||||||
|
else:
|
||||||
|
return jsonify({'status': 'error', 'error': result.get('error')}), 500
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(f"Error during manual push: {e}")
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def start_file_watcher():
|
||||||
|
"""Start the file system watcher"""
|
||||||
|
event_handler = ESPHomeFileHandler()
|
||||||
|
observer = Observer()
|
||||||
|
observer.schedule(event_handler, ESPHOME_CONFIG_DIR, recursive=True)
|
||||||
|
observer.start()
|
||||||
|
logger.info(f"File watcher started on {ESPHOME_CONFIG_DIR}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
time.sleep(1)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
observer.stop()
|
||||||
|
observer.join()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
logger.info("Starting ESPHome Gitea Sync Service")
|
||||||
|
logger.info(f"Config directory: {ESPHOME_CONFIG_DIR}")
|
||||||
|
logger.info(f"Debounce seconds: {DEBOUNCE_SECONDS}")
|
||||||
|
logger.info(f"Auto-push: {AUTO_PUSH}")
|
||||||
|
|
||||||
|
# Initialize git repository
|
||||||
|
initialize_git_repo()
|
||||||
|
|
||||||
|
# Start file watcher in a separate thread
|
||||||
|
watcher_thread = Thread(target=start_file_watcher, daemon=True)
|
||||||
|
watcher_thread.start()
|
||||||
|
|
||||||
|
# Start Flask app
|
||||||
|
app.run(host='0.0.0.0', port=5000, debug=False)
|
||||||
63
docker-compose.yml
Normal file
63
docker-compose.yml
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
# ESPHome service for managing device configurations
|
||||||
|
esphome:
|
||||||
|
image: esphome/esphome:latest
|
||||||
|
container_name: esphome
|
||||||
|
restart: unless-stopped
|
||||||
|
network_mode: host
|
||||||
|
volumes:
|
||||||
|
# Mount the config directory to access all device configs
|
||||||
|
- ./config:/config
|
||||||
|
# ESPHome cache and build artifacts
|
||||||
|
- esphome-cache:/cache
|
||||||
|
environment:
|
||||||
|
- ESPHOME_DASHBOARD_USE_PING=true
|
||||||
|
# ESPHome dashboard runs on port 6052
|
||||||
|
# Using host network mode for mDNS/Avahi discovery and OTA uploads
|
||||||
|
|
||||||
|
# Gitea sync service for synchronizing configs between Gitea and ESPHome
|
||||||
|
webhook:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: esphome-gitea-sync
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "5000:5000"
|
||||||
|
volumes:
|
||||||
|
# Share the same config directory with ESPHome (read-write for git operations)
|
||||||
|
- ./config:/config
|
||||||
|
environment:
|
||||||
|
# Configuration directory (local config subdirectory)
|
||||||
|
- ESPHOME_CONFIG_DIR=/config
|
||||||
|
|
||||||
|
# Debounce delay in seconds to prevent rapid repeated triggers
|
||||||
|
- DEBOUNCE_SECONDS=5
|
||||||
|
|
||||||
|
# Gitea repository configuration
|
||||||
|
- GITEA_URL=https://git.baumann.gr/
|
||||||
|
- GITEA_REPO=adebaumann/ESP-Home-Scripts
|
||||||
|
- GITEA_TOKEN=9254038f8f9863657f0015a9341dda4177e857bd
|
||||||
|
- GITEA_BRANCH=main
|
||||||
|
|
||||||
|
# Auto-push local changes to Gitea (default: false)
|
||||||
|
- AUTO_PUSH=false
|
||||||
|
|
||||||
|
# Git user configuration for commits
|
||||||
|
- GIT_USER_NAME=Adrian A. Baumann
|
||||||
|
- GIT_USER_EMAIL=ade@adebaumann.com
|
||||||
|
depends_on:
|
||||||
|
- esphome
|
||||||
|
# Optional: Uncomment to use host network if webhook needs access to local devices
|
||||||
|
# network_mode: host
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
# Persistent volume for ESPHome build cache
|
||||||
|
esphome-cache:
|
||||||
|
driver: local
|
||||||
|
|
||||||
|
networks:
|
||||||
|
default:
|
||||||
|
name: esphome-network
|
||||||
3
requirements.txt
Normal file
3
requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
Flask==3.0.0
|
||||||
|
watchdog==3.0.0
|
||||||
|
gunicorn==21.2.0
|
||||||
Reference in New Issue
Block a user