Skip to content

Initial support for Websockets connection to Snapcast Server #60

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 26 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
bb15cf5
Initial support for WebSockets connection to
arpena Oct 31, 2023
ad6c61f
Changes to requirements
arpena Nov 15, 2023
304f03b
Update README.md for Websockets
arpena Nov 17, 2023
b3c7bee
Restore setup.py named fields
arpena Nov 24, 2023
7ae0f1b
Corrected some pylint warnings and errors
arpena Nov 24, 2023
58cf719
Fix group friendly name
luar123 Feb 17, 2024
fb97cf7
Synchronize on unknown stream update and ignore input-only streams
luar123 Feb 24, 2024
5b31dc1
Merge pull request #65 from luar123/stream_update
happyleavesaoc Feb 28, 2024
af1c479
bump version
happyleavesaoc Feb 28, 2024
558aaa0
Merge pull request #64 from luar123/debug_msg
happyleavesaoc Mar 1, 2024
4c0c488
v2.3.5
happyleavesaoc Mar 1, 2024
9ab7ae7
Return requests with an error when connection to server is lost
luar123 Mar 2, 2024
b370dd5
Make sure initial sync is valid otherwise stop server. Bug fixes
luar123 Mar 2, 2024
9819c45
Fix tests and add some new. Fix pylint errors
luar123 Mar 2, 2024
837b3e2
Merge pull request #66 from luar123/fix_transact
happyleavesaoc Mar 10, 2024
90dc289
Add functions for Stream.AddStream and Stream.RemoveStream
luar123 Mar 9, 2024
afde781
Merge pull request #67 from luar123/add_stream
happyleavesaoc Mar 16, 2024
aa14561
bump to 2.3.6
happyleavesaoc Mar 16, 2024
3535e37
Initial support for WebSockets connection to
arpena Oct 31, 2023
2c04ebe
Changes to requirements
arpena Nov 15, 2023
8cd3d80
Update README.md for Websockets
arpena Nov 17, 2023
f6bd275
Restore setup.py named fields
arpena Nov 24, 2023
be2bebb
Corrected some pylint warnings and errors
arpena Nov 24, 2023
37e99d6
sync with origin and add test_wsprotocol
arpena Mar 19, 2024
4f01567
sync with origin
arpena Mar 19, 2024
af88877
Merge branch 'master' of github.com:arpena/python-snapcast
arpena Mar 19, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
# python-snapcast

Control [Snapcast](https://github.com/badaix/snapcast) in Python 3. Reads client configurations, updates clients, and receives updates from other controllers.
The connection could be made with the json-rpc or Websockets interface. Websockets is more stable due to [issue](https://github.com/badaix/snapcast/issues/1173) in snapserver.

Supports Snapcast `0.15.0`.
Supports Snapcast `0.15.0`, but works well with latest Snapcast `0.27.0`

## Install

Expand All @@ -18,7 +19,7 @@ import asyncio
import snapcast.control

loop = asyncio.get_event_loop()
server = loop.run_until_complete(snapcast.control.create_server(loop, 'localhost'))
server = loop.run_until_complete(snapcast.control.create_server(loop, 'localhost', port=1780, reconnect=True, use_websockets=True))

# print all client names
for client in server.clients:
Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
[metadata]
description-file = README.md
description_file = README.md
3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

setup(
name='snapcast',
version='2.3.3',
version='2.3.6',
description='Control Snapcast.',
url='https://github.com/happyleavesaoc/python-snapcast/',
license='MIT',
Expand All @@ -12,6 +12,7 @@
install_requires=[
'construct>=2.5.2',
'packaging',
'websockets',
],
classifiers=[
'License :: OSI Approved :: MIT License',
Expand Down
4 changes: 2 additions & 2 deletions snapcast/control/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
from snapcast.control.server import Snapserver, CONTROL_PORT


async def create_server(loop, host, port=CONTROL_PORT, reconnect=False):
async def create_server(loop, host, port=CONTROL_PORT, reconnect=False, use_websockets=False):
"""Server factory."""
server = Snapserver(loop, host, port, reconnect)
server = Snapserver(loop, host, port, reconnect, use_websockets)
await server.start()
return server
3 changes: 2 additions & 1 deletion snapcast/control/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ def group(self):
for group in self._server.groups:
if self.identifier in group.clients:
return group
return None

@property
def friendly_name(self):
Expand Down Expand Up @@ -162,5 +163,5 @@ def set_callback(self, func):
self._callback_func = func

def __repr__(self):
"""String representation."""
"""Return string representation."""
return f'Snapclient {self.version} ({self.friendly_name}, {self.identifier})'
12 changes: 7 additions & 5 deletions snapcast/control/group.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,10 @@ async def set_volume(self, volume):
@property
def friendly_name(self):
"""Get friendly name."""
return self.name if self.name != '' else "+".join(
sorted([self._server.client(c).friendly_name for c in self.clients]))
fname = self.name if self.name != '' else "+".join(
sorted([self._server.client(c).friendly_name for c in self.clients
if c in [client.identifier for client in self._server.clients]]))
return fname if fname != '' else self.identifier

@property
def clients(self):
Expand All @@ -122,7 +124,7 @@ async def add_client(self, client_identifier):
new_clients.append(client_identifier)
await self._server.group_clients(self.identifier, new_clients)
_LOGGER.debug('added %s to %s', client_identifier, self.identifier)
status = await self._server.status()
status = (await self._server.status())[0]
self._server.synchronize(status)
self._server.client(client_identifier).callback()
self.callback()
Expand All @@ -133,7 +135,7 @@ async def remove_client(self, client_identifier):
new_clients.remove(client_identifier)
await self._server.group_clients(self.identifier, new_clients)
_LOGGER.debug('removed %s from %s', client_identifier, self.identifier)
status = await self._server.status()
status = (await self._server.status())[0]
self._server.synchronize(status)
self._server.client(client_identifier).callback()
self.callback()
Expand Down Expand Up @@ -189,5 +191,5 @@ def set_callback(self, func):
self._callback_func = func

def __repr__(self):
"""String representation."""
"""Return string representation."""
return f'Snapgroup ({self.friendly_name}, {self.identifier})'
12 changes: 8 additions & 4 deletions snapcast/control/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
SERVER_ONDISCONNECT = 'Server.OnDisconnect'


# pylint: disable=consider-using-f-string
def jsonrpc_request(method, identifier, params=None):
"""Produce a JSONRPC request."""
return '{}\r\n'.format(json.dumps({
Expand All @@ -33,6 +34,9 @@ def connection_made(self, transport):

def connection_lost(self, exc):
"""When a connection is lost."""
for b in self._buffer.values():
b['error'] = {"code": -1, "message": "connection lost"}
b['flag'].set()
self._callbacks.get(SERVER_ONDISCONNECT)(exc)

def data_received(self, data):
Expand Down Expand Up @@ -74,8 +78,8 @@ async def request(self, method, params):
self._transport.write(jsonrpc_request(method, identifier, params))
self._buffer[identifier] = {'flag': asyncio.Event()}
await self._buffer[identifier]['flag'].wait()
result = self._buffer[identifier]['data']
error = self._buffer[identifier]['error']
del self._buffer[identifier]['data']
del self._buffer[identifier]['error']
result = self._buffer[identifier].get('data')
error = self._buffer[identifier].get('error')
self._buffer[identifier].clear()
del self._buffer[identifier]
return (result, error)
Loading