python-linkplay 0.0.8__py3-none-any.whl → 0.0.10__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/__version__.py +1 -1
 - linkplay/bridge.py +39 -13
 - linkplay/consts.py +13 -0
 - linkplay/controller.py +8 -0
 - linkplay/endpoint.py +25 -1
 - linkplay/utils.py +105 -6
 - {python_linkplay-0.0.8.dist-info → python_linkplay-0.0.10.dist-info}/METADATA +2 -2
 - python_linkplay-0.0.10.dist-info/RECORD +15 -0
 - {python_linkplay-0.0.8.dist-info → python_linkplay-0.0.10.dist-info}/WHEEL +1 -1
 - python_linkplay-0.0.8.dist-info/RECORD +0 -15
 - {python_linkplay-0.0.8.dist-info → python_linkplay-0.0.10.dist-info}/LICENSE +0 -0
 - {python_linkplay-0.0.8.dist-info → python_linkplay-0.0.10.dist-info}/top_level.txt +0 -0
 
    
        linkplay/__version__.py
    CHANGED
    
    | 
         @@ -1 +1 @@ 
     | 
|
| 
       1 
     | 
    
         
            -
            __version__ = '0.0. 
     | 
| 
      
 1 
     | 
    
         
            +
            __version__ = '0.0.10'
         
     | 
    
        linkplay/bridge.py
    CHANGED
    
    | 
         @@ -19,7 +19,7 @@ from linkplay.consts import ( 
     | 
|
| 
       19 
19 
     | 
    
         
             
                SpeakerType,
         
     | 
| 
       20 
20 
     | 
    
         
             
            )
         
     | 
| 
       21 
21 
     | 
    
         
             
            from linkplay.endpoint import LinkPlayEndpoint
         
     | 
| 
       22 
     | 
    
         
            -
            from linkplay.utils import  
     | 
| 
      
 22 
     | 
    
         
            +
            from linkplay.utils import fixup_player_properties
         
     | 
| 
       23 
23 
     | 
    
         | 
| 
       24 
24 
     | 
    
         | 
| 
       25 
25 
     | 
    
         
             
            class LinkPlayDevice:
         
     | 
| 
         @@ -83,10 +83,11 @@ class LinkPlayPlayer: 
     | 
|
| 
       83 
83 
     | 
    
         | 
| 
       84 
84 
     | 
    
         
             
                async def update_status(self) -> None:
         
     | 
| 
       85 
85 
     | 
    
         
             
                    """Update the player status."""
         
     | 
| 
       86 
     | 
    
         
            -
                     
     | 
| 
       87 
     | 
    
         
            -
             
     | 
| 
       88 
     | 
    
         
            -
                     
     | 
| 
       89 
     | 
    
         
            -
             
     | 
| 
      
 86 
     | 
    
         
            +
                    properties: dict[PlayerAttribute, str] = await self.bridge.json_request(
         
     | 
| 
      
 87 
     | 
    
         
            +
                        LinkPlayCommand.PLAYER_STATUS
         
     | 
| 
      
 88 
     | 
    
         
            +
                    )  # type: ignore[assignment]
         
     | 
| 
      
 89 
     | 
    
         
            +
             
     | 
| 
      
 90 
     | 
    
         
            +
                    self.properties = fixup_player_properties(properties)
         
     | 
| 
       90 
91 
     | 
    
         | 
| 
       91 
92 
     | 
    
         
             
                async def next(self) -> None:
         
     | 
| 
       92 
93 
     | 
    
         
             
                    """Play the next song in the playlist."""
         
     | 
| 
         @@ -148,10 +149,19 @@ class LinkPlayPlayer: 
     | 
|
| 
       148 
149 
     | 
    
         
             
                        LinkPlayCommand.SWITCH_MODE.format(PLAY_MODE_SEND_MAP[mode])
         
     | 
| 
       149 
150 
     | 
    
         
             
                    )  # type: ignore[str-format]
         
     | 
| 
       150 
151 
     | 
    
         | 
| 
      
 152 
     | 
    
         
            +
                async def play_preset(self, preset_number: int) -> None:
         
     | 
| 
      
 153 
     | 
    
         
            +
                    """Play a preset."""
         
     | 
| 
      
 154 
     | 
    
         
            +
                    if not 0 < preset_number <= 10:
         
     | 
| 
      
 155 
     | 
    
         
            +
                        raise ValueError("Preset must be between 1 and 10.")
         
     | 
| 
      
 156 
     | 
    
         
            +
                    await self.bridge.request(LinkPlayCommand.PLAY_PRESET.format(preset_number))
         
     | 
| 
      
 157 
     | 
    
         
            +
             
     | 
| 
       151 
158 
     | 
    
         
             
                @property
         
     | 
| 
       152 
159 
     | 
    
         
             
                def muted(self) -> bool:
         
     | 
| 
       153 
160 
     | 
    
         
             
                    """Returns if the player is muted."""
         
     | 
| 
       154 
     | 
    
         
            -
                    return  
     | 
| 
      
 161 
     | 
    
         
            +
                    return (
         
     | 
| 
      
 162 
     | 
    
         
            +
                        self.properties.get(PlayerAttribute.MUTED, MuteMode.UNMUTED)
         
     | 
| 
      
 163 
     | 
    
         
            +
                        == MuteMode.MUTED
         
     | 
| 
      
 164 
     | 
    
         
            +
                    )
         
     | 
| 
       155 
165 
     | 
    
         | 
| 
       156 
166 
     | 
    
         
             
                @property
         
     | 
| 
       157 
167 
     | 
    
         
             
                def title(self) -> str:
         
     | 
| 
         @@ -186,32 +196,46 @@ class LinkPlayPlayer: 
     | 
|
| 
       186 
