Sensor if airport express 2 is playing

WIP

Requirements

  • plistutil (libplist-util if you are using docker)
- platform: command_line
  name: 'Airplay Status'
  command: curl -s -X GET <airport-ip>:7000/info | plistutil | grep "<key>statusFlags</key>" -A1 | grep -Eo '[0-9]{1,4}'
  scan_interval: 10

Which will give you a sensor of the current statusFlags (no idea what they mean). It seems like there are some different statusFlags, but from what I’ve seen they always have 2048 between each other and the status for not connected is between 1 and 2000. So from that I’ve just created a binary sensor to test if the status minus 2048 is positive (someone is connected to airplay) or negative (not in use).

- platform: template
  sensors:
    airplay:
      friendly_name: "Airplay"
      device_class: sound
      value_template: "{{ states.sensor.bose_airplay_status.state | int -2048 > 0  }}"

So far so good.
Edit: statusflags, 0x4XX… off, 0xXXX04 on
https://openairplay.github.io/airplay-spec/status_flags.html

Edit2: All in one

- platform: command_line
  name: 'Airplay 2'
  command: expr $(echo "obase=16;" `curl -s -X GET <airport-ip>:7000/info | plistutil | grep "<key>statusFlags</key>" -A1 | grep -Eo '[0-9]{1,4}'` | bc | cut -c1) - 4 > /dev/null 2>&1 && echo ON || echo OFF
  scan_interval: 10
  device_class: sound

This look amazing, but I can’t get it to work.
As <airport-ip> I used: 192.168.50.xxx, then placed it in sensor.yaml, but that gives an error when checking config.
Where am I supposed to place it?

Edit:
Removing "device_class: sound" fixed it, and I can now see the OFF state, it doesn’t update to ON yet.

edit2:
Made it a binary sensor and it now works a lot better also with the sound class. I got libplist-util to install.
In the command I replaced plistutil with libplist-util. Is there anything else I have to change?

Hi there. Just wanted to say that these tips worked quite nicely. Thought I’d post what worked for me, as part of a an automation that automatically wakes up my older, non-Airplay-2 Marantz receiver when somebody sends audio to my Airport Express.

First, in configuration.yaml, I created a binary sensor that uses a lightly-edited version of the “all-in-one” command posted by @Orrpan above, which polls the Airport Express every 10 seconds and checks whether it is receiving audio:

binary_sensor:
  - platform: command_line
    unique_id: living_room_speaker
    name: 'Living Room Speaker'
    command: 'expr $(echo "obase=16;" `curl -s -X GET {{airport_express_ip}}:7000/info | plistutil | grep "<key>statusFlags</key>" -A1 | grep -Eo "[0-9]{1,4}"` | bc | cut -c1) - 4 > /dev/null 2>&1 && echo "true" || echo "false"'
    payload_on: 'true'
    payload_off: 'false'
    scan_interval: 10

Then, I created an automation in automations.yaml to set the “Airplay 2 Audio” scene, which turns on the AVR, and switches it to the input that has the Airport Express attached to it:

#
# ---------- Airplay ---------------------------------------------------------------------
#
# If the Living Room Speaker is detected on, turn on Airplay on the AVR
#
- alias: "Airplay Autostart"
  trigger:
    platform: state
    entity_id: binary_sensor.living_room_speaker
    to: 'on'
  action:
    service: scene.turn_on
    target:
      entity_id: scene.airplay_2_audio

The scene itself is defined in a scenes.yaml file, containing this content:

- id: airplay_2_audio
  name: Airplay 2 Audio
  entities:
    media_player.marantz_sr6011:
      state: "on"
      volume_level: 0.65               # -15.0 dB
      source: "Airplay 2"
      sound_mode: "STEREO"

…with scenes.yaml included via this line in configuration.yaml:

scene: !include scenes.yaml

Lastly, and perhaps most important in my particular case, this whole setup assumes that the plistutil binary exists somewhere on the box’s path. However, if you are using the Docker container version of Home Assistant, the container won’t have it. So you need to add the Alpine packages libplist and libplist-util into the running container. In the host operating system, you can do it as root via the following command:

docker exec -it home-assistant apk add --no-cache libplist libplist-util,

…where home-assistant is the name of your container. After you run docker-exec, it will cause the container to add the required packages, so when the binary sensor runs, the command won’t error out.

