mirror of
https://github.com/home-assistant/core.git
synced 2026-09-05 10:05:52 -05:00
Exclude non mowers from husqvarna_automower_ble discovery (#151507)
This commit is contained in:
@@ -10,6 +10,8 @@ from automower_ble.mower import Mower
|
||||
from automower_ble.protocol import ResponseResult
|
||||
from bleak import BleakError
|
||||
from bleak_retry_connector import get_device
|
||||
from gardena_bluetooth.const import ScanService
|
||||
from gardena_bluetooth.parse import ManufacturerData, ProductType
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import bluetooth
|
||||
@@ -22,20 +24,31 @@ from .const import DOMAIN, LOGGER
|
||||
|
||||
def _is_supported(discovery_info: BluetoothServiceInfo):
|
||||
"""Check if device is supported."""
|
||||
if ScanService not in discovery_info.service_uuids:
|
||||
LOGGER.debug(
|
||||
"Unsupported device, missing service %s: %s", ScanService, discovery_info
|
||||
)
|
||||
return False
|
||||
|
||||
LOGGER.debug(
|
||||
"%s manufacturer data: %s",
|
||||
discovery_info.address,
|
||||
discovery_info.manufacturer_data,
|
||||
)
|
||||
if not (data := discovery_info.manufacturer_data.get(ManufacturerData.company)):
|
||||
LOGGER.debug(
|
||||
"Unsupported device, missing manufacturer data %s: %s",
|
||||
ManufacturerData.company,
|
||||
discovery_info,
|
||||
)
|
||||
return False
|
||||
|
||||
manufacturer = any(key == 1062 for key in discovery_info.manufacturer_data)
|
||||
service_husqvarna = any(
|
||||
service == "98bd0001-0b0e-421a-84e5-ddbf75dc6de4"
|
||||
for service in discovery_info.service_uuids
|
||||
)
|
||||
manufacturer_data = ManufacturerData.decode(data)
|
||||
product_type = ProductType.from_manufacturer_data(manufacturer_data)
|
||||
|
||||
return manufacturer and service_husqvarna
|
||||
# Some mowers only expose the serial number in the manufacturer data
|
||||
# and not the product type, so we allow None here as well.
|
||||
if product_type not in (ProductType.MOWER, None):
|
||||
LOGGER.debug("Unsupported device: %s (%s)", manufacturer_data, discovery_info)
|
||||
return False
|
||||
|
||||
LOGGER.debug("Supported device: %s", manufacturer_data)
|
||||
return True
|
||||
|
||||
|
||||
def _pin_valid(pin: str) -> bool:
|
||||
|
||||
@@ -12,5 +12,5 @@
|
||||
"dependencies": ["bluetooth_adapters"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/husqvarna_automower_ble",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["automower-ble==0.2.7"]
|
||||
"requirements": ["automower-ble==0.2.7", "gardena-bluetooth==1.6.0"]
|
||||
}
|
||||
|
||||
Generated
+1
@@ -992,6 +992,7 @@ fyta_cli==0.7.2
|
||||
gTTS==2.5.3
|
||||
|
||||
# homeassistant.components.gardena_bluetooth
|
||||
# homeassistant.components.husqvarna_automower_ble
|
||||
gardena-bluetooth==1.6.0
|
||||
|
||||
# homeassistant.components.google_assistant_sdk
|
||||
|
||||
Generated
+1
@@ -862,6 +862,7 @@ fyta_cli==0.7.2
|
||||
gTTS==2.5.3
|
||||
|
||||
# homeassistant.components.gardena_bluetooth
|
||||
# homeassistant.components.husqvarna_automower_ble
|
||||
gardena-bluetooth==1.6.0
|
||||
|
||||
# homeassistant.components.google_assistant_sdk
|
||||
|
||||
@@ -9,15 +9,23 @@ from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.components.bluetooth import inject_bluetooth_service_info
|
||||
|
||||
AUTOMOWER_SERVICE_INFO = BluetoothServiceInfo(
|
||||
AUTOMOWER_SERVICE_INFO_SERIAL = BluetoothServiceInfo(
|
||||
name="305",
|
||||
address="00000000-0000-0000-0000-000000000003",
|
||||
rssi=-63,
|
||||
service_data={},
|
||||
manufacturer_data={1062: b"\x05\x04\xbf\xcf\xbb\r"},
|
||||
service_uuids=[
|
||||
"98bd0001-0b0e-421a-84e5-ddbf75dc6de4",
|
||||
],
|
||||
service_uuids=["98bd0001-0b0e-421a-84e5-ddbf75dc6de4"],
|
||||
source="local",
|
||||
)
|
||||
|
||||
AUTOMOWER_SERVICE_INFO_MOWER = BluetoothServiceInfo(
|
||||
name="305",
|
||||
address="00000000-0000-0000-0000-000000000003",
|
||||
rssi=-63,
|
||||
service_data={},
|
||||
manufacturer_data={1062: bytes.fromhex("02050104060a2301")},
|
||||
service_uuids=["98bd0001-0b0e-421a-84e5-ddbf75dc6de4"],
|
||||
source="local",
|
||||
)
|
||||
|
||||
@@ -27,9 +35,7 @@ AUTOMOWER_UNNAMED_SERVICE_INFO = BluetoothServiceInfo(
|
||||
rssi=-63,
|
||||
service_data={},
|
||||
manufacturer_data={1062: b"\x05\x04\xbf\xcf\xbb\r"},
|
||||
service_uuids=[
|
||||
"98bd0001-0b0e-421a-84e5-ddbf75dc6de4",
|
||||
],
|
||||
service_uuids=["98bd0001-0b0e-421a-84e5-ddbf75dc6de4"],
|
||||
source="local",
|
||||
)
|
||||
|
||||
@@ -39,9 +45,7 @@ AUTOMOWER_MISSING_MANUFACTURER_DATA_SERVICE_INFO = BluetoothServiceInfo(
|
||||
rssi=-63,
|
||||
service_data={},
|
||||
manufacturer_data={},
|
||||
service_uuids=[
|
||||
"98bd0001-0b0e-421a-84e5-ddbf75dc6de4",
|
||||
],
|
||||
service_uuids=["98bd0001-0b0e-421a-84e5-ddbf75dc6de4"],
|
||||
source="local",
|
||||
)
|
||||
|
||||
@@ -51,9 +55,30 @@ AUTOMOWER_UNSUPPORTED_GROUP_SERVICE_INFO = BluetoothServiceInfo(
|
||||
rssi=-63,
|
||||
service_data={},
|
||||
manufacturer_data={1062: b"\x05\x04\xbf\xcf\xbb\r"},
|
||||
service_uuids=[
|
||||
"98bd0001-0b0e-421a-84e5-ddbf75dc6de4",
|
||||
],
|
||||
service_uuids=["98bd0001-0b0e-421a-84e5-ddbf75dc6de4"],
|
||||
source="local",
|
||||
)
|
||||
|
||||
MISSING_SERVICE_SERVICE_INFO = BluetoothServiceInfo(
|
||||
name="Blah",
|
||||
address="00000000-0000-0000-0000-000000000001",
|
||||
rssi=-63,
|
||||
service_data={},
|
||||
manufacturer_data={},
|
||||
service_uuids=[],
|
||||
source="local",
|
||||
)
|
||||
|
||||
|
||||
WATER_TIMER_SERVICE_INFO = BluetoothServiceInfo(
|
||||
name="Timer",
|
||||
address="00000000-0000-0000-0000-000000000001",
|
||||
rssi=-63,
|
||||
service_data={},
|
||||
manufacturer_data={
|
||||
1062: b"\x02\x07d\x02\x05\x01\x02\x08\x00\x02\t\x01\x04\x06\x12\x00\x01"
|
||||
},
|
||||
service_uuids=["98bd0001-0b0e-421a-84e5-ddbf75dc6de4"],
|
||||
source="local",
|
||||
)
|
||||
|
||||
@@ -63,7 +88,7 @@ async def setup_entry(
|
||||
) -> None:
|
||||
"""Make sure the device is available."""
|
||||
|
||||
inject_bluetooth_service_info(hass, AUTOMOWER_SERVICE_INFO)
|
||||
inject_bluetooth_service_info(hass, AUTOMOWER_SERVICE_INFO_SERIAL)
|
||||
|
||||
with patch("homeassistant.components.husqvarna_automower_ble.PLATFORMS", platforms):
|
||||
mock_entry.add_to_hass(hass)
|
||||
|
||||
@@ -9,7 +9,7 @@ import pytest
|
||||
from homeassistant.components.husqvarna_automower_ble.const import DOMAIN
|
||||
from homeassistant.const import CONF_ADDRESS, CONF_CLIENT_ID, CONF_PIN
|
||||
|
||||
from . import AUTOMOWER_SERVICE_INFO
|
||||
from . import AUTOMOWER_SERVICE_INFO_SERIAL
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
@@ -56,9 +56,9 @@ def mock_config_entry() -> MockConfigEntry:
|
||||
domain=DOMAIN,
|
||||
title="Husqvarna AutoMower",
|
||||
data={
|
||||
CONF_ADDRESS: AUTOMOWER_SERVICE_INFO.address,
|
||||
CONF_ADDRESS: AUTOMOWER_SERVICE_INFO_SERIAL.address,
|
||||
CONF_CLIENT_ID: 1197489078,
|
||||
CONF_PIN: "1234",
|
||||
},
|
||||
unique_id=AUTOMOWER_SERVICE_INFO.address,
|
||||
unique_id=AUTOMOWER_SERVICE_INFO_SERIAL.address,
|
||||
)
|
||||
|
||||
@@ -11,11 +11,15 @@ from homeassistant.config_entries import SOURCE_BLUETOOTH, SOURCE_USER
|
||||
from homeassistant.const import CONF_ADDRESS, CONF_CLIENT_ID, CONF_PIN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo
|
||||
|
||||
from . import (
|
||||
AUTOMOWER_MISSING_MANUFACTURER_DATA_SERVICE_INFO,
|
||||
AUTOMOWER_SERVICE_INFO,
|
||||
AUTOMOWER_SERVICE_INFO_MOWER,
|
||||
AUTOMOWER_SERVICE_INFO_SERIAL,
|
||||
AUTOMOWER_UNNAMED_SERVICE_INFO,
|
||||
MISSING_SERVICE_SERVICE_INFO,
|
||||
WATER_TIMER_SERVICE_INFO,
|
||||
)
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
@@ -121,10 +125,16 @@ async def test_user_selection_incorrect_pin(
|
||||
}
|
||||
|
||||
|
||||
async def test_bluetooth(hass: HomeAssistant) -> None:
|
||||
@pytest.mark.parametrize(
|
||||
"service_info",
|
||||
[AUTOMOWER_SERVICE_INFO_MOWER, AUTOMOWER_SERVICE_INFO_SERIAL],
|
||||
)
|
||||
async def test_bluetooth(
|
||||
hass: HomeAssistant, service_info: BluetoothServiceInfo
|
||||
) -> None:
|
||||
"""Test bluetooth device discovery."""
|
||||
|
||||
inject_bluetooth_service_info(hass, AUTOMOWER_SERVICE_INFO)
|
||||
inject_bluetooth_service_info(hass, service_info)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
result = hass.config_entries.flow.async_progress_by_handler(DOMAIN)[0]
|
||||
@@ -157,7 +167,7 @@ async def test_bluetooth_incorrect_pin(
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_BLUETOOTH},
|
||||
data=AUTOMOWER_SERVICE_INFO,
|
||||
data=AUTOMOWER_SERVICE_INFO_SERIAL,
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "bluetooth_confirm"
|
||||
@@ -214,7 +224,7 @@ async def test_bluetooth_unknown_error(
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_BLUETOOTH},
|
||||
data=AUTOMOWER_SERVICE_INFO,
|
||||
data=AUTOMOWER_SERVICE_INFO_SERIAL,
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "bluetooth_confirm"
|
||||
@@ -241,7 +251,7 @@ async def test_bluetooth_not_paired(
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_BLUETOOTH},
|
||||
data=AUTOMOWER_SERVICE_INFO,
|
||||
data=AUTOMOWER_SERVICE_INFO_SERIAL,
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "bluetooth_confirm"
|
||||
@@ -274,18 +284,26 @@ async def test_bluetooth_not_paired(
|
||||
}
|
||||
|
||||
|
||||
async def test_bluetooth_invalid(hass: HomeAssistant) -> None:
|
||||
@pytest.mark.parametrize(
|
||||
"service_info",
|
||||
[
|
||||
AUTOMOWER_MISSING_MANUFACTURER_DATA_SERVICE_INFO,
|
||||
MISSING_SERVICE_SERVICE_INFO,
|
||||
WATER_TIMER_SERVICE_INFO,
|
||||
],
|
||||
)
|
||||
async def test_bluetooth_invalid(
|
||||
hass: HomeAssistant, service_info: BluetoothServiceInfo
|
||||
) -> None:
|
||||
"""Test bluetooth device discovery with invalid data."""
|
||||
|
||||
inject_bluetooth_service_info(
|
||||
hass, AUTOMOWER_MISSING_MANUFACTURER_DATA_SERVICE_INFO
|
||||
)
|
||||
inject_bluetooth_service_info(hass, service_info)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_BLUETOOTH},
|
||||
data=AUTOMOWER_MISSING_MANUFACTURER_DATA_SERVICE_INFO,
|
||||
data=service_info,
|
||||
)
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "no_devices_found"
|
||||
|
||||
@@ -12,7 +12,7 @@ from homeassistant.const import CONF_ADDRESS, CONF_CLIENT_ID, CONF_PIN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
|
||||
from . import AUTOMOWER_SERVICE_INFO
|
||||
from . import AUTOMOWER_SERVICE_INFO_SERIAL
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
@@ -34,7 +34,7 @@ async def test_setup(
|
||||
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
device_entry = device_registry.async_get_device(
|
||||
identifiers={(DOMAIN, f"{AUTOMOWER_SERVICE_INFO.address}_1197489078")}
|
||||
identifiers={(DOMAIN, f"{AUTOMOWER_SERVICE_INFO_SERIAL.address}_1197489078")}
|
||||
)
|
||||
|
||||
assert device_entry == snapshot
|
||||
|
||||
Reference in New Issue
Block a user