Cannot fetch album art from local streamer device due to self-signed certificate

Hi everyone! I’ve got a bit of an issue with displaying album art on the Media Controller card.

In my local network, I have a network streamer device (WiiM Pro) that I’m using to stream music from Apple Music/Spotify/any other music service to my speakers. I have integrated it with my HA using this custom component. In general, everything works fine - playback info and control, volume control, all that stuff. Except for album art.

I have spent some time debugging the issue and pinpointed it to the SSL verification failure. The streamer box exposes an API that the integration uses to communicate with it over HTTPS and uses a self-signed certificate (understandable, it’s a local device with no outside access). The integration itself has no issue with it but the media player component does - the album art is proxied by HA and is fetched via /api/media_player_proxy/{entity_name} endpoint, which returns a 500 response.

This happens only when I’m using AirCast 2 protocol. So I suppose the album art is somehow fetched by the streamer and cached locally (although I don’t know how to verify it). If I use some other protocol (e.g. Spotify Connect) then the album art is correctly displayed. SO: works for Spotify over Spotify Connect, but doesn’t work for the same Spotify over AirPlay 2.

Here is the log generated by an attempt to fetch the image:

2023-11-29 19:46:01.156 ERROR (MainThread) [aiohttp.server] Error handling request
Traceback (most recent call last):
  File "/usr/local/lib/python3.11/site-packages/aiohttp/connector.py", line 980, in _wrap_create_connection
    return await self._loop.create_connection(*args, **kwargs)  # type: ignore[return-value]  # noqa
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/asyncio/base_events.py", line 1112, in create_connection
    transport, protocol = await self._create_connection_transport(
                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/asyncio/base_events.py", line 1145, in _create_connection_transport
    await waiter
  File "/usr/local/lib/python3.11/asyncio/sslproto.py", line 575, in _on_handshake_complete
    raise handshake_exc
  File "/usr/local/lib/python3.11/asyncio/sslproto.py", line 557, in _do_handshake
    self._sslobj.do_handshake()
  File "/usr/local/lib/python3.11/ssl.py", line 979, in do_handshake
    self._sslobj.do_handshake()
ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self-signed certificate (_ssl.c:1006)
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
  File "/usr/local/lib/python3.11/site-packages/aiohttp/web_protocol.py", line 433, in _handle_request
    resp = await request_handler(request)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/aiohttp/web_app.py", line 504, in _handle
    resp = await handler(request)
           ^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/aiohttp/web_middlewares.py", line 117, in impl
    return await handler(request)
           ^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/src/homeassistant/homeassistant/components/http/security_filter.py", line 85, in security_filter_middleware
    return await handler(request)
           ^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/src/homeassistant/homeassistant/components/http/forwarded.py", line 100, in forwarded_middleware
    return await handler(request)
           ^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/src/homeassistant/homeassistant/components/http/request_context.py", line 28, in request_context_middleware
    return await handler(request)
           ^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/src/homeassistant/homeassistant/components/http/ban.py", line 80, in ban_middleware
    return await handler(request)
           ^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/src/homeassistant/homeassistant/components/http/auth.py", line 236, in auth_middleware
    return await handler(request)
           ^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/src/homeassistant/homeassistant/components/http/headers.py", line 31, in headers_middleware
    response = await handler(request)
               ^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/src/homeassistant/homeassistant/components/http/view.py", line 148, in handle
    result = await handler(request, **request.match_info)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/src/homeassistant/homeassistant/components/media_player/__init__.py", line 1179, in get
    data, content_type = await player.async_get_media_image()
                         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/src/homeassistant/homeassistant/components/media_player/__init__.py", line 595, in async_get_media_image
    return await self._async_fetch_image_from_cache(url)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/src/homeassistant/homeassistant/components/media_player/__init__.py", line 1097, in _async_fetch_image_from_cache
    (content, content_type) = await self._async_fetch_image(url)
                              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/src/homeassistant/homeassistant/components/media_player/__init__.py", line 1108, in _async_fetch_image
    return await async_fetch_image(_LOGGER, self.hass, url)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/src/homeassistant/homeassistant/components/media_player/__init__.py", line 1274, in async_fetch_image
    response = await websession.get(url)
               ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/aiohttp/client.py", line 536, in _request
    conn = await self._connector.connect(
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/aiohttp/connector.py", line 540, in connect
    proto = await self._create_connection(req, traces, timeout)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/aiohttp/connector.py", line 901, in _create_connection
    _, proto = await self._create_direct_connection(req, traces, timeout)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/aiohttp/connector.py", line 1209, in _create_direct_connection
    raise last_exc
  File "/usr/local/lib/python3.11/site-packages/aiohttp/connector.py", line 1178, in _create_direct_connection
    transp, proto = await self._wrap_create_connection(
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/aiohttp/connector.py", line 982, in _wrap_create_connection
    raise ClientConnectorCertificateError(req.connection_key, exc) from exc
aiohttp.client_exceptions.ClientConnectorCertificateError: Cannot connect to host 192.168.88.223:443 ssl:True [SSLCertVerificationError: (1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self-signed certificate (_ssl.c:1006)')]

The notable line is:

aiohttp.client_exceptions.ClientConnectorCertificateError: Cannot connect to host 192.168.88.223:443 ssl:True [SSLCertVerificationError: (1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self-signed certificate (_ssl.c:1006)')]

192.168.88.223 is the IP of the streamer. So it looks like HA redirects the request to the streamer, but it sees SSL so it verifies the certificate and throws an error because it’s self-signed.

I’ve tried to hide the API of the streamer behind a reverse-proxy (I’m using Traefik for that) to present HA with an HTTP address and to let Traefik handle and ignore the self-signed cert. Unfortunately, it only partially worked. The streamer only uses HTTP API for some operations, while for others it seems UPnP is used, if I’m reading this code correctly. So, while the HTTP API worked fine, the integration overall broke completely :slight_smile: .

I don’t know what else I could do to fix the issue. I couldn’t find any setting that would make the media player proxy ignore invalid certificates and I don’t know how else I could bypass the issue. So I hope maybe someone here will have some ideas!

Hi @michalkurzeja Did you ever come across a solution for this?
I’ve been trying the same with a Wiim Mini and the integration didn’t provide any metadata, even track info.
So I’m using the DLNA integration instead, which does pull certain metadata, but not album art, when using Airplay. I’d really love to see that, did you have any luck?

I was able to get it to work finally with a ton of customized tricks.

Using the appdaemon I created a python script to hit iTunes. With a simple query for 1 image, no authentication is needed. I pass media_title, media_artist, and media_album_name, which is all that is needed.

Essentially, the python script pulls the image when triggered. I have an automation that fires when the song changes, and the python script grabs the url and downloads the file on the ha file system. I then use a template to store the image_name with a timestamp query, so that it doesn’t cache. If you don’t do this the image will not change, unless you flush your browser cache each song.

Then I use the custom button card to display the image using the template.

type: custom:button-card
entity: media_player.wiim
icon: mdi:music
name: WiiM
show_state: true
state:
  - value: playing
    styles:
      icon:
        - color: yellow
    state_display: >-
      [[[ if (entity.attributes.media_artist && entity.attributes.media_title) {
      return entity.attributes.media_artist + ' - ' +
      entity.attributes.media_title; } else { return 'Playing'; } ]]]
  - value: paused
    styles:
      icon:
        - color: grey
    state_display: Not Playing
  - value: idle
    styles:
      icon:
        - color: grey
    state_display: Not Playing
show_entity_picture: true
entity_picture: "[[[ return states['sensor.album_art'].state; ]]]"

If there is interest I can share more. I am more of a troubleshooter than a blogger, apologies for the sloppy post. I hope someone knows a simpler way, if it is too complex GenAI can help with the code.

In summary:

  1. Use appdaemon to call your customized python script to find and get the file from iTunes, to load a sensor (sensor.album_art), and attach a typestamp query to the URL (?{timestamp})
  2. Build an automation to trigger when the song changes
  3. build a template to define the sensor.album_art
  4. Use the custom:button-card to show the image using javascript notation for the template reference.

Easy right?