Git Product home page Git Product logo

soco-cli's People

Contributors

pwt avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

soco-cli's Issues

Interactive shell sequential commands: CTRL-C should cancel all commands

If you issue a sequential command in the interactive shell such as:

> wait_until 10:00 : line_in on

and then issue a CTRL-C to cancel the command, the wait_until is cancelled, but the line_in on will still execute. The desired behaviour is that the whole sequential command is cancelled.

Group Volume Inconsistencies

First of all, thanks for such a great utility! This could be on my end, but trying to confirm how to validate.

I'm using cached discovery of my speakers (6 in total) and am trying to implement some light automation to set the group, and normalize the volume within the group.

I'm using Python 3.10.2 via a clean venv:

# pip list
Package            Version
------------------ ---------
anyio              3.5.0
appdirs            1.4.4
asgiref            3.5.0
certifi            2021.10.8
charset-normalizer 2.0.11
click              8.0.3
fastapi            0.73.0
h11                0.13.0
idna               3.3
ifaddr             0.1.7
pip                22.0.3
pydantic           1.9.0
rangehttpserver    1.2.0
requests           2.27.1
setuptools         60.7.1
sniffio            1.2.0
soco               0.26.0
soco-cli           0.4.36
starlette          0.17.1
tabulate           0.8.9
typing_extensions  4.0.1
urllib3            1.26.8
uvicorn            0.17.3
xmltodict          0.12.0

The problem I'm having is that if I leverage:
# sonos -l 10.x.x.65 group-vol 25

...I'd expect the group bound to all be set to a volume level of 25.

I'm starting out with this:

# sonos -l _all_ sysinfo 
Office-3: 
Report generated on: 2022-02-03 21:52 UTC (Thursday)

Zone Name    IP Address    Visible    CoOrd    CoOrd IP      Vol.   Mute    State    Model Name    Model No.    HW Version    SW Version
-----------  ------------  ---------  -------  -----------  ------  ------  -------  ------------  -----------  ------------  -----------------
Dining-5     10.x.x.64   Yes        No       10.x.x.65    7     Off     STOPPED  Play:5        S6           1.13.1.7-1.2  67.1-25031 (14.0)
Kitchen-N-1  10.x.x.63   Yes        No       10.x.x.65    4     Off     STOPPED  Play:1        S1           1.8.3.7-1.0   67.1-25031 (14.0)
Kitchen-S-1  10.x.x.60   Yes        No       10.x.x.65    4     Off     STOPPED  Play:1        S1           1.8.3.7-1.0   67.1-25031 (14.0)
Living-1     10.x.x.61   Yes        No       10.x.x.65    4     Off     STOPPED  Play:1        S1           1.8.3.7-1.0   67.1-25031 (14.0)
Master-1     10.x.x.62   Yes        No       10.x.x.65    4     Off     STOPPED  Play:1        S1           1.8.3.7-1.0   67.1-25031 (14.0)
Office-3     10.x.x.65   Yes        Yes                     7     Off     STOPPED  Play:3        S3           1.8.1.3-1.0   67.1-25031 (14.0)

Sonos model numbers present: S1, S3, S6.
Device counts: 6 total Sonos device(s), 3 unique model(s).

But when issue this:
# sonos -l 10.x.x.65 group_vol 50

And then reissue to validate volume levels I end up with something far more random like this:

# sonos -l _all_ sysinfo           
Office-3: 
Report generated on: 2022-02-03 21:55 UTC (Thursday)

Zone Name    IP Address    Visible    CoOrd    CoOrd IP      Vol.   Mute    State    Model Name    Model No.    HW Version    SW Version
-----------  ------------  ---------  -------  -----------  ------  ------  -------  ------------  -----------  ------------  -----------------
Dining-5     10.x.x.64   Yes        No       10.x.x.65    56    Off     STOPPED  Play:5        S6           1.13.1.7-1.2  67.1-25031 (14.0)
Kitchen-N-1  10.x.x.63   Yes        No       10.x.x.65    46    Off     STOPPED  Play:1        S1           1.8.3.7-1.0   67.1-25031 (14.0)
Kitchen-S-1  10.x.x.60   Yes        No       10.x.x.65    46    Off     STOPPED  Play:1        S1           1.8.3.7-1.0   67.1-25031 (14.0)
Living-1     10.x.x.61   Yes        No       10.x.x.65    46    Off     STOPPED  Play:1        S1           1.8.3.7-1.0   67.1-25031 (14.0)
Master-1     10.x.x.62   Yes        No       10.x.x.65    46    Off     STOPPED  Play:1        S1           1.8.3.7-1.0   67.1-25031 (14.0)
Office-3     10.x.x.65   Yes        Yes                     59    Off     STOPPED  Play:3        S3           1.8.1.3-1.0   67.1-25031 (14.0)

Sonos model numbers present: S1, S3, S6.
Device counts: 6 total Sonos device(s), 3 unique model(s).

If I go about this individually - the volumes fall in line as expected:

# sonos -l 10.x.x.60 vol 50
# sonos -l 10.x.x.61 vol 50
# sonos -l 10.x.x.62 vol 50
# sonos -l 10.x.x.63 vol 50
# sonos -l 10.x.x.64 vol 50
# sonos -l 10.x.x.65 vol 50

And now look at the system output again, I get what I'm after:

# sonos -l _all_ sysinfo     
Office-3: 
Report generated on: 2022-02-03 21:58 UTC (Thursday)

Zone Name    IP Address    Visible    CoOrd    CoOrd IP      Vol.   Mute    State    Model Name    Model No.    HW Version    SW Version
-----------  ------------  ---------  -------  -----------  ------  ------  -------  ------------  -----------  ------------  -----------------
Dining-5     10.x.x.64   Yes        No       10.x.x.65    50    Off     STOPPED  Play:5        S6           1.13.1.7-1.2  67.1-25031 (14.0)
Kitchen-N-1  10.x.x..63   Yes        No       10.x.x.65    50    Off     STOPPED  Play:1        S1           1.8.3.7-1.0   67.1-25031 (14.0)
Kitchen-S-1  10.x.x.60   Yes        No       10.x.x.65    50    Off     STOPPED  Play:1        S1           1.8.3.7-1.0   67.1-25031 (14.0)
Living-1     10.x.x.61   Yes        No       10.x.x.65    50    Off     STOPPED  Play:1        S1           1.8.3.7-1.0   67.1-25031 (14.0)
Master-1     10.x.x.62   Yes        No       10.x.x.65    50    Off     STOPPED  Play:1        S1           1.8.3.7-1.0   67.1-25031 (14.0)
Office-3     10.x.x.65   Yes        Yes                     50    Off     STOPPED  Play:3        S3           1.8.1.3-1.0   67.1-25031 (14.0)

Sonos model numbers present: S1, S3, S6.
Device counts: 6 total Sonos device(s), 3 unique model(s).

Curious what I'm doing incorrectly, I haven't looked at the actual code yet - but wanted to see if this may be expected for some reason before I do.

Thanks, again, for the awesome utility!

Action 'add_uri_to_queue' unlisted in docs

Dear maintainers,

Thank you so much for this tool, it's really helpful and saves me time with my custom scripts :)

Before switching to this app,
A feature that I often used in my custom script is add_uri_to_queue, found in SoCo Github repo, docs here:
https://soco.readthedocs.io/en/latest/api/soco.core.html?highlight=_uri#soco.core.SoCo.add_uri_to_queue

I was unable to find this in the README.md however it does work and is in the action_processor.py

Could you document this feature as it really helps me with creating a daily URI playlist for Sonos and might help others as well.

Thank you!

Problems with deleting playlists

Hi,

I'm a but stumped...
Not sure if I'm using it wrong or if I found an error...

