Bose SoundTouch PHP Gateway – Teil 2

Im zweiten Teil vom Bose SoundTouch PHP Gateway wird das Skript um die Funktion “play_soundfile” erweitert. Damit wird das gewünschte Soundfile von einem UPnP / DLNA Server abgespielt und nach der Wiedergabe wird der letzte Zustand auf dem SoundTouch wiederhergestellt. Das Skript funktioniert bei mir ganz gut, es ist aber noch lange nicht perfekt!

UPnP / DLNA Media Server

Die Soundfiles müssen über einen UPnP / DLNA Media Servers bereitgestellt werden. Ich habe den MiniDLNA Server in der Version 1.1.5 verwendet. Achtung! Ältere Versionen vom MiniDLNA Server machen Probleme mit dem Bose SoundTouch. Wer bereits einen UPnP / DLNA Server hat, kann auch diesen verwenden. Es wird kein zusätzlicher Media Server benötigt!

Zum Testen habe ich über unser Text to Speech Gateway ein paar Texte umgewandelt und in das Media Share kopiert. Der Name der Datei sollte möglichst eindeutig sein, damit die Suche später auch die richtige Datei findet.

Loxone Config

Es gibt einen neuen “Befehl bei EIN” der für das Abspielen eines Soundfiles genutzt werden kann. Weitere Infos findet ihr im Beitrag Bose SoundTouch PHP Gateway – Teil 1.

Befehl Beschreibung
/bose/api.php?device=171&action=playfile&value=TTSKlingel Ausgabe von TTSKlingel.mp3

SoundTouch PHP Gateway – Teil 2

Es sind ein paar Zeilen mehr geworden, am Aufbau an sich hat sich aber nichts geändert. Es gibt jetzt drei Variablen, die angepasst werden können bzw. müssen. “Subnet” sieht jetzt etwas anders aus, “SoundfileVolume” ist die Lautstärke mit der das Soundfile abgespielt wird und die “DLNA_ID” wird am besten über die Bose SoundTouch API abgefragt. Über die URL http://<IP SoundTouch>:8090/sources bekommt man alle Sourcen vom SoundTouch aufgelistet. Unter “sourceAccount” findet ihr die ID von dem UPnP / DLNA Media Server.

<html>
<head>
	<meta charset="utf-8" />
	<title>BOSE SOUNDTOUCH GATEWAY</title>
    <style>

    body {
	font-size: 16px;
	font-family: Sans-Serif;
	}
    </style>
</head>

<body>

<?php

$Device = $_GET['device'];
$Action = $_GET['action'];
$Value = $_GET['value'];

if (!is_numeric($Device)) { die ('Wrong or missing parameter DEVICE');}

//////////////////////////////////////////
// Variables
$Subnet = "192.168.1.";

$SoundfileVolume = 30;

$DLNA_ID = "4d696e98-555c-165e-9d53-00166d0a8b1d/0";

//////////////////////////////////////////
// Prepare

$DeviceIP = "$Subnet$Device";

$IP = explode('.',$DeviceIP); 

if (
$IP[0]<=255 && $IP[1]<=255 &&  
$IP[2]<=255 && $IP[3]<=255 &&  
preg_match("!^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$!",$DeviceIP))
{
	echo "Target IP: ". $DeviceIP ."<br>";
	echo "---------------------------------<br>"; 
}
else
{
	die ('Wrong IP Address! Please check subnet variable an device parameter');
}

//////////////////////////////////////////
// Functions

// Press Key
function PressKey($key)
{
	global $DeviceIP;

	$xmldata1 = "<key state=press sender=Gabbo>". $key ."</key>";
	$xmldata2 = "<key state=release sender=Gabbo>". $key ."</key>";
	for ($i=1 ; $i < 3 ; $i++) {
		$curl = curl_init();
		curl_setopt_array($curl, array(
			CURLOPT_URL => "http://". $DeviceIP .":8090/key",
			CURLOPT_HEADER => false,
			CURLOPT_RETURNTRANSFER => true,
			CURLOPT_POST => true,
			CURLOPT_POSTFIELDS => ${"xmldata".$i},
			CURLOPT_HTTPHEADER => array("Content-type: text/xml")));
		$result = curl_exec($curl);
		curl_close($curl);
	}
}