The result of all this is pretty cool. It enabled me to essentially retrofit my 2016-era Marantz AVR, which has Airplay but not Airplay 2 support. All you have to do is send audio to the Airplay 2 “Living Room Speaker,” and within a few seconds the AVR powers up and starts playing through its connected speakers. The Airport Express itself is connected to the Marantz via a Toslink to mini-Toslink optical connector.

@Orrpan, thanks again for posting the solution! It was helpful to me personally, to I wanted to return the favor in case someone else finds it useful.

4 Likes

Another option using pyscript, ended up using this because installing the plistutils wasn’t straightforward in hass os

import aiohttp
import plistlib

state.persist("pyscript.galeria_playing")


@time_trigger("period(2020/01/01, 10sec)")
async def get_giga_state():
    async with aiohttp.ClientSession() as session:
        try:
            async with session.get("http://10.0.1.2:7000/info") as res:
                data = res.read()  # requires await outside HA pyscript environemnt
                plistData = plistlib.loads(data)

                playing = plistData["statusFlags"] & 0x800 != 0

                pyscript.galeria_playing = playing

        except Exception as e:
            pyscript.galeria_playing = "Error: " + repr(e)

1 Like

Thanks for posting this. I am running hass os in a proxmox VM, so I’ll give the python script a try.

I have my airport express connected to nanoleaf panels, so I’d like to switch to a dynamic / music scene when audio is sent to it.

Thanks for posting this. I’ve installed the pyscript integration via HACS and its associated integration. I have selected the “Allow All Imports” and “Access hass as a global variable” in the integration configuration.

What do you mean by:
# requires await outside HA pyscript environemnt
Is that something I need to configure in addition to the installation I have done?

Also, I assume the IP in your script is for the Airplay Express. I have changed the script a little to reflect my setup:

import aiohttp
import plistlib

state.persist("pyscript.nanoleaf_audio")


@time_trigger("period(2022/01/01, 10sec)")
async def get_giga_state():
    async with aiohttp.ClientSession() as session:
        try:
            async with session.get("http://192.168.1.200:7000/info") as res:
                data = res.read()  # requires await outside HA pyscript environment
                plistData = plistlib.loads(data)

                playing = plistData["statusFlags"] & 0x800 != 0

                pyscript.nanoleaf_audio = playing

        except Exception as e:
            pyscript.nanoleaf_audio = "Error: " + repr(e)

I’ve saved the file as nanoleaf_audio.py in the config/pyscript folder.

I gather I look for an entity called pyscript.nanoleaf_audio for the status. Which I have found, though its state is listed as false when I am playing audio there (perhaps that is related to the await outside HA pyscript environment).

I’d be interested to know if I could setup a binary / toggle sensor in Home Assistant and set that according to whether the player is playing or not.

Thanks again for posting this.

*** update ***
It seems to be working. Returns True if playing and False if it isn’t. Doesn’t seem to update when using daap, which is interesting, but I think I can live with that. :+1:

2 Likes

Good to know it works for you

I would have liked it to be a binary sensor but not sure how to do it with py script

Regarding the comment, ha py script environment means that, running inside the py script folder, if you debug the script in a stand alone python environment (outside ha pyscript) then an await is needed for that line. I do this as it’s easier to figure out the code where you can set a breakpoint, watch variables, etc.

1 Like

How do I install libplist-util in docker?

Edit: read the post further up and found the command. Got my sensor up and running so thanks for that.

Any way to make this an integration?

Out of interest do I need to install plist Utils each time the container is updated?

@Orrpan
How do i install the plistutil on HAOS?

This installation was done using debian and docker, so no HAOS.

Hi all,

I’ve implemented this on HAOS, and I have added plistutil by logging in via ssh and executing this command:

apk add libplist-util

after that, the command from the binary sensor works fine when executing it from command line:

[core-ssh ~]$ expr $(echo "obase=16;" `curl -s -X GET 10.10.10.12:7000/info | plistutil | grep "<key>statusFlags</key>" -A1 | grep -Eo "[0-9]{1,4}"` | bc | cut -c1) - 4 > /dev/null 2>&1 && echo "true" || echo "false"
true

however the binary sensor always remains in the ‘not active’ state, and I can’t figure out why.

