Repository: happyleavesaoc/python-snapcast

Issue:
Exceptions thrown when debug logging is enabled
When logging is configured for DEBUG the following exceptions are thrown upon connecting to my Snapcast server.

```
DEBUG:snapcast.control.server:connected to snapserver on wyseguy:1705
DEBUG:snapcast.control.server:stream found: Snapstream (UPnP)
DEBUG:snapcast.control.server:stream found: Snapstream (Airplay)
DEBUG:snapcast.control.server:stream found: Snapstream (Spotify)
DEBUG:snapcast.control.server:stream found: Snapstream (All Streams)
--- Logging error ---
Traceback (most recent call last):
  File "/usr/lib/python3.11/logging/__init__.py", line 1110, in emit
    msg = self.format(record)
          ^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.11/logging/__init__.py", line 953, in format
    return fmt.format(record)
           ^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.11/logging/__init__.py", line 687, in format
    record.message = record.getMessage()
                     ^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.11/logging/__init__.py", line 377, in getMessage
    msg = msg % self.args
          ~~~~^~~~~~~~~~~
  File "/net/wyseguy/mnt/ssd/projects/snapcast-monitor/.venv/lib/python3.11/site-packages/snapcast/control/group.py", line 193, in __repr__
    return f'Snapgroup ({self.friendly_name}, {self.identifier})'
                         ^^^^^^^^^^^^^^^^^^
  File "/net/wyseguy/mnt/ssd/projects/snapcast-monitor/.venv/lib/python3.11/site-packages/snapcast/control/group.py", line 109, in friendly_name
    sorted([self._server.client(c).friendly_name for c in self.clients]))
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/net/wyseguy/mnt/ssd/projects/snapcast-monitor/.venv/lib/python3.11/site-packages/snapcast/control/group.py", line 109, in <listcomp>
    sorted([self._server.client(c).friendly_name for c in self.clients]))
            ^^^^^^^^^^^^^^^^^^^^^^
  File "/net/wyseguy/mnt/ssd/projects/snapcast-monitor/.venv/lib/python3.11/site-packages/snapcast/control/server.py", line 251, in client
    return self._clients[client_identifier]
           ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^
KeyError: 'b8:27:eb:e3:17:de'
Call stack:
  File "/net/wyseguy/mnt/ssd/projects/snapcast-monitor/./error.py", line 18, in <module>
    asyncio.run(main())
  File "/usr/lib/python3.11/asyncio/runners.py", line 190, in run
    return runner.run(main)
  File "/usr/lib/python3.11/asyncio/runners.py", line 118, in run
    return self._loop.run_until_complete(task)
  File "/usr/lib/python3.11/asyncio/base_events.py", line 640, in run_until_complete
    self.run_forever()
  File "/usr/lib/python3.11/asyncio/base_events.py", line 607, in run_forever
    self._run_once()
  File "/usr/lib/python3.11/asyncio/base_events.py", line 1922, in _run_once
    handle._run()
  File "/usr/lib/python3.11/asyncio/events.py", line 80, in _run
    self._context.run(self._callback, *self._args)
  File "/net/wyseguy/mnt/ssd/projects/snapcast-monitor/./error.py", line 14, in main
    await snapcast.control.create_server(loop, "wyseguy")
  File "/net/wyseguy/mnt/ssd/projects/snapcast-monitor/.venv/lib/python3.11/site-packages/snapcast/control/__init__.py", line 9, in create_server
    await server.start()
  File "/net/wyseguy/mnt/ssd/projects/snapcast-monitor/.venv/lib/python3.11/site-packages/snapcast/control/server.py", line 115, in start
    self.synchronize(status)
  File "/net/wyseguy/mnt/ssd/projects/snapcast-monitor/.venv/lib/python3.11/site-packages/snapcast/control/server.py", line 287, in synchronize
    _LOGGER.debug('group found: %s', new_groups[group.get('id')])
Unable to print the message and arguments - possible formatting error.
Use the traceback above to help find the error.
DEBUG:snapcast.control.server:client found: Snapclient 0.27.0 (StereoBerry, b8:27:eb:e3:17:de)
--- Logging error ---
Traceback (most recent call last):
  File "/usr/lib/python3.11/logging/__init__.py", line 1110, in emit
    msg = self.format(record)
          ^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.11/logging/__init__.py", line 953, in format
    return fmt.format(record)
           ^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.11/logging/__init__.py", line 687, in format
    record.message = record.getMessage()
                     ^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.11/logging/__init__.py", line 377, in getMessage
    msg = msg % self.args
          ~~~~^~~~~~~~~~~
  File "/net/wyseguy/mnt/ssd/projects/snapcast-monitor/.venv/lib/python3.11/site-packages/snapcast/control/group.py", line 193, in __repr__
    return f'Snapgroup ({self.friendly_name}, {self.identifier})'
                         ^^^^^^^^^^^^^^^^^^
  File "/net/wyseguy/mnt/ssd/projects/snapcast-monitor/.venv/lib/python3.11/site-packages/snapcast/control/group.py", line 109, in friendly_name
    sorted([self._server.client(c).friendly_name for c in self.clients]))
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/net/wyseguy/mnt/ssd/projects/snapcast-monitor/.venv/lib/python3.11/site-packages/snapcast/control/group.py", line 109, in <listcomp>
    sorted([self._server.client(c).friendly_name for c in self.clients]))
            ^^^^^^^^^^^^^^^^^^^^^^
  File "/net/wyseguy/mnt/ssd/projects/snapcast-monitor/.venv/lib/python3.11/site-packages/snapcast/control/server.py", line 251, in client
    return self._clients[client_identifier]
           ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^
KeyError: 'b8:27:eb:43:7e:9f'
Call stack:
  File "/net/wyseguy/mnt/ssd/projects/snapcast-monitor/./error.py", line 18, in <module>
    asyncio.run(main())
  File "/usr/lib/python3.11/asyncio/runners.py", line 190, in run
    return runner.run(main)
  File "/usr/lib/python3.11/asyncio/runners.py", line 118, in run
    return self._loop.run_until_complete(task)
  File "/usr/lib/python3.11/asyncio/base_events.py", line 640, in run_until_complete
    self.run_forever()
  File "/usr/lib/python3.11/asyncio/base_events.py", line 607, in run_forever
    self._run_once()
  File "/usr/lib/python3.11/asyncio/base_events.py", line 1922, in _run_once
    handle._run()
  File "/usr/lib/python3.11/asyncio/events.py", line 80, in _run
    self._context.run(self._callback, *self._args)
  File "/net/wyseguy/mnt/ssd/projects/snapcast-monitor/./error.py", line 14, in main
    await snapcast.control.create_server(loop, "wyseguy")
  File "/net/wyseguy/mnt/ssd/projects/snapcast-monitor/.venv/lib/python3.11/site-packages/snapcast/control/__init__.py", line 9, in create_server
    await server.start()
  File "/net/wyseguy/mnt/ssd/projects/snapcast-monitor/.venv/lib/python3.11/site-packages/snapcast/control/server.py", line 115, in start
    self.synchronize(status)
  File "/net/wyseguy/mnt/ssd/projects/snapcast-monitor/.venv/lib/python3.11/site-packages/snapcast/control/server.py", line 287, in synchronize
    _LOGGER.debug('group found: %s', new_groups[group.get('id')])
Unable to print the message and arguments - possible formatting error.
Use the traceback above to help find the error.
DEBUG:snapcast.control.server:client found: Snapclient 0.27.0 (NoteBerry, b8:27:eb:43:7e:9f)
DEBUG:snapcast.control.server:Server connected
```

