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)
|
||||
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
|
||||
- Alle Services als Container
|
||||
- 50GB System + 100GB Data Volume
|
||||
```
|
||||
|
||||
## Services
|
||||
@@ -202,4 +207,9 @@ Siehe [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md) fuer:
|
||||
- WireGuard + Fail2Ban Integration
|
||||
- Separates Git-Repository: proxmox-netdata
|
||||
- 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_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:
|
||||
- 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
|
||||
# ============================================
|
||||
@@ -268,3 +309,5 @@ networks:
|
||||
driver: bridge
|
||||
api-net:
|
||||
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