// Get Status
function Status()
{
	global $DeviceIP;

	$curl = curl_init();
	curl_setopt_array($curl, array(
		CURLOPT_URL => "http://". $DeviceIP .":8090/now_playing",
		CURLOPT_HEADER => false,
		CURLOPT_RETURNTRANSFER => true));
	$result = curl_exec($curl);
	curl_close($curl);

	$xmldata = new SimpleXMLElement ($result);
	$Source = $xmldata->ContentItem["source"];

	$Power = 1;
	switch ($Source) {
		case "STANDBY":
			$Mode = "Standby";
			$Power = 0;
		break;

		case "TUNEIN":
			$Mode = "TUNEIN";
		break;

		case "AUX":
			$Mode = "AUX";
		break;

		case "STORED_MUSIC":
			$Mode = "NAS";
		break;
		
		case "BLUETOOTH":
			$Mode = "Bluetooth";
		break;

		case "INVALID_SOURCE":
			$Mode = "Fehler";
		break;
		
		case "INTERNET_RADIO":
			$Mode = "Internet Radio";
		break;
		
		case "SPOTIFY":
			$Mode = "Spotify";
		break;

		case "BLUETOOTH":
			$Mode = "Bluetooth";
		break;
		
		default:
			$Mode = "";
			$Power = 0;
}

	// get now playing
	$NowPlaying = utf8_decode($xmldata->stationName);

	// get contentItem - ItemName
	$CIItemName = utf8_decode($xmldata->ContentItem->itemName);

	// get contentItem - Location
	$CILocation = utf8_decode($xmldata->ContentItem["location"]);

	// get contentItem - SourceAccount
	$CISourceAccount = utf8_decode($xmldata->ContentItem["sourceAccount"]);

	// get description
	$Description = utf8_decode($xmldata->description);

	// get timetotal
	$TimeTotal = utf8_decode($xmldata->time["total"]);		
	
	// get logo
	$LogoURL = utf8_decode($xmldata->art);

	// return results       
	return array(
		"Mode"    	=> $Mode,
		"Power"   	=> $Power,
		"nowplaying"    => $NowPlaying,
		"description"   => $Description,
		"Source"	=> $Source,
		"CIItemName"	=> $CIItemName,
		"CILocation"	=> $CILocation,
		"CISourceAccount"	=> $CISourceAccount,
		"TimeTotal"       => $TimeTotal,
		"LogoURL"       => $LogoURL
	);
}

// Volume
function Get_Volume()
{
	global $DeviceIP;

	$curl = curl_init();
	curl_setopt_array($curl, array(
		CURLOPT_URL => "http://". $DeviceIP .":8090/volume",
		CURLOPT_HEADER => 0,
		CURLOPT_RETURNTRANSFER => 1));
		
	$result = curl_exec($curl);
	$xmldata = new SimpleXMLElement($result);
	$targetvolume = (integer)$xmldata->targetvolume;
	$actualvolume = (integer)$xmldata->actualvolume;
	$mutedvolume = (integer)$xmldata->muteenabled;
	// return results
	return array(   "targetvolume"  => $targetvolume,
					"actualvolume"  => $actualvolume,
					"mutedvolume"   => $mutedvolume);
}

function Set_Volume($volume)
{
	global $DeviceIP;

	$xmldata = "<volume>". $volume ."</volume>";
	$curl = curl_init();
	curl_setopt_array($curl, array(
		CURLOPT_URL => "http://". $DeviceIP .":8090/volume",
		CURLOPT_HEADER => 0,
		CURLOPT_RETURNTRANSFER => 1,
		CURLOPT_POST => 1,
		CURLOPT_POSTFIELDS => $xmldata,
		CURLOPT_HTTPHEADER => array("Content-type: text/xml")));
		
	$result = curl_exec($curl);
	curl_close($curl);
}

function Set_ContentItem($itemName, $location, $source, $sourceAccount)
{
	global $DeviceIP;
	// contentItemSourceAccount
	$xmldata = "<ContentItem source=" . $source . " location='" .  $location . "' sourceAccount='" . $sourceAccount . "'><itemName>" . $itemName . "</itemName></ContentItem>";
	$curl = curl_init();
	curl_setopt_array($curl, array(
		CURLOPT_URL => "http://". $DeviceIP .":8090/select",
		CURLOPT_HEADER => 0,
		CURLOPT_RETURNTRANSFER => 1,
		CURLOPT_POST => 1,
		CURLOPT_POSTFIELDS => $xmldata,
		CURLOPT_HTTPHEADER => array("Content-type: text/xml")));
		
	$result = curl_exec($curl);
	curl_close($curl);
}
	