Script to recreate
```python3
#!/usr/bin/env python3

import asyncio
import logging

import snapcast.control


async def main():
    logging.basicConfig(level=logging.DEBUG)

    # Connect to the Snapcast server
    loop = asyncio.get_running_loop()
    await snapcast.control.create_server(loop, "wyseguy")


try:
    asyncio.run(main())
except KeyboardInterrupt:
    pass
```

Comments:

Patch:
diff --git a/setup.py b/setup.py
index 6c934d0..e2fa579 100644
--- a/setup.py
+++ b/setup.py
@@ -2,7 +2,7 @@ from setuptools import setup
 
 setup(
     name='snapcast',
-    version='2.3.3',
+    version='2.3.4',
     description='Control Snapcast.',
     url='https://github.com/happyleavesaoc/python-snapcast/',
     license='MIT',
diff --git a/snapcast/control/group.py b/snapcast/control/group.py
index 7935b2f..3b9a8be 100644
--- a/snapcast/control/group.py
+++ b/snapcast/control/group.py
@@ -105,8 +105,10 @@ class Snapgroup():
     @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):
diff --git a/snapcast/control/server.py b/snapcast/control/server.py
index e93f5b1..afff4b3 100644
--- a/snapcast/control/server.py
+++ b/snapcast/control/server.py
@@ -284,7 +284,6 @@ class Snapserver():
                 new_groups[group.get('id')].update(group)
             else:
                 new_groups[group.get('id')] = Snapgroup(self, group)