soco lp (list playlists):
1: 1 - Pop l
--- snipp ---
19: WW II - The Pacific
20: xxx
So there is a playlist named xxx:

soco delete_playlist xxx
Error: Playlist 'xxx' not found.

This should work, right?

Debug output is attached...
error.txt

Playing youtube mp4/aac audio stream through play_uri method not working 🔈

Hi,
I started playing around with soco_cli as I am looking for a way to stream youtube audio to my sonos speakers without requiring airplay on my computer. I have successfully managed to play demo mp3 and aac radio streams through the cli's play_uri method. Unfortunately, when trying to play aac encoded audio-only streams by urls taken from youtube-dl I get an error message on the respective host not being accessible.

One such URL is looking like this: https://rr4---sn-i5heen7s.googlevideo.com/videoplayback?expire=1666638001&ei=UIxWY_G1Kfzgx_APkueu-AU&ip=77.3.50.25&id=o-ANVFLH4TUmObAGEXWT00SzJtgJc2O9b0fJyE71QsOJoI&itag=140&source=youtube&requiressl=yes&mh=u4&mm=31%2C26&mn=sn-i5heen7s%2Csn-5goeen76&ms=au%2Conr&mv=m&mvi=4&pl=18&initcwndbps=1175000&vprv=1&mime=audio%2Fmp4&ns=FxlcKbwXSp9ERsyYHX45s1sI&gir=yes&clen=468901609&dur=28973.323&lmt=1653505664922520&mt=1666616255&fvip=2&keepalive=yes&fexp=24001373%2C24007246&c=WEB&txp=4531432&n=Hp0UHG_T1c0-cXOZE&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRQIhAJPJRlzT5in9psVC8L28dcQruOjg689OfP-X4Z-EpQ7HAiAa8drJ_CSZGizLXsfV7Txaj1NSzrbsXJD29YgjhDMaJQ%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRgIhAM8s-MbdJ2J9yZAFKSSbwoDiLEi4om5npFNyPMNdmrc6AiEA53Pgni4PrDcZCJuRObthpHzE7dluJ-e0vHDcoD8D2_g%3D

Expires this evening local time

Does anyone have a clue on how I might get my speakers to play the stream?
Thanks! ✌️

soco-cli commands only discover one out of two active sonos devices

Hi everyone,

I'm runnung two Sonos devices (two IKEA Symfonisk speakers: "Lounge" with IP address 192.168.2.213 and "Right" with IP address 192.168.2.194). Both speakers and the Ubuntu PC running soco-cli are on the same ethernet switch.

I can control them individually without problems using soco-cli commands (i.e. sonos and sonos-discover). Thanks for that :-)

As long as only one of the speakers is switched on, I can also use the room/zone name to control the active speaker.

As soon as I switch on both speakers, the soco-cli programs only discover one of the two speakers. It looks like they randomly choose the one that is found...
I would not care that much... but I would like to pair the two devices to build a stereo speaker. This only works with zone names but not with IP-addresses.
BTW: I also tried the same using the -n parameter (from -n 5 upto -n 20): no difference...

I could not find any issues on github w.r.t. soco or soco-cli that I could relate to the problem described.
Any ideas?

Below please find a sample session including a sonos-discover with debug output. Initially, the "Right" speaker was sitched off. I switched it on after executing the first command (i.e. sonos Lounge volume).

Thanks and all the best,

Thomas

drtjs@myhost:$ sonos Lounge volume
10
drtjs@myhost:
$ sonos 192.168.2.213 volume
10
drtjs@myhost:$ sonos 192.168.2.194 volume
10
drtjs@myhost:
$ sonos --version
soco-cli version: 0.4.52
soco version: 0.28.1
python version: 3.8.10
command path: /home/drtjs/.local/bin/sonos
drtjs@myhost:~$ sonos-discover

Sonos Household:

Room/Zone Name IP Address Device Model Visibility SW Version


Right 192.168.2.194 SYMFONISK Visible 9.3

1 Sonos Household(s) found
1 Sonos device(s) found

Saved speaker data at: /home/drtjs/.soco-cli/speakers_v2.pickle

drtjs@myhost:~$ sonos-discover

Sonos Household:

Room/Zone Name IP Address Device Model Visibility SW Version


Lounge 192.168.2.213 SYMFONISK Visible 9.3

1 Sonos Household(s) found
1 Sonos device(s) found

Saved speaker data at: /home/drtjs/.soco-cli/speakers_v2.pickle

drtjs@myhost:~$ sonos-discover

Sonos Household:

Room/Zone Name IP Address Device Model Visibility SW Version


Right 192.168.2.194 SYMFONISK Visible 9.3

1 Sonos Household(s) found
1 Sonos device(s) found

Saved speaker data at: /home/drtjs/.soco-cli/speakers_v2.pickle

drtjs@myhost:~$ sonos-discover

Sonos Household:

Room/Zone Name IP Address Device Model Visibility SW Version


Right 192.168.2.194 SYMFONISK Visible 9.3

1 Sonos Household(s) found
1 Sonos device(s) found

Saved speaker data at: /home/drtjs/.soco-cli/speakers_v2.pickle

drtjs@myhost:~$ sonos-discover --log=debug
2023-01-04 11:16:56,300 discovery.py:651 - _find_ipv4_networks() - Set of networks to search: {IPv4Network('192.168.2.0/24')}
2023-01-04 11:16:56,305 discovery.py:747 - _sonos_scan_worker_thread() - Scanning port 192.168.2.194:1400
2023-01-04 11:16:56,305 discovery.py:750 - _sonos_scan_worker_thread() - Found open port 1400 at IP '192.168.2.194'
2023-01-04 11:16:56,305 core.py:371 - init() - Created SoCo instance for ip: 192.168.2.194
2023-01-04 11:16:56,306 services.py:220 - getattr() - Dispatching method GetHouseholdID
2023-01-04 11:16:56,306 services.py:470 - send_command() - Request timeout set to 20.0
2023-01-04 11:16:56,310 connectionpool.py:221 - _new_conn() - Starting new HTTP connection (1): 192.168.2.194:1400
2023-01-04 11:16:56,312 discovery.py:747 - _sonos_scan_worker_thread() - Scanning port 192.168.2.213:1400
2023-01-04 11:16:56,312 discovery.py:750 - _sonos_scan_worker_thread() - Found open port 1400 at IP '192.168.2.213'
2023-01-04 11:16:56,312 core.py:371 - init() - Created SoCo instance for ip: 192.168.2.213
2023-01-04 11:16:56,312 services.py:220 - getattr() - Dispatching method GetHouseholdID
2023-01-04 11:16:56,312 services.py:470 - send_command() - Request timeout set to 20.0
2023-01-04 11:16:56,313 connectionpool.py:221 - _new_conn() - Starting new HTTP connection (1): 192.168.2.213:1400
2023-01-04 11:16:56,323 discovery.py:747 - _sonos_scan_worker_thread() - Scanning port 192.168.2.0:1400
2023-01-04 11:16:56,325 discovery.py:747 - _sonos_scan_worker_thread() - Scanning port 192.168.2.255:1400
2023-01-04 11:16:56,332 discovery.py:747 - _sonos_scan_worker_thread() - Scanning port 192.168.2.197:1400
2023-01-04 11:16:56,342 discovery.py:373 - scan_network() - Created 256 scanner threads
2023-01-04 11:16:56,393 discovery.py:747 - _sonos_scan_worker_thread() - Scanning port 192.168.2.200:1400
2023-01-04 11:16:56,393 discovery.py:747 - _sonos_scan_worker_thread() - Scanning port 192.168.2.1:1400
2023-01-04 11:16:56,394 discovery.py:747 - _sonos_scan_worker_thread() - Scanning port 192.168.2.8:1400
2023-01-04 11:16:56,395 connectionpool.py:428 - _make_request() - http://192.168.2.194:1400 "GET /xml/DeviceProperties1.xml HTTP/1.1" 200 19120
2023-01-04 11:16:56,396 connectionpool.py:428 - _make_request() - http://192.168.2.213:1400 "GET /xml/DeviceProperties1.xml HTTP/1.1" 200 19120
2023-01-04 11:16:56,397 discovery.py:747 - _sonos_scan_worker_thread() - Scanning port 192.168.2.203:1400
2023-01-04 11:16:56,397 discovery.py:747 - _sonos_scan_worker_thread() - Scanning port 192.168.2.204:1400
2023-01-04 11:16:56,397 discovery.py:747 - _sonos_scan_worker_thread() - Scanning port 192.168.2.128:1400
2023-01-04 11:16:56,398 discovery.py:747 - _sonos_scan_worker_thread() - Scanning port 192.168.2.2:1400
2023-01-04 11:16:56,401 services.py:483 - send_command() - Sending GetHouseholdID [] to 192.168.2.194
2023-01-04 11:16:56,402 discovery.py:747 - _sonos_scan_worker_thread() - Scanning port 192.168.2.6:1400
2023-01-04 11:16:56,404 services.py:483 - send_command() - Sending GetHouseholdID [] to 192.168.2.213
2023-01-04 11:16:56,413 services.py:484 - send_command() - Sending {'Content-Type': 'text/xml; charset="utf-8"', 'SOAPACTION': 'urn:schemas-upnp-org:service:DeviceProperties:1#GetHouseholdID'},
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<s:Body>
<u:GetHouseholdID xmlns:u="urn:schemas-upnp-org:service:DeviceProperties:1"/>
</s:Body>
</s:Envelope>