function Switch_Power($Value,$PowerState)
{
	if (is_null($Value))
	{
		PressKey("POWER");
		echo "Switch Power State";
	}
	else
	{
		if ($PowerState == 1 && ($Value == 0 || $Value == "off"))
		{
			echo "Switch Power Off";
			PressKey("POWER");
		}

		if ($PowerState == 0 && ($Value == 1 || $Value == "on"))
		{
			echo "Switch Power On";
			PressKey("POWER");
		}
	}
}

function Play_Soundfile($Value)
{
	global $DeviceIP, $DLNA_ID;
	
	// contentItemSourceAccount
	$xmldata = "<ContentItem source=STORED_MUSIC sourceAccount='". $DLNA_ID ."' location='". $Value ."'></ContentItem>";
	echo $xmldata;
	$curl = curl_init();
	curl_setopt_array($curl, array(
		CURLOPT_URL => "http://". $DeviceIP .":8090/select",
		CURLOPT_HEADER => 0,
		CURLOPT_RETURNTRANSFER => 1,
		CURLOPT_POST => 1,
		CURLOPT_POSTFIELDS => $xmldata,
		CURLOPT_HTTPHEADER => array("Content-type: text/xml")));
	$result = curl_exec($curl);
	curl_close($curl);
}

function Search_Soundfile($Value)
{
	global $DeviceIP, $DLNA_ID;

	$xmldata = "<search source='STORED_MUSIC' sourceAccount='". $DLNA_ID ."'><startItem>1</startItem><numItems>100</numItems><searchTerm filter='track'>". $Value ."</searchTerm></search>";
	$curl = curl_init();
	curl_setopt_array($curl, array(
		CURLOPT_URL => "http://". $DeviceIP .":8090/search",
		CURLOPT_HEADER => 0,
		CURLOPT_RETURNTRANSFER => 1,
		CURLOPT_POST => 1,
		CURLOPT_POSTFIELDS => $xmldata,
		CURLOPT_HTTPHEADER => array("Content-type: text/xml")));
	$result = curl_exec($curl);
	$xmldata = new SimpleXMLElement($result);
	
	$TotalItems = utf8_decode($xmldata->totalItems);
	$Location = utf8_decode($xmldata->items->item->ContentItem["location"]);
		
	// return results
	return array(   "TotalItems"  => $TotalItems,
					"Location"   => $Location);	
}

//////////////////////////////////////////

$Device = Status();
$StartVolume = Get_Volume();
//////////////////////////////////////////

switch ($Action)
{

// KEY
	case "key":
		if (empty($Value)) { die ('Missing parameter VALUE');}
		$Value = strtoupper($Value);
		PressKey($Value);
	break;

// POWER
	case "power":
		Switch_Power($Value,$Device['Power']);
	break;

// GETVOL
	case "getvol":
		echo "Lautstärke: ". $StartVolume['targetvolume'] ."%";
	break;

// SETVOL
	case "setvol":
		if (empty($Value)) { die ('Missing parameter VALUE');}
		Set_Volume($Value);
		echo "Lautstärke Neu: ". $Value ."%";
	break;

// VOLUP
	case "volup":
		if (empty($Value))
		{
			$Value = 2;
		}
		$TargetVolume = $StartVolume['targetvolume'] + $Value;
		Set_Volume($TargetVolume);
		echo "Lautstärke Neu: ".  $TargetVolume ."%";
	break;

// VOLDOWN
	case "voldown":
		if (empty($Value))
		{
			$Value = 2;
		}
		$TargetVolume = $StartVolume['targetvolume'] - $Value;
		Set_Volume($TargetVolume);
		echo "Lautstärke Neu: ".  $TargetVolume ."%";
	break;

// PLAYFILE
	case "playfile":

		if (empty($Value)) { die ('Missing parameter VALUE');}

		// Search Soundfile
		$SoundFile = Search_Soundfile($Value);

		if ($SoundFile['TotalItems'] > 1)
		{
			die ('More than one soundfile found! Make sure the file name is unique.');
		}
		
		// Play soundfile
		echo "Play soundfile: ". $Value ."<br>";
		echo "Location: ". $SoundFile['Location'] ."<br>";	
		
		PressKey("STOP");
		
		// Set volume for play soundfile
		Set_Volume($SoundfileVolume);
		
		Play_Soundfile($SoundFile['Location']);
		
		// Wait for soundfile end
		sleep(1); // Wait to read soundfile length
		
		$DeviceNow = Status(); // Get status

		$TimeTotal = $DeviceNow['TimeTotal'];
		echo "Time Total: ". $TimeTotal ."<br>";
		
		if ($TimeTotal > 1)
		{
			sleep($TimeTotal); // Sleep as long as soundfile length
		}
		else
		{
			sleep(2); // If soundfile is to short to read length => sleep 2 seconds
		}
		
		// Set last volume
		Set_Volume($StartVolume['actualvolume']);

		// Set last source
		if ($Device['Source'] == "STANDBY" OR $Device['Source'] == "INVALID_SOURCE")
		{
			Switch_Power(1,0); // Power off
		}
		else
		{
			Set_ContentItem($Device['CIItemName'], $Device['CILocation'], $Device['Source'], $Device['CISourceAccount']);
		}
	break;

// DEFAULT
	default:
		echo "Status: " . $Device[Mode];
		echo "<br>";
		echo "Description: ". $Device[description];
		echo "<br>";
		echo "<br>";		
		echo "<img src=" . $Device[LogoURL] . ">";
	break;
}
?>

