Sonos Announcements & Notifications via audioClip (Sonos Cloud)

With the original sonos binding you can already play announcements, however depending on how you have previously started the music, music will not continue after the announcement.

With this solution I am using sonos cloud service to publish a sound file via the audioClip function.
Tested with OH3.4.1

Disadvantage: The sonos cloud service is not registered as audiosink, therefore you can only use mp3/wav files for notification but not a built in TTS service.

This solution consists out of three parts:

A. One-time installation: Creation & setup of sonos developer account
B. using a rule to manage api access within OH
C. using a script to send the actual notification (can be triggered by any other script or rule)

Step A: Creation & setup of sonos developer account

  1. Go to https://integration.sonos.com/ and create a new developer account or sign-in with your existing account
  2. Create a new control integration & credentials / a key for your integration (you can use https://www.google.com as redirect url and do not need to specify an event callback url)
  3. Open the following URL in your browser, login with your regular sonos account and authorize the new integration:
https://api.sonos.com/login/v3/oauth?client_id=<Your_Client_Credential_Key>&response_type=code&state=<Your_Test_State>&scope=playback-control-all&redirect_uri=<redirect url>
  1. Once you are logged in and forwarded to the redirect URL, you will see the authorization code in the URL.
  2. Create an access & refresh token via the sonos dev page: createToken
    5.1. Use your api key & secret from step 2 for basic-authentication (at the right side of the page)
    5.2 add your authorization code from step 4 as code (in the middle of the page)
    5.3 Click the “Try It” button to execute the request and you will get an access token & refresh token. Store both for later usage.

Step B: Manage API Access within OH

  1. Create a new string item, that will store your access token (access token is only valid for 24hours and needs to be renewed daily)
  2. Install JSONPATH transformation if you have not done it before
  3. Create a new rule, running every 24 hours (e.g. always at 3am in the morning) to refresh the access token and use the following DSL script:
var access_token_item = "<enter item name from step 1>";
var refresh_token = "<enter refresh token from part A, step 5.3>";
var auth = "<enter basic authentication from part A, step 5.1>";
  
var headers = newHashMap("authorization" -> "Basic "+auth, "accept"-> "application/json");
var result = sendHttpPostRequest("https://api.sonos.com/login/v3/oauth/access?grant_type=refresh_token&refresh_token="+refresh_token, "application/json", '',headers,10000); 

var new_access_token = transform("JSONPATH", "$.access_token", result);
postUpdate(access_token_item, new_access_token);
  1. Please note: In case the HTTP request will fail (e.g. no network connection, cloud issue, etc) your access_token_item will contain invalid data. You can always manually run the rule to get a new access token at any time

Step C: Sending an actual notification

  1. Get the unique device name from your sonos thing
  2. Create a new script, add the following javascript code and adjust the placholders:
var x, headers, token, sonos_speaker, url, body, http, accessTokenItem;
http=Java.type("org.openhab.core.model.script.actions.HTTP");

sonos_speaker = '<add the unique device name from step 1>';
accessTokenItem = '<add the name of your item from part B, step 1>';

// Sends an MP3 file to Sonos device as notification
function notify_sonos(x) {

  token = itemRegistry.getItem(accessTokenItem).getState();
  url = 'https://api.ws.sonos.com/control/api/v1/players/'+sonos_speaker+'/audioClip';
  body = ['{"name":"Notification","appId":"","priority":"HIGH","clipType":"CUSTOM","streamUrl":"'+x+'"}';
  headers = [];
  headers["Authorization"] = "Bearer "+token;
  headers["WWW-Authenticate"] =  "Basic";
  var returnvalue = http.sendHttpPostRequest(url, "application/json", body,headers, 10*1000);
}

if (String(ctx['URL']) != 'undefined') {
  notify_sonos(ctx['URL']);
} else if (String(ctx['item']) != 'undefined') {
  notify_sonos(['<URL OF YOUR OH SYSTEM>/static/sounds/',ctx['item'],'.mp3'].join(''));
}

Note 1: I have stored mp3 files in the /html/sounds folder, what will make these mp3 files available via /static/sounds per web. In addition I have named the mp3 files same as the item, whats triggering the notification (e.g. if a frontdoor_doorbell_switch item will trigger a doorbell sound the mp3 file has the same name), so that I can either pas a complete URL as parameter to the script or simple the name of an item and the script will automatically build the correct URL.

Note 2: The sound file needs to be reachable by the sonos speaker

Note 3: due to personal laziness as well as copy & past of other rules I have created the rule to refresh the token in DSL, but the notification script itself in javascript. Feel free to adopt.

Step D: Sample Usage
In order to execute the script from step C, simple add the following javascript code to your rule (there is also a blockly block to execute another rule / script and this code is generated by blockly):

var ruleManager = addFrameworkService('org.openhab.core.automation.RuleManager');

function convertDictionaryToHashMap (dict) {
  if (!dict || dict.length === 0) return null;
  var map = new java.util.HashMap();
  Object.keys(dict).forEach(function (key) {
    map.put(key, dict[key]);
  });
  return map;
}

ruleManager.runNow('<ID of your script>', true, convertDictionaryToHashMap({'URL': 'http://<OH local IP>:8080/static/sounds/sample_announcement.mp3'}));

Note: Should also work with https, but I have not tested if sonos will validate & accept any custom certificate.

5 Likes

You actually did it :+1:

Integration of Sonos cloud API in the binding should also be considered.

Sure, but that’s beyond my skills to update the binding.

There must also be a way to connect via local websocket and then use same / similar audioClip function, so that we could skip the Sonos cloud. I think that’s what homeassistant is doing

Hi!

I have successfully been able to play audioClip connecting to the local websocket.
See: GitHub - seaside1/jrule-sonos: JRule Sonos Integration

1 Like

I have added audio sinks now as well.