Describe the issue you are experiencing
HomePod Push-to-Talk Intercom — Project Summary
Goal
Build a push-to-talk intercom system on Home Assistant OS (HAOS) that broadcasts live voice from a phone to one or more Apple HomePods around the house, similar to Apple's own Home Intercom feature. Requirements:
- Press-and-hold (or tap-to-start/tap-to-stop) recording on a phone
- Broadcast to multiple HomePods simultaneously (Kitchen, Bedroom, Study)
- Usable both at home and away from home
- Ideally embedded as a native-feeling card on the main Home Assistant dashboard, not just a separate panel
- Self-contained on HAOS (no reliance on a separate Unraid/Docker host)
What's Working Today
A complete, functioning system has been built and tested end-to-end:
- Icecast Intercom — a local HAOS app (custom-built, not a pre-made add-on) running Icecast, used during early development for live-streaming experiments.
- Intercom Talk — a local HAOS app (Node.js/Express) that:
- Serves a "Hold to Talk" webpage (accessed via Home Assistant's sidebar panel, using Ingress)
- Captures microphone audio in the browser, converts it to a WAV file via
ffmpeg
- Triggers a Home Assistant webhook once the recording is ready
- Music Assistant — installed as the audio delivery mechanism to the HomePods (via its AirPlay provider), after Home Assistant's built-in Apple TV integration proved unable to play the recorded files (see below).
- A Home Assistant automation ("Intercom Message") — triggered by the webhook, calls
media_player.play_media targeting the Music Assistant HomePod entities (media_player.kitchen_2, media_player.bedroom, media_player.study_2) with the recorded file's URL.
Confirmed working: press the Talk button on the sidebar panel, speak, release, and the message plays through the HomePod(s) shortly after.
Not yet done: multi-HomePod broadcast hasn't been re-tested since the automation was updated to target all three rooms (only single-room Kitchen playback has been confirmed); remote (away-from-home) access has not yet been secured/tested.
What We Tried (in rough chronological order)
- Live real-time streaming to Icecast, triggered on button press, with HomePods pre-connected to a permanently-open "silence" stream so voice could seamlessly replace it. This solved the fundamental timing conflict between Icecast (which only serves a mountpoint while actively receiving a source) and AirPlay (which takes several seconds to negotiate a connection).
- Home Assistant's built-in Apple TV integration (
pyatv) to send the stream/file to the HomePods. This consistently failed with miniaudio.DecodeError: ('failed to init decoder', ...) — confirmed via Home Assistant's own core logs to be a failure inside pyatv's bundled miniaudio library, reproducible even with a completely unrelated, valid, well-known public WAV file (i.e., not caused by anything in our own file/format/server). This appears to be a genuine bug in this version of pyatv/the Apple TV integration.
- Switched delivery to Music Assistant (its own add-on + integration, using its AirPlay provider) instead of the Apple TV integration. This worked immediately — the failure was isolated to
pyatv, not to Home Assistant's media_player.play_media action in general.
- A custom Lovelace dashboard card (
intercom-test-card), to move the Talk button from a separate sidebar panel into the actual dashboard grid, with tap-to-start/tap-to-stop interaction. This required the card's JavaScript to:
- Obtain a valid Home Assistant Ingress session (needed because the add-on's web server is only reachable through Ingress, not a directly exposed port, to avoid exposing anything to the internet)
- Use that session to open a connection (first via WebSocket for live streaming, later via a plain HTTPS file upload for a simpler tap-to-record-then-send flow) directly to the Intercom Talk add-on
- Request microphone access from within the dashboard page
Problems Encountered
Resolved
- Home Assistant Supervisor's
init: false requirement for s6-overlay-based container images (an early s6-overlay-suexec: fatal: can only run as pid 1 crash).
bashio/s6-overlay dependency was itself the source of repeated container crashes; switched to plain sh + jq for reading add-on config.
- Icecast refusing to run as root; fixed via a
<changeowner> block in icecast.xml, inserted via a single-line sed command (a multi-line sed insertion using \n had silently failed due to BusyBox sed's limited support for embedded newlines in the replacement string).
- HA Supervisor's local app store not picking up file changes without an explicit
ha store reload and a version-number bump — editing files in place was not sufficient to trigger a rebuild.
- Ingress URL construction bugs (
fetch('/talk') vs fetch('talk'), location.origin vs window.location.href) that broke requests specifically when the page was loaded through Home Assistant's Ingress proxy (which serves pages from a sub-path, not the domain root).
Unresolved — the core open problem
A custom Lovelace card cannot successfully authenticate to a Home Assistant Ingress-proxied add-on, despite the add-on's own sidebar panel (which also goes through Ingress) working perfectly.
What we found:
- The publicly-known REST endpoint (
hass.callApi('POST', 'hassio/ingress/session')) returns 401 Unauthorized when called from custom card JavaScript.
- Calling the same underlying operation via the WebSocket API instead (
hass.callWS({ type: 'supervisor/api', endpoint: '/ingress/session', method: 'post' })) does succeed and returns a session token.
- However, using that token — via
document.cookie in the same way Home Assistant's own frontend source code does it (ingress_session=<token>; path=/api/hassio_ingress/; SameSite=Strict; Secure) — does not result in a working connection. Every subsequent request to the add-on via /api/hassio_ingress/<slug>/, regardless of method (WebSocket, plain GET, POST), fails with a clean 503 Service Unavailable from Home Assistant's own proxy, and Home Assistant Supervisor's log shows [supervisor.api.ingress] Ingress for local_intercom_talk not available at the same moment — even though the add-on itself is confirmed running and healthy, and is simultaneously reachable and working correctly through its normal sidebar panel.
This was isolated with a minimal test card that does nothing but request a session and issue one plain GET request — ruling out anything specific to WebSockets, file uploads, or our own app's code as the cause.
Questions for Home Assistant / Supervisor Developers
- Is there a supported, documented way for a custom Lovelace card (not a built-in panel) to open a valid Ingress session to a local add-on from within its own JavaScript? The only working example we could find of Ingress session creation is in Home Assistant's own frontend source (
src/data/hassio/ingress.ts), which appears to call a REST endpoint that returns 401 when called the same way from outside the core frontend bundle.
- Does the session token returned by
hass.callWS({ type: 'supervisor/api', endpoint: '/ingress/session', method: 'post' }) differ in any meaningful way (scope, associated user/session context, expiry) from the one generated internally when the frontend navigates to a registered Ingress panel? Our evidence suggests it does, since the resulting cookie is accepted for session creation but not for subsequent routing to the add-on.
- Is
[supervisor.api.ingress] Ingress for local_intercom_talk not available logged only when the target add-on is unreachable, or can it also indicate a session/token mismatch or invalid-session condition specifically? The wording suggests a connectivity problem, but the add-on was confirmed running and reachable via its normal panel at the same time.
- Is there a supported way to register a "webpage"/custom element that runs with the same privileges as a built-in Ingress panel, without going through the full panel registration and navigation flow (i.e., embedded inline in a dashboard rather than as its own full-page panel)?
What type of installation are you running?
Home Assistant OS
Which operating system are you running on?
Home Assistant Operating System
Steps to reproduce the issue
-
Create a local add-on with ingress: true enabled (no direct ports mapping required to reproduce — tested both with and without one alongside Ingress).
-
Confirm the add-on works correctly through its own sidebar panel (Settings → Apps → [addon] → Open Web UI, or tapping it in the sidebar if panel_title/panel_icon are set). This confirms Ingress routing to the add-on works under normal conditions.
-
Create a custom Lovelace card (or any script running in the main dashboard frontend context, separate from the add-on's own Ingress-served page) and, from its JavaScript, request an Ingress session:
const session = await hass.callWS({
type: 'supervisor/api',
endpoint: '/ingress/session',
method: 'post'
});
This call succeeds and returns a session token (e.g. {"session": "<64-char hex token>"}).
-
Set the returned token as a cookie, matching the pattern used in Home Assistant's own frontend source (src/data/hassio/ingress.ts):
document.cookie = ingress_session=${session.session}; path=/api/hassio_ingress/; SameSite=Strict; Secure;
-
From the same card/script, make any request to the add-on via its Ingress path, e.g.:
const res = await fetch(
${location.protocol}//${location.host}/api/hassio_ingress/<addon_slug>/,
{ method: 'GET', credentials: 'include' }
);
Expected result: a 200 response from the add-on, the same as when accessed via its own sidebar panel.
Actual result: a clean 503 Service Unavailable response, every time, regardless of HTTP method (tested with GET, POST with file upload, and WebSocket connection attempts — all fail identically). Home Assistant Supervisor's log shows, at the same moment:
WARNING (MainThread) [supervisor.api.ingress] Ingress for <addon_slug> not available
...despite the add-on being confirmed running (state: started) and simultaneously reachable via its own sidebar panel in the same session.
This was isolated with a minimal test card that does nothing but request a session and issue one plain GET request, to rule out anything specific to WebSockets or file uploads.
Context: the underlying goal was to embed an add-on's own web page/functionality (a microphone-capture push-to-talk interface) directly inside a custom dashboard card, rather than as a separate sidebar panel — so users interact with it inline among their other dashboard tiles. The sidebar-panel approach already works perfectly and was used as the working baseline for comparison above. If there's a supported, documented way to grant a custom card the same Ingress access a registered panel gets, that would also resolve this — we weren't able to find one, but may have missed it.
Anything in the Supervisor logs that might be useful for us?
# Supervisor logs showing repeated Ingress failures during custom card testing,
# captured at the same moments each failed request was made from the card.
# The add-on (local_intercom_talk) was confirmed `state: started` throughout.
[supervisor.api.ingress] No valid ingress session
[supervisor.api.ingress] Ingress for local_intercom_talk not available
[supervisor.api.ingress] Ingress for local_intercom_talk not available
[supervisor.api.ingress] Ingress for local_intercom_talk not available
# The following appeared once, briefly, right after a rebuild/restart of the
# add-on's container — possibly unrelated, but included in case it's connected:
[supervisor.docker.manager] Could not get stats for addon_local_intercom_talk
[supervisor.apps.app] Could not get stats of container for app local_intercom_talk: Could not get stats for addon_local_intercom_talk
System information
System Information
| version |
core-2026.7.4 |
| installation_type |
Home Assistant OS |
| dev |
false |
| hassio |
true |
| docker |
true |
| container_arch |
amd64 |
| user |
root |
| virtualenv |
false |
| python_version |
3.14.6 |
| os_name |
Linux |
| os_version |
6.18.37-haos |
| arch |
x86_64 |
| timezone |
Australia/Sydney |
| config_dir |
/config |
Home Assistant Cloud
| logged_in |
false |
| can_reach_cert_server |
ok |
| can_reach_cloud_auth |
ok |
| can_reach_cloud |
ok |
HACS
| GitHub API |
ok |
| GitHub Content |
ok |
| GitHub Web |
ok |
| HACS Data |
ok |
| GitHub API Calls Remaining |
5000 |
| Installed Version |
2.0.5 |
| Stage |
running |
| Available Repositories |
3455 |
| Downloaded Repositories |
22 |
Home Assistant Supervisor
| host_os |
Home Assistant OS 18.1 |
| update_channel |
stable |
| supervisor_version |
supervisor-2026.07.3 |
| agent_version |
1.9.0 |
| docker_version |
29.5.3 |
| disk_total |
468.7 GB |
| disk_used |
18.0 GB |
| nameservers |
192.168.1.60, 192.168.1.20 |
| healthy |
true |
| supported |
true |
| host_connectivity |
true |
| supervisor_connectivity |
true |
| ntp_synchronized |
true |
| virtualization |
|
| board |
generic-x86-64 |
| disk_life_time |
0 % |
| supervisor_api |
ok |
| version_api |
ok |
| installed_addons |
Matter Server (9.1.0), File editor (6.0.0), Terminal & SSH (10.3.0), OpenThread Border Router (3.0.2), Grafana (12.1.0), Icecast Intercom (1.0.6), Music Assistant (2.9.9), Intercom Talk (1.0.11) |
Dashboards
| dashboards |
13 |
| resources |
10 |
| views |
11 |
| mode |
storage |
Network Configuration
| adapters |
lo (disabled), eno1 (enabled, default, auto), docker0 (disabled), hassio (disabled), veth503784e (disabled), veth35f8854 (disabled), veth3badcb3 (disabled), veth54e344a (disabled), veth67a2591 (disabled), veth11c16a6 (disabled), wpan0 (disabled), vethae82af9 (disabled), veth66f94c1 (disabled) |
| ipv4_addresses |
lo (127.0.0.1/8), eno1 (192.168.1.15/24), docker0 (172.30.232.1/23), hassio (172.30.32.1/23), veth503784e (), veth35f8854 (), veth3badcb3 (), veth54e344a (), veth67a2591 (), veth11c16a6 (), wpan0 (), vethae82af9 (), veth66f94c1 () |
| ipv6_addresses |
lo (::1/128), eno1 (fd5f:3019:ed21:6e39:f567:5a57:1bd1:8cb1/64, fe80::3700:2b90:42a:8c6a/64), docker0 (fd9d:c50c:2aba::1/64, fe80::4c54:faff:fe40:d5ae/64), hassio (fd0c:ac1e:2100::1/48, fe80::6c77:c0ff:fed9:ecc0/64), veth503784e (fe80::60fd:1bff:fea2:4a37/64), veth35f8854 (fe80::d83f:9dff:fe34:9ce9/64), veth3badcb3 (fe80::f8c6:57ff:fe92:7607/64), veth54e344a (fe80::3cdf:1aff:fe75:a12a/64), veth67a2591 (fe80::84b5:cdff:fe4b:e11c/64), veth11c16a6 (fe80::8de:ffff:fe80:3496/64), wpan0 (fd16:cc4d:512d:3b66:0:ff:fe00:fc11/64, fdd0:4bdc:d219:1:f043:d4ac:fd3f:e73c/64, fd16:cc4d:512d:3b66:0:ff:fe00:fc38/64, fd16:cc4d:512d:3b66:0:ff:fe00:fc10/64, fd16:cc4d:512d:3b66:0:ff:fe00:fc00/64, fd16:cc4d:512d:3b66:0:ff:fe00:dc00/64, fd16:cc4d:512d:3b66:4950:407f:467a:8f2d/64, fe80::cea:9fc3:5a2d:9308/64), vethae82af9 (fe80::bc8b:8ff:fefc:2b38/64), veth66f94c1 (fe80::8454:97ff:fe5b:7b4b/64) |
| announce_addresses |
192.168.1.15, fd5f:3019:ed21:6e39:f567:5a57:1bd1:8cb1, fe80::3700:2b90:42a:8c6a |
Recorder
| oldest_recorder_run |
July 14, 2026 at 8:49 AM |
| current_recorder_run |
July 28, 2026 at 12:51 PM |
| estimated_db_size |
921.43 MiB |
| database_engine |
sqlite |
| database_version |
3.53.2 |
WiiM
| configured_devices |
1 |
| reachable_devices |
1/1 |
| multiroom_masters |
0 |
| multiroom_slaves |
0 |
| first_device_api |
OK (polling: 5.0s) |
| integration_version |
2.0.0 |
| pywiim_version |
2.3.0 |
Supervisor diagnostics
No response
Additional information
System versions at time of testing (from attached diagnostics):
- Home Assistant Core: 2026.7.4
- Supervisor: 2026.07.3
- Home Assistant OS: 18.1
- Architecture: x86_64 / amd64
Client tested: Safari on iPadOS, both over home wifi and cellular (4G) — same 503 result in both cases, ruling out network path as a factor.
Note: testing via the native Home Assistant iOS app's built-in browser view produced a different, unrelated symptom (navigator.mediaDevices undefined entirely — a known limitation of that embedded browser context blocking microphone APIs). All results reported above are from genuine Safari, not the app's embedded view, to keep that separate issue out of this report.
Separately (not part of this Supervisor bug, but related context worth flagging): microphone access does not work at all inside the Home Assistant iOS Companion app's own in-app browser, even outside of Ingress/custom-card testing — this is due to WKWebView requiring the host app to implement Apple's media capture permission delegate, which the Companion app doesn't appear to do. This is already tracked separately on home-assistant/iOS (issues #1671 and #1319), so not re-reported here, but it means this feature currently only works via genuine Safari, not the Companion app, regardless of how the Ingress issue above is resolved.
Happy to provide the full custom card source, or set up a minimal reproduction repo, if useful.
Describe the issue you are experiencing
HomePod Push-to-Talk Intercom — Project Summary
Goal
Build a push-to-talk intercom system on Home Assistant OS (HAOS) that broadcasts live voice from a phone to one or more Apple HomePods around the house, similar to Apple's own Home Intercom feature. Requirements:
What's Working Today
A complete, functioning system has been built and tested end-to-end:
ffmpegmedia_player.play_mediatargeting the Music Assistant HomePod entities (media_player.kitchen_2,media_player.bedroom,media_player.study_2) with the recorded file's URL.Confirmed working: press the Talk button on the sidebar panel, speak, release, and the message plays through the HomePod(s) shortly after.
Not yet done: multi-HomePod broadcast hasn't been re-tested since the automation was updated to target all three rooms (only single-room Kitchen playback has been confirmed); remote (away-from-home) access has not yet been secured/tested.
What We Tried (in rough chronological order)
pyatv) to send the stream/file to the HomePods. This consistently failed withminiaudio.DecodeError: ('failed to init decoder', ...)— confirmed via Home Assistant's own core logs to be a failure insidepyatv's bundledminiaudiolibrary, reproducible even with a completely unrelated, valid, well-known public WAV file (i.e., not caused by anything in our own file/format/server). This appears to be a genuine bug in this version ofpyatv/the Apple TV integration.pyatv, not to Home Assistant'smedia_player.play_mediaaction in general.intercom-test-card), to move the Talk button from a separate sidebar panel into the actual dashboard grid, with tap-to-start/tap-to-stop interaction. This required the card's JavaScript to:Problems Encountered
Resolved
init: falserequirement for s6-overlay-based container images (an earlys6-overlay-suexec: fatal: can only run as pid 1crash).bashio/s6-overlay dependency was itself the source of repeated container crashes; switched to plainsh+jqfor reading add-on config.<changeowner>block inicecast.xml, inserted via a single-linesedcommand (a multi-linesedinsertion using\nhad silently failed due to BusyBoxsed's limited support for embedded newlines in the replacement string).ha store reloadand a version-number bump — editing files in place was not sufficient to trigger a rebuild.fetch('/talk')vsfetch('talk'),location.originvswindow.location.href) that broke requests specifically when the page was loaded through Home Assistant's Ingress proxy (which serves pages from a sub-path, not the domain root).Unresolved — the core open problem
A custom Lovelace card cannot successfully authenticate to a Home Assistant Ingress-proxied add-on, despite the add-on's own sidebar panel (which also goes through Ingress) working perfectly.
What we found:
hass.callApi('POST', 'hassio/ingress/session')) returns401 Unauthorizedwhen called from custom card JavaScript.hass.callWS({ type: 'supervisor/api', endpoint: '/ingress/session', method: 'post' })) does succeed and returns a session token.document.cookiein the same way Home Assistant's own frontend source code does it (ingress_session=<token>; path=/api/hassio_ingress/; SameSite=Strict; Secure) — does not result in a working connection. Every subsequent request to the add-on via/api/hassio_ingress/<slug>/, regardless of method (WebSocket, plain GET, POST), fails with a clean503 Service Unavailablefrom Home Assistant's own proxy, and Home Assistant Supervisor's log shows[supervisor.api.ingress] Ingress for local_intercom_talk not availableat the same moment — even though the add-on itself is confirmed running and healthy, and is simultaneously reachable and working correctly through its normal sidebar panel.This was isolated with a minimal test card that does nothing but request a session and issue one plain
GETrequest — ruling out anything specific to WebSockets, file uploads, or our own app's code as the cause.Questions for Home Assistant / Supervisor Developers
src/data/hassio/ingress.ts), which appears to call a REST endpoint that returns401when called the same way from outside the core frontend bundle.hass.callWS({ type: 'supervisor/api', endpoint: '/ingress/session', method: 'post' })differ in any meaningful way (scope, associated user/session context, expiry) from the one generated internally when the frontend navigates to a registered Ingress panel? Our evidence suggests it does, since the resulting cookie is accepted for session creation but not for subsequent routing to the add-on.[supervisor.api.ingress] Ingress for local_intercom_talk not availablelogged only when the target add-on is unreachable, or can it also indicate a session/token mismatch or invalid-session condition specifically? The wording suggests a connectivity problem, but the add-on was confirmed running and reachable via its normal panel at the same time.What type of installation are you running?
Home Assistant OS
Which operating system are you running on?
Home Assistant Operating System
Steps to reproduce the issue
Create a local add-on with
ingress: trueenabled (no directportsmapping required to reproduce — tested both with and without one alongside Ingress).Confirm the add-on works correctly through its own sidebar panel (Settings → Apps → [addon] → Open Web UI, or tapping it in the sidebar if
panel_title/panel_iconare set). This confirms Ingress routing to the add-on works under normal conditions.Create a custom Lovelace card (or any script running in the main dashboard frontend context, separate from the add-on's own Ingress-served page) and, from its JavaScript, request an Ingress session:
const session = await hass.callWS({
type: 'supervisor/api',
endpoint: '/ingress/session',
method: 'post'
});
This call succeeds and returns a session token (e.g.
{"session": "<64-char hex token>"}).Set the returned token as a cookie, matching the pattern used in Home Assistant's own frontend source (src/data/hassio/ingress.ts):
document.cookie =
ingress_session=${session.session}; path=/api/hassio_ingress/; SameSite=Strict; Secure;From the same card/script, make any request to the add-on via its Ingress path, e.g.:
const res = await fetch(
${location.protocol}//${location.host}/api/hassio_ingress/<addon_slug>/,{ method: 'GET', credentials: 'include' }
);
Expected result: a 200 response from the add-on, the same as when accessed via its own sidebar panel.
Actual result: a clean
503 Service Unavailableresponse, every time, regardless of HTTP method (tested with GET, POST with file upload, and WebSocket connection attempts — all fail identically). Home Assistant Supervisor's log shows, at the same moment:WARNING (MainThread) [supervisor.api.ingress] Ingress for <addon_slug> not available
...despite the add-on being confirmed running (
state: started) and simultaneously reachable via its own sidebar panel in the same session.This was isolated with a minimal test card that does nothing but request a session and issue one plain GET request, to rule out anything specific to WebSockets or file uploads.
Context: the underlying goal was to embed an add-on's own web page/functionality (a microphone-capture push-to-talk interface) directly inside a custom dashboard card, rather than as a separate sidebar panel — so users interact with it inline among their other dashboard tiles. The sidebar-panel approach already works perfectly and was used as the working baseline for comparison above. If there's a supported, documented way to grant a custom card the same Ingress access a registered panel gets, that would also resolve this — we weren't able to find one, but may have missed it.
Anything in the Supervisor logs that might be useful for us?
System information
System Information
Home Assistant Cloud
HACS
Home Assistant Supervisor
Dashboards
Network Configuration
Recorder
WiiM
Supervisor diagnostics
No response
Additional information
System versions at time of testing (from attached diagnostics):
Client tested: Safari on iPadOS, both over home wifi and cellular (4G) — same 503 result in both cases, ruling out network path as a factor.
Note: testing via the native Home Assistant iOS app's built-in browser view produced a different, unrelated symptom (
navigator.mediaDevicesundefined entirely — a known limitation of that embedded browser context blocking microphone APIs). All results reported above are from genuine Safari, not the app's embedded view, to keep that separate issue out of this report.Separately (not part of this Supervisor bug, but related context worth flagging): microphone access does not work at all inside the Home Assistant iOS Companion app's own in-app browser, even outside of Ingress/custom-card testing — this is due to WKWebView requiring the host app to implement Apple's media capture permission delegate, which the Companion app doesn't appear to do. This is already tracked separately on home-assistant/iOS (issues #1671 and #1319), so not re-reported here, but it means this feature currently only works via genuine Safari, not the Companion app, regardless of how the Ingress issue above is resolved.
Happy to provide the full custom card source, or set up a minimal reproduction repo, if useful.