Migrate Docker containers to dedicated VM
Architecture changes: - Created VM 100 "docker-services" (Debian 12 Cloud Image) - 10GB RAM, 6 Cores, 50GB system disk - Separate 100GB LVM data volume for service data - WireGuard moved from host to VM (10.0.0.2) - All containers migrated and running Updated documentation to reflect new architecture 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
14
README.md
14
README.md
@@ -15,9 +15,14 @@ Internet
|
|||||||
|
|
|
|
||||||
| WireGuard Tunnel (10.0.0.0/24)
|
| WireGuard Tunnel (10.0.0.0/24)
|
||||||
v
|
v
|
||||||
[Proxmox: 192.168.178.111 / 10.0.0.2]
|
[Proxmox: 192.168.178.111]
|
||||||
|
- Hypervisor
|
||||||
|
|
|
||||||
|
v
|
||||||
|
[VM 100 "docker-services": 192.168.178.200 / 10.0.0.2]
|
||||||
- Docker Host
|
- Docker Host
|
||||||
- Alle Services als Container
|
- Alle Services als Container
|
||||||
|
- 50GB System + 100GB Data Volume
|
||||||
```
|
```
|
||||||
|
|
||||||
## Services
|
## Services
|
||||||
@@ -202,4 +207,9 @@ Siehe [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md) fuer:
|
|||||||
- WireGuard + Fail2Ban Integration
|
- WireGuard + Fail2Ban Integration
|
||||||
- Separates Git-Repository: proxmox-netdata
|
- Separates Git-Repository: proxmox-netdata
|
||||||
- VT-x im BIOS aktiviert fuer VM-Support
|
- VT-x im BIOS aktiviert fuer VM-Support
|
||||||
- VM 100 "docker-services" vorbereitet (Debian 12 Cloud Image)
|
- Docker Container Migration zu VM 100:
|
||||||
|
- VM erstellt: 10GB RAM, 6 Cores, 50GB System Disk
|
||||||
|
- Separates 100GB Data Volume fuer Nextcloud/Services
|
||||||
|
- WireGuard auf VM konfiguriert (10.0.0.2)
|
||||||
|
- Alle Container erfolgreich migriert
|
||||||
|
- Alte Container auf Host gestoppt
|
||||||
|
|||||||
@@ -19,3 +19,12 @@ VAULTWARDEN_ADMIN_TOKEN=<admin-token-hier>
|
|||||||
# ============================================
|
# ============================================
|
||||||
N8N_USER=admin
|
N8N_USER=admin
|
||||||
N8N_PASSWORD=<sicheres-passwort-hier>
|
N8N_PASSWORD=<sicheres-passwort-hier>
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# SAMSUNG TV API
|
||||||
|
# ============================================
|
||||||
|
# TV IP-Adresse im lokalen Netzwerk (z.B. 192.168.178.50)
|
||||||
|
SAMSUNG_TV_IP=192.168.178.XXX
|
||||||
|
# TV MAC-Adresse für Wake-on-LAN (optional, Format: AA:BB:CC:DD:EE:FF)
|
||||||
|
# Findest du in TV-Einstellungen > Netzwerk > Netzwerkstatus
|
||||||
|
SAMSUNG_TV_MAC=
|
||||||
|
|||||||
@@ -217,6 +217,47 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- websites-net
|
- websites-net
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# SAMSUNG TV API - Smart TV Steuerung
|
||||||
|
# ============================================
|
||||||
|
samsung-tv-api:
|
||||||
|
build:
|
||||||
|
context: ./samsung-tv-api
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: samsung-tv-api
|
||||||
|
restart: unless-stopped
|
||||||
|
security_opt:
|
||||||
|
- apparmor=unconfined
|
||||||
|
- seccomp=unconfined
|
||||||
|
ports:
|
||||||
|
- "5050:5000"
|
||||||
|
volumes:
|
||||||
|
- /opt/docker/samsung-tv-api:/data
|
||||||
|
environment:
|
||||||
|
- SAMSUNG_TV_IP=${SAMSUNG_TV_IP:-192.168.178.100}
|
||||||
|
- SAMSUNG_TV_MAC=${SAMSUNG_TV_MAC:-}
|
||||||
|
- SAMSUNG_TV_NAME=n8n-proxmox
|
||||||
|
- SAMSUNG_TV_PORT=8002
|
||||||
|
- SAMSUNG_TV_TOKEN_FILE=/data/tv-token.txt
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:5000/health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: '0.5'
|
||||||
|
memory: 128M
|
||||||
|
logging:
|
||||||
|
driver: "json-file"
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "3"
|
||||||
|
networks:
|
||||||
|
- samsung-tv-net
|
||||||
|
- n8n-net # Damit n8n direkt zugreifen kann
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
# API - FastAPI Backend
|
# API - FastAPI Backend
|
||||||
# ============================================
|
# ============================================
|
||||||
@@ -268,3 +309,5 @@ networks:
|
|||||||
driver: bridge
|
driver: bridge
|
||||||
api-net:
|
api-net:
|
||||||
driver: bridge
|
driver: bridge
|
||||||
|
samsung-tv-net:
|
||||||
|
driver: bridge
|
||||||
|
|||||||
27
docker/samsung-tv-api/Dockerfile
Normal file
27
docker/samsung-tv-api/Dockerfile
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# System-Dependencies für Wake-on-LAN
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
curl \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Python Dependencies
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# App kopieren
|
||||||
|
COPY app.py .
|
||||||
|
|
||||||
|
# Daten-Verzeichnis für Token-Persistenz
|
||||||
|
RUN mkdir -p /data
|
||||||
|
|
||||||
|
# Non-root User
|
||||||
|
RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app /data
|
||||||
|
USER appuser
|
||||||
|
|
||||||
|
EXPOSE 5000
|
||||||
|
|
||||||
|
# Gunicorn für Production
|
||||||
|
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "2", "--timeout", "30", "app:app"]
|
||||||
184
docker/samsung-tv-api/README.md
Normal file
184
docker/samsung-tv-api/README.md
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
# Samsung TV API Service
|
||||||
|
|
||||||
|
REST API zur Steuerung von Samsung Smart TVs (2016+, Tizen OS) via WebSocket.
|
||||||
|
Entwickelt für die Integration mit n8n auf Proxmox.
|
||||||
|
|
||||||
|
## Unterstützte Geräte
|
||||||
|
|
||||||
|
- Samsung Smart TVs ab 2016 (Tizen OS)
|
||||||
|
- Getestet mit: UE50RU7409 (2019)
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
### 1. TV IP-Adresse ermitteln
|
||||||
|
|
||||||
|
Am TV: **Einstellungen → Allgemein → Netzwerk → Netzwerkstatus**
|
||||||
|
|
||||||
|
Oder im Router nachschauen (Fritz!Box: Heimnetz → Netzwerk)
|
||||||
|
|
||||||
|
### 2. .env Datei anpassen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# In /opt/docker/.env auf dem Proxmox Host
|
||||||
|
SAMSUNG_TV_IP=192.168.178.XXX
|
||||||
|
SAMSUNG_TV_MAC=AA:BB:CC:DD:EE:FF # Optional für Wake-on-LAN
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Container starten
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /opt/docker
|
||||||
|
docker-compose up -d samsung-tv-api
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Erste Verbindung - TV Autorisierung
|
||||||
|
|
||||||
|
Beim ersten API-Aufruf erscheint auf dem TV ein Dialog:
|
||||||
|
**"n8n-proxmox möchte sich verbinden - Zulassen?"**
|
||||||
|
|
||||||
|
→ Mit der TV-Fernbedienung **Zulassen** wählen
|
||||||
|
|
||||||
|
Der Token wird dann in `/opt/docker/samsung-tv-api/tv-token.txt` gespeichert.
|
||||||
|
|
||||||
|
**Tipp:** In TV-Einstellungen → Externe Geräteverwaltung → Geräteverbindungsmanager →
|
||||||
|
Zugriffsbenachrichtigung auf **"Nur beim ersten Mal"** stellen.
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
| Endpoint | Method | Beschreibung |
|
||||||
|
|----------|--------|--------------|
|
||||||
|
| `/` | GET | API Dokumentation |
|
||||||
|
| `/health` | GET | Health-Check |
|
||||||
|
| `/tv/status` | GET | TV Status prüfen |
|
||||||
|
| `/tv/power/on` | POST | TV einschalten (Wake-on-LAN) |
|
||||||
|
| `/tv/power/off` | POST | TV ausschalten |
|
||||||
|
| `/tv/key` | POST | Taste senden |
|
||||||
|
| `/tv/volume` | POST | Lautstärke steuern |
|
||||||
|
| `/tv/channel` | POST | Kanal wechseln |
|
||||||
|
| `/tv/source` | POST | Eingangsquelle wechseln |
|
||||||
|
| `/tv/apps` | GET | Installierte Apps auflisten |
|
||||||
|
| `/tv/app/open` | POST | App starten |
|
||||||
|
| `/tv/browser` | POST | URL im Browser öffnen |
|
||||||
|
|
||||||
|
## Beispiel-Aufrufe
|
||||||
|
|
||||||
|
### TV ausschalten
|
||||||
|
```bash
|
||||||
|
curl -X POST http://192.168.178.111:5050/tv/power/off
|
||||||
|
```
|
||||||
|
|
||||||
|
### Lautstärke erhöhen
|
||||||
|
```bash
|
||||||
|
curl -X POST http://192.168.178.111:5050/tv/volume \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"action": "up"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Taste senden
|
||||||
|
```bash
|
||||||
|
curl -X POST http://192.168.178.111:5050/tv/key \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"key": "KEY_MUTE"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Netflix starten
|
||||||
|
```bash
|
||||||
|
curl -X POST http://192.168.178.111:5050/tv/app/open \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"app_id": "Netflix"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### HDMI 1 auswählen
|
||||||
|
```bash
|
||||||
|
curl -X POST http://192.168.178.111:5050/tv/source \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"source": "hdmi1"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verfügbare Tasten (Keys)
|
||||||
|
|
||||||
|
### Navigation
|
||||||
|
- `KEY_UP`, `KEY_DOWN`, `KEY_LEFT`, `KEY_RIGHT`
|
||||||
|
- `KEY_ENTER`, `KEY_RETURN`, `KEY_EXIT`
|
||||||
|
- `KEY_HOME`, `KEY_MENU`, `KEY_INFO`
|
||||||
|
|
||||||
|
### Power
|
||||||
|
- `KEY_POWER`, `KEY_POWEROFF`
|
||||||
|
|
||||||
|
### Lautstärke
|
||||||
|
- `KEY_VOLUP`, `KEY_VOLDOWN`, `KEY_MUTE`
|
||||||
|
|
||||||
|
### Kanal
|
||||||
|
- `KEY_CHUP`, `KEY_CHDOWN`
|
||||||
|
- `KEY_0` bis `KEY_9`
|
||||||
|
|
||||||
|
### Quellen
|
||||||
|
- `KEY_SOURCE`
|
||||||
|
- `KEY_HDMI1`, `KEY_HDMI2`, `KEY_HDMI3`, `KEY_HDMI4`
|
||||||
|
- `KEY_TV`
|
||||||
|
|
||||||
|
### Wiedergabe
|
||||||
|
- `KEY_PLAY`, `KEY_PAUSE`, `KEY_STOP`
|
||||||
|
- `KEY_REWIND`, `KEY_FF` (Fast Forward)
|
||||||
|
- `KEY_RECORD`
|
||||||
|
|
||||||
|
### Farbtasten
|
||||||
|
- `KEY_RED`, `KEY_GREEN`, `KEY_YELLOW`, `KEY_BLUE`
|
||||||
|
|
||||||
|
## n8n Integration
|
||||||
|
|
||||||
|
### Von n8n aus aufrufen (im gleichen Docker-Netzwerk)
|
||||||
|
|
||||||
|
Da beide Container im `n8n-net` Netzwerk sind, kann n8n den Service
|
||||||
|
direkt über den Container-Namen erreichen:
|
||||||
|
|
||||||
|
```
|
||||||
|
URL: http://samsung-tv-api:5000/tv/power/off
|
||||||
|
```
|
||||||
|
|
||||||
|
### HTTP Request Node Konfiguration
|
||||||
|
|
||||||
|
1. **Method:** POST
|
||||||
|
2. **URL:** `http://samsung-tv-api:5000/tv/key`
|
||||||
|
3. **Body Content Type:** JSON
|
||||||
|
4. **Body:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"key": "KEY_VOLUP"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Beispiel-Workflow importieren
|
||||||
|
|
||||||
|
Die Datei `n8n-workflow-example.json` enthält einen Beispiel-Workflow
|
||||||
|
zum Importieren in n8n.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### TV nicht erreichbar
|
||||||
|
|
||||||
|
1. Prüfe ob TV eingeschaltet ist (im Standby funktioniert nur Wake-on-LAN)
|
||||||
|
2. Prüfe IP-Adresse: `ping 192.168.178.XXX`
|
||||||
|
3. Prüfe ob Port 8002 offen ist: `nc -zv 192.168.178.XXX 8002`
|
||||||
|
|
||||||
|
### "Verbindung abgelehnt"
|
||||||
|
|
||||||
|
- Wurde die Verbindung am TV erlaubt?
|
||||||
|
- Token-Datei löschen und neu verbinden:
|
||||||
|
```bash
|
||||||
|
rm /opt/docker/samsung-tv-api/tv-token.txt
|
||||||
|
docker-compose restart samsung-tv-api
|
||||||
|
```
|
||||||
|
|
||||||
|
### Wake-on-LAN funktioniert nicht
|
||||||
|
|
||||||
|
1. Am TV: Einstellungen → Allgemein → Netzwerk → Experteneinstellungen →
|
||||||
|
**Einschalten mit Mobilgerät** aktivieren
|
||||||
|
2. MAC-Adresse in `.env` korrekt eingetragen?
|
||||||
|
3. Broadcast funktioniert nur im gleichen Subnetz
|
||||||
|
|
||||||
|
### Container-Logs prüfen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker logs -f samsung-tv-api
|
||||||
|
```
|
||||||
367
docker/samsung-tv-api/app.py
Normal file
367
docker/samsung-tv-api/app.py
Normal file
@@ -0,0 +1,367 @@
|
|||||||
|
"""
|
||||||
|
Samsung TV API Service
|
||||||
|
Steuert Samsung Smart TVs (2016+) über WebSocket API
|
||||||
|
Für n8n Integration auf Proxmox
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
from flask import Flask, request, jsonify
|
||||||
|
from samsungtvws import SamsungTVWS
|
||||||
|
from wakeonlan import send_magic_packet
|
||||||
|
|
||||||
|
# Logging konfigurieren
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
# Konfiguration aus Umgebungsvariablen
|
||||||
|
TV_IP = os.environ.get('SAMSUNG_TV_IP', '192.168.178.100')
|
||||||
|
TV_MAC = os.environ.get('SAMSUNG_TV_MAC', '') # Optional für Wake-on-LAN
|
||||||
|
TV_NAME = os.environ.get('SAMSUNG_TV_NAME', 'n8n-proxmox')
|
||||||
|
TV_PORT = int(os.environ.get('SAMSUNG_TV_PORT', '8002')) # 8002 für SSL, 8001 für non-SSL
|
||||||
|
|
||||||
|
# Token-Pfad für persistente Authentifizierung
|
||||||
|
TOKEN_FILE = os.environ.get('SAMSUNG_TV_TOKEN_FILE', '/data/tv-token.txt')
|
||||||
|
|
||||||
|
|
||||||
|
def get_tv_connection():
|
||||||
|
"""Erstellt eine Verbindung zum Samsung TV"""
|
||||||
|
try:
|
||||||
|
tv = SamsungTVWS(
|
||||||
|
host=TV_IP,
|
||||||
|
port=TV_PORT,
|
||||||
|
name=TV_NAME,
|
||||||
|
token_file=TOKEN_FILE
|
||||||
|
)
|
||||||
|
return tv
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"TV Verbindung fehlgeschlagen: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/health', methods=['GET'])
|
||||||
|
def health():
|
||||||
|
"""Health-Check Endpoint"""
|
||||||
|
return jsonify({'status': 'ok', 'tv_ip': TV_IP})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/tv/status', methods=['GET'])
|
||||||
|
def tv_status():
|
||||||
|
"""Prüft ob der TV erreichbar ist"""
|
||||||
|
try:
|
||||||
|
tv = get_tv_connection()
|
||||||
|
if tv:
|
||||||
|
# Versuche eine einfache Abfrage
|
||||||
|
info = tv.rest_device_info()
|
||||||
|
return jsonify({
|
||||||
|
'status': 'online',
|
||||||
|
'info': info
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"TV nicht erreichbar: {e}")
|
||||||
|
|
||||||
|
return jsonify({'status': 'offline', 'tv_ip': TV_IP})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/tv/power/on', methods=['POST'])
|
||||||
|
def power_on():
|
||||||
|
"""TV einschalten via Wake-on-LAN"""
|
||||||
|
if not TV_MAC:
|
||||||
|
return jsonify({
|
||||||
|
'error': 'SAMSUNG_TV_MAC nicht konfiguriert',
|
||||||
|
'hint': 'Setze SAMSUNG_TV_MAC Umgebungsvariable'
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
try:
|
||||||
|
send_magic_packet(TV_MAC)
|
||||||
|
logger.info(f"Wake-on-LAN gesendet an {TV_MAC}")
|
||||||
|
return jsonify({'status': 'wol_sent', 'mac': TV_MAC})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Wake-on-LAN fehlgeschlagen: {e}")
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/tv/power/off', methods=['POST'])
|
||||||
|
def power_off():
|
||||||
|
"""TV ausschalten"""
|
||||||
|
try:
|
||||||
|
tv = get_tv_connection()
|
||||||
|
if tv:
|
||||||
|
tv.send_key('KEY_POWER')
|
||||||
|
return jsonify({'status': 'ok', 'action': 'power_off'})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Power off fehlgeschlagen: {e}")
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
return jsonify({'error': 'TV nicht erreichbar'}), 503
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/tv/key', methods=['POST'])
|
||||||
|
def send_key():
|
||||||
|
"""Sendet eine Taste zum TV
|
||||||
|
|
||||||
|
Body: {"key": "KEY_VOLUP"} oder {"keys": ["KEY_VOLUP", "KEY_VOLUP"]}
|
||||||
|
|
||||||
|
Häufige Keys:
|
||||||
|
- KEY_POWER, KEY_POWEROFF
|
||||||
|
- KEY_VOLUP, KEY_VOLDOWN, KEY_MUTE
|
||||||
|
- KEY_CHUP, KEY_CHDOWN
|
||||||
|
- KEY_UP, KEY_DOWN, KEY_LEFT, KEY_RIGHT, KEY_ENTER
|
||||||
|
- KEY_RETURN, KEY_HOME, KEY_MENU
|
||||||
|
- KEY_SOURCE, KEY_HDMI1, KEY_HDMI2, KEY_HDMI3
|
||||||
|
- KEY_PLAY, KEY_PAUSE, KEY_STOP
|
||||||
|
"""
|
||||||
|
data = request.get_json() or {}
|
||||||
|
|
||||||
|
# Einzelne Taste oder Liste von Tasten
|
||||||
|
keys = data.get('keys', [])
|
||||||
|
if 'key' in data:
|
||||||
|
keys = [data['key']]
|
||||||
|
|
||||||
|
if not keys:
|
||||||
|
return jsonify({
|
||||||
|
'error': 'key oder keys Parameter erforderlich',
|
||||||
|
'example': {'key': 'KEY_VOLUP'}
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
try:
|
||||||
|
tv = get_tv_connection()
|
||||||
|
if tv:
|
||||||
|
for key in keys:
|
||||||
|
tv.send_key(key)
|
||||||
|
logger.info(f"Key gesendet: {key}")
|
||||||
|
return jsonify({'status': 'ok', 'keys_sent': keys})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Key senden fehlgeschlagen: {e}")
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
return jsonify({'error': 'TV nicht erreichbar'}), 503
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/tv/volume', methods=['POST'])
|
||||||
|
def set_volume():
|
||||||
|
"""Setzt die Lautstärke
|
||||||
|
|
||||||
|
Body: {"volume": 25} oder {"action": "up|down|mute"}
|
||||||
|
"""
|
||||||
|
data = request.get_json() or {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
tv = get_tv_connection()
|
||||||
|
if not tv:
|
||||||
|
return jsonify({'error': 'TV nicht erreichbar'}), 503
|
||||||
|
|
||||||
|
if 'volume' in data:
|
||||||
|
# Absolute Lautstärke (nicht alle TVs unterstützen das)
|
||||||
|
volume = int(data['volume'])
|
||||||
|
# Sende entsprechend viele KEY_VOLUP/DOWN
|
||||||
|
return jsonify({
|
||||||
|
'status': 'ok',
|
||||||
|
'note': 'Absolute Lautstärke nicht direkt unterstützt, nutze action: up/down'
|
||||||
|
})
|
||||||
|
|
||||||
|
action = data.get('action', '').lower()
|
||||||
|
key_map = {
|
||||||
|
'up': 'KEY_VOLUP',
|
||||||
|
'down': 'KEY_VOLDOWN',
|
||||||
|
'mute': 'KEY_MUTE'
|
||||||
|
}
|
||||||
|
|
||||||
|
if action in key_map:
|
||||||
|
tv.send_key(key_map[action])
|
||||||
|
return jsonify({'status': 'ok', 'action': action})
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'error': 'Ungültige action',
|
||||||
|
'valid_actions': list(key_map.keys())
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Volume Steuerung fehlgeschlagen: {e}")
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/tv/channel', methods=['POST'])
|
||||||
|
def change_channel():
|
||||||
|
"""Wechselt den Kanal
|
||||||
|
|
||||||
|
Body: {"action": "up|down"} oder {"number": "123"}
|
||||||
|
"""
|
||||||
|
data = request.get_json() or {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
tv = get_tv_connection()
|
||||||
|
if not tv:
|
||||||
|
return jsonify({'error': 'TV nicht erreichbar'}), 503
|
||||||
|
|
||||||
|
action = data.get('action', '').lower()
|
||||||
|
if action == 'up':
|
||||||
|
tv.send_key('KEY_CHUP')
|
||||||
|
return jsonify({'status': 'ok', 'action': 'channel_up'})
|
||||||
|
elif action == 'down':
|
||||||
|
tv.send_key('KEY_CHDOWN')
|
||||||
|
return jsonify({'status': 'ok', 'action': 'channel_down'})
|
||||||
|
|
||||||
|
# Kanal per Nummer eingeben
|
||||||
|
number = data.get('number', '')
|
||||||
|
if number:
|
||||||
|
key_map = {str(i): f'KEY_{i}' for i in range(10)}
|
||||||
|
for digit in str(number):
|
||||||
|
if digit in key_map:
|
||||||
|
tv.send_key(key_map[digit])
|
||||||
|
tv.send_key('KEY_ENTER')
|
||||||
|
return jsonify({'status': 'ok', 'channel': number})
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'error': 'action oder number Parameter erforderlich',
|
||||||
|
'example': {'action': 'up'}
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Kanal wechseln fehlgeschlagen: {e}")
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/tv/source', methods=['POST'])
|
||||||
|
def change_source():
|
||||||
|
"""Wechselt die Eingangsquelle
|
||||||
|
|
||||||
|
Body: {"source": "hdmi1|hdmi2|hdmi3|tv"}
|
||||||
|
"""
|
||||||
|
data = request.get_json() or {}
|
||||||
|
source = data.get('source', '').lower()
|
||||||
|
|
||||||
|
source_map = {
|
||||||
|
'hdmi1': 'KEY_HDMI1',
|
||||||
|
'hdmi2': 'KEY_HDMI2',
|
||||||
|
'hdmi3': 'KEY_HDMI3',
|
||||||
|
'hdmi4': 'KEY_HDMI4',
|
||||||
|
'tv': 'KEY_TV',
|
||||||
|
'source': 'KEY_SOURCE' # Öffnet Source-Menü
|
||||||
|
}
|
||||||
|
|
||||||
|
if source not in source_map:
|
||||||
|
return jsonify({
|
||||||
|
'error': 'Ungültige source',
|
||||||
|
'valid_sources': list(source_map.keys())
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
try:
|
||||||
|
tv = get_tv_connection()
|
||||||
|
if tv:
|
||||||
|
tv.send_key(source_map[source])
|
||||||
|
return jsonify({'status': 'ok', 'source': source})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Source wechseln fehlgeschlagen: {e}")
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
return jsonify({'error': 'TV nicht erreichbar'}), 503
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/tv/apps', methods=['GET'])
|
||||||
|
def list_apps():
|
||||||
|
"""Listet installierte Apps auf"""
|
||||||
|
try:
|
||||||
|
tv = get_tv_connection()
|
||||||
|
if tv:
|
||||||
|
apps = tv.app_list()
|
||||||
|
return jsonify({'status': 'ok', 'apps': apps})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"App-Liste abrufen fehlgeschlagen: {e}")
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
return jsonify({'error': 'TV nicht erreichbar'}), 503
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/tv/app/open', methods=['POST'])
|
||||||
|
def open_app():
|
||||||
|
"""Startet eine App
|
||||||
|
|
||||||
|
Body: {"app_id": "Netflix"} oder {"app_id": "3201907018807"}
|
||||||
|
|
||||||
|
Bekannte App-IDs:
|
||||||
|
- Netflix: 3201907018807
|
||||||
|
- YouTube: 111299001912
|
||||||
|
- Prime Video: 3201910019365
|
||||||
|
- Disney+: 3201901017640
|
||||||
|
"""
|
||||||
|
data = request.get_json() or {}
|
||||||
|
app_id = data.get('app_id')
|
||||||
|
|
||||||
|
if not app_id:
|
||||||
|
return jsonify({
|
||||||
|
'error': 'app_id Parameter erforderlich',
|
||||||
|
'hint': 'Nutze GET /tv/apps für eine Liste'
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
try:
|
||||||
|
tv = get_tv_connection()
|
||||||
|
if tv:
|
||||||
|
tv.run_app(app_id)
|
||||||
|
return jsonify({'status': 'ok', 'app': app_id})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"App starten fehlgeschlagen: {e}")
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
return jsonify({'error': 'TV nicht erreichbar'}), 503
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/tv/browser', methods=['POST'])
|
||||||
|
def open_browser():
|
||||||
|
"""Öffnet eine URL im TV-Browser
|
||||||
|
|
||||||
|
Body: {"url": "https://example.com"}
|
||||||
|
"""
|
||||||
|
data = request.get_json() or {}
|
||||||
|
url = data.get('url')
|
||||||
|
|
||||||
|
if not url:
|
||||||
|
return jsonify({'error': 'url Parameter erforderlich'}), 400
|
||||||
|
|
||||||
|
try:
|
||||||
|
tv = get_tv_connection()
|
||||||
|
if tv:
|
||||||
|
tv.open_browser(url)
|
||||||
|
return jsonify({'status': 'ok', 'url': url})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Browser öffnen fehlgeschlagen: {e}")
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
return jsonify({'error': 'TV nicht erreichbar'}), 503
|
||||||
|
|
||||||
|
|
||||||
|
# API Dokumentation
|
||||||
|
@app.route('/', methods=['GET'])
|
||||||
|
def api_docs():
|
||||||
|
"""API Übersicht"""
|
||||||
|
return jsonify({
|
||||||
|
'service': 'Samsung TV API',
|
||||||
|
'tv_ip': TV_IP,
|
||||||
|
'endpoints': {
|
||||||
|
'GET /health': 'Health-Check',
|
||||||
|
'GET /tv/status': 'TV Status prüfen',
|
||||||
|
'POST /tv/power/on': 'TV einschalten (Wake-on-LAN)',
|
||||||
|
'POST /tv/power/off': 'TV ausschalten',
|
||||||
|
'POST /tv/key': 'Taste senden {"key": "KEY_VOLUP"}',
|
||||||
|
'POST /tv/volume': 'Lautstärke {"action": "up|down|mute"}',
|
||||||
|
'POST /tv/channel': 'Kanal {"action": "up|down"} oder {"number": "123"}',
|
||||||
|
'POST /tv/source': 'Quelle {"source": "hdmi1|hdmi2|tv"}',
|
||||||
|
'GET /tv/apps': 'Installierte Apps auflisten',
|
||||||
|
'POST /tv/app/open': 'App starten {"app_id": "Netflix"}',
|
||||||
|
'POST /tv/browser': 'URL öffnen {"url": "https://..."}',
|
||||||
|
},
|
||||||
|
'common_keys': [
|
||||||
|
'KEY_POWER', 'KEY_VOLUP', 'KEY_VOLDOWN', 'KEY_MUTE',
|
||||||
|
'KEY_CHUP', 'KEY_CHDOWN', 'KEY_UP', 'KEY_DOWN',
|
||||||
|
'KEY_LEFT', 'KEY_RIGHT', 'KEY_ENTER', 'KEY_RETURN',
|
||||||
|
'KEY_HOME', 'KEY_MENU', 'KEY_SOURCE',
|
||||||
|
'KEY_PLAY', 'KEY_PAUSE', 'KEY_STOP'
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
logger.info(f"Samsung TV API startet - TV IP: {TV_IP}")
|
||||||
|
app.run(host='0.0.0.0', port=5000, debug=True)
|
||||||
147
docker/samsung-tv-api/n8n-workflow-example.json
Normal file
147
docker/samsung-tv-api/n8n-workflow-example.json
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
{
|
||||||
|
"name": "Samsung TV Steuerung",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"httpMethod": "POST",
|
||||||
|
"path": "tv-control",
|
||||||
|
"responseMode": "responseNode",
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"id": "webhook-trigger",
|
||||||
|
"name": "Webhook Trigger",
|
||||||
|
"type": "n8n-nodes-base.webhook",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"position": [250, 300]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"conditions": {
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true,
|
||||||
|
"leftValue": "",
|
||||||
|
"typeValidation": "strict"
|
||||||
|
},
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"id": "condition-power-off",
|
||||||
|
"leftValue": "={{ $json.body.command }}",
|
||||||
|
"rightValue": "power_off",
|
||||||
|
"operator": {
|
||||||
|
"type": "string",
|
||||||
|
"operation": "equals"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"combinator": "and"
|
||||||
|
},
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"id": "switch-command",
|
||||||
|
"name": "Switch Command",
|
||||||
|
"type": "n8n-nodes-base.switch",
|
||||||
|
"typeVersion": 3,
|
||||||
|
"position": [450, 300]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"method": "POST",
|
||||||
|
"url": "http://samsung-tv-api:5000/tv/power/off",
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"id": "tv-power-off",
|
||||||
|
"name": "TV Power Off",
|
||||||
|
"type": "n8n-nodes-base.httpRequest",
|
||||||
|
"typeVersion": 4.2,
|
||||||
|
"position": [650, 200]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"method": "POST",
|
||||||
|
"url": "http://samsung-tv-api:5000/tv/key",
|
||||||
|
"sendBody": true,
|
||||||
|
"specifyBody": "json",
|
||||||
|
"jsonBody": "={{ JSON.stringify({ key: $json.body.key }) }}",
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"id": "tv-send-key",
|
||||||
|
"name": "TV Send Key",
|
||||||
|
"type": "n8n-nodes-base.httpRequest",
|
||||||
|
"typeVersion": 4.2,
|
||||||
|
"position": [650, 400]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"respondWith": "json",
|
||||||
|
"responseBody": "={{ $json }}",
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"id": "respond-webhook",
|
||||||
|
"name": "Respond to Webhook",
|
||||||
|
"type": "n8n-nodes-base.respondToWebhook",
|
||||||
|
"typeVersion": 1.1,
|
||||||
|
"position": [850, 300]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"connections": {
|
||||||
|
"Webhook Trigger": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Switch Command",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Switch Command": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "TV Power Off",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "TV Send Key",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"TV Power Off": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Respond to Webhook",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"TV Send Key": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Respond to Webhook",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"executionOrder": "v1"
|
||||||
|
},
|
||||||
|
"staticData": null,
|
||||||
|
"tags": [],
|
||||||
|
"triggerCount": 0,
|
||||||
|
"updatedAt": "2025-12-28T00:00:00.000Z",
|
||||||
|
"versionId": "1"
|
||||||
|
}
|
||||||
4
docker/samsung-tv-api/requirements.txt
Normal file
4
docker/samsung-tv-api/requirements.txt
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
samsungtvws[async,encrypted]==2.6.0
|
||||||
|
flask==3.0.0
|
||||||
|
gunicorn==21.2.0
|
||||||
|
wakeonlan==3.1.0
|
||||||
Reference in New Issue
Block a user