forked from TillRepo/IrSender
setup
This commit is contained in:
@@ -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://<rpi-ip>:8080/health
|
||||
```
|
||||
|
||||
Antwort:
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Weboberfläche
|
||||
|
||||
```text
|
||||
http://<rpi-ip>: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://<rpi-ip>: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://<rpi-ip>: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://<rpi-ip>: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.
|
||||
@@ -0,0 +1,66 @@
|
||||
{
|
||||
"carrier": 38000,
|
||||
"repeat": 1,
|
||||
"signal": [
|
||||
{ "type": "mark", "duration": 2666 },
|
||||
{ "type": "space", "duration": 893 },
|
||||
|
||||
{ "type": "mark", "duration": 410 },
|
||||
{ "type": "space", "duration": 934 },
|
||||
|
||||
{ "type": "mark", "duration": 405 },
|
||||
{ "type": "space", "duration": 463 },
|
||||
|
||||
{ "type": "mark", "duration": 431 },
|
||||
{ "type": "space", "duration": 485 },
|
||||
|
||||
{ "type": "mark", "duration": 406 },
|
||||
{ "type": "space", "duration": 921 },
|
||||
|
||||
{ "type": "mark", "duration": 868 },
|
||||
{ "type": "space", "duration": 458 },
|
||||
|
||||
{ "type": "mark", "duration": 410 },
|
||||
{ "type": "space", "duration": 484 },
|
||||
|
||||
{ "type": "mark", "duration": 407 },
|
||||
{ "type": "space", "duration": 487 },
|
||||
|
||||
{ "type": "mark", "duration": 406 },
|
||||
{ "type": "space", "duration": 486 },
|
||||
|
||||
{ "type": "mark", "duration": 409 },
|
||||
{ "type": "space", "duration": 485 },
|
||||
|
||||
{ "type": "mark", "duration": 883 },
|
||||
{ "type": "space", "duration": 905 },
|
||||
|
||||
{ "type": "mark", "duration": 408 },
|
||||
{ "type": "space", "duration": 484 },
|
||||
|
||||
{ "type": "mark", "duration": 409 },
|
||||
{ "type": "space", "duration": 486 },
|
||||
|
||||
{ "type": "mark", "duration": 409 },
|
||||
{ "type": "space", "duration": 484 },
|
||||
|
||||
{ "type": "mark", "duration": 408 },
|
||||
{ "type": "space", "duration": 486 },
|
||||
|
||||
{ "type": "mark", "duration": 409 },
|
||||
{ "type": "space", "duration": 486 },
|
||||
|
||||
{ "type": "mark", "duration": 883 },
|
||||
{ "type": "space", "duration": 458 },
|
||||
|
||||
{ "type": "mark", "duration": 408 },
|
||||
{ "type": "space", "duration": 932 },
|
||||
|
||||
{ "type": "mark", "duration": 406 },
|
||||
{ "type": "space", "duration": 488 },
|
||||
|
||||
{ "type": "mark", "duration": 406 },
|
||||
|
||||
{ "type": "space", "duration": 21217 }
|
||||
]
|
||||
}
|
||||
+275
@@ -0,0 +1,275 @@
|
||||
Listening on http://0.0.0.0:8080
|
||||
Config: commands.json
|
||||
192.168.178.49 [16/May/2026 01:29:20] "GET / HTTP/1.1" 200 -
|
||||
192.168.178.49 [16/May/2026 01:36:27] "GET / HTTP/1.1" 200 -
|
||||
192.168.178.49 [16/May/2026 01:36:30] "GET / HTTP/1.1" 200 -
|
||||
192.168.178.49 [16/May/2026 01:37:00] "GET / HTTP/1.1" 200 -
|
||||
192.168.178.49 [16/May/2026 01:44:07] "GET /run/default-color HTTP/1.1" 404 -
|
||||
192.168.178.49 [16/May/2026 01:44:22] "GET /run/default-color HTTP/1.1" 404 -
|
||||
192.168.178.49 [16/May/2026 01:44:52] "GET /run/default-color HTTP/1.1" 404 -
|
||||
192.168.178.49 [16/May/2026 01:45:22] "GET /run/default-color HTTP/1.1" 404 -
|
||||
192.168.178.49 [16/May/2026 01:45:52] "GET /run/default-color HTTP/1.1" 404 -
|
||||
192.168.178.49 [16/May/2026 01:46:08] "GET /run/default-color HTTP/1.1" 404 -
|
||||
192.168.178.49 [16/May/2026 01:46:27] "GET /run/default-color HTTP/1.1" 404 -
|
||||
192.168.178.49 [16/May/2026 01:46:57] "GET /run/default-color HTTP/1.1" 404 -
|
||||
192.168.178.49 [16/May/2026 01:47:27] "GET /run/default-color HTTP/1.1" 404 -
|
||||
192.168.178.49 [16/May/2026 01:47:57] "GET /run/default-color HTTP/1.1" 404 -
|
||||
192.168.178.49 [16/May/2026 01:48:27] "GET /run/default-color HTTP/1.1" 404 -
|
||||
192.168.178.49 [16/May/2026 01:48:57] "GET /run/default-color HTTP/1.1" 404 -
|
||||
192.168.178.49 [16/May/2026 01:49:27] "GET /run/default-color HTTP/1.1" 404 -
|
||||
192.168.178.49 [16/May/2026 01:49:57] "GET /run/default-color HTTP/1.1" 404 -
|
||||
192.168.178.49 [16/May/2026 01:50:27] "GET /run/default-color HTTP/1.1" 404 -
|
||||
192.168.178.49 [16/May/2026 01:50:57] "GET /run/default-color HTTP/1.1" 404 -
|
||||
192.168.178.49 [16/May/2026 01:51:27] "GET /run/default-color HTTP/1.1" 404 -
|
||||
192.168.178.49 [16/May/2026 01:51:57] "GET /run/default-color HTTP/1.1" 404 -
|
||||
192.168.178.49 [16/May/2026 01:52:10] "GET /run/default-color HTTP/1.1" 404 -
|
||||
192.168.178.49 [16/May/2026 01:52:15] "GET /run/default-color HTTP/1.1" 404 -
|
||||
192.168.178.49 [16/May/2026 01:52:41] "GET /run/default-color HTTP/1.1" 404 -
|
||||
192.168.178.49 [16/May/2026 01:53:11] "GET /run/default-color HTTP/1.1" 404 -
|
||||
192.168.178.49 [16/May/2026 01:53:29] "GET /run/default-color HTTP/1.1" 404 -
|
||||
192.168.178.49 [16/May/2026 01:53:33] "GET /run/default-color HTTP/1.1" 404 -
|
||||
192.168.178.49 [16/May/2026 01:53:36] "GET /run/default-color HTTP/1.1" 404 -
|
||||
192.168.178.49 [16/May/2026 01:54:47] "GET /run/default-color HTTP/1.1" 404 -
|
||||
192.168.178.49 [16/May/2026 01:54:58] "GET / HTTP/1.1" 200 -
|
||||
192.168.178.49 [16/May/2026 01:55:17] "GET /run/default-color HTTP/1.1" 404 -
|
||||
192.168.178.49 [16/May/2026 01:55:47] "GET /run/default-color HTTP/1.1" 404 -
|
||||
192.168.178.49 [16/May/2026 01:56:17] "GET /run/default-color HTTP/1.1" 404 -
|
||||
192.168.178.49 [16/May/2026 01:56:47] "GET /run/default-color HTTP/1.1" 404 -
|
||||
192.168.178.49 [16/May/2026 01:57:17] "GET /run/default-color HTTP/1.1" 404 -
|
||||
192.168.178.49 [16/May/2026 01:57:47] "GET /run/default-color HTTP/1.1" 404 -
|
||||
192.168.178.49 [16/May/2026 01:58:17] "GET /run/default-color HTTP/1.1" 404 -
|
||||
192.168.178.49 [16/May/2026 01:58:47] "GET /run/default-color HTTP/1.1" 404 -
|
||||
192.168.178.49 [16/May/2026 01:59:17] "GET /run/default-color HTTP/1.1" 404 -
|
||||
192.168.178.49 [16/May/2026 01:59:47] "GET /run/default-color HTTP/1.1" 404 -
|
||||
192.168.178.49 [16/May/2026 02:00:17] "GET /run/default-color HTTP/1.1" 404 -
|
||||
192.168.178.49 [16/May/2026 02:00:47] "GET /run/default-color HTTP/1.1" 404 -
|
||||
192.168.178.49 [16/May/2026 02:01:17] "GET /run/default-color HTTP/1.1" 404 -
|
||||
192.168.178.49 [16/May/2026 02:01:47] "GET /run/default-color HTTP/1.1" 404 -
|
||||
192.168.178.49 [16/May/2026 02:02:17] "GET /run/default-color HTTP/1.1" 404 -
|
||||
192.168.178.49 [16/May/2026 02:02:47] "GET /run/default-color HTTP/1.1" 404 -
|
||||
192.168.178.49 [16/May/2026 02:03:17] "GET /run/default-color HTTP/1.1" 404 -
|
||||
192.168.178.49 [16/May/2026 02:03:47] "GET /run/default-color HTTP/1.1" 404 -
|
||||
192.168.178.49 [16/May/2026 02:04:17] "GET /run/default-color HTTP/1.1" 404 -
|
||||
192.168.178.49 [16/May/2026 02:04:47] "GET /run/default-color HTTP/1.1" 404 -
|
||||
192.168.178.49 [16/May/2026 02:05:17] "GET /run/default-color HTTP/1.1" 404 -
|
||||
192.168.178.49 [16/May/2026 02:05:47] "GET /run/default-color HTTP/1.1" 404 -
|
||||
192.168.178.49 [16/May/2026 02:06:17] "GET /run/default-color HTTP/1.1" 404 -
|
||||
192.168.178.49 [16/May/2026 02:06:47] "GET /run/default-color HTTP/1.1" 404 -
|
||||
192.168.178.49 [16/May/2026 02:07:17] "GET /run/default-color HTTP/1.1" 404 -
|
||||
192.168.178.49 [16/May/2026 02:07:47] "GET /run/default-color HTTP/1.1" 404 -
|
||||
192.168.178.49 [16/May/2026 02:08:17] "GET /run/default-color HTTP/1.1" 404 -
|
||||
192.168.178.49 [16/May/2026 02:08:47] "GET /run/default-color HTTP/1.1" 404 -
|
||||
192.168.178.49 [16/May/2026 02:09:17] "GET /run/default-color HTTP/1.1" 404 -
|
||||
192.168.178.49 [16/May/2026 02:09:47] "GET /run/default-color HTTP/1.1" 404 -
|
||||
192.168.178.49 [16/May/2026 02:09:59] "POST /run/default-color HTTP/1.1" 403 -
|
||||
192.168.178.49 [16/May/2026 02:10:07] "POST /run/default-color HTTP/1.1" 403 -
|
||||
192.168.178.49 [16/May/2026 02:10:37] "POST /run/default-color HTTP/1.1" 403 -
|
||||
192.168.178.49 [16/May/2026 02:11:07] "POST /run/default-color HTTP/1.1" 403 -
|
||||
192.168.178.49 [16/May/2026 02:11:37] "POST /run/default-color HTTP/1.1" 403 -
|
||||
192.168.178.49 [16/May/2026 02:12:07] "POST /run/default-color HTTP/1.1" 403 -
|
||||
192.168.178.49 [16/May/2026 02:12:38] "POST /run/default-color HTTP/1.1" 403 -
|
||||
192.168.178.49 [16/May/2026 02:12:59] "POST /run/default-color HTTP/1.1" 403 -
|
||||
192.168.178.49 [16/May/2026 02:13:29] "POST /run/default-color HTTP/1.1" 403 -
|
||||
192.168.178.49 [16/May/2026 02:13:59] "POST /run/default-color HTTP/1.1" 403 -
|
||||
192.168.178.49 [16/May/2026 02:14:24] "POST /run/default-color HTTP/1.1" 403 -
|
||||
192.168.178.49 [16/May/2026 02:14:54] "POST /run/default-color HTTP/1.1" 403 -
|
||||
192.168.178.49 [16/May/2026 02:15:24] "POST /run/default-color HTTP/1.1" 403 -
|
||||
192.168.178.49 [16/May/2026 02:15:54] "POST /run/default-color HTTP/1.1" 403 -
|
||||
192.168.178.49 [16/May/2026 02:16:00] "POST /run/default-color HTTP/1.1" 403 -
|
||||
192.168.178.49 [16/May/2026 02:16:04] "POST /run/default-color HTTP/1.1" 403 -
|
||||
192.168.178.49 [16/May/2026 02:16:34] "POST /run/default-color HTTP/1.1" 403 -
|
||||
192.168.178.49 [16/May/2026 02:17:04] "POST /run/default-color HTTP/1.1" 403 -
|
||||
192.168.178.49 [16/May/2026 02:17:34] "POST /run/default-color HTTP/1.1" 403 -
|
||||
192.168.178.49 [16/May/2026 02:18:04] "POST /run/default-color HTTP/1.1" 403 -
|
||||
192.168.178.49 [16/May/2026 02:18:34] "POST /run/default-color HTTP/1.1" 403 -
|
||||
request_id=08bc90cc-8f80-4670-91fc-34d665e5c815 command_id=default-color command=['ir-ctl', '--send', '/home/timm/IRSender/defaultColor.txt']
|
||||
192.168.178.49 [16/May/2026 02:18:59] "POST /run/default-color HTTP/1.1" 200 -
|
||||
request_id=0779ebeb-ea40-485b-91bf-6dd73b3d8987 command_id=default-color command=['ir-ctl', '--send', '/home/timm/IRSender/defaultColor.txt']
|
||||
192.168.178.49 [16/May/2026 02:19:29] "POST /run/default-color HTTP/1.1" 200 -
|
||||
request_id=7de7058d-9150-4fd8-b101-46dbbe00eaab command_id=default-color command=['ir-ctl', '--send', '/home/timm/IRSender/defaultColor.txt']
|
||||
192.168.178.49 [16/May/2026 02:19:56] "POST /run/default-color HTTP/1.1" 200 -
|
||||
request_id=82cdc6b3-4a66-4eca-b616-558af970abbe command_id=default-color command=['ir-ctl', '--send', '/home/timm/IRSender/defaultColor.txt']
|
||||
192.168.178.49 [16/May/2026 02:20:26] "POST /run/default-color HTTP/1.1" 200 -
|
||||
request_id=c12ad6b0-b449-41ca-bdd6-673fad54162b command_id=default-color command=['ir-ctl', '--send', '/home/timm/IRSender/defaultColor.txt']
|
||||
192.168.178.49 [16/May/2026 02:20:56] "POST /run/default-color HTTP/1.1" 200 -
|
||||
request_id=96c834f2-c3c9-4ffc-97df-9e1f2d9acc49 command_id=default-color command=['ir-ctl', '--send', '/home/timm/IRSender/defaultColor.txt']
|
||||
192.168.178.49 [16/May/2026 02:21:26] "POST /run/default-color HTTP/1.1" 200 -
|
||||
request_id=35ae01f4-2402-4e6e-9620-db0503395af5 command_id=default-color command=['ir-ctl', '--send', '/home/timm/IRSender/defaultColor.txt']
|
||||
192.168.178.49 [16/May/2026 02:21:56] "POST /run/default-color HTTP/1.1" 200 -
|
||||
request_id=60fa399b-792e-4710-bc5e-5fba714a87d7 command_id=default-color command=['ir-ctl', '--send', '/home/timm/IRSender/defaultColor.txt']
|
||||
192.168.178.49 [16/May/2026 02:22:26] "POST /run/default-color HTTP/1.1" 200 -
|
||||
request_id=be4616cd-9033-4f83-934d-928a62352f5f command_id=default-color command=['ir-ctl', '--send', '/home/timm/IRSender/defaultColor.txt']
|
||||
192.168.178.49 [16/May/2026 02:22:56] "POST /run/default-color HTTP/1.1" 200 -
|
||||
request_id=c4667876-9088-4073-a1b1-aa512800e658 command_id=default-color command=['ir-ctl', '--send', '/home/timm/IRSender/defaultColor.txt']
|
||||
192.168.178.49 [16/May/2026 02:23:26] "POST /run/default-color HTTP/1.1" 200 -
|
||||
request_id=e92e3464-6d07-49d8-a575-413a9227569b command_id=default-color command=['ir-ctl', '--send', '/home/timm/IRSender/defaultColor.txt']
|
||||
192.168.178.49 [16/May/2026 02:23:42] "POST /run/aus HTTP/1.1" 404 -
|
||||
192.168.178.49 [16/May/2026 02:23:42] "POST /run/default-color HTTP/1.1" 200 -
|
||||
request_id=13611a6f-57d6-4e0f-832b-93feed7e1789 command_id=default-color command=['ir-ctl', '--send', '/home/timm/IRSender/defaultColor.txt']
|
||||
192.168.178.49 [16/May/2026 02:24:12] "POST /run/aus HTTP/1.1" 404 -
|
||||
192.168.178.49 [16/May/2026 02:24:12] "POST /run/default-color HTTP/1.1" 200 -
|
||||
request_id=39e49f9e-e1ed-4c3e-8e3d-a83b2653ea73 command_id=default-color command=['ir-ctl', '--send', '/home/timm/IRSender/defaultColor.txt']
|
||||
192.168.178.49 [16/May/2026 02:24:35] "POST /run/aus HTTP/1.1" 404 -
|
||||
192.168.178.49 [16/May/2026 02:24:35] "POST /run/default-color HTTP/1.1" 200 -
|
||||
192.168.178.49 [16/May/2026 02:25:05] "POST /run/aus HTTP/1.1" 404 -
|
||||
request_id=a3f3deae-7d43-4cea-8e09-a35ff27a5f93 command_id=default-color command=['ir-ctl', '--send', '/home/timm/IRSender/defaultColor.txt']
|
||||
192.168.178.49 [16/May/2026 02:25:05] "POST /run/default-color HTTP/1.1" 200 -
|
||||
request_id=153f9708-d23f-4ecb-ae6a-966cc854a3bb command_id=default-color command=['ir-ctl', '--send', '/home/timm/IRSender/defaultColor.txt']
|
||||
192.168.178.49 [16/May/2026 02:25:35] "POST /run/aus HTTP/1.1" 404 -
|
||||
192.168.178.49 [16/May/2026 02:25:35] "POST /run/default-color HTTP/1.1" 200 -
|
||||
request_id=8416c145-cd05-43cb-8f37-f1dc58b83cc5 command_id=default-color command=['ir-ctl', '--send', '/home/timm/IRSender/defaultColor.txt']
|
||||
192.168.178.49 [16/May/2026 02:26:05] "POST /run/aus HTTP/1.1" 404 -
|
||||
192.168.178.49 [16/May/2026 02:26:05] "POST /run/default-color HTTP/1.1" 200 -
|
||||
request_id=8b5db5b6-fa6b-4678-9057-8c2f9627cd43 command_id=default-color command=['ir-ctl', '--send', '/home/timm/IRSender/defaultColor.txt']
|
||||
192.168.178.49 [16/May/2026 02:26:35] "POST /run/aus HTTP/1.1" 404 -
|
||||
192.168.178.49 [16/May/2026 02:26:35] "POST /run/default-color HTTP/1.1" 200 -
|
||||
192.168.178.49 [16/May/2026 02:27:05] "POST /run/aus HTTP/1.1" 404 -
|
||||
request_id=8dcf8526-654a-436d-94b4-398d5d743c43 command_id=default-color command=['ir-ctl', '--send', '/home/timm/IRSender/defaultColor.txt']
|
||||
192.168.178.49 [16/May/2026 02:27:05] "POST /run/default-color HTTP/1.1" 200 -
|
||||
request_id=02b217d3-efb4-4c27-8617-ae0b3f1bf63a command_id=default-color command=['ir-ctl', '--send', '/home/timm/IRSender/defaultColor.txt']
|
||||
192.168.178.49 [16/May/2026 02:27:35] "POST /run/aus HTTP/1.1" 404 -
|
||||
192.168.178.49 [16/May/2026 02:27:35] "POST /run/default-color HTTP/1.1" 200 -
|
||||
192.168.178.49 [16/May/2026 02:28:05] "POST /run/aus HTTP/1.1" 404 -
|
||||
request_id=58181ed3-c4c0-4313-ba82-c9bb81c8a382 command_id=default-color command=['ir-ctl', '--send', '/home/timm/IRSender/defaultColor.txt']
|
||||
192.168.178.49 [16/May/2026 02:28:05] "POST /run/default-color HTTP/1.1" 200 -
|
||||
192.168.178.49 [16/May/2026 02:28:35] "POST /run/aus HTTP/1.1" 404 -
|
||||
request_id=8faa0c58-d86c-4ee1-a94d-725810650d0a command_id=default-color command=['ir-ctl', '--send', '/home/timm/IRSender/defaultColor.txt']
|
||||
192.168.178.49 [16/May/2026 02:28:35] "POST /run/default-color HTTP/1.1" 200 -
|
||||
request_id=aaa86e58-08c9-4fe3-a9d6-6be11bdbea9b command_id=default-color command=['ir-ctl', '--send', '/home/timm/IRSender/defaultColor.txt']
|
||||
192.168.178.49 [16/May/2026 02:29:05] "POST /run/aus HTTP/1.1" 404 -
|
||||
192.168.178.49 [16/May/2026 02:29:05] "POST /run/default-color HTTP/1.1" 200 -
|
||||
192.168.178.49 [16/May/2026 02:29:35] "POST /run/aus HTTP/1.1" 404 -
|
||||
request_id=cbb264f0-873b-4643-8eb9-c2951aa2dabf command_id=default-color command=['ir-ctl', '--send', '/home/timm/IRSender/defaultColor.txt']
|
||||
192.168.178.49 [16/May/2026 02:29:35] "POST /run/default-color HTTP/1.1" 200 -
|
||||
192.168.178.49 [16/May/2026 02:30:05] "POST /run/aus HTTP/1.1" 404 -
|
||||
request_id=e54719c6-a2f6-4f04-ab26-7bd118608871 command_id=default-color command=['ir-ctl', '--send', '/home/timm/IRSender/defaultColor.txt']
|
||||
192.168.178.49 [16/May/2026 02:30:05] "POST /run/default-color HTTP/1.1" 200 -
|
||||
192.168.178.49 [16/May/2026 02:30:26] "POST /run/default-color HTTP/1.1" 400 -
|
||||
192.168.178.49 [16/May/2026 02:30:28] "POST /run/default-color HTTP/1.1" 400 -
|
||||
request_id=ce11713a-fc90-4a8a-9926-809eaf3cb3a1 command_id=default-color command=['ir-ctl', '--send', '/home/timm/IRSender/defaultColor.txt']
|
||||
192.168.178.49 [16/May/2026 02:30:35] "POST /run/aus HTTP/1.1" 404 -
|
||||
192.168.178.49 [16/May/2026 02:30:35] "POST /run/default-color HTTP/1.1" 200 -
|
||||
192.168.178.49 [16/May/2026 02:31:05] "POST /run/aus HTTP/1.1" 404 -
|
||||
request_id=5e6843be-7f8f-4753-b22e-6c3df88d92fa command_id=default-color command=['ir-ctl', '--send', '/home/timm/IRSender/defaultColor.txt']
|
||||
192.168.178.49 [16/May/2026 02:31:05] "POST /run/default-color HTTP/1.1" 200 -
|
||||
192.168.178.49 [16/May/2026 02:31:10] "POST /run/aus HTTP/1.1" 404 -
|
||||
192.168.178.49 [16/May/2026 02:32:47] "POST /run/aus HTTP/1.1" 404 -
|
||||
192.168.178.49 [16/May/2026 02:36:07] "POST /run/off HTTP/1.1" 400 -
|
||||
192.168.178.49 [16/May/2026 02:36:20] "POST /run/on HTTP/1.1" 400 -
|
||||
192.168.178.49 [16/May/2026 02:36:42] "POST /run/default-color HTTP/1.1" 400 -
|
||||
192.168.178.49 [16/May/2026 02:40:07] "POST /run/default-color HTTP/1.1" 400 -
|
||||
request_id=0120b342-b821-431b-8c2f-e61c9ccd1779 command_id=default-color command=['ir-ctl', '--send', '/home/timm/IRSender/defaultColor.txt']
|
||||
192.168.178.49 [16/May/2026 02:40:38] "POST /run/default-color HTTP/1.1" 200 -
|
||||
request_id=f0e7ca6c-7a71-4cea-9b28-1e74b503baa8 command_id=off command=['ir-ctl', '--send', '/home/timm/IRSender/off.txt']
|
||||
192.168.178.49 [16/May/2026 02:41:22] "POST /run/off HTTP/1.1" 200 -
|
||||
request_id=57597df5-e9dd-4276-b277-bc51fa13e32f command_id=on command=['ir-ctl', '--send', '/home/timm/IRSender/on.txt']
|
||||
192.168.178.49 [16/May/2026 02:41:23] "POST /run/on HTTP/1.1" 200 -
|
||||
request_id=f9bf9d3b-7bc2-4995-b287-703d02ae78bf command_id=default-color command=['ir-ctl', '--send', '/home/timm/IRSender/defaultColor.txt']
|
||||
192.168.178.49 [16/May/2026 02:41:24] "POST /run/default-color HTTP/1.1" 200 -
|
||||
request_id=86f7a4f2-0f4f-4fc1-8da2-d7eb9f75beff command_id=off command=['ir-ctl', '--send', '/home/timm/IRSender/off.txt']
|
||||
192.168.178.49 [16/May/2026 02:41:38] "POST /run/off HTTP/1.1" 200 -
|
||||
request_id=3a1b50a2-634a-4382-a10b-e1731fcaff81 command_id=on command=['ir-ctl', '--send', '/home/timm/IRSender/on.txt']
|
||||
192.168.178.49 [16/May/2026 02:42:04] "POST /run/on HTTP/1.1" 200 -
|
||||
request_id=15d455b7-5b8e-441e-a65b-96f0e97ec31a command_id=on command=['ir-ctl', '--send', '/home/timm/IRSender/on.txt']
|
||||
192.168.178.49 [16/May/2026 02:42:04] "POST /run/on HTTP/1.1" 200 -
|
||||
request_id=ad0500f8-f2b9-43a7-92e6-459c73240b44 command_id=default-color command=['ir-ctl', '--send', '/home/timm/IRSender/defaultColor.txt']
|
||||
192.168.178.49 [16/May/2026 02:42:14] "POST /run/default-color HTTP/1.1" 200 -
|
||||
request_id=b6bf3486-902a-4fe8-94f2-3b13ef421770 command_id=default-color command=['ir-ctl', '--send', '/home/timm/IRSender/defaultColor.txt']
|
||||
192.168.178.49 [16/May/2026 02:42:14] "POST /run/default-color HTTP/1.1" 200 -
|
||||
Listening on http://0.0.0.0:8080
|
||||
Config: commands.json
|
||||
192.168.178.49 [16/May/2026 08:21:51] "GET / HTTP/1.1" 200 -
|
||||
192.168.178.49 [16/May/2026 08:21:51] "GET /favicon.ico HTTP/1.1" 404 -
|
||||
192.168.178.49 [16/May/2026 08:21:53] "GET / HTTP/1.1" 200 -
|
||||
request_id=ac6710b9-e33b-4df7-8ff4-f94b4f98b918 command_id=brighter command=['ir-ctl', '--send', '/home/timm/IRSender/brighter.txt']
|
||||
192.168.178.49 [16/May/2026 08:21:55] "POST /web/run/brighter HTTP/1.1" 200 -
|
||||
request_id=de27675f-156c-44af-b246-be3eeecb0e3c command_id=darker command=['ir-ctl', '--send', '/home/timm/IRSender/darker.txt']
|
||||
192.168.178.49 [16/May/2026 08:21:59] "POST /web/run/darker HTTP/1.1" 200 -
|
||||
request_id=28987032-93b2-4635-a5c9-37793334fbdc command_id=darker command=['ir-ctl', '--send', '/home/timm/IRSender/darker.txt']
|
||||
192.168.178.49 [16/May/2026 08:22:01] "POST /web/run/darker HTTP/1.1" 200 -
|
||||
request_id=39fbf168-85d4-4b9e-8101-a63279eb9346 command_id=darker command=['ir-ctl', '--send', '/home/timm/IRSender/darker.txt']
|
||||
192.168.178.49 [16/May/2026 08:22:02] "POST /web/run/darker HTTP/1.1" 200 -
|
||||
request_id=ad03bc03-fb6e-4498-b6e9-de220201eace command_id=fade command=['ir-ctl', '--send', '/home/timm/IRSender/fade.txt']
|
||||
192.168.178.49 [16/May/2026 08:22:06] "POST /web/run/fade HTTP/1.1" 200 -
|
||||
request_id=9d254a73-f211-4d00-bae4-2380151b2170 command_id=green command=['ir-ctl', '--send', '/home/timm/IRSender/green.txt']
|
||||
192.168.178.49 [16/May/2026 08:22:09] "POST /web/run/green HTTP/1.1" 200 -
|
||||
request_id=4624c540-7083-470f-9045-3bf8f4a36023 command_id=off command=['ir-ctl', '--send', '/home/timm/IRSender/off.txt']
|
||||
192.168.178.49 [16/May/2026 08:22:15] "POST /web/run/off HTTP/1.1" 200 -
|
||||
request_id=ebdf4c20-645b-4e14-9424-c763c848144a command_id=on command=['ir-ctl', '--send', '/home/timm/IRSender/on.txt']
|
||||
192.168.178.49 [16/May/2026 08:22:18] "POST /web/run/on HTTP/1.1" 200 -
|
||||
request_id=91d5b868-8ac8-44c7-aeab-e3a4f53cfec7 command_id=red command=['ir-ctl', '--send', '/home/timm/IRSender/red.txt']
|
||||
192.168.178.49 [16/May/2026 08:22:22] "POST /web/run/red HTTP/1.1" 200 -
|
||||
request_id=a47da5e7-6ba1-4edc-8234-1693c0cc8291 command_id=purple command=['ir-ctl', '--send', '/home/timm/IRSender/purple.txt']
|
||||
192.168.178.49 [16/May/2026 08:22:24] "POST /web/run/purple HTTP/1.1" 200 -
|
||||
request_id=a8af7d54-0341-453f-94c6-5528d006392b command_id=smooth command=['ir-ctl', '--send', '/home/timm/IRSender/smooth.txt']
|
||||
192.168.178.49 [16/May/2026 08:22:27] "POST /web/run/smooth HTTP/1.1" 200 -
|
||||
request_id=f75cf627-135b-4d2c-9728-040707de6d87 command_id=white command=['ir-ctl', '--send', '/home/timm/IRSender/white.txt']
|
||||
192.168.178.49 [16/May/2026 08:22:32] "POST /web/run/white HTTP/1.1" 200 -
|
||||
request_id=24de491a-786d-4a75-840a-7826310055e6 command_id=yellow command=['ir-ctl', '--send', '/home/timm/IRSender/yellow.txt']
|
||||
192.168.178.49 [16/May/2026 08:22:35] "POST /web/run/yellow HTTP/1.1" 200 -
|
||||
request_id=45256d4b-c898-4b6e-a34d-6920fa35d6b5 command_id=white command=['ir-ctl', '--send', '/home/timm/IRSender/white.txt']
|
||||
192.168.178.49 [16/May/2026 08:22:39] "POST /web/run/white HTTP/1.1" 200 -
|
||||
Listening on http://0.0.0.0:8080
|
||||
Config: commands.json
|
||||
192.168.178.149 [16/May/2026 09:16:43] "GET /apple-touch-icon-precomposed.png HTTP/1.1" 404 -
|
||||
192.168.178.149 [16/May/2026 09:16:43] "GET /apple-touch-icon.png HTTP/1.1" 404 -
|
||||
192.168.178.149 [16/May/2026 09:16:43] "GET /favicon.ico HTTP/1.1" 404 -
|
||||
192.168.178.149 [16/May/2026 09:16:43] "GET / HTTP/1.1" 200 -
|
||||
192.168.178.149 [16/May/2026 09:16:47] "GET / HTTP/1.1" 200 -
|
||||
192.168.178.149 [16/May/2026 09:16:50] "GET /favicon.ico HTTP/1.1" 404 -
|
||||
192.168.178.149 [16/May/2026 09:17:40] "GET / HTTP/1.1" 200 -
|
||||
192.168.178.149 [16/May/2026 09:17:42] "GET / HTTP/1.1" 200 -
|
||||
----------------------------------------
|
||||
Exception occurred during processing of request from ('192.168.178.149', 61332)
|
||||
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
|
||||
----------------------------------------
|
||||
192.168.178.49 [16/May/2026 09:22:40] "GET / HTTP/1.1" 200 -
|
||||
192.168.178.49 [16/May/2026 09:22:41] "GET /favicon.ico HTTP/1.1" 404 -
|
||||
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
|
||||
+168
@@ -0,0 +1,168 @@
|
||||
{
|
||||
"host": "0.0.0.0",
|
||||
"port": 8080,
|
||||
"max_body_bytes": 65536,
|
||||
|
||||
"commands": {
|
||||
"blue": {
|
||||
"description": "Send IR command blue.txt",
|
||||
"web_title": "Blue",
|
||||
"web_enabled": true,
|
||||
"api_key": "change-this-global-api-key",
|
||||
"timeout_seconds": 10,
|
||||
"command": ["ir-ctl", "--send", "/home/timm/IRSender/blue.txt"],
|
||||
"args": {}
|
||||
},
|
||||
|
||||
"brighter": {
|
||||
"description": "Send IR command brighter.txt",
|
||||
"web_title": "Brighter",
|
||||
"web_enabled": true,
|
||||
"api_key": "change-this-global-api-key",
|
||||
"timeout_seconds": 10,
|
||||
"command": ["ir-ctl", "--send", "/home/timm/IRSender/brighter.txt"],
|
||||
"args": {}
|
||||
},
|
||||
|
||||
"darker": {
|
||||
"description": "Send IR command darker.txt",
|
||||
"web_title": "Darker",
|
||||
"web_enabled": true,
|
||||
"api_key": "change-this-global-api-key",
|
||||
"timeout_seconds": 10,
|
||||
"command": ["ir-ctl", "--send", "/home/timm/IRSender/darker.txt"],
|
||||
"args": {}
|
||||
},
|
||||
|
||||
"default-color": {
|
||||
"description": "Send IR command defaultColor.txt",
|
||||
"web_title": "Default Color",
|
||||
"web_enabled": true,
|
||||
"api_key": "change-this-global-api-key",
|
||||
"timeout_seconds": 10,
|
||||
"command": ["ir-ctl", "--send", "/home/timm/IRSender/defaultColor.txt"],
|
||||
"args": {}
|
||||
},
|
||||
|
||||
"fade": {
|
||||
"description": "Send IR command fade.txt",
|
||||
"web_title": "Fade",
|
||||
"web_enabled": true,
|
||||
"api_key": "change-this-global-api-key",
|
||||
"timeout_seconds": 10,
|
||||
"command": ["ir-ctl", "--send", "/home/timm/IRSender/fade.txt"],
|
||||
"args": {}
|
||||
},
|
||||
|
||||
"green": {
|
||||
"description": "Send IR command green.txt",
|
||||
"web_title": "Green",
|
||||
"web_enabled": true,
|
||||
"api_key": "change-this-global-api-key",
|
||||
"timeout_seconds": 10,
|
||||
"command": ["ir-ctl", "--send", "/home/timm/IRSender/green.txt"],
|
||||
"args": {}
|
||||
},
|
||||
|
||||
"licht-aus": {
|
||||
"description": "Send IR command LichtAus.txt",
|
||||
"web_title": "Licht Aus",
|
||||
"web_enabled": true,
|
||||
"api_key": "change-this-global-api-key",
|
||||
"timeout_seconds": 10,
|
||||
"command": ["ir-ctl", "--send", "/home/timm/IRSender/LichtAus.txt"],
|
||||
"args": {}
|
||||
},
|
||||
|
||||
"licht-ein": {
|
||||
"description": "Send IR command LichtEin.txt",
|
||||
"web_title": "Licht Ein",
|
||||
"web_enabled": true,
|
||||
"api_key": "change-this-global-api-key",
|
||||
"timeout_seconds": 10,
|
||||
"command": ["ir-ctl", "--send", "/home/timm/IRSender/LichtEin.txt"],
|
||||
"args": {}
|
||||
},
|
||||
|
||||
"nachtlicht": {
|
||||
"description": "Send IR command Nachtlicht.txt",
|
||||
"web_title": "Nachtlicht",
|
||||
"web_enabled": true,
|
||||
"api_key": "change-this-global-api-key",
|
||||
"timeout_seconds": 10,
|
||||
"command": ["ir-ctl", "--send", "/home/timm/IRSender/Nachtlicht.txt"],
|
||||
"args": {}
|
||||
},
|
||||
|
||||
"off": {
|
||||
"description": "Send IR command off.txt",
|
||||
"web_title": "Off",
|
||||
"web_enabled": true,
|
||||
"api_key": "change-this-global-api-key",
|
||||
"timeout_seconds": 10,
|
||||
"command": ["ir-ctl", "--send", "/home/timm/IRSender/off.txt"],
|
||||
"args": {}
|
||||
},
|
||||
|
||||
"on": {
|
||||
"description": "Send IR command on.txt",
|
||||
"web_title": "On",
|
||||
"web_enabled": true,
|
||||
"api_key": "change-this-global-api-key",
|
||||
"timeout_seconds": 10,
|
||||
"command": ["ir-ctl", "--send", "/home/timm/IRSender/on.txt"],
|
||||
"args": {}
|
||||
},
|
||||
|
||||
"purple": {
|
||||
"description": "Send IR command purple.txt",
|
||||
"web_title": "Purple",
|
||||
"web_enabled": true,
|
||||
"api_key": "change-this-global-api-key",
|
||||
"timeout_seconds": 10,
|
||||
"command": ["ir-ctl", "--send", "/home/timm/IRSender/purple.txt"],
|
||||
"args": {}
|
||||
},
|
||||
|
||||
"red": {
|
||||
"description": "Send IR command red.txt",
|
||||
"web_title": "Red",
|
||||
"web_enabled": true,
|
||||
"api_key": "change-this-global-api-key",
|
||||
"timeout_seconds": 10,
|
||||
"command": ["ir-ctl", "--send", "/home/timm/IRSender/red.txt"],
|
||||
"args": {}
|
||||
},
|
||||
|
||||
"smooth": {
|
||||
"description": "Send IR command smooth.txt",
|
||||
"web_title": "Smooth",
|
||||
"web_enabled": true,
|
||||
"api_key": "change-this-global-api-key",
|
||||
"timeout_seconds": 10,
|
||||
"command": ["ir-ctl", "--send", "/home/timm/IRSender/smooth.txt"],
|
||||
"args": {}
|
||||
},
|
||||
|
||||
"white": {
|
||||
"description": "Send IR command white.txt",
|
||||
"web_title": "White",
|
||||
"web_enabled": true,
|
||||
"api_key": "change-this-global-api-key",
|
||||
"timeout_seconds": 10,
|
||||
"command": ["ir-ctl", "--send", "/home/timm/IRSender/white.txt"],
|
||||
"args": {}
|
||||
},
|
||||
|
||||
"yellow": {
|
||||
"description": "Send IR command yellow.txt",
|
||||
"web_title": "Yellow",
|
||||
"web_enabled": true,
|
||||
"api_key": "change-this-global-api-key",
|
||||
"timeout_seconds": 10,
|
||||
"command": ["ir-ctl", "--send", "/home/timm/IRSender/yellow.txt"],
|
||||
"args": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
{
|
||||
"carrier": 38000,
|
||||
"repeat": 1,
|
||||
"signal": [
|
||||
{ "type": "mark", "duration": 8977 },
|
||||
{ "type": "space", "duration": 4485 },
|
||||
|
||||
{ "type": "mark", "duration": 586 },
|
||||
{ "type": "space", "duration": 1690 },
|
||||
|
||||
{ "type": "mark", "duration": 558 },
|
||||
{ "type": "space", "duration": 573 },
|
||||
|
||||
{ "type": "mark", "duration": 559 },
|
||||
{ "type": "space", "duration": 548 },
|
||||
|
||||
{ "type": "mark", "duration": 582 },
|
||||
{ "type": "space", "duration": 569 },
|
||||
|
||||
{ "type": "mark", "duration": 560 },
|
||||
{ "type": "space", "duration": 545 },
|
||||
|
||||
{ "type": "mark", "duration": 584 },
|
||||
{ "type": "space", "duration": 568 },
|
||||
|
||||
{ "type": "mark", "duration": 558 },
|
||||
{ "type": "space", "duration": 573 },
|
||||
|
||||
{ "type": "mark", "duration": 587 },
|
||||
{ "type": "space", "duration": 521 },
|
||||
|
||||
{ "type": "mark", "duration": 577 },
|
||||
{ "type": "space", "duration": 549 },
|
||||
|
||||
{ "type": "mark", "duration": 606 },
|
||||
{ "type": "space", "duration": 1669 },
|
||||
|
||||
{ "type": "mark", "duration": 564 },
|
||||
{ "type": "space", "duration": 1689 },
|
||||
|
||||
{ "type": "mark", "duration": 587 },
|
||||
{ "type": "space", "duration": 1664 },
|
||||
|
||||
{ "type": "mark", "duration": 582 },
|
||||
{ "type": "space", "duration": 1676 },
|
||||
|
||||
{ "type": "mark", "duration": 551 },
|
||||
{ "type": "space", "duration": 1694 },
|
||||
|
||||
{ "type": "mark", "duration": 560 },
|
||||
{ "type": "space", "duration": 1691 },
|
||||
|
||||
{ "type": "mark", "duration": 558 },
|
||||
{ "type": "space", "duration": 1694 },
|
||||
|
||||
{ "type": "mark", "duration": 558 },
|
||||
{ "type": "space", "duration": 570 },
|
||||
|
||||
{ "type": "mark", "duration": 580 },
|
||||
{ "type": "space", "duration": 549 },
|
||||
|
||||
{ "type": "mark", "duration": 564 },
|
||||
{ "type": "space", "duration": 543 },
|
||||
|
||||
{ "type": "mark", "duration": 580 },
|
||||
{ "type": "space", "duration": 550 },
|
||||
|
||||
{ "type": "mark", "duration": 577 },
|
||||
{ "type": "space", "duration": 552 },
|
||||
|
||||
{ "type": "mark", "duration": 577 },
|
||||
{ "type": "space", "duration": 549 },
|
||||
|
||||
{ "type": "mark", "duration": 604 },
|
||||
{ "type": "space", "duration": 548 },
|
||||
|
||||
{ "type": "mark", "duration": 558 },
|
||||
{ "type": "space", "duration": 546 },
|
||||
|
||||
{ "type": "mark", "duration": 584 },
|
||||
{ "type": "space", "duration": 1692 },
|
||||
|
||||
{ "type": "mark", "duration": 558 },
|
||||
{ "type": "space", "duration": 1692 },
|
||||
|
||||
{ "type": "mark", "duration": 558 },
|
||||
{ "type": "space", "duration": 1693 },
|
||||
|
||||
{ "type": "mark", "duration": 558 },
|
||||
{ "type": "space", "duration": 1693 },
|
||||
|
||||
{ "type": "mark", "duration": 558 },
|
||||
{ "type": "space", "duration": 1696 },
|
||||
|
||||
{ "type": "mark", "duration": 555 },
|
||||
{ "type": "space", "duration": 1697 },
|
||||
|
||||
{ "type": "mark", "duration": 554 },
|
||||
{ "type": "space", "duration": 1694 },
|
||||
|
||||
{ "type": "mark", "duration": 583 },
|
||||
{ "type": "space", "duration": 1668 },
|
||||
|
||||
{ "type": "mark", "duration": 581 },
|
||||
|
||||
{ "type": "space", "duration": 23166 }
|
||||
]
|
||||
}
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
|
||||
import pigpio
|
||||
|
||||
DEFAULT_GPIO = 12
|
||||
|
||||
|
||||
def load_config(path: str) -> dict:
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
|
||||
if "signal" not in data or not isinstance(data["signal"], list):
|
||||
raise ValueError("JSON braucht ein 'signal'-Array.")
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def build_wave(pi: pigpio.pi, gpio: int, carrier_hz: int, signal: list[dict]) -> int:
|
||||
period_us = max(2, round(1_000_000 / carrier_hz))
|
||||
on_us = max(1, period_us // 2)
|
||||
off_us = max(1, period_us - on_us)
|
||||
|
||||
gpio_mask = 1 << gpio
|
||||
pulses = []
|
||||
|
||||
for item in signal:
|
||||
kind = str(item.get("type", "")).lower()
|
||||
duration = int(item.get("duration", 0))
|
||||
|
||||
if duration <= 0:
|
||||
continue
|
||||
|
||||
if kind == "space":
|
||||
pulses.append(pigpio.pulse(0, 0, duration))
|
||||
|
||||
elif kind == "mark":
|
||||
full_cycles = duration // period_us
|
||||
remainder = duration % period_us
|
||||
|
||||
for _ in range(full_cycles):
|
||||
pulses.append(pigpio.pulse(gpio_mask, 0, on_us))
|
||||
pulses.append(pigpio.pulse(0, gpio_mask, off_us))
|
||||
|
||||
if remainder > 0:
|
||||
pulses.append(pigpio.pulse(gpio_mask, 0, remainder))
|
||||
|
||||
else:
|
||||
raise ValueError(f"Unbekannter type: {kind} (erlaubt: mark, space)")
|
||||
|
||||
pi.wave_clear()
|
||||
pi.wave_add_generic(pulses)
|
||||
wid = pi.wave_create()
|
||||
|
||||
return wid
|
||||
|
||||
|
||||
def send_wave(pi: pigpio.pi, wid: int, repeat: int) -> None:
|
||||
for i in range(repeat):
|
||||
pi.wave_send_once(wid)
|
||||
while pi.wave_tx_busy():
|
||||
time.sleep(0.001)
|
||||
|
||||
if i != repeat - 1:
|
||||
time.sleep(0.03)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if len(sys.argv) != 2:
|
||||
print("Usage: python3 ir_send.py signal.json")
|
||||
return 1
|
||||
|
||||
cfg = load_config(sys.argv[1])
|
||||
|
||||
gpio = int(cfg.get("gpio", DEFAULT_GPIO))
|
||||
carrier = int(cfg.get("carrier", 38000))
|
||||
repeat = int(cfg.get("repeat", 1))
|
||||
|
||||
if repeat < 1:
|
||||
repeat = 1
|
||||
|
||||
pi = pigpio.pi()
|
||||
if not pi.connected:
|
||||
print("Fehler: pigpiod läuft nicht.")
|
||||
return 1
|
||||
|
||||
wid = -1
|
||||
try:
|
||||
pi.set_mode(gpio, pigpio.OUTPUT)
|
||||
pi.write(gpio, 0)
|
||||
|
||||
wid = build_wave(pi, gpio, carrier, cfg["signal"])
|
||||
if wid < 0:
|
||||
print("Fehler: wave_create fehlgeschlagen.")
|
||||
return 1
|
||||
|
||||
send_wave(pi, wid, repeat)
|
||||
print("IR-Signal gesendet.")
|
||||
return 0
|
||||
|
||||
finally:
|
||||
try:
|
||||
if wid >= 0:
|
||||
pi.wave_tx_stop()
|
||||
pi.wave_delete(wid)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
pi.write(gpio, 0)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
pi.wave_clear()
|
||||
pi.stop()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,20 @@
|
||||
List of all Commands:
|
||||
add .txt
|
||||
|
||||
- red
|
||||
- green
|
||||
- blue
|
||||
- white
|
||||
- yellow
|
||||
- purple
|
||||
|
||||
- defaultColor (Is set on boot)
|
||||
|
||||
- on
|
||||
- off
|
||||
|
||||
- brighter
|
||||
- darker
|
||||
|
||||
- smooth
|
||||
- fade
|
||||
Executable
+499
@@ -0,0 +1,499 @@
|
||||
#!/usr/bin/env python3
|
||||
import html
|
||||
import json
|
||||
import re
|
||||
import secrets
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from urllib.parse import urlparse, parse_qs
|
||||
|
||||
CONFIG_PATH = Path(sys.argv[1] if len(sys.argv) > 1 else "./commands.json")
|
||||
WEB_CSRF_TOKEN = secrets.token_urlsafe(32)
|
||||
|
||||
|
||||
def load_config():
|
||||
with CONFIG_PATH.open("r", encoding="utf-8") as f:
|
||||
cfg = json.load(f)
|
||||
|
||||
cfg.setdefault("host", "0.0.0.0")
|
||||
cfg.setdefault("port", 8080)
|
||||
cfg.setdefault("max_body_bytes", 65536)
|
||||
cfg.setdefault("commands", {})
|
||||
|
||||
if not isinstance(cfg["commands"], dict) or not cfg["commands"]:
|
||||
raise RuntimeError("No commands configured")
|
||||
|
||||
validate_config(cfg)
|
||||
return cfg
|
||||
|
||||
|
||||
def validate_config(cfg):
|
||||
for command_id, command in cfg["commands"].items():
|
||||
if not re.fullmatch(r"[a-zA-Z0-9_-]{1,64}", command_id):
|
||||
raise RuntimeError(f"Invalid command id: {command_id}")
|
||||
|
||||
if "command" not in command or not isinstance(command["command"], list) or not command["command"]:
|
||||
raise RuntimeError(f"Command '{command_id}' needs a non-empty command list")
|
||||
|
||||
command.setdefault("args", {})
|
||||
command.setdefault("timeout_seconds", 30)
|
||||
|
||||
if int(command["timeout_seconds"]) <= 0:
|
||||
raise RuntimeError(f"Command '{command_id}' has invalid timeout_seconds")
|
||||
|
||||
if not command.get("web_enabled", False):
|
||||
if not command.get("api_key") or len(command["api_key"]) < 16:
|
||||
raise RuntimeError(f"Command '{command_id}' needs api_key with at least 16 chars")
|
||||
|
||||
placeholders = set()
|
||||
for part in command["command"]:
|
||||
for match in re.findall(r"{([a-zA-Z0-9_]+)}", str(part)):
|
||||
placeholders.add(match)
|
||||
|
||||
missing_args = placeholders - set(command["args"].keys())
|
||||
if missing_args:
|
||||
raise RuntimeError(
|
||||
f"Command '{command_id}' uses undefined args: {', '.join(sorted(missing_args))}"
|
||||
)
|
||||
|
||||
for arg_name, rule in command["args"].items():
|
||||
if not re.fullmatch(r"[a-zA-Z0-9_]{1,64}", arg_name):
|
||||
raise RuntimeError(f"Command '{command_id}' has invalid arg name: {arg_name}")
|
||||
|
||||
pattern = rule.get("pattern", r"^[a-zA-Z0-9_.:/ -]{0,128}$")
|
||||
try:
|
||||
re.compile(pattern)
|
||||
except re.error as exc:
|
||||
raise RuntimeError(
|
||||
f"Command '{command_id}' arg '{arg_name}' has invalid regex: {exc}"
|
||||
)
|
||||
|
||||
if command.get("web_enabled", False) and rule.get("required", False):
|
||||
if "web_default" not in rule and "default" not in rule:
|
||||
raise RuntimeError(
|
||||
f"Web command '{command_id}' needs web_default/default for required arg '{arg_name}'"
|
||||
)
|
||||
|
||||
|
||||
CONFIG = load_config()
|
||||
|
||||
|
||||
class ApiHandler(BaseHTTPRequestHandler):
|
||||
server_version = "RpiScriptApi/1.3"
|
||||
|
||||
def log_message(self, fmt, *args):
|
||||
print(
|
||||
"%s [%s] %s"
|
||||
% (self.client_address[0], self.log_date_time_string(), fmt % args)
|
||||
)
|
||||
|
||||
def send_json(self, status, payload):
|
||||
data = json.dumps(payload, indent=2).encode("utf-8")
|
||||
self.send_response(status)
|
||||
self.send_header("Content-Type", "application/json; charset=utf-8")
|
||||
self.send_header("Content-Length", str(len(data)))
|
||||
self.send_security_headers()
|
||||
self.end_headers()
|
||||
self.wfile.write(data)
|
||||
|
||||
def send_html(self, status, body):
|
||||
data = body.encode("utf-8")
|
||||
self.send_response(status)
|
||||
self.send_header("Content-Type", "text/html; charset=utf-8")
|
||||
self.send_header("Content-Length", str(len(data)))
|
||||
self.send_security_headers()
|
||||
self.end_headers()
|
||||
self.wfile.write(data)
|
||||
|
||||
def send_security_headers(self):
|
||||
self.send_header("X-Content-Type-Options", "nosniff")
|
||||
self.send_header("X-Frame-Options", "DENY")
|
||||
self.send_header("Referrer-Policy", "no-referrer")
|
||||
self.send_header("Cache-Control", "no-store")
|
||||
self.send_header(
|
||||
"Content-Security-Policy",
|
||||
"default-src 'self'; style-src 'unsafe-inline'; form-action 'self'; frame-ancestors 'none'"
|
||||
)
|
||||
|
||||
def read_body_raw(self):
|
||||
try:
|
||||
length = int(self.headers.get("Content-Length", "0"))
|
||||
except ValueError:
|
||||
raise ValueError("invalid content-length")
|
||||
|
||||
if length > int(CONFIG["max_body_bytes"]):
|
||||
raise ValueError("request body too large")
|
||||
|
||||
if length == 0:
|
||||
return b""
|
||||
|
||||
return self.rfile.read(length)
|
||||
|
||||
def read_json_body(self):
|
||||
raw = self.read_body_raw()
|
||||
if not raw:
|
||||
return {}
|
||||
|
||||
try:
|
||||
return json.loads(raw.decode("utf-8"))
|
||||
except json.JSONDecodeError as exc:
|
||||
raise ValueError(f"invalid json: {exc}")
|
||||
|
||||
def read_form_body(self):
|
||||
raw = self.read_body_raw()
|
||||
if not raw:
|
||||
return {}
|
||||
|
||||
content_type = self.headers.get("Content-Type", "")
|
||||
if "application/x-www-form-urlencoded" not in content_type:
|
||||
raise ValueError("invalid form content-type")
|
||||
|
||||
parsed = parse_qs(raw.decode("utf-8"), keep_blank_values=True)
|
||||
return {k: v[0] if v else "" for k, v in parsed.items()}
|
||||
|
||||
def do_GET(self):
|
||||
path = urlparse(self.path).path
|
||||
|
||||
if path == "/":
|
||||
self.send_html(200, render_index())
|
||||
return
|
||||
|
||||
if path == "/health":
|
||||
self.send_json(200, {"ok": True})
|
||||
return
|
||||
|
||||
if path == "/commands":
|
||||
self.send_json(200, list_commands())
|
||||
return
|
||||
|
||||
self.send_json(404, {"ok": False, "error": "not found"})
|
||||
|
||||
def do_POST(self):
|
||||
path = urlparse(self.path).path
|
||||
|
||||
api_match = re.fullmatch(r"/run/([a-zA-Z0-9_-]+)", path)
|
||||
web_match = re.fullmatch(r"/web/run/([a-zA-Z0-9_-]+)", path)
|
||||
|
||||
if api_match:
|
||||
self.handle_run_api(api_match.group(1))
|
||||
return
|
||||
|
||||
if web_match:
|
||||
self.handle_run_web(web_match.group(1))
|
||||
return
|
||||
|
||||
self.send_json(404, {"ok": False, "error": "not found"})
|
||||
|
||||
def handle_run_api(self, command_id):
|
||||
command_cfg = CONFIG["commands"].get(command_id)
|
||||
|
||||
if not command_cfg:
|
||||
self.send_json(404, {"ok": False, "error": "unknown command"})
|
||||
return
|
||||
|
||||
api_key = self.headers.get("X-API-Key", "")
|
||||
expected_key = command_cfg.get("api_key", "")
|
||||
|
||||
if not expected_key or not secrets.compare_digest(api_key, expected_key):
|
||||
self.send_json(403, {"ok": False, "error": "invalid api key"})
|
||||
return
|
||||
|
||||
try:
|
||||
payload = self.read_json_body()
|
||||
args = payload.get("args", {})
|
||||
if not isinstance(args, dict):
|
||||
raise ValueError("args must be an object")
|
||||
|
||||
validated_args = validate_args(command_cfg, args)
|
||||
command = build_command(command_cfg, validated_args)
|
||||
result = run_command(command_id, command, command_cfg)
|
||||
|
||||
status = 200 if result["ok"] else 500
|
||||
self.send_json(status, result)
|
||||
|
||||
except subprocess.TimeoutExpired as exc:
|
||||
self.send_json(504, timeout_result(command_id, exc))
|
||||
|
||||
except Exception as exc:
|
||||
self.send_json(400, {"ok": False, "error": str(exc)})
|
||||
|
||||
def handle_run_web(self, command_id):
|
||||
command_cfg = CONFIG["commands"].get(command_id)
|
||||
|
||||
if not command_cfg:
|
||||
self.send_html(404, render_page("Unknown command", "<p>Unknown command.</p>"))
|
||||
return
|
||||
|
||||
if not command_cfg.get("web_enabled", False):
|
||||
self.send_html(403, render_page("Forbidden", "<p>Command is not enabled for web UI.</p>"))
|
||||
return
|
||||
|
||||
try:
|
||||
form = self.read_form_body()
|
||||
csrf = form.get("csrf", "")
|
||||
|
||||
if not secrets.compare_digest(csrf, WEB_CSRF_TOKEN):
|
||||
raise ValueError("invalid csrf token")
|
||||
|
||||
args = {}
|
||||
for name, rule in command_cfg.get("args", {}).items():
|
||||
if "web_default" in rule:
|
||||
args[name] = rule["web_default"]
|
||||
elif "default" in rule:
|
||||
args[name] = rule["default"]
|
||||
elif rule.get("required", False):
|
||||
raise ValueError(f"missing web_default for required arg: {name}")
|
||||
|
||||
validated_args = validate_args(command_cfg, args)
|
||||
command = build_command(command_cfg, validated_args)
|
||||
result = run_command(command_id, command, command_cfg)
|
||||
|
||||
self.send_html(200, render_result(command_id, result))
|
||||
|
||||
except subprocess.TimeoutExpired as exc:
|
||||
self.send_html(504, render_result(command_id, timeout_result(command_id, exc)))
|
||||
|
||||
except Exception as exc:
|
||||
self.send_html(400, render_page("Error", f"<pre>{html.escape(str(exc))}</pre>"))
|
||||
|
||||
|
||||
def list_commands():
|
||||
return {
|
||||
"ok": True,
|
||||
"commands": [
|
||||
{
|
||||
"id": command_id,
|
||||
"description": command.get("description", ""),
|
||||
"args": list(command.get("args", {}).keys()),
|
||||
"web_enabled": bool(command.get("web_enabled", False))
|
||||
}
|
||||
for command_id, command in CONFIG["commands"].items()
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
def validate_args(command_cfg, incoming_args):
|
||||
arg_rules = command_cfg.get("args", {})
|
||||
validated = {}
|
||||
|
||||
unknown = set(incoming_args.keys()) - set(arg_rules.keys())
|
||||
if unknown:
|
||||
raise ValueError(f"unknown args: {', '.join(sorted(unknown))}")
|
||||
|
||||
for name, rule in arg_rules.items():
|
||||
value = incoming_args.get(name, rule.get("default"))
|
||||
|
||||
if value in [None, ""]:
|
||||
if rule.get("required", False):
|
||||
raise ValueError(f"missing arg: {name}")
|
||||
value = ""
|
||||
|
||||
value = str(value)
|
||||
pattern = rule.get("pattern", r"^[a-zA-Z0-9_.:/ -]{0,128}$")
|
||||
|
||||
if not re.fullmatch(pattern, value):
|
||||
raise ValueError(f"invalid arg: {name}")
|
||||
|
||||
validated[name] = value
|
||||
|
||||
allowlist = command_cfg.get("allowlist")
|
||||
if allowlist:
|
||||
checked_values = [
|
||||
validated.get(name)
|
||||
for name in command_cfg.get("allowlist_args", [])
|
||||
]
|
||||
|
||||
if checked_values not in allowlist:
|
||||
raise ValueError(f"not allowed: {checked_values}")
|
||||
|
||||
return validated
|
||||
|
||||
|
||||
def build_command(command_cfg, args):
|
||||
command = []
|
||||
|
||||
for part in command_cfg["command"]:
|
||||
value = str(part)
|
||||
for key, arg_value in args.items():
|
||||
value = value.replace("{" + key + "}", arg_value)
|
||||
command.append(value)
|
||||
|
||||
return command
|
||||
|
||||
|
||||
def run_command(command_id, command, command_cfg):
|
||||
request_id = str(uuid.uuid4())
|
||||
started = time.time()
|
||||
timeout = int(command_cfg.get("timeout_seconds", 30))
|
||||
|
||||
env = {
|
||||
"PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
|
||||
"LANG": "C.UTF-8",
|
||||
"LC_ALL": "C.UTF-8",
|
||||
"REQUEST_ID": request_id
|
||||
}
|
||||
|
||||
env.update({str(k): str(v) for k, v in command_cfg.get("environment", {}).items()})
|
||||
|
||||
print(f"request_id={request_id} command_id={command_id} command={command}")
|
||||
|
||||
completed = subprocess.run(
|
||||
command,
|
||||
shell=False,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=timeout,
|
||||
check=False,
|
||||
env=env
|
||||
)
|
||||
|
||||
duration_ms = int((time.time() - started) * 1000)
|
||||
|
||||
return {
|
||||
"ok": completed.returncode == 0,
|
||||
"request_id": request_id,
|
||||
"command_id": command_id,
|
||||
"command": command,
|
||||
"exit_code": completed.returncode,
|
||||
"timed_out": False,
|
||||
"duration_ms": duration_ms,
|
||||
"stdout": completed.stdout,
|
||||
"stderr": completed.stderr
|
||||
}
|
||||
|
||||
|
||||
def timeout_result(command_id, exc):
|
||||
return {
|
||||
"ok": False,
|
||||
"request_id": str(uuid.uuid4()),
|
||||
"command_id": command_id,
|
||||
"command": getattr(exc, "cmd", []),
|
||||
"exit_code": None,
|
||||
"timed_out": True,
|
||||
"duration_ms": None,
|
||||
"stdout": exc.stdout or "",
|
||||
"stderr": exc.stderr or "command timed out"
|
||||
}
|
||||
|
||||
|
||||
def render_index():
|
||||
cards = []
|
||||
|
||||
for command_id, command in CONFIG["commands"].items():
|
||||
if not command.get("web_enabled", False):
|
||||
continue
|
||||
|
||||
title = html.escape(command.get("web_title", command_id))
|
||||
desc = html.escape(command.get("description", ""))
|
||||
|
||||
cards.append(f"""
|
||||
<form method="post" action="/web/run/{html.escape(command_id)}" class="card">
|
||||
<input type="hidden" name="csrf" value="{html.escape(WEB_CSRF_TOKEN)}">
|
||||
<h2>{title}</h2>
|
||||
<p>{desc}</p>
|
||||
<button type="submit">Run</button>
|
||||
</form>
|
||||
""")
|
||||
|
||||
if not cards:
|
||||
cards.append("<p>No web-enabled commands configured.</p>")
|
||||
|
||||
return render_page("Raspberry Pi Script API", "\n".join(cards))
|
||||
|
||||
|
||||
def render_result(command_id, result):
|
||||
body = f"""
|
||||
<p><a href="/">Back</a></p>
|
||||
<h2>Result: {html.escape(command_id)}</h2>
|
||||
|
||||
<table>
|
||||
<tr><th>OK</th><td>{html.escape(str(result.get("ok")))}</td></tr>
|
||||
<tr><th>Exit Code</th><td>{html.escape(str(result.get("exit_code")))}</td></tr>
|
||||
<tr><th>Timed out</th><td>{html.escape(str(result.get("timed_out")))}</td></tr>
|
||||
<tr><th>Duration</th><td>{html.escape(str(result.get("duration_ms")))} ms</td></tr>
|
||||
<tr><th>Request ID</th><td>{html.escape(str(result.get("request_id")))}</td></tr>
|
||||
</table>
|
||||
|
||||
<h3>stdout</h3>
|
||||
<pre>{html.escape(str(result.get("stdout", "")))}</pre>
|
||||
|
||||
<h3>stderr</h3>
|
||||
<pre>{html.escape(str(result.get("stderr", "")))}</pre>
|
||||
"""
|
||||
|
||||
return render_page("Command Result", body)
|
||||
|
||||
|
||||
def render_page(title, body):
|
||||
return f"""<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>{html.escape(title)}</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style>
|
||||
body {{
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
margin: 2rem;
|
||||
background: #f6f7f8;
|
||||
color: #222;
|
||||
}}
|
||||
.card {{
|
||||
background: white;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 12px;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
max-width: 520px;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,.08);
|
||||
}}
|
||||
button {{
|
||||
font-size: 1rem;
|
||||
padding: .65rem 1rem;
|
||||
border: 0;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
background: #222;
|
||||
color: white;
|
||||
}}
|
||||
pre {{
|
||||
background: #111;
|
||||
color: #eee;
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
overflow: auto;
|
||||
}}
|
||||
table {{
|
||||
border-collapse: collapse;
|
||||
background: white;
|
||||
margin-bottom: 1rem;
|
||||
}}
|
||||
th, td {{
|
||||
border: 1px solid #ddd;
|
||||
padding: .5rem .8rem;
|
||||
text-align: left;
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>{html.escape(title)}</h1>
|
||||
{body}
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
|
||||
def main():
|
||||
server = ThreadingHTTPServer((CONFIG["host"], int(CONFIG["port"])), ApiHandler)
|
||||
print(f"Listening on http://{CONFIG['host']}:{CONFIG['port']}")
|
||||
print(f"Config: {CONFIG_PATH}")
|
||||
server.serve_forever()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
{
|
||||
"carrier": 38000,
|
||||
"repeat": 1,
|
||||
"signal": [
|
||||
{ "type": "mark", "duration": 4502 },
|
||||
{ "type": "space", "duration": 4487 },
|
||||
|
||||
{ "type": "mark", "duration": 555 },
|
||||
{ "type": "space", "duration": 599 },
|
||||
|
||||
{ "type": "mark", "duration": 556 },
|
||||
{ "type": "space", "duration": 571 },
|
||||
|
||||
{ "type": "mark", "duration": 533 },
|
||||
{ "type": "space", "duration": 1701 },
|
||||
|
||||
{ "type": "mark", "duration": 557 },
|
||||
{ "type": "space", "duration": 571 },
|
||||
|
||||
{ "type": "mark", "duration": 532 },
|
||||
{ "type": "space", "duration": 1708 },
|
||||
|
||||
{ "type": "mark", "duration": 552 },
|
||||
{ "type": "space", "duration": 572 },
|
||||
|
||||
{ "type": "mark", "duration": 555 },
|
||||
{ "type": "space", "duration": 1677 },
|
||||
|
||||
{ "type": "mark", "duration": 559 },
|
||||
{ "type": "space", "duration": 543 },
|
||||
|
||||
{ "type": "mark", "duration": 586 },
|
||||
{ "type": "space", "duration": 1676 },
|
||||
|
||||
{ "type": "mark", "duration": 534 },
|
||||
{ "type": "space", "duration": 594 },
|
||||
|
||||
{ "type": "mark", "duration": 557 },
|
||||
{ "type": "space", "duration": 542 },
|
||||
|
||||
{ "type": "mark", "duration": 587 },
|
||||
{ "type": "space", "duration": 543 },
|
||||
|
||||
{ "type": "mark", "duration": 586 },
|
||||
{ "type": "space", "duration": 571 },
|
||||
|
||||
{ "type": "mark", "duration": 532 },
|
||||
{ "type": "space", "duration": 1704 },
|
||||
|
||||
{ "type": "mark", "duration": 556 },
|
||||
{ "type": "space", "duration": 1679 },
|
||||
|
||||
{ "type": "mark", "duration": 528 },
|
||||
{ "type": "space", "duration": 568 },
|
||||
|
||||
{ "type": "mark", "duration": 561 },
|
||||
{ "type": "space", "duration": 1703 },
|
||||
|
||||
{ "type": "mark", "duration": 558 },
|
||||
{ "type": "space", "duration": 1682 },
|
||||
|
||||
{ "type": "mark", "duration": 527 },
|
||||
{ "type": "space", "duration": 595 },
|
||||
|
||||
{ "type": "mark", "duration": 557 },
|
||||
{ "type": "space", "duration": 555 },
|
||||
|
||||
{ "type": "mark", "duration": 574 },
|
||||
{ "type": "space", "duration": 573 },
|
||||
|
||||
{ "type": "mark", "duration": 531 },
|
||||
{ "type": "space", "duration": 597 },
|
||||
|
||||
{ "type": "mark", "duration": 555 },
|
||||
{ "type": "space", "duration": 546 },
|
||||
|
||||
{ "type": "mark", "duration": 561 },
|
||||
{ "type": "space", "duration": 594 },
|
||||
|
||||
{ "type": "mark", "duration": 535 },
|
||||
{ "type": "space", "duration": 594 },
|
||||
|
||||
{ "type": "mark", "duration": 535 },
|
||||
{ "type": "space", "duration": 567 },
|
||||
|
||||
{ "type": "mark", "duration": 587 },
|
||||
{ "type": "space", "duration": 574 },
|
||||
|
||||
{ "type": "mark", "duration": 554 },
|
||||
{ "type": "space", "duration": 1679 },
|
||||
|
||||
{ "type": "mark", "duration": 531 },
|
||||
{ "type": "space", "duration": 1700 },
|
||||
|
||||
{ "type": "mark", "duration": 536 },
|
||||
{ "type": "space", "duration": 1699 },
|
||||
|
||||
{ "type": "mark", "duration": 559 },
|
||||
{ "type": "space", "duration": 540 },
|
||||
|
||||
{ "type": "mark", "duration": 571 },
|
||||
{ "type": "space", "duration": 1698 },
|
||||
|
||||
{ "type": "mark", "duration": 555 },
|
||||
|
||||
{ "type": "space", "duration": 21428 }
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user