mirror of
https://github.com/home-assistant/core.git
synced 2025-11-09 02:49:40 +00:00
remove helpers, delegate to lib
This commit is contained in:
@@ -1,105 +0,0 @@
|
|||||||
"""Bluetooth support for Shelly integration."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import logging
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
ALLTERCO_MFID = 0x0BA9
|
|
||||||
|
|
||||||
# Block types in manufacturer data
|
|
||||||
BLOCK_TYPE_FLAGS = 0x01
|
|
||||||
BLOCK_TYPE_MAC = 0x0A
|
|
||||||
BLOCK_TYPE_MODEL = 0x0B
|
|
||||||
|
|
||||||
# Shelly bitfield flags (block type 0x01)
|
|
||||||
FLAG_DISCOVERABLE = 1 << 0
|
|
||||||
FLAG_AUTH_ENABLED = 1 << 1
|
|
||||||
FLAG_RPC_OVER_BLE_ENABLED = 1 << 2
|
|
||||||
FLAG_BUZZER_ENABLED = 1 << 3
|
|
||||||
FLAG_IN_PAIRING_MODE = 1 << 4
|
|
||||||
|
|
||||||
|
|
||||||
def parse_shelly_manufacturer_data(
|
|
||||||
manufacturer_data: dict[int, bytes],
|
|
||||||
) -> dict[str, int | str] | None:
|
|
||||||
"""Parse Shelly manufacturer data from BLE advertisement.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
manufacturer_data: Manufacturer data from BLE advertisement
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dict with parsed data (flags, mac, model) or None if invalid
|
|
||||||
|
|
||||||
"""
|
|
||||||
if ALLTERCO_MFID not in manufacturer_data:
|
|
||||||
return None
|
|
||||||
|
|
||||||
data = manufacturer_data[ALLTERCO_MFID]
|
|
||||||
if len(data) < 1:
|
|
||||||
return None
|
|
||||||
|
|
||||||
result: dict[str, int | str] = {}
|
|
||||||
offset = 0
|
|
||||||
|
|
||||||
# Parse blocks
|
|
||||||
while offset < len(data):
|
|
||||||
if offset + 1 > len(data):
|
|
||||||
break
|
|
||||||
|
|
||||||
block_type = data[offset]
|
|
||||||
offset += 1
|
|
||||||
|
|
||||||
if block_type == BLOCK_TYPE_FLAGS:
|
|
||||||
# 2 bytes of flags
|
|
||||||
if offset + 2 > len(data):
|
|
||||||
break
|
|
||||||
flags = int.from_bytes(data[offset : offset + 2], byteorder="little")
|
|
||||||
result["flags"] = flags
|
|
||||||
offset += 2
|
|
||||||
|
|
||||||
elif block_type == BLOCK_TYPE_MAC:
|
|
||||||
# 6 bytes MAC address
|
|
||||||
if offset + 6 > len(data):
|
|
||||||
break
|
|
||||||
mac_bytes = data[offset : offset + 6]
|
|
||||||
# Format as standard MAC address
|
|
||||||
result["mac"] = ":".join(f"{b:02X}" for b in mac_bytes)
|
|
||||||
offset += 6
|
|
||||||
|
|
||||||
elif block_type == BLOCK_TYPE_MODEL:
|
|
||||||
# 2 bytes model ID
|
|
||||||
if offset + 2 > len(data):
|
|
||||||
break
|
|
||||||
model_id = int.from_bytes(data[offset : offset + 2], byteorder="little")
|
|
||||||
result["model_id"] = model_id
|
|
||||||
offset += 2
|
|
||||||
|
|
||||||
else:
|
|
||||||
# Unknown block type - can't continue parsing
|
|
||||||
_LOGGER.debug("Unknown block type in manufacturer data: 0x%02X", block_type)
|
|
||||||
break
|
|
||||||
|
|
||||||
return result if result else None
|
|
||||||
|
|
||||||
|
|
||||||
def has_rpc_over_ble(manufacturer_data: dict[int, bytes]) -> bool:
|
|
||||||
"""Check if device has RPC-over-BLE enabled.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
manufacturer_data: Manufacturer data from BLE advertisement
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True if RPC-over-BLE is enabled
|
|
||||||
|
|
||||||
"""
|
|
||||||
parsed = parse_shelly_manufacturer_data(manufacturer_data)
|
|
||||||
if not parsed or "flags" not in parsed:
|
|
||||||
return False
|
|
||||||
|
|
||||||
flags = parsed["flags"]
|
|
||||||
if not isinstance(flags, int):
|
|
||||||
return False
|
|
||||||
|
|
||||||
return bool(flags & FLAG_RPC_OVER_BLE_ENABLED)
|
|
||||||
@@ -7,6 +7,8 @@ from collections.abc import AsyncIterator, Mapping
|
|||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from typing import Any, Final
|
from typing import Any, Final
|
||||||
|
|
||||||
|
from aioshelly.ble.manufacturer_data import has_rpc_over_ble
|
||||||
|
from aioshelly.ble.provisioning import async_provision_wifi, async_scan_wifi_networks
|
||||||
from aioshelly.block_device import BlockDevice
|
from aioshelly.block_device import BlockDevice
|
||||||
from aioshelly.common import ConnectionOptions, get_info
|
from aioshelly.common import ConnectionOptions, get_info
|
||||||
from aioshelly.const import BLOCK_GENERATIONS, DEFAULT_HTTP_PORT, RPC_GENERATIONS
|
from aioshelly.const import BLOCK_GENERATIONS, DEFAULT_HTTP_PORT, RPC_GENERATIONS
|
||||||
@@ -19,6 +21,7 @@ from aioshelly.exceptions import (
|
|||||||
RpcCallError,
|
RpcCallError,
|
||||||
)
|
)
|
||||||
from aioshelly.rpc_device import RpcDevice
|
from aioshelly.rpc_device import RpcDevice
|
||||||
|
from aioshelly.zeroconf import async_lookup_device_by_name
|
||||||
from bleak.backends.device import BLEDevice
|
from bleak.backends.device import BLEDevice
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
@@ -46,7 +49,6 @@ from homeassistant.helpers.selector import (
|
|||||||
)
|
)
|
||||||
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
|
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
|
||||||
|
|
||||||
from .ble_manufacturer_data import has_rpc_over_ble
|
|
||||||
from .ble_provisioning import (
|
from .ble_provisioning import (
|
||||||
ProvisioningState,
|
ProvisioningState,
|
||||||
async_get_provisioning_registry,
|
async_get_provisioning_registry,
|
||||||
@@ -63,7 +65,6 @@ from .const import (
|
|||||||
BLEScannerMode,
|
BLEScannerMode,
|
||||||
)
|
)
|
||||||
from .coordinator import ShellyConfigEntry, async_reconnect_soon
|
from .coordinator import ShellyConfigEntry, async_reconnect_soon
|
||||||
from .provision_wifi import async_provision_wifi, async_scan_wifi_networks
|
|
||||||
from .utils import (
|
from .utils import (
|
||||||
get_block_device_sleep_period,
|
get_block_device_sleep_period,
|
||||||
get_coap_context,
|
get_coap_context,
|
||||||
@@ -76,7 +77,6 @@ from .utils import (
|
|||||||
get_ws_context,
|
get_ws_context,
|
||||||
mac_address_from_name,
|
mac_address_from_name,
|
||||||
)
|
)
|
||||||
from .zeroconf_helpers import async_lookup_device_by_name
|
|
||||||
|
|
||||||
CONFIG_SCHEMA: Final = vol.Schema(
|
CONFIG_SCHEMA: Final = vol.Schema(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,77 +0,0 @@
|
|||||||
"""WiFi provisioning via BLE for Shelly devices."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from typing import Any, cast
|
|
||||||
|
|
||||||
from aioshelly.common import ConnectionOptions
|
|
||||||
from aioshelly.rpc_device import RpcDevice
|
|
||||||
from bleak.backends.device import BLEDevice
|
|
||||||
|
|
||||||
|
|
||||||
async def async_scan_wifi_networks(ble_device: BLEDevice) -> list[dict[str, Any]]:
|
|
||||||
"""Scan for WiFi networks via BLE.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
ble_device: BLE device to connect to
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
List of WiFi networks with ssid, rssi, auth fields
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
DeviceConnectionError: If connection to device fails
|
|
||||||
RpcCallError: If RPC call fails
|
|
||||||
|
|
||||||
"""
|
|
||||||
options = ConnectionOptions(ble_device=ble_device)
|
|
||||||
device = await RpcDevice.create(
|
|
||||||
aiohttp_session=None,
|
|
||||||
ws_context=None,
|
|
||||||
ip_or_options=options,
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
await device.initialize()
|
|
||||||
# WiFi scan can take up to 20 seconds - use 30s timeout to be safe
|
|
||||||
scan_result = await device.call_rpc("WiFi.Scan", timeout=30)
|
|
||||||
return cast(list[dict[str, Any]], scan_result.get("results", []))
|
|
||||||
finally:
|
|
||||||
await device.shutdown()
|
|
||||||
|
|
||||||
|
|
||||||
async def async_provision_wifi(ble_device: BLEDevice, ssid: str, password: str) -> None:
|
|
||||||
"""Provision WiFi credentials to device via BLE.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
ble_device: BLE device to connect to
|
|
||||||
ssid: WiFi network SSID
|
|
||||||
password: WiFi network password
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
DeviceConnectionError: If connection to device fails
|
|
||||||
RpcCallError: If RPC call fails
|
|
||||||
|
|
||||||
"""
|
|
||||||
options = ConnectionOptions(ble_device=ble_device)
|
|
||||||
device = await RpcDevice.create(
|
|
||||||
aiohttp_session=None,
|
|
||||||
ws_context=None,
|
|
||||||
ip_or_options=options,
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
await device.initialize()
|
|
||||||
await device.call_rpc(
|
|
||||||
"WiFi.SetConfig",
|
|
||||||
{
|
|
||||||
"config": {
|
|
||||||
"sta": {
|
|
||||||
"ssid": ssid,
|
|
||||||
"pass": password,
|
|
||||||
"enable": True,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
finally:
|
|
||||||
await device.shutdown()
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
"""Zeroconf helper functions for Shelly integration."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from zeroconf import IPVersion
|
|
||||||
from zeroconf.asyncio import AsyncServiceInfo, AsyncZeroconf
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
async def async_lookup_device_by_name(
|
|
||||||
aiozc: AsyncZeroconf, device_name: str
|
|
||||||
) -> tuple[str, int] | None:
|
|
||||||
"""Look up a Shelly device by name via zeroconf.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
aiozc: AsyncZeroconf instance
|
|
||||||
device_name: Device name (e.g., "ShellyPlugUS-C049EF8873E8")
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Tuple of (host, port) if found, None otherwise
|
|
||||||
|
|
||||||
"""
|
|
||||||
service_name = f"{device_name}._http._tcp.local."
|
|
||||||
|
|
||||||
_LOGGER.debug("Active lookup for: %s", service_name)
|
|
||||||
service_info = AsyncServiceInfo("_http._tcp.local.", service_name)
|
|
||||||
|
|
||||||
if await service_info.async_request(aiozc.zeroconf, 5000):
|
|
||||||
addresses = service_info.parsed_addresses(IPVersion.V4Only)
|
|
||||||
if addresses and service_info.port:
|
|
||||||
host = addresses[0]
|
|
||||||
port = service_info.port
|
|
||||||
_LOGGER.debug("Found device via active lookup at %s:%s", host, port)
|
|
||||||
return (host, port)
|
|
||||||
_LOGGER.debug("Active lookup found service but no IPv4 addresses or port")
|
|
||||||
else:
|
|
||||||
_LOGGER.debug("Active lookup did not find service")
|
|
||||||
|
|
||||||
return None
|
|
||||||
Reference in New Issue
Block a user