-            _LOGGER.debug('group found: %s', new_groups[group.get('id')])
             for client in group.get('clients'):
                 if client.get('id') in self._clients:
                     new_clients[client.get('id')] = self._clients[client.get('id')]
@@ -292,6 +291,7 @@ class Snapserver():
                 else:
                     new_clients[client.get('id')] = Snapclient(self, client)
                 _LOGGER.debug('client found: %s', new_clients[client.get('id')])
+            _LOGGER.debug('group found: %s', new_groups[group.get('id')])
         self._groups = new_groups
         self._clients = new_clients
         self._streams = new_streams
@@ -402,14 +402,21 @@ class Snapserver():
 
     def _on_stream_update(self, data):
         """Handle stream update."""
-        self._streams[data.get('id')].update(data.get('stream'))
-        _LOGGER.debug('stream %s updated', self._streams[data.get('id')].friendly_name)
-        self._streams[data.get("id")].callback()
-        for group in self._groups.values():
-            if group.stream == data.get('id'):
-                group.callback()
-                for clientID in group.clients:
-                    self._clients.get(clientID).callback()
+        if data.get('id') in self._streams:
+            self._streams[data.get('id')].update(data.get('stream'))
+            _LOGGER.debug('stream %s updated', self._streams[data.get('id')].friendly_name)
+            self._streams[data.get("id")].callback()
+            for group in self._groups.values():
+                if group.stream == data.get('id'):
+                    group.callback()
+                    for clientID in group.clients:
+                        self._clients.get(clientID).callback()
+        else:
+            if data.get('stream', {}).get('uri', {}).get('query', {}).get('codec') == 'null':
+                _LOGGER.debug('stream %s is input-only, ignore', data.get('id'))
+            else:
+                _LOGGER.info('stream %s not found, synchronize', data.get('id'))
+                self.synchronize(self.status())
 
     def set_on_update_callback(self, func):
         """Set on update callback function."""


Test patch:
diff --git a/tests/test_group.py b/tests/test_group.py
index 5c3563b..bd99ec2 100644
--- a/tests/test_group.py
+++ b/tests/test_group.py
@@ -28,21 +28,26 @@ class TestSnapgroup(unittest.TestCase):
         client.callback = MagicMock()
         client.update_volume = MagicMock()
         client.friendly_name = 'A'
+        client.identifier = 'a'
         server.streams = [stream]
         server.stream = MagicMock(return_value=stream)
         server.client = MagicMock(return_value=client)
+        server.clients = [client]
         self.group = Snapgroup(server, data)
 
     def test_init(self):
         self.assertEqual(self.group.identifier, 'test')
         self.assertEqual(self.group.name, '')
-        self.assertEqual(self.group.friendly_name, 'A+A')
+        self.assertEqual(self.group.friendly_name, 'A')
         self.assertEqual(self.group.stream, 'test stream')
         self.assertEqual(self.group.muted, False)
         self.assertEqual(self.group.volume, 50)
         self.assertEqual(self.group.clients, ['a', 'b'])
         self.assertEqual(self.group.stream_status, 'playing')
 
+    def test_repr(self):
+        self.assertEqual(self.group.__repr__(), 'Snapgroup (A, test)')
+
     def test_update(self):
         self.group.update({
             'stream_id': 'other stream'