diff --git a/README.md b/README.md index 0a51a67..1af1b83 100644 --- a/README.md +++ b/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 diff --git a/docker/.env.template b/docker/.env.template index 840bf62..0fd6ff7 100644 --- a/docker/.env.template +++ b/docker/.env.template @@ -19,3 +19,12 @@ VAULTWARDEN_ADMIN_TOKEN= # ============================================ N8N_USER=admin N8N_PASSWORD= + +# ============================================ +# 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= diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 5fc328e..2728d15 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -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 diff --git a/docker/samsung-tv-api/Dockerfile b/docker/samsung-tv-api/Dockerfile new file mode 100644 index 0000000..c65bde4 --- /dev/null +++ b/docker/samsung-tv-api/Dockerfile @@ -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"] diff --git a/docker/samsung-tv-api/README.md b/docker/samsung-tv-api/README.md new file mode 100644 index 0000000..a147d91 --- /dev/null +++ b/docker/samsung-tv-api/README.md @@ -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 +``` diff --git a/docker/samsung-tv-api/app.py b/docker/samsung-tv-api/app.py new file mode 100644 index 0000000..816ee65 --- /dev/null +++ b/docker/samsung-tv-api/app.py @@ -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) diff --git a/docker/samsung-tv-api/n8n-workflow-example.json b/docker/samsung-tv-api/n8n-workflow-example.json new file mode 100644 index 0000000..0450caa --- /dev/null +++ b/docker/samsung-tv-api/n8n-workflow-example.json @@ -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" +} diff --git a/docker/samsung-tv-api/requirements.txt b/docker/samsung-tv-api/requirements.txt new file mode 100644 index 0000000..132d6fb --- /dev/null +++ b/docker/samsung-tv-api/requirements.txt @@ -0,0 +1,4 @@ +samsungtvws[async,encrypted]==2.6.0 +flask==3.0.0 +gunicorn==21.2.0 +wakeonlan==3.1.0