2023-01-04 11:16:56,414 services.py:484 - send_command() - Sending {'Content-Type': 'text/xml; charset="utf-8"', 'SOAPACTION': 'urn:schemas-upnp-org:service:DeviceProperties:1#GetHouseholdID'},
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<s:Body>
<u:GetHouseholdID xmlns:u="urn:schemas-upnp-org:service:DeviceProperties:1"/>
</s:Body>
</s:Envelope>

2023-01-04 11:16:56,417 connectionpool.py:221 - _new_conn() - Starting new HTTP connection (1): 192.168.2.213:1400
2023-01-04 11:16:56,419 connectionpool.py:221 - _new_conn() - Starting new HTTP connection (1): 192.168.2.194:1400
2023-01-04 11:16:56,424 connectionpool.py:428 - _make_request() - http://192.168.2.213:1400 "POST /DeviceProperties/Control HTTP/1.1" 200 306
2023-01-04 11:16:56,425 services.py:493 - send_command() - Received {'CONTENT-LENGTH': '306', 'CONTENT-TYPE': 'text/xml; charset="utf-8"', 'EXT': '', 'Server': 'Linux UPnP/1.0 Sonos/47.2-59120 (ZPS21)', 'Connection': 'close'}, <s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><s:Body><u:GetHouseholdIDResponse xmlns:u="urn:schemas-upnp-org:service:DeviceProperties:1"></u:GetHouseholdIDResponse></s:Body></s:Envelope>
2023-01-04 11:16:56,425 services.py:495 - send_command() - Received status 200 from 192.168.2.213
2023-01-04 11:16:56,425 services.py:220 - getattr() - Dispatching method GetZoneGroupState
2023-01-04 11:16:56,425 services.py:470 - send_command() - Request timeout set to 20.0
2023-01-04 11:16:56,428 connectionpool.py:221 - _new_conn() - Starting new HTTP connection (1): 192.168.2.213:1400
2023-01-04 11:16:56,428 connectionpool.py:428 - _make_request() - http://192.168.2.194:1400 "POST /DeviceProperties/Control HTTP/1.1" 200 306
2023-01-04 11:16:56,429 services.py:493 - send_command() - Received {'CONTENT-LENGTH': '306', 'CONTENT-TYPE': 'text/xml; charset="utf-8"', 'EXT': '', 'Server': 'Linux UPnP/1.0 Sonos/47.2-59120 (ZPS21)', 'Connection': 'close'}, <s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><s:Body><u:GetHouseholdIDResponse xmlns:u="urn:schemas-upnp-org:service:DeviceProperties:1"></u:GetHouseholdIDResponse></s:Body></s:Envelope>
2023-01-04 11:16:56,430 services.py:495 - send_command() - Received status 200 from 192.168.2.194
2023-01-04 11:16:56,430 services.py:220 - getattr() - Dispatching method GetZoneGroupState
2023-01-04 11:16:56,430 services.py:470 - send_command() - Request timeout set to 20.0
2023-01-04 11:16:56,432 connectionpool.py:221 - _new_conn() - Starting new HTTP connection (1): 192.168.2.194:1400
2023-01-04 11:16:56,437 connectionpool.py:428 - _make_request() - http://192.168.2.194:1400 "GET /xml/ZoneGroupTopology1.xml HTTP/1.1" 200 8276
2023-01-04 11:16:56,438 connectionpool.py:428 - _make_request() - http://192.168.2.213:1400 "GET /xml/ZoneGroupTopology1.xml HTTP/1.1" 200 8276
2023-01-04 11:16:56,440 services.py:483 - send_command() - Sending GetZoneGroupState [] to 192.168.2.194
2023-01-04 11:16:56,441 services.py:484 - send_command() - Sending {'Content-Type': 'text/xml; charset="utf-8"', 'SOAPACTION': 'urn:schemas-upnp-org:service:ZoneGroupTopology:1#GetZoneGroupState'},
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<s:Body>
<u:GetZoneGroupState xmlns:u="urn:schemas-upnp-org:service:ZoneGroupTopology:1"/>
</s:Body>
</s:Envelope>

2023-01-04 11:16:56,443 connectionpool.py:221 - _new_conn() - Starting new HTTP connection (1): 192.168.2.194:1400
2023-01-04 11:16:56,445 services.py:483 - send_command() - Sending GetZoneGroupState [] to 192.168.2.213
2023-01-04 11:16:56,445 services.py:484 - send_command() - Sending {'Content-Type': 'text/xml; charset="utf-8"', 'SOAPACTION': 'urn:schemas-upnp-org:service:ZoneGroupTopology:1#GetZoneGroupState'},
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<s:Body>
<u:GetZoneGroupState xmlns:u="urn:schemas-upnp-org:service:ZoneGroupTopology:1"/>
</s:Body>
</s:Envelope>

