Few weeks ago I purchased 2 BeoPlay M5 loudspeakers.

In fact, I like them a lot - I already tried the Sonos-system but it was to closed for me (PLAY:1 has no AUX-Input), I don't like the Bose-sound anymore and I always wanted to have some B&O-speakers. Until now this was only a dream since they are really expensive - but the M5 is different.
This speaker is the cheapest in the range of the Beolink multiroom-system. Not that I am using this special feature but well it exists.

The problem with all of these cool multiroom-speakers is, that most of them use a closed API to communicate with apps.
I had e.q. one problem, when I wanted to set the volume of a special input but I only could specify the global volume of the speaker.

After some time looking around, I found out that all apps share the same GUI for system management. Soon I realized that the speakers run a dedicated web server and that they serve a webpage!

Hooray I could reach it via browser!

Soon I decided, that I want to embed these speakers in my HomeKit-setup! So no lets get around with the API!

This is the first time for me to decipher an API and I can encourage everyone, this is not at all magic! I only used my Safari-Browser and looked around the basic code of the webpage each loudspeaker served.

So first I tried to analyze all open ports of the speaker. The tool I used is NMAP ("Network Mapper") to scan for open ports.

$ sudo nmap -O 192.168.2.22

Starting Nmap 7.40 ( https://nmap.org ) at 2017-03-13 22:40 CET  
Nmap scan report for 192.168.2.22  
Host is up (0.0063s latency).  
Not shown: 992 closed ports  
PORT      STATE SERVICE  
80/tcp    open  http  
1234/tcp  open  hotline  
5000/tcp  open  upnp  
5222/tcp  open  xmpp-client  
8008/tcp  open  http  
8009/tcp  open  ajp13  
8080/tcp  open  http-proxy  
10001/tcp open  scp-config  
MAC Address: XX:XX:XX:XX:XX:XX (XX Technology)  
Device type: general purpose  
Running: Linux 3.X|4.X  
OS CPE: cpe:/o:linux:linux_kernel:3 cpe:/o:linux:linux_kernel:4  
OS details: Linux 3.2 - 4.6  
Network Distance: 1 hop  

It revealed a running webserver at port 80 so I pointed my browser to this address and found a nicely looking B&O configuration page which is also available from the apps!

The important slider is the one at the bottom and its a div with and id of VolumeCurrentSlider. After a quick search I found a JavaScript-file named page_sound_vol.js that adds this little slider some functionality.

One is for example the function currentVolWrite(value) whereas the value stands for the Integer-volume the user sets with the slider.

function currentVolWrite( value )  
{
    var volumeData = NSDK_MakeNsdkValue( { volume: value, volumeSource: "website" }, "beoSoundVolumeData" );
    return NSDK_Activate( "BeoSound:/setVolume", volumeData, "beoSoundVolumeData" );
}

Ah okay so it creates a special variable and then sends it to the next function called NSDK_Activate. This function is part of a different JavaScript-file common.js.

function NSDK_Activate( path, _value_optional /* defaults to: NSDK_MakeNsdkValue( true ) */ )  
{
    var value;
    if( typeof( _value_optional ) === 'undefined' ) {
        value = NSDK_MakeNsdkValue( true );
    } else {
        value = _value_optional;
    }

    return NSDK_SetData( path, value, "activate" );
};

So again this function just checks for the given value and passes it to the next function NSDK_SetData. This function consists of some more checking and passing the variables to the next low-level function.

function NSDK_SetData( path, data, _roles_optional /* defaults to: "value" */ )  
{
    if( !NSDK_Init() ) {
        var ret = jQuery.Deferred();
        var error = { message: "NSDK api not available" };
        console.log( error.message );
        ret.reject( error );
        return ret.promise();
    }

    var roles;
    if( typeof( _roles_optional ) === "string" ) {
        roles = _roles_optional;
    } else {
        roles = "value";
    }

    return nsdk_api.setData( path, roles, data );
};

The last and lowest API function call is the setData-function of the nsdk_api-object which is an instance of the NSDKAPIClient object.

function NSDKAPIClient() {  
  var self = this;
  this.root_uri = "/api";

  // ... some other functions ...

  this.setData = function(path, roles, value, success) {
    var ret = jQuery.Deferred();
    var uri = this.root_uri+"/setData";

    $.getJSON(uri, {'path': path, 'roles': roles, 'value': JSON.stringify(value, null), '_nocache': _nocache()})
    // ... some console output ...
    return ret.promise();
  };

 // ... more other functions ...

Okay now I finally found the root of the API and I started to reconstruct the call for an absolute volume change.

Basically the API-call should now look like:

192.168.2.22'ROOT_URI'+'URI'?path='PATH'&roles='ROLES'&value='VALUE'  
  1. The basic-address consists of an API-call so the 'ROOT_URI' stands in the source-code of the webpage as /api. The same for the 'URI' which seems to be /setData.

  2. Then the 'PATH' was given as BeoSound:/setVolume in the currentVolWrite( value )-function.

  3. The 'ROLES'-variable was preset with activate from the NSDK_Activate( ... )-function.

  4. The hardest part was to decipher the value. This thing seemed like a big object but I wasn't quite sure of its structure so I needed to investigate it further.

I was so happy when I found out, that Safari supports breakpoints in the websites-code so I set one at the $.getJSON-line and triggered a volume change.
Then I analyzed the JSON.stringify(...)-part and retrieved the wanted value:

{"type":"beoSoundVolumeData","beoSoundVolumeData":{"volume":25,"volumeSource":"website"}

So whats now the final call?

192.168.2.22/api/setData?path=BeoSound:/setVolume&roles=activate&value={"type":"beoSoundVolumeData","beoSoundVolumeData":{"volume":25,"volumeSource":"website"}}  

Awesome - Now I can set the volume with an easy link!

So whats next? Make it a Node-API, and create a HomeBridge-Platform! But thats for another time... ;)