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:
Martin Eckardt
2025-12-28 21:04:47 +01:00
parent 3f74077c3e
commit 64bcc0091a
8 changed files with 793 additions and 2 deletions

View File

@@ -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

View File

@@ -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=

View File

@@ -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

View 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"]

View 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
```

View 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)

View 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"
}

View File

@@ -0,0 +1,4 @@
samsungtvws[async,encrypted]==2.6.0
flask==3.0.0
gunicorn==21.2.0
wakeonlan==3.1.0