diff --git a/README.md b/README.md
index 1a4a43a..5700214 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,37 @@
## Release
-v.0.9 (2016-11-20)
+v1.0 (2017-02-18)
+
+ -- Attention: cleaned-up configuration file. Please re-configure your Sonos Broker installation
+ -- command "transport_actions" to Sonos Broker and Sonos command line tool
+ -- this options shows all possible actions for the current track (e.g. Next, Stop, Play ...)
+ -- command 'nightmode' added (only for supported speakers)
+ -- bug: Spotify Radio was handled as a normal radio station and should be fixed
+ -- GoogleTTS improvements
+ -- GoogleTTS: files now stored with md5 sum of tts_language and tts_string to reduce the filename length
+ -- GoogleTTS: now works in streaning mode per default, no web service is needed.
+ -- GoogleTTS: the local ip address for the streaming url will be detected automatically (by default)
+ -- play_tts: (optional) attribute 'force_stream_mode' (re)-added to Sonos Broker and Command line tool
+ -- bug: endless loop while trying to play a track from a non-existing url
+ -- bug: wrong path in systemd script
+ -- bug: the 'volume' of all zone members will now be restored correctly after playing the snippet
+ -- command optional parameter 'play' added to command "unjoin"
+ -- with 'play' set to true, the prevoiusly played track (before joining a group) will resumed
+ -- changed executable name from "sonos_broker" to "sonos-broker"
+ -- changed command line tool from "sonos_cmd" to "sonos-cmd"
+ -- changed default installation path of sonos_broker.cfg to /etc/default/sonos-broker
+ -- stopping a running Sonos Broker instance is handled a bit more gracefully
+ -- updated setup script
+ -- auto-start scripts for systemd and upstart automatically placed in the appropriate folder by the
+ installation script
+ -- 'daemonize' behaviour removed
+ -- unnecessary parameter 'stop' removed
+ -- updated documentation
+ -- added "zone_member" command to Sonos Broker (to retrieve this value actively)
+ -- added commands 'join' and 'unjoin' to Sonos Broker commandline tool
+ -- bug: error when trying to decode non-ascii chars and the systems stdout was no set to utf8
+
+v0.9 (2016-11-20)
-- added missing 'track_album' property
-- added missing 'track_album' to Sonos-Broker commandline
@@ -14,55 +45,6 @@ v.0.9 (2016-11-20)
-- bugfixed: Sonos Broker user-specific server port was ignored
-- updated documentation
-v.0.8.2 (2016-11-14)
-
- -- fixed bug in GoogleTTS
-
-v.0.8.1 (2016-11-14)
-
- -- changed commandline arguments to control the Sonos Broker.
- Available arguments:
-
- start [-d] [-c] [-h] (-d=debug mode, -c=user-specified config file, -h=help)
- stop
- list
-
- -- fixed an issue when executing sonos_broker with '-l' parameter
-
-v0.8 (2016-11-11)
-
- -- **ATTENTION:** commands "get_playlist" and "set_playlist" removed. I decided to stick with
- the internal Sonos playlists.
- -- new implementation Google TTS: Captcha and other issues should now be solved (for this time)
- -- **ATTENTION:** parameter "force_stream_mode" removed for command "play_tts" caused by the new
- implementation for Google TTS. The possibility for an additional TTS "stream mode" was removed.
- (see documentation Google TTS for setup)
- -- new command "load_sonos_playlist". See documentation for implementation.
- -- command "sonos_broker_version" added
- -- command 'clear_queue' added
- -- command 'play_tunein' added. Play any TuneIn radio station by a given name
- -- SoCo framework changes (v0.12) with some bugfixes
- -- removed some unused functions
- -- fixed error when calling sonos_broker with 'l' (scan only flag)
- -- small bugfixes
- -- Deezer tracks and their metadata are handled correctly now
- -- Sonos-Broker commandline tools has now parameters (type "sonos_cmd -h" for help)
-
-v0.7 (2016-01-04)
-
- -- command "discover" added to force a manual scan for Sonos speaker in the network
- -- command "balance" can now take the optional parameter "group_command"; documentation updated
- -- property "status" now triggers a value change notification to all connected clients
- -- bugfix: setting play, pause, stop could lead to an infinite loop (play-pause-play ...)
- -- added a valid user-agent for Google TTS requests, this should solve the captcha issue
- -- property 'model_number' added
- -- property 'display_version' added
- -- property 'household_id' added (a unique identifier for all players in a household)
- -- some changes in SoCo framework
- -- bugfixes in command-line tool
- -- command 'balance' (especially for sonos amp and stereo paired sonos speaker) added
-
-
## Overview
@@ -80,78 +62,86 @@ smart home environment (https://github.com/mknx/smarthome/).
## Requirements
+#### Deleting old files
+
+If're updating the Broker it may be a good idea to delete old files. Please adapt the paths to your system.
+
+```
+sudo rm -rf /usr/local/bin/sonos*
+sudo rm -rf /usr/local/lib/python3.5/site-packages/*sonos*
+sudo rm -rf /usr/local/lib/python3.5/site-packages/soco
+```
+
#### Server-side
python3.4
python3 libraries 'requests' and 'xmltodict'
-```
-pip3 install requests
-pip3 install xmltodict
-```
#### Client-side
Nothing special, just send your commands over http (JSON format) or use the Smarthome.py plugin to control
the speakers within Smarthome.py.
+You can use the included built-in implementation, the [Sonos Broker commandline tool](#cmd_tool)
## Installation
#### Setup
-Under the github folder "server.sonos/dist/" you'll find the actual release as a tar.gz file.
-Unzip this file with:
+Under the github folder "server.sonos/dist/" you'll find the actual release as a tar.gz file. Here are two ways to
+install the Sonos Broker:
+
+##### 1. pip
- tar -xvf sonos_broker_release.tar.gz
+If python3-pip is installed, you can simply call
-(adjust the filename to your needs)
+ python3 -m pip -v install sonos-broker-{release-version}.tar.gz
-Go to the unpacked folder and run setup.py with:
+Every dependency should be installed automatically.
+
+##### 2. manually
- sudo python3 setup.py install
+Untar the file with and install it manually.
-This command will install all the python packages and places the start script to the python folder
-"/user/local/bin"
+ tar -xvf sonos-broker-{release}.tar.gz
+ cd sonos-broker-{release}
+ sudo python3 setup.py install --force
-Make the file executable and run the sonos_broker with:
+If an error occurred, you should try to (re)-install all necessary dependencies:
+
+ pip3 install requests
+ pip3 install xmltodict
- chmod +x sonos_broker
- ./sonos_broker
+Both methods will install ```sonos-broker``` and ```sonos-cmd``` under ```/usr/local/bin``` to make both commands
+system-wide executable.
+The default config file is installed under ```/etc/default/sonos-broker``.
-Normally, the script finds the internal ip address of your computer. If not, you have to edit your sonos_broker.cfg.
+The internal ip address should be set up automatically. If not, you have to edit ```/etc/default/sonos-broker```
[sonos_broker]
- server_ip = x.x.x.x
-
-(x.x.x.x means your ip: run ifconfig - a to find it out)
+ server_ip = x.x.x.x #your ip here
#### Configuration / Start options
-You can edit the settings of Sonos Broker. Open 'sonos_broker.cfg' with your favorite editor and edit the file.
-All values within the config file should be self-explaining. For Google-TTS options, see the appropriate section in this
-Readme.
+You can edit the settings of Sonos Broker. Open '/etc/default/sonos-broker' (by default) with your favorite editor and
+edit the file. All values within the config file should be self-explaining. For Google-TTS options, see the appropriate
+section in this Readme.
-If you start the sonos broker with
+You can start the sonos broker with
```
sonos_broker start
```
-the server will be automatically daemonized.
-You can add the -d (--debug) parameter to hold the process in the foreground.
+You can add the -d (--debug) parameter to get more output
```
sonos_broker start -d
```
-An user-specified config file can be passed with the '-c' flag
-```
-sonos_broker start -c
-```
-
-You can stop the server with
+An user-specified config file can be passed with the '-c' flag. Default: /etc/default/sonos-broker
```
-sonos_broker stop
+sonos_broker start -c /your/config/file/path/here
```
To get a short overview of your speakers in the network start the server with
@@ -163,23 +153,39 @@ To get an overview of all parameters type
```
sonos_broker -h
```
-or
+and / or
```
sonos_broker {command} -h
```
-To autostart the service on system boot, please follow the instruction for your linux distribution and put this
-script in the right place.
+After the successful installation with ```sudo python3 setup.py install --force``` an autostart script should be placed
+automatically in your systems autostart directory. The autostart implementations are integrated for systems based on
+'SYSTEMD' and 'UPSTART'. 'SYSVINIT' is NOT supported because of there are too many different sysvinit implementations.
+To control the Broker via the system service control, see the commands below:
+
+SYSTEMD:
+```sudo systemctl [start|stop|restart] sonos-broker```
+
+For autostart:
+```sudo systemctl enable sonos-broker```
+
+
+UPSTART:
+```sudo service sonos-broker [start|stop|restart]```
+
+For autostart uncomment following line in ```/etc/init/sonos-broker.conf```:
+```#start on runlevel [2345]```
+
-To get some debug output, please edit the sonos_broker.cfg and uncomment this line in the logging section (or use the
--d start parameter):
+To get some more debug output when running the Broker as a service, please edit the Sonos Broker config file
+(default: /etc/default/sonos-broker) and uncomment this line in the logging section:
loglevel = debug
You can set the debug level to debug, info, warning, error, critical.
Additionally, you can specify a file to pipe the debug log to this file.
- logfile = log.txt
+ logfile = /path/to/your/log.txt
## Interactive Command Line
@@ -187,7 +193,7 @@ Additionally, you can specify a file to pipe the debug log to this file.
You can control the Broker and your speakers without implementing your own client. To start the interactive
command line (the Broker must be running) type
```
-./sonos_cmd
+sonos-cmd
```
in the root folder of the Sonos Broker.
@@ -231,54 +237,32 @@ exit
redirects you to the first command line level.
-## Google TTS Support
-
-Sonos broker features the Google Text-To-Speech API. You can play any text limited to 100 chars.
-
-
-#### Prerequisite:
-
-- local / remote mounted folder or share with read/write access
-- http access to this local folder (e.g. /var/www)
-- settings configured in sonos_broker.conf
+## Integrated webservice for audio files
-#### Internals
-
-If a text is given to the google tts function, sonos broker makes a http request to the Google API. The response is
-stored as a mp3-file to the local / remote folder.
-
-Before the request is made ('local mode'), the broker checks whether a file exists
-with the same name. The file name of a tts-file is always: BASE64(_).mp3
-You can set a file quota in the config file. This limits the amount of disk space the broker can use to save tts files.
-If the quota exceeds, you will receive a message. By default the quota is set to 100 mb.
+Sonos Broker integrates an internal webservice to serve audio files for Sonos speakers. You can enable this feature in
+the \[webservice\] section of the Sonos Broker configuration. If enabled, you can store audio files in the configurable
+web root path (valid extensions: aac, mp4, mp3, ogg, wav, web) and they can be played with the
+[play_snippet](#p_snippet) command. The URL is available via http://SONOS_BROKER_IP:SONOS_BROKER_PORT/.
+This service is also helpful for the Google TTS functionality.
- sonos_broker.cfg:
- [google_tts]
- quota = 200
-
-By default, Google TTS support is disabled. To enable the service, add following line to sonos_broker.cfg:
-
- sonos_broker.cfg:
+## Google TTS Support
- [google_tts]
- enable = true
+Sonos Broker features the Google Text-To-Speech API. You can play any text limited to 100 chars.
-You have to set the local save path (where the mp3 is stored) and the accessible local url:
- sonos_broker.cfg
+#### Prerequisite:
- [google_tts]
- save_path =/your/path/here
- server_url = http://192.168.0.2/tts
+- internet connection
+- settings configured in sonos-broker configuration file (default: /etc/default/sonos-broker)
-This is an example of a google_tts section in the sonos_broker.cfg file:
+#### Internals
- [google_tts]
- enable=true
- quota=200
- save_path =/your/path/here
- server_url = http://192.168.0.2/tts
+If a text is given to the google tts function, the Sonos Broker makes a http request to the Google API and the tts
+string will be played by your Sonos loudspeakers. Additionally, you can set-up the Broker to store the request as a
+mp3 file. This is a kind of local caching: if a tts string is requested which is already stored as a mp3 file, this
+file will be served to the Sonos speakers. To enable this feature, you have to configure the webservice feature of
+the Sonos Broker. Have a look at the configuration file for more details [/etc/default/sonos-broker]
## Implementation:
@@ -348,6 +332,7 @@ In almost any cases, you'll get the appropriate response in the following JSON f
"track_position": "00:00:00",
"track_title": "Das Baby im Schafspelz",
"track_uri": "x-sonos-spotify:spotify%3atrack%3a3xCk8npVehdV55KuPdjrmZ?sid=9&flags=32",
+ "transport_actions": "Set,Stop,Pause,Play,Next"
"treble": 0,
"uid": "rincon_000e58c3892e01410",
"volume": 8,
@@ -406,6 +391,8 @@ Click on the links below to get a detailed command descriptions and their usage.
###### [set_balance](#s_balance)
###### [get_loudness](#g_loudness)
###### [set_loudness](#s_loudness)
+###### [get_nightmode](#g_nightmode)
+###### [set_nightmode](#s_nightmode)
###### [get_led](#g_led)
###### [set_led](#s_led)
###### [get_playmode](#g_playmode)
@@ -417,6 +404,7 @@ Click on the links below to get a detailed command descriptions and their usage.
###### [get_track_artist](#g_track_artist)
###### [get_track_album_art](#g_track_album_art)
###### [get_track_uri](#g_track_uri)
+###### [get_transport_actions](#g_transport_actions)
###### [get_playlist_position](#g_playlist_position)
###### [get_playlist_total_tracks](#g_playlist_total_tracks)
###### [get_radio_station](#g_radio_station)
@@ -437,6 +425,7 @@ Click on the links below to get a detailed command descriptions and their usage.
###### [get_sonos_playlists](#get_sonos_playlists)
###### [load_sonos_playlist](#load_sonos_playlist)
###### [refresh_media_library](#ref_lib)
+###### [zone_members](#zone_members)
###### [get_wifi_state](#get_wifi)
###### [set_wifi_state](#set_wifi)
###### [discover](#discover)
@@ -1319,6 +1308,72 @@ No special parameter needed.
The response is only sent if the new value is different from the old value.
+----
+#### get_nightmode
+ Gets the current nightmode setting from a Sonos speaker. The nightmode feature is only avaliable for certain Sonos
+ speakers, e.g. the Sonos Playbar. For all other speakers, this value should always be 0.
+ In most cases, you don't have to execute this command, because all subscribed clients will be notified automatically
+ about 'nightmode'-status changes.
+
+| parameter | required / optional | valid values | description |
+| :-------- | :------------------ | :----------- | :---------- |
+| uid | required | | The UID of the Sonos speaker. |
+
+######Example
+ JSON format:
+ {
+ 'command': 'get_nightmode',
+ 'parameter': {
+ 'uid': 'rincon_b8e93730d19801410'
+ }
+ }
+
+######HTTP Response
+ HTTP 200 OK or Exception with HTTP status 400 and the specific error message.
+
+######UDP Response sent to subscribed clients:
+ JSON format:
+ {
+ ...
+ "nightmode": 0|1,
+ "uid": "rincon_b8e93730d19801410",
+ ...
+ }
+
+----
+#### set_nightmode
+ Sets the nightmode option for a Sonos speaker. If the Sonos speaker does not support this feature, an error message
+ is thrown.
+
+| parameter | required / optional | valid values | description |
+| :-------- | :------------------ | :----------- | :---------- |
+| uid | required | | The UID of the Sonos speaker. |
+| nightmode | required | 0 or 1 | The nightmode to be set. |
+
+######Example
+ JSON format:
+ {
+ 'command': 'set_nightmode',
+ 'parameter': {
+ 'uid': 'rincon_b8e93730d19801410',
+ 'nightmode': 1
+ }
+ }
+
+######HTTP Response
+ HTTP 200 OK or Exception with HTTP status 400 and the specific error message.
+
+######UDP Response sent to subscribed clients:
+ JSON format:
+ {
+ ...
+ "nightmode": 0|1,
+ "uid": "rincon_b8e93730d19801410",
+ ...
+ }
+
+ The response is only sent if the new value is different from the old value.
+
----
#### get_led
Gets the current led status from a Sonos speaker.
@@ -1515,6 +1570,7 @@ No special parameter needed.
The response is only sent if the new value is different from the old value.
+----
#### get_track_album
Returns the album title of the currently played track.
In most cases, you don't have to execute this command, because all subscribed clients will be notified automatically
@@ -1545,6 +1601,7 @@ No special parameter needed.
...
}
+----
#### get_track_title
Returns the title of the currently played track.
In most cases, you don't have to execute this command, because all subscribed clients will be notified automatically
@@ -1575,6 +1632,7 @@ No special parameter needed.
...
}
+----
#### get_track_artist
Returns the artist of the currently played track.
In most cases, you don't have to execute this command, because all subscribed clients will be notified automatically
@@ -1605,6 +1663,7 @@ No special parameter needed.
...
}
+----
#### get_track_album_art
Returns the album-cover url of the currently played track.
In most cases, you don't have to execute this command, because all subscribed clients will be notified automatically
@@ -1635,6 +1694,7 @@ No special parameter needed.
...
}
+----
#### get_track_uri
Returns the track url of the currently played track.
In most cases, you don't have to execute this command, because all subscribed clients will be notified automatically
@@ -1666,7 +1726,39 @@ No special parameter needed.
}
All URIs can be passed to the play_uri and play_snippet functions.
+
+----
+#### get_transport_actions
+ Returns the available transport actions for the current track. This could be useful for a UI to display certain
+ control items (or not).
+ In most cases, you don't have to execute this command, because all subscribed clients will be notified automatically
+ about 'transport_actions'-status changes.
+
+| parameter | required / optional | valid values | description |
+| :-------- | :------------------ | :----------- | :---------- |
+| uid | required | | The UID of the Sonos speaker. |
+
+######Example
+ JSON format:
+ {
+ 'command': 'get_transport_actions',
+ 'parameter': {
+ 'uid': 'rincon_b8e93730d19801410'
+ }
+ }
+######HTTP Response
+ HTTP 200 OK or Exception with HTTP status 400 and the specific error message.
+
+######UDP Response sent to subscribed clients:
+ JSON format:
+ {
+ ...
+ "transport_actions": "Set,Stop,Pause,Play,Next",
+ "uid": "rincon_b8e93730d19801410",
+ ...
+ }
+
----
#### get_playlist_position
Returns the position of the currently played track in the playlist.
@@ -1833,13 +1925,15 @@ No special parameter needed.
| parameter | required / optional | valid values | description |
| :-------- | :------------------ | :----------- | :---------- |
| uid | required | | The UID of the Sonos speaker. |
+| play | optional | True/False, 0/1 | Should the speaker automatically start playing after unjoin. |
######Example
JSON format:
{
'command': 'unjoin',
'parameter': {
- 'uid': 'rincon_b8e93730d19801410'
+ 'uid': 'rincon_b8e93730d19801410',
+ 'play': False
}
}
@@ -2053,6 +2147,7 @@ No special parameter needed.
| fade_in | optional | 0 or 1 | If True, the volume for the resumed track / radio fades in |
| volume | optional | -1 - 100 | The snippet volume. If -1 (default) the current volume is used. After the snippet was played, the prevoius volume value is set. |
| group_command | optional | 0 or 1 | If 'True', the command is executed for all zone members of the speaker. This affects only the parameter 'volume'.|
+| force_stream_mode | optional | 0 or 1 | If 'True', Google TTS is streamed directly without storing the track locally. This overrides the Broker settings.|
######Example
JSON format:
@@ -2063,7 +2158,8 @@ No special parameter needed.
'tts': 'Die Temperatur im Wohnzimmer beträgt 2 Grad Celsius.'
'language': 'de',
'volume': 30,
- 'group_command': 1
+ 'group_command': 1,
+ 'force_stream_mode': 0
}
}
@@ -2146,47 +2242,52 @@ No special parameter needed.
######UDP Response sent to subscribed clients:
JSON format:
{
- "additional_zone_members": "",
- "alarms": "",
- "balance": 0,
+ "additional_zone_members": "rincon_112ef9e4892e00001",
+ "alarms": {
+ "32": {
+ "Duration": "02:00:00",
+ "Enabled": false,
+ "IncludedLinkZones": false,
+ "PlayMode": "SHUFFLE_NOREPEAT",
+ "Recurrence": "DAILY",
+ "StartTime": "07:00:00",
+ "Volume": 25
+ }
+ },
"bass": 0,
- "display_version": "6.0",
- "hardware_version": "1.8.1.2-2",
- "household_id": "Sonos_Ef8RhcyY1ijYDDFp1I3GitguTP",
- "ip": "192.168.0.11",
- "is_coordinator": true,
+ "hardware_version": "1.8.3.7-2",
+ "ip": "192.168.0.4",
"led": 1,
"loudness": 1,
- "mac_address": "B8-E9-37-38-E1-72",
+ "mac_address": "10:1F:21:C3:77:1A",
"max_volume": -1,
- "model": "Sonos PLAY:3",
- "model_number": "S3",
- "mute": 0,
- "pause": 1,
+ "model": "Sonos PLAY:1",
+ "mute": "0",
+ "pause": 0,
"play": 0,
"playlist_position": "1",
"playlist_total_tracks": "10",
"playmode": "normal",
"radio_show": "",
"radio_station": "",
- "serial_number": "B8-E9-37-38-E1-72:5",
- "software_version": "31.3-22220",
- "sonos_playlists": "Morning,Evening,U2",
+ "serial_number": "00-0E-58-C3-89-2E:7",
+ "software_version": "27.2-80271",
+ "sonos_playlists": "DepecheMode,my_fav-list2,my-fav-list2",
"status": true,
- "stop": 0,
+ "stop": 1,
"streamtype": "music",
- "track_album_art": "http://192.168.0.11:1400/getaa?s=1&u=x-sonos-http%3atr%253a119434742.mp3%3fsid%3d2%26flags%3d8224%26sn%3d1",
- "track_artist": "Was ist Was",
- "track_duration": "0:02:22",
+ "track_album": "Feuerwehrmann Sam 02",
+ "track_album_art": "http://192.168.0.4:1400/getaa?s=1&u=x-sonos-spotify%3aspotify%253atrack%253a3xCk8npVehdV55KuPdjrmZ%3fsid%3d9%26flags%3d32",
+ "track_artist": "Feuerwehrmann Sam & Clemens Gerhard",
+ "track_duration": "0:10:15",
"track_position": "00:00:00",
- "track_title": "Europa - Teil 10",
- "track_uri": "x-sonos-http:tr%3a119434742.mp3?sid=2&flags=8224&sn=1",
+ "track_title": "Das Baby im Schafspelz",
+ "track_uri": "x-sonos-spotify:spotify%3atrack%3a3xCk8npVehdV55KuPdjrmZ?sid=9&flags=32",
+ "transport_actions": "Set,Stop,Pause,Play,Next"
"treble": 0,
- "tts_local_mode": false,
- "uid": "rincon_b8e91111d11111400",
- "volume": 17,
- "wifi_state": 1,
- "zone_icon": "/img/icon-S3.png",
+ "uid": "rincon_000e58c3892e01410",
+ "volume": 8,
+ "zone_icon": "x-rincon-roomicon:bedroom",
"zone_name": "Kinderzimmer"
}
@@ -2390,6 +2491,39 @@ This has some disadvantages. Please read the Google TTS section in this document
No UDP response
+----
+#### zone_members
+ Gets all additional zone members of the group the current speaker is part of. If the speaker is the only member of the
+ group, the response is empty.
+ In most cases, you don't have to execute this command, because all subscribed clients will be notified automatically
+ about 'zone_members'-status changes.
+
+| parameter | required / optional | valid values | description |
+| :-------- | :------------------ | :----------- | :---------- |
+| uid | required | | The UID of the Sonos speaker. |
+
+######Example
+ JSON format:
+ {
+ 'command': 'zone_members',
+ 'parameter': {
+ 'uid': rincon_b8e91111d11111400
+ }
+
+######HTTP Response
+ HTTP 200 OK
+ or
+ Exception with HTTP status 400 and the specific error message.
+
+###### UDP Response sent to subscribed clients:
+ JSON format:
+ {
+ ...
+ "zone_members": ["rincon_c4441111d11111400", "rincon_d5ee1111d11111400"]
+ "uid": "rincon_b8e91111d11111400"
+ ...
+ }
+
----
#### get_wifi_state
Gets the current wifi status. Since there is no sonos event for the wifi state, you have to trigger
@@ -2492,4 +2626,4 @@ This has some disadvantages. Please read the Google TTS section in this document
Exception with HTTP status 400 and the specific error message.
###### UDP Response sent to subscribed clients:
- No UDP response
+ No UDP response
\ No newline at end of file
diff --git a/plugin.sonos/README.md b/plugin.sonos/README.md
index 5d155e3..11930d9 100644
--- a/plugin.sonos/README.md
+++ b/plugin.sonos/README.md
@@ -1,47 +1,49 @@
-This sub-project is a client implementation fpr the Sonos Broker. It is a plugin for the
-Smarthome.py framework (https://github.com/mknx/smarthome).
+This sub-project is a client implementation for the Sonos Broker. It is a plugin for the
+SmarthomeNG framework (https://github.com/smarthomeNG).
##Release
-v0.9 (2016-11-20)
-
+v1.0 (2017-02-15)
+
+ -- dpt3 functionality added for volume item
+ -- command 'transport_actions' added
+ -- command 'nightmode' added
+ -- play_tts: attribute 'force_stream_mode' (re)-added
+ -- added attribute 'play' to 'unjoin'
+ -- resumes the last played track / radio before join to another group
-- added missing 'track_album' property
-- add new property 'playlist_total_tracks'
- -- change expected Sonos Broker version to 0.9
+ -- attribute 'is_coordiantor' in example has now the right value
+ -- version string updated
+ -- change expected Sonos Broker version to v1.0
-v0.8.2 (2016-11-14)
+## Overview
- -- change expected Sonos Broker version to 0.8.2
-
-v0.8.1 (2016-11-14)
+[1. Requirements](#req)
+
+[2. Integration in SmarthomeNG](#shng)
+
+[3. Volume DPT3 support](#dpt)
+
+[4. Group behavior](#group)
+
+[5. Methods](#meth)
+
+[6. SmartVISU Integration](#visu)
+
+[7. FAQ](#faq)
- -- switching versioning to the current Sonos Broker version
- -- change expected Sonos Broker version to 0.8.1
-
-v1.8 (2016-11-11)
-
- -- ATTENTION: commands 'get_playlist' and 'set_playlist' removed and replaced by 'sonos_playlists' and
- 'load_sonos_playlist'
- --command "load_sonos_playlist" with parameter added. The commands loads a Sonos playlist by its name.
- -- optional parameters: play_after_insert, clear_queue
- -- command "play_tunein" added
- -- 'play_tunein' expects a radio station name. The name will be searched within TuneIn and the
- first match is played. To make sure the correct radio station is played provide the full radio
- station showing in the Sonos app.
- -- 'clear_queue' command added. The command clears the current queue.
- -- version check against Sonos Broker to identify an out-dated plugin or Broker
-
-## Requirements:
+##Requirements:
- sonos_broker server v0.8.3
+ Sonos Broker v1.0
(https://github.com/pfischi/shSonos)
- SmarthomeNG
+ SmarthomeNG 1.3
(https://github.com/smarthomeNG/smarthome)
-## Integration in Smarthome.py
+##Integration in SmarthomeNG
Go to /usr/smarthome/etc and edit plugins.conf and add ths entry:
@@ -57,9 +59,7 @@ the same system.
The ***refresh*** parameter specifies, how often the broker is requested for sonos status updates (default: 120s).
Normally, all changes to the speakers will be triggered automatically to the plugin.
-Go to /usr/smarthome/items
-
-Create a file named sonos.conf.
+Go to /usr/smarthome/items. Create a file named sonos.conf or copy the sonos.conf from the examples folder.
Edit file with this sample of mine:
@@ -90,8 +90,6 @@ Edit file with this sample of mine:
[[volume]]
type = num
- enforce_updates = True
- visu_acl = rw
sonos_recv = volume
sonos_send = volume
@@ -99,6 +97,16 @@ Edit file with this sample of mine:
type = bool
value = 0
+ [[[volume_dpt3]]]
+ type = list
+ sonos_volume_dpt3 = foo
+ sonos_vol_step = 2
+ sonos_vol_time = 1
+
+ [[[[helper]]]]
+ type = num
+ sonos_send = volume
+
[[max_volume]]
type = num
enforce_updates = True
@@ -238,6 +246,10 @@ Edit file with this sample of mine:
[[[group_command]]]
type = bool
value = 0
+
+ [[[force_stream_mode]]]
+ type = bool
+ value = 0
[[radio_show]]
type = str
@@ -325,6 +337,10 @@ Edit file with this sample of mine:
enforce_updates = True
sonos_send = unjoin
visu_acl = rw
+
+ [[[play]]]
+ type = bool
+ value = 1
[[partymode]]
type = foo
@@ -386,7 +402,14 @@ Edit file with this sample of mine:
[[[group_command]]]
type = bool
value = 0
-
+
+ [[nightmode]]
+ type = bool
+ enforce_updates = True
+ visu_acl = rw
+ sonos_recv = nightmode
+ sonos_send = nightmode
+
[[playmode]]
type = str
enforce_updates = True
@@ -451,6 +474,11 @@ Edit file with this sample of mine:
type = bool
enforce_updates = True
sonos_send = clear_queue
+
+ [[transport_actions]]
+ type = str
+ sonos_recv = transport_actions
+
This sonos.conf file implements most of the commands to interact with the Sonos Broker. Please follow the detailed
@@ -463,7 +491,49 @@ Edit file with this sample of mine:
http:///client/list
-## Group behaviour
+##Volume DPT3 support
+
+If you take look at the ```volume``` item in your Sonos items configuration you should find something like this:
+```
+[[volume]]
+ type = num
+ sonos_recv = volume
+ sonos_send = volume
+
+ [[[group_command]]]
+ type = bool
+ value = 0
+
+ [[[volume_dpt3]]]
+ type = list
+ sonos_volume_dpt3 = foo
+ sonos_vol_step = 2
+ sonos_vol_time = 1
+
+ [[[[helper]]]]
+ type = num
+ sonos_send = volume
+```
+If you want to use a dim-like functionality to control the volume (e.g. with a button), you can edit the
+```volume_dpt3``` item. ***sonos_vol_step*** (default: 2) defines the volume step for up and down, ***sonos_vol_time***
+(default: 1) the time between the steps. Both values are optional, if not set, the default value is used. A real-world
+example could look like this:
+```
+[[[volume_dpt3]]]
+ type = list
+ knx_dpt = 3
+ knx_listen = 7/0/0
+ sonos_volume_dpt3 = foo
+ sonos_vol_step = 2
+ sonos_vol_time = 1
+
+ [[[[helper]]]]
+ type = num
+ sonos_send = volume
+```
+Don't change the items name, otherwise the function will not work.
+
+##Group behaviour
If two or more speakers are in the same zone, most of the commands are automatically executed for all zone
members. Normally the Sonos API requires to send the command to the zone master. This is done by the Broker
@@ -501,7 +571,7 @@ Edit file with this sample of mine:
loudness
balance
-## Methods
+##Methods
get_favorite_radiostations(, )
@@ -559,36 +629,22 @@ discover()
sh.sonos.discover()
-## smartVISU Integration
-
-more information here: https://github.com/pfischi/shSonos/tree/develop/widget.smartvisu
+##smartVISU Integration
+More information [--> HERE <--](https://github.com/pfischi/shSonos/tree/develop/widget.smartvisu)
-## Logic examples
-
-To run this plugin with a logic, here is my example:
-
-Go to /usr/smarthome/logics and create a self-named file (e.g. sonos.py)
-Edit this file and place your logic here:
-
-
- #!/usr/bin/env python
- #
-
- if sh.ow.ibutton():
- sh.sonos.mute(1)
- else:
- sh.sonos.mute(0)
+##FAQ
-
- Last step: go to /usr/smarthome/etc and edit logics.conf
- Add a section for your logic:
-
- # logic
- [sonos_logic]
- filename = sonos.py
- watch_item = ow.ibutton
-
-
-In this small example, the sonos speaker with uid RINCON_000E58D5892E11230 is muted when the iButton is connected
-to an iButton Probe.
\ No newline at end of file
+##### utf-8 codec error
+If you're using Onkelandy's SmarthomeNG Image (and other Linux distros), following error can occurred if you're using
+non-ASCII characters for Sonos speaker names:
+```
+UnicodeDecodeError: 'utf-8' codec can't decode byte 0xb0 in position 37: invalid start byte
+```
+This happens because the 'stdout' setting of the system is set to an ASCII character set. You can this by entering
+following command in your console:
+```
+export LC_ALL=de_DE.utf8
+export LANGUAGE=de_DE.utf8
+```
+For more information about 'locales', please follow this [--> LINK <--](https://www.thomas-krenn.com/de/wiki/Locales_unter_Ubuntu_konfigurieren)
\ No newline at end of file
diff --git a/plugin.sonos/__init__.py b/plugin.sonos/__init__.py
index 8084fb0..b85546e 100755
--- a/plugin.sonos/__init__.py
+++ b/plugin.sonos/__init__.py
@@ -1,46 +1,25 @@
#!/usr/bin/env python3
-# vim: set encoding=utf-8 tabstop=4 softtabstop=4 shiftwidth=4 expandtab
-# ########################################################################
-# Copyright 2013 KNX-User-Forum e.V. http://knx-user-forum.de/
-#########################################################################
-# This file is part of SmartHome.py. http://mknx.github.io/smarthome/
-#
-# SmartHome.py is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# SmartHome.py is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with SmartHome.py. If not, see .
-#########################################################################
import http
import logging
import lib.connection
import lib.tools
-import os
import re
-import socket
import threading
import json
-import fcntl
-import struct
import requests
+import time
+from lib.model.smartplugin import SmartPlugin
-EXPECTED_BROKER_VERSION = "0.9"
-logger = logging.getLogger('')
+EXPECTED_BROKER_VERSION = "1.0"
sonos_speaker = {}
class UDPDispatcher(lib.connection.Server):
def __init__(self, ip, port, sh):
+ self._logger = logging.getLogger('sonos')
lib.connection.Server.__init__(self, ip, port, proto='UDP')
self.dest = 'udp:' + ip + ':{port}'.format(port=port)
- logger.debug('starting udp listener with {url}'.format(url=self.dest))
+ self._logger.debug('starting udp listener with {url}'.format(url=self.dest))
self._sh = sh
self.connect()
@@ -48,9 +27,9 @@ def handle_connection(self):
try:
data, address = self.socket.recvfrom(10000)
address = "{}:{}".format(address[0], address[1])
- logger.debug("{}: incoming connection from {}".format('sonos', address))
+ self._logger.debug("{}: incoming connection from {}".format('sonos', address))
except Exception as err:
- logger.error("{}: {}".format(self._name, err))
+ self._logger.error("{}: {}".format(self._name, err))
return
try:
@@ -58,9 +37,9 @@ def handle_connection(self):
uid = sonos['uid']
if not uid:
- logger.error("No uid found in sonos udp response!\nResponse: {}")
+ self._logger.error("No uid found in sonos udp response!\nResponse: {}")
if uid not in sonos_speaker:
- logger.warning("no sonos speaker configured with uid '{uid}".format(uid=uid))
+ self._logger.warning("no sonos speaker configured with uid '{uid}".format(uid=uid))
return
for key, value in sonos.items():
@@ -71,20 +50,28 @@ def handle_connection(self):
item(value, 'Sonos', '')
except Exception as err:
- logger.error("Error parsing sonos broker response!\nError: {}".format(err))
+ self._logger.error("Error parsing sonos broker response!\nError: {}".format(err))
-class Sonos():
- def __init__(self, smarthome, listen_host='0.0.0.0', listen_port=9999, broker_url=None, refresh=120):
+class Sonos(SmartPlugin):
+ PLUGIN_VERSION = "1.3.0.1"
+ ALLOW_MULTIINSTANCE = False
+
+ def __init__(self, sh, listen_host='0.0.0.0', listen_port=9999, broker_url=None, refresh=120):
+
self._sonoslock = threading.Lock()
self._lan_ip = get_lan_ip()
+ self._logger = logging.getLogger('sonos')
+ self._dpt3_vol_step = 1
+ self._dpt3_vol_time = 1
+ self._dpt3_vol_max = 100
if not self._lan_ip:
- logger.critical("Could not fetch internal ip address. Set it manually!")
+ self._logger.critical("Could not fetch internal ip address. Set it manually!")
self.alive = False
return
- logger.info("using local ip address {ip}".format(ip=self._lan_ip))
+ self._logger.info("using local ip address {ip}".format(ip=self._lan_ip))
# check broker variable
if broker_url:
@@ -92,10 +79,10 @@ def __init__(self, smarthome, listen_host='0.0.0.0', listen_port=9999, broker_ur
else:
self._broker_url = "http://{ip}:12900".format(ip=self._lan_ip)
if self._broker_url:
- logger.warning("No broker url given, assuming current ip and default broker port: {url}".
- format(url=self._broker_url))
+ self._logger.warning("No broker url given, assuming current ip and default broker port: {url}".
+ format(url=self._broker_url))
else:
- logger.error("Could not detect broker url !!!")
+ self._logger.error("Could not detect broker url !!!")
return
# normalize broker url
@@ -105,30 +92,29 @@ def __init__(self, smarthome, listen_host='0.0.0.0', listen_port=9999, broker_ur
# version check against Sonos Broker
broker_version = self._send_cmd(SonosCommand.sonos_broker_version())
- logger.debug("Sonos broker version: {version}".format(version=broker_version))
+ self._logger.debug("Sonos broker version: {version}".format(version=broker_version))
try:
if EXPECTED_BROKER_VERSION != broker_version:
- logger.warning("This plugin is desgined to work with Sonos Broker version {version}. "
- "Your plugin version is probably out-of-date or too new. "
- "Please update your plugin and/or the Sonos Broker Server".format(
+ self._logger.warning("This plugin is desgined to work with Sonos Broker version {version}. "
+ "Your plugin version is probably out-of-date or too new. "
+ "Please update your plugin and/or the Sonos Broker Server".format(
version=EXPECTED_BROKER_VERSION))
except Exception:
- logger.warning("Unknown Sonos broker version string '{version_string}.'".
- format(version_string=broker_version))
+ self._logger.warning("Unknown Sonos broker version string '{version_string}.'".
+ format(version_string=broker_version))
- # ini vars
self._listen_host = listen_host
self._listen_port = listen_port
- self._sh = smarthome
+ self._sh = sh
self._command = SonosCommand()
- logger.debug('refresh sonos speakers every {refresh} seconds'.format(refresh=refresh))
+ self._logger.debug('refresh sonos speakers every {refresh} seconds'.format(refresh=refresh))
# add current_state command to scheduler
self._sh.scheduler.add('sonos-update', self._subscribe, cycle=refresh)
# start UDP listener
- UDPDispatcher(self._listen_host, self._listen_port, smarthome)
+ UDPDispatcher(self._listen_host, self._listen_port, self._sh)
def run(self):
self.alive = True
@@ -138,7 +124,7 @@ def _subscribe(self):
"""
Subscribe the plugin to the Sonos Broker
"""
- logger.debug('(re)registering to sonos broker server ...')
+ self._logger.debug('(re)registering to sonos broker server ...')
self._send_cmd(SonosCommand.subscribe(self._lan_ip, self._listen_port))
for uid, speaker in sonos_speaker.items():
@@ -148,7 +134,7 @@ def _unsubscribe(self):
"""
Unsubscribe the plugin from the Sonos Broker
"""
- logger.debug('unsubscribing from sonos broker server ...')
+ self._logger.debug('unsubscribing from sonos broker server ...')
self._send_cmd(SonosCommand.unsubscribe(self._lan_ip, self._listen_port))
def stop(self):
@@ -162,11 +148,14 @@ def stop(self):
def _resolve_uid(self, item):
uid = None
- parent_item = item.return_parent()
+ if 'volume_dpt3.helper' in item._name:
+ parent_item = item.return_parent().return_parent().return_parent()
+ else:
+ parent_item = item.return_parent()
if (parent_item is not None) and ('sonos_uid' in parent_item.conf):
uid = parent_item.conf['sonos_uid'].lower()
else:
- logger.warning("sonos: could not resolve sonos_uid".format(item))
+ self._logger.warning("sonos: could not resolve sonos_uid".format(item))
return uid
def parse_item(self, item):
@@ -178,7 +167,7 @@ def parse_item(self, item):
return None
attr = item.conf['sonos_recv']
- logger.debug("sonos: {} receives updates by {}".format(item, attr))
+ self._logger.debug("sonos: {} receives updates by {}".format(item, attr))
if not uid in sonos_speaker:
sonos_speaker[uid] = SonosSpeaker()
@@ -189,20 +178,71 @@ def parse_item(self, item):
attr_list.append(item)
if 'sonos_send' in item.conf:
- try:
- self._sonoslock.acquire()
- uid = self._resolve_uid(item)
- if uid is None:
- return None
+ uid = self._resolve_uid(item)
+ if uid is None:
+ return None
+
+ attr = item.conf['sonos_send']
+ self._logger.debug("sonos: {} is send to {}".format(item, attr))
+ return self._update_item
+
+ if self.has_iattr(item.conf, 'sonos_volume_dpt3'):
+ if not self.has_iattr(item.conf, 'sonos_vol_step'):
+ item.conf['sonos_vol_step'] = self._dpt3_vol_step
+ self._logger.warning("Sonos: no sonos_vol_step defined, using default value {step}.".
+ format(step=self._dpt3_vol_step))
+
+ if not self.has_iattr(item.conf, 'sonos_vol_time'):
+ item.conf['sonos_vol_time'] = self._dpt3_vol_time
+ self._logger.warning("Sonos: no sonos_vol_time defined, using default value {time}.".
+ format(time=self._dpt3_vol_time))
+
+ if not self.has_iattr(item.conf, 'sonos_vol_max'):
+ item.conf['sonos_vol_max'] = self._dpt3_vol_max
+ self._logger.warning("Sonos: no sonos_vol_max defined, using default value {max}.".
+ format(max=self._dpt3_vol_max))
- attr = item.conf['sonos_send']
- logger.debug("sonos: {} is send to {}".format(item, attr))
- return self._update_item
- finally:
- self._sonoslock.release()
+ return self._handle_volume_dpt3
return None
+ def _handle_volume_dpt3(self, item, caller=None, source=None, dest=None):
+ self._logger.debug(caller)
+ if caller != 'Sonos':
+
+ volume_helper = None
+
+ volume_item = item.return_parent()
+ if volume_item is None:
+ self._logger.warning("Sonos: no parent volume item found for volume_dpt3 item!")
+ return
+
+ dpt3_helper_name = '{}.helper'.format(item._name)
+
+ for child in item.return_children():
+ if child._name == dpt3_helper_name:
+ volume_helper = child
+
+ if volume_helper is None:
+ self._logger.warning("Sonos: no child helper item found for volume_dpt3 item!")
+ return
+
+ volume_helper(volume_item())
+ vol_step = int(item.conf['sonos_vol_step'])
+ vol_time = int(item.conf['sonos_vol_time'])
+ vol_max = int(item.conf['sonos_vol_max'])
+
+ if item()[1] == 1:
+ if item()[0] == 1:
+ # up
+ volume_helper.fade(vol_max, vol_step, vol_time)
+ else:
+ # down
+ volume_helper.fade(0-vol_step, vol_step, vol_time)
+ else:
+ volume_helper(int(volume_helper() + 1))
+ volume_helper(int(volume_helper() - 1))
+
def parse_logic(self, logic):
pass
@@ -259,6 +299,7 @@ def _update_item(self, item, caller=None, source=None, dest=None):
if child._name.lower() == group_item_name.lower():
group_command = child()
break
+ value = 0 if value < 0 else value
cmd = self._command.volume(uid, value, group_command)
if command == 'max_volume':
@@ -301,6 +342,10 @@ def _update_item(self, item, caller=None, source=None, dest=None):
break
cmd = self._command.treble(uid, value, group_command)
+ if command == 'nightmode':
+ if isinstance(value, bool):
+ cmd = self._command.nightmode(uid, value)
+
if command == 'loudness':
if isinstance(value, bool):
group_item_name = '{}.group_command'.format(item._name)
@@ -316,7 +361,7 @@ def _update_item(self, item, caller=None, source=None, dest=None):
if value in ['normal', 'shuffle_norepeat', 'shuffle', 'repeat_all']:
cmd = self._command.playmode(uid, value)
else:
- logger.warning(
+ self._logger.warning(
"Ignoring PLAYMODE command. Value {value} not a valid paramter!".format(value=value))
if command == 'next':
@@ -369,8 +414,10 @@ def _update_item(self, item, caller=None, source=None, dest=None):
force_stream_mode = child()
if child._name.lower() == fade_item_name.lower():
fade_in = child()
- cmd = self._command.play_tts(uid, value, language, volume, group_command, force_stream_mode,
- fade_in)
+ if child._name.lower() == fade_item_name.lower():
+ fade_in = child()
+ cmd = self._command.play_tts(uid, value, language, volume, group_command, fade_in,
+ force_stream_mode)
if command == 'load_sonos_playlist':
clear_item_name = '{}.clear_queue'.format(item._name)
@@ -386,7 +433,7 @@ def _update_item(self, item, caller=None, source=None, dest=None):
if command == 'seek':
if not re.match(r'^[0-9][0-9]?:[0-9][0-9]:[0-9][0-9]$', value):
- logger.warning('invalid timestamp for sonos seek command, use HH:MM:SS format')
+ self._logger.warning('invalid timestamp for sonos seek command, use HH:MM:SS format')
cmd = None
else:
cmd = self._command.seek(uid, value)
@@ -398,7 +445,13 @@ def _update_item(self, item, caller=None, source=None, dest=None):
cmd = self._command.join(uid, value)
if command == 'unjoin':
- cmd = self._command.unjoin(uid)
+ play_item_name = '{}.play'.format(item._name)
+ play = 0
+ for child in item.return_children():
+ if child._name.lower() == play_item_name.lower():
+ play = child()
+ break
+ cmd = self._command.unjoin(uid, play)
if command == 'partymode':
cmd = self._command.partymode(uid)
@@ -432,10 +485,10 @@ def _update_item(self, item, caller=None, source=None, dest=None):
for child in item.return_children():
if child._name.lower() == persistent_item_name.lower():
if value != 0 and persistent == 1:
- logger.warning("command wifi_state: persistent parameter with value '1' will"
- "only affect wifi_state with value '1' (the wifi interface will"
- "remain deactivated after reboot). Ignoring 'persistent' "
- "parameter.")
+ self._logger.warning("command wifi_state: persistent parameter with value '1' will"
+ "only affect wifi_state with value '1' (the wifi interface will"
+ "remain deactivated after reboot). Ignoring 'persistent' "
+ "parameter.")
else:
persistent = child()
break
@@ -448,7 +501,7 @@ def _update_item(self, item, caller=None, source=None, dest=None):
def _send_cmd(self, payload):
try:
- logger.debug("Sending request: {0}".format(payload))
+ self._logger.debug("Sending request: {0}".format(payload))
headers = {'Content-type': 'application/json', 'Accept': 'text/plain'}
response = requests.post(self._broker_url, data=json.dumps(payload), headers=headers)
@@ -457,16 +510,16 @@ def _send_cmd(self, payload):
html_end = "