python-linkplay 0.0.1__py3-none-any.whl → 0.0.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- linkplay/__main__.py +9 -4
- linkplay/__version__.py +1 -1
- linkplay/bridge.py +18 -4
- linkplay/consts.py +1 -0
- linkplay/controller.py +34 -0
- linkplay/discovery.py +23 -19
- linkplay/utils.py +4 -1
- {python_linkplay-0.0.1.dist-info → python_linkplay-0.0.3.dist-info}/METADATA +1 -1
- python_linkplay-0.0.3.dist-info/RECORD +14 -0
- python_linkplay-0.0.1.dist-info/RECORD +0 -13
- {python_linkplay-0.0.1.dist-info → python_linkplay-0.0.3.dist-info}/LICENSE +0 -0
- {python_linkplay-0.0.1.dist-info → python_linkplay-0.0.3.dist-info}/WHEEL +0 -0
- {python_linkplay-0.0.1.dist-info → python_linkplay-0.0.3.dist-info}/top_level.txt +0 -0
linkplay/__main__.py
CHANGED
@@ -1,14 +1,19 @@
|
|
1
1
|
import asyncio
|
2
2
|
import aiohttp
|
3
3
|
|
4
|
-
from linkplay.
|
4
|
+
from linkplay.controller import LinkPlayController
|
5
5
|
|
6
6
|
|
7
7
|
async def main():
|
8
8
|
async with aiohttp.ClientSession() as session:
|
9
|
-
|
10
|
-
|
11
|
-
|
9
|
+
controller = LinkPlayController(session)
|
10
|
+
await controller.discover()
|
11
|
+
|
12
|
+
for bridge in controller.bridges:
|
13
|
+
print(bridge)
|
14
|
+
|
15
|
+
for multiroom in controller.multirooms:
|
16
|
+
print(multiroom.followers)
|
12
17
|
|
13
18
|
if __name__ == "__main__":
|
14
19
|
asyncio.run(main())
|
linkplay/__version__.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__version__ = '0.0.
|
1
|
+
__version__ = '0.0.3'
|
linkplay/bridge.py
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
from __future__ import annotations
|
2
|
-
|
2
|
+
from typing import Any
|
3
3
|
from aiohttp import ClientSession
|
4
4
|
|
5
5
|
from linkplay.consts import (
|
@@ -15,8 +15,10 @@ from linkplay.consts import (
|
|
15
15
|
InputMode,
|
16
16
|
SpeakerType,
|
17
17
|
PlayingMode,
|
18
|
-
INPUT_MODE_MAP
|
18
|
+
INPUT_MODE_MAP,
|
19
|
+
MultiroomAttribute
|
19
20
|
)
|
21
|
+
|
20
22
|
from linkplay.utils import session_call_api_json, session_call_api_ok, decode_hexstr
|
21
23
|
|
22
24
|
|
@@ -243,9 +245,21 @@ class LinkPlayMultiroom():
|
|
243
245
|
leader: LinkPlayBridge
|
244
246
|
followers: list[LinkPlayBridge]
|
245
247
|
|
246
|
-
def __init__(self, leader: LinkPlayBridge
|
248
|
+
def __init__(self, leader: LinkPlayBridge):
|
247
249
|
self.leader = leader
|
248
|
-
self.followers =
|
250
|
+
self.followers = []
|
251
|
+
|
252
|
+
async def update_status(self, bridges: list[LinkPlayBridge]) -> None:
|
253
|
+
"""Updates the multiroom status."""
|
254
|
+
properties: dict[Any, Any] = await self.leader.json_request(LinkPlayCommand.MULTIROOM_LIST)
|
255
|
+
|
256
|
+
self.followers = []
|
257
|
+
if int(properties[MultiroomAttribute.NUM_FOLLOWERS]) == 0:
|
258
|
+
return
|
259
|
+
|
260
|
+
follower_uuids = [follower[MultiroomAttribute.UUID] for follower in properties[MultiroomAttribute.FOLLOWER_LIST]]
|
261
|
+
new_followers = [bridge for bridge in bridges if bridge.device.uuid in follower_uuids]
|
262
|
+
self.followers.extend(new_followers)
|
249
263
|
|
250
264
|
async def ungroup(self) -> None:
|
251
265
|
"""Ungroups the multiroom group."""
|
linkplay/consts.py
CHANGED
linkplay/controller.py
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
from aiohttp import ClientSession
|
2
|
+
|
3
|
+
from linkplay.bridge import LinkPlayBridge, LinkPlayMultiroom
|
4
|
+
from linkplay.discovery import discover_linkplay_bridges
|
5
|
+
|
6
|
+
|
7
|
+
class LinkPlayController():
|
8
|
+
"""Represents a LinkPlay controller to manage the devices and multirooms."""
|
9
|
+
|
10
|
+
session: ClientSession
|
11
|
+
bridges: list[LinkPlayBridge]
|
12
|
+
multirooms: list[LinkPlayMultiroom]
|
13
|
+
|
14
|
+
def __init__(self, session: ClientSession):
|
15
|
+
self.session = session
|
16
|
+
self.bridges = []
|
17
|
+
self.multirooms = []
|
18
|
+
|
19
|
+
async def discover(self) -> None:
|
20
|
+
"""Attempts to discover LinkPlay devices on the local network."""
|
21
|
+
|
22
|
+
# Discover new bridges
|
23
|
+
discovered_bridges = await discover_linkplay_bridges(self.session)
|
24
|
+
current_bridges = [bridge.device.uuid for bridge in self.bridges]
|
25
|
+
new_bridges = [discovered_bridge for discovered_bridge in discovered_bridges if discovered_bridge.device.uuid not in current_bridges]
|
26
|
+
self.bridges.extend(new_bridges)
|
27
|
+
|
28
|
+
# Create new multirooms
|
29
|
+
for new_bridge in new_bridges:
|
30
|
+
self.multirooms.append(LinkPlayMultiroom(new_bridge))
|
31
|
+
|
32
|
+
# Update multirooms
|
33
|
+
for multiroom in self.multirooms:
|
34
|
+
await multiroom.update_status(self.bridges)
|
linkplay/discovery.py
CHANGED
@@ -5,7 +5,7 @@ from async_upnp_client.search import async_search
|
|
5
5
|
from async_upnp_client.utils import CaseInsensitiveDict
|
6
6
|
|
7
7
|
from linkplay.consts import UPNP_DEVICE_TYPE, LinkPlayCommand, MultiroomAttribute
|
8
|
-
from linkplay.bridge import LinkPlayBridge
|
8
|
+
from linkplay.bridge import LinkPlayBridge
|
9
9
|
from linkplay.exceptions import LinkPlayRequestException
|
10
10
|
|
11
11
|
|
@@ -21,9 +21,9 @@ async def linkplay_factory_bridge(ip_address: str, session: ClientSession) -> Li
|
|
21
21
|
return bridge
|
22
22
|
|
23
23
|
|
24
|
-
async def discover_linkplay_bridges(session: ClientSession) -> list[LinkPlayBridge]:
|
24
|
+
async def discover_linkplay_bridges(session: ClientSession, discovery_through_multiroom: bool = True) -> list[LinkPlayBridge]:
|
25
25
|
"""Attempts to discover LinkPlay devices on the local network."""
|
26
|
-
|
26
|
+
bridges: dict[str, LinkPlayBridge] = {}
|
27
27
|
|
28
28
|
async def add_linkplay_device_to_list(upnp_device: CaseInsensitiveDict):
|
29
29
|
ip_address: str | None = upnp_device.get('_host')
|
@@ -32,32 +32,36 @@ async def discover_linkplay_bridges(session: ClientSession) -> list[LinkPlayBrid
|
|
32
32
|
return
|
33
33
|
|
34
34
|
if bridge := await linkplay_factory_bridge(ip_address, session):
|
35
|
-
|
35
|
+
bridges[bridge.device.uuid] = bridge
|
36
36
|
|
37
37
|
await async_search(
|
38
38
|
search_target=UPNP_DEVICE_TYPE,
|
39
39
|
async_callback=add_linkplay_device_to_list
|
40
40
|
)
|
41
41
|
|
42
|
-
|
42
|
+
# Discover additional bridges through grouped multirooms
|
43
|
+
if discovery_through_multiroom:
|
44
|
+
multiroom_discovered_bridges: dict[str, LinkPlayBridge] = {}
|
45
|
+
for bridge in bridges.values():
|
46
|
+
for new_bridge in await discover_bridges_through_multiroom(bridge, session):
|
47
|
+
multiroom_discovered_bridges[new_bridge.device.uuid] = new_bridge
|
43
48
|
|
49
|
+
bridges = bridges | multiroom_discovered_bridges
|
44
50
|
|
45
|
-
|
46
|
-
"""Discovers multirooms through the list of provided bridges."""
|
47
|
-
multirooms: list[LinkPlayMultiroom] = []
|
51
|
+
return list(bridges.values())
|
48
52
|
|
49
|
-
for bridge in bridges:
|
50
|
-
properties: dict[Any, Any] = await bridge.json_request(LinkPlayCommand.MULTIROOM_LIST)
|
51
53
|
|
52
|
-
|
53
|
-
|
54
|
+
async def discover_bridges_through_multiroom(bridge: LinkPlayBridge,
|
55
|
+
session: ClientSession) -> list[LinkPlayBridge]:
|
56
|
+
"""Discovers bridges through the multiroom of the provided bridge."""
|
57
|
+
properties: dict[Any, Any] = await bridge.json_request(LinkPlayCommand.MULTIROOM_LIST)
|
54
58
|
|
55
|
-
|
56
|
-
|
57
|
-
follower_uuid = follower[MultiroomAttribute.UUID]
|
58
|
-
if follower_bridge := next((b for b in bridges if b.device.uuid == follower_uuid), None):
|
59
|
-
followers.append(follower_bridge)
|
59
|
+
if int(properties[MultiroomAttribute.NUM_FOLLOWERS]) == 0:
|
60
|
+
return []
|
60
61
|
|
61
|
-
|
62
|
+
followers: list[LinkPlayBridge] = []
|
63
|
+
for follower in properties[MultiroomAttribute.FOLLOWER_LIST]:
|
64
|
+
if new_bridge := await linkplay_factory_bridge(follower[MultiroomAttribute.IP], session):
|
65
|
+
followers.append(new_bridge)
|
62
66
|
|
63
|
-
return
|
67
|
+
return followers
|
linkplay/utils.py
CHANGED
@@ -56,4 +56,7 @@ async def session_call_api_ok(endpoint: str, session: ClientSession, command: st
|
|
56
56
|
|
57
57
|
def decode_hexstr(hexstr: str) -> str:
|
58
58
|
"""Decode a hex string."""
|
59
|
-
|
59
|
+
try:
|
60
|
+
return bytes.fromhex(hexstr).decode("utf-8")
|
61
|
+
except ValueError:
|
62
|
+
return hexstr
|
@@ -0,0 +1,14 @@
|
|
1
|
+
linkplay/__init__.py,sha256=y9ZehEq-KhS3cwn-PUpwVSJGfDUx7e5wf_G6guODcTk,56
|
2
|
+
linkplay/__main__.py,sha256=hVlIhEeSfIRlTjNsDXVMSAtOatbUqqIMdXPxdwBBE1c,447
|
3
|
+
linkplay/__version__.py,sha256=yyLtGsBhynC-sSHdax-rRGvHNjH-gdWxIY68WnPVfDw,22
|
4
|
+
linkplay/bridge.py,sha256=NX02ZBcbaz7yZv1EeFvQ9CAUh5jVcuSABtpKIY4aj74,10948
|
5
|
+
linkplay/consts.py,sha256=KIBGrRQqxd1B4kRO0Vl0e5-UzbMLzJGC_ECohzkSRwQ,7750
|
6
|
+
linkplay/controller.py,sha256=Rp92CQdpwreH-I132DrxogiZ_I8NngdmCeXzfQYAFMQ,1244
|
7
|
+
linkplay/discovery.py,sha256=NIUC3FVPH-FcHvMmojJw0s0w2Q-WCuulGrQA-PDLcsw,2642
|
8
|
+
linkplay/exceptions.py,sha256=tWJWHsKVkUEq3Yet1Z739IxcaQT8YamDeSp0tqHde9c,107
|
9
|
+
linkplay/utils.py,sha256=E_SjIyeK76ishhwuU24m28y1FDAMEj1QbRNt-aHIMdA,2137
|
10
|
+
python_linkplay-0.0.3.dist-info/LICENSE,sha256=bgEtxMyjEHX_4uwaAY3GCFTm234D4AOZ5dM15sk26ms,1073
|
11
|
+
python_linkplay-0.0.3.dist-info/METADATA,sha256=ifK_uu-ZBe81Wcqo8YoIObMiuPQ6ub1QUio7Mnm5JYE,2859
|
12
|
+
python_linkplay-0.0.3.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
13
|
+
python_linkplay-0.0.3.dist-info/top_level.txt,sha256=CpSaOVPTzJf5TVIL7MrotSCR34gcIOQy-11l4zGmxxM,9
|
14
|
+
python_linkplay-0.0.3.dist-info/RECORD,,
|
@@ -1,13 +0,0 @@
|
|
1
|
-
linkplay/__init__.py,sha256=y9ZehEq-KhS3cwn-PUpwVSJGfDUx7e5wf_G6guODcTk,56
|
2
|
-
linkplay/__main__.py,sha256=AiwpQKJIV9HrSqj0qU-a8MKE-AUjbfTrQ6st45U78rE,382
|
3
|
-
linkplay/__version__.py,sha256=DsAHdxLC16H2VjdFOU5tBx2xT9VnNQ-XbTS24fRCa_w,22
|
4
|
-
linkplay/bridge.py,sha256=l5Vvtp2SHF2FYMa67Yf-daRGsErMRXB1GMPzHhwwG6k,10344
|
5
|
-
linkplay/consts.py,sha256=XpRJaPmz39EKFAZrgZEJRloL5lEI3p8jLPgrEJHgbjk,7736
|
6
|
-
linkplay/discovery.py,sha256=wmF7fsCd4htr7zHfDRh9z0J2cu1OjK50ZQaBfg3WlaU,2303
|
7
|
-
linkplay/exceptions.py,sha256=tWJWHsKVkUEq3Yet1Z739IxcaQT8YamDeSp0tqHde9c,107
|
8
|
-
linkplay/utils.py,sha256=L8P7n1JtI0Q62fHV2TC5K03TuPQXesSikXLVje6Av-E,2079
|
9
|
-
python_linkplay-0.0.1.dist-info/LICENSE,sha256=bgEtxMyjEHX_4uwaAY3GCFTm234D4AOZ5dM15sk26ms,1073
|
10
|
-
python_linkplay-0.0.1.dist-info/METADATA,sha256=-JpSNww6wpyDmpWHdQ-fAZXYBP4cCbPPUpSQCxh6sg0,2859
|
11
|
-
python_linkplay-0.0.1.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
12
|
-
python_linkplay-0.0.1.dist-info/top_level.txt,sha256=CpSaOVPTzJf5TVIL7MrotSCR34gcIOQy-11l4zGmxxM,9
|
13
|
-
python_linkplay-0.0.1.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|