2023-01-04 11:16:56,447 connectionpool.py:221 - _new_conn() - Starting new HTTP connection (1): 192.168.2.213:1400
2023-01-04 11:16:56,451 connectionpool.py:428 - _make_request() - http://192.168.2.194:1400 "POST /ZoneGroupTopology/Control HTTP/1.1" 200 1254
2023-01-04 11:16:56,452 services.py:493 - send_command() - Received {'CONTENT-LENGTH': '1254', 'CONTENT-TYPE': 'text/xml; charset="utf-8"', 'EXT': '', 'Server': 'Linux UPnP/1.0 Sonos/47.2-59120 (ZPS21)', 'Connection': 'close'}, <s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><s:Body><u:GetZoneGroupStateResponse xmlns:u="urn:schemas-upnp-org:service:ZoneGroupTopology:1"><ZoneGroups><ZoneGroup Coordinator="RINCON_347E5CF3316801400" ID="RINCON_347E5CF3316801400:3713604349"><ZoneGroupMember UUID="RINCON_347E5CF3316801400" Location="http://192.168.2.194:1400/xml/device_description.xml" ZoneName="Right" Icon="" Configuration="1" SoftwareVersion="47.2-59120" MinCompatibleVersion="46.0-00000" LegacyCompatibleVersion="36.0-00000" BootSeq="9" TVConfigurationError="0" HdmiCecAvailable="0" WirelessMode="0" WirelessLeafOnly="0" HasConfiguredSSID="0" ChannelFreq="2412" BehindWifiExtender="0" WifiEnabled="1" Orientation="0" RoomCalibrationState="4" SecureRegState="1" VoiceState="0" AirPlayEnabled="0" IdleState="1"/></ZoneGroup></ZoneGroups></u:GetZoneGroupStateResponse></s:Body></s:Envelope>
2023-01-04 11:16:56,452 services.py:495 - send_command() - Received status 200 from 192.168.2.194
2023-01-04 11:16:56,453 connectionpool.py:428 - _make_request() - http://192.168.2.213:1400 "POST /ZoneGroupTopology/Control HTTP/1.1" 200 1256
2023-01-04 11:16:56,454 services.py:493 - send_command() - Received {'CONTENT-LENGTH': '1256', 'CONTENT-TYPE': 'text/xml; charset="utf-8"', 'EXT': '', 'Server': 'Linux UPnP/1.0 Sonos/47.2-59120 (ZPS21)', 'Connection': 'close'}, <s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><s:Body><u:GetZoneGroupStateResponse xmlns:u="urn:schemas-upnp-org:service:ZoneGroupTopology:1"><ZoneGroups><ZoneGroup Coordinator="RINCON_347E5C3314FC01400" ID="RINCON_347E5C3314FC01400:3650021371"><ZoneGroupMember UUID="RINCON_347E5C3314FC01400" Location="http://192.168.2.213:1400/xml/device_description.xml" ZoneName="Lounge" Icon="" Configuration="1" SoftwareVersion="47.2-59120" MinCompatibleVersion="46.0-00000" LegacyCompatibleVersion="36.0-00000" BootSeq="26" TVConfigurationError="0" HdmiCecAvailable="0" WirelessMode="0" WirelessLeafOnly="0" HasConfiguredSSID="0" ChannelFreq="2412" BehindWifiExtender="0" WifiEnabled="1" Orientation="0" RoomCalibrationState="4" SecureRegState="1" VoiceState="0" AirPlayEnabled="0" IdleState="0"/></ZoneGroup></ZoneGroups></u:GetZoneGroupStateResponse></s:Body></s:Envelope>
2023-01-04 11:16:56,454 services.py:495 - send_command() - Received status 200 from 192.168.2.213
2023-01-04 11:16:56,455 zonegroupstate.py:176 - process_payload() - Updating ZGS with poll payload from 192.168.2.213 (1/2 processed)
2023-01-04 11:16:56,456 zonegroupstate.py:164 - update_cache() - Setting ZGS cache to 5s
2023-01-04 11:16:56,456 discovery.py:752 - _sonos_scan_worker_thread() - Confirmed Sonos device at IP '192.168.2.213'
2023-01-04 11:16:56,456 zonegroupstate.py:176 - process_payload() - Updating ZGS with poll payload from 192.168.2.194 (2/2 processed)
2023-01-04 11:16:56,457 zonegroupstate.py:164 - update_cache() - Setting ZGS cache to 5s
2023-01-04 11:16:56,457 discovery.py:752 - _sonos_scan_worker_thread() - Confirmed Sonos device at IP '192.168.2.194'
2023-01-04 11:16:56,510 discovery.py:747 - _sonos_scan_worker_thread() - Scanning port 192.168.2.223:1400

LOTS OF SCAN WORKER THREAD LINES DELETED w.r.t scanning ports on IP-addresses where there were no sonos devices

2023-01-04 11:16:57,343 discovery.py:747 - _sonos_scan_worker_thread() - Scanning port 192.168.2.198:1400
2023-01-04 11:16:57,353 discovery.py:378 - scan_network() - All 256 scanner threads terminated
2023-01-04 11:16:57,353 zonegroupstate.py:136 - poll() - Cache still active (GetZoneGroupState) during poll for 192.168.2.213
2023-01-04 11:16:57,353 zonegroupstate.py:136 - poll() - Cache still active (GetZoneGroupState) during poll for 192.168.2.194
2023-01-04 11:16:57,353 discovery.py:399 - scan_network() - Include_invisible: True | multi_household: True | 1 Zones: {SoCo("192.168.2.194")}
2023-01-04 11:16:57,354 speakers.py:208 - get_sonos_device_data() - Querying device at 192.168.2.194
2023-01-04 11:16:57,356 connectionpool.py:221 - _new_conn() - Starting new HTTP connection (1): 192.168.2.194:1400
2023-01-04 11:16:57,370 connectionpool.py:428 - _make_request() - http://192.168.2.194:1400 "GET /xml/device_description.xml HTTP/1.1" 200 None
2023-01-04 11:16:57,373 zonegroupstate.py:136 - poll() - Cache still active (GetZoneGroupState) during poll for 192.168.2.194

Sonos Household:

Room/Zone Name IP Address Device Model Visibility SW Version


Right 192.168.2.194 SYMFONISK Visible 9.3

1 Sonos Household(s) found
1 Sonos device(s) found

Saved speaker data at: /home/drtjs/.soco-cli/speakers_v2.pickle

drtjs@myhost:~$

New 'surround_enabled' option giving error

Hi,

I've just upgraded to version 0.4.26 and testing new 'sub_enabled' and 'surround_enabled'. The first of these works fine, but the second errors. I'm expecting both to give the answer as 'on'. Output from both commands with log set to info:

sonos --log info -l Lounge sub_enabled

2021-12-14 10:42:34,784 sonos.py:130 - main() - Setting up handlers for: [<Signals.SIGINT: 2>, <Signals.SIGTERM: 15>]
2021-12-14 10:42:34,784 sonos.py:237 - main() - No 'SPKR' environment variable set
2021-12-14 10:42:34,785 sonos.py:260 - main() - Found 1 action sequence(s): [['Lounge', 'sub_enabled']]
2021-12-14 10:42:34,785 utils.py:582 - get_speaker() - Using local speaker list
2021-12-14 10:42:34,785 match_speaker_names.py:18 - speaker_name_matches() - Found exact speaker name match for 'Lounge'
2021-12-14 10:42:34,786 sonos.py:470 - main() - Invoking 'run_command' with '<SoCo object at ip 192.168.0.233> sub_enabled ...'
2021-12-14 10:42:34,858 api.py:125 - run_command() - Return value: (0, 'on', '')
on

