diff --git a/README.md b/README.md new file mode 100644 index 0000000..2db7f4d --- /dev/null +++ b/README.md @@ -0,0 +1,464 @@ +# Raspberry Pi IR Web API + +Kleine Python Web API für Raspberry Pi zur Ausführung definierter IR-Kommandos mittels `ir-ctl`. + +Features: + +- REST API mit API-Key +- Kleine Weboberfläche ohne Login +- JSON-basierte Konfiguration +- Definierte Kommandos +- Keine Shell Injection (`shell=False`) +- Timeout pro Kommando +- Logging +- Geeignet für openHAB HTTP Binding +- Kein Docker notwendig + +--- + +# Architektur + +```text ++-------------------+ +| openHAB / Browser | ++-------------------+ + | + v ++-------------------+ +| Python Web API | +| script-api.py | ++-------------------+ + | + v ++-------------------+ +| ir-ctl | +| Linux IR Subsystem| ++-------------------+ + | + v ++-------------------+ +| IR Sender GPIO | ++-------------------+ +``` + +--- + +# Voraussetzungen + +## Pakete installieren + +```bash +sudo apt update +sudo apt install -y ir-ctl python3 +``` + +Optional: + +```bash +sudo apt install -y v4l-utils +``` + +--- + +# Dateien + +```text +/home/timm/IRSender/ +├── script-api.py +├── commands.json +├── apiout.log +├── blue.txt +├── brighter.txt +├── darker.txt +├── defaultColor.txt +├── fade.txt +├── green.txt +├── LichtAus.txt +├── LichtEin.txt +├── Nachtlicht.txt +├── off.txt +├── on.txt +├── purple.txt +├── red.txt +├── smooth.txt +├── white.txt +└── yellow.txt +``` + +--- + +# Starten + +```bash +cd /home/timm/IRSender + +chmod +x script-api.py + +nohup python3 -u script-api.py ./commands.json >> apiout.log 2>&1 & +``` + +--- + +# Stoppen + +```bash +pkill -f script-api.py +``` + +--- + +# Logs anzeigen + +```bash +tail -f /home/timm/IRSender/apiout.log +``` + +--- + +# Health Check + +```bash +curl http://:8080/health +``` + +Antwort: + +```json +{ + "ok": true +} +``` + +--- + +# Weboberfläche + +```text +http://:8080/ +``` + +Die Weboberfläche benötigt keinen Login. + +Nur Commands mit: + +```json +"web_enabled": true +``` + +werden angezeigt. + +--- + +# REST API + +## Kommando ausführen + +### Request + +```bash +curl -s -X POST \ + -H "X-API-Key: change-this-global-api-key" \ + -H "Content-Type: application/json" \ + http://:8080/run/lirc-tv-power \ + -d '{"args":{}}' +``` + +### Response + +```json +{ + "ok": true, + "request_id": "f8d7f6a1", + "command_id": "lirc-tv-power", + "exit_code": 0, + "timed_out": false, + "duration_ms": 124, + "stdout": "", + "stderr": "" +} +``` + +--- + +# Kommandos anzeigen + +```bash +curl http://:8080/commands +``` + +--- + +# commands.json + +## Beispiel + +```json +{ + "host": "0.0.0.0", + "port": 8080, + + "commands": { + "lirc-tv-power": { + "description": "Send TV power button", + "web_title": "TV Power", + "web_enabled": true, + "api_key": "change-this-global-api-key", + "timeout_seconds": 10, + + "command": [ + "ir-ctl", + "--send", + "/home/timm/IRSender/LichtEin.txt" + ], + + "args": {} + } + } +} +``` + +--- + +# Neues Kommando hinzufügen + +## Beispiel + +```json +"my-command": { + "description": "Example command", + "web_title": "Example", + "web_enabled": true, + "api_key": "change-this-global-api-key", + "timeout_seconds": 10, + + "command": [ + "ir-ctl", + "--send", + "/home/timm/IRSender/example.txt" + ], + + "args": {} +} +``` + +--- + +# openHAB Integration + +## HTTP Thing + +### Thing + +```text +UID: http:url:ir_sender +label: IR Sender +thingTypeUID: http:url + +configuration: + baseURL: http://:8080 + commandMethod: POST + contentType: application/json + + headers: + - X-API-Key=change-this-global-api-key +``` + +--- + +## Channel + +```text +channels: + - id: lirc_tv_power + channelTypeUID: http:switch + label: TV Power + + configuration: + mode: WRITEONLY + commandExtension: /run/lirc-tv-power + + onValue: {"args":{}} + offValue: {"args":{}} +``` + +--- + +## Item + +```text +Switch IR_TV_Power "TV Power" + { channel="http:url:ir_sender:lirc_tv_power" } +``` + +--- + +# Systemd Service + +## Service Datei + +```ini +[Unit] +Description=Raspberry Pi IR Web API +After=network-online.target + +[Service] +Type=simple + +User=timm +WorkingDirectory=/home/timm/IRSender + +ExecStart=/usr/bin/python3 /home/timm/IRSender/script-api.py /home/timm/IRSender/commands.json + +Restart=always +RestartSec=3 + +NoNewPrivileges=true +PrivateTmp=true + +ProtectSystem=full +ProtectHome=false + +[Install] +WantedBy=multi-user.target +``` + +--- + +## Installation + +```bash +sudo nano /etc/systemd/system/rpi-ir-api.service +``` + +Service einfügen. + +Dann: + +```bash +sudo systemctl daemon-reload +sudo systemctl enable --now rpi-ir-api +``` + +--- + +# Service Status + +```bash +sudo systemctl status rpi-ir-api +``` + +--- + +# Service Logs + +```bash +journalctl -u rpi-ir-api -f +``` + +--- + +# Sicherheit + +## Implementiert + +- Keine Shell-Ausführung +- Keine freien Kommandos +- API-Key Prüfung +- JSON Validierung +- Timeout pro Kommando +- Regex Validierung +- HTML Escaping +- CSRF Token für Web UI +- Security Headers + +--- + +# Wichtig + +## Niemals erlauben: + +```json +"command": ["bash", "-c", "{user_input}"] +``` + +oder: + +```python +shell=True +``` + +Sonst wird aus der Lichtsteuerung sehr schnell eine Fernwartung mit Spezialeffekten. + +--- + +# Troubleshooting + +## 403 invalid api key + +API Key falsch oder Header fehlt. + +Prüfen: + +```bash +-H "X-API-Key: change-this-global-api-key" +``` + +--- + +## 400 invalid json + +Body ist kein gültiges JSON. + +Korrekt: + +```json +{"args":{}} +``` + +--- + +## 500 command failed + +`ir-ctl` oder Datei prüfen: + +```bash +ir-ctl --send /home/timm/IRSender/LichtEin.txt +``` + +--- + +## Permission denied + +GPIO / IR Rechte prüfen. + +Test: + +```bash +sudo ir-ctl --send /home/timm/IRSender/LichtEin.txt +``` + +Falls nur mit sudo funktioniert: + +```bash +sudo usermod -aG video timm +``` + +Danach neu anmelden. + +--- + +# Backup + +Wichtig sichern: + +```text +script-api.py +commands.json +*.txt +``` + +--- + +# Lizenz + +Internes Projekt. diff --git a/apiout.log b/apiout.log index b449dbb..a39055c 100644 --- a/apiout.log +++ b/apiout.log @@ -241,3 +241,35 @@ ConnectionResetError: [Errno 104] Connection reset by peer 192.168.178.49 [16/May/2026 10:27:25] "GET / HTTP/1.1" 200 - 192.168.178.49 [16/May/2026 10:27:25] "GET /favicon.ico HTTP/1.1" 404 - 192.168.178.49 [16/May/2026 10:27:37] "GET / HTTP/1.1" 200 - +192.168.178.149 [16/May/2026 11:22:14] "GET / HTTP/1.1" 200 - +request_id=c69ae4c0-2071-48ea-9500-235ae9568273 command_id=fade command=['ir-ctl', '--send', '/home/timm/IRSender/fade.txt'] +192.168.178.149 [16/May/2026 11:22:20] "POST /web/run/fade HTTP/1.1" 200 - +192.168.178.149 [16/May/2026 11:22:28] "GET / HTTP/1.1" 200 - +request_id=8a4e9c5b-b93d-47a3-8a3a-79e38c35a454 command_id=default-color command=['ir-ctl', '--send', '/home/timm/IRSender/defaultColor.txt'] +192.168.178.149 [16/May/2026 11:22:31] "POST /web/run/default-color HTTP/1.1" 200 - +192.168.178.149 [16/May/2026 11:22:35] "GET / HTTP/1.1" 200 - +request_id=09e16188-3704-4265-a1c7-9772611ff0d5 command_id=licht-aus command=['ir-ctl', '--send', '/home/timm/IRSender/LichtAus.txt'] +192.168.178.149 [16/May/2026 11:22:42] "POST /web/run/licht-aus HTTP/1.1" 200 - +192.168.178.149 [16/May/2026 11:22:48] "GET / HTTP/1.1" 200 - +request_id=65cb85ac-7aa2-486b-b4f9-af87e9e9c68e command_id=licht-ein command=['ir-ctl', '--send', '/home/timm/IRSender/LichtEin.txt'] +192.168.178.149 [16/May/2026 11:22:54] "POST /web/run/licht-ein HTTP/1.1" 200 - +192.168.178.149 [16/May/2026 11:22:57] "GET / HTTP/1.1" 200 - +---------------------------------------- +Exception occurred during processing of request from ('192.168.178.149', 61580) +Traceback (most recent call last): + File "/usr/lib/python3.9/socketserver.py", line 650, in process_request_thread + self.finish_request(request, client_address) + File "/usr/lib/python3.9/socketserver.py", line 360, in finish_request + self.RequestHandlerClass(request, client_address, self) + File "/usr/lib/python3.9/socketserver.py", line 720, in __init__ + self.handle() + File "/usr/lib/python3.9/http/server.py", line 434, in handle + self.handle_one_request() + File "/usr/lib/python3.9/http/server.py", line 402, in handle_one_request + self.raw_requestline = self.rfile.readline(65537) + File "/usr/lib/python3.9/socket.py", line 704, in readinto + return self._sock.recv_into(b) +ConnectionResetError: [Errno 104] Connection reset by peer +---------------------------------------- +Listening on http://0.0.0.0:8080 +Config: commands.json