Skip to content

Commit 473cdf4

Browse files
authored
Add BLE RPC support (#975)
1 parent 3545acc commit 473cdf4

File tree

12 files changed

+1643
-65
lines changed

12 files changed

+1643
-65
lines changed

aioshelly/block_device/device.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ def __init__(
8484
options: ConnectionOptions,
8585
) -> None:
8686
"""Device init."""
87+
if options.ip_address is None:
88+
raise ValueError("Block devices require ip_address")
8789
self.coap_context: COAP = coap_context
8890
self.aiohttp_session: ClientSession = aiohttp_session
8991
self.options: ConnectionOptions = options
@@ -114,6 +116,8 @@ async def create(
114116
) -> BlockDevice:
115117
"""Device creation."""
116118
options = await process_ip_or_options(ip_or_options)
119+
if options.ip_address is None:
120+
raise ValueError("Block devices require ip_address")
117121
# Try sending cit/s request to trigger a sleeping device
118122
try:
119123
await coap_context.request(options.ip_address, "s")
@@ -129,6 +133,8 @@ async def create(
129133
@property
130134
def ip_address(self) -> str:
131135
"""Device ip address."""
136+
if self.options.ip_address is None:
137+
raise RuntimeError("Block device ip_address is None")
132138
return self.options.ip_address
133139

134140
async def initialize(self) -> None:
@@ -150,10 +156,10 @@ async def initialize(self) -> None:
150156
self.initialized = False
151157
self.coap_s = None
152158

153-
ip = self.options.ip_address
159+
ip = self.ip_address
154160
try:
155161
self._shelly = await get_info(
156-
self.aiohttp_session, self.options.ip_address, self.options.device_mac
162+
self.aiohttp_session, self.ip_address, self.options.device_mac
157163
)
158164

159165
if self.requires_auth and not self.options.auth:
@@ -293,7 +299,7 @@ async def update_settings(self) -> None:
293299

294300
async def update_shelly(self) -> None:
295301
"""Device update for /shelly (HTTP)."""
296-
self._shelly = await get_info(self.aiohttp_session, self.options.ip_address)
302+
self._shelly = await get_info(self.aiohttp_session, self.ip_address)
297303

298304
async def _update_cit_d(self) -> None:
299305
"""Update CoAP cit/d.
@@ -349,7 +355,7 @@ async def http_request(
349355
if self.options.auth is None and self.requires_auth:
350356
raise InvalidAuthError("auth missing and required")
351357

352-
host = self.options.ip_address
358+
host = self.ip_address
353359
_LOGGER.debug("host %s: http request: /%s (params=%s)", host, path, params)
354360
try:
355361
resp: ClientResponse = await self.aiohttp_session.request(

aioshelly/common.py

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,14 @@
77
import logging
88
from dataclasses import dataclass
99
from socket import gethostbyname
10-
from typing import Any
10+
from typing import TYPE_CHECKING, Any
1111

1212
from aiohttp import BasicAuth, ClientSession, ClientTimeout
1313
from yarl import URL
1414

15+
if TYPE_CHECKING:
16+
from bleak import BLEDevice
17+
1518
from .const import (
1619
CONNECT_ERRORS,
1720
DEFAULT_HTTP_PORT,
@@ -37,16 +40,23 @@
3740
class ConnectionOptions:
3841
"""Shelly options for connection."""
3942

40-
ip_address: str
43+
ip_address: str | None = None
4144
username: str | None = None
4245
password: str | None = None
4346
temperature_unit: str = "C"
4447
auth: BasicAuth | None = None
4548
device_mac: str | None = None
4649
port: int = DEFAULT_HTTP_PORT
50+
ble_device: BLEDevice | None = None
4751

4852
def __post_init__(self) -> None:
4953
"""Call after initialization."""
54+
if self.ip_address is None and self.ble_device is None:
55+
raise ValueError("Must provide either ip_address or ble_device")
56+
57+
if self.ip_address is not None and self.ble_device is not None:
58+
raise ValueError("Cannot provide both ip_address and ble_device")
59+
5060
if self.username is not None:
5161
if self.password is None:
5262
raise ValueError("Supply both username and password")
@@ -60,17 +70,19 @@ def __post_init__(self) -> None:
6070
async def process_ip_or_options(ip_or_options: IpOrOptionsType) -> ConnectionOptions:
6171
"""Return ConnectionOptions class from ip str or ConnectionOptions."""
6272
if isinstance(ip_or_options, str):
63-
options = ConnectionOptions(ip_or_options)
73+
options = ConnectionOptions(ip_address=ip_or_options)
6474
else:
6575
options = ip_or_options
6676

67-
try:
68-
ipaddress.ip_address(options.ip_address)
69-
except ValueError:
70-
loop = asyncio.get_running_loop()
71-
options.ip_address = await loop.run_in_executor(
72-
None, gethostbyname, options.ip_address
73-
)
77+
# Only process IP address if provided (not for BLE connections)
78+
if options.ip_address is not None:
79+
try:
80+
ipaddress.ip_address(options.ip_address)
81+
except ValueError:
82+
loop = asyncio.get_running_loop()
83+
options.ip_address = await loop.run_in_executor(
84+
None, gethostbyname, options.ip_address
85+
)
7486

7587
return options
7688

aioshelly/exceptions.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,14 @@ class DeviceConnectionTimeoutError(DeviceConnectionError):
3939
"""Exception indicates device connection timeout errors."""
4040

4141

42+
class BleConnectionError(DeviceConnectionError):
43+
"""Exception indicates Bluetooth Low Energy connection errors."""
44+
45+
46+
class BleCharacteristicNotFoundError(BleConnectionError):
47+
"""Exception indicates required BLE GATT characteristic not found."""
48+
49+
4250
class InvalidAuthError(ShellyError):
4351
"""Raised to indicate invalid or missing authentication error."""
4452

0 commit comments

Comments
 (0)