Need little help with first custom integration

I am asking for help with the first integration, unfortunately I do not know Python and I would like to integrate the Internet radio with Home Assistant (so that it appears as media_player). There is an API in python: https://github.com/edberoi/python-airmusicapi/tree/main

which I want to transfer to HA, unfortunately the attempt with ChatGPT failed :wink: I have it for now, but it doesn’t work, I mean it’s trying to connect to the radio (wakes it up from standby) but that’s all. If someone can guide me to implement one function (e.g. mute), I can probably handle the rest myself. Here are the files for fun:

https://github.com/DominikWrobel/airmusic/tree/main/custom_components/airmusic

For now, as I wrote, media_player appears next to the entry in configuration.yaml

airmusic:
  host: 192.168.0.248

Thank you in advance for your help.

Your code looks pretty decent for someone who doesnt know python. Just a question to enable me to help you. Is there a reason you are not using the python api you linked to and writing your own?

I would advise to use the known working one first ( its not on pypi so you will need to put it in your custom component folder). Then make sure you have it working to your radio by creating a test script to get it to mute. When you know this is working, you can then transfer it into your HA integration (which i can help you more with when ready).

Well my first try was to use the original python api and get it to work as a media_player. I’ve tried to see if I can change the files from Frontier Silicon integration to make it work, but I had a hard time figuring out what is what. Then I used ChatGPT and it made changes to the original api file.

The Python API works I’ve posted about it here: https://community.home-assistant.io/t/airmusic-control-by-mediau/388429

I made a remote in Home Assistant using shell_command and it works. But I would like to make an integration. If you can help me with something like mute to work I think I can handle the rest.

Ok, so copy the airmusicapi init.py from the original api link into your custom components folder and rename it to airmusicapi.py

In media_player.py import it

from .airmusicapi import airmusic

Then in your init function in mediaplayer.py

self._airmusic = airmusic(ip_address, timeout)

Then instead of using

async def async_mute_volume(self, mute)

You can use

def mute_volume(self, mute):
    self._airmusic.mute = mute

Using the non async method, HA will actually run it an executor (no difference from what you have done just simpler code).

EDIT: Im making the assumption it is your reweitten api that you are having issue with as i see the mute method calls a non existant method (send_cmd). If it is some other issue, describe more and provide any logging output.

1 Like

OK, I’ve made the changes you posted and got this error:

Error while setting up airmusic platform for media_player

Traceback (most recent call last):
  File "/usr/src/homeassistant/homeassistant/helpers/entity_platform.py", line 366, in _async_setup_platform
    await asyncio.shield(awaitable)
  File "/config/custom_components/airmusic/media_player.py", line 41, in async_setup_platform
    async_add_entities([AirMusicDevice(hass, ip_address)])
                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/config/custom_components/airmusic/media_player.py", line 60, in __init__
    self._airmusic = airmusic(ip_address, timeout)
                                          ^^^^^^^
NameError: name 'timeout' is not defined

So I removed timeout from:

self._airmusic = airmusic(ip_address, timeout)

and it works - I can mute the radio!!!

Now if I can get info back if radio is muted then I can work on the rest of it!

Sorry, yes should have said to delare timeout, as in,

timeout = 10

before initiating your api.

In terms of getting status of mute.

async def async_update(self):
        try:
            self._muted = await self._hass.async_add_executor_job(self._airmusic.muted)
        except Exception as e:
            _LOGGER.error(f"Error updating AirMusic device: {e}")

Getting close, but I get an error:

