Skip to content

[BUG] Media URLs unreachable when Home Assistant uses external URL — satellite cannot play TTS/announcements #244

@Gifford47

Description

@Gifford47

Media URLs unreachable when Home Assistant uses external URL — satellite cannot play TTS/announcements

Problem

When the LVA satellite registers via mDNS, Home Assistant may construct media URLs (TTS, announcements, wake word downloads) using its external URL — e.g. a Nabu Casa cloud address. The satellite sits on the same LAN and cannot reach that external address, so mpv fails silently and no audio is played.

Example

HA advertises via _home-assistant._tcp.local.:

internal_url=https://example.duckdns.org:8123   ← with port → routes through LAN
external_url=https://example.duckdns.org        ← without port → routes through cloud

HA sends a TTS URL using the external address:

https://example.duckdns.org/api/tts_proxy/abc123.mp3

The satellite cannot reach this → no audio.

Using the internal_url instead would work:

https://example.duckdns.org:8123/api/tts_proxy/abc123.mp3

Affected functionality

  • TTS responses — voice assistant replies are silent
  • Announcements (including pre-announce sounds) — assist_satellite.announce produces no audio
  • External wake word model downloads — model files hosted behind the HA API cannot be fetched

Root cause in code

HA constructs media URLs server-side. The LVA receives and passes them to mpv/urlopen without any transformation in three places inside satellite.py:

Location Code What it receives
handle_voice_event() self._tts_url = data.get("url") (lines ~193, ~219) TTS audio URL
handle_message()VoiceAssistantAnnounceRequest msg.media_id, msg.preannounce_media_id (lines ~259-262) Announcement URLs
_download_external_wake_word() external_wake_word.url (lines ~548, ~566) Wake word config/model URLs

Proposed solution: auto-discover HA internal URL via mDNS

Home Assistant already broadcasts its internal_url in the TXT record of its _home-assistant._tcp.local. mDNS service. The LVA already uses the zeroconf library for its own registration — we can reuse the same AsyncZeroconf instance to browse for HA and extract internal_url automatically.

When a media URL arrives from HA, replace scheme://host:port with the discovered internal_url values, keeping path/query/fragment intact.

Behavior

Config Source of rewrite URL
Nothing set (default) Auto-discovered from _home-assistant._tcp.local. TXT internal_url
--ha-url / HA_URL set Manual override — skip discovery, use this value
Discovery finds nothing No rewriting, original URLs pass through (current behavior)

Implementation details

1. linux_voice_assistant/zeroconf.py — add HA discovery

Extend HomeAssistantZeroconf to browse for _home-assistant._tcp.local. and extract internal_url from the TXT record:

class HomeAssistantZeroconf:
    def __init__(self, port, mac_address, host_ip_address, name=None):
        # ... existing init ...
        self.ha_internal_url: Optional[str] = None
 
    async def register_server(self) -> None:
        # ... existing service registration ...
        pass
 
    async def discover_ha(self) -> Optional[str]:
        """Browse for Home Assistant and return internal_url from TXT record."""
        info = AsyncServiceInfo(
            "_home-assistant._tcp.local.",
            "Home._home-assistant._tcp.local.",
        )
        if await info.async_request(self._aiozc.zeroconf, timeout=5000):
            properties = {
                k.decode() if isinstance(k, bytes) else k:
                v.decode() if isinstance(v, bytes) else v
                for k, v in info.properties.items()
            }
            internal_url = properties.get("internal_url")
            if internal_url:
                # Strip trailing slash if present
                self.ha_internal_url = internal_url.rstrip("/")
                _LOGGER.info(
                    "Discovered HA internal URL via mDNS: %s",
                    self.ha_internal_url,
                )
                return self.ha_internal_url
 
        _LOGGER.warning(
            "Could not discover Home Assistant via mDNS — "
            "media URL rewriting disabled"
        )
        return None

2. linux_voice_assistant/util.py — add rewrite_ha_url()

from urllib.parse import urlparse, urlunparse
 
