Initiales Release: Busch-Radio Spotify Bridge v1.0.0
Stellt Spotify-Musik via Spotify Connect als HTTP-MP3-Stream bereit, den das Busch-Jäger Unterputz-Internetradio direkt abspielen kann. Komponenten: - ha-addon/: Home Assistant Add-on (librespot + ffmpeg + Icecast2) - custom_components/: Optionale HA Integration mit Media-Player-Entity - README.md: Vollständige Installations- und Konfigurationsanleitung - example-config.yaml: Kommentierte Beispielkonfiguration Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
22
.gitignore
vendored
Normal file
22
.gitignore
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*.pyo
|
||||
.pytest_cache/
|
||||
|
||||
# Docker
|
||||
.docker/
|
||||
|
||||
# Editor
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Secrets (niemals committen!)
|
||||
secrets.yaml
|
||||
.env
|
||||
359
README.md
Normal file
359
README.md
Normal file
@@ -0,0 +1,359 @@
|
||||
# Busch-Radio Spotify Bridge
|
||||
|
||||
Spotify-Musik auf dem Busch-Jäger Unterputz-Internetradio – ohne Bluetooth.
|
||||
|
||||
## Problem
|
||||
|
||||
Das **Busch-Jäger Unterputz-Internetradio** (z.B. 8217 U) kann nur vordefinierte oder eigene Internetradio-Streams abspielen. Es hat kein Bluetooth und keine Spotify-Integration. Spotify-Musik lässt sich damit direkt nicht wiedergeben.
|
||||
|
||||
## Lösung
|
||||
|
||||
Dieses Projekt enthält:
|
||||
|
||||
1. **Home Assistant Add-on** – erzeugt ein virtuelles **Spotify Connect-Gerät** im Netzwerk und stellt das Audio als HTTP-MP3-Stream bereit
|
||||
2. **Custom Component** (optional) – zeigt den Stream-Status als Media-Player-Entity in HA an
|
||||
|
||||
```
|
||||
Spotify-App
|
||||
│
|
||||
│ Spotify Connect (WiFi)
|
||||
▼
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ Home Assistant Add-on │
|
||||
│ │
|
||||
│ librespot ──(PCM)──▶ ffmpeg ──(MP3)──▶ Icecast │
|
||||
│ (Spotify Connect) (Kodierung) (HTTP-Server) │
|
||||
└─────────────────────────────────────────────┘
|
||||
│
|
||||
│ HTTP-Stream: http://ha-ip:8000/stream.mp3
|
||||
▼
|
||||
Busch-Jäger Radio
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Voraussetzungen
|
||||
|
||||
| Anforderung | Details |
|
||||
|---|---|
|
||||
| Home Assistant | OS, Supervised oder Container (Add-on benötigt Supervisor) |
|
||||
| Spotify Premium | Pflicht für Spotify Connect |
|
||||
| Busch-Jäger Radio | Muss benutzerdefinierte Stream-URLs unterstützen |
|
||||
| Netzwerk | HA und Smartphone im gleichen WLAN/LAN |
|
||||
|
||||
> **Wichtig:** Die kostenlose Version von Spotify unterstützt **kein** Spotify Connect. Ein Premium-Abo ist erforderlich.
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
### 1. Add-on installieren
|
||||
|
||||
#### Option A: Repository in HA hinzufügen
|
||||
|
||||
1. Navigiere zu **Einstellungen → Add-ons → Add-on Store**
|
||||
2. Klicke oben rechts auf **⋮ → Repositories**
|
||||
3. Trage die URL dieses Repositories ein
|
||||
4. Das Add-on **Busch-Radio Spotify Bridge** erscheint im Store
|
||||
5. Installiere es (Kompilierung von librespot dauert ~10-15 Minuten)
|
||||
|
||||
#### Option B: Manuell (für Entwickler)
|
||||
|
||||
```bash
|
||||
# Repository klonen
|
||||
git clone https://gitea.example.com/retr0/busch-radio-spotify.git
|
||||
|
||||
# ha-addon-Verzeichnis in HA Add-on-Pfad kopieren
|
||||
# (unter Home Assistant OS: /addons/busch_radio_spotify)
|
||||
cp -r busch-radio-spotify/ha-addon /addons/busch_radio_spotify
|
||||
|
||||
# In HA: Einstellungen → Add-ons → Add-on Store → ⋮ → Lokale Add-ons prüfen
|
||||
```
|
||||
|
||||
### 2. Add-on konfigurieren
|
||||
|
||||
Navigiere zu **Einstellungen → Add-ons → Busch-Radio Spotify Bridge → Konfiguration**:
|
||||
|
||||
```yaml
|
||||
device_name: "Busch-Radio" # Name in der Spotify-App
|
||||
bitrate: 320 # 96 / 160 / 320 kbps
|
||||
stream_port: 8000 # HTTP-Port des Streams
|
||||
stream_mount: "/stream.mp3" # URL-Pfad
|
||||
icecast_password: "geheim" # Sicherheitspasswort (bitte ändern!)
|
||||
username: "" # Spotify-Konto (optional, s.u.)
|
||||
password: "" # Spotify-Passwort (optional, s.u.)
|
||||
```
|
||||
|
||||
> **Hinweis zu `username`/`password`:** Diese Felder sind optional. Ohne Zugangsdaten
|
||||
> wird das Gerät über **Zeroconf/mDNS** automatisch im Netzwerk gefunden – das ist
|
||||
> die empfohlene Methode. Zugangsdaten werden nur benötigt, wenn mDNS im Netzwerk
|
||||
> blockiert ist.
|
||||
|
||||
### 3. Netzwerk-Port freigeben
|
||||
|
||||
Im Tab **Netzwerk** des Add-ons den Port `8000` auf `8000` mappen (oder einen anderen freien Port wählen).
|
||||
|
||||
### 4. Add-on starten
|
||||
|
||||
Im Tab **Info** auf **Starten** klicken. Die Logs zeigen:
|
||||
|
||||
```
|
||||
[INFO] Busch-Radio Spotify Bridge v1.0.0
|
||||
[INFO] Stream-URL für das Busch-Jäger Radio:
|
||||
[INFO] http://<homeassistant-ip>:8000/stream.mp3
|
||||
[INFO] Wähle 'Busch-Radio' in der Spotify-App als Abspielgerät.
|
||||
```
|
||||
|
||||
### 5. Stream im Busch-Jäger Radio eintragen
|
||||
|
||||
Im Radio-Menü (je nach Modell verschieden) eine neue Internetradio-Station anlegen:
|
||||
|
||||
```
|
||||
URL: http://<homeassistant-ip>:8000/stream.mp3
|
||||
```
|
||||
|
||||
Ersetze `<homeassistant-ip>` durch die tatsächliche IP-Adresse deines Home Assistant, z.B. `192.168.1.100`.
|
||||
|
||||
> **Tipp:** Die IP-Adresse findest du in HA unter **Einstellungen → System → Netzwerk**.
|
||||
|
||||
### 6. Optional: Custom Component installieren
|
||||
|
||||
Für die Status-Anzeige in HA:
|
||||
|
||||
```bash
|
||||
# custom_components-Verzeichnis in HA config kopieren
|
||||
cp -r custom_components/busch_radio_spotify \
|
||||
/config/custom_components/busch_radio_spotify
|
||||
|
||||
# HA neu starten
|
||||
```
|
||||
|
||||
Dann unter **Einstellungen → Integrationen → Integration hinzufügen → Busch-Radio Spotify Bridge** einrichten.
|
||||
|
||||
---
|
||||
|
||||
## Bedienung
|
||||
|
||||
1. Öffne die **Spotify-App** auf Smartphone oder Computer
|
||||
2. Spiele ein Lied ab
|
||||
3. Tippe auf das **Gerät-Symbol** (unten in der App)
|
||||
4. Wähle **"Busch-Radio"** (oder deinen konfigurierten Gerätenamen)
|
||||
5. Die Musik wird automatisch über den Stream an das Radio übertragen
|
||||
|
||||
> Das Radio muss die Stream-URL bereits abgespielt haben oder neu eingestellt werden,
|
||||
> sobald Spotify mit dem Abspielen beginnt.
|
||||
|
||||
---
|
||||
|
||||
## Einrichtung der Stream-URL im Radio (Modellabhängig)
|
||||
|
||||
### Busch-Jäger 8217 U / ähnliche Modelle
|
||||
|
||||
1. Ins Radio-Menü navigieren (meist über die Benutzeroberfläche oder App)
|
||||
2. **Meine Sender → Sender hinzufügen** oder **Internetradio → Eigene URL**
|
||||
3. URL eingeben: `http://192.168.1.100:8000/stream.mp3`
|
||||
4. Namen vergeben: z.B. "Spotify"
|
||||
5. Speichern und Stream starten
|
||||
|
||||
### Alternativ: Über vTuner/Frontier Silicon Portal
|
||||
|
||||
Einige Busch-Jäger Modelle nutzen vTuner als Radio-Backend:
|
||||
1. Auf `http://www.wifiradio-frontier.com` registrieren
|
||||
2. Unter "Meine Sender" → "Sender hinzufügen" die Stream-URL eintragen
|
||||
3. Im Radio synchronisieren
|
||||
|
||||
---
|
||||
|
||||
## Custom Component – Media Player Entity
|
||||
|
||||
Nach der Installation der Custom Component erscheint ein neuer Entity in HA:
|
||||
|
||||
**Entity-ID:** `media_player.busch_radio_192_168_1_100`
|
||||
|
||||
| State | Bedeutung |
|
||||
|---|---|
|
||||
| `playing` | Mindestens ein Hörer verbunden |
|
||||
| `idle` | Quelle aktiv, aber kein Hörer |
|
||||
| `off` | Icecast nicht erreichbar |
|
||||
|
||||
**Attribute:**
|
||||
|
||||
| Attribut | Beschreibung |
|
||||
|---|---|
|
||||
| `stream_url` | Die URL für das Radio |
|
||||
| `listeners` | Anzahl aktiver Hörer |
|
||||
| `source_connected` | Ob librespot an Icecast sendet |
|
||||
| `bitrate_kbps` | Aktuelle Bitrate |
|
||||
|
||||
**Dashboard-Karte (Beispiel):**
|
||||
|
||||
```yaml
|
||||
type: media-control
|
||||
entity: media_player.busch_radio_192_168_1_100
|
||||
```
|
||||
|
||||
**Automatisierung – Radio starten wenn Spotify beginnt:**
|
||||
|
||||
```yaml
|
||||
automation:
|
||||
alias: "Busch-Radio wenn Spotify spielt"
|
||||
trigger:
|
||||
platform: state
|
||||
entity_id: media_player.busch_radio_192_168_1_100
|
||||
to: "playing"
|
||||
action:
|
||||
service: notify.mobile_app
|
||||
data:
|
||||
message: "Spotify spielt auf dem Busch-Radio"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Technische Details
|
||||
|
||||
### Komponenten
|
||||
|
||||
| Komponente | Funktion | Version |
|
||||
|---|---|---|
|
||||
| [librespot](https://github.com/librespot-org/librespot) | Spotify Connect Client | Aktuell via cargo |
|
||||
| [ffmpeg](https://ffmpeg.org) | PCM → MP3 Transkodierung | Via Alpine apk |
|
||||
| [Icecast](https://icecast.org) | HTTP-Stream-Server | Via Alpine apk |
|
||||
|
||||
### Signalfluss im Detail
|
||||
|
||||
```
|
||||
Spotify CDN → (verschlüsselt, OGG Vorbis)
|
||||
→ librespot (dekodiert, gibt PCM aus)
|
||||
→ stdout (44100 Hz, S16LE, Stereo = 176.4 KB/s)
|
||||
→ ffmpeg stdin
|
||||
→ MP3-Kodierung (libmp3lame, 320 kbps ≈ 40 KB/s)
|
||||
→ Icecast (HTTP-Streaming-Server)
|
||||
→ Busch-Jäger Radio (HTTP-Client)
|
||||
```
|
||||
|
||||
### PCM-Format von librespot
|
||||
- Sample Rate: 44100 Hz
|
||||
- Bit-Tiefe: 16-bit signed, Little-Endian
|
||||
- Kanäle: 2 (Stereo)
|
||||
- Rohe Datenrate: ~172 KB/s
|
||||
|
||||
### Restart-Verhalten
|
||||
|
||||
Die Pipeline `librespot | ffmpeg` wird bei Absturz automatisch nach 5 Sekunden neu gestartet. Icecast bleibt davon unberührt.
|
||||
|
||||
---
|
||||
|
||||
## Fehlerbehebung
|
||||
|
||||
### Das Gerät erscheint nicht in der Spotify-App
|
||||
|
||||
**Ursachen & Lösungen:**
|
||||
|
||||
- **Add-on läuft nicht** → Add-on-Status in HA prüfen
|
||||
- **Anderes WLAN/Subnetz** → Smartphone und HA müssen im selben Netzwerk sein
|
||||
- **mDNS blockiert** → Spotify-Zugangsdaten in der Add-on-Konfig eintragen
|
||||
- **Gerätename bereits vergeben** → `device_name` in der Konfig ändern
|
||||
- **Firewall** → Port 5353 (mDNS/UDP) und Port 8000 (TCP) freigeben
|
||||
|
||||
### Kein Audio / Stream leer
|
||||
|
||||
```bash
|
||||
# Im Browser öffnen – sollte die Icecast-Statusseite zeigen:
|
||||
http://homeassistant-ip:8000
|
||||
|
||||
# Wenn das funktioniert, aber kein Audio:
|
||||
# → In Spotify das Gerät "Busch-Radio" auswählen und abspielen
|
||||
```
|
||||
|
||||
### Stream bricht ab beim Pausieren
|
||||
|
||||
Das ist das erwartete Verhalten: Wenn Spotify pausiert, sendet librespot nichts mehr an Icecast. Das Radio verliert die Verbindung.
|
||||
|
||||
**Lösung:** Nach dem Fortsetzen der Wiedergabe in Spotify den Stream im Radio kurz stoppen und wieder starten.
|
||||
|
||||
*Zukünftige Verbesserung: Stille-Generierung bei Pause geplant.*
|
||||
|
||||
### ffmpeg startet nicht / Kodierungsfehler
|
||||
|
||||
```bash
|
||||
# Add-on Logs prüfen auf Zeilen wie:
|
||||
[WARNING] [ffmpeg] ...
|
||||
|
||||
# Häufige Ursachen:
|
||||
# - libmp3lame nicht installiert (ffmpeg ohne MP3-Support)
|
||||
# - Icecast-Passwort stimmt nicht überein
|
||||
# - Port 8000 bereits belegt
|
||||
```
|
||||
|
||||
### Icecast antwortet mit 401 (in der Custom Component)
|
||||
|
||||
Das Passwort in der Custom Component-Konfiguration stimmt nicht mit `icecast_password` im Add-on überein. Integration neu einrichten.
|
||||
|
||||
---
|
||||
|
||||
## Rechtliche Hinweise
|
||||
|
||||
### librespot und Spotify ToS
|
||||
|
||||
- Dieses Projekt nutzt **librespot**, eine quelloffene Reimplementierung des Spotify-Clients
|
||||
- Librespot ist ein **Spotify Connect-Client** und kein Downloader/Recorder
|
||||
- Audio wird **nicht gespeichert** – nur live gestreamt
|
||||
- Die Nutzung im privaten Heimbereich ist technisch weitgehend akzeptiert, kann aber gegen **Abschnitt 2.5** von Spotifys Nutzungsbedingungen verstoßen
|
||||
- **Spotify Premium ist erforderlich** – kostenlose Konten funktionieren nicht
|
||||
- Dieses Projekt ist ausschließlich für den **persönlichen Privatgebrauch** gedacht
|
||||
|
||||
### Verwendete Open-Source-Lizenzen
|
||||
|
||||
- **librespot** – MIT License
|
||||
- **ffmpeg** – LGPL 2.1+ / GPL 2+
|
||||
- **Icecast** – GPL 2
|
||||
|
||||
---
|
||||
|
||||
## Projektstruktur
|
||||
|
||||
```
|
||||
busch-radio-spotify/
|
||||
├── ha-addon/ # Home Assistant Add-on
|
||||
│ ├── Dockerfile # Multi-Stage Build (Rust + Alpine)
|
||||
│ ├── config.yaml # Add-on Manifest & Optionen
|
||||
│ ├── build.yaml # Multi-Arch Build-Konfiguration
|
||||
│ └── run.sh # Startup-Script (Icecast + librespot + ffmpeg)
|
||||
│
|
||||
├── custom_components/ # Optionale HA Integration
|
||||
│ └── busch_radio_spotify/
|
||||
│ ├── __init__.py # Integration Setup
|
||||
│ ├── manifest.json # Integration Manifest
|
||||
│ ├── media_player.py # Media Player Entity (Icecast-Polling)
|
||||
│ ├── config_flow.py # Grafische Einrichtung
|
||||
│ ├── strings.json # UI-Texte
|
||||
│ └── translations/
|
||||
│ ├── de.json # Deutsch
|
||||
│ └── en.json # Englisch
|
||||
│
|
||||
├── example-config.yaml # Kommentierte Beispielkonfiguration
|
||||
└── README.md # Diese Datei
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Geplante Verbesserungen
|
||||
|
||||
- [ ] Stille-Generierung bei Spotify-Pause (verhindert Stream-Abbruch im Radio)
|
||||
- [ ] Lautstärke-Steuerung über HA (librespot unterstützt DBUS)
|
||||
- [ ] Track-Metadaten (Titel, Künstler) via librespot-Event-System
|
||||
- [ ] OGG-Stream Option für bessere Qualität bei gleicher Bitrate
|
||||
- [ ] Automatischer Start des Radios beim Verbinden (via HA-Automation)
|
||||
|
||||
---
|
||||
|
||||
## Beitragen
|
||||
|
||||
Issues und Pull Requests sind willkommen auf:
|
||||
`https://gitea.example.com/retr0/busch-radio-spotify`
|
||||
|
||||
---
|
||||
|
||||
## Lizenz
|
||||
|
||||
MIT License – siehe [LICENSE](LICENSE)
|
||||
47
custom_components/busch_radio_spotify/__init__.py
Normal file
47
custom_components/busch_radio_spotify/__init__.py
Normal file
@@ -0,0 +1,47 @@
|
||||
"""Busch-Radio Spotify Bridge – Home Assistant Custom Component.
|
||||
|
||||
Diese Integration stellt einen Media-Player-Entity bereit, der den Status
|
||||
des Icecast-Streams (vom Add-on) in der HA-Oberfläche anzeigt.
|
||||
|
||||
Voraussetzung: Das 'Busch-Radio Spotify Bridge' Add-on muss laufen.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = "busch_radio_spotify"
|
||||
PLATFORMS = ["media_player"]
|
||||
|
||||
CONF_HOST = "host"
|
||||
CONF_PORT = "port"
|
||||
CONF_MOUNT = "mount"
|
||||
CONF_PASSWORD = "password"
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Integration aus einem Config-Entry aufsetzen."""
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
hass.data[DOMAIN][entry.entry_id] = entry.data
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
_LOGGER.info(
|
||||
"Busch-Radio Spotify Bridge verbunden: %s:%s%s",
|
||||
entry.data[CONF_HOST],
|
||||
entry.data[CONF_PORT],
|
||||
entry.data[CONF_MOUNT],
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Config-Entry entladen."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(entry.entry_id, None)
|
||||
return unload_ok
|
||||
109
custom_components/busch_radio_spotify/config_flow.py
Normal file
109
custom_components/busch_radio_spotify/config_flow.py
Normal file
@@ -0,0 +1,109 @@
|
||||
"""Config Flow für die Busch-Radio Spotify Bridge Integration.
|
||||
|
||||
Führt den Nutzer durch die Einrichtung in der HA-Oberfläche.
|
||||
Fragt nach:
|
||||
- Hostname/IP des HA-Hosts (normalerweise 'localhost' wenn Add-on läuft)
|
||||
- Port (Standard: 8000)
|
||||
- Stream-Mount-Pfad (Standard: /stream.mp3)
|
||||
- Icecast Admin-Passwort (identisch mit 'icecast_password' in der Add-on Konfig)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
import aiohttp
|
||||
import voluptuous as vol
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from . import CONF_HOST, CONF_MOUNT, CONF_PASSWORD, CONF_PORT, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_HOST = "localhost"
|
||||
DEFAULT_PORT = 8000
|
||||
DEFAULT_MOUNT = "/stream.mp3"
|
||||
DEFAULT_PASSWORD = "busch-radio-geheim"
|
||||
|
||||
|
||||
async def _test_icecast_connection(
|
||||
hass: HomeAssistant, host: str, port: int, password: str
|
||||
) -> str | None:
|
||||
"""Verbindung zu Icecast testen.
|
||||
|
||||
Gibt None zurück wenn alles ok, sonst einen Fehlercode.
|
||||
"""
|
||||
session = async_get_clientsession(hass)
|
||||
url = f"http://{host}:{port}/admin/stats"
|
||||
auth = aiohttp.BasicAuth("admin", password)
|
||||
|
||||
try:
|
||||
async with session.get(
|
||||
url,
|
||||
auth=auth,
|
||||
timeout=aiohttp.ClientTimeout(total=5),
|
||||
) as resp:
|
||||
if resp.status == 200:
|
||||
return None
|
||||
if resp.status == 401:
|
||||
return "invalid_auth"
|
||||
return "cannot_connect"
|
||||
except aiohttp.ClientConnectorError:
|
||||
return "cannot_connect"
|
||||
except Exception:
|
||||
return "unknown"
|
||||
|
||||
|
||||
class BuschRadioSpotifyConfigFlow(
|
||||
config_entries.ConfigFlow, domain=DOMAIN
|
||||
):
|
||||
"""Config Flow für Busch-Radio Spotify Bridge."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict | None = None
|
||||
) -> config_entries.FlowResult:
|
||||
"""Ersten Einrichtungsschritt anzeigen."""
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
if user_input is not None:
|
||||
error = await _test_icecast_connection(
|
||||
self.hass,
|
||||
user_input[CONF_HOST],
|
||||
user_input[CONF_PORT],
|
||||
user_input[CONF_PASSWORD],
|
||||
)
|
||||
|
||||
if error is None:
|
||||
# Doppelte Einrichtung verhindern
|
||||
await self.async_set_unique_id(
|
||||
f"{user_input[CONF_HOST]}:{user_input[CONF_PORT]}"
|
||||
)
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
return self.async_create_entry(
|
||||
title=f"Busch-Radio ({user_input[CONF_HOST]}:{user_input[CONF_PORT]})",
|
||||
data=user_input,
|
||||
)
|
||||
else:
|
||||
errors["base"] = error
|
||||
|
||||
schema = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_HOST, default=DEFAULT_HOST): str,
|
||||
vol.Required(CONF_PORT, default=DEFAULT_PORT): vol.All(
|
||||
vol.Coerce(int), vol.Range(min=1, max=65535)
|
||||
),
|
||||
vol.Required(CONF_MOUNT, default=DEFAULT_MOUNT): str,
|
||||
vol.Required(CONF_PASSWORD, default=DEFAULT_PASSWORD): str,
|
||||
}
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=schema,
|
||||
errors=errors,
|
||||
)
|
||||
12
custom_components/busch_radio_spotify/manifest.json
Normal file
12
custom_components/busch_radio_spotify/manifest.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"domain": "busch_radio_spotify",
|
||||
"name": "Busch-Radio Spotify Bridge",
|
||||
"version": "1.0.0",
|
||||
"documentation": "https://gitea.example.com/retr0/busch-radio-spotify",
|
||||
"issue_tracker": "https://gitea.example.com/retr0/busch-radio-spotify/issues",
|
||||
"requirements": [],
|
||||
"dependencies": [],
|
||||
"codeowners": ["@retr0"],
|
||||
"iot_class": "local_polling",
|
||||
"config_flow": true
|
||||
}
|
||||
202
custom_components/busch_radio_spotify/media_player.py
Normal file
202
custom_components/busch_radio_spotify/media_player.py
Normal file
@@ -0,0 +1,202 @@
|
||||
"""Media-Player-Entity für den Busch-Radio Spotify Bridge Stream.
|
||||
|
||||
Pollt den Icecast-Admin-Endpoint alle 30 Sekunden und zeigt:
|
||||
- Ob der Stream aktiv ist (Quelle verbunden)
|
||||
- Anzahl der aktuellen Hörer
|
||||
- Die Stream-URL zum Eintragen ins Radio
|
||||
|
||||
Der Entity-Status entspricht dem HA MediaPlayerState:
|
||||
- PLAYING → mindestens ein Hörer verbunden
|
||||
- IDLE → Quelle aktiv, aber kein Hörer
|
||||
- OFF → Icecast nicht erreichbar / Add-on gestoppt
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import xml.etree.ElementTree as ET
|
||||
from datetime import timedelta
|
||||
|
||||
import aiohttp
|
||||
from homeassistant.components.media_player import (
|
||||
MediaPlayerEntity,
|
||||
MediaPlayerEntityFeature,
|
||||
MediaPlayerState,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import CONF_HOST, CONF_MOUNT, CONF_PASSWORD, CONF_PORT, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=30)
|
||||
REQUEST_TIMEOUT = aiohttp.ClientTimeout(total=5)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Media-Player-Entity aus Config-Entry anlegen."""
|
||||
async_add_entities([BuschRadioMediaPlayer(hass, entry)], update_before_add=True)
|
||||
|
||||
|
||||
class BuschRadioMediaPlayer(MediaPlayerEntity):
|
||||
"""Repräsentiert den Busch-Radio Spotify Bridge Stream.
|
||||
|
||||
Zeigt den Icecast-Stream-Status in der HA-Oberfläche an.
|
||||
Die eigentliche Wiedergabe wird über die Spotify-App gesteuert.
|
||||
"""
|
||||
|
||||
_attr_icon = "mdi:radio"
|
||||
_attr_has_entity_name = False
|
||||
_attr_should_poll = True
|
||||
|
||||
# Dieser Player hat keine eigenen Steuer-Features (Spotify-App übernimmt das),
|
||||
# aber wir exportieren PLAY/PAUSE als Dummy damit HA ihn als Media-Player erkennt.
|
||||
_attr_supported_features = MediaPlayerEntityFeature(0)
|
||||
|
||||
def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
self._hass = hass
|
||||
self._entry = entry
|
||||
self._host: str = entry.data[CONF_HOST]
|
||||
self._port: int = entry.data[CONF_PORT]
|
||||
self._mount: str = entry.data[CONF_MOUNT]
|
||||
self._password: str = entry.data[CONF_PASSWORD]
|
||||
|
||||
self._listeners: int = 0
|
||||
self._source_connected: bool = False
|
||||
self._available: bool = False
|
||||
self._bitrate: str | None = None
|
||||
self._audio_codec: str | None = None
|
||||
|
||||
self._attr_name = f"Busch-Radio ({self._host})"
|
||||
self._attr_unique_id = f"busch_radio_{self._host}_{self._port}"
|
||||
|
||||
# ── Properties ───────────────────────────────────────────────────────────
|
||||
|
||||
@property
|
||||
def stream_url(self) -> str:
|
||||
"""Öffentliche Stream-URL."""
|
||||
return f"http://{self._host}:{self._port}{self._mount}"
|
||||
|
||||
@property
|
||||
def state(self) -> MediaPlayerState:
|
||||
if not self._available:
|
||||
return MediaPlayerState.OFF
|
||||
if not self._source_connected:
|
||||
return MediaPlayerState.IDLE
|
||||
if self._listeners > 0:
|
||||
return MediaPlayerState.PLAYING
|
||||
return MediaPlayerState.IDLE
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
return True # Entity immer verfügbar; Status spiegelt Icecast-Zustand
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict:
|
||||
return {
|
||||
"stream_url": self.stream_url,
|
||||
"listeners": self._listeners,
|
||||
"source_connected": self._source_connected,
|
||||
"bitrate_kbps": self._bitrate,
|
||||
"codec": self._audio_codec,
|
||||
"icecast_status_url": f"http://{self._host}:{self._port}",
|
||||
}
|
||||
|
||||
@property
|
||||
def media_title(self) -> str | None:
|
||||
if self._source_connected:
|
||||
return "Spotify"
|
||||
return None
|
||||
|
||||
@property
|
||||
def media_artist(self) -> str | None:
|
||||
if self._listeners > 0:
|
||||
return f"{self._listeners} Hörer"
|
||||
return None
|
||||
|
||||
# ── Update ───────────────────────────────────────────────────────────────
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Icecast-Stats abfragen und Entity-Status aktualisieren."""
|
||||
session = async_get_clientsession(self._hass)
|
||||
stats_url = f"http://{self._host}:{self._port}/admin/stats"
|
||||
auth = aiohttp.BasicAuth("admin", self._password)
|
||||
|
||||
try:
|
||||
async with session.get(
|
||||
stats_url, auth=auth, timeout=REQUEST_TIMEOUT
|
||||
) as resp:
|
||||
if resp.status == 200:
|
||||
text = await resp.text()
|
||||
self._parse_icecast_stats(text)
|
||||
self._available = True
|
||||
elif resp.status == 401:
|
||||
_LOGGER.warning(
|
||||
"Icecast: Authentifizierung fehlgeschlagen. "
|
||||
"Passwort in der Integration prüfen."
|
||||
)
|
||||
self._available = False
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
"Icecast-Stats: HTTP %d von %s", resp.status, stats_url
|
||||
)
|
||||
self._available = False
|
||||
|
||||
except aiohttp.ClientConnectorError:
|
||||
_LOGGER.debug(
|
||||
"Icecast nicht erreichbar unter %s:%d (Add-on läuft?)",
|
||||
self._host,
|
||||
self._port,
|
||||
)
|
||||
self._available = False
|
||||
self._source_connected = False
|
||||
self._listeners = 0
|
||||
|
||||
except Exception as exc:
|
||||
_LOGGER.warning("Fehler beim Abrufen der Icecast-Stats: %s", exc)
|
||||
self._available = False
|
||||
|
||||
def _parse_icecast_stats(self, xml_text: str) -> None:
|
||||
"""Icecast-XML-Statistiken parsen."""
|
||||
try:
|
||||
root = ET.fromstring(xml_text)
|
||||
except ET.ParseError as exc:
|
||||
_LOGGER.warning("Icecast-XML konnte nicht geparst werden: %s", exc)
|
||||
return
|
||||
|
||||
# Suche nach unserem Mount-Point
|
||||
self._source_connected = False
|
||||
self._listeners = 0
|
||||
self._bitrate = None
|
||||
self._audio_codec = None
|
||||
|
||||
for source in root.findall("source"):
|
||||
mount = source.get("mount", "")
|
||||
if mount != self._mount:
|
||||
continue
|
||||
|
||||
self._source_connected = True
|
||||
|
||||
listeners_el = source.find("listeners")
|
||||
if listeners_el is not None and listeners_el.text:
|
||||
try:
|
||||
self._listeners = int(listeners_el.text)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
bitrate_el = source.find("bitrate")
|
||||
if bitrate_el is not None:
|
||||
self._bitrate = bitrate_el.text
|
||||
|
||||
codec_el = source.find("audio_codecname")
|
||||
if codec_el is not None:
|
||||
self._audio_codec = codec_el.text
|
||||
|
||||
break
|
||||
24
custom_components/busch_radio_spotify/strings.json
Normal file
24
custom_components/busch_radio_spotify/strings.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Busch-Radio Spotify Bridge",
|
||||
"description": "Verbinde die Integration mit dem laufenden Busch-Radio Add-on.\nDas Passwort muss mit dem 'icecast_password' in der Add-on Konfiguration übereinstimmen.",
|
||||
"data": {
|
||||
"host": "Hostname oder IP-Adresse",
|
||||
"port": "Icecast-Port",
|
||||
"mount": "Stream-Mount-Pfad",
|
||||
"password": "Icecast Admin-Passwort"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Verbindung zu Icecast fehlgeschlagen. Läuft das Add-on?",
|
||||
"invalid_auth": "Falsches Passwort. Mit 'icecast_password' in der Add-on Konfig vergleichen.",
|
||||
"unknown": "Unbekannter Fehler. Prüfe die Logs."
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Diese Icecast-Instanz ist bereits konfiguriert."
|
||||
}
|
||||
}
|
||||
}
|
||||
24
custom_components/busch_radio_spotify/translations/de.json
Normal file
24
custom_components/busch_radio_spotify/translations/de.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Busch-Radio Spotify Bridge",
|
||||
"description": "Verbinde die Integration mit dem laufenden Busch-Radio Add-on.\nDas Passwort muss mit dem 'icecast_password' in der Add-on Konfiguration übereinstimmen.",
|
||||
"data": {
|
||||
"host": "Hostname oder IP-Adresse",
|
||||
"port": "Icecast-Port",
|
||||
"mount": "Stream-Mount-Pfad",
|
||||
"password": "Icecast Admin-Passwort"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Verbindung zu Icecast fehlgeschlagen. Läuft das Add-on?",
|
||||
"invalid_auth": "Falsches Passwort. Mit 'icecast_password' in der Add-on Konfig vergleichen.",
|
||||
"unknown": "Unbekannter Fehler. Prüfe die Logs."
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Diese Icecast-Instanz ist bereits konfiguriert."
|
||||
}
|
||||
}
|
||||
}
|
||||
24
custom_components/busch_radio_spotify/translations/en.json
Normal file
24
custom_components/busch_radio_spotify/translations/en.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Busch-Radio Spotify Bridge",
|
||||
"description": "Connect the integration to the running Busch-Radio add-on.\nThe password must match 'icecast_password' in the add-on configuration.",
|
||||
"data": {
|
||||
"host": "Hostname or IP address",
|
||||
"port": "Icecast port",
|
||||
"mount": "Stream mount path",
|
||||
"password": "Icecast admin password"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Failed to connect to Icecast. Is the add-on running?",
|
||||
"invalid_auth": "Wrong password. Compare with 'icecast_password' in the add-on config.",
|
||||
"unknown": "Unknown error. Check the logs."
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "This Icecast instance is already configured."
|
||||
}
|
||||
}
|
||||
}
|
||||
43
example-config.yaml
Normal file
43
example-config.yaml
Normal file
@@ -0,0 +1,43 @@
|
||||
# ================================================================
|
||||
# Beispielkonfiguration: Busch-Radio Spotify Bridge Add-on
|
||||
# ================================================================
|
||||
#
|
||||
# Diese Optionen werden in der HA Add-on Oberfläche eingetragen
|
||||
# (Einstellungen → Add-ons → Busch-Radio Spotify Bridge → Konfiguration).
|
||||
#
|
||||
# Alternativ direkt in der YAML-Ansicht.
|
||||
# ================================================================
|
||||
|
||||
# Name des virtuellen Spotify-Connect-Geräts.
|
||||
# Erscheint in der Spotify-App unter "Geräte".
|
||||
device_name: "Busch-Radio"
|
||||
|
||||
# Audio-Bitrate in kbps. Höhere Werte = bessere Qualität, mehr Bandbreite.
|
||||
# Erlaubte Werte: 96, 160, 320
|
||||
bitrate: 320
|
||||
|
||||
# Port auf dem der Icecast-Stream lauscht.
|
||||
# Dieser Port muss im Add-on unter "Netzwerk" freigegeben sein.
|
||||
stream_port: 8000
|
||||
|
||||
# Pfad des Streams. Wird an die URL angehängt.
|
||||
# Empfehlung: mit .mp3 enden für maximale Kompatibilität.
|
||||
stream_mount: "/stream.mp3"
|
||||
|
||||
# Passwort für Icecast (intern verwendet, nicht für Hörer sichtbar).
|
||||
# Ändere diesen Wert aus Sicherheitsgründen!
|
||||
icecast_password: "mein-sicheres-passwort"
|
||||
|
||||
# Optional: Spotify Premium Zugangsdaten.
|
||||
# Wenn leer, wird Zeroconf/mDNS-Erkennung verwendet (empfohlen).
|
||||
# Das Gerät wird dann automatisch in der Spotify-App im lokalen Netz gefunden.
|
||||
username: ""
|
||||
password: ""
|
||||
|
||||
# ================================================================
|
||||
# Stream-URL für das Busch-Jäger Radio:
|
||||
# http://<homeassistant-ip>:8000/stream.mp3
|
||||
#
|
||||
# Beispiel:
|
||||
# http://192.168.1.100:8000/stream.mp3
|
||||
# ================================================================
|
||||
49
ha-addon/Dockerfile
Normal file
49
ha-addon/Dockerfile
Normal file
@@ -0,0 +1,49 @@
|
||||
ARG BUILD_FROM
|
||||
|
||||
# ============================================================
|
||||
# Build stage: Librespot aus Rust-Quellcode kompilieren
|
||||
# ============================================================
|
||||
FROM rust:alpine AS librespot-builder
|
||||
|
||||
RUN apk add --no-cache \
|
||||
musl-dev \
|
||||
pkgconfig \
|
||||
openssl-dev \
|
||||
openssl-libs-static
|
||||
|
||||
# Librespot ohne Hardware-Audio-Backends kompilieren.
|
||||
# --no-default-features entfernt ALSA/PulseAudio; der Pipe-Backend
|
||||
# ist immer verfügbar und reicht für diesen Anwendungsfall.
|
||||
RUN cargo install librespot \
|
||||
--no-default-features \
|
||||
--root /install
|
||||
|
||||
# ============================================================
|
||||
# Runtime stage: HA-Basis-Image + ffmpeg + icecast
|
||||
# ============================================================
|
||||
FROM ${BUILD_FROM}
|
||||
|
||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||
|
||||
# Abhängigkeiten installieren
|
||||
# icecast liegt in Alpine's Community-Repository
|
||||
RUN apk add --no-cache \
|
||||
bash \
|
||||
jq \
|
||||
curl \
|
||||
python3 \
|
||||
ffmpeg \
|
||||
&& apk add --no-cache \
|
||||
--repository=https://dl-cdn.alpinelinux.org/alpine/edge/community \
|
||||
icecast \
|
||||
|| apk add --no-cache icecast
|
||||
|
||||
# Librespot-Binary aus dem Build-Stage übernehmen
|
||||
COPY --from=librespot-builder /install/bin/librespot /usr/local/bin/librespot
|
||||
RUN chmod +x /usr/local/bin/librespot
|
||||
|
||||
# Add-on Dateien kopieren
|
||||
COPY run.sh /run.sh
|
||||
RUN chmod a+x /run.sh
|
||||
|
||||
CMD ["/run.sh"]
|
||||
11
ha-addon/build.yaml
Normal file
11
ha-addon/build.yaml
Normal file
@@ -0,0 +1,11 @@
|
||||
---
|
||||
# Multi-Architektur Build-Konfiguration für das HA Add-on.
|
||||
# Jede Architektur bekommt das passende Alpine-basierte HA-Basisimage.
|
||||
build_from:
|
||||
aarch64: ghcr.io/home-assistant/aarch64-base:latest
|
||||
amd64: ghcr.io/home-assistant/amd64-base:latest
|
||||
armhf: ghcr.io/home-assistant/armhf-base:latest
|
||||
armv7: ghcr.io/home-assistant/armv7-base:latest
|
||||
|
||||
args:
|
||||
BUILD_FROM: ""
|
||||
51
ha-addon/config.yaml
Normal file
51
ha-addon/config.yaml
Normal file
@@ -0,0 +1,51 @@
|
||||
---
|
||||
name: "Busch-Radio Spotify Bridge"
|
||||
version: "1.0.0"
|
||||
slug: busch_radio_spotify
|
||||
description: >
|
||||
Streamt Spotify-Musik als Internet-Radio-Stream für das Busch-Jäger Unterputz-Internetradio.
|
||||
Das Add-on erzeugt ein virtuelles Spotify Connect-Gerät (via librespot) und stellt das Audio
|
||||
als MP3-Stream über Icecast bereit.
|
||||
url: "https://gitea.example.com/retr0/busch-radio-spotify"
|
||||
arch:
|
||||
- aarch64
|
||||
- amd64
|
||||
- armhf
|
||||
- armv7
|
||||
init: false
|
||||
homeassistant: "2023.1.0"
|
||||
|
||||
# ---- Konfigurationsoptionen ----
|
||||
options:
|
||||
device_name: "Busch-Radio"
|
||||
bitrate: 320
|
||||
stream_port: 8000
|
||||
stream_mount: "/stream.mp3"
|
||||
icecast_password: "busch-radio-geheim"
|
||||
username: ""
|
||||
password: ""
|
||||
|
||||
schema:
|
||||
device_name: str
|
||||
bitrate: "list(96|160|320)"
|
||||
stream_port: port
|
||||
stream_mount: str
|
||||
icecast_password: str
|
||||
username: str?
|
||||
password: password?
|
||||
|
||||
# ---- Netzwerk ----
|
||||
ports:
|
||||
"8000/tcp": 8000
|
||||
|
||||
ports_description:
|
||||
"8000/tcp": "Icecast HTTP-Stream (für das Radio)"
|
||||
|
||||
# ---- UI ----
|
||||
panel_icon: mdi:radio
|
||||
panel_title: "Busch-Radio"
|
||||
|
||||
# ---- Sonstiges ----
|
||||
map:
|
||||
- config:rw
|
||||
- share:rw
|
||||
209
ha-addon/run.sh
Normal file
209
ha-addon/run.sh
Normal file
@@ -0,0 +1,209 @@
|
||||
#!/usr/bin/with-contenv bashio
|
||||
# ==============================================================================
|
||||
# Busch-Radio Spotify Bridge – Startup-Script
|
||||
#
|
||||
# Startet:
|
||||
# 1. Icecast2 – HTTP-Stream-Server
|
||||
# 2. librespot – Virtueller Spotify Connect-Empfänger (gibt PCM auf stdout aus)
|
||||
# 3. ffmpeg – Kodiert PCM → MP3 und schickt es als Icecast-Quelle
|
||||
#
|
||||
# Bei Absturz wird die komplette Pipeline nach 5 Sekunden neu gestartet.
|
||||
# ==============================================================================
|
||||
|
||||
set -u
|
||||
|
||||
# ── Konfiguration lesen ───────────────────────────────────────────────────────
|
||||
DEVICE_NAME=$(bashio::config 'device_name')
|
||||
BITRATE=$(bashio::config 'bitrate')
|
||||
STREAM_PORT=$(bashio::config 'stream_port')
|
||||
STREAM_MOUNT=$(bashio::config 'stream_mount')
|
||||
ICECAST_PASSWORD=$(bashio::config 'icecast_password')
|
||||
|
||||
bashio::log.info "╔══════════════════════════════════════════════╗"
|
||||
bashio::log.info "║ Busch-Radio Spotify Bridge v1.0.0 ║"
|
||||
bashio::log.info "╚══════════════════════════════════════════════╝"
|
||||
bashio::log.info "Gerätename : ${DEVICE_NAME}"
|
||||
bashio::log.info "Bitrate : ${BITRATE} kbps"
|
||||
bashio::log.info "Stream-Port : ${STREAM_PORT}"
|
||||
bashio::log.info "Stream-Pfad : ${STREAM_MOUNT}"
|
||||
|
||||
# ── Verzeichnisse anlegen ─────────────────────────────────────────────────────
|
||||
mkdir -p /var/log/icecast /run/icecast /tmp/busch-radio
|
||||
|
||||
# ── Icecast-Webroot ermitteln ─────────────────────────────────────────────────
|
||||
# Alpine und Debian legen Icecast in unterschiedliche Pfade
|
||||
ICECAST_WEBROOT=""
|
||||
for candidate in /usr/share/icecast /usr/share/icecast2; do
|
||||
if [ -d "$candidate" ]; then
|
||||
ICECAST_WEBROOT="$candidate"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$ICECAST_WEBROOT" ]; then
|
||||
bashio::log.warning "Icecast-Webroot nicht gefunden, verwende /usr/share/icecast"
|
||||
ICECAST_WEBROOT="/usr/share/icecast"
|
||||
mkdir -p "${ICECAST_WEBROOT}/web" "${ICECAST_WEBROOT}/admin"
|
||||
fi
|
||||
|
||||
bashio::log.debug "Icecast-Webroot: ${ICECAST_WEBROOT}"
|
||||
|
||||
# ── Icecast-Konfiguration generieren ─────────────────────────────────────────
|
||||
bashio::log.info "Icecast-Konfiguration wird erstellt..."
|
||||
|
||||
cat > /tmp/busch-radio/icecast.xml <<ICECAST_EOF
|
||||
<icecast>
|
||||
<location>Home Assistant</location>
|
||||
<admin>admin@localhost</admin>
|
||||
|
||||
<limits>
|
||||
<clients>10</clients>
|
||||
<sources>2</sources>
|
||||
<queue-size>524288</queue-size>
|
||||
<client-timeout>30</client-timeout>
|
||||
<header-timeout>15</header-timeout>
|
||||
<source-timeout>10</source-timeout>
|
||||
<burst-on-connect>0</burst-on-connect>
|
||||
<burst-size>65536</burst-size>
|
||||
</limits>
|
||||
|
||||
<authentication>
|
||||
<source-password>${ICECAST_PASSWORD}</source-password>
|
||||
<relay-password>${ICECAST_PASSWORD}</relay-password>
|
||||
<admin-user>admin</admin-user>
|
||||
<admin-password>${ICECAST_PASSWORD}</admin-password>
|
||||
</authentication>
|
||||
|
||||
<hostname>0.0.0.0</hostname>
|
||||
|
||||
<listen-socket>
|
||||
<port>${STREAM_PORT}</port>
|
||||
</listen-socket>
|
||||
|
||||
<mount>
|
||||
<mount-name>${STREAM_MOUNT}</mount-name>
|
||||
<public>0</public>
|
||||
<stream-name>Busch-Radio Spotify Bridge</stream-name>
|
||||
<stream-description>Spotify via Home Assistant</stream-description>
|
||||
<bitrate>${BITRATE}</bitrate>
|
||||
<type>audio/mpeg</type>
|
||||
</mount>
|
||||
|
||||
<fileserve>1</fileserve>
|
||||
|
||||
<paths>
|
||||
<basedir>${ICECAST_WEBROOT}</basedir>
|
||||
<logdir>/var/log/icecast</logdir>
|
||||
<webroot>${ICECAST_WEBROOT}/web</webroot>
|
||||
<adminroot>${ICECAST_WEBROOT}/admin</adminroot>
|
||||
<pidfile>/run/icecast/icecast.pid</pidfile>
|
||||
</paths>
|
||||
|
||||
<logging>
|
||||
<!-- "-" loggt auf stderr → wird von HA erfasst -->
|
||||
<accesslog>-</accesslog>
|
||||
<errorlog>-</errorlog>
|
||||
<loglevel>2</loglevel>
|
||||
<logsize>10000</logsize>
|
||||
</logging>
|
||||
|
||||
<security>
|
||||
<chroot>0</chroot>
|
||||
</security>
|
||||
</icecast>
|
||||
ICECAST_EOF
|
||||
|
||||
# ── Icecast starten ───────────────────────────────────────────────────────────
|
||||
bashio::log.info "Icecast wird gestartet (Port: ${STREAM_PORT})..."
|
||||
icecast -c /tmp/busch-radio/icecast.xml &
|
||||
ICECAST_PID=$!
|
||||
|
||||
# Kurz warten bis Icecast hochgefahren ist
|
||||
sleep 3
|
||||
|
||||
if ! kill -0 "${ICECAST_PID}" 2>/dev/null; then
|
||||
bashio::log.fatal "Icecast konnte nicht gestartet werden! Prüfe die Konfiguration."
|
||||
exit 1
|
||||
fi
|
||||
bashio::log.info "Icecast läuft (PID: ${ICECAST_PID})"
|
||||
|
||||
# ── Librespot-Argumente zusammenstellen ───────────────────────────────────────
|
||||
LIBRESPOT_ARGS=(
|
||||
"--name" "${DEVICE_NAME}"
|
||||
"--bitrate" "${BITRATE}"
|
||||
"--backend" "pipe"
|
||||
"--disable-audio-cache"
|
||||
"--initial-volume" "100"
|
||||
)
|
||||
|
||||
# Optionale Spotify-Zugangsdaten
|
||||
SP_USER=$(bashio::config 'username')
|
||||
SP_PASS=$(bashio::config 'password')
|
||||
|
||||
if [ -n "${SP_USER}" ]; then
|
||||
LIBRESPOT_ARGS+=("--username" "${SP_USER}")
|
||||
bashio::log.info "Spotify-Konto: ${SP_USER}"
|
||||
else
|
||||
bashio::log.info "Kein Spotify-Konto konfiguriert (Zeroconf/mDNS-Erkennung aktiv)"
|
||||
fi
|
||||
|
||||
if [ -n "${SP_PASS}" ]; then
|
||||
LIBRESPOT_ARGS+=("--password" "${SP_PASS}")
|
||||
fi
|
||||
|
||||
# ── Icecast-Source-URL ────────────────────────────────────────────────────────
|
||||
ICECAST_SOURCE_URL="icecast://source:${ICECAST_PASSWORD}@localhost:${STREAM_PORT}${STREAM_MOUNT}"
|
||||
|
||||
# ── Stream-URL ausgeben ───────────────────────────────────────────────────────
|
||||
bashio::log.info "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
bashio::log.info "Stream-URL für das Busch-Jäger Radio:"
|
||||
bashio::log.info " http://<homeassistant-ip>:${STREAM_PORT}${STREAM_MOUNT}"
|
||||
bashio::log.info ""
|
||||
bashio::log.info "Icecast-Statusseite:"
|
||||
bashio::log.info " http://<homeassistant-ip>:${STREAM_PORT}"
|
||||
bashio::log.info "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
bashio::log.info "Wähle '${DEVICE_NAME}' in der Spotify-App als Abspielgerät."
|
||||
bashio::log.info "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
|
||||
# ── Aufräumen bei SIGTERM ─────────────────────────────────────────────────────
|
||||
cleanup() {
|
||||
bashio::log.info "Add-on wird beendet..."
|
||||
kill "${ICECAST_PID}" 2>/dev/null || true
|
||||
exit 0
|
||||
}
|
||||
trap cleanup SIGTERM SIGINT
|
||||
|
||||
# ── Haupt-Schleife: Pipeline starten und bei Absturz neu starten ──────────────
|
||||
#
|
||||
# Signalfluss:
|
||||
# librespot (Spotify Connect) → stdout (raw PCM 44100Hz/S16LE/Stereo)
|
||||
# ↓
|
||||
# ffmpeg (PCM → MP3 Encoding) → Icecast (HTTP-Stream)
|
||||
# ↓
|
||||
# Busch-Jäger Radio (HTTP-Client)
|
||||
#
|
||||
# Wenn entweder librespot oder ffmpeg abstürzt, wird die komplette Pipeline
|
||||
# nach RESTART_DELAY Sekunden neu gestartet.
|
||||
RESTART_DELAY=5
|
||||
ATTEMPT=0
|
||||
|
||||
while true; do
|
||||
ATTEMPT=$((ATTEMPT + 1))
|
||||
bashio::log.info "Pipeline-Start (Versuch ${ATTEMPT})..."
|
||||
|
||||
librespot "${LIBRESPOT_ARGS[@]}" \
|
||||
| ffmpeg \
|
||||
-hide_banner \
|
||||
-loglevel warning \
|
||||
-f s16le -ar 44100 -ac 2 \
|
||||
-i pipe:0 \
|
||||
-codec:a libmp3lame \
|
||||
-b:a "${BITRATE}k" \
|
||||
-q:a 2 \
|
||||
-f mp3 \
|
||||
"${ICECAST_SOURCE_URL}" \
|
||||
|| true
|
||||
|
||||
bashio::log.warning "Pipeline beendet. Neustart in ${RESTART_DELAY}s..."
|
||||
sleep "${RESTART_DELAY}"
|
||||
done
|
||||
Reference in New Issue
Block a user