Rejestrator: custom_components.airmusic.media_player
Źródło: custom_components/airmusic/media_player.py:122
integracja: AirMusic Integration (dokumentacja)
Pierwsze zdarzenie: 14:21:57 (2 zdarzenia)
Ostatnio zalogowany: 14:22:03
Error updating AirMusic device: Blocking calls must be done in the executor or a separate thread; Use `await hass.async_add_executor_job()`; at custom_components/airmusic/airmusicapi.py, line 134: result = requests.get('http://{}:{}/{}'.format(self.device_address, port, cmd), (offender: /usr/local/lib/python3.12/site-packages/urllib3/connection.py, line 219: return _HTTPConnection.putrequest(self, method, url, *args, **kwargs))

Setting timeout = 10 works fine.

Couple of things. You now have 2 async_update functions. You can only have 1, so maybe use your original and comment out the updates youre not using yet as you develop and put your mute status in that.

Also, maybe try (for the mute status),

self._muted = await self._hass.async_add_executor_job(self._airmusic.get_mute())

Ie use the function instead of the mute property.

OK, got it, now working on the rest of the commands. Thank you very much for the help!

No problem. Good luck with the rest of it and reach out if you get stuck.

Well I’m stuck :wink: I’ve menaged to set volume and get the info back, mute works well, but when I try to set turn on or any other key I can’t get it to work. Play is also a miss for me, maybe because the api has play_pause not just play?

Also no play status update, the info is in the get_playinfo in the form of xml:

Przechwytywanie

The sid defines the state:

    SID = {1, 'Stopped',
           2, 'Buffering',
           6, 'Playing',
           7, 'Ending',
           9, 'Paused',
           12, 'Reading from file',
           14, 'failed to connect', }

Ok, while the radio airmusic device may return xml, it looks like the api converts this to a dict.

As such, you can get the sid by doing this.

You are getting the playinfo in your async_update but assigning it to a local variable.

Instead, assign it to an instance variable.

self._status = await self._hass.async_add_executor_job(self._airmusic.get_playinfo)

You will need to declare it in your init function.

Then the sid will be

sid = self._status.get("sid",0)

As you can see whether it is playing or paused, do something like this in your play function

def media_play(self):
        if self._status.get("sid") != 6:
            self._airmusic.play_pause():
            self._state = STATE_PLAYING

And then similar for pause. You may need to change the sid number for each one based on what states you can play/pause from. If you need to have 2 or more states but not all do…

if self._status.get("sid") in [1,9]:

Not sure about turning on. As there is not specific api function for that how would you do it?

Got the info if playing or not working, had to use self._state instead of self._status, but it works ok!

For the power on, maybe I can use key command and add it to API? Something like this:

    def set_turn_on(self):
        resp = self.send_cmd('Sendkey?key=7')
        return None

Have no idea if this is even possible.

One more thing in the init I have

self._volume = 0

So the valume is 0 when the integration starts, how do I get the real valume set in the radio to show up?

I wouldnt set self._state to your raw info as it is a special HA variable.

Do this, and then you can also ref it in other parts.

self._status = await self._hass.async_add_executor_job(self._airmusic.get_playinfo)
self._state = STATE_PLAYING if self._status.get("sid") != 6 else STATE_PAUSED if self._status.get("sid") != 9 else STATE_IDLE

And declare it in your init

    def __init__(self, hass, ip_address):
        self._hass = hass
        self._ip_address = ip_address
        self._state = STATE_OFF
        self._volume = 0
        self._muted = False
        self._source = None
        self._airmusic = airmusic(ip_address, timeout = 10)
        self._turn_off = self.turn_off
        self._turn_on = self.turn_on
        self._stop = self.stop
        self._pause = self.pause
        self._status = {}

In terms of sending keys, the api already has a function for that. So,

    def turn_on(self, turn_on):
        self._airmusic.send_rc_key(7)

In terms of volume, you can get HA to run your async_update when creating by doing

add_entities[AirMusicDevice(hass, ip_address)], update_before_add=True)

Which will set this at startup

Thank you, just one more question I get this error while trying to turn off the radio:

[140167015801056] Unexpected exception