def rewrite_ha_url(url, ha_url):
    """Rewrite media URL(s) to use the internal HA URL.
 
    Replaces scheme://host:port with ha_url while keeping
    path, query, and fragment intact.
    Local file paths and non-HTTP URLs pass through unchanged.
    Returns input unchanged when ha_url is None.
    """
    if not ha_url:
        return url
 
    if isinstance(url, list):
        return [_rewrite_single(u, ha_url) for u in url]
 
    return _rewrite_single(url, ha_url)
 
 
def _rewrite_single(url, ha_url):
    if not url or not url.startswith(("http://", "https://")):
        return url
 
    target = urlparse(ha_url)
    original = urlparse(url)
 
    rewritten = urlunparse((
        target.scheme,
        target.netloc,
        original.path,
        original.params,
        original.query,
        original.fragment,
    ))
 
    if rewritten != url:
        _LOGGER.debug("Rewrote URL: %s -> %s", url, rewritten)
 
    return rewritten

3. linux_voice_assistant/models.py — add field to ServerState

@dataclass
class ServerState:
    # ... existing fields ...
    ha_url: Optional[str] = None   # internal HA URL for media rewriting

4. linux_voice_assistant/__main__.py — wire it up

Add --ha-url as optional manual override:

parser.add_argument(
    "--ha-url",
    help="Internal Home Assistant URL (e.g. http://192.168.1.50:8123). "
         "Overrides auto-discovery. When set, media URLs from HA are "
         "rewritten to use this address.",
)

In the startup sequence, after zeroconf registration:

# Auto discovery (zeroconf, mDNS)
discovery = HomeAssistantZeroconf(
    port=args.port, name=state.name,
    mac_address=state.mac_address, host_ip_address=host_ip_address,
)
await discovery.register_server()
 
# Resolve HA internal URL for media rewriting
if args.ha_url:
    state.ha_url = args.ha_url
    _LOGGER.info("Using manual HA URL override: %s", state.ha_url)
else:
    state.ha_url = await discovery.discover_ha()

Pass to ServerState:

state = ServerState(
    # ... existing args ...
    ha_url=args.ha_url,  # may be None, updated after discovery
)

5. linux_voice_assistant/satellite.py — apply rewriting at the 3 entry points

from .util import call_all, rewrite_ha_url
 
# --- TTS URLs (handle_voice_event, ~line 193 and ~219): ---
# Before:
self._tts_url = data.get("url")
# After:
self._tts_url = rewrite_ha_url(data.get("url"), self.state.ha_url)
 
 
# --- Announcement URLs (handle_message, ~line 258-262): ---
# Before:
urls = []
if msg.preannounce_media_id:
    urls.append(msg.preannounce_media_id)
urls.append(msg.media_id)
# After:
urls = []
if msg.preannounce_media_id:
    urls.append(rewrite_ha_url(msg.preannounce_media_id, self.state.ha_url))
urls.append(rewrite_ha_url(msg.media_id, self.state.ha_url))
 
 
# --- Wake word downloads (_download_external_wake_word, ~line 548 and ~566): ---
# Before:
with urlopen(external_wake_word.url) as request:
# After:
with urlopen(rewrite_ha_url(external_wake_word.url, self.state.ha_url)) as request:
# (same for model_url further down)

6. docker-entrypoint.sh — map env variable

if [ -n "${HA_URL}" ]; then
  EXTRA_ARGS+=( "--ha-url" "$HA_URL" )
fi

7. .env.example — document the new variable

### Internal Home Assistant URL override (optional):
# Normally auto-discovered via mDNS. Set this only if auto-discovery
# does not work or you need a specific URL.
# HA_URL="http://192.168.1.50:8123"

Summary of changes

File Change
zeroconf.py Add discover_ha() — browse _home-assistant._tcp.local., extract internal_url from TXT
util.py Add rewrite_ha_url() — replace scheme+host+port, keep path
models.py Add ha_url: Optional[str] to ServerState
__main__.py Add --ha-url argument; call discover_ha() on startup; set state.ha_url
satellite.py Apply rewrite_ha_url() at the 3 URL entry points
docker-entrypoint.sh Map HA_URL env var to --ha-url
.env.example Document HA_URL

Environment

  • LVA version: v1.1.9
  • Home Assistant: 2026.3.1 with Cloud / external URL configured
  • Hardware: Raspberry Pi 4 on same LAN as HA server
  • Deployment: Docker / systemd

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions