315 lines
14 KiB
Markdown
315 lines
14 KiB
Markdown
# Main Gate Controller
|
||
|
||
Eine vollständig über die Home-Assistant-UI konfigurierbare Custom Integration für
|
||
ein elektrisches Fußgängertor. Die Integration ersetzt eine bestehende Lösung aus
|
||
Skripten, Timern und Helpern und steuert das Tor indirekt über eine bereits
|
||
vorhandene Switch-Entity (z. B. `switch.shellyplusuni_8c4f00a6ce94_output_0`).
|
||
|
||
> Vor der ersten Veröffentlichung müssen die Platzhalter in `manifest.json`,
|
||
> `hacs.json`, dieser README und in `LICENSE` durch die tatsächlichen Werte
|
||
> (GitHub-Benutzername, Klarname) ersetzt werden. Die Platzhalter tragen das
|
||
> Präfix `YOUR_GITHUB_USERNAME`.
|
||
|
||
## Features
|
||
|
||
- **Mehrere Tore** – pro Tor wird ein eigener Config Entry angelegt.
|
||
- **Konfiguration über die UI** – inklusive modernem Entity-Selector für die
|
||
Switch-Entity, NumberSelectorn für Zeiten und EntitySelector für Notify-Entities.
|
||
- **Config Flow + Options Flow** – alle Parameter lassen sich nachträglich in
|
||
`Settings → Devices & Services → Main Gate Controller → Configure` ändern.
|
||
- **Mode: single** – ein laufender Ablauf kann nicht durch einen zweiten
|
||
Button-Press unterbrochen werden.
|
||
- **Robuste State Machine** – `closed → opening → open → closing → closed` mit
|
||
Debug-Log auf jedem Übergang. Eine fehlgeschlagene Benachrichtigung führt
|
||
nicht zum Abbruch. Ein fehlgeschlagener Switch-Service bricht den Ablauf ab
|
||
und setzt den Status zurück auf `closed`.
|
||
- **Synchronisation mit dem Sensor** – `remaining_seconds` wird während der
|
||
Offen-Phase jede Sekunde aktualisiert und steht sofort im Dashboard zur
|
||
Verfügung.
|
||
- **Restart-sicher** – nach einem Home-Assistant-Neustart beginnt der Status
|
||
mit `closed`, `running=false`. Ein ggf. unterbrochener Ablauf wird
|
||
protokolliert.
|
||
- **Diagnostics** – `home-assistant://developer-tools/...` Diagnose-Export ohne
|
||
sensible Daten.
|
||
|
||
## Ablauf
|
||
|
||
Wenn ein Tor geöffnet wird, führt die Integration exakt die folgende Sequenz
|
||
aus und nutzt ausschließlich Service-Calls auf `switch.turn_on` /
|
||
`switch.turn_off`:
|
||
|
||
```
|
||
1. Status: opening
|
||
2. (optional) Notification „Opening Main Gate (Pedestrians)“
|
||
3. switch.turn_off
|
||
4. warten (pulse_duration, Standard 1 s)
|
||
5. switch.turn_on
|
||
6. warten (pulse_duration)
|
||
7. switch.turn_off
|
||
8. Status: open
|
||
9. Countdown (open_duration, Standard 20 s), remaining_seconds zählt sekündlich herunter
|
||
10. Status: closing
|
||
11. (optional) Notification „Closing Main Gate“
|
||
12. switch.turn_on
|
||
13. warten (pulse_duration)
|
||
14. switch.turn_off
|
||
15. warten (pulse_duration)
|
||
16. Status: closed
|
||
```
|
||
|
||
## Installation via HACS (Custom Repository)
|
||
|
||
1. **HACS** in der Sidebar öffnen.
|
||
2. Auf das **⋮-Menü → Custom repositories** klicken.
|
||
3. Die URL dieses GitHub-Repositories eintragen, z. B.
|
||
`https://github.com/<your-username>/ha-main-gate-controller`.
|
||
4. **Typ** auf `Integration` stehen lassen.
|
||
5. **Repository hinzufügen** klicken.
|
||
6. In der HACS-Übersicht erscheint *Main Gate Controller* und kann über
|
||
**Download** installiert werden.
|
||
7. Falls erforderlich Home Assistant neu starten.
|
||
8. **Settings → Devices & Services → Add Integration** öffnen.
|
||
9. **Main Gate Controller** auswählen.
|
||
10. Das Gate konfigurieren (Name, Switch-Entity, Zeiten, optional Notifications).
|
||
|
||
## Manuelle Installation (ohne HACS)
|
||
|
||
```
|
||
custom_components/main_gate_controller/
|
||
```
|
||
|
||
in das Home-Assistant-Konfigurationsverzeichnis kopieren, Home Assistant
|
||
neu starten und denselben UI-Pfad ab Schritt 8 verwenden.
|
||
|
||
## Einrichtung
|
||
|
||
### Config Flow
|
||
|
||
Der Config Flow ist ein einzelner Schritt. Die folgenden Felder werden
|
||
abgefragt:
|
||
|
||
| Feld | Pflicht | Default | Beschreibung |
|
||
| ----------------------------- | :----: | -------------------------------------- | ----------------------------------------------------------------------- |
|
||
| `Gate name` | ✅ | `Main Gate` | Anzeigename – wird als Device-Name und Sensor-Entity-ID-Basis verwendet |
|
||
| `Gate switch` | ✅ | — | Switch-Entity zur Torsteuerung (Selector: `EntitySelector(domain="switch")`) |
|
||
| `Open duration (seconds)` | ✅ | `20` | Wie lange das Tor offengehalten wird |
|
||
| `Pulse duration (seconds)` | ✅ | `1.0` | Ein-/Aus-Pulsdauer der Switch-Impulse |
|
||
| `Send notifications` | ✅ | `false` | Aktiviert/deaktiviert alle Benachrichtigungen |
|
||
| `Notification targets` | ⛔ | leer | Notify-Entities (Selector: `EntitySelector(domain="notify", multiple=True)`) |
|
||
| `Opening notification text` | ⛔ | `Opening Main Gate (Pedestrians)` | Text der Öffnungs-Benachrichtigung |
|
||
| `Closing notification text` | ⛔ | `Closing Main Gate` | Text der Schließ-Benachrichtigung |
|
||
|
||
Wird `Send notifications` aktiviert ohne dass Targets ausgewählt sind, meldet
|
||
der Flow einen Fehler. Außerdem wird verhindert, dass dieselbe Switch-Entity
|
||
an mehrere Config Entries gebunden wird (Unique-ID-Schutz).
|
||
|
||
### Options Flow
|
||
|
||
Über **Settings → Devices & Services → Main Gate Controller → Configure**
|
||
lassen sich `Open duration`, `Pulse duration`, `Send notifications`,
|
||
`Notification targets` und beide Notification-Texte jederzeit nachjustieren.
|
||
Der Name und die Switch-Entity sind im Options Flow bewusst nicht editierbar;
|
||
dafür ist das Löschen und Neuanlegen eines Gates der vorgesehene Weg.
|
||
|
||
### Switch- und Notify-Domain
|
||
|
||
Die Integration geht davon aus, dass die ausgewählte Switch-Entity und die
|
||
ausgewählten Notify-Entities bereits in Home Assistant existieren (z. B. Shelly,
|
||
Sonoff, MQTT, Companion-App). Es gibt **keine** direkte Geräte-Abhängigkeit.
|
||
|
||
## Entities
|
||
|
||
Für jedes Gate (jeden Config Entry) wird ein gemeinsames Device angelegt mit
|
||
`manufacturer = Custom` und `model = Main Gate Controller`. Folgende Entities
|
||
gehören zum Device:
|
||
|
||
### Button `button.<gate>_open`
|
||
|
||
Startet den kompletten Ablauf. Friendly-Name: `Open`. Ist bereits ein Ablauf
|
||
aktiv, wird der Druck ignoriert (logischer `mode: single`). Wird sauber
|
||
protokolliert, nicht als Fehler.
|
||
|
||
### Sensor `sensor.<gate>_status`
|
||
|
||
Maschinenlesbarer State, standardmäßig einer von `closed`, `opening`, `open`,
|
||
`closing`. Attribute (immer vorhanden):
|
||
|
||
| Attribut | Typ | Beschreibung |
|
||
| -------------------- | -------------- | --------------------------------------------------- |
|
||
| `remaining_seconds` | `int \| null` | Sekunden bis zum automatischen Schließen |
|
||
| `duration` | `number` | Konfigurierte `Open duration` |
|
||
| `running` | `bool` | `True`, solange ein Ablauf läuft |
|
||
| `started_at` | `string\|null` | ISO 8601 UTC – Zyklusstart |
|
||
| `finishes_at` | `string\|null` | ISO 8601 UTC – berechnetes Ende der Öffnungsphase |
|
||
|
||
Während der `open`-Phase wird `remaining_seconds` ungefähr jede Sekunde
|
||
aktualisiert.
|
||
|
||
> **Bewusste Designentscheidung:** Es wird kein zusätzlicher
|
||
> `binary_sensor.<gate>_running` erzeugt. Das `running`-Attribut am
|
||
> Statussensor liefert dieselbe Information ohne eine weitere Entity, und
|
||
> `sensor.<gate>_status.options` enthält ohnehin alle Zustände.
|
||
|
||
## Beispiel Dashboard
|
||
|
||
Eine kompakte, einzelne Tile-Card reicht aus. Die Tile-Card unterstützt
|
||
das `state_content`-Feld, das beliebige Attribute der Entity anzeigen kann.
|
||
Wir übergeben den maschinenlesbaren State **und** die verbleibenden Sekunden:
|
||
|
||
```yaml
|
||
type: tile
|
||
entity: sensor.main_gate_status
|
||
name: Main Gate
|
||
icon: mdi:gate
|
||
state_content:
|
||
- state
|
||
- remaining_seconds
|
||
```
|
||
|
||
### Wer das Ergebnis in der „sprechenden“ Variante sehen will
|
||
|
||
Da der Sensor-State `closed/opening/open/closing` maschinenlesbar bleibt,
|
||
werden die deutschen Bezeichnungen „Geschlossen/Öffnet/Offen/Schließt“ nicht
|
||
im State selbst, sondern als Übersetzung im Frontend angezeigt. **Auf einer
|
||
Tile-Card sieht man also je nach Frontend-Sprache entweder die englische
|
||
oder die lokalisierte Variante – aber derselbe Sensor.**
|
||
|
||
Wer auf den englischen Buttons-State explizit verzichten will, kann alternativ
|
||
mit dem `mdi:clock-digital`-Icon und einer Text-Karte arbeiten:
|
||
|
||
```yaml
|
||
type: conditional
|
||
conditions:
|
||
- entity: sensor.main_gate_status
|
||
state: open
|
||
card:
|
||
type: markdown
|
||
content: >-
|
||
Open · {{ state_attr('sensor.main_gate_status', 'remaining_seconds') }}s
|
||
```
|
||
|
||
## Update-Prozess
|
||
|
||
HACS markiert neue Releases automatisch. Nach einer Aktualisierung empfiehlt
|
||
sich ein Home-Assistant-Neustart, weil die Integration während eines laufenden
|
||
Ablaufs nicht in-place ausgetauscht werden kann.
|
||
|
||
> Aktuell ist keine Reload-Annotation aktiv: ein bereits laufender Zyklus geht
|
||
> beim Update verloren und der Status wird auf `closed` zurückgesetzt. Das ist
|
||
> mit einem schief gestellten Tor-Position fehlertolerant (siehe
|
||
> *Einschränkungen*).
|
||
|
||
## Entwicklung
|
||
|
||
```bash
|
||
git clone https://github.com/<your-username>/ha-main-gate-controller
|
||
cd ha-main-gate-controller
|
||
python3 -m venv .venv
|
||
source .venv/bin/activate
|
||
pip install --upgrade pip
|
||
pip install -r requirements_test.txt
|
||
|
||
# Statische Prüfung
|
||
ruff check custom_components tests
|
||
|
||
# Tests
|
||
pytest
|
||
```
|
||
|
||
Die Tests verwenden das Standard-Testwerkzeug
|
||
[`pytest-homeassistant-custom-component`](https://github.com/jaisenbe/pytest-homeassistant-custom-component),
|
||
welches eine bestimmte Version von Home Assistant pinnt. Damit lassen sich
|
||
Config-Flow, Entity-Lebenszyklus und asynchrone Service-Calls mit dem
|
||
echten HA-Testkern testen. Die Integration wird über den
|
||
`custom_components/`-Ordner direkt neben den Tests gefunden.
|
||
|
||
## Release-Prozess
|
||
|
||
1. Änderungen hochladen und prüfen, dass alle CI-Checks grün sind
|
||
(HACS validation, Hassfest, pytest).
|
||
2. Im Repository die Versionsnummer in `manifest.json` und im Tag
|
||
synchron anpassen. Der Release-Workflow
|
||
(`.github/workflows/release.yml`) erzwingt das. Ziel: `version: 0.1.0`
|
||
im Manifest ↔ Git-Tag `v0.1.0`.
|
||
3. Tag setzen und pushen:
|
||
```bash
|
||
git tag v0.1.0
|
||
git push origin v0.1.0
|
||
```
|
||
4. GitHub Actions baut das Release-Artefakt (Zip der Integration) und
|
||
veröffentlicht es auf der GitHub-Releases-Seite.
|
||
|
||
## Troubleshooting
|
||
|
||
### „cannot add entities“ / Integration lädt nicht
|
||
|
||
- Home Assistant neu starten, damit `custom_components/main_gate_controller`
|
||
einmal frisch gelesen wird.
|
||
- Logs:
|
||
```yaml
|
||
logger:
|
||
logs:
|
||
custom_components.main_gate_controller: debug
|
||
```
|
||
|
||
### Button bleibt ohne Wirkung
|
||
|
||
- Konfiguration prüfen: Settings → Devices & Services → Main Gate Controller →
|
||
Gerät → Entity → Status sollte `closed` sein. Wenn nicht, einmal manuell
|
||
über die Switch-Entity steuern, um sicher zu sein, dass das Relais
|
||
überhaupt reagiert.
|
||
|
||
### Benachrichtigungen kommen nicht an
|
||
|
||
- `Send notifications` aktiviert? Targets ausgewählt?
|
||
- Die Entity-Selector-Suche zeigt nur `notify.*`-Entities; bei
|
||
Companion-Apps darauf achten, dass die Mobile-App bereits ein
|
||
`notify.mobile_app_<phone>` angelegt hat.
|
||
|
||
### Status bleibt nach Stromausfall auf einer Phase hängen
|
||
|
||
- Die Integration ist **blind** für die reale Tor-Position: nach einem
|
||
Neustart beginnt `sensor.<gate>_status` mit `closed`. Wenn das Tor
|
||
während des Ausfalls halb offen geblieben ist, muss es von Hand
|
||
abgeschlossen werden.
|
||
|
||
## Architektur
|
||
|
||
```
|
||
custom_components/main_gate_controller/
|
||
├── __init__.py # async_setup_entry, async_unload_entry, DeviceInfo
|
||
├── manifest.json # Domain, Version, Codeowners, iot_class=calculated
|
||
├── const.py # Konstanten / Defaults
|
||
├── coordinator.py # MainGateController ← Zustandsmaschine, Countdown, Service-Calls
|
||
├── config_flow.py # MainGateConfigFlow + MainGateOptionsFlow + Selectors
|
||
├── button.py # button.<gate>_open
|
||
├── sensor.py # sensor.<gate>_status + Attribute
|
||
├── diagnostics.py # anonymisierter Diagnose-Export
|
||
├── strings.json # Übersetzungs-Schlüssel
|
||
└── translations/
|
||
├── en.json
|
||
└── de.json
|
||
```
|
||
|
||
Alle Service-Calls (`switch.turn_on/off`, `notify.send_message`) laufen
|
||
ausschließlich gegen die ausgewählten Home-Assistant-Entities. Es findet kein
|
||
direkter Zugriff auf Hersteller-Integrationen (z. B. Shelly) statt.
|
||
|
||
## Bekannte Einschränkungen / Design-Entscheidungen
|
||
|
||
- **Kein Binary-Sensor „Running“.** Der Statussensor liefert `running`
|
||
bereits als Attribut; eine zweite Entity würde keinen Mehrwert bieten.
|
||
- **Blind-Betrieb.** Die Integration hat keine Rückmeldung über die
|
||
tatsächliche Tor-Position. Wenn das Relais angezogen hat, heißt das
|
||
nicht zwingend, dass das Tor offen ist. Wer eine echtzeitnahe Statusanzeige
|
||
benötigt, sollte die Tor-Endlage zusätzlich an einen Home-Assistant-Sensor
|
||
melden und diesen Automations-Auslöser verwenden.
|
||
- **Restart mitten im Zyklus.** Beim Reload/Unload wird der laufende Zyklus
|
||
abgebrochen und der Status auf `closed` zurückgesetzt. Die reale
|
||
Tor-Position muss im Zweifel per Hand geprüft werden.
|
||
- **Switch-Fehler → safe default.** Schlägt ein `switch.turn_*`-Aufruf
|
||
fehl, wird der Zyklus abgebrochen und der Status auf `closed` gesetzt;
|
||
ein Alarm bleibt im Log. Im Zweifel ist das Relais möglicherweise
|
||
trotzdem ein- oder ausgeschaltet worden, das lässt sich aber nicht
|
||
garantiert ermitteln.
|