Traceback (most recent call last):
  File "/usr/src/homeassistant/homeassistant/components/websocket_api/commands.py", line 241, in handle_call_service
    response = await hass.services.async_call(
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/src/homeassistant/homeassistant/core.py", line 2741, in async_call
    response_data = await coro
                    ^^^^^^^^^^
  File "/usr/src/homeassistant/homeassistant/core.py", line 2784, in _execute_service
    return await target(service_call)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/src/homeassistant/homeassistant/helpers/service.py", line 977, in entity_service_call
    single_response = await _handle_entity_call(
                      ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/src/homeassistant/homeassistant/helpers/service.py", line 1049, in _handle_entity_call
    result = await task
             ^^^^^^^^^^
  File "/usr/src/homeassistant/homeassistant/components/media_player/__init__.py", line 794, in async_turn_off
    await self.hass.async_add_executor_job(self.turn_off)
  File "/usr/local/lib/python3.12/concurrent/futures/thread.py", line 58, in run
    result = self.fn(*self.args, **self.kwargs)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TypeError: AirMusicDevice.turn_off() missing 1 required positional argument: 'turn_off'

edit:
nevermind got it working with this:

    async def async_turn_on(self):
        await self._hass.async_add_executor_job(self._airmusic.send_rc_key, 7)
        self._state = STATE_ON

and removing from init

So I’ve made some progress on the integration, after fail with chatGPT i went with rewriting the whole code over using as an example the enigma2 custom component, the resoult is here:

https://github.com/DominikWrobel/airmusic/tree/main/custom_components/airmusic

It all looks good on one radio, but on my second one I have problems with getting info back to HA, this is due to authentication which is basic and looks like this:

auth=('su3g4go6sk7', 'ji39454xu/^')

The bottom radio has no auth the top has it:

Przechwytywanie

And while I can send commands to the radio, the one with auth does not give info back (only power and vol status, no info about the radio station or logo), the info is there as I can reach it using nodered. Can you help me with this auth problem?

Second problem is I want to have the next and previous track control in the card, like here:

Przechwytywanie

but I can’t get it to work, I want to change radio stations with this.

Thanks for any help!

OK, so you have moved away from the known working api again. Is there a reason for this? You never said before if this works or not.

Couple of questions.

  1. When you say second device has no auth, do you mean that it doesnt need the auth sending to it?

  2. Does both these devices provide the same xml when queried and do you query them the same? If it works in nodered, can you send me the flow?

Give me a couple of days and i will send you some code to try that should be able to do what you need. I wont be able to test as have no similar device, so may need a little joint debugging.

It will be ChatMSP! :grin:

Well the api works, but as it just sends commands to the radio via http like “/Sendkey?key=” or “/gochild?id=” and gets the response in form of “/playinfo” or similar I just use that directly in the media_player.py it is just easier for me to work on, I can get the sources names and then call them using just the code in media_player.py

  1. One device has no need for auth to get the info, for example:

Przechwytywanie

But the other needs auth or it gives 401 erro

Przechwytywanie

In nodered I can get this info by using auth basic, here is the flow:

[{"id":"45131d0546c39da9","type":"debug","z":"60fa0ad9aa6c0942","name":"debug 1","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":600,"y":240,"wires":[]},{"id":"ce553a068cb7ec00","type":"inject","z":"60fa0ad9aa6c0942","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":140,"y":240,"wires":[["0118bb5b994fdd90"]]},{"id":"0118bb5b994fdd90","type":"http request","z":"60fa0ad9aa6c0942","name":"Kuchnia","method":"GET","ret":"txt","paytoqs":"ignore","url":"192.168.0.142/list?id=1&start=1&count=20","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"basic","senderr":false,"headers":[],"x":300,"y":240,"wires":[["33c824b293c0d5ab"]]},{"id":"33c824b293c0d5ab","type":"xml","z":"60fa0ad9aa6c0942","name":"","property":"payload","attr":"","chr":"","x":450,"y":240,"wires":[["45131d0546c39da9"]]}]

All the commands in both radios are the same, like IP/playinfo, IP//list?id=1&start=1&count=20 even the ip:8080/playlogo.jpg is on both radios, but on one it needs auth!

EDIT:
Well scrap that, I forgot to init the second radio after last changes to auth I’ve made and it seems to work now, (you need to send IP/init command after restart of the radio or the playinfo wont work!)

But as you can see, I need to auth the playlogo.jpg which is on port 8080, the code I have does not work:

                    self._image_url = 'http://' + \
                                        'roosu3g4go6sk7' + ':' + \
                                        'ji39454xu/^' + \
                                        '@' + self._host + ':' + \
                                        str(self._port) + '/playlogo' + \
                                        reference.replace(":", "_")[:-1] \
                                        + '.jpg'

The auth that worked is here:

async with self._opener.get(uri, auth=aiohttp.BasicAuth('su3g4go6sk7', 'ji39454xu/^', encoding='utf-8')) as resp:

So after a long fight :rofl: I found a partial solution to the logo problem:

        # If powered on
        if self._pwstate == 'playing':
            playinfo_xml = await self.request_call('/playinfo')
            soup = BeautifulSoup(playinfo_xml, features = "xml")
            servicename = soup.station_info.renderContents().decode('UTF8')
            reference = soup.sid.renderContents().decode('UTF8')
            eventtitle = soup.song.renderContents().decode('UTF8')
            eventid = soup.artist.renderContents().decode('UTF8')

            if reference == '6':
                response = await self.hass.async_add_executor_job(
                    requests.get,
                    'http://' + self._host + ':' + str(self._port) + '/playlogo.jpg',
                    {'auth': HTTPBasicAuth('su3g4go6sk7', 'ji39454xu/^')}
                )
                self._image_url = response.url
            else:
                self._image_url = None

            _LOGGER.debug("Airmusic: [update] - Eventtitle for host %s = %s",
                          self._host, eventtitle)
            # Info of selected source and title
            self._selected_source = servicename 
            self._selected_media_content_id = eventid
            self._selected_media_title = servicename + ' - ' + eventid + ' - ' + eventtitle

The logo is showing and updated when I change the station, but with the radio that needs authentication I get this error:

Error retrieving proxied image from http://192.168.0.142:8080/playlogo.jpg?auth=%3Crequests.auth.HTTPBasicAuth+object+at+0x7fc3a7b23800%3E

And I’m stuck, and have no other ideas, any help would be great!

EDIT:

With this code:


            if reference == '6':
                response = await self.hass.async_add_executor_job(
                    requests.get,
                    'http://' + self._host + ':' + str(self._port) + '/playlogo.jpg',
                    {'auth': HTTPBasicAuth('su3g4go6sk7', 'ji39454xu/^')}
                )
                self._image_url = response.url
            else:
                self._image_url = None

the logo on the radio with authentication does not work, on the other one without auth it works and is updated every time I change the radio station.

With this code:

            if reference == '6':
                response = await self.hass.async_add_executor_job(
                    requests.get,
                    'http://su3g4go6sk7:ji39454xu%2F%5E@' + self._host + ':' + str(self._port) + '/playlogo.jpg'
                )
                self._image_url = response.url
            else:
                self._image_url = None

Logos work on both of the radios but ARE NOT updated upon station change :crazy_face: