Fix ISEO Argo BLE offering unrelated devices for discovery (#182315)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Federico Zivolo
2026-09-22 13:53:26 +02:00
committed by GitHub
co-authored by Claude Opus 5
parent 176dacad95
commit 1923c2e88d
4 changed files with 100 additions and 69 deletions
@@ -33,12 +33,27 @@ from .const import CONF_PRIV_SCALAR, DEFAULT_USER_SUBTYPE, DOMAIN
_LOGGER = logging.getLogger(__name__)
# OUI assigned to Iseo Serrature s.p.a.; locks advertise a public address.
_ISEO_OUI = "00:15:42"
def _generate_identity() -> ec.EllipticCurvePrivateKey:
"""Generate a fresh SECP224R1 private key for use as an Argo BT identity."""
return ec.generate_private_key(ec.SECP224R1())
def _is_iseo_lock(info: BluetoothServiceInfoBleak) -> bool:
"""Return True when the advertisement really comes from an ISEO lock.
The 0xF000-0xF03F device-type UUID the discovery matcher keys on sits in a
range the Bluetooth SIG leaves unassigned and other vendors reuse freely, so
the address has to carry ISEO's OUI as well.
"""
return info.address.lower().startswith(_ISEO_OUI) and is_iseo_advertisement(
list(info.service_uuids or [])
)
def _discover_locks(hass: HomeAssistant) -> list[BluetoothServiceInfoBleak]:
"""Query HA's bluetooth integration for nearby ISEO locks."""
all_devices = sorted(
@@ -52,7 +67,7 @@ def _discover_locks(hass: HomeAssistant) -> list[BluetoothServiceInfoBleak]:
found: list[BluetoothServiceInfoBleak] = []
for info in all_devices:
if not is_iseo_advertisement(list(info.service_uuids or [])):
if not _is_iseo_lock(info):
continue
_LOGGER.debug(
" %s name=%r rssi=%d — ISEO lock",
@@ -145,12 +160,18 @@ class IseoConfigFlow(ConfigFlow, domain=DOMAIN):
self, discovery_info: BluetoothServiceInfoBleak
) -> ConfigFlowResult:
"""Called by HA when a matching BLE advertisement is seen."""
if not _is_iseo_lock(discovery_info):
_LOGGER.debug(
"Ignoring %s (%s): not an ISEO lock, service UUIDs %s",
discovery_info.name,
discovery_info.address,
list(discovery_info.service_uuids or []),
)
return self.async_abort(reason="not_iseo_device")
await self.async_set_unique_id(format_mac(discovery_info.address))
self._abort_if_unique_id_configured()
if not is_iseo_advertisement(list(discovery_info.service_uuids or [])):
return self.async_abort(reason="not_iseo_device")
priv = await self.hass.async_add_executor_job(_generate_identity)
priv_int = priv.private_numbers().private_value
new_uuid = uuid_module.uuid4().bytes
+9 -2
View File
@@ -7,10 +7,17 @@ from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
MOCK_ADDRESS = "AA:BB:CC:DD:EE:FF"
MOCK_ADDRESS = "00:15:42:AA:BB:CC" # 00:15:42 is ISEO's OUI
MOCK_UUID_HEX = "eaa06132486f426cb0d26c6b9b578add"
MOCK_PRIV_SCALAR = "0x" + "a" * 56 # 224-bit hex scalar
# A lock advertises a 0xF0xx device-type UUID; the rest of the list carries
# protocol info and, when the feature is enabled, its system state.
ISEO_SERVICE_UUIDS = [
"0000f001-0000-1000-8000-00805f9b34fb",
"0000d004-0000-1000-8000-00805f9b34fb",
]
# A fake BluetoothServiceInfoBleak for testing
MOCK_SERVICE_INFO = BluetoothServiceInfoBleak(
name="ISEO Lock",
@@ -18,7 +25,7 @@ MOCK_SERVICE_INFO = BluetoothServiceInfoBleak(
rssi=-60,
manufacturer_data={},
service_data={},
service_uuids=["0000f000-0000-1000-8000-00805f9b34fb"],
service_uuids=ISEO_SERVICE_UUIDS,
source="local",
device=MagicMock(),
advertisement=MagicMock(),
@@ -32,7 +32,7 @@
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'aa:bb:cc:dd:ee:ff',
'unique_id': '00:15:42:aa:bb:cc',
'unit_of_measurement': None,
})
# ---
@@ -16,7 +16,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from homeassistant.helpers.device_registry import format_mac
from . import MOCK_ADDRESS, MOCK_SERVICE_INFO, MOCK_UUID_HEX
from . import ISEO_SERVICE_UUIDS, MOCK_ADDRESS, MOCK_SERVICE_INFO, MOCK_UUID_HEX
from tests.common import MockConfigEntry
@@ -52,15 +52,11 @@ async def test_bluetooth_discovery_confirm_and_register(
mock_iseo_client: MagicMock,
) -> None:
"""Test full bluetooth discovery → confirm → gw_register flow."""
with patch(
"homeassistant.components.iseo_argo_ble.config_flow.is_iseo_advertisement",
return_value=True,
):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_BLUETOOTH},
data=MOCK_SERVICE_INFO,
)
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_BLUETOOTH},
data=MOCK_SERVICE_INFO,
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "bluetooth_confirm"
@@ -146,15 +142,42 @@ async def test_bluetooth_discovery_abort_if_already_configured(
assert result["reason"] == "already_configured"
async def test_bluetooth_discovery_not_iseo(hass: HomeAssistant) -> None:
"""Test bluetooth discovery aborts for non-ISEO devices."""
@pytest.mark.parametrize(
("address", "service_uuids"),
[
pytest.param(
MOCK_ADDRESS,
["0000180f-0000-1000-8000-00805f9b34fb"],
id="iseo_address_without_device_type_uuid",
),
pytest.param(
"F8:24:41:C5:98:2C",
ISEO_SERVICE_UUIDS,
id="device_type_uuid_without_iseo_address",
),
pytest.param(
"F8:24:41:C5:98:2C",
[],
id="neither",
),
],
)
async def test_bluetooth_discovery_not_iseo(
hass: HomeAssistant, address: str, service_uuids: list[str]
) -> None:
"""Test bluetooth discovery aborts for non-ISEO devices.
The 0xF000-0xF03F device-type range is unassigned by the Bluetooth SIG, so
unrelated vendors advertise in it too; matching it must not be enough on its
own to offer a discovery.
"""
non_iseo_info = BluetoothServiceInfoBleak(
name="SomeOtherDevice",
address="11:22:33:44:55:66",
address=address,
rssi=-70,
manufacturer_data={},
service_data={},
service_uuids=["0000180f-0000-1000-8000-00805f9b34fb"], # not ISEO
service_uuids=service_uuids,
source="local",
device=MagicMock(),
advertisement=MagicMock(),
@@ -163,15 +186,11 @@ async def test_bluetooth_discovery_not_iseo(hass: HomeAssistant) -> None:
tx_power=None,
)
with patch(
"homeassistant.components.iseo_argo_ble.config_flow.is_iseo_advertisement",
return_value=False,
):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_BLUETOOTH},
data=non_iseo_info,
)
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_BLUETOOTH},
data=non_iseo_info,
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "not_iseo_device"
@@ -199,15 +218,11 @@ async def test_gw_register_connection_error(
"""Test gw_register handles connection error."""
mock_iseo_client.setup_gateway.side_effect = IseoConnectionError
with patch(
"homeassistant.components.iseo_argo_ble.config_flow.is_iseo_advertisement",
return_value=True,
):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_BLUETOOTH},
data=MOCK_SERVICE_INFO,
)
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_BLUETOOTH},
data=MOCK_SERVICE_INFO,
)
# Move to gw_register
result2 = await hass.config_entries.flow.async_configure(
@@ -231,15 +246,11 @@ async def test_gw_register_auth_error(
"""Test gw_register handles auth error."""
mock_iseo_client.setup_gateway.side_effect = IseoAuthError
with patch(
"homeassistant.components.iseo_argo_ble.config_flow.is_iseo_advertisement",
return_value=True,
):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_BLUETOOTH},
data=MOCK_SERVICE_INFO,
)
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_BLUETOOTH},
data=MOCK_SERVICE_INFO,
)
# Move to gw_register
result2 = await hass.config_entries.flow.async_configure(
@@ -260,15 +271,11 @@ async def test_gw_register_no_ble_device(
hass: HomeAssistant,
) -> None:
"""Test gw_register handles case where ble_device is None."""
with patch(
"homeassistant.components.iseo_argo_ble.config_flow.is_iseo_advertisement",
return_value=True,
):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_BLUETOOTH},
data=MOCK_SERVICE_INFO,
)
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_BLUETOOTH},
data=MOCK_SERVICE_INFO,
)
# Move to gw_register
result2 = await hass.config_entries.flow.async_configure(
@@ -296,15 +303,11 @@ async def test_gw_register_unknown_error(
"""Test gw_register handles unknown error."""
mock_iseo_client.setup_gateway.side_effect = Exception("BOOM")
with patch(
"homeassistant.components.iseo_argo_ble.config_flow.is_iseo_advertisement",
return_value=True,
):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_BLUETOOTH},
data=MOCK_SERVICE_INFO,
)
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_BLUETOOTH},
data=MOCK_SERVICE_INFO,
)
# Move to gw_register
result2 = await hass.config_entries.flow.async_configure(
@@ -321,14 +324,14 @@ async def test_gw_register_unknown_error(
async def test_discover_locks(hass: HomeAssistant) -> None:
"""Test the _discover_locks helper function."""
"""Test _discover_locks skips devices that only look like a lock."""
non_iseo_info = BluetoothServiceInfoBleak(
name="Other",
address="11:22:33:44:55:66",
address="F8:24:41:C5:98:2C",
rssi=-70,
manufacturer_data={},
service_data={},
service_uuids=[],
service_uuids=ISEO_SERVICE_UUIDS,
source="local",
device=MagicMock(),
advertisement=MagicMock(),