sonos --log info -l Lounge surround_enable`

2021-12-14 10:44:38,413 sonos.py:130 - main() - Setting up handlers for: [<Signals.SIGINT: 2>, <Signals.SIGTERM: 15>]
2021-12-14 10:44:38,413 sonos.py:237 - main() - No 'SPKR' environment variable set
2021-12-14 10:44:38,413 sonos.py:260 - main() - Found 1 action sequence(s): [['Lounge', 'surround_enabled']]
2021-12-14 10:44:38,414 utils.py:582 - get_speaker() - Using local speaker list
2021-12-14 10:44:38,414 match_speaker_names.py:18 - speaker_name_matches() - Found exact speaker name match for 'Lounge'
2021-12-14 10:44:38,415 sonos.py:470 - main() - Invoking 'run_command' with '<SoCo object at ip 192.168.0.233> surround_enabled ...'
2021-12-14 10:44:38,415 api.py:88 - run_command() - Exception: 'tuple' object has no attribute 'switch_to_coordinator'
2021-12-14 10:44:38,415 api.py:125 - run_command() - Return value: (1, '', "Error: 'tuple' object has no attribute 'switch_to_coordinator'")
Error: 'tuple' object has no attribute 'switch_to_coordinator'

sonos --version

soco-cli version: 0.4.26
soco version:     0.25.0
python version:   3.9.1

Thanks,
Richard

Sonos Controls from Raspberry Pi

Hello,

I am trying to control Sonos from Raspberry Pi. I am able to control the volume but not other functions. Not sure what I am missing.
I am trying to control status lights, switch the Sonos connect (named Office) to line-in and also select (everywhere) from the group to play music to all speakers.

Below is my script:


import soco
from soco from SoCo
from soco_cli import api
from soco_cli import action_processor as ap
from soco_cli.api import run_command

Office = SoCo('172.17.16.133')
Office.volume = 10
Office.status_light = 'on'
Office.line_in = 'switch_to_line_in'

Can you please help with this?

audio_format giving error

Hi,

I am getting an error with the new audio_format action. Error is:

Error: 'SoCo' object has no attribute 'audio_input_format'

I think it's a simple typo at line 3259 of action_processor.py, which should be calling the soco function 'soundbar_audio_input_format' rather than 'audio_input_format'

For info my versions are:

sonos --version
soco-cli version: 0.4.28
soco version: 0.25.2
python version: 3.10.1

Error message when queueing URIs

I have my fileserver and filename-to-URI translator working. Adding URIs to the queue works, except that, when loading to an empty queue, the first URI winds up at the end, with the others being loaded in the correct sequence ahead of it. Should I always force the fueue position instead of "next"? That worked to get them in order, and I can do a little scripting to retrieve the queue and figure out the right numbers. Meanwhile, at every invocation of sonos add_uri_to_queue, I get one of these error messages. If you let me know where you think they might originate, I can add some debugging code to try to find the cause.

ran@delve:trackserver$ for i in cat cs_uris;do sonos add_uri_to_queue $i next;done
<function zero_parameters..wrapper at 0x7fe2a85809d0>
<function zero_parameters..wrapper at 0x7f787d97b9d0>
<function zero_parameters..wrapper at 0x7f7966cd9a60>
<function zero_parameters..wrapper at 0x7ff2d9330a60>
<function zero_parameters..wrapper at 0x7f8b053c99d0>
<function zero_parameters..wrapper at 0x7f09344039d0>
<function zero_parameters..wrapper at 0x7f45a108e9d0>
<function zero_parameters..wrapper at 0x7f115cb779d0>
<function zero_parameters..wrapper at 0x7fb2ec2f09d0>
<function zero_parameters..wrapper at 0x7faf7d4269d0>
<function zero_parameters..wrapper at 0x7f5164c47a60>
<function zero_parameters..wrapper at 0x7f7e9368aa60>

Here are the URIs. I don't see anything odd about them. Maybe the numeric IP address instead of a host name??
cs_uris.txt

Python on Windows

Hi

I tried soco-cli in a raspberry.. worked perfectly
Not used to Python in windows, I have setup the environment and installed the soco-cli package..

First I navigate to the Python package repository on my machine..

cd C:\Users\Username\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.9_qbz5n2kfra8p0\LocalCache\local-packages\Python39\site-packages

From here, I can run

Python3 discovery.py
and it outputs my setup..

Sonos Household: Sonos__xxxxmyuid_

Room/Zone Name    IP Address      Device Model    Visibility    SW Version
----------------  --------------  --------------  ------------  ------------
Anton             192.168.xx.122  One SL          Visible       14.0
Bureau            192.168.xx.108  Play:1          Visible       14.0
Kitchen           192.168.xx.170  One             Visible       14.0
Living Room       192.168.xx.119  Play:1          Visible       14.0
Parents           192.168.xx.176  One             Visible       14.0

1 Sonos Household(s) found
5 Sonos device(s) found

Saved speaker data at: C:\Users\Username/.soco-cli/speakers_v2.pickle

But then I try to run sonos SPEAKER play_file myfile.mp3 as I did on the raspbian..

python3 sonos.py Kitchen play_file myfile.mp3

Output

Traceback (most recent call last):
  File "C:\Users\Username\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.9_qbz5n2kfra8p0\LocalCache\local-packages\Python39\site-packages\soco_cli\sonos.py", line 36, in <module>
    from .wait_actions import process_wait
ImportError: attempted relative import with no known parent package

Would you have any idea on how to turn this around ?
Thanks

Thanks for making this splendid tool

It takes a lot of effort to integrate Sonos API just to make an alternative for people who live in the command line.
Thanks for making this splendid tool and for all the hard work you put in. ☺️

What is the "local library"?

The README makes references to a "local library" as distinct from the "Sonos library". Is this something created on the local filesystem by the Sonos software?

I'm a Linux user, and thought (mistakenly, apparently) that it referred to my filesystem, but I get "not found" errors when I try to use local files as track and playlist names.

Issue with 'play_fav_radio_station_no' function

Hi,

play_fav_radio_station_no don't always play the right radio:

sonos -l Salon favourite_radio_stations

    1: Amazing Blues
    2: France Inter 95.9 (Émissions-débats France)
    3: K6 FM 101.6 (Musique Locale)
    4: RTL 104.2 (Monde)
    5: Radio Classique 105.8 (Classique)
    6: Rire et Chansons 91.8 (Comédie)
    7: SomaFM: Indie Pop Rocks!
    8: franceinfo 101.2 (Émissions-débats France)

sonos -l Salon play_fav_radio_station_no 1    # play 1 - OK
sonos -l Salon play_fav_radio_station_no 3    # play 8 - NOK

play_fav_radio_station_no 1 -> play 1
play_fav_radio_station_no 2 -> play 2
play_fav_radio_station_no 3 -> play 8
play_fav_radio_station_no 4 -> play 3
play_fav_radio_station_no 5 -> play 5
play_fav_radio_station_no 6 -> play 6
play_fav_radio_station_no 7 -> play 4
play_fav_radio_station_no 8 -> play 7

"Error: 'NoneType' object is not iterable" using sonos-discover

Hello Team!

I am getting the following error while trying to utilize sonos-discover to use soco-cli.

Error: 'NoneType' object is not iterable

(soco-cli)  ~/projects/temp/soco-cli$ sonos-discover -t 256 -n 1.0 -m 16
Error: 'NoneType' object is not iterable

(soco-cli)  ~/projects/temp/soco-cli$ sonos-discover
Error: 'NoneType' object is not iterable

On the first command I was using -m 16 due to my sonos devices being on a different vlan than my computer. I was not able to find a way to manually create a cached set of devices to use in soco-cli.

I setup my env the following way

pipenv install --python 3
pipevn shell
pipenv install soco-cli

Python = Python 3.8.6
OS = POP_OS 20.10

What else can I provide or dig into looking at this?

Error: 1: list index out of range | 2: list index out of range when trying playing podcast.

First, thanks for this wonderful tool. I've added several alias in my .zshrc to quickly control my Sonos One.

I've added a podcast (French podcast: Le moment Meurice) from the Pocket Casts app in my fav list:

❯ sonos Salon list_favs

    1: Flow, votre mix personnalisé (Deezer)
    2: France Inter 95.9 (Émissions-débats France)
    3: K6 FM 101.6 (Musique Locale)
    4: Le moment Meurice

I get this error when I try to play the podcast:

❯ sonos Salon play_fav 'Le moment Meurice'
Error: 1: list index out of range | 2: list index out of range

❯ sonos Salon play_fav "Meurice"
Error: 1: list index out of range | 2: list index out of range

Maybe there is another way to play podcasts from solo-cli ?

Edit: try adding the podcast from TuneIn, same error.

Adding some usefull commands

Hello, thank you for sonos-cli I love it !

It would be so nice if you could add some commands like this one :

  • if_coordinator : I must do : if [[ $(sonos $speaker if_playing info | grep 'is_coordinator = True') ]] then; for example for grouping to the coordinator but it take a long time to execute

  • if_queued and if_not_queued: For example for not adding to queue every time we want to play the queue : `sonos $speaker if_queued play_queue : $speaker if_not_queued add_playlist_to queue playlist : play_queue

  • rel_subgain : I must do bass_sub_level=$(sonos $speaker sub_gain); new_bass_sub_level=$((bass_sub_level-5)); sonos $speaker sub_gain $new_bass_sub_level" and it's very long too

  • rel_treble : I must do treble_level=$(sonos $speaker treble); new_treble_level=$((treble_level-5)); sonos $speaker treble $new_treble_level

  • rel_bass