10 Gedanken zu „Bose SoundTouch PHP Gateway – Teil 2“

  1. Hey,

    ist das eigentlich die letzte Version, oder hast du daran noch weiter gearbeitet… ich finde die API echt klasse und werde meine Boxen jetzt auch in Loxone einbinden… das war das letzte was ich noch “zu fuss” beim Haus verlassen ausschalten musste…

    1. Hallo Nico,

      es freut mich, dass dir die API gefällt! Das ist berits die letzte Version die ich auch im Einsatz habe. Mehr Funktionen benötigen wir aktuell nicht. Es könnte aber sein, dass im LoxBerry Projekt (könnte aber auch ein anderes Projekt gewesen sein) eine erweiterte Version enthalten ist.

      Viele Grüße
      Stefan

  2. Wenn ich es nicht vor 2 Wochen eingerichtet hätte und es nicht funktioniert hätte, würde ich sagen hier geht was nicht 😉

    Ich weiß nicht ob es mit einem Update von Bose zu tun hat… aber ich kann mit Power Value=0 nicht mehr ausschalten….
    Auf der Status Seite steht im eingeschalteten Zustand auch nichts mehr…

    Wenn Sie aus ist, steht dort Standby….
    Allerdings kann ich jetzt mit Power Value=1 im eingeschalteten Zustand ausschalten… Geht also als Toggle…
    Wobei das nicht so toll ist, weil ich will ja z.B. Ausschalten wenn ich das Haus verlasse und nicht einschalten, wenn Sie schon aus war ;_)

    Kannst du das bei deinen Boxen evtl. nachvollziehen?

  3. ich habe den Fehler gefunden… Es wurde ja auf TuneIN statt WebRadio umgestellt…

    Auf der Statusseite der Box ist die SOURCE bei mir nun TUNEIN, diesen Status gab es bisher nicht, daher wird die Box nicht als “An” gesehen und somit geht auch das ausschalten nicht mehr…

    Einfach in die Switch-Schleife in Zeile 93 noch einfügen:

    case “TUNEIN”:
    $Mode = “TUNEIN”;
    break;

    Dann wird auch TUNEIN erkannt… und alles läuft wieder rund 🙂

    1. Hallo Stefan,

      ich habe es noch nicht mit PHP7 probiert, sollte aber funktionieren?! Hast du Probleme mit dem Bose SoundTouch PHP Gateway auf deinem System?

      Viele Grüße
      Stefan

  4. I am a basic user to Loxone and have most things set up , could someone just give a simple step by step guide to getting the Bose working on Loxone config maybe with some examples please.

  5. hi and thanks for the great job !
    i have one problem , with the playfile function, it works great with another file played with DLNA, but when listening to tunein, the radio won’t come back after the file is played ??
    any idea why ?
    Thank you 🙂

  6. mal eine ganz pauschale Frage: wo bau ich das ganze in meine Loxone-Config ein? Nehmt ihr da direkt die einzelnen virtuellen Ausgänge / Eingänge oder gibt’s da ein einen “Audioplayer” Baustein den ich dann diese HTTP-Commands als Ein/Ausgang mitgeben kann?

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert