Hallo HA-Freunde!
Vielleicht hilfts dem ein oder anderen â vor allem Eltern hier im Forum ![]()
Worum gehts?
Ich wollte unseren WLAN-AP im Kinderzimmer nachts wirklich aus haben. Nicht âSSID verstecktâ, nicht âBandsteering blockt 2.4 GHzâ â sondern Radio off, Funkstille, kein Signal. Aus zwei Gruenden:
-
Strahlung im Schlafzimmer â wir wollten den AP einfach lieber nicht durchballern lassen wenn die Kinder schlafen.
-
âBildschirmzeit zu Endeâ-Funktion â die Kids haben mal Tablets in der Hand, mal nicht. WLAN um 21:00 wegnehmen ist eine sanfte aber wirksame Bremse. Am Wochenende darf es laenger gehen.
Und dann noch der Modus-Schalter: wenn Besuch da ist oder ich abends noch ne Folge auf dem Tablet zu Ende schaue â tap auf âimmer_anâ und der AP bleibt durch. Morgens wieder zurueck auf âautoâ. Klingt simpel, ist aber genau das was mir bei reinen Zeitplaenen immer gefehlt hat.
DER WICHTIGSTE GRUND: UniFi OS macht das NICHT richtig!
Bevor wir loslegen â das ist der eigentliche Aufhaenger fuer diesen ganzen Post:
Die in UniFi OS eingebaute âWiFi Scheduleâ-Funktion schaltet das Radio NICHT aus! Sie unterdrueckt nur die SSID-Bekanntmachung â das Funkmodul sendet trotzdem weiter (Beacons, Probe-Responses, je nach Firmware mehr oder weniger). Wenn ihr also ueber UniFi â WiFi â Schedule âOffâ zwischen 22:00 und 06:00 setzt:
-
Radio sendet weiter (messt es selber mit Wifi Analyzer, ihr seht den AP) -
Stromverbrauch nahezu identisch -
Funkstille im Kinderzimmer? Fehlanzeige
Das hat mich erstmal ueberrascht. Ich dachte echt der Zeitplan macht den Job â tut er nicht. Erst als ich ueber den disabled-Flag der Device-API gestolpert bin, gings los: das deaktiviert das Radio wirklich physisch, der AP ist im Wifi-Scanner komplett weg.
Genau dafuer ist diese HA-Bastelloesung. Es gibt schlicht keinen ârichtigenâ Aus-Schalter in der UniFi-UI selber â den muss man sich ueber die API holen.
Das Endergebnis sieht so aus (Screenshot vom Dashboard):
Modus + Status oben, darunter die vier Zeitfelder â direkt im Dashboard editierbar. Keine YAML-Frickelei mehr fĂŒr Zeit-Ănderungen.
Warum nicht die offizielle UniFi-Integration?
Hab ich auch erst versucht. Die unifi-Integration in HA bringt Switches fĂŒr Clients, fĂŒr Ports, fĂŒr Reconnect-Aktionen â aber nicht das AP-Radio. Das geht nur ĂŒber den device-Endpunkt der Controller-REST-API. Also: kleines Python-Skript drum bauen, fertig. Ist ĂŒberschaubar.
Wenn jemand einen Weg ĂŒber die offizielle Integration kennt â ich freu mich ĂŒber Hinweise! ![]()
Was ihr braucht
-
Einen UniFi-Controller (Cloud Key, UDM, Dream Router oder selbst gehostete UniFi Network Application). Bei mir Self-Hosted auf Port 8443
-
Home Assistant (getestet auf 2025.10.1)
-
AP mit eindeutigem Namen im Controller (Devices â Einstellungen â Name): bei mir
U6+_BZundU6+_WZ -
~15 Minuten Zeit
Schritt 1: Lokalen UniFi-User anlegen
WICHTIG: kein SSO-Account! Wir brauchen einen âklassischenâ lokalen Admin sonst klappt der API-Login nicht.
UniFi Network App â Settings â Admins & Users â Add New Admin â âRestrict to local access onlyâ
-
Username:
ha-integration -
Passwort: irgendwas langes Random (NICHT euer Hauptpasswort)
-
Role: Site Admin
Schritt 2: Python-Skript
Das macht die eigentliche Arbeit. Ein Kommando, ein AP, on oder off. Wir packenâs nach /usr/local/bin/unifi-ap-radio.py auf den Server der HA ausfĂŒhrt:
#!/usr/bin/env python3
"""
unifi-ap-radio.py - UniFi-AP-Radios per API ein/aus
Aufruf:
unifi-ap-radio.py off <ap_name>
unifi-ap-radio.py on <ap_name>
Config: ~/.config/unifi-ap-radio.ini (chmod 600!)
[controller]
host = https://192.168.0.52:8443
user = ha-integration
password = STARK_UND_EINMALIG
site = default
"""
import sys
import configparser
import pathlib
import requests
from urllib3 import disable_warnings
disable_warnings()
CONFIG = pathlib.Path.home() / ".config" / "unifi-ap-radio.ini"
def die(msg, code=1):
print(f"FEHLER: {msg}", file=sys.stderr)
sys.exit(code)
def main():
if len(sys.argv) != 3 or sys.argv[1] not in ("on", "off"):
die("Aufruf: unifi-ap-radio.py {on|off} <ap_name>", code=2)
action, ap_name = sys.argv[1], sys.argv[2]
if not CONFIG.exists():
die(f"Config fehlt: {CONFIG}")
cfg = configparser.ConfigParser()
cfg.read(CONFIG)
c = cfg["controller"]
s = requests.Session()
s.verify = False # selbstsigniertes Cert vom Controller â passt
# Login
r = s.post(
f"{c['host']}/api/login",
json={"username": c["user"], "password": c["password"]},
timeout=10,
)
if not r.ok:
die(f"Login fehlgeschlagen: {r.status_code}")
# AP per Namen suchen
r = s.get(f"{c['host']}/api/s/{c['site']}/stat/device", timeout=10)
devs = r.json().get("data", [])
ap = next((d for d in devs if d.get("name") == ap_name), None)
if not ap:
existing = ", ".join(d.get("name", "?") for d in devs)
die(f"AP nicht gefunden: '{ap_name}'. Vorhanden: {existing}")
# Radio + LED zusammen schalten
disabled = action == "off"
payload = {
"disabled": disabled,
"led_override": "off" if disabled else "default",
}
r = s.put(
f"{c['host']}/api/s/{c['site']}/rest/device/{ap['_id']}",
json=payload,
timeout=10,
)
if not r.ok:
die(f"PUT fehlgeschlagen: {r.status_code}")
print(f"OK: {ap_name} disabled={disabled}")
if __name__ == "__main__":
main()
Drauf machen und Config setzen:
chmod 755 /usr/local/bin/unifi-ap-radio.py
mkdir -p /root/.config
cat > /root/.config/unifi-ap-radio.ini <<'EOF'
[controller]
host = https://192.168.0.52:8443
user = ha-integration
password = MEIN_LANGES_PASSWORT
site = default
EOF
chmod 600 /root/.config/unifi-ap-radio.ini
# Testen!
/usr/local/bin/unifi-ap-radio.py off U6+_BZ
# Bei dir sollte stehen: OK: U6+_BZ disabled=True
# AP sollte 2-3 Sek spÀter dunkel werden (auch die LED), kein WLAN mehr.
/usr/local/bin/unifi-ap-radio.py on U6+_BZ
# Und schon ist er wieder an.
Wenn das hier schon klappt â der schwierige Teil ist durch. Der Rest ist HA-Standard. ![]()
Heads-up fĂŒr UDM/UDR/Cloud Key Gen2+ User: Bei UniFi-OS-GerĂ€ten muss der Pfad anders aussehen:
:443/proxy/network/api/loginstatt:8443/api/login, und CSRF-Token muss man auch mitschleppen. Habe ich selbst nicht ausprobiert weil ich auf der Self-Hosted-App bin. Gibtâs aber Beispiele im Netz. Falls hier jemand das fertig hat â bitte gerne posten, dann ergĂ€nze ichâs oben.
Schritt 3: Home Assistant Konfiguration
Jetzt die HA-Seite. Drei Blöcke in die configuration.yaml:
Shell-Commands (so ruft HA das Skript)
shell_command:
ap_bz_off: "/usr/local/bin/unifi-ap-radio.py off U6+_BZ"
ap_bz_on: "/usr/local/bin/unifi-ap-radio.py on U6+_BZ"
Helpers fĂŒr Modus + Zeiten
input_select:
ap_bz_modus:
name: AP BZ Modus
options:
- auto
- immer_an
- immer_aus
icon: mdi:wifi-cog
input_datetime:
ap_bz_start_weekday:
name: AP BZ Start Mo-Fr
has_date: false
has_time: true
icon: mdi:wifi-arrow-up
ap_bz_end_weekday:
name: AP BZ Ende Mo-Fr
has_date: false
has_time: true
icon: mdi:wifi-arrow-down
ap_bz_start_weekend:
name: AP BZ Start Sa-So
has_date: false
has_time: true
icon: mdi:wifi-arrow-up
ap_bz_end_weekend:
name: AP BZ Ende Sa-So
has_date: false
has_time: true
icon: mdi:wifi-arrow-down
Der âSoll der gerade an sein?â-Sensor
Hier wirdâs interessant. Das Template muss nĂ€mlich auch funktionieren wenn das Zeitfenster ĂŒber Mitternacht geht (z. B. 21:00 bis 06:00 â also âabends ausschalten, morgens wieder anâ). Da muss man drei FĂ€lle abklappern:
template:
- binary_sensor:
- name: AP BZ Soll AN
unique_id: ap_bz_soll_an
icon: mdi:wifi
state: >
{% set today_we = now().weekday() >= 5 %}
{% set yest_we = ((now().weekday() - 1) % 7) >= 5 %}
{% set s_today = states('input_datetime.ap_bz_start_weekend' if today_we else 'input_datetime.ap_bz_start_weekday') %}
{% set e_today = states('input_datetime.ap_bz_end_weekend' if today_we else 'input_datetime.ap_bz_end_weekday') %}
{% set s_yest = states('input_datetime.ap_bz_start_weekend' if yest_we else 'input_datetime.ap_bz_start_weekday') %}
{% set e_yest = states('input_datetime.ap_bz_end_weekend' if yest_we else 'input_datetime.ap_bz_end_weekday') %}
{% set now_t = now().strftime('%H:%M:%S') %}
{% set in_today_normal = s_today <= e_today and s_today <= now_t < e_today %}
{% set in_today_wrap = s_today > e_today and now_t >= s_today %}
{% set in_yest_tail = s_yest > e_yest and now_t < e_yest %}
{{ in_today_normal or in_today_wrap or in_yest_tail }}
Was hier passiert in Kurzfassung:
-
Fall 1 â
in_today_normal: âganz normalâ wie 06:30 bis 21:00 an einem Wochentag â heute Start, heute Ende, jetzt dazwischen -
Fall 2 â
in_today_wrap: Wenn Start > Ende (also z. B. 22:00 bis 06:00) und es ist jetzt nach 22:00 â an -
Fall 3 â
in_yest_tail: Gleiche Logik, aber wir sind im âTailâ â also es ist z. B. 05:00 morgens und das Fenster von gestern lĂ€uft noch
Bonus: Da gestern auch ein Wochenend-Tag gewesen sein kann, mĂŒssen wir bei s_yest/e_yest ein eigenes yest_we Flag bauen. Sonst wĂŒrde Montag um 02:00 die falsche End-Zeit gegen Sonntag-Nachmittag vergleichen ![]()
Automation
- id: "20260519_unifi_ap_bz_sync"
alias: AP BZ folgt Modus und Zeitfenstern
triggers:
- trigger: state
entity_id: binary_sensor.ap_bz_soll_an
- trigger: state
entity_id: input_select.ap_bz_modus
- trigger: homeassistant
event: start
actions:
- choose:
- conditions: "{{ is_state('input_select.ap_bz_modus', 'immer_an') }}"
sequence:
- action: shell_command.ap_bz_on
- conditions: "{{ is_state('input_select.ap_bz_modus', 'immer_aus') }}"
sequence:
- action: shell_command.ap_bz_off
- conditions: "{{ is_state('input_select.ap_bz_modus', 'auto') and is_state('binary_sensor.ap_bz_soll_an', 'on') }}"
sequence:
- action: shell_command.ap_bz_on
- conditions: "{{ is_state('input_select.ap_bz_modus', 'auto') and is_state('binary_sensor.ap_bz_soll_an', 'off') }}"
sequence:
- action: shell_command.ap_bz_off
mode: single
Der homeassistant.start Trigger ist klein aber wichtig â nach HA-Restart wird der Zustand sofort wieder synchronisiert. Sonst könnte der AP in einem falschen Zustand âhĂ€ngenâ bleiben.
Schritt 4: Dashboard
Tile-Cards, einfach gehalten. Erste Reihe Modus + Status (je 6 Cols), dann pro Zeitfeld eine Card volle Breite:
type: heading
heading: WLAN AP BZ
icon: mdi:wifi-cog
# Reihe 1
type: tile
entity: input_select.ap_bz_modus
name: Modus
icon: mdi:wifi-cog
color: blue
grid_options: { columns: 6, rows: 1 }
type: tile
entity: binary_sensor.ap_bz_soll_an
name: Status
icon: mdi:wifi
color: primary
grid_options: { columns: 6, rows: 1 }
# Reihe 2-5: Zeitfelder, volle Breite
type: tile
entity: input_datetime.ap_bz_start_weekday
name: Mo-Fr Start
icon: mdi:wifi-arrow-up
grid_options: { columns: 12, rows: 1 }
# ... entsprechend fĂŒr end_weekday, start_weekend, end_weekend
Tap auf 'ne Zeit öffnet den Picker direkt. Praktisch.
Mehrere APs?
Klar â der Stack ist pro AP geklont, alles mit einem anderen Suffix:
shell_command:
ap_bz_off: "/usr/local/bin/unifi-ap-radio.py off U6+_BZ"
ap_bz_on: "/usr/local/bin/unifi-ap-radio.py on U6+_BZ"
ap_wz_off: "/usr/local/bin/unifi-ap-radio.py off U6+_WZ"
ap_wz_on: "/usr/local/bin/unifi-ap-radio.py on U6+_WZ"
Bei mir lĂ€uft das fĂŒr Wohnzimmer (Eltern-Schedule, abends spĂ€ter aus) und Budlizimmer (Kinder, frĂŒher aus). Funktioniert beides parallel ohne Probleme.
Stolperfallen die ich hatte
âAP nicht gefundenâ â Name im Controller checken, der muss byte-genau passen. Wenn U6+_BZ da steht, dann muss das Skript auch U6+_BZ aufrufen, nicht U6+ BZ oder U6+-BZ. Sonderzeichen sind erlaubt aber genau matchen.
âLogin fehlgeschlagenâ â fast immer der SSO-Account vs. Local-Admin-Account. Local-Only nochmal anlegen, andere Credentials in der ini.
AP wacht erst nach 30+ Sekunden auf â habe ich bei sehr alten AC-Pro mal gesehen. U6+ macht das in 5-7 Sek. Falls jemand das Problem hat: force_provision ĂŒber die Controller-UI löst das in der Regel.
Klienten hĂ€ngen âverbunden aber kein Internetâ â kann passieren wenn der AP der einzige im Bereich ist. Logisch. Bei uns ist im EG noch ein U6+ aktiv wĂ€hrend der OG-AP nachts schlĂ€ft.
HA-Restart und AP bleibt im falschen Zustand â genau dafĂŒr ist der homeassistant.start Trigger im Automation. Falls ihr den vergesst, kann der AP nach HA-Restart âhĂ€ngenâ. Bei mir hat das einmal zu 'nem Sonntag-Morgen âwarum geht das Internet nicht?!â gefĂŒhrt ![]()
Sicherheit
-
INI mit Klartextpasswort: chmod 600. Wer das File lesen kann, hat Zugriff aufs Controller-Konto
-
Der lokale Admin hat keinen Cloud-Zugriff â Schaden bei Leak ist auf LAN begrenzt
-
Skript lÀuft als der User der HA startet. Bei meinem DietPi-Setup ist das
root, daher Config in/root/.config/. Bei HA-OS oder HACS-Container mĂŒsst ihrpathlib.Path.home()evtl. an euren Pfad anpassen
Fazit
LĂ€uft bei mir seit ein paar Wochen ohne Probleme. Kinder haben sich an die âWLAN-Zeitenâ gewöhnt, ich hab Funkruhe im Schlafzimmer und kann jederzeit ĂŒberbrĂŒcken wenn ich will. WĂ€re froh ĂŒber Feedback, VerbesserungsvorschlĂ€ge, andere Lösungen â vor allem fĂŒr UDM/UDR User wĂ€re ein Kommentar mit dem angepassten Login-Pfad Gold wert. ![]()
Falls ihr Fragen habt, einfach reinhauen. Wennâs sich lohnt baue ich auch ein eigenes Repo daraus mit 'ner README.
Liebe GrĂŒĂe,
Setup-Stack bei mir
-
DietPi auf Raspberry Pi 5 (HA 2025.10.1 als Docker-loses Setup)
-
UniFi Network Application 8.x (Self-Hosted)
-
2Ă UniFi U6+ APs (Wohnzimmer + Kinderzimmer/Schlafzimmer)
-
Beide APs auf 2.4 GHz Channel 1 (Co-Channel-Deployment, falls jemand fragt â Zigbee+Thread sind die GrĂŒnde dafĂŒr, aber das ist ein anderer Thread
)