And also some commands for having the state of all speaker :
For example :

  • info_state :
    which return for example :
speaker_1 :  is_coordinator = False
Playback is in progress: Artist: xx Album: Untitled (xxx) Title: xx 
Volume : 60
Treble : 0
Bass : 0
Balance : 0
Loudness : on

speaker_2 :  is_coordinator = True
Using Line In (state: PAUSED_PLAYBACK
Volume : 60
Treble : 0
Bass : -3
Balance : -25
Loudness : off

speaker_3 : Using Line In (state: PAUSED_PLAYBACK)
Volume : 60
Treble : 0
Bass : -3
Balance : 25
Loudness : on

speaker_4 : Using Line In (state: PAUSED_PLAYBACK)
Volume : 25
Treble : -1
Bass : 0
Balance : 0
Loudness : on

speaker_5 : Playback is stopped
Volume : 25
Treble : 0
Bass : 0
Balance : 0
Loudness : on

speaker_6 : Playback is stopped: Channel: Radio.com Elapsed: 0:00:00
Volume : 25
Treble : -3
Bass : 0
Balance : 0
Loudness : on

speaker_Sub : on
Volume : 2
  • info_playing :
    which return for example :
speaker_1 : 
Playback is in progress: Artist: xx Album: Untitled  Title: xxxx

speaker_2 : 
Using Line In (state: PAUSED_PLAYBACK)

speaker_3 : 
Using Line In (state: PAUSED_PLAYBACK)

speaker_4 : 
Using Line In (state: PAUSED_PLAYBACK)

speaker_5 : 
Playback is stopped: Elapsed: 0:00:00

speaker_6 : 
Playback is stopped: Channel: Radio.com Elapsed: 0:00:00
  • And the ability to group many speakers to the coordinator in one command. For example : sonos speaker_1 speaker_2 speaker_3 group if_coordinator speaker 4 or better the ability to define a coordinator : sonos speaker_1 speaker_2 speaker_3 group $speaker as_coordinator and this action as_coordinator make the speaker $speaker the coordinator

I hope you will find the time to do it; it would be so helpful to me.

Thanks :-)

Play local mp3 files from Linux file system

Hi Avantrec, thank you for the soco-cli software.
Unfortunately I can't see how to add local linux file system mp3 files to either favourites or queues so I can then set them to play.

Can you tell me how this might be done please?

Kind regards
Martin

Feature request - always action request on coordinator

Hi,

I have a suggestion for a slight change in behaviour:

Often I will want to perform an action - say 'pause' on a particular speaker that I am listening to - say 'kitchen'.

So I run 'sonos -l kitchen pause', only to be told that kitchen is not the coordinator. I then have to figure out which speaker is the coordinator and run the same command on the correct speaker. I was wondering if your script could figure this out for me? I want to pause the music in the group that 'kitchen' is part of, but I don't really care who the coordinator is - I just want the music paused.

I'm not sure if there are disadvantges to this approach, but it seems it could be useful?

Richard

Create or modify an alarm with a radio stream always play default CHIME

Hi,

I tried

soco -l Salon create_alarm '10:00,01:30,WEEKDAYS,ON,"http://stream.live.vc.bbcmedia.co.uk/bbc_radio_fourfm",NORMAL,50,OFF'
soco -l Salon modify_alarm 4 '09:45,01:05,WEEKDAYS,YES,"https://icecast.radiofrance.fr/fipjazz-midfi.mp3",SHUFFLE,15,NO'

and the alarm play the CHIME

Playing radio stream is fine

soco -l Salon play_uri "https://icecast.radiofrance.fr/fipjazz-midfi.mp3"
soco -l Salon play_uri "http://stream.live.vc.bbcmedia.co.uk/bbc_radio_fourfm"

It worked in the past.

sonos --actions pipe-able

Greetings, pwt!

Is it possible to make sonos --actions pipe-able?

For example, it would be nice to be able to do:
sonos --actions | grep "speaker" or sonos --actions | tee actions.txt

For me, these commands produce the following error:
Traceback (most recent call last): File "/usr/local/bin/sonos", line 8, in <module> sys.exit(main()) File "/usr/local/lib/python3.9/site-packages/soco_cli/sonos.py", line 151, in main list_actions() File "/usr/local/lib/python3.9/site-packages/soco_cli/action_processor.py", line 2589, in list_actions items_per_line = get_terminal_size().columns // item_spacing OSError: [Errno 25] Inappropriate ioctl for device
I am guessing this is related to sub-processes created by the api and the inability to do i/o to/from the sub-process.

Session Info:
I am operating on an M1 macbook pro, but have installed soco-cli under an x86_64 brew installation.
soco-cli version: 0.4.46 soco version: 0.26.3 python version: 3.9.10 command path: /usr/local/bin/sonos

As always, many thanks.

Saved History not working

Was just testing the new version 0.3.8, installed through pip.

For some reason, I don't get an error, however the command line history save is not working.

I'm using Python 3.9.1 on a MacBook Pro running Big Sur.

While browsing through the commit/code I couldn't really figure out why.

After some more testing, this is what finally got it to work as expected:

In a Terminal, type:
cd ~
mkdir .soco-cli

Then from that moment on, sonos will create and append/save to the file shell-history.txt.

Maybe a Python command needs to be added to create this directory in case it doesn't exist?

Keep up the good work :)

HTTP API Server + (async) play_dir = error when trying to play a second dir

Hey there,

Thanks to the latest iOS version of the official SONOS app (ahem), I'm trying to make a web interface to soco-cli using the built-in API server and so far so good for my usage:

14C38257-F647-4C54-B816-8EAFC481706C_4_5005_c

I'm retrieving the volume from speakers and I'm able to launch a directory full of flac music from my local library (yay!).

But, when I'm hitting async_play_dir for the second time, the first one is not cancelled even if I paused the speaker.

I guess it's because the play_dir action is launched as a subprocess and the pid is not kept to be killed once the custom server need to be ended.

@pwt do you have the same interpretation? I know the async feature is quite recent but it actually doesn't work either with the sync call. I guess the json response should return the pid of the process to be killed or the server should keep it in memory to be able to stop it prior to be able to play another directory. I'm willing to initiate a PR with some guidance (and open-source that frontend at some point).

A lone newline is displayed when run_command returns an empty output_msg

Thanks for all the work you've been putting into improving this tool recently.

I believe this bug was introduced in 0184bb3.

The code that processes the result of run_command is now:

if exit_code == 0:
    print(output_msg)
else:
    print(error_msg)

So if output_msg is empty then a blank line is produced on stdout because print() will always print a newline.

This may or may not affect error_msg in a similar way.

Restoring queue(s) after switching off and on speakers

