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 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
+ ```
@@ -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,8 @@
1
+ # Hardcoded UUIDs used by Ryse devices
2
+ HARDCODED_UUIDS = {
3
+ "rx_uuid": "a72f2801-b0bd-498b-b4cd-4a3901388238",
4
+ "tx_uuid": "a72f2802-b0bd-498b-b4cd-4a3901388238",
5
+ }
6
+
7
+ # Pairing mode flag
8
+ PAIRING_MODE_FLAG = 0x01
@@ -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
+ bleak>=0.22.0
@@ -0,0 +1 @@
1
+ ryseble
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+