Exclude non mowers from husqvarna_automower_ble discovery (#151507)

This commit is contained in:
Erik Montnemery
2025-09-03 09:23:07 +00:00
committed by Franck Nijhof
parent ed9e46bbca
commit 869801b643
8 changed files with 100 additions and 42 deletions
@@ -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"]
}
+1
View File
@@ -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
+1
View File
@@ -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