diff --git a/.coveralls.yml b/.coveralls.yml
new file mode 100755
index 0000000..71578e5
--- /dev/null
+++ b/.coveralls.yml
@@ -0,0 +1,3 @@
+service_name: travis-ci
+coverage_clover: clover.xml
+json_path: coveralls.json
\ No newline at end of file
diff --git a/.editorconfig b/.editorconfig
old mode 100644
new mode 100755
diff --git a/.gitignore b/.gitignore
old mode 100644
new mode 100755
index 3b77c7c..053d9bb
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,7 @@
# Created by .ignore support plugin (hsz.mobi)
/vendor/
-/test/env.yml
+/tests/env.yml
/phpunit.xml
/composer.lock
/.php_cs.cache
+/.idea/
\ No newline at end of file
diff --git a/.styleci.yml b/.styleci.yml
old mode 100644
new mode 100755
diff --git a/.travis.yml b/.travis.yml
old mode 100644
new mode 100755
index 4bb081c..1dae047
--- a/.travis.yml
+++ b/.travis.yml
@@ -4,12 +4,30 @@ php:
- 5.6
- 7.0
- 7.1
+ - 7.2
+ - nightly
-sudo: false
+matrix:
+ allow_failures:
+ - php: nightly
+
+env:
+ - COMPOSER_OPTS=""
+ - COMPOSER_OPTS="--prefer-lowest"
install:
- - travis_retry composer install --no-interaction --prefer-source
+ - composer self-update --snapshot
+ - composer update $COMPOSER_OPTS
script:
- - cp test/env.yml.dist test/env.yml
- - vendor/bin/phpunit --verbose --coverage-text
+ - vendor/bin/phpunit --coverage-clover=clover.xml
+ - tests/lint.sh
+
+after_success:
+ - vendor/bin/coveralls
+
+git:
+ depth: 5
+
+dist: trusty
+sudo: false
\ No newline at end of file
diff --git a/CREDITS.md b/CREDITS.md
new file mode 100755
index 0000000..9c4f083
--- /dev/null
+++ b/CREDITS.md
@@ -0,0 +1,5 @@
+This api is highly inspired by the excellent Github project
+
+[KnpLabs/php-github-api](https://github.com/KnpLabs/php-github-api)
+
+[duncan3dc/sonos](https://github.com/duncan3dc/sonos)
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
new file mode 100755
index 0000000..0c95716
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2013 Thomas Park
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
\ No newline at end of file
diff --git a/README.md b/README.md
old mode 100644
new mode 100755
index f6b5ebd..ef1eec0
--- a/README.md
+++ b/README.md
@@ -1,9 +1,8 @@
# Yamaha MusicCast API PHP Library
-A simple wrapper for the Yamaha MusicCast API.
-Not all call's have been implemented yet so pull requests are welcome!
+A php library for interacting with Yamaha MusicCast speakers.
-[](https://travis-ci.org/samvdb/php-musiccast-api)
+[](https://travis-ci.org/grandDam/php-musiccast-api)
Based on the API specification found at [https://jayvee.com.au/downloads/commands/yamaha/YXC_API_Spec_Basic.pdf](https://jayvee.com.au/downloads/commands/yamaha/YXC_API_Spec_Basic.pdf)
@@ -25,27 +24,24 @@ $ curl -s http://getcomposer.org/installer | php
Via composer
```bash
-$ composer require samvdb/php-musiccast-api php-http/guzzle6-adapter
+$ composer require granddam/php-musiccast-api php-http/guzzle6-adapter
```
You can install any adapter you want but guzzle is probably fine for what you want to do.
-## Creating a client
+## Examples
+Start all groups playing music
```php
-$yamaha = new MusicCast\Client([
- 'host' => 'localhost',
- 'port' => 80, // default value
-]);
-```
-
-## Using the API
-
-
-```php
-$result = $yamaha->api('zone')->status('main');
-print_r($result);
-
+require 'vendor/autoload.php';
+
+$musicCast = new MusicCast\Network();
+$controllers = $musicCast->getControllers();
+foreach ($controllers as $controller) {
+ echo $controller->getGroup() + "\n";
+ echo "\tState: " . $controller->getStateName() . "\n";
+ $controller->play();
+}
```
## Enabling events
@@ -89,11 +85,6 @@ while(true) {
$ composer test
```
-## Credits
-
-This api is highly inspired by the excellent Github api client made by KnpLabs!
-
-[KnpLabs/php-github-api](https://github.com/KnpLabs/php-github-api)
diff --git a/composer.json b/composer.json
old mode 100644
new mode 100755
index 558f8ff..f341a64
--- a/composer.json
+++ b/composer.json
@@ -1,46 +1,65 @@
{
- "name": "samvdb/php-musiccast-api",
- "description": "PHP Wrapper for Yamaha MusicCast API",
- "type": "library",
- "keywords": ["yamaha", "musiccast", "api"],
- "license": "MIT",
- "authors": [
- {
- "name": "Sam Van der Borght",
- "email": "samvanderborght@gmail.com"
- }
- ],
- "require": {
- "php": "^5.5 || ^7.0",
- "psr/http-message": "^1.0",
- "psr/cache": "^1.0",
- "php-http/httplug": "^1.1",
- "php-http/discovery": "^1.0",
- "php-http/client-implementation": "^1.0",
- "php-http/client-common": "^1.3",
- "php-http/cache-plugin": "^1.2",
- "symfony/yaml": "^3.2",
- "myclabs/php-enum": "^1.5"
+ "name": "granddam/php-musiccast-api",
+ "description": "A PHP library for interacting with Yamaha MusicCast speakers",
+ "type": "library",
+ "keywords": [
+ "yamaha",
+ "musiccast",
+ "api",
+ "client"
+ ],
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "Sam Van der Borght",
+ "email": "samvanderborght@gmail.com"
},
- "require-dev": {
- "squizlabs/php_codesniffer": "^2.7",
- "phpunit/phpunit": "^4.0 || ^5.5",
- "phpunit/php-code-coverage": "^4",
- "php-http/guzzle6-adapter": "^1.0",
- "guzzlehttp/psr7": "^1.2",
- "phpro/grumphp": "^0.11.1",
- "nikic/php-parser": "^3.0",
- "friendsofphp/php-cs-fixer": "^2.0"
- },
- "autoload": {
- "psr-4": { "MusicCast\\": "lib/MusicCast/" }
- },
- "scripts": {
- "test": "vendor/bin/phpunit"
- },
- "extra": {
- "branch-alias": {
- "dev-master": "1.0-dev"
- }
+ {
+ "name": "Damien SUROT",
+ "email": "damien@toxeek.com"
+ }
+ ],
+ "require": {
+ "php": "^5.6 || ^7.0",
+ "psr/http-message": "^1.0",
+ "psr/cache": "^1.0",
+ "php-http/httplug": "^1.1",
+ "php-http/discovery": "^1.0",
+ "php-http/client-implementation": "^1.0",
+ "php-http/client-common": "^1.3",
+ "php-http/cache-plugin": "^1.2",
+ "symfony/yaml": "^3.2",
+ "myclabs/php-enum": "^1.5",
+ "cache/doctrine-adapter": "^1.0",
+ "doctrine/cache": "^1.4"
+ },
+ "require-dev": {
+ "squizlabs/php_codesniffer": "^2.7",
+ "mockery/mockery": "^0.9.4",
+ "phpunit/phpunit": "^5.7.14",
+ "phpunit/php-code-coverage": "^4",
+ "php-http/guzzle6-adapter": "^1.0",
+ "guzzlehttp/psr7": "^1.2",
+ "phpro/grumphp": "^0.11.1",
+ "nikic/php-parser": "^3.0",
+ "friendsofphp/php-cs-fixer": "^2.0",
+ "satooshi/php-coveralls": "^1.0"
+ },
+ "autoload": {
+ "psr-4": {
+ "MusicCast\\": "src/",
+ "MusicCastTests\\": "tests/"
+ }
+ },
+ "scripts": {
+ "test": "vendor/bin/phpunit"
+ },
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0-dev"
}
+ },
+ "conflict": {
+ "php-http/message-factory": "< 1.0.2"
+ }
}
diff --git a/grumphp.yml b/grumphp.yml
old mode 100644
new mode 100755
index f239ccb..7dc771f
--- a/grumphp.yml
+++ b/grumphp.yml
@@ -10,7 +10,7 @@ parameters:
standard: PSR2
ignore_patterns:
- "spec/*Spec.php"
- - "test/*.php"
+ - "tests/*.php"
metadata:
priority: 300
phpcsfixer: ~
@@ -18,7 +18,6 @@ parameters:
phpparser:
visitors:
no_exit_statements: ~
- never_use_else: ~
forbidden_function_calls:
blacklist: [var_dump]
phpunit:
diff --git a/lib/MusicCast/Api/Network.php b/lib/MusicCast/Api/Network.php
deleted file mode 100644
index 1b1b998..0000000
--- a/lib/MusicCast/Api/Network.php
+++ /dev/null
@@ -1,11 +0,0 @@
-
- */
-class Network extends AbstractApi
-{
-}
diff --git a/lib/MusicCast/Api/System.php b/lib/MusicCast/Api/System.php
deleted file mode 100644
index 52a540b..0000000
--- a/lib/MusicCast/Api/System.php
+++ /dev/null
@@ -1,60 +0,0 @@
-
- */
-class System extends AbstractApi
-{
- /**
- * For retrieving basic information of a Device
- *
- * @return array
- */
- public function deviceInfo()
- {
- return $this->get('/getDeviceInfo');
- }
-
- /**
- * For retrieving feature information equipped with a Device
- */
- public function features()
- {
- return $this->get('/getFeatures');
- }
-
- public function networkStatus()
- {
- return $this->get('/getNetworkStatus');
- }
-
- public function functionStatus()
- {
- return $this->get('/getFuncStatus');
- }
-
- public function autoPowerStandby($enable = true)
- {
- throw new \Exception('Not implemented');
- }
-
- public function locationInfo()
- {
- return $this->get('/getLocationInfo');
- }
-
- public function sendIrCode($code)
- {
- throw new \Exception('Not implemented');
-
- return $this->get('/sendIrCode?code=1');
- }
-
- public function get($path, array $parameters = array(), array $requestHeaders = array())
- {
- return parent::get('/system' . $path, $parameters, $requestHeaders);
- }
-}
diff --git a/lib/MusicCast/Api/Usb.php b/lib/MusicCast/Api/Usb.php
deleted file mode 100644
index 0a55a5b..0000000
--- a/lib/MusicCast/Api/Usb.php
+++ /dev/null
@@ -1,11 +0,0 @@
-
- */
-class Usb extends Network
-{
-}
diff --git a/lib/MusicCast/Api/Zone.php b/lib/MusicCast/Api/Zone.php
deleted file mode 100644
index e2a4760..0000000
--- a/lib/MusicCast/Api/Zone.php
+++ /dev/null
@@ -1,25 +0,0 @@
-call($zone, 'getStatus');
- }
-
-
- private function call($zone, $path)
- {
- return $this->get(rawurlencode($zone).'/'.$path);
- }
-}
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
old mode 100644
new mode 100755
index 461db94..1a25369
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -1,31 +1,10 @@
-
-
-
-
-
- ./test/MusicCast/
-
-
-
-
-
- functional
-
-
-
+
+
+ tests
+
-
- ./lib/MusicCast/
+
+ src
-
+
\ No newline at end of file
diff --git a/lib/MusicCast/Api/AbstractApi.php b/src/Api/AbstractApi.php
old mode 100644
new mode 100755
similarity index 93%
rename from lib/MusicCast/Api/AbstractApi.php
rename to src/Api/AbstractApi.php
index 31715e5..a52697b
--- a/lib/MusicCast/Api/AbstractApi.php
+++ b/src/Api/AbstractApi.php
@@ -28,12 +28,11 @@ public function configure()
* Send a GET request with query parameters.
*
* @param string $path Request path.
- * @param array $parameters GET parameters.
* @param array $requestHeaders Request Headers.
*
* @return array|string
*/
- protected function get($path, array $parameters = array(), array $requestHeaders = array())
+ protected function get($path, array $requestHeaders = array())
{
$response = $this->client->getHttpClient()->get($path, $requestHeaders);
diff --git a/lib/MusicCast/Api/ApiInterface.php b/src/Api/ApiInterface.php
old mode 100644
new mode 100755
similarity index 100%
rename from lib/MusicCast/Api/ApiInterface.php
rename to src/Api/ApiInterface.php
diff --git a/lib/MusicCast/Api/CD.php b/src/Api/CD.php
old mode 100644
new mode 100755
similarity index 100%
rename from lib/MusicCast/Api/CD.php
rename to src/Api/CD.php
diff --git a/src/Api/Distribution.php b/src/Api/Distribution.php
new file mode 100755
index 0000000..fdd41ee
--- /dev/null
+++ b/src/Api/Distribution.php
@@ -0,0 +1,87 @@
+
+ */
+
+class Distribution extends AbstractApi
+{
+ /**
+ * @return array
+ */
+ public function getDistributionInfo()
+ {
+ return $this->callGet('getDistributionInfo');
+ }
+
+ private function callGet($path)
+ {
+ return $this->get('/dist/' . $path);
+ }
+
+ /**
+ * @param int $num
+ * @return array
+ */
+ public function startDistribution($num = 0)
+ {
+ return $this->callGet('startDistribution?num=' . rawurlencode($num));
+ }
+
+ /**
+ * @param string $groupId
+ * @param string $zone
+ * @param string $server_ip_addr
+ * @return array
+ */
+ public function setClientInfo($groupId = '', $zone = 'main', $server_ip_addr = null)
+ {
+ $info = array('group_id' => $groupId);
+ $info['zone'] = array($zone);
+ if ($server_ip_addr != null) {
+ $info['server_ip_address'] = $server_ip_addr;
+ }
+ return $this->callPost('setClientInfo', $info);
+ }
+
+ /**
+ * @param $name
+ * @return array
+ */
+ public function setGroupName($name)
+ {
+ return $this->callPost('setGroupName', array('name' => $name));
+ }
+
+ private function callPost($path, array $parameters = array())
+ {
+ return $this->post('/dist/' . $path, $parameters);
+ }
+
+
+ /**
+ * @param $groupId
+ * @param $type
+ * @param $zone
+ * @param array $client_list Clients IP
+ * @return array
+ */
+ public function setServerInfo($groupId, $type = null, $zone = null, array $client_list = null)
+ {
+ $info = array('group_id' => $groupId);
+ if ($type != null) {
+ $info['type'] = $type;
+ }
+ if ($zone != null) {
+ $info['zone'] = $zone;
+ }
+ if ($client_list != null) {
+ $info['client_list'] = $client_list;
+ }
+ return $this->callPost('setServerInfo', array($info));
+ }
+}
diff --git a/lib/MusicCast/Api/Event.php b/src/Api/Event.php
old mode 100644
new mode 100755
similarity index 73%
rename from lib/MusicCast/Api/Event.php
rename to src/Api/Event.php
index e9ce4c9..f1d36fd
--- a/lib/MusicCast/Api/Event.php
+++ b/src/Api/Event.php
@@ -1,6 +1,5 @@
sprintf('%s', $port),
];
-// $plugin = $this->client->getPlugin(AddBasePath::class);
-// $this->client->removePlugin(AddBasePath::class);
- return $this->get('', [], $headers);
-// $this->client->addPlugin($plugin);
+ // $plugin = $this->client->getPlugin(AddBasePath::class);
+ // $this->client->removePlugin(AddBasePath::class);
+ return $this->get('', $headers);
+ // $this->client->addPlugin($plugin);
}
}
diff --git a/src/Api/NetworkUSB.php b/src/Api/NetworkUSB.php
new file mode 100755
index 0000000..7bec23b
--- /dev/null
+++ b/src/Api/NetworkUSB.php
@@ -0,0 +1,310 @@
+
+ * @author Damien Surot
+ */
+class NetworkUSB extends AbstractApi
+{
+
+ /**
+ * For retrieving preset information. Presets are common use among Net/USB related input sources
+ *
+ * @return array
+ */
+ public function getPresetInfo()
+ {
+ return $this->callGet('getPresetInfo');
+ }
+
+ /**
+ *For retrieving playback information
+ *
+ * @return array
+ */
+ public function getPlayInfo()
+ {
+ return $this->callGet('getPlayInfo');
+ }
+
+ /**
+ * For controlling playback status
+ *
+ * @param string $playback Specifies playback status
+ * Values: "play" / "stop" / "pause" / "play_pause" / "previous" / "next" /
+ * "fast_reverse_start" / "fast_reverse_end" / "fast_forward_start" /
+ * "fast_forward_end"
+ * @return array
+ */
+ public function setPlayback($playback)
+ {
+ return $this->callGet('setPlayback?playback=' . rawurlencode($playback));
+ }
+
+ /**
+ * For toggling repeat setting. No direct / discrete setting commands available
+ *
+ * @return array
+ */
+ public function toggleRepeat()
+ {
+ return $this->callGet('toggleRepeat');
+ }
+
+ /**
+ * For toggling shuffle setting. No direct / discrete setting commands available
+ *
+ * @return array
+ */
+ public function toggleShuffle()
+ {
+ return $this->callGet('toggleShuffle');
+ }
+
+ /**
+ * For retrieving list information. Basically this info is available to all relevant inputs, not limited to
+ * or independent from current input
+ *
+ * @param string $list_id Specifies list ID. If nothing specified, "main" is chosen implicitly
+ * Values: "main" (common for all Net/USB sources)
+ * "auto_complete" (Pandora)
+ * "search_artist" (Pandora)
+ * "search_track" (Pandora)
+ *
+ * @param string $input Specifies target Input ID. Controls for setListControl are to work
+ * with the input specified here
+ * Values: Input IDs for Net/USB related sources
+ *
+ * @param integer $index Specifies the reference index (offset from the beginning of the list).
+ * Note that this index must be in multiple of 8. If nothing was
+ * specified, the reference index previously specified would be used
+ * Values: 0, 8, 16, 24, ..., 64984, 64992
+ *
+ * @param integer $size Specifies max list size retrieved at a time
+ * Value Range: 1 - 8
+ *
+ * @param string $lang Specifies list language. But menu names or text info are not
+ * always necessarily pulled in a language specified here. If nothing
+ * specified, English ("en") is used implicitly
+ * Values: "en" (English)/ "ja" (Japanese)/ "fr" (French)/ "de"
+ * (German)/ "es" (Spanish)/ "ru" (Russian)/ "it" (Italy)/ "zh" (Chinese)
+ *
+ * @return array
+ */
+ public function getListInfo($input, $size, $list_id = 'main', $index = null, $lang = null)
+ {
+ return $this->callGet('getListInfo?list_id=' . rawurlencode($list_id) . '&input=' . rawurlencode($input) .
+ (isset($index) ? '&index=' . rawurlencode($index) : '') . '&size=' . rawurlencode($size) .
+ (isset($lang) ? '&lang=' . rawurlencode($lang) : ''));
+ }
+
+ /**
+ * For control a list. Controllable list info is not limited to or independent from current input
+ *
+ * @param string $list_id Specifies list ID. If nothing specified, "main" is chosen implicitly
+ * Values: "main" (common for all Net/USB sources)
+ * "auto_complete" (Pandora)
+ * "search_artist" (Pandora)
+ * "search_track" (Pandora)
+ *
+ * @param string $type Specifies list transition type.
+ * "select" is to enter and get into one deeper layer than the current layer where the element specified by
+ * the index belongs to.
+ * "play" is to start playback current index element,
+ * "return" is to go back one upper layer than current.
+ * "select" and "play" needs to specify an index at the same time.
+ * In case to “select” an element with its attribute being "Capable of Search", specify search text using
+ * setSearchString in advance. (Or it is possible to specify search text and move layers at the same
+ * time by specifying an index in setSearchString)
+ * Values: "select" / "play" / "return"
+ *
+ * @param integer $index Specifies the reference index (offset from the beginning of the list).
+ * Note that this index must be in multiple of 8. If nothing was
+ * specified, the reference index previously specified would be used
+ * Values: 0, 8, 16, 24, ..., 64984, 64992
+ *
+ * @param string $zone Specifies target zone to playback. In the specified zone, input
+ * change occurs at the same time of playback.
+ * This parameter is valid only when type "play" is specified. If
+ * nothing is specified, "main" is chosen implicitly
+ * Values: : "main" / "zone2" / "zone3" / "zone4"
+ *
+ * @return array
+ */
+ public function setListControl($type, $list_id = 'main', $index = null, $zone = null)
+ {
+ return $this->callGet('setListControl?list_id=' . rawurlencode($list_id) . '&type=' . rawurlencode($type) .
+ (isset($index) ? '&index=' . rawurlencode($index) : '') .
+ (isset($zone) ? '&zone=' . rawurlencode($zone) : ''));
+ }
+
+ /**
+ * For setting search text. Specifies string executing this API before select an element with its
+ * attribute being “Capable of Search” or retrieve info about searching list(Pandora).
+ *
+ * @param string $list_id Specifies list ID. If nothing specified, "main" is chosen implicitly
+ * Values: "main" (common for all Net/USB sources)
+ * "auto_complete" (Pandora)
+ * "search_artist" (Pandora)
+ * "search_track" (Pandora)
+ *
+ * @param string $string Setting search text
+ *
+ * @param integer $index Specifies an element position in the list being selected (offset from
+ * the beginning of the list).Valid only when the list_id is "main"
+ * Specifies index an element with its attribute being "Capable of
+ * Search" Controls same as setListControl "select" are to work with
+ * the index an element specified. If no index is specified, non-actions
+ * of select
+ * Values : 0 ~ 64999
+ * @return array
+ */
+ public function setSearchString($string, $list_id = 'main', $index = null)
+ {
+ return $this->callGet('setSearchString?list_id=' . rawurlencode($list_id) . '&string=' . rawurlencode($string) .
+ (isset($index) ? '&index=' . rawurlencode($index) : ''));
+ }
+
+ /**
+ * For recalling a content preset
+ *
+ * @param string $zone Specifies station recalling zone. This causes input change in specified zone
+ * Values: "main" / "zone2" / "zone3" / "zone4"
+ *
+ * @param integer $num Specifies Preset number
+ * Value: one in the range gotten via /system/getFeatures
+ * @return array
+ */
+ public function recallPreset($zone, $num)
+ {
+ return $this->callGet('recallPreset?zone=' . rawurlencode($zone) . '&num=' . rawurlencode($num));
+ }
+
+ /**
+ * For registering current content to a preset. Presets are common use among Net/USB related input sources.
+ *
+ * @param integer $num Specifying a preset number
+ * Value: one in the range gotten via /system/getFeatures
+ *
+ * @return array
+ */
+ public function storePreset($num)
+ {
+ return $this->callGet('storePreset?num=' . rawurlencode($num));
+ }
+
+ /**
+ * For retrieving account information registered on Device
+ *
+ * @return array
+ */
+ public function getAccountStatus()
+ {
+ return $this->callGet('getAccountStatus');
+ }
+
+ /**
+ * For switching account for service corresponding multi account
+ *
+ * @param string $input Specifies target Input ID.
+ * Value: "pandora"
+ * @param integer $index Specifies switch account index
+ * Value : 0 ~ 7 (Pandora)
+ * @param integer $timeout Specifies timeout duration(ms) for this API process. If specifies 0,
+ * treat as maximum value.
+ * Value: 0 ~ 60000
+ * @return array
+ */
+ public function switchAccount($input, $index, $timeout)
+ {
+ return $this->callGet('switchAccount?input=' . rawurlencode($input) . '&index=' . rawurlencode($index)
+ . '&timeout=' . rawurlencode($timeout));
+ }
+
+ /**
+ * @param $input
+ * @param $type
+ * @param $timeout
+ * @return array|string
+ */
+ public function getServiceInfo($input, $type, $timeout)
+ {
+ return $this->callGet('getServiceInfo?input=' . rawurlencode($input) . '&type=' . rawurlencode($type)
+ . '&timeout=' . rawurlencode($timeout));
+ }
+
+ /**
+ * @return array
+ */
+ public function getMcPlaylistName()
+ {
+ return $this->callGet('getMcPlaylistName');
+ }
+
+ /**
+ * @param $bank
+ * @param $type
+ * @param int $index
+ * @param string $zone
+ * @return array|string
+ */
+ public function manageMcPlaylist($bank, $type, $index = 0, $zone = 'main')
+ {
+ return $this->callGet('manageMcPlaylist?bank=' . rawurlencode($bank) . '&type=' . rawurlencode($type)
+ . '&index=' . rawurlencode($index) . '&zone=' . rawurlencode($zone));
+ }
+
+ /**
+ * @param $bank
+ * @param int $index
+ * @return array|string
+ */
+ public function getMcPlaylist($bank, $index = 0)
+ {
+ return $this->callGet('getMcPlaylist?bank=' . rawurlencode($bank) . '&index=' . rawurlencode($index));
+ }
+
+ /**
+ * @param integer $index
+ * @return array
+ */
+ public function getPlayQueue($index = 0)
+ {
+ return $this->callGet('getPlayQueue?index=' . rawurlencode($index));
+ }
+
+ /**
+ * @return array
+ */
+ public function getRecentInfo()
+ {
+ return $this->callGet('getRecentInfo');
+ }
+
+ /**
+ * @param $zone
+ * @param $uri
+ * @return array
+ */
+ public function setYmapUri($zone, $uri)
+ {
+ return $this->callPost('setYmapUri', array('zone' => $zone, "uri" => $uri));
+ }
+
+
+ private function callGet($path)
+ {
+ return $this->get('/netusb/' . $path);
+ }
+
+ private function callPost($path, array $parameters = array())
+ {
+ return $this->post('/netusb/' . $path, $parameters);
+ }
+}
diff --git a/src/Api/System.php b/src/Api/System.php
new file mode 100755
index 0000000..4254460
--- /dev/null
+++ b/src/Api/System.php
@@ -0,0 +1,138 @@
+
+ * @author Damien Surot
+ */
+/**
+ * @author Sam Van der Borght
+ */
+class System extends AbstractApi
+{
+ /**
+ * For retrieving basic information of a Device
+ *
+ * @return array
+ */
+ public function getDeviceInfo()
+ {
+ return $this->call('getDeviceInfo');
+ }
+
+ /**
+ * For retrieving feature information equipped with a Device
+ *
+ * @return array
+ */
+ public function getFeatures()
+ {
+ return $this->call('getFeatures');
+ }
+
+ /**
+ * For retrieving network related setup / information
+ *
+ * @return array
+ */
+ public function getNetworkStatus()
+ {
+ return $this->call('getNetworkStatus');
+ }
+
+ /**
+ * For retrieving setup/information of overall system function. Parameters are readable only when
+ * corresponding functions are available in “func_list” of /system/getFeatures
+ *
+ * @return array
+ */
+ public function getFuncStatus()
+ {
+ return $this->call('getFuncStatus');
+ }
+
+ /**
+ * For setting Auto Power Standby status. Actual operations/reactions of enabling Auto Power
+ * Standby depend on each Device
+ *
+ * @param bool $enable Specifies Auto Power Standby status
+ * @return array
+ */
+ public function setAutoPowerStandby($enable = true)
+ {
+ return $this->call('setAutoPowerStandby?enable=' . rawurlencode($enable ? 'true' : 'false'));
+ }
+
+ /**
+ * For retrieving Location information
+ * @return array
+ */
+ public function getLocationInfo()
+ {
+ return $this->call('getLocationInfo');
+ }
+
+ /**
+ * For sending specific remote IR code. A Device is operated same as remote IR code reception. But
+ * continuous IR code cannot be used in this command. Refer to each Device’s IR code list for details
+ * @param string $code IR code in 8-digit hex
+ * @return array
+ */
+ public function sendIrCode($code)
+ {
+ return $this->call('sendIrCode?code=' . rawurlencode($code));
+ }
+
+ /**
+ * @return array
+ */
+ public function getNameText()
+ {
+ return $this->call('getNameText');
+ }
+
+ /**
+ * @param string $type
+ * @return array
+ */
+ public function isNewFirmwareAvailable($type = 'network')
+ {
+ return $this->call('isNewFirmwareAvailable?type=' . rawurlencode($type));
+ }
+
+ /**
+ * @return array
+ */
+ public function getTag()
+ {
+ return $this->call('getTag');
+ }
+
+ /**
+ * @return array
+ */
+ public function getDisklavierSettings()
+ {
+ return $this->call('getDisklavierSettings');
+ }
+
+
+ /**
+ * @return array
+ */
+ public function getMusicCastTreeInfo()
+ {
+ return $this->call('getMusicCastTreeInfo');
+ }
+
+ /**
+ * @param $path
+ * @return array
+ */
+ private function call($path)
+ {
+ return $this->get('/system/' . $path);
+ }
+}
diff --git a/lib/MusicCast/Api/Tuner.php b/src/Api/Tuner.php
old mode 100644
new mode 100755
similarity index 100%
rename from lib/MusicCast/Api/Tuner.php
rename to src/Api/Tuner.php
diff --git a/src/Api/Zone.php b/src/Api/Zone.php
new file mode 100755
index 0000000..edb557b
--- /dev/null
+++ b/src/Api/Zone.php
@@ -0,0 +1,158 @@
+
+ */
+class Zone extends AbstractApi
+{
+
+
+ /**
+ * Returns basic information of each Zone like power, volume, input and so on
+ *
+ * @param string $zone target zone. Available for zones with this function
+ * Values: "main" / "zone2" / "zone3" / "zone4"
+ *
+ * @return array
+ */
+ public function getStatus($zone)
+ {
+ return $this->call($zone, 'getStatus');
+ }
+
+
+ /**
+ * Returns a list of Sound Program available in each Zone. It is possible for the list contents to
+ be dynamically changed
+ *
+ * @param string $zone target zone. Available for zones with this function
+ * Values: "main" / "zone2" / "zone3" / "zone4"
+ *
+ * @return array
+ */
+ public function getSoundProgramList($zone)
+ {
+ return $this->call($zone, 'getSoundProgramList');
+ }
+
+ /**
+ * Set the power status of each Zone
+ *
+ * @param string $zone target zone. Available for zones with this function
+ * Values: "main" / "zone2" / "zone3" / "zone4"
+ * @param string $power power status
+ * Values: "on" / "standby" / "toggle"
+ *
+ * @return array
+ */
+ public function setPower($zone, $power)
+ {
+ return $this->call($zone, 'setPower?power='.rawurlencode($power));
+ }
+
+ /**
+ * Set Sleep Timer for each Zone
+ With Zone B enabled Devices, target Zone is described as Master Power, but Main Zone is used to
+ set it up via YXC
+ *
+ * @param string $zone target zone. Available for zones with this function
+ * Values: "main" / "zone2" / "zone3" / "zone4"
+ * @param string $sleep Sleep Time (unit in minutes)
+ *
+ * @return array
+ */
+ public function setSleep($zone, $sleep)
+ {
+ return $this->call($zone, 'setSleep?sleep='.rawurlencode($sleep));
+ }
+
+ /**
+ * Set Sleep Timer for each Zone
+ With Zone B enabled Devices, target Zone is described as Master Power, but Main Zone is used to
+ set it up via YXC
+ *
+ * @param string $zone target zone. Available for zones with this function
+ * Values: "main" / "zone2" / "zone3" / "zone4"
+ * @param string $volume Specifies volume value
+ * Value Range: calculated by minimum/maximum/step values gotten
+ * via /system/getFeatures
+ * @param string $step Specifies volume step value if the volume is “up” or “down”. If
+ * nothing specified, minimum step value is used implicitly
+ *
+ * @return array
+ */
+ public function setVolume($zone, $volume, $step = null)
+ {
+ return $this->call($zone, 'setVolume?volume='.rawurlencode($volume).
+ (isset($step) ? '&step='.rawurlencode($step) : ''));
+ }
+
+ /**
+ * Set mute status in each Zone
+ *
+ * @param string $zone target zone. Available for zones with this function
+ * Values: "main" / "zone2" / "zone3" / "zone4"
+ * @param string $mute Mute status
+ *
+ * @return array
+ */
+ public function setMute($zone, $mute)
+ {
+ return $this->call($zone, 'setMute?enable='.rawurlencode($mute?'true':'false'));
+ }
+
+ /**
+ * Selecting each Zone input
+ *
+ * @param string $zone target zone. Available for zones with this function
+ * Values: "main" / "zone2" / "zone3" / "zone4"
+ * @param string $input Specifies Input ID
+ * Values: Input IDs gotten via /system/getFeatures
+ * @param string $mode Specifies select mode. If no parameter is specified, actions of input
+ * change depend on a Device’s specification
+ * Value: "autoplay_disabled" (Restricts Auto Play of Net/USB related Inputs).
+ *
+ * @return array
+ */
+ public function setInput($zone, $input, $mode = null)
+ {
+ return $this->call($zone, 'setInput?input='.rawurlencode($input).
+ (isset($mode) ? '&mode='.rawurlencode($mode) : ''));
+ }
+
+ /**
+ * Let a Device do necessary process before changing input in a specific zone. This is valid only
+ * when “prepare_input_change” exists in “func_list” found in /system/getFuncStatus.
+ * MusicCast CONTROLLER executes this API when an input icon is selected in a Room, right
+ * before sending various APIs (of retrieving list information etc.) regarding selecting input
+ *
+ * @param string $zone target zone. Available for zones with this function
+ * Values: "main" / "zone2" / "zone3" / "zone4"
+ * @param string $input Specifies Input ID
+ * Values: Input IDs gotten via /system/getFeatures
+ *
+ * @return array
+ */
+ public function prepareInputChange($zone, $input)
+ {
+ return $this->call($zone, 'prepareInputChange?input='.rawurlencode($input));
+ }
+
+ /**
+ * @param $zone
+ * @return array|string
+ */
+ public function getSignalInfo($zone)
+ {
+ return $this->call($zone, 'getSignalInfo');
+ }
+
+
+ private function call($zone, $path)
+ {
+ return $this->get(rawurlencode($zone).'/'.$path);
+ }
+}
diff --git a/src/Cache.php b/src/Cache.php
new file mode 100755
index 0000000..321ec0a
--- /dev/null
+++ b/src/Cache.php
@@ -0,0 +1,29 @@
+
+ */
+class Cache extends DoctrineCachePool
+{
+ const MINUTE = 60;
+ const HOUR = self::MINUTE * 60;
+ const DAY = self::HOUR * 60;
+
+ public function __construct()
+ {
+ $cache = new FilesystemCache(sys_get_temp_dir() . DIRECTORY_SEPARATOR . "musiccast");
+ parent::__construct($cache);
+ }
+}
diff --git a/lib/MusicCast/Client.php b/src/Client.php
old mode 100644
new mode 100755
similarity index 94%
rename from lib/MusicCast/Client.php
rename to src/Client.php
index f6e342d..bb94aa7
--- a/lib/MusicCast/Client.php
+++ b/src/Client.php
@@ -1,23 +1,23 @@
setAllowedTypes('port', ['integer']);
$this->options = $resolver->resolve($options);
- $this->postResolve($options);
+ $this->postResolve();
return $this->options;
}
@@ -303,9 +302,8 @@ private function configureOptions($options = [])
/**
* Post resolve
*
- * @param array $options
*/
- protected function postResolve(array $options = [])
+ protected function postResolve()
{
$this->options['base_url'] = sprintf(
'http://%s:%s',
diff --git a/src/Controller.php b/src/Controller.php
new file mode 100755
index 0000000..b0f7179
--- /dev/null
+++ b/src/Controller.php
@@ -0,0 +1,437 @@
+
+ */
+class Controller extends Speaker
+{
+ /**
+ * No music playing, but not paused.
+ *
+ */
+ const STATE_STOPPED = 201;
+
+ /**
+ * Currently plating music.
+ */
+ const STATE_PLAYING = 202;
+
+ /**
+ * Music is currently paused.
+ */
+ const STATE_PAUSED = 203;
+
+ /**
+ * The speaker is currently working on either playing or pausing.
+ *
+ * Check it's state again in a second or two
+ */
+ const STATE_TRANSITIONING = 204;
+
+ /**
+ * The speaker is in an unknown state.
+ *
+ * This should only happen if Sonos introduce a new state that this code has not been updated to handle.
+ */
+ const STATE_UNKNOWN = 205;
+
+
+ /**
+ * @var Network $network The network instance this Controller is part of.
+ */
+ protected $network;
+
+ /**
+ * @var
+ */
+ private $distribution_id;
+
+ /**
+ * Create a Controller instance from a speaker.
+ *
+ * The speaker must be a coordinator.
+ *
+ * @param Speaker $speaker
+ * @param Network $network
+ * @param int $distribution_id
+ */
+ public function __construct(Speaker $speaker, Network $network, $distribution_id)
+ {
+ parent::__construct($speaker->device);
+ if (!$speaker->isCoordinator()) {
+ throw new \InvalidArgumentException("You cannot create a Controller instance from a Speaker that is
+ not the coordinator of it's group");
+ }
+ $this->network = $network;
+ $this->distribution_id = $distribution_id;
+ }
+
+ /**
+ * Get the speakers that are in the group of this controller.
+ *
+ * @return Speaker[]
+ */
+ public function getSpeakers()
+ {
+ $group = [];
+ $speakers = $this->network->getSpeakers();
+
+ foreach ($speakers as $speaker) {
+ if ($speaker->getUuid() == $this->getUuid()) {
+ $group[] = $speaker;
+ continue;
+ }
+ if ($speaker->getGroup() === $this->getGroup() && $this->getGroup() != Speaker::NO_GROUP) {
+ $group[] = $speaker;
+ }
+ }
+
+
+ return $group;
+ }
+
+ /**
+ * Check if this speaker is the coordinator of it's current group.
+ *
+ * This method is only here to override the method from the Speaker class.
+ * A Controller instance is always the coordinator of it's group.
+ *
+ * @return bool
+ */
+ public function isCoordinator()
+ {
+ return true;
+ }
+
+ /**
+ * Get the current state of the group of speakers.
+ *
+ * @return int One of the class STATE_ constants
+ */
+ public function getState()
+ {
+ $name = $this->getStateName();
+ switch ($name) {
+ case "stop":
+ return self::STATE_STOPPED;
+ case "play":
+ return self::STATE_PLAYING;
+ case "pause":
+ return self::STATE_PAUSED;
+ case "fast_reverse":
+ return self::STATE_TRANSITIONING;
+ case "fast_forward":
+ return self::STATE_TRANSITIONING;
+ }
+ return self::STATE_UNKNOWN;
+ }
+
+ /**
+ * Get the current state of the group of speakers as the string reported by sonos: PLAYING, PAUSED_PLAYBACK, etc
+ *
+ * @return string
+ */
+ public function getStateName()
+ {
+ return $this->call('netusb', 'getPlayInfo')['playback'];
+ }
+
+ /**
+ * Get attributes about the currently active track in the queue.
+ *
+ * @return State Track data containing the following elements
+ */
+ public function getStateDetails()
+ {
+ $json = $this->call('netusb', 'getPlayInfo');
+ return State::buildState($json, $this->getIp());
+ }
+
+ /**
+ * Set the state of the group.
+ *
+ * @param int $state One of the class STATE_ constants
+ *
+ * @return static
+ */
+ public function setState($state)
+ {
+ switch ($state) {
+ case self::STATE_PLAYING:
+ return $this->play();
+ case self::STATE_PAUSED:
+ return $this->pause();
+ case self::STATE_STOPPED:
+ return $this->stop();
+ }
+ throw new \InvalidArgumentException("Unknown state: {$state})");
+ }
+
+ /**
+ * Start playing the active music for this group.
+ *
+ * @return static
+ */
+ public function play()
+ {
+ return $this->setPlayback('play');
+ }
+
+ private function setPlayback($playback)
+ {
+ return $this->call('netusb', 'setPlayback', [$playback]);
+ }
+
+ /**
+ * Pause the group.
+ *
+ * @return static
+ */
+ public function pause()
+ {
+ return $this->setPlayback('pause');
+ }
+
+ /**
+ * Pause the group.
+ *
+ * @return static
+ */
+ public function stop()
+ {
+ return $this->setPlayback('stop');
+ }
+
+ /**
+ * Skip to the next track in the current queue.
+ *
+ * @return static
+ */
+ public function next()
+ {
+ return $this->setPlayback('next');
+ }
+
+ /**
+ * Skip back to the previous track in the current queue.
+ *
+ * @return static
+ */
+ public function previous()
+ {
+ return $this->setPlayback('previous');
+ }
+
+
+ public function setInput($input)
+ {
+ if (key_exists('prepareInputChange', $this->call('system', 'getFuncStatus'))) {
+ $this->call('zone', 'prepareInputChange', ['main', $input]);
+ }
+ $this->call('zone', 'setInput', ['main', $input]);
+ }
+
+ /**
+ * Adds the specified speaker to the group of this Controller.
+ *
+ * @param Speaker $speaker The speaker to add to the group
+ *
+ * @return static
+ */
+ public function addSpeaker(Speaker $speaker)
+ {
+ if ($speaker->getUuid() === $this->getUuid()) {
+ return $this;
+ }
+ if (!in_array($speaker, $this->getSpeakers())) {
+ $group = $this->getGroup();
+ if ($group == Speaker::NO_GROUP) {
+ $group = md5($this->device->getIp());
+ }
+ if ($speaker->getGroup() == Speaker::NO_GROUP) {
+ $speaker->call('dist', 'setClientInfo', [$group, 'main', $this->device->getIp()]);
+ $this->call(
+ 'dist',
+ 'setServerInfo',
+ [$group, 'add', 'main', array($speaker->device->getIp())]
+ );
+ $this->call('dist', 'startDistribution', [$this->distribution_id]);
+ }
+ }
+ return $this;
+ }
+
+
+ /**
+ * Removes the specified speaker from the group of this Controller.
+ *
+ * @param Speaker $speaker The speaker to remove from the group
+ *
+ * @return static
+ */
+ public function removeSpeaker(Speaker $speaker)
+ {
+ if ($speaker->getUuid() === $this->getUuid()) {
+ return $this;
+ }
+ if (in_array($speaker, $this->getSpeakers())) {
+ $speaker->call('dist', 'setClientInfo');
+ $this->call(
+ 'dist',
+ 'setServerInfo',
+ [$this->getGroup(), 'remove', 'main', array($speaker->device->getIp())]
+ );
+ $this->call('dist', 'startDistribution', [$this->distribution_id]);
+ }
+ return $this;
+ }
+
+ /**
+ * Removes all speakers from the group of this Controller.
+ *
+ * @return static
+ */
+ public function removeAllSpeakers()
+ {
+ foreach ($this->getSpeakers() as $speaker) {
+ $this->removeSpeaker($speaker);
+ }
+ return $this;
+ }
+
+
+ /**
+ * Adjust the volume of all the speakers controlled by this Controller.
+ *
+ * @param int $adjust A relative amount between -100 and 100
+ *
+ * @return static
+ */
+ public function adjustVolume($adjust)
+ {
+ $speakers = $this->getSpeakers();
+ foreach ($speakers as $speaker) {
+ $speaker->adjustVolume($adjust);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Check if repeat is currently active.
+ *
+ * @return bool
+ */
+ public function getRepeat()
+ {
+ return $this->call('netusb', 'getPlayInfo')['repeat'] != 'off';
+ }
+
+
+ /**
+ * Turn repeat mode on or off.
+ *
+ * @return static
+ */
+ public function toggleRepeat()
+ {
+ return $this->call('netusb', 'toggleRepeat');
+ }
+
+
+ /**
+ * Check if shuffle is currently active.
+ *
+ * @return bool
+ */
+ public function getShuffle()
+ {
+ return $this->call('netusb', 'getPlayInfo')['shuffle'] != 'off';
+ }
+
+ /**
+ * Turn shuffle mode on or off.
+ *
+ * @return static
+ */
+ public function toggleShuffle()
+ {
+ return $this->call('netusb', 'toggleShuffle');
+ }
+
+
+ /**
+ * Get the queue for this controller.
+ *
+ * @return Queue
+ */
+ public function getQueue()
+ {
+ return new Queue($this->call('netusb', 'getPlayQueue'));
+ }
+
+
+ /**
+ * Get the network instance used by this controller.
+ *
+ * @return Network
+ */
+ public function getNetwork()
+ {
+ return $this->network;
+ }
+
+ public function powerOn()
+ {
+ $speakers = $this->getSpeakers();
+ foreach ($speakers as $speaker) {
+ $speaker->powerOn();
+ }
+ return $this;
+ }
+
+ public function standBy()
+ {
+ $speakers = $this->getSpeakers();
+ foreach ($speakers as $speaker) {
+ $speaker->standBy();
+ }
+ return $this;
+ }
+
+ /**
+ * Mute all speakers controlled.
+ *
+ * @param bool $mute Whether the speakers should be muted or not
+ *
+ * @return static
+ */
+ public function mute($mute = true)
+ {
+ $speakers = $this->getSpeakers();
+ foreach ($speakers as $speaker) {
+ $speaker->mute();
+ }
+ return $this;
+ }
+
+ /**
+ * Mute all speakers controlled.
+ *
+ * @return static
+ */
+ public function unmute()
+ {
+ $speakers = $this->getSpeakers();
+ foreach ($speakers as $speaker) {
+ $speaker->unmute();
+ }
+ return $this;
+ }
+}
diff --git a/src/Device.php b/src/Device.php
new file mode 100755
index 0000000..cdb9839
--- /dev/null
+++ b/src/Device.php
@@ -0,0 +1,147 @@
+
+ */
+class Device implements LoggerAwareInterface
+{
+ /**
+ * @var string $ip Device Ip
+ */
+ protected $ip;
+ /**
+ * @var Client $client MusicCast Client
+ */
+ private $client;
+ /**
+ * @var CacheInterface $cache The cache object to use for the expensive multicast discover
+ * to find MusicCast devices on the network.
+ */
+ protected $cache;
+
+ /**
+ * @var LoggerInterface $logger The logging object.
+ */
+ protected $logger;
+
+
+ /**
+ * Device constructor.
+ * @param $ip
+ * @param int $port
+ * @param CacheInterface|null $cache
+ * @param LoggerInterface|null $logger
+ */
+ public function __construct($ip, $port = 80, CacheInterface $cache = null, LoggerInterface $logger = null)
+ {
+ $this->ip = $ip;
+ $this->client = new Client(['host' => $ip, 'port' => $port]);
+ if ($cache === null) {
+ $cache = new Cache();
+ }
+ $this->cache = $cache;
+
+ if ($logger === null) {
+ $logger = new NullLogger;
+ }
+ $this->logger = $logger;
+ }
+
+ /**
+ * Set the logger object to use.
+ *
+ * @var LoggerInterface $logger The logging object
+ *
+ * @return static
+ */
+ public function setLogger(LoggerInterface $logger)
+ {
+ $this->logger = $logger;
+
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getIp()
+ {
+ return $this->ip;
+ }
+
+
+ public function call($api, $method, array $args = [])
+ {
+ return call_user_func_array(array($this->client->api($api), $method), $args);
+ }
+
+
+ public function getDeviceInfo()
+ {
+ $cacheKey = $this->getCacheKey() . __FUNCTION__;
+ if ($this->cache->has($cacheKey)) {
+ return $this->cache->get($cacheKey);
+ }
+ $info = $this->call('system', 'getDeviceInfo');
+ $this->cache->set($cacheKey, $info);
+ return $info;
+ }
+
+ private function getCacheKey()
+ {
+ return $this->ip;
+ }
+
+ public function getLocationInfo()
+ {
+ $cacheKey = $this->getCacheKey() . __FUNCTION__;
+ if ($this->cache->has($cacheKey)) {
+ return $this->cache->get($cacheKey);
+ }
+ $info = $this->call('system', 'getLocationInfo');
+ $this->cache->set($cacheKey, $info);
+ return $info;
+ }
+
+ public function getMusicCastTreeInfo()
+ {
+ $cacheKey = $this->getCacheKey() . __FUNCTION__;
+ if ($this->cache->has($cacheKey)) {
+ return $this->cache->get($cacheKey);
+ }
+ $info = $this->call('system', 'getMusicCastTreeInfo');
+ $this->cache->set($cacheKey, $info);
+ return $info;
+ }
+
+ public function getNetworkStatus()
+ {
+ $cacheKey = $this->getCacheKey() . __FUNCTION__;
+ if ($this->cache->has($cacheKey)) {
+ return $this->cache->get($cacheKey);
+ }
+ $info = $this->call('system', 'getNetworkStatus');
+ $this->cache->set($cacheKey, $info);
+ return $info;
+ }
+
+ public function getUuid()
+ {
+ return $this->getDeviceInfo()['device_id'];
+ }
+}
diff --git a/lib/MusicCast/Enum/ResponseCodes.php b/src/Enum/ResponseCodes.php
old mode 100644
new mode 100755
similarity index 100%
rename from lib/MusicCast/Enum/ResponseCodes.php
rename to src/Enum/ResponseCodes.php
diff --git a/lib/MusicCast/Exception/BadMethodCallException.php b/src/Exception/BadMethodCallException.php
old mode 100644
new mode 100755
similarity index 100%
rename from lib/MusicCast/Exception/BadMethodCallException.php
rename to src/Exception/BadMethodCallException.php
diff --git a/lib/MusicCast/Exception/ErrorException.php b/src/Exception/ErrorException.php
old mode 100644
new mode 100755
similarity index 100%
rename from lib/MusicCast/Exception/ErrorException.php
rename to src/Exception/ErrorException.php
diff --git a/lib/MusicCast/Exception/ExceptionInterface.php b/src/Exception/ExceptionInterface.php
old mode 100644
new mode 100755
similarity index 100%
rename from lib/MusicCast/Exception/ExceptionInterface.php
rename to src/Exception/ExceptionInterface.php
diff --git a/lib/MusicCast/Exception/InvalidArgumentException.php b/src/Exception/InvalidArgumentException.php
old mode 100644
new mode 100755
similarity index 100%
rename from lib/MusicCast/Exception/InvalidArgumentException.php
rename to src/Exception/InvalidArgumentException.php
diff --git a/src/Exception/NotImplementedException.php b/src/Exception/NotImplementedException.php
new file mode 100755
index 0000000..6ee46c9
--- /dev/null
+++ b/src/Exception/NotImplementedException.php
@@ -0,0 +1,7 @@
+
+ */
+class Favorite
+{
+ /**
+ * @var string|null $name The name of the playlist.
+ */
+ protected $name;
+ protected $input;
+ protected $id;
+ protected $speaker;
+
+
+ /**
+ * Create an instance of the Playlist class.
+ *
+ * @param int $index
+ * @param array $data
+ * @param Speaker $speaker A controller instance on the playlist's network
+ */
+ public function __construct($index, $data, Speaker $speaker)
+ {
+ $this->id = $index;
+ $this->name = $data['text'];
+ $this->input = $data['input'];
+ $this->speaker = $speaker;
+ }
+
+
+ /**
+ * Get the id of the playlist.
+ *
+ * @return string
+ */
+ public function getId()
+ {
+ return $this->id;
+ }
+
+
+ /**
+ * Get the name of the playlist.
+ *
+ * @return string
+ */
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ /**
+ * @return mixed
+ */
+ public function getInput()
+ {
+ return $this->input;
+ }
+
+
+ public function play()
+ {
+ $this->speaker->call('netusb', 'recallPreset', ['main', $this->id]);
+ }
+}
diff --git a/lib/MusicCast/HttpClient/Message/ResponseMediator.php b/src/HttpClient/Message/ResponseMediator.php
old mode 100644
new mode 100755
similarity index 100%
rename from lib/MusicCast/HttpClient/Message/ResponseMediator.php
rename to src/HttpClient/Message/ResponseMediator.php
diff --git a/lib/MusicCast/HttpClient/Plugin/AddBasePath.php b/src/HttpClient/Plugin/AddBasePath.php
old mode 100644
new mode 100755
similarity index 100%
rename from lib/MusicCast/HttpClient/Plugin/AddBasePath.php
rename to src/HttpClient/Plugin/AddBasePath.php
diff --git a/lib/MusicCast/HttpClient/Plugin/History.php b/src/HttpClient/Plugin/History.php
old mode 100644
new mode 100755
similarity index 100%
rename from lib/MusicCast/HttpClient/Plugin/History.php
rename to src/HttpClient/Plugin/History.php
diff --git a/lib/MusicCast/HttpClient/Plugin/MusicCastExceptionThrower.php b/src/HttpClient/Plugin/MusicCastExceptionThrower.php
old mode 100644
new mode 100755
similarity index 94%
rename from lib/MusicCast/HttpClient/Plugin/MusicCastExceptionThrower.php
rename to src/HttpClient/Plugin/MusicCastExceptionThrower.php
index 68cfcd9..69328c9
--- a/lib/MusicCast/HttpClient/Plugin/MusicCastExceptionThrower.php
+++ b/src/HttpClient/Plugin/MusicCastExceptionThrower.php
@@ -26,7 +26,8 @@ public function handleRequest(RequestInterface $request, callable $next, callabl
}
throw new ErrorException(ResponseCodes::getMessage($code), 400);
- } elseif ($response->getStatusCode() === 200) {
+ }
+ if ($response->getStatusCode() === 200) {
return $response;
}
diff --git a/src/Network.php b/src/Network.php
new file mode 100755
index 0000000..410f87a
--- /dev/null
+++ b/src/Network.php
@@ -0,0 +1,352 @@
+
+ */
+class Network implements LoggerAwareInterface
+{
+
+ /**
+ * @var Speaker[]|null $speakers Speakers that are available on the current network.
+ */
+ protected $speakers;
+
+ /**
+ * @var Playlist[]|null $playlists Playlists that are available on the current network.
+ */
+ protected $playlists;
+
+ /**
+ * @var CacheInterface $cache The cache object to use for the expensive multicast discover to find
+ * MusicCast devices on the network.
+ */
+ protected $cache;
+
+ /**
+ * @var LoggerInterface $logger The logging object.
+ */
+ protected $logger;
+
+ /**
+ * @var $multicastAddress string The multicast address to use for SSDP discovery.
+ */
+ protected $multicastAddress = "239.255.255.250";
+
+ /**
+ * @var $networkInterface string The network interface to use for SSDP discovery.
+ */
+ protected $networkInterface;
+ /**
+ * @var Favorite[]|null $favorites Favorites that are available on the current network.
+ */
+ protected $favorites;
+
+ /**
+ * Create a new instance.
+ *
+ * @param CacheInterface $cache The cache object to use for the expensive multicast discover to find
+ * MusicCast devices on the network
+ * @param LoggerInterface $logger The logging object
+ */
+ public function __construct(CacheInterface $cache = null, LoggerInterface $logger = null)
+ {
+ if ($cache === null) {
+ $cache = new Cache();
+ }
+ $this->cache = $cache;
+
+ if ($logger === null) {
+ $logger = new NullLogger;
+ }
+ $this->logger = $logger;
+ }
+
+ /**
+ * @param LoggerInterface $logger
+ */
+ public function setLogger(LoggerInterface $logger)
+ {
+ $this->logger = $logger;
+ }
+
+ /**
+ * @return string
+ */
+ public function getMulticastAddress()
+ {
+ return $this->multicastAddress;
+ }
+
+ /**
+ * @param $multicastAddress
+ */
+ public function setMulticastAddress($multicastAddress)
+ {
+ $this->multicastAddress = $multicastAddress;
+ }
+
+ /**
+ * @return string
+ */
+ public function getNetworkInterface()
+ {
+ return $this->networkInterface;
+ }
+
+ /**
+ * @param $networkInterface
+ */
+ public function setNetworkInterface($networkInterface)
+ {
+ $this->networkInterface = $networkInterface;
+ }
+
+ /**
+ * Get a Controller instance from the network.
+ *
+ * Useful for managing playlists/alarms, as these need a controller but it doesn't matter which one.
+ *
+ * @return Controller|null
+ */
+ public function getController()
+ {
+ $controllers = $this->getControllers();
+ if ($controller = reset($controllers)) {
+ return $controller;
+ }
+ return null;
+ }
+
+ /**
+ * Get all the coordinators on the network.
+ *
+ * @return Controller[]
+ */
+ public function getControllers()
+ {
+ $controllers = [];
+ $speakers = $this->getSpeakers();
+ $index = 0;
+ foreach ($speakers as $speaker) {
+ if (!$speaker->isCoordinator()) {
+ continue;
+ }
+ $controllers[$speaker->getIp()] = new Controller($speaker, $this, $index++);
+ }
+ return $controllers;
+ }
+
+ /**
+ * @return Speaker[]|null
+ */
+ public function getSpeakers()
+ {
+ if (is_array($this->speakers)) {
+ return $this->speakers;
+ }
+
+ $this->logger->debug("creating speaker instances");
+
+ $cacheKey = $this->getCacheKey();
+
+ if ($this->cache->has($cacheKey)) {
+ $this->logger->debug("getting device info from cache");
+ $devices = $this->cache->get($cacheKey);
+ } else {
+ $devices = $this->getDevices();
+
+ # Only cache the devices if we actually found some
+ if (count($devices) > 0) {
+ $this->cache->set($cacheKey, $devices);
+ }
+ }
+
+ if (count($devices) < 1) {
+ throw new \RuntimeException("No devices found on the current network");
+ }
+
+
+ # Get the MusicCast devices from 1 speaker
+ $ip = reset($devices);
+ $device = new Device($ip, 80);
+ $this->logger->debug("Getting devices info from: {$ip}");
+ $treeInfo = $device->getMusicCastTreeInfo();
+ $this->speakers = [];
+ foreach ($treeInfo['mac_address_list'] as $addr) {
+ $ip = $addr['ip_address'];
+ $speaker = new Speaker(new Device($ip, 80));
+ $this->speakers[$ip] = $speaker;
+ }
+
+ return $this->speakers;
+ }
+
+ public function getSpeakerByName($name)
+ {
+ $roughMatch = false;
+ $speakers = $this->getSpeakers();
+ foreach ($speakers as $speaker) {
+ if ($speaker->getName() === $name) {
+ return $speaker;
+ }
+ if (strtolower($speaker->getName()) === strtolower($name)) {
+ $roughMatch = $speaker;
+ }
+ }
+ if ($roughMatch) {
+ return $roughMatch;
+ }
+ return null;
+ }
+
+ protected function getCacheKey()
+ {
+ $cacheKey = "devices";
+
+ $cacheKey .= "_" . gettype($this->networkInterface);
+ $cacheKey .= "_" . $this->networkInterface;
+
+ $cacheKey .= "_" . $this->multicastAddress;
+
+ return $cacheKey;
+ }
+
+ /**
+ * Get all the devices on the current network.
+ *
+ * @return string[] An array of ip addresses
+ */
+ protected function getDevices()
+ {
+ $this->logger->debug("discovering devices...");
+
+ $port = 1900;
+
+ $sock = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);
+
+ $level = getprotobyname("ip");
+
+ socket_set_option($sock, $level, IP_MULTICAST_TTL, 2);
+
+ if ($this->networkInterface !== null) {
+ socket_set_option($sock, $level, IP_MULTICAST_IF, $this->networkInterface);
+ }
+
+ $data = "M-SEARCH * HTTP/1.1\r\n";
+ $data .= "HOST: {$this->multicastAddress}:1900\r\n";
+ $data .= "MAN: \"ssdp:discover\"\r\n";
+ $data .= "MX: 2\r\n";
+ $data .= "ST: urn:schemas-upnp-org:device:MediaRenderer:1\r\n";
+
+
+ $this->logger->debug($data);
+
+ socket_sendto($sock, $data, strlen($data), null, $this->multicastAddress, $port);
+
+ $read = [$sock];
+ $write = [];
+ $except = [];
+ $name = null;
+ $port = null;
+ $tmp = "";
+
+ $response = "";
+ while (socket_select($read, $write, $except, 1)) {
+ socket_recvfrom($sock, $tmp, 2048, null, $name, $port);
+ $response .= $tmp;
+ }
+
+ $this->logger->debug($response);
+
+ $devices = [];
+ foreach (explode("\r\n\r\n", $response) as $reply) {
+ if (!$reply) {
+ continue;
+ }
+
+ $data = [];
+ foreach (explode("\r\n", $reply) as $line) {
+ if (!$pos = strpos($line, ":")) {
+ continue;
+ }
+ $key = strtolower(substr($line, 0, $pos));
+ $val = trim(substr($line, $pos + 1));
+ $data[$key] = $val;
+ }
+ $devices[] = $data;
+ }
+
+ $return = [];
+ $unique = [];
+ foreach ($devices as $device) {
+ if ($device["st"] !== "urn:schemas-upnp-org:device:MediaRenderer:1") {
+ continue;
+ }
+ if (in_array($device["usn"], $unique)) {
+ continue;
+ }
+ $this->logger->debug("found device: {usn}", $device);
+
+ $url = parse_url($device["location"]);
+ $ip = $url["host"];
+
+ $return[] = $ip;
+ $unique[] = $device["usn"];
+ }
+
+ return $return;
+ }
+
+ /**
+ * Get the coordinator for the specified ip address.
+ *
+ * @param string $ip The ip address of the speaker
+ *
+ * @return Controller|null
+ */
+ public function getControllerByIp($ip)
+ {
+ $speakers = $this->getSpeakers();
+ if (!array_key_exists($ip, $speakers)) {
+ throw new \InvalidArgumentException("No speaker found for the IP address '{$ip}'");
+ }
+
+ foreach ($this->getControllers() as $controller) {
+ if ($controller->getIp() === $ip) {
+ return $controller;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Get the speaker for the specified ip address.
+ *
+ * @param string $ip The ip address of the speaker
+ *
+ * @return Speaker
+ */
+ public function getSpeakerByIp($ip)
+ {
+ $speakers = $this->getSpeakers();
+ if (!array_key_exists($ip, $speakers)) {
+ throw new \InvalidArgumentException("No speaker found for the IP address '{$ip}'");
+ }
+ return $speakers[$ip];
+ }
+}
diff --git a/src/Playlist.php b/src/Playlist.php
new file mode 100755
index 0000000..6960907
--- /dev/null
+++ b/src/Playlist.php
@@ -0,0 +1,65 @@
+
+ */
+class Playlist
+{
+ /**
+ * @var string|null $name The name of the playlist.
+ */
+ protected $name;
+ protected $id;
+ private $speaker;
+
+
+ /**
+ * Create an instance of the Playlist class.
+ *
+ * @param int $bank
+ * @param string $name
+ * @param Speaker $controller A speaker instance on the playlist's network
+ */
+ public function __construct($bank, $name, Speaker $controller)
+ {
+ $this->id = $bank;
+ $this->name = $name;
+ $this->speaker = $controller;
+ }
+
+
+ /**
+ * Get the id of the playlist.
+ *
+ * @return string
+ */
+ public function getId()
+ {
+ return $this->id;
+ }
+
+
+ /**
+ * Get the name of the playlist.
+ *
+ * @return string
+ */
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ public function play($index = 0)
+ {
+ $this->speaker->call('netusb', 'manageMcPlaylist', [$this->id, 'play', $index]);
+ }
+}
diff --git a/src/Queue.php b/src/Queue.php
new file mode 100755
index 0000000..12dccb7
--- /dev/null
+++ b/src/Queue.php
@@ -0,0 +1,70 @@
+count = $queueInfo['max_line'];
+ $this->playing_index = $queueInfo['playing_index'];
+ foreach ($queueInfo['track_info'] as $track_info) {
+ $this->tracks[] = new Track($track_info['input'], $track_info['text'], $track_info['thumbnail']);
+ }
+ }
+
+ /**
+ * The number of tracks in the queue.
+ *
+ * @return int
+ */
+ public function count()
+ {
+ return $this->count;
+ }
+
+ /**
+ * @return Track[]
+ */
+ public function getTracks()
+ {
+ return $this->tracks;
+ }
+
+ /**
+ * @return mixed
+ */
+ public function getPlayingIndex()
+ {
+ return $this->playing_index;
+ }
+}
diff --git a/src/Speaker.php b/src/Speaker.php
new file mode 100755
index 0000000..b821400
--- /dev/null
+++ b/src/Speaker.php
@@ -0,0 +1,446 @@
+
+ */
+class Speaker
+{
+ protected $model;
+ protected $name;
+ protected $device;
+ /**
+ * @var
+ */
+ protected $playlists;
+
+ /**
+ * @var
+ */
+ protected $favorites;
+
+ const NO_GROUP = "NoGroup";
+
+ /**
+ * Create an instance of the Speaker class.
+ *
+ * @param Device $device the ip address that the speaker is listening on
+ */
+ public function __construct($device)
+ {
+ $this->device = $device;
+ $this->model = $device->getDeviceInfo()['model_name'];
+ $this->name = $device->getNetworkStatus()['network_name'];
+ }
+
+ public function call($api, $method, array $args = [])
+ {
+ return $this->device->call($api, $method, $args);
+ }
+
+ public function getIp()
+ {
+ return $this->device->getIp();
+ }
+
+ /**
+ * @return mixed
+ */
+ public function getModel()
+ {
+ return $this->model;
+ }
+
+ /**
+ * @return mixed
+ */
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ /**
+ * Get the uuid of the group this speaker is a member of.
+ *
+ * @return string
+ */
+ public function getGroup()
+ {
+ $group = $this->call('dist', 'getDistributionInfo')['group_id'];
+ if (is_numeric($group) && intval($group == 0)) {
+ $group = self::NO_GROUP;
+ }
+ return $group;
+ }
+
+ /**
+ * Get the name of the group this speaker is a member of.
+ *
+ * @return string
+ */
+ public function getGroupName()
+ {
+ $group = $this->call('dist', 'getDistributionInfo')['group_name'];
+ return $group;
+ //return str_replace('(Linked) ', '', $group);
+ }
+
+ /**
+ * Check if this speaker is the coordinator of it's current group.
+ *
+ * @return bool
+ */
+ public function isCoordinator()
+ {
+ $role = $this->call('dist', 'getDistributionInfo')['role'];
+ return $role == 'server' || $this->getGroup() == Speaker::NO_GROUP;
+ }
+
+ /**
+ * Get the uuid of this speaker.
+ *
+ * @return string The uuid of this speaker
+ */
+ public function getUuid()
+ {
+ return $this->device->getUuid();
+ }
+
+ /**
+ * Get the current volume of this speaker.
+ *
+ * @return int
+ */
+ public function getVolume()
+ {
+ return (int)$this->call('zone', 'getStatus', ['main'])['volume'];
+ }
+
+ /**
+ * Adjust the volume of this speaker to a specific value.
+ *
+ * @param int $volume The amount to set the volume to between 0 and 100
+ *
+ * @return static
+ */
+ public function setVolume($volume)
+ {
+ $this->call('zone', 'setVolume', ['main', $volume]);
+ return $this;
+ }
+
+
+ /**
+ * Adjust the volume of this speaker by a relative amount.
+ *
+ * @param int $adjust The amount to adjust by between -100 and 100
+ *
+ * @return static
+ */
+ public function adjustVolume($adjust)
+ {
+ $this->call('zone', 'setVolume', ['main', $adjust > 0 ? 'up' : 'down', abs($adjust)]);
+ return $this;
+ }
+
+ /**
+ * Check if this speaker is currently muted.
+ *
+ * @return bool
+ */
+ public function isMuted()
+ {
+ return $this->call('zone', 'getStatus', ['main'])['mute'];
+ }
+
+ /**
+ * Unmute this speaker.
+ *
+ * @return static
+ */
+ public function unmute()
+ {
+ return $this->mute(false);
+ }
+
+ /**
+ * Mute this speaker.
+ *
+ * @param bool $mute Whether the speaker should be muted or not
+ *
+ * @return static
+ */
+ public function mute($mute = true)
+ {
+ $this->call('zone', 'setMute', ['main', $mute]);
+ return $this;
+ }
+
+ /**
+ * Power On this speaker.
+ *
+ * @return static
+ */
+ public function powerOn()
+ {
+ return $this->setPower('on');
+ }
+
+
+ /**
+ * Power On this speaker.
+ *
+ * @return bool
+ */
+ public function isPowerOn()
+ {
+ return $this->call('zone', 'getStatus', ['main'])["power"] == 'on';
+ }
+
+ /**
+ * Stand by this speaker.
+ *
+ * @return static
+ */
+ public function standBy()
+ {
+ return $this->setPower('standby');
+ }
+
+
+ /**
+ * Power toggle this speaker.
+ *
+ * @return static
+ */
+ public function powerToggle()
+ {
+ return $this->setPower('toggle');
+ }
+
+ private function setPower($power)
+ {
+ $this->call('zone', 'setPower', ['main', $power]);
+ return $this;
+ }
+
+ /**
+ * Get the currently active media info.
+ *
+ * @return array
+ */
+ public function getInput()
+ {
+ return $this->call('zone', 'getStatus', ['main'])['input'];
+ }
+
+
+ public function isStreaming()
+ {
+ $input = 'input' . $this->call('zone', 'getStatus', ['main'])['input'];
+ return $input == "tuner" || stripos($input, "hdmi") != false || stripos($input, "av") != false
+ || stripos($input, "aux") != false || stripos($input, "audio") != false;
+ }
+
+ /**
+ * Get List of available input
+ *
+ * @return array
+ */
+ public function getInputList()
+ {
+ $tags = $this->call('system', 'getTag');
+ $inputs = $tags['input_list'];
+ $return = [];
+ foreach ($inputs as $input) {
+ $return[] = $input['id'];
+ }
+ return $return;
+ }
+
+
+ /**
+ * Check if a playlist with the specified name exists on this network.
+ *
+ * If no case-sensitive match is found it will return a case-insensitive match.
+ *
+ * @param string $name The name of the playlist
+ *
+ * @return bool
+ */
+ public function hasPlaylist($name)
+ {
+ $playlists = $this->getPlaylists();
+ foreach ($playlists as $playlist) {
+ if ($playlist->getName() === $name) {
+ return true;
+ }
+ if (strtolower($playlist->getName()) === strtolower($name)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Get all the playlists available on the network.
+ *
+ * @return Playlist[]
+ */
+ public function getPlaylists()
+ {
+ if (is_array($this->playlists)) {
+ return $this->playlists;
+ }
+ $playlist_names = $this->call('netusb', 'getMcPlaylistName')['name_list'];
+ $playlists = [];
+ $index = 1;
+ foreach ($playlist_names as $playlist_name) {
+ $playlists[$playlist_name] = new Playlist($index++, $playlist_name, $this);
+ }
+ return $this->playlists = $playlists;
+ }
+
+ /**
+ * Get the playlist with the specified name.
+ *
+ * If no case-sensitive match is found it will return a case-insensitive match.
+ *
+ * @param string $name The name of the playlist
+ *
+ * @return Playlist|null
+ */
+ public function getPlaylistByName($name)
+ {
+ $roughMatch = false;
+ $playlists = $this->getPlaylists();
+ foreach ($playlists as $playlist) {
+ if ($playlist->getName() === $name) {
+ return $playlist;
+ }
+ if (strtolower($playlist->getName()) === strtolower($name)) {
+ $roughMatch = $playlist;
+ }
+ }
+ if ($roughMatch) {
+ return $roughMatch;
+ }
+ return null;
+ }
+
+ /**
+ * Get the playlist with the specified id.
+ *
+ * @param int $id The ID of the playlist
+ *
+ * @return Playlist
+ */
+ public function getPlaylistById($id)
+ {
+ $playlists = $this->getPlaylists();
+ foreach ($playlists as $playlist) {
+ if ($playlist->getId() === $id) {
+ return $playlist;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Check if a playlist with the specified name exists on this network.
+ *
+ * If no case-sensitive match is found it will return a case-insensitive match.
+ *
+ * @param string $name The name of the playlist
+ *
+ * @return bool
+ */
+ public function hasFavorite($name)
+ {
+ $favorites = $this->getFavorites();
+ foreach ($favorites as $item) {
+ if ($item->getName() === $name) {
+ return true;
+ }
+ if (strtolower($item->getName()) === strtolower($name)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Get Favorites on the network.
+ *
+ * @return Favorite[]
+ */
+ public function getFavorites()
+ {
+ if (is_array($this->favorites)) {
+ return $this->favorites;
+ }
+ $favorites_info = $this->call('netusb', 'getPresetInfo')['preset_info'];
+ $favorites = [];
+ $index = 0;
+ foreach ($favorites_info as $item) {
+ $index++;
+ if ($item['text'] != '') {
+ $favorites[$item['text']] = new Favorite($index, $item, $this);
+ }
+ }
+ return $this->favorites = $favorites;
+ }
+
+ /**
+ * Get the playlist with the specified name.
+ *
+ * If no case-sensitive match is found it will return a case-insensitive match.
+ *
+ * @param string $name The name of the playlist
+ *
+ * @return Favorite|null
+ */
+ public function getFavoriteByName($name)
+ {
+ $roughMatch = false;
+ $favorites = $this->getFavorites();
+ foreach ($favorites as $item) {
+ if ($item->getName() === $name) {
+ return $item;
+ }
+ if (strtolower($item->getName()) === strtolower($name)) {
+ $roughMatch = $item;
+ }
+ }
+ if ($roughMatch) {
+ return $roughMatch;
+ }
+ return null;
+ }
+
+ /**
+ * Get the playlist with the specified id.
+ *
+ * @param int $id The ID of the playlist
+ *
+ * @return Favorite
+ */
+ public function getFavoriteById($id)
+ {
+ $favorites = $this->getFavorites();
+ foreach ($favorites as $item) {
+ if ($item->getId() === $id) {
+ return $item;
+ }
+ }
+ return null;
+ }
+}
diff --git a/src/State.php b/src/State.php
new file mode 100755
index 0000000..6df2f7c
--- /dev/null
+++ b/src/State.php
@@ -0,0 +1,59 @@
+
+ */
+class State
+{
+ /**
+ * @var string $duration The duration of the currently active track (hh:mm:ss).
+ */
+ public $duration = "";
+
+ /**
+ * @var string $position The position of the currently active track (hh:mm:ss).
+ */
+ public $position = "";
+
+ public $track;
+
+ /**
+ * Create a Track object.
+ */
+ public function __construct()
+ {
+ }
+
+ /**
+ * Update the track properties using an xml element.
+ *
+ * @param $playInfo
+ * @param $ip
+ * @return static
+ */
+ public static function buildState($playInfo, $ip)
+ {
+ $state = new State();
+ if ($art = $playInfo['albumart_url']) {
+ if (substr($art, 0, 4) !== "http") {
+ $art = ltrim($art, "/");
+ $art = sprintf("http://%s:80/%s", $ip, $art);
+ }
+ }
+ $state->track = new Track(
+ $playInfo['input'],
+ $playInfo['track'],
+ $art,
+ $playInfo['artist'],
+ $playInfo['album']
+ );
+ $state->duration = $playInfo['total_time'];
+ $state->position = $playInfo['play_time'];
+ return $state;
+ }
+}
diff --git a/src/Tracks/Track.php b/src/Tracks/Track.php
new file mode 100755
index 0000000..9f8ccc7
--- /dev/null
+++ b/src/Tracks/Track.php
@@ -0,0 +1,96 @@
+
+ */
+class Track
+{
+
+ /**
+ * @var string $title The name of the track.
+ */
+ protected $title = "";
+
+ /**
+ * @var string $artist The name of the artist of the track.
+ */
+ protected $artist = "";
+
+ /**
+ * @var string $album The name of the album of the track.
+ */
+ protected $album = "";
+
+ /**
+ * @var string $albumArt The full path to the album art for this track.
+ */
+ protected $albumArt = "";
+
+ protected $input = "";
+
+ /**
+ * Track constructor.
+ * @param $input
+ * @param $title
+ * @param $albumArt
+ * @param null $artist
+ * @param null $album
+ */
+ public function __construct($input, $title, $albumArt, $artist = null, $album = null)
+ {
+ $this->title = $title;
+ $this->artist = $artist;
+ $this->album = $album;
+ $this->albumArt = stripcslashes($albumArt);
+ $this->input = $input;
+ }
+
+ /**
+ * @return string
+ */
+ public function getTitle()
+ {
+ return $this->title;
+ }
+
+ /**
+ * @return string
+ */
+ public function getArtist()
+ {
+ return $this->artist;
+ }
+
+ /**
+ * @return string
+ */
+ public function getAlbum()
+ {
+ return $this->album;
+ }
+
+ /**
+ * @return string
+ */
+ public function getAlbumArt()
+ {
+ return $this->albumArt;
+ }
+
+ /**
+ * @return string
+ */
+ public function getInput()
+ {
+ return $this->input;
+ }
+}
diff --git a/test/bootstrap.php b/test/bootstrap.php
deleted file mode 100644
index a021287..0000000
--- a/test/bootstrap.php
+++ /dev/null
@@ -1,14 +0,0 @@
-add('MusicCast\Tests', __DIR__);
-return $loader;
diff --git a/tests/Api/DistributionLiveTest.php b/tests/Api/DistributionLiveTest.php
new file mode 100755
index 0000000..7b1b3d8
--- /dev/null
+++ b/tests/Api/DistributionLiveTest.php
@@ -0,0 +1,58 @@
+
+ */
+
+namespace MusicCastTests\Api;
+
+class DistributionLiveTest extends \MusicCastTests\LiveTest
+{
+ protected function setUp()
+ {
+ parent::setUp();
+ }
+
+ /**
+ * @test
+ */
+ public function testGetDistributionInfo()
+ {
+ self::assertArrayHasKey('group_id', $this->client->api('dist')->getDistributionInfo());
+ }
+
+ /**
+ * @test
+ */
+ public function testStartDistribution()
+ {
+ //self::assertArrayHasKey('???', $this->client->api('dist')->startDistribution());
+ $this->markTestSkipped('dist/startDistribution method not implemented');
+ }
+
+ /**
+ * @test
+ */
+ public function testSetClientInfo()
+ {
+ self::assertArrayHasKey('response_code', $this->client->api('dist')->setClientInfo());
+ }
+
+ /**
+ * @test
+ */
+ public function testSetGroupName()
+ {
+ //self::assertArrayHasKey('???', $this->client->api('dist')->setGroupName());
+ $this->markTestSkipped('dist/setGroupName method not implemented');
+ }
+
+
+ /**
+ * @test
+ */
+ public function testSetServerInfo()
+ {
+ //self::assertArrayHasKey('group_id', $this->client->api('dist')->setServerInfo());
+ $this->markTestSkipped('dist/setServerInfo method not implemented');
+ }
+}
diff --git a/tests/Api/NetworkUSBLiveTest.php b/tests/Api/NetworkUSBLiveTest.php
new file mode 100755
index 0000000..a918b8b
--- /dev/null
+++ b/tests/Api/NetworkUSBLiveTest.php
@@ -0,0 +1,164 @@
+
+ */
+
+namespace MusicCastTests\Api;
+
+use Symfony\Component\Yaml\Yaml;
+
+class NetworkUSBLiveTest extends \MusicCastTests\LiveTest
+{
+ protected function setUp()
+ {
+ parent::setUp();
+ $options = Yaml::parse(file_get_contents(__DIR__ . '/../env.yml'));
+ $controller = $this->network->getControllerByIp($options['host']);
+ $controller->getPlaylistById(1)->play();
+ }
+ /**
+ * @test
+ */
+ public function testGetPresetInfo()
+ {
+ self::assertArrayHasKey('preset_info', $this->client->api('netusb')->getPresetInfo());
+ }
+
+ /**
+ * @test
+ */
+ public function testGetPlayInfo()
+ {
+ self::assertArrayHasKey('input', $this->client->api('netusb')->getPlayInfo());
+ }
+
+ /**
+ * @test
+ */
+ public function testSetPlayback()
+ {
+ self::assertArrayHasKey('response_code', $this->client->api('netusb')->setPlayback("play_pause"));
+ self::assertArrayHasKey('response_code', $this->client->api('netusb')->setPlayback("play_pause"));
+ }
+
+ /**
+ * @test
+ */
+ public function testToggleRepeat()
+ {
+ self::assertArrayHasKey('response_code', $this->client->api('netusb')->toggleRepeat());
+ }
+
+ /**
+ * @test
+ */
+ public function testToggleShuffle()
+ {
+ self::assertArrayHasKey('response_code', $this->client->api('netusb')->toggleShuffle());
+ }
+
+ /**
+ * @test
+ */
+ public function testGetListInfo()
+ {
+ self::assertArrayHasKey(
+ 'list_info',
+ $this->client->api('netusb')->getListInfo('bluetooth', 5)
+ );
+ }
+
+ /**
+ * @test
+ */
+ public function testSetListControl()
+ {
+ //self::assertArrayHasKey('???', $this->client->api('netusb')->setListControl());
+ $this->markTestSkipped('netusb/setListControl method not implemented');
+ }
+
+ /**
+ * @test
+ */
+ public function testSetSearchString()
+ {
+ //self::assertArrayHasKey('???', $this->client->api('netusb')->setSearchString());
+ $this->markTestSkipped('netusb/setSearchString method not implemented');
+ }
+
+ /**
+ * @test
+ */
+ public function testRecallPreset()
+ {
+ self::assertArrayHasKey('response_code', $this->client->api('netusb')->recallPreset('main', 1));
+ }
+
+ /**
+ * @test
+ */
+ public function testStorePreset()
+ {
+ //self::assertArrayHasKey('???', $this->client->api('netusb')->storePreset());
+ $this->markTestSkipped('netusb/storePreset method not implemented');
+ }
+
+ /**
+ * @test
+ */
+ public function testGetAccountStatus()
+ {
+ self::assertArrayHasKey('service_list', $this->client->api('netusb')->getAccountStatus());
+ }
+
+ /**
+ * @test
+ */
+ public function testSwitchAccount()
+ {
+ //self::assertArrayHasKey('???', $this->client->api('netusb')->switchAccount());
+ $this->markTestSkipped('netusb/switchAccount method not implemented');
+ }
+
+ /**
+ * @test
+ */
+ public function testGetServiceInfo()
+ {
+ //self::assertArrayHasKey('???', $this->client->api('netusb')->getServiceInfo());
+ $this->markTestSkipped('netusb/getServiceInfo method not implemented');
+ }
+
+ /**
+ * @test
+ */
+ public function testGetMcPlaylistName()
+ {
+ self::assertArrayHasKey('name_list', $this->client->api('netusb')->getMcPlaylistName());
+ }
+
+ /**
+ * @test
+ */
+ public function testGetPlayQueue()
+ {
+ self::assertArrayHasKey('type', $this->client->api('netusb')->getPlayQueue());
+ }
+
+ /**
+ * @test
+ */
+ public function testGetRecentInfo()
+ {
+ self::assertArrayHasKey('recent_info', $this->client->api('netusb')->getRecentInfo());
+ }
+
+ /**
+ * @test
+ */
+ public function testSetYmapUri()
+ {
+ //self::assertArrayHasKey('???', $this->client->api('netusb')->setYmapUri());
+ $this->markTestSkipped('netusb/setYmapUri method not implemented');
+ }
+}
diff --git a/tests/Api/SystemLiveTest.php b/tests/Api/SystemLiveTest.php
new file mode 100755
index 0000000..728e4d6
--- /dev/null
+++ b/tests/Api/SystemLiveTest.php
@@ -0,0 +1,124 @@
+
+ */
+
+namespace MusicCastTests\Api;
+
+class SystemLiveTest extends \MusicCastTests\LiveTest
+{
+ protected function setUp()
+ {
+ parent::setUp();
+ }
+ /**
+ * @test
+ */
+ public function testGetDeviceInfo()
+ {
+ self::assertArrayHasKey('model_name', $this->client->api('system')->getDeviceInfo());
+ }
+
+ /**
+ * @test
+ */
+ public function testGetFeatures()
+ {
+ self::assertArrayHasKey('system', $this->client->api('system')->getFeatures());
+ self::assertArrayHasKey('func_list', $this->client->api('system')->getFeatures()['system']);
+ }
+
+ /**
+ * @test
+ */
+ public function testGetNetworkStatus()
+ {
+ self::assertArrayHasKey('network_name', $this->client->api('system')->getNetworkStatus());
+ }
+
+ /**
+ * @test
+ */
+ public function testGetFuncStatus()
+ {
+ $this->client->api('system')->getFuncStatus();
+ }
+
+ /**
+ * @test
+ */
+ public function testSetAutoPowerStandby()
+ {
+ $funcStatus = $this->client->api('system')->getFuncStatus();
+ if (array_key_exists('auto_power_standby', $funcStatus)) {
+ $previous = $funcStatus['auto_power_standby'];
+ $this->client->api('system')->setAutoPowerStandby(!$previous);
+ sleep(1);
+ self::assertTrue($this->client->api('system')->getFuncStatus()['auto_power_standby'] != $previous);
+ $this->client->api('system')->setAutoPowerStandby($previous);
+ sleep(1);
+ self::assertTrue($this->client->api('system')->getFuncStatus()['auto_power_standby'] == $previous);
+ return;
+ }
+ echo 'Can\'t test setAutoPowerStandby on this device';
+ }
+
+ /**
+ * @test
+ */
+ public function testGetLocationInfo()
+ {
+ self::assertArrayHasKey('name', $this->client->api('system')->getLocationInfo());
+ }
+
+
+ /**
+ * @test
+ */
+ public function testSendIrCode()
+ {
+ $this->client->api('system')->sendIrCode('00000000');
+ }
+
+ /**
+ * @test
+ */
+ public function testGetNameText()
+ {
+ self::assertArrayHasKey('zone_list', $this->client->api('system')->getNameText());
+ }
+
+ /**
+ * @test
+ */
+ public function testIsNewFirmwareAvailable()
+ {
+ self::assertArrayHasKey('available', $this->client->api('system')->isNewFirmwareAvailable());
+ }
+
+ /**
+ * @test
+ */
+ public function testGetTag()
+ {
+ self::assertArrayHasKey('zone_list', $this->client->api('system')->getTag());
+ }
+
+ /**
+ * @test
+ */
+ public function testGetDisklavierSettings()
+ {
+ //self::assertArrayHasKey('enable', $this->client->api('system')->getDisklavierSettings());
+ $this->markTestSkipped('system/getDisklavierSettings method not implemented');
+ }
+
+
+ /**
+ * @test
+ */
+ public function testGetMusicCastTreeInfo()
+ {
+ self::assertArrayHasKey('mode', $this->client->api('system')->getMusicCastTreeInfo());
+ }
+}
diff --git a/tests/Api/ZoneLiveTest.php b/tests/Api/ZoneLiveTest.php
new file mode 100755
index 0000000..2c6e61c
--- /dev/null
+++ b/tests/Api/ZoneLiveTest.php
@@ -0,0 +1,94 @@
+
+ */
+
+namespace MusicCastTests\Api;
+
+use MusicCast\Exception\ErrorException;
+
+class ZoneLiveTest extends \MusicCastTests\LiveTest
+{
+ protected function setUp()
+ {
+ parent::setUp();
+ }
+ /**
+ * @test
+ */
+ public function testGetStatus()
+ {
+ self::assertArrayHasKey('power', $this->client->api('zone')->getStatus('main'));
+ }
+
+ /**
+ * @test
+ */
+ public function testGetStatusThrowErrorExceptionIfBadZoneName()
+ {
+ $this->expectException(ErrorException::class);
+ $this->client->api('zone')->getStatus('fakeZone');
+ }
+
+ public function testGetSoundProgramList()
+ {
+ self::assertArrayHasKey('sound_program_list', $this->client->api('zone')->getSoundProgramList('main'));
+ }
+
+ public function testSetPower()
+ {
+ $power = $this->client->api('zone')->getStatus('main')['power'];
+ $this->client->api('zone')->setPower('main', $power);
+ }
+
+ public function setSleep()
+ {
+ $sleep = $this->client->api('zone')->getStatus('main')['sleep'];
+ $this->client->api('zone')->setSleep('main', $sleep);
+ }
+
+ public function testAdjustVolume()
+ {
+ $volume = $this->client->api('zone')->getStatus('main')['volume'];
+ $this->client->api('zone')->setVolume('main', 'up', '1');
+ sleep(1);
+ self::assertEquals($volume + 1, $this->client->api('zone')->getStatus('main')['volume']);
+ $this->client->api('zone')->setVolume('main', 'down', '1');
+ sleep(1);
+ self::assertEquals($volume, $this->client->api('zone')->getStatus('main')['volume']);
+ }
+
+ public function testSetVolume()
+ {
+ $volume = $this->client->api('zone')->getStatus('main')['volume'];
+ $this->client->api('zone')->setVolume('main', $volume + 1);
+ sleep(1);
+ self::assertEquals($volume + 1, $this->client->api('zone')->getStatus('main')['volume']);
+ $this->client->api('zone')->setVolume('main', $volume);
+ sleep(1);
+ self::assertEquals($volume, $this->client->api('zone')->getStatus('main')['volume']);
+ }
+
+ public function setMute()
+ {
+ $mute = $this->client->api('zone')->getStatus('main')['mute'];
+ $this->client->api('zone')->setMute('main', !$mute);
+ sleep(1);
+ self::assertEquals(!$mute, $this->client->api('zone')->getStatus('main')['mute']);
+ $this->client->api('zone')->setMute('main', $mute);
+ sleep(1);
+ self::assertEquals($mute, $this->client->api('zone')->getStatus('main')['volume']);
+ }
+
+ public function testPrepareInputChange()
+ {
+ $input = $this->client->api('zone')->getStatus('main')['input'];
+ $this->client->api('zone')->prepareInputChange('main', $input);
+ }
+
+ public function testSetInput()
+ {
+ $input = $this->client->api('zone')->getStatus('main')['input'];
+ $this->client->api('zone')->setInput('main', $input);
+ }
+}
diff --git a/test/MusicCast/Tests/ClientTest.php b/tests/ClientTest.php
old mode 100644
new mode 100755
similarity index 60%
rename from test/MusicCast/Tests/ClientTest.php
rename to tests/ClientTest.php
index 1390cd7..22abe1c
--- a/test/MusicCast/Tests/ClientTest.php
+++ b/tests/ClientTest.php
@@ -1,35 +1,30 @@
options = Yaml::parse(file_get_contents(__DIR__ . '/../../env.yml'));
- }
-
- public function test()
- {
- $client = new Client($this->options);
-// var_dump($client->api('system')->functionStatus());die;
-// var_dump($client->api('system')->sendIrCode());
- }
-
/**
* @test
*/
public function shouldNotHaveToPassHttpClientToConstructor()
{
$client = new Client($this->options);
-
self::assertInstanceOf(\Http\Client\HttpClient::class, $client->getHttpClient());
}
+
+ protected function setUp()
+ {
+ parent::setUp();
+ $this->options = Yaml::parse(file_get_contents(__DIR__ . '/env.yml'));
+ }
}
diff --git a/tests/ControllerLiveTest.php b/tests/ControllerLiveTest.php
new file mode 100755
index 0000000..720b166
--- /dev/null
+++ b/tests/ControllerLiveTest.php
@@ -0,0 +1,234 @@
+controller = $this->network->getControllerByIp($this->options['host']);
+ $this->controller->powerOn();
+ $this->controller->getPlaylistById(1)->play();
+ time_nanosleep(0, 500 * 1000000);//500ms
+ }
+
+ public function testConstructor1()
+ {
+ foreach ($this->network->getSpeakers() as $speaker) {
+ if ($speaker->isCoordinator()) {
+ $controller = new Controller($speaker, $this->network, 0);
+ $this->assertSame($speaker->getIp(), $controller->getIp());
+ return;
+ }
+ }
+
+ throw new \Exception("No speakers found that are the coordinator of their group");
+ }
+
+
+ public function testConstructor2()
+ {
+ $this->expectException("InvalidArgumentException");
+
+ foreach ($this->network->getSpeakers() as $speaker) {
+ if (!$speaker->isCoordinator()) {
+ new Controller($speaker, $this->network, 0);
+ return;
+ }
+ }
+
+ $this->markTestSkipped("No speakers found that are not the coordinator of their group");
+ }
+
+
+ public function testIsCoordinator()
+ {
+ $this->assertTrue($this->controller->isCoordinator());
+ }
+
+
+ public function testGetStateName()
+ {
+ $states = ["play", "stop", "pause", "play_pause", "previous", "next",
+ "fast_reverse_start", "fast_reverse_end", "fast_forward_start",
+ "fast_forward_end"];
+ foreach ($this->network->getControllers() as $controller) {
+ $this->assertContains($controller->getStateName(), $states);
+ }
+ }
+
+
+ public function testGetState()
+ {
+ $states = [Controller::STATE_STOPPED, Controller::STATE_PLAYING, Controller::STATE_PAUSED, Controller::STATE_TRANSITIONING];
+ foreach ($this->network->getControllers() as $controller) {
+ $this->assertContains($controller->getState(), $states);
+ }
+ }
+
+
+ public function testGetStateDetails()
+ {
+ $track_keys = ["input", "title", "artist", "album", "albumArt"];
+ $state_keys = ["duration", "position"];
+ $state = $this->controller->getStateDetails();
+ foreach ($state_keys as $key) {
+ $this->assertObjectHasAttribute($key, $state);
+ }
+ foreach ($track_keys as $key) {
+ $this->assertObjectHasAttribute($key, $state->track);
+ }
+ }
+
+
+ public function testNext()
+ {
+ $controller = $this->controller;
+ $number = $controller->getQueue()->getPlayingIndex();
+ $controller->next();
+ $this->assertSame($controller->getQueue()->getPlayingIndex(), $number + 1);
+ }
+
+
+ public function testPrevious()
+ {
+ $controller = $this->controller;
+ $number = $controller->getQueue()->getPlayingIndex();
+ $controller->previous();
+ $this->assertSame($controller->getQueue()->getPlayingIndex(), $number);
+ }
+
+
+ public function testGetSpeakers()
+ {
+ $speakers = $this->controller->getSpeakers();
+ $this->assertContainsOnlyInstancesOf(Speaker::class, $speakers);
+ }
+
+
+ public function testSetVolume()
+ {
+ $controller = $this->controller;
+ $volume = $controller->getVolume();
+ $controller->setVolume($volume - 3);
+ sleep(1);
+ foreach ($controller->getSpeakers() as $speaker) {
+ $this->assertSame($volume - 3, $speaker->getVolume());
+ }
+ }
+
+
+ public function testAdjustVolume1()
+ {
+ $controller = $this->controller;
+ $volume = $controller->getVolume();
+ $controller->setVolume($volume - 3);
+ sleep(1);
+ $controller->adjustVolume(3);
+ sleep(1);
+ foreach ($controller->getSpeakers() as $speaker) {
+ $this->assertSame($volume, $speaker->getVolume());
+ }
+ }
+
+
+ public function testAdjustVolume2()
+ {
+ $controller = $this->controller;
+ $volume = $controller->getVolume();
+ $controller->setVolume($volume + 3);
+ sleep(1);
+ $controller->adjustVolume(3 * -1);
+ sleep(1);
+ foreach ($controller->getSpeakers() as $speaker) {
+ $this->assertSame($volume, $speaker->getVolume());
+ }
+ }
+
+ public function testGetPlaylists()
+ {
+ $playlists = $this->controller->getPlaylists();
+ self::assertNotNull($playlists);
+ }
+
+ public function testGetPlaylistBy()
+ {
+ $playlists = $this->controller->getPlaylists();
+ $seek = reset($playlists);
+ $playlist = $this->controller->getPlaylistById($seek->getId());
+ self::assertEquals($seek, $playlist);
+ $playlist = $this->controller->getPlaylistByName($seek->getName());
+ self::assertEquals($seek, $playlist);
+ }
+
+ public function testGetFavorites()
+ {
+ $playlists = $this->controller->getFavorites();
+ self::assertNotNull($playlists);
+ }
+
+ public function testGetFavoriteBy()
+ {
+ $favorites = $this->controller->getFavorites();
+ $seek = reset($favorites);
+ $favorite = $this->controller->getFavoriteById($seek->getId());
+ self::assertEquals($seek, $favorite);
+ $favorite = $this->controller->getFavoriteByName($seek->getName());
+ self::assertEquals($seek, $favorite);
+ }
+
+ public function testAddSpeaker()
+ {
+ foreach ($this->network->getSpeakers() as $speaker) {
+ if (!$speaker->isCoordinator() && $speaker->getGroup() == Speaker::NO_GROUP) {
+ $this->controller->addSpeaker($speaker);
+ $this->assertSame($speaker->getGroup(), $this->controller->getGroup());
+ return;
+ }
+ }
+ }
+
+ public function testRemoveSpeaker()
+ {
+ foreach ($this->controller->getSpeakers() as $speaker) {
+ $this->controller->removeSpeaker($speaker);
+ $this->assertNotSame($speaker->getGroup(), $this->controller->getGroup());
+ return;
+ }
+ }
+
+ public function testPowerOn()
+ {
+ $this->controller->powerOn();
+ }
+
+
+
+ public function testMute()
+ {
+ $this->controller->mute();
+ }
+
+
+
+ public function testUnMute()
+ {
+ $this->controller->unmute();
+ }
+}
diff --git a/tests/ControllerTest.php b/tests/ControllerTest.php
new file mode 100755
index 0000000..b8aee2f
--- /dev/null
+++ b/tests/ControllerTest.php
@@ -0,0 +1,129 @@
+controller = $this->getController();
+ }
+
+ public function getState()
+ {
+ self::assertEquals(Controller::STATE_PLAYING, $this->controller->getState());
+ }
+
+ public function testGetStateDetails()
+ {
+ $state = $this->controller->getStateDetails();
+ self::assertEquals(
+ new Track(
+ "server",
+ "Voulez-Vous",
+ "http://localhost:80/YamahaRemoteControl/AlbumART/AlbumART1115.jpg",
+ "ABBA",
+ "Gold: Greatest Hits"
+ ),
+ $state->track
+ );
+ self::assertEquals("0", $state->duration);
+ self::assertEquals("190", $state->position);
+ }
+
+
+ public function testGetPlaylists()
+ {
+ $playlists = $this->controller->getPlaylists();
+ $keys = ["WakeUp",
+ "Rock",
+ "Lounge",
+ "John",
+ "Jane"];
+ foreach ($keys as $key) {
+ self::assertArrayHasKey($key, $playlists);
+ }
+ self::assertTrue(sizeof($playlists) == sizeof($keys));
+ }
+
+ public function getPlaylistByName()
+ {
+ $playlist = $this->controller->getPlaylistByName("WakeUp");
+ self::assertEquals("WakeUp", $playlist->getName());
+ }
+
+ public function getPlaylistByNameIgnoreCase()
+ {
+ $playlist = $this->controller->getPlaylistByName("wAkEuP");
+ self::assertEquals("WakeUp", $playlist->getName());
+ }
+
+ public function testGetPlaylistById()
+ {
+ $playlist = $this->controller->getPlaylistById(1);
+ self::assertEquals("WakeUp", $playlist->getName());
+ }
+
+ public function testGetFavorites()
+ {
+ $favorites = $this->controller->getFavorites();
+ $keys = ["OÜI FM Classic Rock",
+ "4U Funky Classics"];
+ foreach ($keys as $key) {
+ self::assertArrayHasKey($key, $favorites);
+ }
+ self::assertTrue(sizeof($favorites) == sizeof($keys));
+ }
+
+ public function getFavoriteByName()
+ {
+ $favorite = $this->controller->getFavoriteByName("WakeUp");
+ self::assertEquals("OÜI FM Classic Rock", $favorite->getName());
+ }
+
+ public function getFavoriteByNameIgnoreCase()
+ {
+ $favorite = $this->controller->getFavoriteByName("wAkEuP");
+ self::assertEquals("OÜI FM Classic Rock", $favorite->getName());
+ }
+
+ public function testGetFavoriteById()
+ {
+ $favorite = $this->controller->getFavoriteById(1);
+ self::assertEquals("OÜI FM Classic Rock", $favorite->getName());
+ }
+
+ public function testHasFavorite()
+ {
+ self::assertTrue($this->controller->hasFavorite("OÜI FM Classic Rock"));
+ self::assertTrue($this->controller->hasFavorite("OÜI fm clASSic rOCk"));
+ self::assertFalse($this->controller->hasFavorite("Non existing favorite"));
+ }
+
+ public function testGetQueue()
+ {
+ $queue = $this->controller->getQueue();
+ self::assertEquals(0, $queue->getPlayingIndex());
+ self::assertEquals(4, $queue->count());
+ self::assertEquals(new Track(
+ "server",
+ "Fernando",
+ "http:\/\/192.168.86.4:50002\/transcoder\/jpegtnscaler.cgi\/ebdart\/73993.jpg"
+ ), $queue->getTracks()[0]);
+ }
+}
diff --git a/tests/DeviceTest.php b/tests/DeviceTest.php
new file mode 100755
index 0000000..503c565
--- /dev/null
+++ b/tests/DeviceTest.php
@@ -0,0 +1,73 @@
+device = $this->getDevice();
+ }
+
+ public function testCall()
+ {
+ self::assertEquals('ABCDEEFAA063', $this->device->call('system', 'getDeviceInfo')['device_id']);
+ }
+
+ public function testGetDeviceInfo()
+ {
+ $infokeys = ["model_name", "destination", "device_id", "system_id", "system_version", "api_version",
+ "netmodule_version", "netmodule_checksum", "operation_mode",
+ "update_error_code"];
+ $info = $this->device->getDeviceInfo();
+ foreach ($infokeys as $infokey) {
+ self::assertArrayHasKey($infokey, $info);
+ }
+ }
+
+ public function testGetLocationInfo()
+ {
+ $infokeys = ["id", "name", "zone_list"];
+ $info = $this->device->getLocationInfo();
+ foreach ($infokeys as $infokey) {
+ self::assertArrayHasKey($infokey, $info);
+ }
+ }
+
+ public function testGetMusicCastTreeInfo()
+ {
+ $infokeys = ["mode", "own_mac_idx", "mac_address_list", "ap_list", "hop_num"];
+ $info = $this->device->getMusicCastTreeInfo();
+ foreach ($infokeys as $infokey) {
+ self::assertArrayHasKey($infokey, $info);
+ }
+ }
+
+ public function testGetNetworkStatus()
+ {
+ $infokeys = ["network_name", "connection", "ip_address", "default_gateway", "wireless_lan"];
+ $info = $this->device->getNetworkStatus();
+ foreach ($infokeys as $infokey) {
+ self::assertArrayHasKey($infokey, $info);
+ }
+ }
+
+ public function testGetUuid()
+ {
+ self::assertEquals("ABCDEEFAA063", $this->device->getUuid());
+ }
+}
diff --git a/tests/LiveTest.php b/tests/LiveTest.php
new file mode 100755
index 0000000..b4db291
--- /dev/null
+++ b/tests/LiveTest.php
@@ -0,0 +1,46 @@
+network = new Network();
+
+ if (empty($_ENV["MUSICCAST_LIVE_TESTS"])) {
+ $this->markTestSkipped("Ignoring live tests
+ (these can be run setting the MUSICCAST_LIVE_TESTS environment variable)");
+ return;
+ }
+
+ try {
+ $this->network->getSpeakers();
+ } catch (\Exception $e) {
+ $this->markTestSkipped("No speakers found on the current network");
+ }
+ $this->options = Yaml::parse(file_get_contents(__DIR__ . '/env.yml'));
+ $this->client = new Client($this->options);
+ }
+}
diff --git a/tests/MockTest.php b/tests/MockTest.php
new file mode 100755
index 0000000..604a76a
--- /dev/null
+++ b/tests/MockTest.php
@@ -0,0 +1,128 @@
+network = Mockery::mock(Network::class);
+ $this->network->shouldReceive("getSpeakers")->andReturn([]);
+ $this->client = MockTest::mockCLient();
+ }
+
+ private static function mockCLient()
+ {
+ $client = Mockery::mock("MusicCast\Client");
+
+ $systemApi = Mockery::mock("MusicCast\Api\System");
+ MockTest::mockMethod($systemApi, 'system', "getDeviceInfo");
+ MockTest::mockMethod($systemApi, 'system', "getDisklavierSettings");
+ MockTest::mockMethod($systemApi, 'system', "getFeatures");
+ MockTest::mockMethod($systemApi, 'system', "getFuncStatus");
+ MockTest::mockMethod($systemApi, 'system', "getLocationInfo");
+ MockTest::mockMethod($systemApi, 'system', "getMusicCastTreeInfo");
+ MockTest::mockMethod($systemApi, 'system', "getNameText");
+ MockTest::mockMethod($systemApi, 'system', "getNetworkStatus");
+ MockTest::mockMethod($systemApi, 'system', "getTag");
+ MockTest::mockMethod($systemApi, 'system', "isNewFirmwareAvailable");
+ MockTest::mockMethod($systemApi, 'system', "sendIrCode");
+ $client->shouldReceive("api")->with('system')->andReturn($systemApi);
+
+ $distApi = Mockery::mock("MusicCast\Api\Distribution");
+ MockTest::mockMethod($distApi, 'dist', "getDistributionInfo");
+ MockTest::mockMethod($distApi, 'dist', "setClientInfo");
+ MockTest::mockMethod($distApi, 'dist', "setGroupName");
+ MockTest::mockMethod($distApi, 'dist', "setServerInfo");
+ MockTest::mockMethod($distApi, 'dist', "startDistribution");
+ $client->shouldReceive("api")->with('dist')->andReturn($distApi);
+
+ $zoneApi = Mockery::mock("MusicCast\Api\Zone");
+ MockTest::mockMethod($zoneApi, 'zone', "getSignalInfo");
+ MockTest::mockMethod($zoneApi, 'zone', "getSoundProgramList");
+ MockTest::mockMethod($zoneApi, 'zone', "getStatus");
+ MockTest::mockMethod($zoneApi, 'zone', "prepareInputChange");
+ MockTest::mockMethod($zoneApi, 'zone', "setInput");
+ MockTest::mockMethod($zoneApi, 'zone', "setMute");
+ MockTest::mockMethod($zoneApi, 'zone', "setPower");
+ MockTest::mockMethod($zoneApi, 'zone', "setSleep");
+ MockTest::mockMethod($zoneApi, 'zone', "setVolume");
+ $client->shouldReceive("api")->with('zone')->andReturn($zoneApi);
+
+ $netusbApi = Mockery::mock("MusicCast\Api\NetworkUSB");
+ MockTest::mockMethod($netusbApi, 'netusb', "getAccountStatus");
+ MockTest::mockMethod($netusbApi, 'netusb', "getListInfo");
+ MockTest::mockMethod($netusbApi, 'netusb', "getMcPlaylist");
+ MockTest::mockMethod($netusbApi, 'netusb', "getMcPlaylistName");
+ MockTest::mockMethod($netusbApi, 'netusb', "getPlayInfo");
+ MockTest::mockMethod($netusbApi, 'netusb', "getPlayQueue");
+ MockTest::mockMethod($netusbApi, 'netusb', "getPresetInfo");
+ MockTest::mockMethod($netusbApi, 'netusb', "getRecentInfo");
+ MockTest::mockMethod($netusbApi, 'netusb', "manageMcPlaylist");
+ MockTest::mockMethod($netusbApi, 'netusb', "recallPreset");
+ MockTest::mockMethod($netusbApi, 'netusb', "setPlayback");
+ MockTest::mockMethod($netusbApi, 'netusb', "storePreset");
+ MockTest::mockMethod($netusbApi, 'netusb', "toggleRepeat");
+ MockTest::mockMethod($netusbApi, 'netusb', "toggleShuffle");
+ $client->shouldReceive("api")->with('netusb')->andReturn($netusbApi);
+ return $client;
+ }
+
+ private static function mockMethod($api, $apiName, $method)
+ {
+ $api->shouldReceive($method)->andReturn(json_decode(
+ file_get_contents(__DIR__ . '/assets/' . $apiName . '/' . $method . '.json'),
+ true
+ ));
+ }
+
+ protected function getController()
+ {
+ $speaker = $this->getSpeaker();
+ return new Controller($speaker, $this->network, 0);
+ }
+
+ protected function getSpeaker()
+ {
+ $speaker = new Speaker($this->getDevice());
+ return $speaker;
+ }
+
+ protected function getDevice()
+ {
+ $device = new Device("localhost", 80, new DoctrineCachePool(new VoidCache()), new NullLogger());
+ $reflection = new ReflectionClass($device);
+ $reflection_property = $reflection->getProperty('client');
+ $reflection_property->setAccessible(true);
+ $reflection_property->setValue($device, $this->client);
+ return $device;
+ }
+}
diff --git a/tests/NetworkLiveTest.php b/tests/NetworkLiveTest.php
new file mode 100755
index 0000000..148710f
--- /dev/null
+++ b/tests/NetworkLiveTest.php
@@ -0,0 +1,37 @@
+network->getSpeakers();
+ self::assertNotNull($speakers);
+ }
+
+ public function testGetControllers()
+ {
+ $controllers = $this->network->getControllers();
+ self::assertNotNull($controllers);
+ }
+
+ public function testGetController()
+ {
+ $controller = $this->network->getController();
+ self::assertNotNull($controller);
+ }
+
+ public function testGetControllerByIp()
+ {
+ $ip = $this->network->getController()->getIp();
+ $controller = $this->network->getControllerByIp($ip);
+ self::assertEquals($ip, $controller->getIp());
+ }
+}
diff --git a/tests/NetworkTest.php b/tests/NetworkTest.php
new file mode 100755
index 0000000..577036d
--- /dev/null
+++ b/tests/NetworkTest.php
@@ -0,0 +1,69 @@
+network = new Network;
+ }
+
+ public function testDefaultValues()
+ {
+ $this->assertSame("devices_NULL__239.255.255.250", $this->getCacheKey());
+ }
+
+ protected function getCacheKey()
+ {
+ $class = new \ReflectionClass($this->network);
+ $method = $class->getMethod("getCacheKey");
+ $method->setAccessible(true);
+ return $method->invoke($this->network);
+ }
+
+ public function testSetMulticastAddress()
+ {
+ $this->network->setMulticastAddress("127.0.0.1");
+ $this->assertSame("devices_NULL__127.0.0.1", $this->getCacheKey());
+ }
+
+ public function testGetNetworkInterface()
+ {
+ $this->assertNull($this->network->getNetworkInterface());
+ }
+
+ public function testSetNetworkInterfaceString()
+ {
+ $this->network->setNetworkInterface("eth0");
+ $this->assertSame("eth0", $this->network->getNetworkInterface());
+ $this->assertSame("devices_string_eth0_239.255.255.250", $this->getCacheKey());
+ }
+
+ public function testSetNetworkInterfaceInteger()
+ {
+ $this->network->setNetworkInterface(0);
+ $this->assertSame(0, $this->network->getNetworkInterface());
+ $this->assertSame("devices_integer_0_239.255.255.250", $this->getCacheKey());
+ }
+
+ public function testSetNetworkInterfaceEmptyString()
+ {
+ $this->network->setNetworkInterface("");
+ $this->assertSame("", $this->network->getNetworkInterface());
+ $this->assertSame("devices_string__239.255.255.250", $this->getCacheKey());
+ }
+}
diff --git a/tests/SpeakerTest.php b/tests/SpeakerTest.php
new file mode 100755
index 0000000..72c7a45
--- /dev/null
+++ b/tests/SpeakerTest.php
@@ -0,0 +1,81 @@
+speaker = $this->getSpeaker();
+ }
+
+ public function tearDown()
+ {
+ Mockery::close();
+ }
+
+ public function testGetModel()
+ {
+ self::assertEquals("RX-V481D", $this->speaker->getModel());
+ }
+
+ public function testGetName()
+ {
+ self::assertEquals("BedRoom", $this->speaker->getName());
+ }
+
+ public function testGetGroup()
+ {
+ self::assertEquals(Speaker::NO_GROUP, $this->speaker->getGroup());
+ }
+
+ public function testIsCoordinator()
+ {
+ self::assertTrue($this->speaker->isCoordinator());
+ }
+
+ public function testGetUuid()
+ {
+ self::assertEquals("ABCDEEFAA063", $this->speaker->getUuid());
+ }
+
+ public function testGetVolume()
+ {
+ self::assertEquals("77", $this->speaker->getVolume());
+ }
+
+ public function isMuted()
+ {
+ self::assertFalse($this->speaker->isMuted());
+ }
+
+ public function testInput()
+ {
+ self::assertEquals("tuner", $this->speaker->getInput());
+ }
+
+ public function testIsPowerOn()
+ {
+ self::assertFalse($this->speaker->isPowerOn());
+ }
+
+ public function testPowerOn()
+ {
+ self::assertNotNull($this->speaker->powerOn());
+ }
+}
diff --git a/tests/assets/dist/getDistributionInfo.json b/tests/assets/dist/getDistributionInfo.json
new file mode 100755
index 0000000..a5011c5
--- /dev/null
+++ b/tests/assets/dist/getDistributionInfo.json
@@ -0,0 +1,8 @@
+{
+ "response_code": 0,
+ "group_id": "00000000000000000000000000000000",
+ "group_name": "(Linked) BedRoom",
+ "role": "none",
+ "server_zone": "main",
+ "client_list": []
+}
\ No newline at end of file
diff --git a/tests/assets/dist/setClientInfo.json b/tests/assets/dist/setClientInfo.json
new file mode 100755
index 0000000..bf326b8
--- /dev/null
+++ b/tests/assets/dist/setClientInfo.json
@@ -0,0 +1,3 @@
+{
+ "response_code": 0
+}
\ No newline at end of file
diff --git a/tests/assets/dist/setGroupName.json b/tests/assets/dist/setGroupName.json
new file mode 100755
index 0000000..bf326b8
--- /dev/null
+++ b/tests/assets/dist/setGroupName.json
@@ -0,0 +1,3 @@
+{
+ "response_code": 0
+}
\ No newline at end of file
diff --git a/tests/assets/dist/setServerInfo.json b/tests/assets/dist/setServerInfo.json
new file mode 100755
index 0000000..bf326b8
--- /dev/null
+++ b/tests/assets/dist/setServerInfo.json
@@ -0,0 +1,3 @@
+{
+ "response_code": 0
+}
\ No newline at end of file
diff --git a/tests/assets/dist/startDistribution.json b/tests/assets/dist/startDistribution.json
new file mode 100755
index 0000000..bf326b8
--- /dev/null
+++ b/tests/assets/dist/startDistribution.json
@@ -0,0 +1,3 @@
+{
+ "response_code": 0
+}
\ No newline at end of file
diff --git a/tests/assets/netusb/getAccountStatus.json b/tests/assets/netusb/getAccountStatus.json
new file mode 100755
index 0000000..ec80ace
--- /dev/null
+++ b/tests/assets/netusb/getAccountStatus.json
@@ -0,0 +1,45 @@
+{
+ "response_code": 0,
+ "service_list": [
+ {
+ "id": "napster",
+ "registered": false,
+ "login_status": "logged_out",
+ "username": "",
+ "type": "formal",
+ "trial_time_left": 0
+ },
+ {
+ "id": "juke",
+ "registered": false,
+ "login_status": "logged_out",
+ "username": "",
+ "type": "formal",
+ "trial_time_left": 0
+ },
+ {
+ "id": "qobuz",
+ "registered": false,
+ "login_status": "logged_out",
+ "username": "",
+ "type": "formal",
+ "trial_time_left": 0
+ },
+ {
+ "id": "tidal",
+ "registered": false,
+ "login_status": "logged_out",
+ "username": "",
+ "type": "formal",
+ "trial_time_left": 0
+ },
+ {
+ "id": "deezer",
+ "registered": true,
+ "login_status": "logged_in",
+ "username": "",
+ "type": "formal",
+ "trial_time_left": 0
+ }
+ ]
+}
\ No newline at end of file
diff --git a/tests/assets/netusb/getListInfo.json b/tests/assets/netusb/getListInfo.json
new file mode 100755
index 0000000..625a7f2
--- /dev/null
+++ b/tests/assets/netusb/getListInfo.json
@@ -0,0 +1,23 @@
+{
+ "response_code": 0,
+ "input": "server",
+ "menu_layer": 0,
+ "max_line": 2,
+ "index": 0,
+ "playing_index": -1,
+ "menu_name": "SERVER",
+ "list_info": [
+ {
+ "text": "DiskStation",
+ "subtexts": [],
+ "thumbnail": "",
+ "attribute": 2
+ },
+ {
+ "text": "Kodi (Kodi)",
+ "subtexts": [],
+ "thumbnail": "",
+ "attribute": 2
+ }
+ ]
+}
\ No newline at end of file
diff --git a/tests/assets/netusb/getMcPlaylist.json b/tests/assets/netusb/getMcPlaylist.json
new file mode 100755
index 0000000..44725e4
--- /dev/null
+++ b/tests/assets/netusb/getMcPlaylist.json
@@ -0,0 +1,32 @@
+{
+ "response_code": 0,
+ "max_line": 4,
+ "bank": 1,
+ "index": 0,
+ "track_info": [
+ {
+ "input": "server",
+ "text": "Fernando",
+ "thumbnail": "http:\/\/192.168.86.4:50002\/transcoder\/jpegtnscaler.cgi\/ebdart\/73993.jpg",
+ "attribute": 63
+ },
+ {
+ "input": "server",
+ "text": "Jailbreak",
+ "thumbnail": "http:\/\/192.168.86.4:50002\/transcoder\/jpegtnscaler.cgi\/ebdart\/81777.jpg",
+ "attribute": 63
+ },
+ {
+ "input": "server",
+ "text": "Jailbreak",
+ "thumbnail": "http:\/\/192.168.86.4:50002\/transcoder\/jpegtnscaler.cgi\/ebdart\/81777.jpg",
+ "attribute": 63
+ },
+ {
+ "input": "server",
+ "text": "Stairway To Heaven",
+ "thumbnail": "http:\/\/192.168.86.4:50002\/transcoder\/jpegtnscaler.cgi\/ebdart\/76674.jpg",
+ "attribute": 63
+ }
+ ]
+}
\ No newline at end of file
diff --git a/tests/assets/netusb/getMcPlaylistName.json b/tests/assets/netusb/getMcPlaylistName.json
new file mode 100755
index 0000000..63c2cf3
--- /dev/null
+++ b/tests/assets/netusb/getMcPlaylistName.json
@@ -0,0 +1,10 @@
+{
+ "response_code": 0,
+ "name_list": [
+ "WakeUp",
+ "Rock",
+ "Lounge",
+ "John",
+ "Jane"
+ ]
+}
\ No newline at end of file
diff --git a/tests/assets/netusb/getPlayInfo.json b/tests/assets/netusb/getPlayInfo.json
new file mode 100755
index 0000000..b3fd14d
--- /dev/null
+++ b/tests/assets/netusb/getPlayInfo.json
@@ -0,0 +1,27 @@
+{
+ "response_code": 0,
+ "input": "server",
+ "play_queue_type": "user",
+ "playback": "play",
+ "repeat": "off",
+ "shuffle": "off",
+ "play_time": 190,
+ "total_time": 0,
+ "artist": "ABBA",
+ "album": "Gold: Greatest Hits",
+ "track": "Voulez-Vous",
+ "albumart_url": "/YamahaRemoteControl/AlbumART/AlbumART1115.jpg",
+ "albumart_id": 1115,
+ "usb_devicetype": "unknown",
+ "auto_stopped": false,
+ "attribute": 83886591,
+ "repeat_available": [
+ "off",
+ "one",
+ "all"
+ ],
+ "shuffle_available": [
+ "off",
+ "on"
+ ]
+}
\ No newline at end of file
diff --git a/tests/assets/netusb/getPlayQueue.json b/tests/assets/netusb/getPlayQueue.json
new file mode 100755
index 0000000..14f1fcc
--- /dev/null
+++ b/tests/assets/netusb/getPlayQueue.json
@@ -0,0 +1,33 @@
+{
+ "response_code": 0,
+ "type": "user",
+ "max_line": 4,
+ "playing_index": 0,
+ "index": 0,
+ "track_info": [
+ {
+ "input": "server",
+ "text": "Fernando",
+ "thumbnail": "http:\/\/192.168.86.4:50002\/transcoder\/jpegtnscaler.cgi\/ebdart\/73993.jpg",
+ "attribute": 49
+ },
+ {
+ "input": "server",
+ "text": "Jailbreak",
+ "thumbnail": "http:\/\/192.168.86.4:50002\/transcoder\/jpegtnscaler.cgi\/ebdart\/81777.jpg",
+ "attribute": 49
+ },
+ {
+ "input": "server",
+ "text": "Jailbreak",
+ "thumbnail": "http:\/\/192.168.86.4:50002\/transcoder\/jpegtnscaler.cgi\/ebdart\/81777.jpg",
+ "attribute": 49
+ },
+ {
+ "input": "server",
+ "text": "Stairway To Heaven",
+ "thumbnail": "http:\/\/192.168.86.4:50002\/transcoder\/jpegtnscaler.cgi\/ebdart\/76674.jpg",
+ "attribute": 49
+ }
+ ]
+}
\ No newline at end of file
diff --git a/tests/assets/netusb/getPresetInfo.json b/tests/assets/netusb/getPresetInfo.json
new file mode 100755
index 0000000..d230a51
--- /dev/null
+++ b/tests/assets/netusb/getPresetInfo.json
@@ -0,0 +1,171 @@
+{
+ "response_code": 0,
+ "preset_info": [
+ {
+ "input": "net_radio",
+ "text": "OÜI FM Classic Rock",
+ "attribute": 0
+ },
+ {
+ "input": "net_radio",
+ "text": "4U Funky Classics",
+ "attribute": 0
+ },
+ {
+ "input": "unknown",
+ "text": ""
+ },
+ {
+ "input": "unknown",
+ "text": ""
+ },
+ {
+ "input": "unknown",
+ "text": ""
+ },
+ {
+ "input": "unknown",
+ "text": ""
+ },
+ {
+ "input": "unknown",
+ "text": ""
+ },
+ {
+ "input": "unknown",
+ "text": ""
+ },
+ {
+ "input": "unknown",
+ "text": ""
+ },
+ {
+ "input": "unknown",
+ "text": ""
+ },
+ {
+ "input": "unknown",
+ "text": ""
+ },
+ {
+ "input": "unknown",
+ "text": ""
+ },
+ {
+ "input": "unknown",
+ "text": ""
+ },
+ {
+ "input": "unknown",
+ "text": ""
+ },
+ {
+ "input": "unknown",
+ "text": ""
+ },
+ {
+ "input": "unknown",
+ "text": ""
+ },
+ {
+ "input": "unknown",
+ "text": ""
+ },
+ {
+ "input": "unknown",
+ "text": ""
+ },
+ {
+ "input": "unknown",
+ "text": ""
+ },
+ {
+ "input": "unknown",
+ "text": ""
+ },
+ {
+ "input": "unknown",
+ "text": ""
+ },
+ {
+ "input": "unknown",
+ "text": ""
+ },
+ {
+ "input": "unknown",
+ "text": ""
+ },
+ {
+ "input": "unknown",
+ "text": ""
+ },
+ {
+ "input": "unknown",
+ "text": ""
+ },
+ {
+ "input": "unknown",
+ "text": ""
+ },
+ {
+ "input": "unknown",
+ "text": ""
+ },
+ {
+ "input": "unknown",
+ "text": ""
+ },
+ {
+ "input": "unknown",
+ "text": ""
+ },
+ {
+ "input": "unknown",
+ "text": ""
+ },
+ {
+ "input": "unknown",
+ "text": ""
+ },
+ {
+ "input": "unknown",
+ "text": ""
+ },
+ {
+ "input": "unknown",
+ "text": ""
+ },
+ {
+ "input": "unknown",
+ "text": ""
+ },
+ {
+ "input": "unknown",
+ "text": ""
+ },
+ {
+ "input": "unknown",
+ "text": ""
+ },
+ {
+ "input": "unknown",
+ "text": ""
+ },
+ {
+ "input": "unknown",
+ "text": ""
+ },
+ {
+ "input": "unknown",
+ "text": ""
+ },
+ {
+ "input": "unknown",
+ "text": ""
+ }
+ ],
+ "func_list": [
+ "clear",
+ "move"
+ ]
+}
\ No newline at end of file
diff --git a/tests/assets/netusb/getRecentInfo.json b/tests/assets/netusb/getRecentInfo.json
new file mode 100755
index 0000000..fda1bf5
--- /dev/null
+++ b/tests/assets/netusb/getRecentInfo.json
@@ -0,0 +1,285 @@
+{
+ "response_code": 0,
+ "recent_info": [
+ {
+ "input": "server",
+ "text": "Fernando",
+ "albumart_url": "http:\/\/192.168.86.4:50002\/transcoder\/jpegtnscaler.cgi\/ebdart\/73993.jpg",
+ "play_count": 22,
+ "attribute": 30
+ },
+ {
+ "input": "net_radio",
+ "text": "001A_A Funk",
+ "albumart_url": "http:\/\/item.radio456.com:80\/007452\/logo\/logo-75542.jpg",
+ "play_count": 1,
+ "attribute": 0
+ },
+ {
+ "input": "net_radio",
+ "text": "4U Classic Rock",
+ "albumart_url": "http:\/\/item.radio456.com:80\/007452\/logo\/logo-19467.jpg",
+ "play_count": 2,
+ "attribute": 0
+ },
+ {
+ "input": "server",
+ "text": "Stairway To Heaven",
+ "albumart_url": "http:\/\/192.168.86.4:50002\/transcoder\/jpegtnscaler.cgi\/ebdart\/76674.jpg",
+ "play_count": 6,
+ "attribute": 30
+ },
+ {
+ "input": "server",
+ "text": "Jailbreak",
+ "albumart_url": "http:\/\/192.168.86.4:50002\/transcoder\/jpegtnscaler.cgi\/ebdart\/81777.jpg",
+ "play_count": 21,
+ "attribute": 30
+ },
+ {
+ "input": "net_radio",
+ "text": "4U Funky Classics",
+ "albumart_url": "http:\/\/item.radio456.com:80\/007452\/logo\/logo-21350.jpg",
+ "play_count": 2,
+ "attribute": 0
+ },
+ {
+ "input": "server",
+ "text": "What's Next To The Moon",
+ "albumart_url": "http:\/\/192.168.86.4:50002\/transcoder\/jpegtnscaler.cgi\/ebdart\/81783.jpg",
+ "play_count": 1,
+ "attribute": 30
+ },
+ {
+ "input": "server",
+ "text": "Sin City",
+ "albumart_url": "http:\/\/192.168.86.4:50002\/transcoder\/jpegtnscaler.cgi\/ebdart\/81788.jpg",
+ "play_count": 1,
+ "attribute": 30
+ },
+ {
+ "input": "server",
+ "text": "Riff Raff",
+ "albumart_url": "http:\/\/192.168.86.4:50002\/transcoder\/jpegtnscaler.cgi\/ebdart\/81789.jpg",
+ "play_count": 1,
+ "attribute": 30
+ },
+ {
+ "input": "server",
+ "text": "Gimme A Bullet",
+ "albumart_url": "http:\/\/192.168.86.4:50002\/transcoder\/jpegtnscaler.cgi\/ebdart\/81785.jpg",
+ "play_count": 1,
+ "attribute": 30
+ },
+ {
+ "input": "server",
+ "text": "Down Payment Blues",
+ "albumart_url": "http:\/\/192.168.86.4:50002\/transcoder\/jpegtnscaler.cgi\/ebdart\/81790.jpg",
+ "play_count": 1,
+ "attribute": 30
+ },
+ {
+ "input": "server",
+ "text": "Rock N Roll Damnation",
+ "albumart_url": "http:\/\/192.168.86.4:50002\/transcoder\/jpegtnscaler.cgi\/ebdart\/81787.jpg",
+ "play_count": 1,
+ "attribute": 30
+ },
+ {
+ "input": "server",
+ "text": "Rock n Roll Singer",
+ "albumart_url": "http:\/\/192.168.86.4:50002\/transcoder\/jpegtnscaler.cgi\/ebdart\/81842.jpg",
+ "play_count": 1,
+ "attribute": 30
+ },
+ {
+ "input": "server",
+ "text": "It's A Long Way To The Top (If You Wanna Rock nRoll)",
+ "albumart_url": "http:\/\/192.168.86.4:50002\/transcoder\/jpegtnscaler.cgi\/ebdart\/81838.jpg",
+ "play_count": 1,
+ "attribute": 30
+ },
+ {
+ "input": "server",
+ "text": "Moby Dick",
+ "albumart_url": "http:\/\/192.168.86.4:50002\/transcoder\/jpegtnscaler.cgi\/ebdart\/76670.jpg",
+ "play_count": 1,
+ "attribute": 30
+ },
+ {
+ "input": "server",
+ "text": "Stairway To Heaven",
+ "albumart_url": "http:\/\/192.168.86.4:50002\/transcoder\/jpegtnscaler.cgi\/ebdart\/76633.jpg",
+ "play_count": 1,
+ "attribute": 30
+ },
+ {
+ "input": "net_radio",
+ "text": "Allzic Radio Blues",
+ "albumart_url": "http:\/\/item.radio456.com:80\/007452\/logo\/logo-74510.jpg",
+ "play_count": 1,
+ "attribute": 0
+ },
+ {
+ "input": "deezer",
+ "text": "Brothers In Arms",
+ "albumart_url": "https:\/\/e-cdns-images.dzcdn.net:443\/images\/cover\/c5478962996d285c9e0ec4860e061350\/500x500-000000-80-0-0.jpg",
+ "play_count": 1,
+ "attribute": 0
+ },
+ {
+ "input": "deezer",
+ "text": "Killing In The Name (Remastered)",
+ "albumart_url": "https:\/\/e-cdns-images.dzcdn.net:443\/images\/cover\/8d836096d689280eef281d1eb0eb4fc5\/500x500-000000-80-0-0.jpg",
+ "play_count": 1,
+ "attribute": 0
+ },
+ {
+ "input": "deezer",
+ "text": "Walk on the Wild Side (Remastered)",
+ "albumart_url": "https:\/\/e-cdns-images.dzcdn.net:443\/images\/cover\/7ba918c690c408d13a6a93a3a9d59867\/500x500-000000-80-0-0.jpg",
+ "play_count": 1,
+ "attribute": 0
+ },
+ {
+ "input": "deezer",
+ "text": "Imagine (2010 - Remaster)",
+ "albumart_url": "https:\/\/e-cdns-images.dzcdn.net:443\/images\/cover\/78c41058a6e7e1546beb7216ec6eddca\/500x500-000000-80-0-0.jpg",
+ "play_count": 1,
+ "attribute": 0
+ },
+ {
+ "input": "deezer",
+ "text": "You Really Got Me (Remastered)",
+ "albumart_url": "https:\/\/e-cdns-images.dzcdn.net:443\/images\/cover\/b1730707727bf955d951f329bbfcb7ae\/500x500-000000-80-0-0.jpg",
+ "play_count": 1,
+ "attribute": 0
+ },
+ {
+ "input": "deezer",
+ "text": "Sweet Child O' Mine",
+ "albumart_url": "https:\/\/e-cdns-images.dzcdn.net:443\/images\/cover\/9087b7676d5f37612ee954d4ffc6f082\/500x500-000000-80-0-0.jpg",
+ "play_count": 1,
+ "attribute": 0
+ },
+ {
+ "input": "deezer",
+ "text": "Killing In The Name (Remastered)",
+ "albumart_url": "https:\/\/e-cdns-images.dzcdn.net:443\/images\/cover\/8d836096d689280eef281d1eb0eb4fc5\/500x500-000000-80-0-0.jpg",
+ "play_count": 1,
+ "attribute": 0
+ },
+ {
+ "input": "deezer",
+ "text": "Black Hole Sun",
+ "albumart_url": "https:\/\/e-cdns-images.dzcdn.net:443\/images\/cover\/94248b664be0890be31cabf62a068293\/500x500-000000-80-0-0.jpg",
+ "play_count": 1,
+ "attribute": 0
+ },
+ {
+ "input": "deezer",
+ "text": "Everlong",
+ "albumart_url": "https:\/\/e-cdns-images.dzcdn.net:443\/images\/cover\/f737d214917c549e651acc4aca11f5b8\/500x500-000000-80-0-0.jpg",
+ "play_count": 1,
+ "attribute": 0
+ },
+ {
+ "input": "deezer",
+ "text": "Come As You Are",
+ "albumart_url": "https:\/\/e-cdns-images.dzcdn.net:443\/images\/cover\/d6feb0afde4aa63160de2d6e8d55160d\/500x500-000000-80-0-0.jpg",
+ "play_count": 1,
+ "attribute": 0
+ },
+ {
+ "input": "deezer",
+ "text": "Sunday Bloody Sunday",
+ "albumart_url": "https:\/\/e-cdns-images.dzcdn.net:443\/images\/cover\/76c4322b0ea4ab5ed1e3a9f7d3208e28\/500x500-000000-80-0-0.jpg",
+ "play_count": 1,
+ "attribute": 0
+ },
+ {
+ "input": "deezer",
+ "text": "Killing In The Name (Remastered)",
+ "albumart_url": "https:\/\/e-cdns-images.dzcdn.net:443\/images\/cover\/8d836096d689280eef281d1eb0eb4fc5\/500x500-000000-80-0-0.jpg",
+ "play_count": 2,
+ "attribute": 0
+ },
+ {
+ "input": "deezer",
+ "text": "Sunday Bloody Sunday",
+ "albumart_url": "https:\/\/e-cdns-images.dzcdn.net:443\/images\/cover\/76c4322b0ea4ab5ed1e3a9f7d3208e28\/500x500-000000-80-0-0.jpg",
+ "play_count": 2,
+ "attribute": 0
+ },
+ {
+ "input": "deezer",
+ "text": "Come As You Are",
+ "albumart_url": "https:\/\/e-cdns-images.dzcdn.net:443\/images\/cover\/d6feb0afde4aa63160de2d6e8d55160d\/500x500-000000-80-0-0.jpg",
+ "play_count": 3,
+ "attribute": 0
+ },
+ {
+ "input": "deezer",
+ "text": "Everlong",
+ "albumart_url": "https:\/\/e-cdns-images.dzcdn.net:443\/images\/cover\/f737d214917c549e651acc4aca11f5b8\/500x500-000000-80-0-0.jpg",
+ "play_count": 3,
+ "attribute": 0
+ },
+ {
+ "input": "deezer",
+ "text": "Black Hole Sun",
+ "albumart_url": "https:\/\/e-cdns-images.dzcdn.net:443\/images\/cover\/94248b664be0890be31cabf62a068293\/500x500-000000-80-0-0.jpg",
+ "play_count": 4,
+ "attribute": 0
+ },
+ {
+ "input": "deezer",
+ "text": "Killing In The Name (Remastered)",
+ "albumart_url": "https:\/\/e-cdns-images.dzcdn.net:443\/images\/cover\/8d836096d689280eef281d1eb0eb4fc5\/500x500-000000-80-0-0.jpg",
+ "play_count": 5,
+ "attribute": 0
+ },
+ {
+ "input": "deezer",
+ "text": "Sweet Child O' Mine",
+ "albumart_url": "https:\/\/e-cdns-images.dzcdn.net:443\/images\/cover\/9087b7676d5f37612ee954d4ffc6f082\/500x500-000000-80-0-0.jpg",
+ "play_count": 5,
+ "attribute": 0
+ },
+ {
+ "input": "deezer",
+ "text": "You Really Got Me (Remastered)",
+ "albumart_url": "https:\/\/e-cdns-images.dzcdn.net:443\/images\/cover\/b1730707727bf955d951f329bbfcb7ae\/500x500-000000-80-0-0.jpg",
+ "play_count": 6,
+ "attribute": 0
+ },
+ {
+ "input": "deezer",
+ "text": "Imagine (2010 - Remaster)",
+ "albumart_url": "https:\/\/e-cdns-images.dzcdn.net:443\/images\/cover\/78c41058a6e7e1546beb7216ec6eddca\/500x500-000000-80-0-0.jpg",
+ "play_count": 5,
+ "attribute": 0
+ },
+ {
+ "input": "deezer",
+ "text": "Walk on the Wild Side (Remastered)",
+ "albumart_url": "https:\/\/e-cdns-images.dzcdn.net:443\/images\/cover\/7ba918c690c408d13a6a93a3a9d59867\/500x500-000000-80-0-0.jpg",
+ "play_count": 2,
+ "attribute": 0
+ },
+ {
+ "input": "deezer",
+ "text": "Walk on the Wild Side (Remastered)",
+ "albumart_url": "https:\/\/e-cdns-images.dzcdn.net:443\/images\/cover\/7ba918c690c408d13a6a93a3a9d59867\/500x500-000000-80-0-0.jpg",
+ "play_count": 1,
+ "attribute": 0
+ },
+ {
+ "input": "net_radio",
+ "text": "Allzic Radio Jazz Lounge",
+ "albumart_url": "http:\/\/item.radio456.com:80\/007452\/logo\/logo-74488.jpg",
+ "play_count": 19,
+ "attribute": 0
+ }
+ ]
+}
\ No newline at end of file
diff --git a/tests/assets/netusb/manageMcPlaylist.json b/tests/assets/netusb/manageMcPlaylist.json
new file mode 100755
index 0000000..bf326b8
--- /dev/null
+++ b/tests/assets/netusb/manageMcPlaylist.json
@@ -0,0 +1,3 @@
+{
+ "response_code": 0
+}
\ No newline at end of file
diff --git a/tests/assets/netusb/recallPreset.json b/tests/assets/netusb/recallPreset.json
new file mode 100755
index 0000000..bf326b8
--- /dev/null
+++ b/tests/assets/netusb/recallPreset.json
@@ -0,0 +1,3 @@
+{
+ "response_code": 0
+}
\ No newline at end of file
diff --git a/tests/assets/netusb/setPlayback.json b/tests/assets/netusb/setPlayback.json
new file mode 100755
index 0000000..bf326b8
--- /dev/null
+++ b/tests/assets/netusb/setPlayback.json
@@ -0,0 +1,3 @@
+{
+ "response_code": 0
+}
\ No newline at end of file
diff --git a/tests/assets/netusb/storePreset.json b/tests/assets/netusb/storePreset.json
new file mode 100755
index 0000000..bf326b8
--- /dev/null
+++ b/tests/assets/netusb/storePreset.json
@@ -0,0 +1,3 @@
+{
+ "response_code": 0
+}
\ No newline at end of file
diff --git a/tests/assets/netusb/toggleRepeat.json b/tests/assets/netusb/toggleRepeat.json
new file mode 100755
index 0000000..bf326b8
--- /dev/null
+++ b/tests/assets/netusb/toggleRepeat.json
@@ -0,0 +1,3 @@
+{
+ "response_code": 0
+}
\ No newline at end of file
diff --git a/tests/assets/netusb/toggleShuffle.json b/tests/assets/netusb/toggleShuffle.json
new file mode 100755
index 0000000..bf326b8
--- /dev/null
+++ b/tests/assets/netusb/toggleShuffle.json
@@ -0,0 +1,3 @@
+{
+ "response_code": 0
+}
\ No newline at end of file
diff --git a/tests/assets/system/getDeviceInfo.json b/tests/assets/system/getDeviceInfo.json
new file mode 100755
index 0000000..44ac9c1
--- /dev/null
+++ b/tests/assets/system/getDeviceInfo.json
@@ -0,0 +1,13 @@
+{
+ "response_code": 0,
+ "model_name": "RX-V481D",
+ "destination": "BG",
+ "device_id": "ABCDEEFAA063",
+ "system_id": "ABCC5863",
+ "system_version": 1.22,
+ "api_version": 1.17,
+ "netmodule_version": "1145 ",
+ "netmodule_checksum": "86FB2237",
+ "operation_mode": "normal",
+ "update_error_code": "FFFFFFFF"
+}
\ No newline at end of file
diff --git a/tests/assets/system/getDisklavierSettings.json b/tests/assets/system/getDisklavierSettings.json
new file mode 100755
index 0000000..2f4d794
--- /dev/null
+++ b/tests/assets/system/getDisklavierSettings.json
@@ -0,0 +1,12 @@
+{
+ "response_code": 0,
+ "enable": false,
+ "mode": "output_primary",
+ "input": "unknown",
+ "pair": {
+ "mac_address": "000000000000"
+ },
+ "disklavier": {
+ "ip_address": "0.0.0.0"
+ }
+}
\ No newline at end of file
diff --git a/tests/assets/system/getFeatures.json b/tests/assets/system/getFeatures.json
new file mode 100755
index 0000000..e629385
--- /dev/null
+++ b/tests/assets/system/getFeatures.json
@@ -0,0 +1,347 @@
+{
+ "response_code": 0,
+ "system": {
+ "func_list": [
+ "wired_lan",
+ "wireless_lan",
+ "wireless_direct",
+ "network_standby",
+ "network_standby_auto",
+ "bluetooth_standby",
+ "bluetooth_tx_setting",
+ "zone_b_volume_sync",
+ "hdmi_out_1",
+ "airplay",
+ "disklavier_settings"
+ ],
+ "zone_num": 2,
+ "input_list": [
+ {
+ "id": "spotify",
+ "distribution_enable": true,
+ "rename_enable": false,
+ "account_enable": false,
+ "play_info_type": "netusb"
+ },
+ {
+ "id": "juke",
+ "distribution_enable": true,
+ "rename_enable": false,
+ "account_enable": true,
+ "play_info_type": "netusb"
+ },
+ {
+ "id": "qobuz",
+ "distribution_enable": true,
+ "rename_enable": false,
+ "account_enable": true,
+ "play_info_type": "netusb"
+ },
+ {
+ "id": "airplay",
+ "distribution_enable": false,
+ "rename_enable": false,
+ "account_enable": false,
+ "play_info_type": "netusb"
+ },
+ {
+ "id": "mc_link",
+ "distribution_enable": false,
+ "rename_enable": true,
+ "account_enable": false,
+ "play_info_type": "netusb"
+ },
+ {
+ "id": "server",
+ "distribution_enable": true,
+ "rename_enable": true,
+ "account_enable": false,
+ "play_info_type": "netusb"
+ },
+ {
+ "id": "net_radio",
+ "distribution_enable": true,
+ "rename_enable": true,
+ "account_enable": false,
+ "play_info_type": "netusb"
+ },
+ {
+ "id": "bluetooth",
+ "distribution_enable": true,
+ "rename_enable": false,
+ "account_enable": false,
+ "play_info_type": "netusb"
+ },
+ {
+ "id": "usb",
+ "distribution_enable": true,
+ "rename_enable": true,
+ "account_enable": false,
+ "play_info_type": "netusb"
+ },
+ {
+ "id": "tuner",
+ "distribution_enable": true,
+ "rename_enable": true,
+ "account_enable": false,
+ "play_info_type": "tuner"
+ },
+ {
+ "id": "hdmi1",
+ "distribution_enable": true,
+ "rename_enable": true,
+ "account_enable": false,
+ "play_info_type": "none"
+ },
+ {
+ "id": "hdmi2",
+ "distribution_enable": true,
+ "rename_enable": true,
+ "account_enable": false,
+ "play_info_type": "none"
+ },
+ {
+ "id": "hdmi3",
+ "distribution_enable": true,
+ "rename_enable": true,
+ "account_enable": false,
+ "play_info_type": "none"
+ },
+ {
+ "id": "hdmi4",
+ "distribution_enable": true,
+ "rename_enable": true,
+ "account_enable": false,
+ "play_info_type": "none"
+ },
+ {
+ "id": "av1",
+ "distribution_enable": true,
+ "rename_enable": true,
+ "account_enable": false,
+ "play_info_type": "none"
+ },
+ {
+ "id": "av2",
+ "distribution_enable": true,
+ "rename_enable": true,
+ "account_enable": false,
+ "play_info_type": "none"
+ },
+ {
+ "id": "av3",
+ "distribution_enable": true,
+ "rename_enable": true,
+ "account_enable": false,
+ "play_info_type": "none"
+ },
+ {
+ "id": "av4",
+ "distribution_enable": true,
+ "rename_enable": true,
+ "account_enable": false,
+ "play_info_type": "none"
+ },
+ {
+ "id": "audio1",
+ "distribution_enable": true,
+ "rename_enable": true,
+ "account_enable": false,
+ "play_info_type": "none"
+ },
+ {
+ "id": "audio2",
+ "distribution_enable": true,
+ "rename_enable": true,
+ "account_enable": false,
+ "play_info_type": "none"
+ },
+ {
+ "id": "aux",
+ "distribution_enable": true,
+ "rename_enable": true,
+ "account_enable": false,
+ "play_info_type": "none"
+ }
+ ],
+ "ymap_list": [
+ "vtuner"
+ ]
+ },
+ "zone": [
+ {
+ "id": "main",
+ "func_list": [
+ "power",
+ "sleep",
+ "volume",
+ "mute",
+ "sound_program",
+ "direct",
+ "enhancer",
+ "tone_control",
+ "signal_info",
+ "prepare_input_change",
+ "link_control",
+ "link_audio_delay"
+ ],
+ "input_list": [
+ "spotify",
+ "juke",
+ "qobuz",
+ "airplay",
+ "mc_link",
+ "server",
+ "net_radio",
+ "bluetooth",
+ "usb",
+ "tuner",
+ "hdmi1",
+ "hdmi2",
+ "hdmi3",
+ "hdmi4",
+ "av1",
+ "av2",
+ "av3",
+ "av4",
+ "audio1",
+ "audio2",
+ "aux"
+ ],
+ "sound_program_list": [
+ "munich",
+ "vienna",
+ "chamber",
+ "cellar_club",
+ "roxy_theatre",
+ "bottom_line",
+ "sports",
+ "action_game",
+ "roleplaying_game",
+ "music_video",
+ "standard",
+ "spectacle",
+ "sci-fi",
+ "adventure",
+ "drama",
+ "mono_movie",
+ "2ch_stereo",
+ "5ch_stereo",
+ "surr_decoder",
+ "straight"
+ ],
+ "tone_control_mode_list": [
+ "manual"
+ ],
+ "link_control_list": [
+ "speed",
+ "standard",
+ "stability"
+ ],
+ "link_audio_delay_list": [
+ "audio_sync",
+ "lip_sync"
+ ],
+ "range_step": [
+ {
+ "id": "volume",
+ "min": 0,
+ "max": 161,
+ "step": 1
+ },
+ {
+ "id": "tone_control",
+ "min": -12,
+ "max": 12,
+ "step": 1
+ }
+ ]
+ },
+ {
+ "id": "zone2",
+ "zone_b": true,
+ "func_list": [
+ "power",
+ "volume",
+ "mute",
+ "prepare_input_change"
+ ],
+ "input_list": [
+ "spotify",
+ "juke",
+ "qobuz",
+ "airplay",
+ "mc_link",
+ "server",
+ "net_radio",
+ "bluetooth",
+ "usb",
+ "tuner",
+ "hdmi1",
+ "hdmi2",
+ "hdmi3",
+ "hdmi4",
+ "av1",
+ "av2",
+ "av3",
+ "av4",
+ "audio1",
+ "audio2",
+ "aux"
+ ],
+ "range_step": [
+ {
+ "id": "volume",
+ "min": 0,
+ "max": 161,
+ "step": 1
+ }
+ ]
+ }
+ ],
+ "tuner": {
+ "func_list": [
+ "fm",
+ "rds",
+ "dab"
+ ],
+ "range_step": [
+ {
+ "id": "fm",
+ "min": 87500,
+ "max": 108000,
+ "step": 50
+ }
+ ],
+ "preset": {
+ "type": "separate",
+ "num": 40
+ }
+ },
+ "netusb": {
+ "func_list": [
+ "recent_info",
+ "play_queue",
+ "mc_playlist"
+ ],
+ "preset": {
+ "num": 40
+ },
+ "recent_info": {
+ "num": 40
+ },
+ "play_queue": {
+ "size": 200
+ },
+ "mc_playlist": {
+ "size": 200,
+ "num": 5
+ },
+ "vtuner_fver": "A"
+ },
+ "distribution": {
+ "server_zone_list": [
+ "main"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/tests/assets/system/getFuncStatus.json b/tests/assets/system/getFuncStatus.json
new file mode 100755
index 0000000..e9e2be7
--- /dev/null
+++ b/tests/assets/system/getFuncStatus.json
@@ -0,0 +1,5 @@
+{
+ "response_code": 0,
+ "zone_b_volume_sync": true,
+ "hdmi_out_1": true
+}
\ No newline at end of file
diff --git a/tests/assets/system/getLocationInfo.json b/tests/assets/system/getLocationInfo.json
new file mode 100755
index 0000000..53e95b9
--- /dev/null
+++ b/tests/assets/system/getLocationInfo.json
@@ -0,0 +1,9 @@
+{
+ "response_code": 0,
+ "id": "a9957ae56c9f48d4a547c49029ae04f9",
+ "name": "Home",
+ "zone_list": {
+ "main": true,
+ "zone2": false
+ }
+}
\ No newline at end of file
diff --git a/tests/assets/system/getMusicCastTreeInfo.json b/tests/assets/system/getMusicCastTreeInfo.json
new file mode 100755
index 0000000..4cce1b8
--- /dev/null
+++ b/tests/assets/system/getMusicCastTreeInfo.json
@@ -0,0 +1,50 @@
+{
+ "response_code": 0,
+ "mode": "root_wireless",
+ "own_mac_idx": 0,
+ "mac_address_list": [
+ {
+ "hop_num": 0,
+ "mac_address": "ABCDEFE12F67",
+ "ap_status": "none",
+ "child_num": 0,
+ "ip_address": "192.168.1.9"
+ },
+ {
+ "hop_num": 0,
+ "mac_address": "ABCDEA3DFA1D",
+ "ap_status": "none",
+ "child_num": 0,
+ "ip_address": "192.168.1.14"
+ },
+ {
+ "hop_num": 0,
+ "mac_address": "ABCDE62642B5",
+ "ap_status": "none",
+ "child_num": 0,
+ "ip_address": "192.168.1.13"
+ },
+ {
+ "hop_num": 0,
+ "mac_address": "ABCDEA00A677",
+ "ap_status": "none",
+ "child_num": 0,
+ "ip_address": "192.168.1.10"
+ }
+ ],
+ "ap_list": [
+ {
+ "ssid": "privateWIFI",
+ "key": "",
+ "type": "wpa2-psk(aes)",
+ "bssid": "703ACB751410"
+ },
+ {
+ "ssid": "privateWIFI",
+ "key": "",
+ "type": "wpa2-psk(aes)",
+ "bssid": "703ACB751922"
+ }
+ ],
+ "hop_num": 0
+}
\ No newline at end of file
diff --git a/tests/assets/system/getNameText.json b/tests/assets/system/getNameText.json
new file mode 100755
index 0000000..12bf340
--- /dev/null
+++ b/tests/assets/system/getNameText.json
@@ -0,0 +1,181 @@
+{
+ "response_code": 0,
+ "zone_list": [
+ {
+ "id": "main",
+ "text": "BedRomm"
+ },
+ {
+ "id": "zone2",
+ "text": "Room2"
+ }
+ ],
+ "input_list": [
+ {
+ "id": "tuner",
+ "text": "Tuner"
+ },
+ {
+ "id": "hdmi1",
+ "text": "HDMI1"
+ },
+ {
+ "id": "hdmi2",
+ "text": "HDMI2"
+ },
+ {
+ "id": "hdmi3",
+ "text": "HDMI3"
+ },
+ {
+ "id": "hdmi4",
+ "text": "HDMI4"
+ },
+ {
+ "id": "av1",
+ "text": "AV1"
+ },
+ {
+ "id": "av2",
+ "text": "AV2"
+ },
+ {
+ "id": "av3",
+ "text": "AV3"
+ },
+ {
+ "id": "av4",
+ "text": "AV4"
+ },
+ {
+ "id": "aux",
+ "text": "AUX"
+ },
+ {
+ "id": "audio1",
+ "text": "Audio1"
+ },
+ {
+ "id": "audio2",
+ "text": "Audio2"
+ },
+ {
+ "id": "usb",
+ "text": "USB"
+ },
+ {
+ "id": "bluetooth",
+ "text": "Bluetooth"
+ },
+ {
+ "id": "server",
+ "text": "Server"
+ },
+ {
+ "id": "net_radio",
+ "text": "Net Radio"
+ },
+ {
+ "id": "spotify",
+ "text": "Spotify"
+ },
+ {
+ "id": "juke",
+ "text": "JUKE"
+ },
+ {
+ "id": "airplay",
+ "text": "AirPlay"
+ },
+ {
+ "id": "qobuz",
+ "text": "Qobuz"
+ },
+ {
+ "id": "mc_link",
+ "text": "MC Link"
+ }
+ ],
+ "sound_program_list": [
+ {
+ "id": "munich",
+ "text": "Hall in Munich"
+ },
+ {
+ "id": "vienna",
+ "text": "Hall in Vienna"
+ },
+ {
+ "id": "chamber",
+ "text": "Chamber"
+ },
+ {
+ "id": "cellar_club",
+ "text": "Cellar Club"
+ },
+ {
+ "id": "roxy_theatre",
+ "text": "The Roxy Theatre"
+ },
+ {
+ "id": "bottom_line",
+ "text": "The Bottom Line"
+ },
+ {
+ "id": "sports",
+ "text": "Sports"
+ },
+ {
+ "id": "action_game",
+ "text": "Action Game"
+ },
+ {
+ "id": "roleplaying_game",
+ "text": "Roleplaying Game"
+ },
+ {
+ "id": "music_video",
+ "text": "Music Video"
+ },
+ {
+ "id": "standard",
+ "text": "Standard"
+ },
+ {
+ "id": "spectacle",
+ "text": "Spectacle"
+ },
+ {
+ "id": "sci-fi",
+ "text": "Sci-Fi"
+ },
+ {
+ "id": "adventure",
+ "text": "Adventure"
+ },
+ {
+ "id": "drama",
+ "text": "Drama"
+ },
+ {
+ "id": "mono_movie",
+ "text": "Mono Movie"
+ },
+ {
+ "id": "2ch_stereo",
+ "text": "2ch Stereo"
+ },
+ {
+ "id": "5ch_stereo",
+ "text": "5ch Stereo"
+ },
+ {
+ "id": "surr_decoder",
+ "text": "Surround Decoder"
+ },
+ {
+ "id": "straight",
+ "text": "Straight"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/tests/assets/system/getNetworkStatus.json b/tests/assets/system/getNetworkStatus.json
new file mode 100755
index 0000000..37c3250
--- /dev/null
+++ b/tests/assets/system/getNetworkStatus.json
@@ -0,0 +1,37 @@
+{
+ "response_code": 0,
+ "network_name": "BedRoom",
+ "connection": "wireless_lan",
+ "dhcp": true,
+ "ip_address": "192.168.1.9",
+ "subnet_mask": "255.255.255.0",
+ "default_gateway": "192.168.1.1",
+ "dns_server_1": "192.168.1.1",
+ "dns_server_2": "0.0.0.0",
+ "wireless_lan": {
+ "ssid": "WIFIssid",
+ "type": "wpa2-psk(aes)",
+ "key": "",
+ "ch": 6,
+ "strength": 100
+ },
+ "wireless_direct": {
+ "ssid": "RX-V481D FAA063",
+ "type": "none",
+ "key": ""
+ },
+ "musiccast_network": {
+ "ready": true,
+ "device_type": "standard",
+ "child_num": 0,
+ "ch": 0,
+ "initial_join_running": false
+ },
+ "mac_address": {
+ "wired_lan": "ABCDEEFAA063",
+ "wireless_lan": "ABCDEFE12F66",
+ "wireless_direct": "ABCDEFE12F67"
+ },
+ "vtuner_id": "ABCDEEFAA063",
+ "airplay_pin": ""
+}
\ No newline at end of file
diff --git a/tests/assets/system/getTag.json b/tests/assets/system/getTag.json
new file mode 100755
index 0000000..a012e2f
--- /dev/null
+++ b/tests/assets/system/getTag.json
@@ -0,0 +1,99 @@
+{
+ "response_code": 0,
+ "zone_list": [
+ {
+ "id": "main",
+ "tag": 0
+ },
+ {
+ "id": "zone2",
+ "tag": 0
+ }
+ ],
+ "input_list": [
+ {
+ "id": "tuner",
+ "tag": 0
+ },
+ {
+ "id": "hdmi1",
+ "tag": 0
+ },
+ {
+ "id": "hdmi2",
+ "tag": 0
+ },
+ {
+ "id": "hdmi3",
+ "tag": 0
+ },
+ {
+ "id": "hdmi4",
+ "tag": 0
+ },
+ {
+ "id": "av1",
+ "tag": 0
+ },
+ {
+ "id": "av2",
+ "tag": 0
+ },
+ {
+ "id": "av3",
+ "tag": 0
+ },
+ {
+ "id": "av4",
+ "tag": 0
+ },
+ {
+ "id": "aux",
+ "tag": 0
+ },
+ {
+ "id": "audio1",
+ "tag": 0
+ },
+ {
+ "id": "audio2",
+ "tag": 0
+ },
+ {
+ "id": "usb",
+ "tag": 0
+ },
+ {
+ "id": "bluetooth",
+ "tag": 0
+ },
+ {
+ "id": "server",
+ "tag": 0
+ },
+ {
+ "id": "net_radio",
+ "tag": 0
+ },
+ {
+ "id": "spotify",
+ "tag": 0
+ },
+ {
+ "id": "juke",
+ "tag": 0
+ },
+ {
+ "id": "airplay",
+ "tag": 0
+ },
+ {
+ "id": "qobuz",
+ "tag": 0
+ },
+ {
+ "id": "mc_link",
+ "tag": 0
+ }
+ ]
+}
\ No newline at end of file
diff --git a/tests/assets/system/isNewFirmwareAvailable.json b/tests/assets/system/isNewFirmwareAvailable.json
new file mode 100755
index 0000000..97c5c7d
--- /dev/null
+++ b/tests/assets/system/isNewFirmwareAvailable.json
@@ -0,0 +1,4 @@
+{
+ "response_code": 0,
+ "available": true
+}
\ No newline at end of file
diff --git a/tests/assets/system/sendIrCode.json b/tests/assets/system/sendIrCode.json
new file mode 100755
index 0000000..bf326b8
--- /dev/null
+++ b/tests/assets/system/sendIrCode.json
@@ -0,0 +1,3 @@
+{
+ "response_code": 0
+}
\ No newline at end of file
diff --git a/tests/assets/zone/getSignalInfo.json b/tests/assets/zone/getSignalInfo.json
new file mode 100755
index 0000000..4327c9c
--- /dev/null
+++ b/tests/assets/zone/getSignalInfo.json
@@ -0,0 +1,8 @@
+{
+ "response_code": 0,
+ "audio": {
+ "error": 0,
+ "format": "MP3",
+ "fs": "22.05 kHz"
+ }
+}
\ No newline at end of file
diff --git a/tests/assets/zone/getSoundProgramList.json b/tests/assets/zone/getSoundProgramList.json
new file mode 100755
index 0000000..adeba71
--- /dev/null
+++ b/tests/assets/zone/getSoundProgramList.json
@@ -0,0 +1,25 @@
+{
+ "response_code": 0,
+ "sound_program_list": [
+ "munich",
+ "vienna",
+ "chamber",
+ "cellar_club",
+ "roxy_theatre",
+ "bottom_line",
+ "sports",
+ "action_game",
+ "roleplaying_game",
+ "music_video",
+ "standard",
+ "spectacle",
+ "sci-fi",
+ "adventure",
+ "drama",
+ "mono_movie",
+ "2ch_stereo",
+ "5ch_stereo",
+ "surr_decoder",
+ "straight"
+ ]
+}
\ No newline at end of file
diff --git a/tests/assets/zone/getStatus.json b/tests/assets/zone/getStatus.json
new file mode 100755
index 0000000..b208307
--- /dev/null
+++ b/tests/assets/zone/getStatus.json
@@ -0,0 +1,21 @@
+{
+ "response_code": 0,
+ "power": "standby",
+ "sleep": 0,
+ "volume": 77,
+ "mute": false,
+ "max_volume": 161,
+ "input": "tuner",
+ "distribution_enable": true,
+ "sound_program": "5ch_stereo",
+ "direct": false,
+ "enhancer": true,
+ "tone_control": {
+ "mode": "manual",
+ "bass": 0,
+ "treble": 0
+ },
+ "link_control": "standard",
+ "link_audio_delay": "lip_sync",
+ "disable_flags": 0
+}
\ No newline at end of file
diff --git a/tests/assets/zone/prepareInputChange.json b/tests/assets/zone/prepareInputChange.json
new file mode 100755
index 0000000..bf326b8
--- /dev/null
+++ b/tests/assets/zone/prepareInputChange.json
@@ -0,0 +1,3 @@
+{
+ "response_code": 0
+}
\ No newline at end of file
diff --git a/tests/assets/zone/setInput.json b/tests/assets/zone/setInput.json
new file mode 100755
index 0000000..bf326b8
--- /dev/null
+++ b/tests/assets/zone/setInput.json
@@ -0,0 +1,3 @@
+{
+ "response_code": 0
+}
\ No newline at end of file
diff --git a/tests/assets/zone/setMute.json b/tests/assets/zone/setMute.json
new file mode 100755
index 0000000..bf326b8
--- /dev/null
+++ b/tests/assets/zone/setMute.json
@@ -0,0 +1,3 @@
+{
+ "response_code": 0
+}
\ No newline at end of file
diff --git a/tests/assets/zone/setPower.json b/tests/assets/zone/setPower.json
new file mode 100755
index 0000000..bf326b8
--- /dev/null
+++ b/tests/assets/zone/setPower.json
@@ -0,0 +1,3 @@
+{
+ "response_code": 0
+}
\ No newline at end of file
diff --git a/tests/assets/zone/setSleep.json b/tests/assets/zone/setSleep.json
new file mode 100755
index 0000000..bf326b8
--- /dev/null
+++ b/tests/assets/zone/setSleep.json
@@ -0,0 +1,3 @@
+{
+ "response_code": 0
+}
\ No newline at end of file
diff --git a/tests/assets/zone/setVolume.json b/tests/assets/zone/setVolume.json
new file mode 100755
index 0000000..bf326b8
--- /dev/null
+++ b/tests/assets/zone/setVolume.json
@@ -0,0 +1,3 @@
+{
+ "response_code": 0
+}
\ No newline at end of file
diff --git a/tests/bootstrap.php b/tests/bootstrap.php
new file mode 100755
index 0000000..8a40f76
--- /dev/null
+++ b/tests/bootstrap.php
@@ -0,0 +1,19 @@
+ 0 ]]; then
+ status=255
+ fi
+done
+
+exit $status
\ No newline at end of file