I'm doing home automation with Home Assistant, and as part of this, my speakers get switched off via a smart plug when I leave my home, and get switched on automatically when I return. However, it seems that the queues are saved on the speakers in a non-persistent way since they are gone after switching the speakers back on. I have experimented with SoCo to save the queues before switching speakers off, and restoring them afterwards. This works to some extent, more precisely, it appears to work fine for queues containing songs from my local library (served by a NAS via Samba), but not for, e.g., queues containing a Deezer playlist. My question is whether this might be possible with soco-cli? I must admit that I do not expect this, since it seems to be a problem with SoCo (or even Sonos itself), but I thought that I'd ask anyways :-)

Just in case, here's the code I'm using at the moment:

import json
import time
import traceback

from soco.data_structures import DidlMusicTrack, Queue
from soco.snapshot import Snapshot
from soco.discovery import by_name, discover


state.persist('pyscript.sonos_system_state')


ignored_attributes = {'device', 'media_metadata', 'queue', "__len__"}


@service
def save_sonos_system_state():
    start_time = time.time()

    devices = [d.group.coordinator for d in task.executor(discover)]
    uids = [d.uid for d in devices]
    assert len(uids) == len(set(uids))

    try:
        state.delete("pyscript.sonos_system_state")
    except NameError:
        pass
    pyscript.sonos_system_state = ""
    state.setattr("pyscript.sonos_system_state.devices", "")

    failed_devices = []
    for device in devices:
        try:
            snapshot = Snapshot(device, snapshot_queue=True)
            task.executor(snapshot.snapshot)
            snapshot_dict = to_dict(snapshot)
            snapshot_json = json.dumps(snapshot_dict)
            state.setattr(f"pyscript.sonos_system_state.{device.uid}", snapshot_json)

            temp = pyscript.sonos_system_state
            temp = device.player_name if temp == "" else f"{temp}, {device.player_name}"
            pyscript.sonos_system_state = temp

            temp = state.getattr("pyscript.sonos_system_state")["devices"]
            temp = device.uid if temp == "" else f"{temp},{device.uid}"
            state.setattr(f"pyscript.sonos_system_state.devices", temp)

            log.debug(f"Device {device.player_name} (UID: {device.uid}) saved successfully")
        except Exception:
            failed_devices.append(device)
            log.error(f"Error while saving device {device.player_name} (UID: {device.uid}):\n" + repr(traceback.format_exc()))

    duration = int(time.time() - start_time)
    success_devices = [d for d in devices if d not in failed_devices]
    log.info(
        f"Saving Sonos system state done: Saved {len(success_devices)} devices ({pyscript.sonos_system_state}), took {duration}s")


@service
def restore_sonos_system_state():
    try:
        state.get("pyscript.sonos_system_state")
    except NameError:
        log.warning("Sonos restore called, but no saved system state found. Nothing to do...")
        return

    start_time = time.time()

    state_attributes = state.getattr("pyscript.sonos_system_state")
    device_uids = state_attributes["devices"].split(",")

    device_names = {}
    failed_uids = []
    for device_uid in device_uids:
        try:
            snapshot_json = state_attributes[device_uid]
            snapshot_dict = json.loads(snapshot_json)
            snapshot = from_dict(snapshot_dict)
            device_names[device_uid] = snapshot.device.player_name
            task.executor(snapshot.restore, True)
            log.debug(f"Device {snapshot.device.player_name} (UID: {device_uid}) restored successfully")
        except Exception:
            log.error(f"Error while restoring device {device_uid}:\n" + repr(traceback.format_exc()))
            failed_uids.append(device_uid)

    if len(failed_uids) > 0:
        log.error(f"Errors occured while restoring device states (see above). State is not deleted (in case it's a "
                  f"temporary problem) - try service call again later...")
    else:
        state.delete("pyscript.sonos_system_state")

    duration = int(time.time() - start_time)
    success_uids = [u for u in device_uids if u not in failed_uids]
    log.info(f"Sonos system restore done: restored {len(success_uids)} devices ({', '.join(map(lambda u : device_names[u], success_uids))}), took {duration}s")


def to_dict(snapshot):
    result = {
        'attributes': {k: v for k, v in snapshot.__dict__.items() if k not in ignored_attributes},
        'device_uid': snapshot.device.uid,
        'device_name': snapshot.device.player_name
    }
    if snapshot.queue:
        result['queue'] = [{
            "elements": [e.to_dict() for e in q],
            "number_returned": q.number_returned,
            "total_matches": q.total_matches,
            "update_id": q.update_id
        } for q in snapshot.queue]
    return result


def from_dict(snapshot_dict):
    device = find_device(snapshot_dict["device_uid"], snapshot_dict["device_name"])
    snapshot = Snapshot(device)

    for k, v in snapshot_dict["attributes"].items():
        snapshot.__dict__[k] = v

    if 'queue' in snapshot_dict:
        snapshot.queue = []
        for q in snapshot_dict['queue']:
            queue = Queue([], q["number_returned"], q["total_matches"], q["update_id"])
            for e in q['elements']:
                track = DidlMusicTrack.from_dict(e)
                track.uri = track.resources[0].uri  # hack to avoid SoCo crashing - with this it works :-)
                queue.append(track)
            snapshot.queue.append(queue)

    return snapshot


def find_device(uid, name):
    devices = [d.group.coordinator for d in task.executor(discover) if d.uid == uid]
    if len(devices) == 0:
        raise Exception(f"Did not find device {name} (UID: {uid})")
    if len(devices) > 1:
        raise Exception(f"Found more then one device with UID {uid}: {', '.join(d.player_name for d in devices)}")
    device = devices[0]
    if device.player_name != name:
        log.warning(f"UID {device.uid}: Name of device ({device.player_name}) differs from name in snapshot ({name}). "
                    f"This might be because the device has been renamed. Restoring to this device anyways.")
    return device

In case you are wondering: There's of course some Home Assistant specific stuff in there - e.g. the state stuff etc. The main point is that I'm reading the queues, translate them into a JSON serializable dictionary, and save them into the (persistent) Home Assistant state, and (obviously :-) ) go the other direction for restoring the queues.

URL with Macro argument

Hi there,

I just discovered this fantastic tool.

The following commands are working:

sonos Chambre sharelink "https://open.spotify.com/track/6cpcorzV5cmVjBsuAXq4wD" start
sonos Chambre play_from_queue 1

Then I put this commands into a macro. It's working with
play_chambre = Chambre sharelink "https://open.spotify.com/track/6cpcorzV5cmVjBsuAXq4wD" start : Chambre play_from_queue 1

Then I tried to add an argument into the URL provided using:
play_chambre = Chambre sharelink %1 start : Chambre play_from_queue 1

When I tried: http://192.168.1.83:8000/macro/play_chambre/https://open.spotify.com/intl-fr/album/4V9BgSnwEMP5yRnZE5flSP
It's not working. I also tried to URL encode the spotify link. It's not working either.

Do you have any idea?

Hitting an edge case on play_file

Hi,

I am hitting an edge case in your latest play_file implementation.

In my case, always the new fallback with speaker reachability test is used, because my speakers are in a different vlan.

