ryseble 1.0.0__tar.gz
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.
- ryseble-1.0.0/LICENSE +20 -0
- ryseble-1.0.0/PKG-INFO +43 -0
- ryseble-1.0.0/README.md +28 -0
- ryseble-1.0.0/pyproject.toml +21 -0
- ryseble-1.0.0/ryseble/__init__.py +26 -0
- ryseble-1.0.0/ryseble/bluetoothctl.py +72 -0
- ryseble-1.0.0/ryseble/constants.py +8 -0
- ryseble-1.0.0/ryseble/device.py +126 -0
- ryseble-1.0.0/ryseble/packets.py +24 -0
- ryseble-1.0.0/ryseble.egg-info/PKG-INFO +43 -0
- ryseble-1.0.0/ryseble.egg-info/SOURCES.txt +13 -0
- ryseble-1.0.0/ryseble.egg-info/dependency_links.txt +1 -0
- ryseble-1.0.0/ryseble.egg-info/requires.txt +1 -0
- ryseble-1.0.0/ryseble.egg-info/top_level.txt +1 -0
- ryseble-1.0.0/setup.cfg +4 -0
ryseble-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
Copyright (c) 2025 RYSE
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
8
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
9
|
+
in the Software without restriction, including without limitation the rights
|
|
10
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
11
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
12
|
+
furnished to do so, subject to the following conditions:
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
16
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
17
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
18
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
19
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
20
|
+
SOFTWARE.
|
ryseble-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ryseble
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Python library for RYSE BLE Smart Shade devices (used by Home Assistant integration).
|
|
5
|
+
Author-email: MOHAMED Kallel <mohamed.kallel@yahoo.fr>
|
|
6
|
+
License: MIT
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Requires-Python: >=3.9
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: bleak>=0.22.0
|
|
14
|
+
Dynamic: license-file
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# === README.md ===
|
|
18
|
+
# ryseble
|
|
19
|
+
|
|
20
|
+
A small Python library to interact with RYSE BLE Smart Shade devices. Designed to be
|
|
21
|
+
used by Home Assistant integrations but reusable in other projects.
|
|
22
|
+
|
|
23
|
+
## Features
|
|
24
|
+
- Pair/connect/disconnect with device
|
|
25
|
+
- Read/write raw packets
|
|
26
|
+
- Helpers to build position/get-position packets
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install ryseble
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Usage (example)
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from ryseble.device import RyseBLEDevice
|
|
38
|
+
from ryseble.packets import build_position_packet, build_get_position_packet
|
|
39
|
+
|
|
40
|
+
device = RyseBLEDevice(address="AA:BB:CC:DD:EE:FF", rx_uuid="...", tx_uuid="...")
|
|
41
|
+
await device.pair()
|
|
42
|
+
await device.write_data(build_position_packet(50))
|
|
43
|
+
```
|
ryseble-1.0.0/README.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
|
|
2
|
+
# === README.md ===
|
|
3
|
+
# ryseble
|
|
4
|
+
|
|
5
|
+
A small Python library to interact with RYSE BLE Smart Shade devices. Designed to be
|
|
6
|
+
used by Home Assistant integrations but reusable in other projects.
|
|
7
|
+
|
|
8
|
+
## Features
|
|
9
|
+
- Pair/connect/disconnect with device
|
|
10
|
+
- Read/write raw packets
|
|
11
|
+
- Helpers to build position/get-position packets
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install ryseble
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Usage (example)
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
from ryseble.device import RyseBLEDevice
|
|
23
|
+
from ryseble.packets import build_position_packet, build_get_position_packet
|
|
24
|
+
|
|
25
|
+
device = RyseBLEDevice(address="AA:BB:CC:DD:EE:FF", rx_uuid="...", tx_uuid="...")
|
|
26
|
+
await device.pair()
|
|
27
|
+
await device.write_data(build_position_packet(50))
|
|
28
|
+
```
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "ryseble"
|
|
3
|
+
version = "1.0.0"
|
|
4
|
+
description = "Python library for RYSE BLE Smart Shade devices (used by Home Assistant integration)."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.9"
|
|
7
|
+
license = {text = "MIT"}
|
|
8
|
+
authors = [ { name = "MOHAMED Kallel", email = "mohamed.kallel@yahoo.fr" } ]
|
|
9
|
+
classifiers = [
|
|
10
|
+
"Programming Language :: Python :: 3",
|
|
11
|
+
"License :: OSI Approved :: MIT License",
|
|
12
|
+
"Operating System :: OS Independent",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
dependencies = [
|
|
16
|
+
"bleak>=0.22.0"
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[build-system]
|
|
20
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
21
|
+
build-backend = "setuptools.build_meta"
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Ryse BLE Python Library
|
|
3
|
+
~~~~~~~~~~~~~~~~~~~~~~~
|
|
4
|
+
This library provides tools to communicate with Ryse gears
|
|
5
|
+
over Bluetooth Low Energy (BLE).
|
|
6
|
+
|
|
7
|
+
Modules:
|
|
8
|
+
- device: RyseBLEDevice class for managing connections
|
|
9
|
+
- packets: Helpers to build BLE packets
|
|
10
|
+
- constants: Protocol constants and UUIDs
|
|
11
|
+
- bluetoothctl: Bluetoothctl wrapper functions
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from .device import RyseBLEDevice
|
|
15
|
+
from .packets import build_position_packet, build_get_position_packet
|
|
16
|
+
from .constants import HARDCODED_UUIDS, PAIRING_MODE_FLAG
|
|
17
|
+
from . import bluetoothctl
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"RyseBLEDevice",
|
|
21
|
+
"build_position_packet",
|
|
22
|
+
"build_get_position_packet",
|
|
23
|
+
"HARDCODED_UUIDS",
|
|
24
|
+
"PAIRING_MODE_FLAG",
|
|
25
|
+
"bluetoothctl",
|
|
26
|
+
]
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import subprocess
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
def close_process(process: subprocess.Popen) -> None:
|
|
6
|
+
"""Close a running process gracefully."""
|
|
7
|
+
if process and process.poll() is None:
|
|
8
|
+
process.terminate()
|
|
9
|
+
try:
|
|
10
|
+
process.wait(timeout=2)
|
|
11
|
+
except subprocess.TimeoutExpired:
|
|
12
|
+
process.kill()
|
|
13
|
+
|
|
14
|
+
async def run_command(command: str) -> str:
|
|
15
|
+
"""Run a shell command asynchronously and return its output."""
|
|
16
|
+
proc = await asyncio.create_subprocess_shell(
|
|
17
|
+
command,
|
|
18
|
+
stdout=asyncio.subprocess.PIPE,
|
|
19
|
+
stderr=asyncio.subprocess.PIPE,
|
|
20
|
+
)
|
|
21
|
+
stdout, stderr = await proc.communicate()
|
|
22
|
+
if proc.returncode != 0:
|
|
23
|
+
raise RuntimeError(f"Command failed: {command}\n{stderr.decode()}")
|
|
24
|
+
return stdout.decode().strip()
|
|
25
|
+
|
|
26
|
+
def start_bluetoothctl() -> subprocess.Popen:
|
|
27
|
+
"""Start a bluetoothctl subprocess."""
|
|
28
|
+
return subprocess.Popen(
|
|
29
|
+
["bluetoothctl"],
|
|
30
|
+
stdin=subprocess.PIPE,
|
|
31
|
+
stdout=subprocess.PIPE,
|
|
32
|
+
stderr=subprocess.PIPE,
|
|
33
|
+
text=True,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
async def send_command_in_process(
|
|
37
|
+
process: subprocess.Popen, command: str, delay: float = 2
|
|
38
|
+
) -> None:
|
|
39
|
+
"""Send a command to bluetoothctl process."""
|
|
40
|
+
if process.stdin is None:
|
|
41
|
+
raise RuntimeError("Process has no stdin")
|
|
42
|
+
process.stdin.write(command + "\n")
|
|
43
|
+
process.stdin.flush()
|
|
44
|
+
await asyncio.sleep(delay)
|
|
45
|
+
|
|
46
|
+
async def is_device_connected(address: str) -> bool:
|
|
47
|
+
"""Check if a device is connected via bluetoothctl."""
|
|
48
|
+
output = await run_command(f"bluetoothctl info {address}")
|
|
49
|
+
return "Connected: yes" in output
|
|
50
|
+
|
|
51
|
+
async def is_device_bonded(address: str) -> bool:
|
|
52
|
+
"""Check if a device is bonded via bluetoothctl."""
|
|
53
|
+
output = await run_command(f"bluetoothctl info {address}")
|
|
54
|
+
return "Bonded: yes" in output
|
|
55
|
+
|
|
56
|
+
async def is_device_paired(address: str) -> bool:
|
|
57
|
+
"""Check if a device is paired via bluetoothctl."""
|
|
58
|
+
output = await run_command(f"bluetoothctl info {address}")
|
|
59
|
+
return "Paired: yes" in output
|
|
60
|
+
|
|
61
|
+
async def get_first_manufacturer_data_byte(mac_address: str) -> Optional[int]:
|
|
62
|
+
"""Fetch the first manufacturer data byte for a given MAC address."""
|
|
63
|
+
output = await run_command(f"bluetoothctl info {mac_address}")
|
|
64
|
+
for line in output.splitlines():
|
|
65
|
+
if "ManufacturerData Key" in line:
|
|
66
|
+
try:
|
|
67
|
+
# Extract first byte (assumes hex like 0x01 0x02 …)
|
|
68
|
+
hex_values = line.split(":")[1].strip().split()
|
|
69
|
+
return int(hex_values[0], 16)
|
|
70
|
+
except Exception:
|
|
71
|
+
return None
|
|
72
|
+
return None
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""RyseBLEDevice: async wrapper around Bleak for RYSE devices.
|
|
2
|
+
|
|
3
|
+
This module intentionally keeps responsibilities small: connect/disconnect,
|
|
4
|
+
read/write GATT characteristics, and deliver notifications via a callback.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
from typing import Callable, Optional
|
|
11
|
+
from bleak import BleakClient, BleakScanner
|
|
12
|
+
import logging
|
|
13
|
+
|
|
14
|
+
_LOGGER = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
class RyseBLEDevice:
|
|
17
|
+
"""Represent a RYSE device and provide async methods to interact with it."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, address: Optional[str] = None, rx_uuid: Optional[str] = None, tx_uuid: Optional[str] = None):
|
|
20
|
+
self.address = address
|
|
21
|
+
self.rx_uuid = rx_uuid
|
|
22
|
+
self.tx_uuid = tx_uuid
|
|
23
|
+
self.client: Optional[BleakClient] = None
|
|
24
|
+
# Optional async callback: async def cb(position: int)
|
|
25
|
+
self.update_callback: Optional[Callable[[int], None]] = None
|
|
26
|
+
|
|
27
|
+
async def pair(self, timeout: float = 30.0) -> bool:
|
|
28
|
+
"""Connect to the device and subscribe to notifications."""
|
|
29
|
+
if not self.address:
|
|
30
|
+
_LOGGER.error("No device address provided for pairing.")
|
|
31
|
+
return False
|
|
32
|
+
|
|
33
|
+
_LOGGER.debug("Connecting to device %s", self.address)
|
|
34
|
+
self.client = BleakClient(self.address)
|
|
35
|
+
try:
|
|
36
|
+
await self.client.connect(timeout=timeout)
|
|
37
|
+
if self.client.is_connected:
|
|
38
|
+
_LOGGER.debug("Connected to %s", self.address)
|
|
39
|
+
if self.rx_uuid:
|
|
40
|
+
try:
|
|
41
|
+
await self.client.start_notify(self.rx_uuid, self._notification_handler)
|
|
42
|
+
_LOGGER.debug("Started notify on %s", self.rx_uuid)
|
|
43
|
+
except Exception as e:
|
|
44
|
+
_LOGGER.debug("Could not start notify: %s", e)
|
|
45
|
+
return True
|
|
46
|
+
except Exception as e:
|
|
47
|
+
_LOGGER.error("Failed to connect to %s: %s", self.address, e)
|
|
48
|
+
return False
|
|
49
|
+
|
|
50
|
+
async def disconnect(self):
|
|
51
|
+
if self.client:
|
|
52
|
+
try:
|
|
53
|
+
if self.rx_uuid:
|
|
54
|
+
try:
|
|
55
|
+
await self.client.stop_notify(self.rx_uuid)
|
|
56
|
+
except Exception:
|
|
57
|
+
pass
|
|
58
|
+
await self.client.disconnect()
|
|
59
|
+
except Exception as e:
|
|
60
|
+
_LOGGER.debug("Error during disconnect: %s", e)
|
|
61
|
+
finally:
|
|
62
|
+
self.client = None
|
|
63
|
+
|
|
64
|
+
async def _notification_handler(self, sender, data: bytes):
|
|
65
|
+
"""Handle BLE notifications from the device.
|
|
66
|
+
|
|
67
|
+
Filter and extract position update (protocol-specific). If a callback
|
|
68
|
+
is registered it will be awaited.
|
|
69
|
+
"""
|
|
70
|
+
try:
|
|
71
|
+
# Basic validation based on observed protocol
|
|
72
|
+
if len(data) >= 5 and data[0] == 0xF5 and data[2] == 0x01 and data[3] == 0x18:
|
|
73
|
+
# ignore REPORT USER TARGET data
|
|
74
|
+
return
|
|
75
|
+
|
|
76
|
+
_LOGGER.debug("Received notification: %s", data.hex())
|
|
77
|
+
|
|
78
|
+
if len(data) >= 5 and data[0] == 0xF5 and data[2] == 0x01 and data[3] == 0x07:
|
|
79
|
+
position = data[4]
|
|
80
|
+
_LOGGER.debug("Parsed position: %d", position)
|
|
81
|
+
if self.update_callback:
|
|
82
|
+
# allow callback to be coroutine or normal function
|
|
83
|
+
if asyncio.iscoroutinefunction(self.update_callback):
|
|
84
|
+
await self.update_callback(position)
|
|
85
|
+
else:
|
|
86
|
+
# run sync callbacks in event loop
|
|
87
|
+
loop = asyncio.get_running_loop()
|
|
88
|
+
loop.call_soon(self.update_callback, position)
|
|
89
|
+
except Exception as e:
|
|
90
|
+
_LOGGER.exception("Error in notification handler: %s", e)
|
|
91
|
+
|
|
92
|
+
async def read_data(self) -> Optional[bytes]:
|
|
93
|
+
"""Read raw data from RX characteristic (if available)."""
|
|
94
|
+
if not self.client or not self.client.is_connected:
|
|
95
|
+
_LOGGER.debug("Client not connected for read")
|
|
96
|
+
return None
|
|
97
|
+
if not self.rx_uuid:
|
|
98
|
+
_LOGGER.debug("No RX UUID configured for read")
|
|
99
|
+
return None
|
|
100
|
+
try:
|
|
101
|
+
data = await self.client.read_gatt_char(self.rx_uuid)
|
|
102
|
+
return bytes(data)
|
|
103
|
+
except Exception as e:
|
|
104
|
+
_LOGGER.error("read_data failed: %s", e)
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
async def write_data(self, data: bytes) -> bool:
|
|
108
|
+
"""Write raw bytes to TX characteristic."""
|
|
109
|
+
if not self.client or not self.client.is_connected:
|
|
110
|
+
_LOGGER.debug("Client not connected for write")
|
|
111
|
+
return False
|
|
112
|
+
if not self.tx_uuid:
|
|
113
|
+
_LOGGER.debug("No TX UUID configured for write")
|
|
114
|
+
return False
|
|
115
|
+
try:
|
|
116
|
+
await self.client.write_gatt_char(self.tx_uuid, data)
|
|
117
|
+
_LOGGER.debug("Wrote %d bytes to %s", len(data), self.tx_uuid)
|
|
118
|
+
return True
|
|
119
|
+
except Exception as e:
|
|
120
|
+
_LOGGER.error("write_data failed: %s", e)
|
|
121
|
+
return False
|
|
122
|
+
|
|
123
|
+
@staticmethod
|
|
124
|
+
async def discover(timeout: float = 5.0):
|
|
125
|
+
"""Discover BLE devices using BleakScanner; returns list of bleak device objects."""
|
|
126
|
+
return await BleakScanner.discover(timeout=timeout)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
def build_position_packet(pos: int) -> bytes:
|
|
2
|
+
"""Build a packet to set the shade position.
|
|
3
|
+
|
|
4
|
+
Args:
|
|
5
|
+
pos: Desired position (0–100).
|
|
6
|
+
|
|
7
|
+
Returns:
|
|
8
|
+
Bytes representing the command packet.
|
|
9
|
+
"""
|
|
10
|
+
if not (0 <= pos <= 100):
|
|
11
|
+
raise ValueError("Position must be between 0 and 100")
|
|
12
|
+
|
|
13
|
+
# Example format: [0x01, pos]
|
|
14
|
+
return bytes([0x01, pos])
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def build_get_position_packet() -> bytes:
|
|
18
|
+
"""Build a packet to request the current shade position.
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
Bytes representing the command packet.
|
|
22
|
+
"""
|
|
23
|
+
# Example format: [0x02]
|
|
24
|
+
return bytes([0x02])
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ryseble
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Python library for RYSE BLE Smart Shade devices (used by Home Assistant integration).
|
|
5
|
+
Author-email: MOHAMED Kallel <mohamed.kallel@yahoo.fr>
|
|
6
|
+
License: MIT
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Requires-Python: >=3.9
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: bleak>=0.22.0
|
|
14
|
+
Dynamic: license-file
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# === README.md ===
|
|
18
|
+
# ryseble
|
|
19
|
+
|
|
20
|
+
A small Python library to interact with RYSE BLE Smart Shade devices. Designed to be
|
|
21
|
+
used by Home Assistant integrations but reusable in other projects.
|
|
22
|
+
|
|
23
|
+
## Features
|
|
24
|
+
- Pair/connect/disconnect with device
|
|
25
|
+
- Read/write raw packets
|
|
26
|
+
- Helpers to build position/get-position packets
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install ryseble
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Usage (example)
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from ryseble.device import RyseBLEDevice
|
|
38
|
+
from ryseble.packets import build_position_packet, build_get_position_packet
|
|
39
|
+
|
|
40
|
+
device = RyseBLEDevice(address="AA:BB:CC:DD:EE:FF", rx_uuid="...", tx_uuid="...")
|
|
41
|
+
await device.pair()
|
|
42
|
+
await device.write_data(build_position_packet(50))
|
|
43
|
+
```
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
ryseble/__init__.py
|
|
5
|
+
ryseble/bluetoothctl.py
|
|
6
|
+
ryseble/constants.py
|
|
7
|
+
ryseble/device.py
|
|
8
|
+
ryseble/packets.py
|
|
9
|
+
ryseble.egg-info/PKG-INFO
|
|
10
|
+
ryseble.egg-info/SOURCES.txt
|
|
11
|
+
ryseble.egg-info/dependency_links.txt
|
|
12
|
+
ryseble.egg-info/requires.txt
|
|
13
|
+
ryseble.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
bleak>=0.22.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ryseble
|
ryseble-1.0.0/setup.cfg
ADDED