Home Assitant Integration
This commit is contained in:
@@ -1,53 +1,326 @@
|
|||||||
# RelayTV Panel (Home Assistant)
|
# RelayTV – Home Assistant Integration
|
||||||
|
|
||||||
A minimal Home Assistant integration that adds a **RelayTV** sidebar panel (iframe) pointing at your RelayTV instance.
|
RelayTV transforms Home Assistant into a **programmable local media automation engine**.
|
||||||
|
|
||||||
- **Domain:** `relaytv`
|
It exposes your RelayTV server as a `media_player` (plus services), supports queue-based playback, and makes it easy to drive **local HDMI playback** from automations (doorbells, sensors, schedules, scenes, and AI).
|
||||||
- **Type:** Sidebar panel (iframe)
|
|
||||||
- **Entities:** None (RelayTV remains the control surface)
|
|
||||||
|
|
||||||
## Install via HACS (Custom Repository)
|
> Tip: The YAML below uses the `relaytv.*` service names as examples. If your integration exposes slightly different names/fields, keep the **automation patterns** and adjust the service calls accordingly.
|
||||||
|
|
||||||
1. In Home Assistant, go to **HACS → Integrations**
|
---
|
||||||
2. Open the menu (⋮) → **Custom repositories**
|
|
||||||
3. Add this repository URL, category **Integration**
|
|
||||||
4. Install **RelayTV Panel**
|
|
||||||
5. Restart Home Assistant
|
|
||||||
6. Add the integration: **Settings → Devices & Services → Add Integration → RelayTV Panel**
|
|
||||||
7. Enter your RelayTV base URL (example: `http://relaytv-host:8787`)
|
|
||||||
|
|
||||||
## Manual Install
|
# 🚀 What RelayTV Enables Today
|
||||||
|
|
||||||
|
## 🎬 Media Player Control
|
||||||
|
- `media_player` entity support
|
||||||
|
- Play / pause / stop / seek / volume
|
||||||
|
- Queue management
|
||||||
|
- Resume playback support (where implemented)
|
||||||
|
- Local HDMI playback
|
||||||
|
- yt-dlp powered “smart URL” stream resolution
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🔵 Practical Automation Use Cases (Available Now)
|
||||||
|
|
||||||
|
- 🚪 Doorbell / camera pop on the TV
|
||||||
|
- ⏰ Morning video alarm clock
|
||||||
|
- 🌙 Bedtime routines (relaxing queue + auto stop)
|
||||||
|
- 🏠 Home/Away/Movie mode scenes that change what’s on screen
|
||||||
|
- 📺 Queue-based “experiences” (workouts, parties, timers, intros)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🟢 Creative & Fun Automation Ideas
|
||||||
|
|
||||||
|
- 👻 Late-night motion jump scares
|
||||||
|
- 🧟 Halloween mode (seasonal triggers + ambient loops)
|
||||||
|
- 🐶 Dog cam on bark detection
|
||||||
|
- 🎉 Birthday surprise mode
|
||||||
|
- 🎮 Easter-egg triggers (sensor patterns launch hidden clips)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🔴 Advanced Home Automation Scenarios
|
||||||
|
|
||||||
|
- 🤖 AI-powered announcements (TTS + relevant clip)
|
||||||
|
- 🌧 Weather-based media (radar, warnings, checklist videos)
|
||||||
|
- 📈 Energy monitoring visualizations (play alert/graph clip)
|
||||||
|
- 🛡 “Command center” camera rotation on alarm events
|
||||||
|
- 📰 AI daily digest queue (headlines + summaries)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🧠 Core Services (Examples)
|
||||||
|
|
||||||
|
| Service | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `relaytv.play_now` | Immediately plays a URL |
|
||||||
|
| `relaytv.enqueue` | Adds URL to queue |
|
||||||
|
| `relaytv.smart_url` | Resolves URL via yt-dlp (when applicable) |
|
||||||
|
| `relaytv.clear_queue` | Clears current queue |
|
||||||
|
| `relaytv.seek` | Seek to a time position |
|
||||||
|
| `relaytv.volume_set` | Adjust volume |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# ✅ Automation Examples (YAML)
|
||||||
|
|
||||||
|
## 1) Doorbell → Show Front Door Camera (RTSP)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
alias: RelayTV - Doorbell Camera Pop
|
||||||
|
mode: single
|
||||||
|
trigger:
|
||||||
|
- platform: state
|
||||||
|
entity_id: binary_sensor.front_door_ding
|
||||||
|
to: "on"
|
||||||
|
action:
|
||||||
|
- service: relaytv.play_now
|
||||||
|
target:
|
||||||
|
entity_id: media_player.relaytv_living_room
|
||||||
|
data:
|
||||||
|
url: "rtsp://user:pass@10.0.55.10:554/stream1"
|
||||||
|
- service: relaytv.volume_set
|
||||||
|
target:
|
||||||
|
entity_id: media_player.relaytv_living_room
|
||||||
|
data:
|
||||||
|
volume_level: 0.55
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2) Morning Video Alarm Clock (Playlist + Lights)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
alias: RelayTV - Morning Video Alarm
|
||||||
|
mode: single
|
||||||
|
trigger:
|
||||||
|
- platform: time
|
||||||
|
at: "06:45:00"
|
||||||
|
condition:
|
||||||
|
- condition: state
|
||||||
|
entity_id: person.mark
|
||||||
|
state: "home"
|
||||||
|
action:
|
||||||
|
- service: light.turn_on
|
||||||
|
target:
|
||||||
|
entity_id: light.bedroom
|
||||||
|
data:
|
||||||
|
brightness_pct: 10
|
||||||
|
- delay: "00:00:05"
|
||||||
|
- service: relaytv.play_now
|
||||||
|
target:
|
||||||
|
entity_id: media_player.relaytv_bedroom
|
||||||
|
data:
|
||||||
|
url: "https://www.youtube.com/playlist?list=YOUR_PLAYLIST_ID"
|
||||||
|
- service: relaytv.volume_set
|
||||||
|
target:
|
||||||
|
entity_id: media_player.relaytv_bedroom
|
||||||
|
data:
|
||||||
|
volume_level: 0.25
|
||||||
|
- delay: "00:05:00"
|
||||||
|
- service: light.turn_on
|
||||||
|
target:
|
||||||
|
entity_id: light.bedroom
|
||||||
|
data:
|
||||||
|
brightness_pct: 45
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3) Late-Night Motion → Jump Scare (Fun Mode)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
alias: RelayTV - Late Night Jump Scare
|
||||||
|
mode: single
|
||||||
|
trigger:
|
||||||
|
- platform: state
|
||||||
|
entity_id: binary_sensor.hall_motion
|
||||||
|
to: "on"
|
||||||
|
condition:
|
||||||
|
- condition: time
|
||||||
|
after: "01:00:00"
|
||||||
|
before: "04:00:00"
|
||||||
|
action:
|
||||||
|
- service: relaytv.volume_set
|
||||||
|
target:
|
||||||
|
entity_id: media_player.relaytv_living_room
|
||||||
|
data:
|
||||||
|
volume_level: 0.85
|
||||||
|
- service: light.turn_on
|
||||||
|
target:
|
||||||
|
entity_id: light.hallway
|
||||||
|
data:
|
||||||
|
brightness_pct: 100
|
||||||
|
- service: relaytv.play_now
|
||||||
|
target:
|
||||||
|
entity_id: media_player.relaytv_living_room
|
||||||
|
data:
|
||||||
|
url: "https://www.youtube.com/watch?v=YOUR_JUMPSCARE_CLIP"
|
||||||
|
- delay: "00:00:12"
|
||||||
|
- service: light.turn_off
|
||||||
|
target:
|
||||||
|
entity_id: light.hallway
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4) Alarm Trigger → “Command Center” Camera Rotation (Queue)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
alias: RelayTV - Alarm Command Center
|
||||||
|
mode: restart
|
||||||
|
trigger:
|
||||||
|
- platform: state
|
||||||
|
entity_id: alarm_control_panel.house
|
||||||
|
to: "triggered"
|
||||||
|
action:
|
||||||
|
- service: relaytv.clear_queue
|
||||||
|
target:
|
||||||
|
entity_id: media_player.relaytv_living_room
|
||||||
|
- service: relaytv.enqueue
|
||||||
|
target:
|
||||||
|
entity_id: media_player.relaytv_living_room
|
||||||
|
data:
|
||||||
|
url: "rtsp://user:pass@10.0.55.10:554/front"
|
||||||
|
- service: relaytv.enqueue
|
||||||
|
target:
|
||||||
|
entity_id: media_player.relaytv_living_room
|
||||||
|
data:
|
||||||
|
url: "rtsp://user:pass@10.0.55.10:554/driveway"
|
||||||
|
- service: relaytv.enqueue
|
||||||
|
target:
|
||||||
|
entity_id: media_player.relaytv_living_room
|
||||||
|
data:
|
||||||
|
url: "rtsp://user:pass@10.0.55.10:554/backyard"
|
||||||
|
- service: relaytv.play_now
|
||||||
|
target:
|
||||||
|
entity_id: media_player.relaytv_living_room
|
||||||
|
data:
|
||||||
|
url: "rtsp://user:pass@10.0.55.10:554/front"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5) Party Mode Script (Intro Clip + Playlist)
|
||||||
|
|
||||||
|
Create this as a **Script** in HA (Settings → Automations & Scenes → Scripts).
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
party_mode_relaytv:
|
||||||
|
alias: Party Mode (RelayTV)
|
||||||
|
mode: single
|
||||||
|
sequence:
|
||||||
|
- service: relaytv.clear_queue
|
||||||
|
target:
|
||||||
|
entity_id: media_player.relaytv_living_room
|
||||||
|
- service: relaytv.enqueue
|
||||||
|
target:
|
||||||
|
entity_id: media_player.relaytv_living_room
|
||||||
|
data:
|
||||||
|
url: "https://www.youtube.com/watch?v=YOUR_INTRO_CLIP"
|
||||||
|
- service: relaytv.enqueue
|
||||||
|
target:
|
||||||
|
entity_id: media_player.relaytv_living_room
|
||||||
|
data:
|
||||||
|
url: "https://www.youtube.com/playlist?list=YOUR_PARTY_PLAYLIST"
|
||||||
|
- service: relaytv.volume_set
|
||||||
|
target:
|
||||||
|
entity_id: media_player.relaytv_living_room
|
||||||
|
data:
|
||||||
|
volume_level: 0.65
|
||||||
|
- service: relaytv.play_now
|
||||||
|
target:
|
||||||
|
entity_id: media_player.relaytv_living_room
|
||||||
|
data:
|
||||||
|
url: "https://www.youtube.com/watch?v=YOUR_INTRO_CLIP"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6) “AI Announcement” Pattern (TTS + Video)
|
||||||
|
|
||||||
|
This example uses HA TTS to speak, then plays a related clip.
|
||||||
|
(You can swap TTS for Assist, Piper, cloud TTS, etc.)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
alias: RelayTV - AI Announcement Pattern
|
||||||
|
mode: single
|
||||||
|
trigger:
|
||||||
|
- platform: state
|
||||||
|
entity_id: binary_sensor.garage_door_open_too_long
|
||||||
|
to: "on"
|
||||||
|
action:
|
||||||
|
- service: tts.speak
|
||||||
|
data:
|
||||||
|
media_player_entity_id: media_player.kitchen_speaker
|
||||||
|
message: "Garage door has been open for a while."
|
||||||
|
- service: relaytv.play_now
|
||||||
|
target:
|
||||||
|
entity_id: media_player.relaytv_living_room
|
||||||
|
data:
|
||||||
|
url: "https://www.youtube.com/watch?v=YOUR_ALERT_CLIP"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7) Energy Spike → Alert Clip + Reduce Loads
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
alias: RelayTV - Energy Spike Alert
|
||||||
|
mode: single
|
||||||
|
trigger:
|
||||||
|
- platform: numeric_state
|
||||||
|
entity_id: sensor.home_power_kw
|
||||||
|
above: 8.0
|
||||||
|
for: "00:02:00"
|
||||||
|
action:
|
||||||
|
- service: relaytv.play_now
|
||||||
|
target:
|
||||||
|
entity_id: media_player.relaytv_living_room
|
||||||
|
data:
|
||||||
|
url: "https://www.youtube.com/watch?v=YOUR_POWER_ALERT_CLIP"
|
||||||
|
- service: switch.turn_off
|
||||||
|
target:
|
||||||
|
entity_id:
|
||||||
|
- switch.space_heater
|
||||||
|
- switch.shop_fan
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🛣 Roadmap (Future Capabilities)
|
||||||
|
|
||||||
|
## 🔔 Interrupt + Resume Service
|
||||||
|
Temporarily interrupt playback for alerts, then auto-resume prior content.
|
||||||
|
|
||||||
|
## 🪟 Overlay Mode
|
||||||
|
Display notification banners without stopping playback.
|
||||||
|
|
||||||
|
## 🔄 Multi-Screen Sync
|
||||||
|
Synchronize playback across multiple RelayTV servers (perfect for whole-home announcements).
|
||||||
|
|
||||||
|
## 🖼 Snapshot Previews
|
||||||
|
Capture/preview before switching content (helps with camera pops).
|
||||||
|
|
||||||
|
## 🎛 Sensor → Stream Mapping UI
|
||||||
|
Map motion/doorbell sensors to streams directly in the integration options UI (no YAML required).
|
||||||
|
|
||||||
|
## 🧠 AI Clip Generator
|
||||||
|
Generate short clips (image + TTS + theme) for alerts, seasons, and events.
|
||||||
|
|
||||||
|
## 📢 Smart Alert Replacement
|
||||||
|
Insert custom “house announcements” during downtime or between queued items.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 📦 Installation
|
||||||
|
|
||||||
|
## HACS
|
||||||
|
1. Add as Custom Repository (Integration)
|
||||||
|
2. Install
|
||||||
|
3. Restart Home Assistant
|
||||||
|
4. Add Integration
|
||||||
|
5. Configure server address
|
||||||
|
|
||||||
|
## Manual
|
||||||
Copy `custom_components/relaytv` into:
|
Copy `custom_components/relaytv` into:
|
||||||
|
|
||||||
```
|
```text
|
||||||
/config/custom_components/relaytv
|
/config/custom_components/relaytv
|
||||||
```
|
```
|
||||||
|
|
||||||
Restart Home Assistant, then add the integration from the UI.
|
Restart Home Assistant.
|
||||||
|
|
||||||
## Configuration
|
---
|
||||||
|
|
||||||
During setup you provide:
|
|
||||||
|
|
||||||
- **RelayTV base URL** (required)
|
|
||||||
|
|
||||||
Options allow:
|
|
||||||
|
|
||||||
- Sidebar title
|
|
||||||
- Sidebar icon (MDI)
|
|
||||||
- Sidebar path (URL slug)
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
- This integration does **not** create a `media_player` entity.
|
|
||||||
- It embeds the RelayTV UI at `/ui` via iframe.
|
|
||||||
|
|
||||||
## Versioning
|
|
||||||
|
|
||||||
This repo uses semantic versioning.
|
|
||||||
Current version: **v0.1.0**
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
|
# 🔐 License
|
||||||
TBD
|
TBD
|
||||||
|
|||||||
@@ -1,43 +1,36 @@
|
|||||||
"""RelayTV integration.
|
"""RelayTV integration."""
|
||||||
|
|
||||||
This integration:
|
|
||||||
1) Registers a Home Assistant sidebar iframe panel that embeds the RelayTV web UI.
|
|
||||||
2) Exposes a RelayTV media_player entity backed by RelayTV's local HTTP API.
|
|
||||||
3) Provides services (e.g., relaytv.smart_url) for automations and mobile share flows.
|
|
||||||
|
|
||||||
The API layer targets RelayTV's canonical endpoints (see RelayTV server docs/API.md).
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from homeassistant.core import HomeAssistant
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.components import frontend
|
from homeassistant.components import frontend
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.helpers import aiohttp_client
|
from homeassistant.const import CONF_ENTITY_ID
|
||||||
|
from homeassistant.core import HomeAssistant, ServiceCall
|
||||||
|
from homeassistant.helpers import aiohttp_client, entity_registry as er
|
||||||
|
from homeassistant.helpers.storage import Store
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
CONF_BASE_URL,
|
CONF_BASE_URL,
|
||||||
CONF_PANEL_ICON,
|
CONF_PANEL_ENABLED,
|
||||||
CONF_PANEL_PATH,
|
CONF_PANEL_TARGET_ENTRY_ID,
|
||||||
CONF_PANEL_TITLE,
|
DATA_API,
|
||||||
|
DATA_COORDINATOR,
|
||||||
|
DATA_PANEL_SETTINGS,
|
||||||
|
DATA_STORE,
|
||||||
DEFAULT_PANEL_ICON,
|
DEFAULT_PANEL_ICON,
|
||||||
DEFAULT_PANEL_PATH,
|
DEFAULT_PANEL_PATH,
|
||||||
DEFAULT_PANEL_TITLE,
|
DEFAULT_PANEL_TITLE,
|
||||||
DATA_API,
|
|
||||||
DATA_COORDINATOR,
|
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
PLATFORMS,
|
PLATFORMS,
|
||||||
SERVICE_SMART_URL,
|
|
||||||
SERVICE_PLAY_NOW,
|
|
||||||
SERVICE_ANNOUNCE,
|
SERVICE_ANNOUNCE,
|
||||||
|
SERVICE_PLAY_NOW,
|
||||||
|
SERVICE_SMART_URL,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .relaytv_api import RelayTVApi
|
|
||||||
from .coordinator import RelayTVCoordinator
|
from .coordinator import RelayTVCoordinator
|
||||||
|
from .relaytv_api import RelayTVApi
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -47,72 +40,188 @@ def _normalize_base_url(raw: str) -> str:
|
|||||||
raw = (raw or "").strip()
|
raw = (raw or "").strip()
|
||||||
if not raw:
|
if not raw:
|
||||||
return ""
|
return ""
|
||||||
# Allow users to paste host:port, add scheme.
|
|
||||||
if "://" not in raw:
|
if "://" not in raw:
|
||||||
raw = f"http://{raw}"
|
raw = f"http://{raw}"
|
||||||
# Basic parse/normalize; keep path if user provided one.
|
parsed = urlparse(raw)
|
||||||
p = urlparse(raw)
|
if not parsed.netloc:
|
||||||
if not p.netloc:
|
|
||||||
return raw
|
return raw
|
||||||
# Remove trailing slash to avoid double slashes when HA appends.
|
normalized = f"{parsed.scheme}://{parsed.netloc}{parsed.path}".rstrip("/")
|
||||||
normalized = f"{p.scheme}://{p.netloc}{p.path}".rstrip("/")
|
if parsed.query:
|
||||||
if p.query:
|
normalized += f"?{parsed.query}"
|
||||||
normalized += f"?{p.query}"
|
|
||||||
return normalized
|
return normalized
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup(hass: HomeAssistant, config: dict) -> bool:
|
||||||
|
"""Set up the RelayTV domain."""
|
||||||
|
hass.data.setdefault(DOMAIN, {})
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def _async_ensure_settings(hass: HomeAssistant) -> dict:
|
||||||
|
domain_data = hass.data.setdefault(DOMAIN, {})
|
||||||
|
if DATA_STORE not in domain_data:
|
||||||
|
domain_data[DATA_STORE] = Store(hass, 1, f"{DOMAIN}_panel_settings")
|
||||||
|
if DATA_PANEL_SETTINGS not in domain_data:
|
||||||
|
domain_data[DATA_PANEL_SETTINGS] = await domain_data[DATA_STORE].async_load() or {
|
||||||
|
CONF_PANEL_ENABLED: True,
|
||||||
|
CONF_PANEL_TARGET_ENTRY_ID: None,
|
||||||
|
}
|
||||||
|
return domain_data[DATA_PANEL_SETTINGS]
|
||||||
|
|
||||||
|
|
||||||
|
async def _async_save_settings(hass: HomeAssistant) -> None:
|
||||||
|
data = hass.data.setdefault(DOMAIN, {})
|
||||||
|
await data[DATA_STORE].async_save(data[DATA_PANEL_SETTINGS])
|
||||||
|
|
||||||
|
|
||||||
|
def _get_entry_data(hass: HomeAssistant, entry_id: str) -> dict | None:
|
||||||
|
return hass.data.get(DOMAIN, {}).get(entry_id)
|
||||||
|
|
||||||
|
|
||||||
|
def _fallback_entry_id(hass: HomeAssistant) -> str | None:
|
||||||
|
for entry_id, value in hass.data.get(DOMAIN, {}).items():
|
||||||
|
if isinstance(value, dict) and DATA_API in value:
|
||||||
|
return entry_id
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_entry_id_for_call(hass: HomeAssistant, call: ServiceCall) -> str | None:
|
||||||
|
entity_ids: list[str] = []
|
||||||
|
raw_entity = call.data.get(CONF_ENTITY_ID)
|
||||||
|
if isinstance(raw_entity, str):
|
||||||
|
entity_ids = [raw_entity]
|
||||||
|
elif isinstance(raw_entity, list):
|
||||||
|
entity_ids = [item for item in raw_entity if isinstance(item, str)]
|
||||||
|
|
||||||
|
registry = er.async_get(hass)
|
||||||
|
|
||||||
|
if not entity_ids:
|
||||||
|
device_id = call.data.get("device_id")
|
||||||
|
device_ids = [device_id] if isinstance(device_id, str) else device_id
|
||||||
|
if isinstance(device_ids, list):
|
||||||
|
for item in device_ids:
|
||||||
|
if not isinstance(item, str):
|
||||||
|
continue
|
||||||
|
for reg_entry in er.async_entries_for_device(registry, item):
|
||||||
|
if reg_entry.entity_id.startswith("media_player."):
|
||||||
|
entity_ids.append(reg_entry.entity_id)
|
||||||
|
|
||||||
|
for entity_id in entity_ids:
|
||||||
|
reg_entry = registry.async_get(entity_id)
|
||||||
|
if reg_entry and _get_entry_data(hass, reg_entry.config_entry_id):
|
||||||
|
return reg_entry.config_entry_id
|
||||||
|
|
||||||
|
panel_target = hass.data.get(DOMAIN, {}).get(DATA_PANEL_SETTINGS, {}).get(CONF_PANEL_TARGET_ENTRY_ID)
|
||||||
|
if panel_target and _get_entry_data(hass, panel_target):
|
||||||
|
return panel_target
|
||||||
|
return _fallback_entry_id(hass)
|
||||||
|
|
||||||
|
|
||||||
|
def _register_panel(hass: HomeAssistant, *, path: str, title: str, icon: str, url: str) -> None:
|
||||||
|
frontend.async_register_built_in_panel(
|
||||||
|
hass,
|
||||||
|
component_name="iframe",
|
||||||
|
sidebar_title=title,
|
||||||
|
sidebar_icon=icon,
|
||||||
|
frontend_url_path=path,
|
||||||
|
config={"url": url},
|
||||||
|
require_admin=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _async_unregister_panel(hass: HomeAssistant) -> None:
|
||||||
|
try:
|
||||||
|
frontend.async_remove_panel(hass, DEFAULT_PANEL_PATH)
|
||||||
|
except Exception:
|
||||||
|
_LOGGER.debug("Panel removal failed (it may not exist)", exc_info=True)
|
||||||
|
|
||||||
|
|
||||||
|
async def _async_update_panel(hass: HomeAssistant) -> None:
|
||||||
|
settings = await _async_ensure_settings(hass)
|
||||||
|
_async_unregister_panel(hass)
|
||||||
|
if not settings.get(CONF_PANEL_ENABLED, True):
|
||||||
|
return
|
||||||
|
|
||||||
|
target_entry_id = settings.get(CONF_PANEL_TARGET_ENTRY_ID)
|
||||||
|
target = _get_entry_data(hass, target_entry_id) if target_entry_id else None
|
||||||
|
if target is None:
|
||||||
|
fallback_id = _fallback_entry_id(hass)
|
||||||
|
if not fallback_id:
|
||||||
|
return
|
||||||
|
settings[CONF_PANEL_TARGET_ENTRY_ID] = fallback_id
|
||||||
|
await _async_save_settings(hass)
|
||||||
|
target = _get_entry_data(hass, fallback_id)
|
||||||
|
target_entry_id = fallback_id
|
||||||
|
|
||||||
|
if not target:
|
||||||
|
return
|
||||||
|
|
||||||
|
url = target[DATA_API].base_url
|
||||||
|
_register_panel(
|
||||||
|
hass,
|
||||||
|
path=DEFAULT_PANEL_PATH,
|
||||||
|
title=DEFAULT_PANEL_TITLE,
|
||||||
|
icon=DEFAULT_PANEL_ICON,
|
||||||
|
url=url,
|
||||||
|
)
|
||||||
|
_LOGGER.info("Registered RelayTV panel to entry %s (%s)", target_entry_id, url)
|
||||||
|
|
||||||
|
|
||||||
|
async def _async_set_default_sidebar_target(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||||
|
settings = await _async_ensure_settings(hass)
|
||||||
|
settings[CONF_PANEL_TARGET_ENTRY_ID] = entry.entry_id
|
||||||
|
await _async_save_settings(hass)
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
"""Set up RelayTV from a config entry."""
|
"""Set up RelayTV from a config entry."""
|
||||||
base_url = _normalize_base_url(entry.data.get(CONF_BASE_URL, ""))
|
base_url = _normalize_base_url(entry.data.get(CONF_BASE_URL, ""))
|
||||||
if not base_url:
|
if not base_url:
|
||||||
_LOGGER.error("RelayTV base URL is empty; panel will not be registered")
|
_LOGGER.error("RelayTV base URL is empty")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Store runtime objects
|
await _async_ensure_settings(hass)
|
||||||
hass.data.setdefault(DOMAIN, {})
|
|
||||||
session = aiohttp_client.async_get_clientsession(hass)
|
session = aiohttp_client.async_get_clientsession(hass)
|
||||||
api = RelayTVApi(session=session, base_url=base_url)
|
api = RelayTVApi(session=session, base_url=base_url)
|
||||||
coordinator = RelayTVCoordinator(hass=hass, api=api)
|
coordinator = RelayTVCoordinator(hass=hass, api=api)
|
||||||
hass.data[DOMAIN][entry.entry_id] = {DATA_API: api, DATA_COORDINATOR: coordinator}
|
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {DATA_API: api, DATA_COORDINATOR: coordinator}
|
||||||
|
|
||||||
# Prime coordinator (non-fatal if it fails; entity will show unavailable).
|
|
||||||
await coordinator.async_config_entry_first_refresh()
|
await coordinator.async_config_entry_first_refresh()
|
||||||
|
|
||||||
title = entry.options.get(CONF_PANEL_TITLE, DEFAULT_PANEL_TITLE)
|
|
||||||
icon = entry.options.get(CONF_PANEL_ICON, DEFAULT_PANEL_ICON)
|
|
||||||
path = entry.options.get(CONF_PANEL_PATH, DEFAULT_PANEL_PATH)
|
|
||||||
|
|
||||||
_register_panel(hass, path=path, title=title, icon=icon, url=base_url)
|
|
||||||
|
|
||||||
# Register platforms/entities
|
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
|
|
||||||
# Register services
|
await _async_set_default_sidebar_target(hass, entry)
|
||||||
async def _handle_smart_url(call):
|
await _async_update_panel(hass)
|
||||||
|
|
||||||
|
async def _handle_smart_url(call: ServiceCall):
|
||||||
url = (call.data.get("url") or "").strip()
|
url = (call.data.get("url") or "").strip()
|
||||||
if not url:
|
if not url:
|
||||||
return
|
return
|
||||||
await api.smart_url(url)
|
entry_id = _resolve_entry_id_for_call(hass, call)
|
||||||
await coordinator.async_request_refresh()
|
store = _get_entry_data(hass, entry_id) if entry_id else None
|
||||||
|
if not store:
|
||||||
|
return
|
||||||
|
await store[DATA_API].smart_url(url)
|
||||||
|
await store[DATA_COORDINATOR].async_request_refresh()
|
||||||
|
|
||||||
async def _handle_play_now(call):
|
async def _handle_play_now(call: ServiceCall):
|
||||||
"""Play a URL immediately (maps to RelayTV POST /play).
|
|
||||||
|
|
||||||
Note: RelayTV /play clears the queue.
|
|
||||||
"""
|
|
||||||
url = (call.data.get("url") or "").strip()
|
url = (call.data.get("url") or "").strip()
|
||||||
if not url:
|
if not url:
|
||||||
return
|
return
|
||||||
use_ytdlp = call.data.get("use_ytdlp")
|
entry_id = _resolve_entry_id_for_call(hass, call)
|
||||||
cec = call.data.get("cec")
|
store = _get_entry_data(hass, entry_id) if entry_id else None
|
||||||
await api.play(url=url, use_ytdlp=use_ytdlp, cec=cec)
|
if not store:
|
||||||
await coordinator.async_request_refresh()
|
return
|
||||||
|
await store[DATA_API].play(
|
||||||
|
url=url,
|
||||||
|
use_ytdlp=call.data.get("use_ytdlp"),
|
||||||
|
cec=call.data.get("cec"),
|
||||||
|
)
|
||||||
|
await store[DATA_COORDINATOR].async_request_refresh()
|
||||||
|
|
||||||
async def _handle_announce(call):
|
async def _handle_announce(call: ServiceCall):
|
||||||
# RelayTV doesn't have a distinct "announce" mode; alias to play_now.
|
|
||||||
await _handle_play_now(call)
|
await _handle_play_now(call)
|
||||||
|
|
||||||
# Register services once (first config entry wins).
|
|
||||||
if not hass.services.has_service(DOMAIN, SERVICE_SMART_URL):
|
if not hass.services.has_service(DOMAIN, SERVICE_SMART_URL):
|
||||||
hass.services.async_register(DOMAIN, SERVICE_SMART_URL, _handle_smart_url)
|
hass.services.async_register(DOMAIN, SERVICE_SMART_URL, _handle_smart_url)
|
||||||
if not hass.services.has_service(DOMAIN, SERVICE_PLAY_NOW):
|
if not hass.services.has_service(DOMAIN, SERVICE_PLAY_NOW):
|
||||||
@@ -125,56 +234,36 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
|
|
||||||
|
|
||||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
"""Unload RelayTV Web UI config entry."""
|
"""Unload RelayTV entry."""
|
||||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||||
|
|
||||||
path = entry.options.get(CONF_PANEL_PATH, DEFAULT_PANEL_PATH)
|
|
||||||
try:
|
|
||||||
frontend.async_remove_panel(hass, path)
|
|
||||||
except Exception: # pragma: no cover
|
|
||||||
_LOGGER.debug("Panel removal failed (it may not exist)", exc_info=True)
|
|
||||||
|
|
||||||
# Remove services only if this is the last entry.
|
|
||||||
hass.data.get(DOMAIN, {}).pop(entry.entry_id, None)
|
hass.data.get(DOMAIN, {}).pop(entry.entry_id, None)
|
||||||
if not hass.data.get(DOMAIN):
|
|
||||||
try:
|
|
||||||
hass.services.async_remove(DOMAIN, SERVICE_SMART_URL)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
settings = await _async_ensure_settings(hass)
|
||||||
|
if settings.get(CONF_PANEL_TARGET_ENTRY_ID) == entry.entry_id:
|
||||||
|
replacement_id = _fallback_entry_id(hass)
|
||||||
|
settings[CONF_PANEL_TARGET_ENTRY_ID] = replacement_id
|
||||||
|
await _async_save_settings(hass)
|
||||||
|
_LOGGER.info(
|
||||||
|
"RelayTV sidebar target removed (%s); switched to %s",
|
||||||
|
entry.entry_id,
|
||||||
|
replacement_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not any(cfg_entry.entry_id in hass.data.get(DOMAIN, {}) for cfg_entry in hass.config_entries.async_entries(DOMAIN)):
|
||||||
|
_async_unregister_panel(hass)
|
||||||
|
for service_name in (SERVICE_SMART_URL, SERVICE_PLAY_NOW, SERVICE_ANNOUNCE):
|
||||||
|
if hass.services.has_service(DOMAIN, service_name):
|
||||||
|
hass.services.async_remove(DOMAIN, service_name)
|
||||||
|
return unload_ok
|
||||||
|
|
||||||
|
await _async_update_panel(hass)
|
||||||
return unload_ok
|
return unload_ok
|
||||||
|
|
||||||
|
|
||||||
async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||||
"""Handle options updates by re-registering the panel."""
|
"""Handle entry updates."""
|
||||||
base_url = _normalize_base_url(entry.data.get(CONF_BASE_URL, ""))
|
base_url = _normalize_base_url(entry.data.get(CONF_BASE_URL, ""))
|
||||||
title = entry.options.get(CONF_PANEL_TITLE, DEFAULT_PANEL_TITLE)
|
store = _get_entry_data(hass, entry.entry_id)
|
||||||
icon = entry.options.get(CONF_PANEL_ICON, DEFAULT_PANEL_ICON)
|
|
||||||
path = entry.options.get(CONF_PANEL_PATH, DEFAULT_PANEL_PATH)
|
|
||||||
|
|
||||||
try:
|
|
||||||
frontend.async_remove_panel(hass, path)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
_register_panel(hass, path=path, title=title, icon=icon, url=base_url)
|
|
||||||
|
|
||||||
# Update API base_url if needed
|
|
||||||
store = hass.data.get(DOMAIN, {}).get(entry.entry_id)
|
|
||||||
if store and base_url:
|
if store and base_url:
|
||||||
store[DATA_API].base_url = base_url
|
store[DATA_API].base_url = base_url
|
||||||
|
await _async_update_panel(hass)
|
||||||
|
|
||||||
def _register_panel(hass: HomeAssistant, *, path: str, title: str, icon: str, url: str) -> None:
|
|
||||||
"""Register the sidebar iframe panel."""
|
|
||||||
# We use the built-in iframe panel. Keyword args protect against HA signature drift.
|
|
||||||
frontend.async_register_built_in_panel(
|
|
||||||
hass,
|
|
||||||
component_name="iframe",
|
|
||||||
sidebar_title=title,
|
|
||||||
sidebar_icon=icon,
|
|
||||||
frontend_url_path=path,
|
|
||||||
config={"url": url},
|
|
||||||
require_admin=False,
|
|
||||||
)
|
|
||||||
_LOGGER.info("Registered RelayTV Web UI panel at /%s → %s", path, url)
|
|
||||||
|
|||||||
@@ -2,23 +2,43 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant import config_entries
|
from homeassistant import config_entries
|
||||||
|
from homeassistant.const import CONF_NAME
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
|
from homeassistant.helpers.storage import Store
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
CONF_BASE_URL,
|
CONF_BASE_URL,
|
||||||
CONF_PANEL_ICON,
|
CONF_PANEL_ENABLED,
|
||||||
CONF_PANEL_PATH,
|
CONF_PANEL_TARGET_ENTRY_ID,
|
||||||
CONF_PANEL_TITLE,
|
CONF_SERVER_NAME,
|
||||||
DEFAULT_PANEL_ICON,
|
DATA_PANEL_SETTINGS,
|
||||||
DEFAULT_PANEL_PATH,
|
DATA_STORE,
|
||||||
DEFAULT_PANEL_TITLE,
|
DEFAULT_PANEL_TITLE,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_base_url(raw: str) -> str:
|
||||||
|
"""Normalize user input into a URL safe for iframe embedding."""
|
||||||
|
raw = (raw or "").strip()
|
||||||
|
if not raw:
|
||||||
|
return ""
|
||||||
|
if "://" not in raw:
|
||||||
|
raw = f"http://{raw}"
|
||||||
|
parsed = urlparse(raw)
|
||||||
|
if not parsed.netloc:
|
||||||
|
return raw
|
||||||
|
normalized = f"{parsed.scheme}://{parsed.netloc}{parsed.path}".rstrip("/")
|
||||||
|
if parsed.query:
|
||||||
|
normalized += f"?{parsed.query}"
|
||||||
|
return normalized
|
||||||
|
|
||||||
|
|
||||||
class RelayTVWebUIConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
class RelayTVWebUIConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
"""Handle a config flow for RelayTV Web UI panel."""
|
"""Handle a config flow for RelayTV Web UI panel."""
|
||||||
|
|
||||||
@@ -28,18 +48,24 @@ class RelayTVWebUIConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
errors = {}
|
errors = {}
|
||||||
|
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
base_url = (user_input.get(CONF_BASE_URL) or "").strip()
|
base_url = _normalize_base_url(user_input.get(CONF_BASE_URL, ""))
|
||||||
|
name = (user_input.get(CONF_SERVER_NAME) or "").strip()
|
||||||
if not base_url:
|
if not base_url:
|
||||||
errors["base"] = "missing_base_url"
|
errors["base"] = "missing_base_url"
|
||||||
|
elif not name:
|
||||||
|
errors["base"] = "missing_name"
|
||||||
else:
|
else:
|
||||||
# Single instance is usually sufficient; users can duplicate by cloning the folder/domain if needed.
|
await self.async_set_unique_id(base_url)
|
||||||
await self.async_set_unique_id(DOMAIN)
|
|
||||||
self._abort_if_unique_id_configured()
|
self._abort_if_unique_id_configured()
|
||||||
return self.async_create_entry(title=DEFAULT_PANEL_TITLE, data={CONF_BASE_URL: base_url})
|
return self.async_create_entry(
|
||||||
|
title=name,
|
||||||
|
data={CONF_BASE_URL: base_url, CONF_NAME: name},
|
||||||
|
)
|
||||||
|
|
||||||
schema = vol.Schema(
|
schema = vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Required(CONF_BASE_URL): str,
|
vol.Required(CONF_BASE_URL, default="http://localhost:8787"): str,
|
||||||
|
vol.Required(CONF_SERVER_NAME, default=DEFAULT_PANEL_TITLE): str,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -55,29 +81,44 @@ class RelayTVWebUIOptionsFlow(config_entries.OptionsFlow):
|
|||||||
"""Handle options for RelayTV Web UI panel."""
|
"""Handle options for RelayTV Web UI panel."""
|
||||||
|
|
||||||
def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
|
def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
|
||||||
# NOTE: In modern Home Assistant, OptionsFlow exposes a read-only
|
|
||||||
# `config_entry` property, so we cannot assign to it.
|
|
||||||
# Store it on a private attribute instead.
|
|
||||||
self._config_entry = config_entry
|
self._config_entry = config_entry
|
||||||
|
|
||||||
async def async_step_init(self, user_input=None):
|
async def async_step_init(self, user_input=None):
|
||||||
|
settings_store = Store(self.hass, 1, f"{DOMAIN}_panel_settings")
|
||||||
|
settings = await settings_store.async_load() or {}
|
||||||
|
|
||||||
|
entries = self.hass.config_entries.async_entries(DOMAIN)
|
||||||
|
choices = {entry.entry_id: entry.title for entry in entries}
|
||||||
|
|
||||||
|
current_target = settings.get(CONF_PANEL_TARGET_ENTRY_ID)
|
||||||
|
if current_target not in choices and choices:
|
||||||
|
current_target = entries[-1].entry_id
|
||||||
|
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
return self.async_create_entry(title="", data=user_input)
|
chosen_target = user_input.get(CONF_PANEL_TARGET_ENTRY_ID)
|
||||||
|
if chosen_target not in choices and choices:
|
||||||
|
chosen_target = entries[-1].entry_id
|
||||||
|
|
||||||
|
updated = {
|
||||||
|
CONF_PANEL_ENABLED: bool(user_input.get(CONF_PANEL_ENABLED, True)),
|
||||||
|
CONF_PANEL_TARGET_ENTRY_ID: chosen_target,
|
||||||
|
}
|
||||||
|
await settings_store.async_save(updated)
|
||||||
|
self.hass.data.setdefault(DOMAIN, {})[DATA_STORE] = settings_store
|
||||||
|
self.hass.data[DOMAIN][DATA_PANEL_SETTINGS] = updated
|
||||||
|
await self.hass.config_entries.async_reload(self._config_entry.entry_id)
|
||||||
|
return self.async_create_entry(title="", data={})
|
||||||
|
|
||||||
schema = vol.Schema(
|
schema = vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Optional(
|
vol.Required(
|
||||||
CONF_PANEL_TITLE,
|
CONF_PANEL_ENABLED,
|
||||||
default=self._config_entry.options.get(CONF_PANEL_TITLE, DEFAULT_PANEL_TITLE),
|
default=settings.get(CONF_PANEL_ENABLED, True),
|
||||||
): str,
|
): bool,
|
||||||
vol.Optional(
|
vol.Required(
|
||||||
CONF_PANEL_ICON,
|
CONF_PANEL_TARGET_ENTRY_ID,
|
||||||
default=self._config_entry.options.get(CONF_PANEL_ICON, DEFAULT_PANEL_ICON),
|
default=current_target,
|
||||||
): str,
|
): vol.In(choices),
|
||||||
vol.Optional(
|
|
||||||
CONF_PANEL_PATH,
|
|
||||||
default=self._config_entry.options.get(CONF_PANEL_PATH, DEFAULT_PANEL_PATH),
|
|
||||||
): str,
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -5,9 +5,12 @@ DOMAIN = "relaytv"
|
|||||||
PLATFORMS: list[str] = ["media_player"]
|
PLATFORMS: list[str] = ["media_player"]
|
||||||
|
|
||||||
CONF_BASE_URL = "base_url"
|
CONF_BASE_URL = "base_url"
|
||||||
|
CONF_SERVER_NAME = "server_name"
|
||||||
CONF_PANEL_TITLE = "panel_title"
|
CONF_PANEL_TITLE = "panel_title"
|
||||||
CONF_PANEL_ICON = "panel_icon"
|
CONF_PANEL_ICON = "panel_icon"
|
||||||
CONF_PANEL_PATH = "panel_path"
|
CONF_PANEL_PATH = "panel_path"
|
||||||
|
CONF_PANEL_ENABLED = "panel_enabled"
|
||||||
|
CONF_PANEL_TARGET_ENTRY_ID = "panel_target_entry_id"
|
||||||
|
|
||||||
DEFAULT_PANEL_TITLE = "RelayTV"
|
DEFAULT_PANEL_TITLE = "RelayTV"
|
||||||
DEFAULT_PANEL_ICON = "mdi:cast"
|
DEFAULT_PANEL_ICON = "mdi:cast"
|
||||||
@@ -21,3 +24,5 @@ SERVICE_ANNOUNCE = "announce"
|
|||||||
# Data keys
|
# Data keys
|
||||||
DATA_COORDINATOR = "coordinator"
|
DATA_COORDINATOR = "coordinator"
|
||||||
DATA_API = "api"
|
DATA_API = "api"
|
||||||
|
DATA_STORE = "store"
|
||||||
|
DATA_PANEL_SETTINGS = "panel_settings"
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ from homeassistant.components.media_player.const import (
|
|||||||
MediaPlayerState,
|
MediaPlayerState,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.const import CONF_NAME
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
@@ -137,13 +138,14 @@ class RelayTVMediaPlayer(CoordinatorEntity, MediaPlayerEntity):
|
|||||||
"""RelayTV as a HA media_player."""
|
"""RelayTV as a HA media_player."""
|
||||||
|
|
||||||
_attr_has_entity_name = True
|
_attr_has_entity_name = True
|
||||||
_attr_name = "RelayTV"
|
_attr_name = None
|
||||||
|
|
||||||
def __init__(self, entry: ConfigEntry, coordinator, api) -> None:
|
def __init__(self, entry: ConfigEntry, coordinator, api) -> None:
|
||||||
super().__init__(coordinator)
|
super().__init__(coordinator)
|
||||||
self._entry = entry
|
self._entry = entry
|
||||||
self._api = api
|
self._api = api
|
||||||
self._attr_unique_id = f"{entry.entry_id}_player"
|
self._attr_unique_id = f"{entry.entry_id}_player"
|
||||||
|
self._attr_name = entry.data.get(CONF_NAME, entry.title)
|
||||||
|
|
||||||
self._attr_supported_features = (
|
self._attr_supported_features = (
|
||||||
MediaPlayerEntityFeature.PLAY
|
MediaPlayerEntityFeature.PLAY
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
smart_url:
|
smart_url:
|
||||||
name: Smart play URL
|
name: Smart play URL
|
||||||
description: Resolve and play a shared URL in RelayTV (RelayTV POST /smart).
|
description: Resolve and play a shared URL in RelayTV (RelayTV POST /smart).
|
||||||
|
target:
|
||||||
|
entity:
|
||||||
|
domain: media_player
|
||||||
|
integration: relaytv
|
||||||
|
device:
|
||||||
|
integration: relaytv
|
||||||
fields:
|
fields:
|
||||||
url:
|
url:
|
||||||
required: true
|
required: true
|
||||||
@@ -11,6 +17,12 @@ smart_url:
|
|||||||
play_now:
|
play_now:
|
||||||
name: Play now
|
name: Play now
|
||||||
description: Play a URL immediately (RelayTV POST /play). This clears the queue.
|
description: Play a URL immediately (RelayTV POST /play). This clears the queue.
|
||||||
|
target:
|
||||||
|
entity:
|
||||||
|
domain: media_player
|
||||||
|
integration: relaytv
|
||||||
|
device:
|
||||||
|
integration: relaytv
|
||||||
fields:
|
fields:
|
||||||
url:
|
url:
|
||||||
required: true
|
required: true
|
||||||
@@ -31,6 +43,12 @@ play_now:
|
|||||||
announce:
|
announce:
|
||||||
name: Announce
|
name: Announce
|
||||||
description: Alias for play_now. (RelayTV does not currently implement a distinct announcement mode.)
|
description: Alias for play_now. (RelayTV does not currently implement a distinct announcement mode.)
|
||||||
|
target:
|
||||||
|
entity:
|
||||||
|
domain: media_player
|
||||||
|
integration: relaytv
|
||||||
|
device:
|
||||||
|
integration: relaytv
|
||||||
fields:
|
fields:
|
||||||
url:
|
url:
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
@@ -4,25 +4,27 @@
|
|||||||
"step": {
|
"step": {
|
||||||
"user": {
|
"user": {
|
||||||
"title": "Connect to RelayTV",
|
"title": "Connect to RelayTV",
|
||||||
"description": "Enter the base URL where RelayTV is reachable from Home Assistant (example: http://relaytv-host:8787).",
|
"description": "Add a RelayTV server reachable from Home Assistant.",
|
||||||
"data": {
|
"data": {
|
||||||
"base_url": "RelayTV base URL"
|
"base_url": "RelayTV server address",
|
||||||
|
"server_name": "Name"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"missing_base_url": "Please enter a RelayTV base URL."
|
"missing_base_url": "Please enter a RelayTV base URL.",
|
||||||
|
"missing_name": "Please enter a name.",
|
||||||
|
"already_configured": "This RelayTV server is already configured."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
"step": {
|
"step": {
|
||||||
"init": {
|
"init": {
|
||||||
"title": "Panel options",
|
"title": "Sidebar panel",
|
||||||
"description": "Customize how the RelayTV panel appears in the Home Assistant sidebar.",
|
"description": "Choose whether the sidebar is enabled and which server it displays.",
|
||||||
"data": {
|
"data": {
|
||||||
"panel_title": "Sidebar title",
|
"panel_enabled": "Enable sidebar panel",
|
||||||
"panel_icon": "Sidebar icon (MDI)",
|
"panel_target_entry_id": "Server shown in sidebar"
|
||||||
"panel_path": "Sidebar path (URL slug)"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user