From time to time I need to play up to 5 files with short a duration (2-5 seconds) in a row.
If I do that I sometimes get SoCo-CLI: Macro: Exit code = 1 [Error: Can't determine an IP address for web server] and the file is skipped.

I traced down the issue and the reason for this is that the function always tries to bind on PORT_END but the port is not free at this time.

I am unsure if this is related that http_connection.close() is never called, or the unbind of the port takes some time on os level.

I did a short PoC fix, and iterated through PORT_START until PORT_END to find a valid port to bind and the issue never came up again, even after aggressivly spamming the play_file command with 1 sec mp3 files.

Possible fix:

def find_valid_port() -> Optional[int]:
    for port in range(PORT_START, PORT_END + 1):
        try:
            with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
                s.bind(('127.0.0.1', port))
                return port
        except socket.error:
            continue

    logging.error(
    "Could not find a free port to bind"
    )
    return None
 # If that fails, try to find a host IP address that can reach the target speaker
    for adapter in adapters:
        for ip in adapter.ips:
            if ip.is_IPv4 and ip.ip != "127.0.0.1":
                try:
                    logging.info(
                        "Checking target speaker's reachability from IP address '{}'"
                        .format(ip.ip)
                    )
                    port = find_valid_port()
                    if port is not None:
                        http_connection = http.client.HTTPConnection(
                            speaker.ip_address,
                            port=1400,
                            timeout=0.5,
                            source_address=(ip.ip, port),
                        )
                        http_connection.request("GET", "/status/info")
                        if http_connection.getresponse().status == 200:
                            return ip.ip
                except:
                    continue

The new function find_valid_port(...) could even be reused in def http_server(...)

Thanks in advance.

sonos fails on parsing DeviceProperties1.xml

Ubuntu 20.04
python 3.8.10
Just installed soco-0.29.1 and soco-cli-0.4.70 via pip
Sonos Play:1 from a thrift store. Works okay with noson, but I dislike the app. I did some experimenting with noson's cli application and got info on the Play:1's version:
(SONOS)Request: SoftwareVersion = 74.0-43312
(SONOS)Request: DisplaySoftwareVersion = 15.7
(SONOS)Request: HardwareVersion = 1.20.1.6-1.2
(SONOS)Request: IPAddress = 192.168.0.23
(SONOS)Request: MACAddress = 94:9F:3E:16:0A:E0
(SONOS)Request: CopyrightInfo = © 2003-2023, Sonos, Inc. All rights reserved.

I set the SPKR variable, and I get the same results from 'sonos status' 'sonos "Living Roon" status' and 'sonos play_file filename':
Error: Failed to parse: http://192.168.0.23:1400/xml/DeviceProperties1.xml

So it found the Sonos, and appears to have downloaded the properties file, but doesn't like what it sees. I grabbed the file with wget, xmllint doesn't complain about the syntax, kwrite appears to display it correctly, and xmltodict.parse() makes a dict without complaint.

I did a search for the "Failed to parse" message in the soco and soco-cli source, but couldn't find it. So I can't tell who's unhappy, or why. I'm guessing that the version of firmware in my Sonos is producing an XML file with different tag names, or in a different order, or somesuch, but that's just a guess. I've attached a copy so someone can make a more informed analysis.

DeviceProperties1.txt

Speed improvement for volume change.

I use the following command to increase the volume:

sonos office volume $(($(sonos office volume) + 1))

This essentially gets the volume, increases the value with one, and uses that value to set the new volume. Since this uses two Sonos cli calls, it's pretty slow. Is there a better way to do this? Something like:

sonos office volume +1

Thanks!

Is it really neccessary that the HTTP server must be in the same subnet as the sonos speeakers?

Hi,
my speakers are seperated in an IOT vlan, and I want to use soco-cli.

From network side, everthing is setup correctly and I can reach and find all the speakers although they are in a different network than my smartphone for example.

After playing arround with soco-cli play_file commands, I noticed that it does not work properly because soco-cli is trying to find an ip address in the same address range as the speakers for spinning up the http server.

My question is: is this limitation really necessary?

https://github.com/avantrec/soco-cli/blob/250c83f9e2458b099e9f400880d054f76af053d5/soco_cli/play_local_file.py#L135C63-L135C63

Thanks in advance

Python version compatibilty

Hi,

A recent update has caused soco-cli to be incompatible with python version versions lower than 3.7. Documenation still says 'Requires Python 3.5+ for most functions. Python 3.7+ is required for the play_file action.', but I get following error on my system that is currently stuck on Python 3.6.8:

root>/bin/python3 --version
Python 3.6.8
root>
root>sonos version
Traceback (most recent call last):
File "/usr/local/bin/sonos", line 5, in
from soco_cli.sonos import main
File "/usr/local/lib/python3.6/site-packages/soco_cli/sonos.py", line 11, in
from .action_processor import list_actions, process_action
File "/usr/local/lib/python3.6/site-packages/soco_cli/action_processor.py", line 19, in
from .play_local_file import is_supported_type, play_local_file
File "/usr/local/lib/python3.6/site-packages/soco_cli/play_local_file.py", line 4, in
from http.server import ThreadingHTTPServer
ImportError: cannot import name 'ThreadingHTTPServer'

Commenting out line 4 in play_local_file.py allows me to run sonos command again, so is it possible to check which version of python is being run, to allow compatibilty for Python lower than 3.7 ?

Thanks :)

sonos if_playing doesn't works with grouped devices

Hello, thank you very much for soco-cli, it's really great :-)

I have found a little problem with sonos if_playing command with grouped devices.

When I group sonos's devices together and when I execute this command :
sonos connect group one : roam group one : roam_2 group one : roam_3 group one
sonos one if_playing volume 20 : connect if_playing rel_vol -6 : roam if_playing volume 13 : roam_2 vol 10 : roam_3 rel_vol 6
Only the volume of sonos one, roam_2 and roam_3 change

So it's only changing the volume of the coordinator one with ìf_playing

Take what is currently playing on Mac and play on Sonos

Hi I was wondering if it is possible to basically replicate selecting the Sonos as an airplay speaker and playing the current Mac audio to it? Wanted to say auto move the current movie audio I'm watching on Netflix on my Mac over to my Sonos speakers with the press of a button on my stream deck. Anyway to do this?

Queueing local files

I really want the ability to queue files because it gives better control over play, and was puzzled why it wasn't included.

So I did some experimenting with setting up a simple webserver, and queueing up local URIs. It seems to work, after a fashion, but I've seen some events that suggest that there may be good reasons that the feature is not already done.

I'm okay with only being able to queue files that live within the artist/album/tracks hierarchy that I've been using for decades. So I'm going to firm up the webserver and write some code to generate the URIs. Would you be interested in including that as part of the soco-cli package? If so, I'll build in a config file so people can easily set it up for their own system. I'd also look into writing the URI generation code in a way that it could be incorporated as a sonos action.

Can't seem to play RTSP stream created by Audio Sharing

When I try to share the audio from my linux laptop using the Audio Sharing program, I get a notification that there is a device connecting to the audio stream but then the state of the speaker is STOPPED.

I've tried a couple of different ways of sending the stream:

play_uri "rtsp://192.168.xxx.xxx:8554/audio"
play_uri "x-rincon-mp3radio://192.168.xxx.xxx:8554/audio"

I've disabled firewalld and I'm getting the connected notification so I know networking isn't an issue.

There is no error that I can see so I'm a little stumped.

Soundcloud support

Hey folks,

First of all thanks for the work you've put into this library.

I was wondering if it was possible to add soundcloud support to it. Afaics it should not be substantially different from how spotify works, right?

Any idea how to start or how difficult that would be to add?

Thanks and greetings,
Johannes

How are we supposed to interact with play_file/dir playlists?

I'm still working on a web frontend and I wonder how I am supposed to reach the prev/next files and on a more general matter how I can retrieve the filenames from the currently playing directory?

I hacked a bit the next command using seek_forward 1h because it didn't look to be possible to jump to the next file but it's probably a bad idea(?).

I also retrieve the current playing file extracting it from get_uri at the moment but I wonder if I'm missing something in the logic and it doesn't allow me to get the directory/album currently playing.

Reading the code, the server doesn't look to keep a reference to the current playing dir.

My hope was digging into the interaction_manager to learn how to do it but the NEXT command is actually stopping the speaker?

It's starting to take shape!

Capture d’écran 2024-05-19 à 16 28 48

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.