196 
     | 
    
         
             
                @property
         
     | 
| 
       187 
197 
     | 
    
         
             
                def status(self) -> PlayingStatus:
         
     | 
| 
       188 
198 
     | 
    
         
             
                    """Returns the current playing status."""
         
     | 
| 
       189 
     | 
    
         
            -
                    return PlayingStatus( 
     | 
| 
      
 199 
     | 
    
         
            +
                    return PlayingStatus(
         
     | 
| 
      
 200 
     | 
    
         
            +
                        self.properties.get(PlayerAttribute.PLAYING_STATUS, PlayingStatus.STOPPED)
         
     | 
| 
      
 201 
     | 
    
         
            +
                    )
         
     | 
| 
       190 
202 
     | 
    
         | 
| 
       191 
203 
     | 
    
         
             
                @property
         
     | 
| 
       192 
204 
     | 
    
         
             
                def equalizer_mode(self) -> EqualizerMode:
         
     | 
| 
       193 
205 
     | 
    
         
             
                    """Returns the current equalizer mode."""
         
     | 
| 
       194 
     | 
    
         
            -
                    return EqualizerMode( 
     | 
| 
      
 206 
     | 
    
         
            +
                    return EqualizerMode(
         
     | 
| 
      
 207 
     | 
    
         
            +
                        self.properties.get(PlayerAttribute.EQUALIZER_MODE, EqualizerMode.CLASSIC)
         
     | 
| 
      
 208 
     | 
    
         
            +
                    )
         
     | 
| 
       195 
209 
     | 
    
         | 
| 
       196 
210 
     | 
    
         
             
                @property
         
     | 
| 
       197 
211 
     | 
    
         
             
                def speaker_type(self) -> SpeakerType:
         
     | 
| 
       198 
212 
     | 
    
         
             
                    """Returns the current speaker the player is playing on."""
         
     | 
| 
       199 
     | 
    
         
            -
                    return SpeakerType( 
     | 
| 
      
 213 
     | 
    
         
            +
                    return SpeakerType(
         
     | 
| 
      
 214 
     | 
    
         
            +
                        self.properties.get(PlayerAttribute.SPEAKER_TYPE, SpeakerType.MAIN_SPEAKER)
         
     | 
| 
      
 215 
     | 
    
         
            +
                    )
         
     | 
| 
       200 
216 
     | 
    
         | 
| 
       201 
217 
     | 
    
         
             
                @property
         
     | 
| 
       202 
218 
     | 
    
         
             
                def channel_type(self) -> ChannelType:
         
     | 
| 
       203 
219 
     | 
    
         
             
                    """Returns the channel the player is playing on."""
         
     | 
| 
       204 
     | 
    
         
            -
                    return ChannelType( 
     | 
| 
      
 220 
     | 
    
         
            +
                    return ChannelType(
         
     | 
| 
      
 221 
     | 
    
         
            +
                        self.properties.get(PlayerAttribute.CHANNEL_TYPE, ChannelType.STEREO)
         
     | 
| 
      
 222 
     | 
    
         
            +
                    )
         
     | 
| 
       205 
223 
     | 
    
         | 
| 
       206 
224 
     | 
    
         
             
                @property
         
     | 
| 
       207 
225 
     | 
    
         
             
                def play_mode(self) -> PlayingMode:
         
     | 
| 
       208 
226 
     | 
    
         
             
                    """Returns the current playing mode of the player."""
         
     | 
| 
       209 
     | 
    
         
            -
                    return PlayingMode( 
     | 
| 
      
 227 
     | 
    
         
            +
                    return PlayingMode(
         
     | 
| 
      
 228 
     | 
    
         
            +
                        self.properties.get(PlayerAttribute.PLAYBACK_MODE, PlayingMode.IDLE)
         
     | 
| 
      
 229 
     | 
    
         
            +
                    )
         
     | 
| 
       210 
230 
     | 
    
         | 
| 
       211 
231 
     | 
    
         
             
                @property
         
     | 
| 
       212 
232 
     | 
    
         
             
                def loop_mode(self) -> LoopMode:
         
     | 
| 
       213 
233 
     | 
    
         
             
                    """Returns the current playlist mode."""
         
     | 
| 
       214 
     | 
    
         
            -
                    return LoopMode( 
     | 
| 
      
 234 
     | 
    
         
            +
                    return LoopMode(
         
     | 
| 
      
 235 
     | 
    
         
            +
                        self.properties.get(
         
     | 
| 
      
 236 
     | 
    
         
            +
                            PlayerAttribute.PLAYLIST_MODE, LoopMode.CONTINUOUS_PLAYBACK
         
     | 
| 
      
 237 
     | 
    
         
            +
                        )
         
     | 
| 
      
 238 
     | 
    
         
            +
                    )
         
     | 
| 
       215 
239 
     | 
    
         | 
| 
       216 
240 
     | 
    
         | 
| 
       217 
241 
     | 
    
         
             
            class LinkPlayBridge:
         
     | 
| 
         @@ -292,7 +316,9 @@ class LinkPlayMultiroom: 
     | 
|
| 
       292 
316 
     | 
    
         | 
| 
       293 
317 
     | 
    
         
             
                async def set_volume(self, value: int) -> None:
         
     | 
| 
       294 
318 
     | 
    
         
             
                    """Sets the volume for the multiroom group."""
         
     | 
| 
       295 
     | 
    
         
            -
                     
     | 
| 
      
 319 
     | 
    
         
            +
                    if not 0 <= value <= 100:
         
     | 
| 
      
 320 
     | 
    
         
            +
                        raise ValueError("Volume must be between 0 and 100")
         
     | 
| 
      
 321 
     | 
    
         
            +
             
     | 
| 
       296 
322 
     | 
    
         
             
                    str_vol = str(value)
         
     | 
| 
       297 
323 
     | 
    
         
             
                    await self.leader.request(LinkPlayCommand.MULTIROOM_VOL.format(str_vol))  # type: ignore[str-format]
         
     | 
| 
       298 
324 
     | 
    
         | 
    
        linkplay/consts.py
    CHANGED
    
    | 
         @@ -4,6 +4,8 @@ API_ENDPOINT: str = "{}/httpapi.asp?command={}" 
     | 
|
| 
       4 
4 
     | 
    
         
             
            API_TIMEOUT: int = 2
         
     | 
| 
       5 
5 
     | 
    
         
             
            UNKNOWN_TRACK_PLAYING: str = "Unknown"
         
     | 
| 
       6 
6 
     | 
    
         
             
            UPNP_DEVICE_TYPE = "urn:schemas-upnp-org:device:MediaRenderer:1"
         
     | 
| 
      
 7 
     | 
    
         
            +
            TCPPORT = 8899
         
     | 
| 
      
 8 
     | 
    
         
            +
            TCP_MESSAGE_LENGTH = 1024
         
     | 
| 
       7 
9 
     | 
    
         | 
| 
       8 
10 
     | 
    
         
             
            MTLS_CERTIFICATE_CONTENTS = """
         
     | 
| 
       9 
11 
     | 
    
         
             
            -----BEGIN PRIVATE KEY-----
         
     | 
| 
         @@ -89,6 +91,17 @@ class LinkPlayCommand(StrEnum): 
     | 
|
| 
       89 
91 
     | 
    
         
             
                MULTIROOM_MUTE = "setPlayerCmd:slave_mute:mute"
         
     | 
| 
       90 
92 
     | 
    
         
             
                MULTIROOM_UNMUTE = "setPlayerCmd:slave_mute:unmute"
         
     | 
| 
       91 
93 
     | 
    
         
             
                MULTIROOM_JOIN = "ConnectMasterAp:JoinGroupMaster:eth{}:wifi0.0.0.0"
         
     | 
| 
      
 94 
     | 
    
         
            +
                PLAY_PRESET = "MCUKeyShortClick:{}"
         
     | 
| 
      
 95 
     | 
    
         
            +
             
     | 
| 
      
 96 
     | 
    
         
            +
             
     | 
| 
      
 97 
     | 
    
         
            +
            class LinkPlayTcpUartCommand(StrEnum):
         
     | 
| 
      
 98 
     | 
    
         
            +
                """Defined LinkPlay TCPUART commands."""
         
     | 
| 
      
 99 
     | 
    
         
            +
             
     | 
| 
      
 100 
     | 
    
         
            +
                GET_METADATA = "MCU+MEA+GET"
         
     | 
| 
      
 101 
     | 
    
         
            +
                PRESET_PLAY = "MCU+KEY+{:03}"
         
     | 
| 
      
 102 
     | 
    
         
            +
                PRESET_NEXT = "MCU+KEY+NXT"
         
     | 
| 
      
 103 
     | 
    
         
            +
                INPUT_WIFI = "MCU+PLM+000"
         
     | 
| 
      
 104 
     | 
    
         
            +
                INPUT_BLUETOOTH = "MCU+PLM+006"
         
     | 
| 
       92 
105 
     | 
    
         | 
| 
       93 
106 
     | 
    
         | 
| 
       94 
107 
     | 
    
         
             
            class SpeakerType(StrEnum):
         
     | 
    
        linkplay/controller.py
    CHANGED
    
    | 
         @@ -29,6 +29,14 @@ class LinkPlayController: 
     | 
|
| 
       29 
29 
     | 
    
         
             
                    ]
         
     | 
| 
       30 
30 
     | 
    
         
             
                    self.bridges.extend(new_bridges)
         
     | 
| 
       31 
31 
     | 
    
         | 
| 
      
 32 
     | 
    
         
            +
                async def add_bridge(self, bridge_to_add: LinkPlayBridge) -> None:
         
     | 
| 
      
 33 
     | 
    
         
            +
                    """Add given LinkPlay device if not already added."""
         
     | 
| 
      
 34 
     | 
    
         
            +
             
     | 
| 
      
 35 
     | 
    
         
            +
                    # Add bridge
         
     | 
| 
      
 36 
     | 
    
         
            +
                    current_bridges = [bridge.device.uuid for bridge in self.bridges]
         
     | 
| 
      
 37 
     | 
    
         
            +
                    if bridge_to_add.device.uuid not in current_bridges:
         
     | 
| 
      
 38 
     | 
    
         
            +
                        self.bridges.append(bridge_to_add)
         
     | 
| 
      
 39 
     | 
    
         
            +
             
     | 
| 
       32 
40 
     | 
    
         
             
                async def discover_multirooms(self) -> None:
         
     | 
| 
       33 
41 
     | 
    
         
             
                    """Attempts to discover multirooms on the local network."""
         
     | 
| 
       34 
42 
     | 
    
         | 
    
        linkplay/endpoint.py
    CHANGED
    
    | 
         @@ -1,8 +1,15 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            import asyncio
         
     | 
| 
       1 
2 
     | 
    
         
             
            from abc import ABC, abstractmethod
         
     | 
| 
       2 
3 
     | 
    
         | 
| 
       3 
4 
     | 
    
         
             
            from aiohttp import ClientSession
         
     | 
| 
       4 
5 
     | 
    
         | 
| 
       5 
     | 
    
         
            -
            from linkplay. 
     | 
| 
      
 6 
     | 
    
         
            +
            from linkplay.consts import TCPPORT
         
     | 
| 
      
 7 
     | 
    
         
            +
            from linkplay.utils import (
         
     | 
| 
      
 8 
     | 
    
         
            +
                call_tcpuart,
         
     | 
| 
      
 9 
     | 
    
         
            +
                call_tcpuart_json,
         
     | 
| 
      
 10 
     | 
    
         
            +
                session_call_api_json,
         
     | 
| 
      
 11 
     | 
    
         
            +
                session_call_api_ok,
         
     | 
| 
      
 12 
     | 
    
         
            +
            )
         
     | 
| 
       6 
13 
     | 
    
         | 
| 
       7 
14 
     | 
    
         | 
| 
       8 
15 
     | 
    
         
             
            class LinkPlayEndpoint(ABC):
         
     | 
| 
         @@ -38,3 +45,20 @@ class LinkPlayApiEndpoint(LinkPlayEndpoint): 
     | 
|
| 
       38 
45 
     | 
    
         | 
| 
       39 
46 
     | 
    
         
             
                def __str__(self) -> str:
         
     | 
| 
       40 
47 
     | 
    
         
             
                    return self._endpoint
         
     | 
| 
      
 48 
     | 
    
         
            +
             
     | 
| 
      
 49 
     | 
    
         
            +
             
     | 
| 
      
 50 
     | 
    
         
            +
            class LinkPlayTcpUartEndpoint(LinkPlayEndpoint):
         
     | 
| 
      
 51 
     | 
    
         
            +
                """Represents a LinkPlay TCPUART API endpoint."""
         
     | 
| 
      
 52 
     | 
    
         
            +
             
     | 
| 
      
 53 
     | 
    
         
            +
                def __init__(
         
     | 
| 
      
 54 
     | 
    
         
            +
                    self, *, connection: tuple[asyncio.StreamReader, asyncio.StreamWriter]
         
     | 
| 
      
 55 
     | 
    
         
            +
                ):
         
     | 
| 
      
 56 
     | 
    
         
            +
                    self._connection = connection
         
     | 
| 
      
 57 
     | 
    
         
            +
             
     | 
| 
      
 58 
     | 
    
         
            +
                async def request(self, command: str) -> None:
         
     | 
| 
      
 59 
     | 
    
         
            +
                    reader, writer = self._connection
         
     | 
| 
      
 60 
     | 
    
         
            +
                    await call_tcpuart(reader, writer, command)
         
     | 
| 
      
 61 
     | 
    
         
            +
             
     | 
| 
      
 62 
     | 
    
         
            +
                async def json_request(self, command: str) -> dict[str, str]:
         
     | 
| 
      
 63 
     | 
    
         
            +
                    reader, writer = self._connection
         
     | 
| 
      
 64 
     | 
    
         
            +
                    return await call_tcpuart_json(reader, writer, command)
         
     | 
    
        linkplay/utils.py
    CHANGED
    
    | 
         @@ -1,20 +1,31 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            import asyncio
         
     | 
| 
       2 
2 
     | 
    
         
             
            import contextlib
         
     | 
| 
       3 
3 
     | 
    
         
             
            import json
         
     | 
| 
      
 4 
     | 
    
         
            +
            import logging
         
     | 
| 
       4 
5 
     | 
    
         
             
            import os
         
     | 
| 
       5 
6 
     | 
    
         
             
            import socket
         
     | 
| 
       6 
7 
     | 
    
         
             
            import ssl
         
     | 
| 
      
 8 
     | 
    
         
            +
            from concurrent.futures import ThreadPoolExecutor
         
     | 
| 
       7 
9 
     | 
    
         
             
            from http import HTTPStatus
         
     | 
| 
       8 
     | 
    
         
            -
            from typing import Dict
         
     | 
| 
       9 
10 
     | 
    
         | 
| 
       10 
11 
     | 
    
         
             
            import aiofiles
         
     | 
| 
       11 
12 
     | 
    
         
             
            import async_timeout
         
     | 
| 
       12 
13 
     | 
    
         
             
            from aiohttp import ClientError, ClientSession, TCPConnector
         
     | 
| 
       13 
14 
     | 
    
         
             
            from appdirs import AppDirs
         
     | 
| 
       14 
     | 
    
         
            -
             
     | 
| 
       15 
     | 
    
         
            -
             
     | 
| 
      
 15 
     | 
    
         
            +
            from deprecated import deprecated
         
     | 
| 
      
 16 
     | 
    
         
            +
             
     | 
| 
      
 17 
     | 
    
         
            +
            from linkplay.consts import (
         
     | 
| 
      
 18 
     | 
    
         
            +
                API_ENDPOINT,
         
     | 
| 
      
 19 
     | 
    
         
            +
                API_TIMEOUT,
         
     | 
| 
      
 20 
     | 
    
         
            +
                MTLS_CERTIFICATE_CONTENTS,
         
     | 
| 
      
 21 
     | 
    
         
            +
                TCP_MESSAGE_LENGTH,
         
     | 
| 
      
 22 
     | 
    
         
            +
                PlayerAttribute,
         
     | 
| 
      
 23 
     | 
    
         
            +
                PlayingStatus,
         
     | 
| 
      
 24 
     | 
    
         
            +
            )
         
     | 
| 
       16 
25 
     | 
    
         
             
            from linkplay.exceptions import LinkPlayRequestException
         
     | 
| 
       17 
26 
     | 
    
         | 
| 
      
 27 
     | 
    
         
            +
            _LOGGER = logging.getLogger(__name__)
         
     | 
| 
      
 28 
     | 
    
         
            +
             
     | 
| 
       18 
29 
     | 
    
         | 
| 
       19 
30 
     | 
    
         
             
            async def session_call_api(endpoint: str, session: ClientSession, command: str) -> str:
         
     | 
| 
       20 
31 
     | 
    
         
             
                """Calls the LinkPlay API and returns the result as a string.
         
     | 
| 
         @@ -51,7 +62,7 @@ async def session_call_api(endpoint: str, session: ClientSession, command: str) 
     | 
|
| 
       51 
62 
     | 
    
         | 
| 
       52 
63 
     | 
    
         
             
            async def session_call_api_json(
         
     | 
| 
       53 
64 
     | 
    
         
             
                endpoint: str, session: ClientSession, command: str
         
     | 
| 
       54 
     | 
    
         
            -
            ) ->  
     | 
| 
      
 65 
     | 
    
         
            +
            ) -> dict[str, str]:
         
     | 
| 
       55 
66 
     | 
    
         
             
                """Calls the LinkPlay API and returns the result as a JSON object."""
         
     | 
| 
       56 
67 
     | 
    
         
             
                result = await session_call_api(endpoint, session, command)
         
     | 
| 
       57 
68 
     | 
    
         
             
                return json.loads(result)  # type: ignore
         
     | 
| 
         @@ -67,6 +78,44 @@ async def session_call_api_ok( 
     | 
|
| 
       67 
78 
     | 
    
         
             
                    raise LinkPlayRequestException(f"Didn't receive expected OK from {endpoint}")
         
     | 
| 
       68 
79 
     | 
    
         | 
| 
       69 
80 
     | 
    
         | 
| 
      
 81 
     | 
    
         
            +
            async def call_tcpuart(
         
     | 
| 
      
 82 
     | 
    
         
            +
                reader: asyncio.StreamReader, writer: asyncio.StreamWriter, cmd: str
         
     | 
| 
      
 83 
     | 
    
         
            +
            ) -> str:
         
     | 
| 
      
 84 
     | 
    
         
            +
                """Get the latest data from TCP UART service."""
         
     | 
| 
      
 85 
     | 
    
         
            +
                payload_header: str = "18 96 18 20 "
         
     | 
| 
      
 86 
     | 
    
         
            +
                payload_length: str = format(len(cmd), "02x")
         
     | 
| 
      
 87 
     | 
    
         
            +
                payload_command_header: str = " 00 00 00 c1 02 00 00 00 00 00 00 00 00 00 00 "
         
     | 
| 
      
 88 
     | 
    
         
            +
                payload_command_content: str = " ".join(hex(ord(c))[2:] for c in cmd)
         
     | 
| 
      
 89 
     | 
    
         
            +
             
     | 
| 
      
 90 
     | 
    
         
            +
                async with async_timeout.timeout(API_TIMEOUT):
         
     | 
| 
      
 91 
     | 
    
         
            +
                    writer.write(
         
     | 
| 
      
 92 
     | 
    
         
            +
                        bytes.fromhex(
         
     | 
| 
      
 93 
     | 
    
         
            +
                            payload_header
         
     | 
| 
      
 94 
     | 
    
         
            +
                            + payload_length
         
     | 
| 
      
 95 
     | 
    
         
            +
                            + payload_command_header
         
     | 
| 
      
 96 
     | 
    
         
            +
                            + payload_command_content
         
     | 
| 
      
 97 
     | 
    
         
            +
                        )
         
     | 
| 
      
 98 
     | 
    
         
            +
                    )
         
     | 
| 
      
 99 
     | 
    
         
            +
             
     | 
| 
      
 100 
     | 
    
         
            +
                    data: bytes = await reader.read(TCP_MESSAGE_LENGTH)
         
     | 
| 
      
 101 
     | 
    
         
            +
             
     | 
| 
      
 102 
     | 
    
         
            +
                    if data == b"":
         
     | 
| 
      
 103 
     | 
    
         
            +
                        raise LinkPlayRequestException("No data received from socket")
         
     | 
| 
      
 104 
     | 
    
         
            +
             
     | 
| 
      
 105 
     | 
    
         
            +
                    return str(repr(data))
         
     | 
| 
      
 106 
     | 
    
         
            +
             
     | 
| 
      
 107 
     | 
    
         
            +
             
     | 
| 
      
 108 
     | 
    
         
            +
            async def call_tcpuart_json(
         
     | 
| 
      
 109 
     | 
    
         
            +
                reader: asyncio.StreamReader, writer: asyncio.StreamWriter, cmd: str
         
     | 
| 
      
 110 
     | 
    
         
            +
            ) -> dict[str, str]:
         
     | 
| 
      
 111 
     | 
    
         
            +
                """Get JSON data from TCPUART service."""
         
     | 
| 
      
 112 
     | 
    
         
            +
                raw_response: str = await call_tcpuart(reader, writer, cmd)
         
     | 
| 
      
 113 
     | 
    
         
            +
                strip_start = raw_response.find("{")
         
     | 
| 
      
 114 
     | 
    
         
            +
                strip_end = raw_response.find("}", strip_start) + 1
         
     | 
| 
      
 115 
     | 
    
         
            +
                data = raw_response[strip_start:strip_end]
         
     | 
| 
      
 116 
     | 
    
         
            +
                return json.loads(data)  # type: ignore
         
     | 
| 
      
 117 
     | 
    
         
            +
             
     | 
| 
      
 118 
     | 
    
         
            +
             
     | 
| 
       70 
119 
     | 
    
         
             
            def decode_hexstr(hexstr: str) -> str:
         
     | 
| 
       71 
120 
     | 
    
         
             
                """Decode a hex string."""
         
     | 
| 
       72 
121 
     | 
    
         
             
                try:
         
     | 
| 
         @@ -75,6 +124,7 @@ def decode_hexstr(hexstr: str) -> str: 
     | 
|
| 
       75 
124 
     | 
    
         
             
                    return hexstr
         
     | 
| 
       76 
125 
     | 
    
         | 
| 
       77 
126 
     | 
    
         | 
| 
      
 127 
     | 
    
         
            +
            @deprecated(version="0.0.9", reason="Use async_create_unverified_context instead")
         
     | 
| 
       78 
128 
     | 
    
         
             
            def create_unverified_context() -> ssl.SSLContext:
         
     | 
| 
       79 
129 
     | 
    
         
             
                """Creates an unverified SSL context with the default mTLS certificate."""
         
     | 
| 
       80 
130 
     | 
    
         
             
                dirs = AppDirs("python-linkplay")
         
     | 
| 
         @@ -90,16 +140,38 @@ def create_unverified_context() -> ssl.SSLContext: 
     | 
|
| 
       90 
140 
     | 
    
         
             
                return create_ssl_context(path=mtls_certificate_path)
         
     | 
| 
       91 
141 
     | 
    
         | 
| 
       92 
142 
     | 
    
         | 
| 
       93 
     | 
    
         
            -
            async def async_create_unverified_context( 
     | 
| 
      
 143 
     | 
    
         
            +
            async def async_create_unverified_context(
         
     | 
| 
      
 144 
     | 
    
         
            +
                executor: ThreadPoolExecutor | None = None,
         
     | 
| 
      
 145 
     | 
    
         
            +
            ) -> ssl.SSLContext:
         
     | 
| 
       94 
146 
     | 
    
         
             
                """Asynchronously creates an unverified SSL context with the default mTLS certificate."""
         
     | 
| 
       95 
147 
     | 
    
         
             
                async with aiofiles.tempfile.NamedTemporaryFile(
         
     | 
| 
       96 
148 
     | 
    
         
             
                    "w", encoding="utf-8"
         
     | 
| 
       97 
149 
     | 
    
         
             
                ) as mtls_certificate:
         
     | 
| 
       98 
150 
     | 
    
         
             
                    await mtls_certificate.write(MTLS_CERTIFICATE_CONTENTS)
         
     | 
| 
       99 
151 
     | 
    
         
             
                    await mtls_certificate.flush()
         
     | 
| 
       100 
     | 
    
         
            -
                     
     | 
| 
      
 152 
     | 
    
         
            +
                    certfile: str = str(mtls_certificate.name)
         
     | 
| 
      
 153 
     | 
    
         
            +
                    return await async_create_ssl_context(certfile=certfile, executor=executor)
         
     | 
| 
       101 
154 
     | 
    
         | 
| 
       102 
155 
     | 
    
         | 
| 
      
 156 
     | 
    
         
            +
            async def async_create_ssl_context(
         
     | 
| 
      
 157 
     | 
    
         
            +
                *, certfile: str, executor: ThreadPoolExecutor | None = None
         
     | 
| 
      
 158 
     | 
    
         
            +
            ) -> ssl.SSLContext:
         
     | 
| 
      
 159 
     | 
    
         
            +
                """Creates an SSL context from given certificate file."""
         
     | 
| 
      
 160 
     | 
    
         
            +
                sslcontext: ssl.SSLContext = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
         
     | 
| 
      
 161 
     | 
    
         
            +
                sslcontext.check_hostname = False
         
     | 
| 
      
 162 
     | 
    
         
            +
                sslcontext.verify_mode = ssl.CERT_NONE
         
     | 
| 
      
 163 
     | 
    
         
            +
             
     | 
| 
      
 164 
     | 
    
         
            +
                loop = asyncio.get_running_loop()
         
     | 
| 
      
 165 
     | 
    
         
            +
                await loop.run_in_executor(executor, sslcontext.load_cert_chain, certfile)
         
     | 
| 
      
 166 
     | 
    
         
            +
             
     | 
| 
      
 167 
     | 
    
         
            +
                with contextlib.suppress(AttributeError):
         
     | 
| 
      
 168 
     | 
    
         
            +
                    # This only works for OpenSSL >= 1.0.0
         
     | 
| 
      
 169 
     | 
    
         
            +
                    sslcontext.options |= ssl.OP_NO_COMPRESSION
         
     | 
| 
      
 170 
     | 
    
         
            +
                sslcontext.set_default_verify_paths()
         
     | 
| 
      
 171 
     | 
    
         
            +
                return sslcontext
         
     | 
| 
      
 172 
     | 
    
         
            +
             
     | 
| 
      
 173 
     | 
    
         
            +
             
     | 
| 
      
 174 
     | 
    
         
            +
            @deprecated(version="0.0.9", reason="Use async_create_ssl_context instead")
         
     | 
| 
       103 
175 
     | 
    
         
             
            def create_ssl_context(path: str) -> ssl.SSLContext:
         
     | 
| 
       104 
176 
     | 
    
         
             
                """Creates an SSL context from given certificate file."""
         
     | 
| 
       105 
177 
     | 
    
         
             
                sslcontext: ssl.SSLContext = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
         
     | 
| 
         @@ -113,6 +185,9 @@ def create_ssl_context(path: str) -> ssl.SSLContext: 
     | 
|
| 
       113 
185 
     | 
    
         
             
                return sslcontext
         
     | 
| 
       114 
186 
     | 
    
         | 
| 
       115 
187 
     | 
    
         | 
| 
      
 188 
     | 
    
         
            +
            @deprecated(
         
     | 
| 
      
 189 
     | 
    
         
            +
                version="0.0.9", reason="Use async_create_unverified_client_session instead"
         
     | 
| 
      
 190 
     | 
    
         
            +
            )
         
     | 
| 
       116 
191 
     | 
    
         
             
            def create_unverified_client_session() -> ClientSession:
         
     | 
| 
       117 
192 
     | 
    
         
             
                """Creates a ClientSession using the default unverified SSL context"""
         
     | 
| 
       118 
193 
     | 
    
         
             
                context: ssl.SSLContext = create_unverified_context()
         
     | 
| 
         @@ -125,3 +200,27 @@ async def async_create_unverified_client_session() -> ClientSession: 
     | 
|
| 
       125 
200 
     | 
    
         
             
                context: ssl.SSLContext = await async_create_unverified_context()
         
     | 
| 
       126 
201 
     | 
    
         
             
                connector: TCPConnector = TCPConnector(family=socket.AF_UNSPEC, ssl=context)
         
     | 
| 
       127 
202 
     | 
    
         
             
                return ClientSession(connector=connector)
         
     | 
| 
      
 203 
     | 
    
         
            +
             
     | 
| 
      
 204 
     | 
    
         
            +
             
     | 
| 
      
 205 
     | 
    
         
            +
            def fixup_player_properties(
         
     | 
| 
      
 206 
     | 
    
         
            +
                properties: dict[PlayerAttribute, str],
         
     | 
| 
      
 207 
     | 
    
         
            +
            ) -> dict[PlayerAttribute, str]:
         
     | 
| 
      
 208 
     | 
    
         
            +
                """Fixes up PlayerAttribute in a dict."""
         
     | 
| 
      
 209 
     | 
    
         
            +
                properties[PlayerAttribute.TITLE] = decode_hexstr(
         
     | 
| 
      
 210 
     | 
    
         
            +
                    properties.get(PlayerAttribute.TITLE, "")
         
     | 
| 
      
 211 
     | 
    
         
            +
                )
         
     | 
| 
      
 212 
     | 
    
         
            +
                properties[PlayerAttribute.ARTIST] = decode_hexstr(
         
     | 
| 
      
 213 
     | 
    
         
            +
                    properties.get(PlayerAttribute.ARTIST, "")
         
     | 
| 
      
 214 
     | 
    
         
            +
                )
         
     | 
| 
      
 215 
     | 
    
         
            +
                properties[PlayerAttribute.ALBUM] = decode_hexstr(
         
     | 
| 
      
 216 
     | 
    
         
            +
                    properties.get(PlayerAttribute.ALBUM, "")
         
     | 
| 
      
 217 
     | 
    
         
            +
                )
         
     | 
| 
      
 218 
     | 
    
         
            +
             
     | 
| 
      
 219 
     | 
    
         
            +
                # Fixup playing status "none" by setting it to "stopped"
         
     | 
| 
      
 220 
     | 
    
         
            +
                if (
         
     | 
| 
      
 221 
     | 
    
         
            +
                    properties.get(PlayerAttribute.PLAYING_STATUS, "")
         
     | 
| 
      
 222 
     | 
    
         
            +
                    not in PlayingStatus.__members__.values()
         
     | 
| 
      
 223 
     | 
    
         
            +
                ):
         
     | 
| 
      
 224 
     | 
    
         
            +
                    properties[PlayerAttribute.PLAYING_STATUS] = PlayingStatus.STOPPED
         
     | 
| 
      
 225 
     | 
    
         
            +
             
     | 
| 
      
 226 
     | 
    
         
            +
                return properties
         
     | 
| 
         @@ -1,6 +1,6 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            Metadata-Version: 2.1
         
     | 
| 
       2 
2 
     | 
    
         
             
            Name: python_linkplay
         
     | 
| 
       3 
     | 
    
         
            -
            Version: 0.0. 
     | 
| 
      
 3 
     | 
    
         
            +
            Version: 0.0.10
         
     | 
| 
       4 
4 
     | 
    
         
             
            Summary: A Python Library for Seamless LinkPlay Device Control
         
     | 
| 
       5 
5 
     | 
    
         
             
            Author: Velleman Group nv
         
     | 
| 
       6 
6 
     | 
    
         
             
            License: MIT
         
     | 
| 
         @@ -34,7 +34,7 @@ A Python Library for Seamless LinkPlay Device Control 
     | 
|
| 
       34 
34 
     | 
    
         | 
| 
       35 
35 
     | 
    
         
             
            ## Intro
         
     | 
| 
       36 
36 
     | 
    
         | 
| 
       37 
     | 
    
         
            -
            Welcome to python-linkplay, a powerful and user-friendly Python library designed to simplify the integration and control of LinkPlay-enabled devices in your projects. LinkPlay technology empowers a wide range of smart audio devices, making them interconnected and easily controllable. With python- 
     | 
| 
      
 37 
     | 
    
         
            +
            Welcome to python-linkplay, a powerful and user-friendly Python library designed to simplify the integration and control of LinkPlay-enabled devices in your projects. LinkPlay technology empowers a wide range of smart audio devices, making them interconnected and easily controllable. With python-linkplay, you can harness this capability and seamlessly manage your LinkPlay devices from within your Python applications.
         
     | 
| 
       38 
38 
     | 
    
         | 
| 
       39 
39 
     | 
    
         
             
            ## Key features
         
     | 
| 
       40 
40 
     | 
    
         | 
| 
         @@ -0,0 +1,15 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            linkplay/__init__.py,sha256=y9ZehEq-KhS3cwn-PUpwVSJGfDUx7e5wf_G6guODcTk,56
         
     | 
| 
      
 2 
     | 
    
         
            +
            linkplay/__main__.py,sha256=Wcza80QaWfOaHjyJEfQYhB9kiPLE0NOqIj4zVWv2Nqs,577
         
     | 
| 
      
 3 
     | 
    
         
            +
            linkplay/__version__.py,sha256=op0YgbBDjpD8XP_2_aEIvFkIRz9w0nlrOp56GUt0XxQ,23
         
     | 
| 
      
 4 
     | 
    
         
            +
            linkplay/bridge.py,sha256=LXUc1zcRh1Hx1QauhlpA9da5k7f6h3KLfGRA1jAbTPU,11602
         
     | 
| 
      
 5 
     | 
    
         
            +
            linkplay/consts.py,sha256=OGEj34YTiEWRBPjIebokDOVKOsa-DpZkCkUpThO8IIc,13068
         
     | 
| 
      
 6 
     | 
    
         
            +
            linkplay/controller.py,sha256=IYoXvHh2zhrsRoRG7gwYFoWSIrL5Hl9hR7c2dhGPNX8,2484
         
     | 
| 
      
 7 
     | 
    
         
            +
            linkplay/discovery.py,sha256=aEzN_94pKLmHKYIL7DxSW0FYRsaF2ruZe2bwXz0zf5U,4299
         
     | 
| 
      
 8 
     | 
    
         
            +
            linkplay/endpoint.py,sha256=aWNiiU6h3gIWiNzcnavfA8IMZLufv9A8Cm5qphRpRvA,2158
         
     | 
| 
      
 9 
     | 
    
         
            +
            linkplay/exceptions.py,sha256=tWJWHsKVkUEq3Yet1Z739IxcaQT8YamDeSp0tqHde9c,107
         
     | 
| 
      
 10 
     | 
    
         
            +
            linkplay/utils.py,sha256=WVKdxITDymLCmKGqlD9Ieyb96qZ-QSC9oIe-KGW4IFU,7827
         
     | 
| 
      
 11 
     | 
    
         
            +
            python_linkplay-0.0.10.dist-info/LICENSE,sha256=bgEtxMyjEHX_4uwaAY3GCFTm234D4AOZ5dM15sk26ms,1073
         
     | 
| 
      
 12 
     | 
    
         
            +
            python_linkplay-0.0.10.dist-info/METADATA,sha256=ZzxS4W64XCLZXbDssg4YNhtbAmDkAgg-_aF_tY6SJYs,2988
         
     | 
| 
      
 13 
     | 
    
         
            +
            python_linkplay-0.0.10.dist-info/WHEEL,sha256=cVxcB9AmuTcXqmwrtPhNK88dr7IR_b6qagTj0UvIEbY,91
         
     | 
| 
      
 14 
     | 
    
         
            +
            python_linkplay-0.0.10.dist-info/top_level.txt,sha256=CpSaOVPTzJf5TVIL7MrotSCR34gcIOQy-11l4zGmxxM,9
         
     | 
| 
      
 15 
     | 
    
         
            +
            python_linkplay-0.0.10.dist-info/RECORD,,
         
     | 
| 
         @@ -1,15 +0,0 @@ 
     | 
|
| 
       1 
     | 
    
         
            -
            linkplay/__init__.py,sha256=y9ZehEq-KhS3cwn-PUpwVSJGfDUx7e5wf_G6guODcTk,56
         
     | 
| 
       2 
     | 
    
         
            -
            linkplay/__main__.py,sha256=Wcza80QaWfOaHjyJEfQYhB9kiPLE0NOqIj4zVWv2Nqs,577
         
     | 
| 
       3 
     | 
    
         
            -
            linkplay/__version__.py,sha256=YQY-i8MemQTvHljd9BqOGgeJEu3FrzurE3TEKDwAax4,22
         
     | 
| 
       4 
     | 
    
         
            -
            linkplay/bridge.py,sha256=KGD-gwJRhDIdE9jAOUyVlcpfGDb1diDUkggTDvVkf-M,11164
         
     | 
| 
       5 
     | 
    
         
            -
            linkplay/consts.py,sha256=wz1lVRz-9hkymc9ucV_LHldcu-msYvimI0tjr2Ncgoc,12734
         
     | 
| 
       6 
     | 
    
         
            -
            linkplay/controller.py,sha256=JIQAKPs3EK7ZwzoyzSy0HBl21gH9Cc9RrLXIGOMzkCM,2146
         
     | 
| 
       7 
     | 
    
         
            -
            linkplay/discovery.py,sha256=aEzN_94pKLmHKYIL7DxSW0FYRsaF2ruZe2bwXz0zf5U,4299
         
     | 
| 
       8 
     | 
    
         
            -
            linkplay/endpoint.py,sha256=qbB977_KltNRZlWlm-3JiByPZiie84Hn2TL523IfqGs,1486
         
     | 
| 
       9 
     | 
    
         
            -
            linkplay/exceptions.py,sha256=tWJWHsKVkUEq3Yet1Z739IxcaQT8YamDeSp0tqHde9c,107
         
     | 
| 
       10 
     | 
    
         
            -
            linkplay/utils.py,sha256=gEFrajQHejetKaVpYWxkZLn7Nh4W3PaQSknDMU2eKIU,4520
         
     | 
| 
       11 
     | 
    
         
            -
            python_linkplay-0.0.8.dist-info/LICENSE,sha256=bgEtxMyjEHX_4uwaAY3GCFTm234D4AOZ5dM15sk26ms,1073
         
     | 
| 
       12 
     | 
    
         
            -
            python_linkplay-0.0.8.dist-info/METADATA,sha256=IAO7Ix1g0elKKcDGjYpXlrTx1bDfb9vMnitiAOdgWcA,2987
         
     | 
| 
       13 
     | 
    
         
            -
            python_linkplay-0.0.8.dist-info/WHEEL,sha256=HiCZjzuy6Dw0hdX5R3LCFPDmFS4BWl8H-8W39XfmgX4,91
         
     | 
| 
       14 
     | 
    
         
            -
            python_linkplay-0.0.8.dist-info/top_level.txt,sha256=CpSaOVPTzJf5TVIL7MrotSCR34gcIOQy-11l4zGmxxM,9
         
     | 
| 
       15 
     | 
    
         
            -
            python_linkplay-0.0.8.dist-info/RECORD,,
         
     | 
| 
         
            File without changes
         
     | 
| 
         
            File without changes
         
     |