thanks in advance for your advice,
L

1 Like

hi, I figured out what was wrong.

plistutil was not available in the ha container context. I got around that by executing a startup.d script each time the ha container launches which adds the package

then add this line to homassistant.sh in folder startup.d

apk add libplist-util

I made a very simple docker container that exposes the Airport Express playing status via REST: GitHub - reefab/airport-express-status: Trivial Python Rest API server that does one thing: give it the hostname/ip of a airport express and it'll tell you if it's currently receiving an Airplay stream.

4 Likes

The idea is great and I was looking for something like this. However, running the pyscript code above ends up with

data = res.read() # requires await outside HA pyscript environment
_ _______ ^
AttributeError: ‘_RequestContextManager’ object has no attribute ‘read’

I dont have the skills in python how to fix this. And where is comes from.

Does anybody have a solution? Many thanks in advance.

Hi all,
I’ve got the docker implementation of Reefab running. However, I think it isn’t working with my old Airport Express 802.11g or original version. Port 7000 as being read by the script is for the RTSP protocol. When I use nmap to find out which ports are open, it does not state port 7000.
I know that the title of the topic state “Airport Express 2” but I hoped that the old version also worked.

So, to be sure, am I correct that the original Airport will not work with this script?
Thanks for your answer

This is awesome! Is there a way for us to make it into an Addon that you can configure through the UI?

Have not had the time to test it yet

Don’t know how original works, this was done using a gen 2

Works fine! Created pr to @reefab

1 Like

You can also use layer of NodeRED on top of the docker client if you can not get the sensor to work in your configuration.yml. In my example the docker is accessible on port 8234.

NodeRED:

[{"id":"b5a4580c4273494b","type":"http request","z":"1a9d952166d0cc1f","name":"Request status","method":"GET","ret":"txt","paytoqs":"ignore","url":"http://{{ docker_ip }}:{{ docker_port }}/{{ express_ip }}","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[],"x":380,"y":140,"wires":[["e975834f64d62efb"]]},{"id":"b001225c5f0babef","type":"inject","z":"1a9d952166d0cc1f","name":"Airport HiFi","props":[{"p":"express_ip","v":"hifi","vt":"str"},{"p":"docker_ip","v":"10.0.0.3","vt":"str"},{"p":"docker_port","v":"8234","vt":"str"}],"repeat":"1","crontab":"","once":true,"onceDelay":"","topic":"","x":170,"y":140,"wires":[["b5a4580c4273494b"]]},{"id":"b6e6bf5ed045ce9d","type":"function","z":"1a9d952166d0cc1f","name":"get status","func":"msg.payload = msg.payload[\"Status\"]\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":360,"y":260,"wires":[["c13031efa1d405a5"]]},{"id":"e975834f64d62efb","type":"json","z":"1a9d952166d0cc1f","name":"JSON","property":"payload","action":"obj","pretty":false,"x":350,"y":200,"wires":[["b6e6bf5ed045ce9d"]]},{"id":"c13031efa1d405a5","type":"ha-binary-sensor","z":"1a9d952166d0cc1f","name":"Airplay HiFi","entityConfig":"4ffe7c8090463de6","version":0,"state":"payload","stateType":"msg","attributes":[],"inputOverride":"allow","outputProperties":[],"x":610,"y":140,"wires":[["0b9ffebb061d47dc"]]},{"id":"0b9ffebb061d47dc","type":"debug","z":"1a9d952166d0cc1f","name":"output","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":735,"y":140,"wires":[],"l":false},{"id":"4ffe7c8090463de6","type":"ha-entity-config","server":"c843d27b.2eebe","deviceConfig":"","name":"","version":"6","entityType":"binary_sensor","haConfig":[{"property":"name","value":"Airplay HiFi"},{"property":"icon","value":""},{"property":"entity_picture","value":""},{"property":"entity_category","value":"diagnostic"},{"property":"device_class","value":"sound"}],"resend":false,"debugEnabled":false},{"id":"c843d27b.2eebe","type":"server","name":"Home Assistant","addon":true,"rejectUnauthorizedCerts":true,"ha_boolean":"","connectionDelay":false,"cacheJson":false,"heartbeat":false,"heartbeatInterval":"","statusSeparator":"","enableGlobalContextStore":false}]