Repository: StevenLooman/async_upnp_client

Issue:
Ability to get `UpnpService` by id rather than type? 
On Linn devices, the manufacturer may increase the service version in their description document. For example the service description on a recent firmware for the volume service is:

```xml
            <service>
                <serviceType>urn:av-openhome-org:service:Volume:4</serviceType>
                <serviceId>urn:av-openhome-org:serviceId:Volume</serviceId>
                <SCPDURL>/4c494e4e-1234-ab12-abcd-01234567819f/Upnp/av.openhome.org-Volume-4/service.xml</SCPDURL>
                <controlURL>/4c494e4e-1234-ab12-abcd-01234567819f/av.openhome.org-Volume-4/control</controlURL>
                <eventSubURL>/4c494e4e-1234-ab12-abcd-01234567819f/av.openhome.org-Volume-4/event</eventSubURL>
            </service>
```

However on an older version or software open home player, the service description is:

```xml
            <service>
                <serviceType>urn:av-openhome-org:service:Volume:1</serviceType>
                <serviceId>urn:av-openhome-org:serviceId:Volume</serviceId>
                <SCPDURL>/4c494e4e-1234-ab12-abcd-01234567819f/Upnp/av.openhome.org-Volume-1/service.xml</SCPDURL>
                <controlURL>/4c494e4e-1234-ab12-abcd-01234567819f/av.openhome.org-Volume-1/control</controlURL>
                <eventSubURL>/4c494e4e-1234-ab12-abcd-01234567819f/av.openhome.org-Volume-1/event</eventSubURL>
            </service>
```

Would it be possible to add a facility to fetch the service from a client by using the `serviceId` field in addition to `serviceType`? 

I'm using your library in an `openhome` integration for HomeAssistant (homeassistant.io) and to support all potential iterations of the volume and product services on supported products I need to attempt all potential versions of a service (https://github.com/bazwilliams/openhomedevice/blob/rewrite-unit-tests/openhomedevice/Device.py#L32) this also means should Linn bring out a newer version of their firmware with a newer service, this integration would be partially unavailable despite the newer service being backwards compatible. 

I'm happy to propose a PR if you'd like?  

Discussion:
StevenLooman: Thank you for this change. Can you add a line to `CHANGES.rst`?

Patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index 91ef03d..e023450 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -4,6 +4,7 @@ Changes
 0.16.1 (unreleased)
 
 - Don't double-unescape action responses (#50)
+- Add `UpnpDevice.service_id()` to get service by service_id. (@bazwilliams)
 
 
 0.16.0 (2021-03-30)
diff --git a/async_upnp_client/client.py b/async_upnp_client/client.py
index fc77560..2d548a4 100644
--- a/async_upnp_client/client.py
+++ b/async_upnp_client/client.py
@@ -217,6 +217,13 @@ class UpnpDevice:
         """Get service by service_type."""
         return self.services[service_type]
 
+    def service_id(self, service_id: str) -> Optional["UpnpService"]:
+        """Get service by service_id."""
+        for service in self.services.values():
+            if service.service_id == service_id:
+                return service
+        return None
+
     async def async_ping(self) -> None:
         """Ping the device."""
         await self.requester.async_http_request("GET", self.device_url)


Test patch:
diff --git a/tests/test_upnp_client.py b/tests/test_upnp_client.py
index 2864da1..ec11311 100644
--- a/tests/test_upnp_client.py
+++ b/tests/test_upnp_client.py
@@ -32,6 +32,9 @@ class TestUpnpStateVariable:
         service = device.service("urn:schemas-upnp-org:service:RenderingControl:1")
         assert service
 
+        service_by_id = device.service_id("urn:upnp-org:serviceId:RenderingControl")
+        assert service_by_id == service
+
         state_var = service.state_variable("Volume")
         assert state_var