Require service on fjaraskupan to detect it (#170363)

This commit is contained in:
Joakim Plate
2026-05-18 11:00:05 +02:00
committed by GitHub
parent a333f31d44
commit 88ca0faea0
7 changed files with 159 additions and 47 deletions
@@ -3,7 +3,7 @@
from collections.abc import Callable
import logging
from fjaraskupan import Device
from fjaraskupan import UUID_SERVICE, Device
from homeassistant.components.bluetooth import (
BluetoothCallbackMatcher,
@@ -37,6 +37,7 @@ PLATFORMS = [
]
_LOGGER = logging.getLogger(__name__)
_UUID = str(UUID_SERVICE).lower()
async def async_setup_entry(hass: HomeAssistant, entry: FjaraskupanConfigEntry) -> bool:
@@ -44,39 +45,45 @@ async def async_setup_entry(hass: HomeAssistant, entry: FjaraskupanConfigEntry)
entry.runtime_data = {}
def detection_callback(
service_info: BluetoothServiceInfoBleak, change: BluetoothChange
def data_callback(
service_info: BluetoothServiceInfoBleak, change_: BluetoothChange
) -> None:
if change != BluetoothChange.ADVERTISEMENT:
if (data := entry.runtime_data.get(service_info.address)) is None:
_LOGGER.debug("Ignoring: %s", service_info)
return
if data := entry.runtime_data.get(service_info.address):
_LOGGER.debug("Update: %s", service_info)
data.detection_callback(service_info)
else:
_LOGGER.debug("Detected: %s", service_info)
device = Device(service_info.device.address)
device_info = DeviceInfo(
connections={(dr.CONNECTION_BLUETOOTH, service_info.address)},
identifiers={(DOMAIN, service_info.address)},
manufacturer="Fjäråskupan",
name="Fjäråskupan",
)
_LOGGER.debug("Update: %s", service_info)
data.detection_callback(service_info)
coordinator: FjaraskupanCoordinator = FjaraskupanCoordinator(
hass, entry, device, device_info
)
coordinator.detection_callback(service_info)
def detect_callback(
service_info: BluetoothServiceInfoBleak, change_: BluetoothChange
) -> None:
if service_info.address in entry.runtime_data:
return
entry.runtime_data[service_info.address] = coordinator
async_dispatcher_send(
hass, f"{DISPATCH_DETECTION}.{entry.entry_id}", coordinator
)
_LOGGER.debug("Detected: %s", service_info)
device = Device(service_info.device.address)
device_info = DeviceInfo(
connections={(dr.CONNECTION_BLUETOOTH, service_info.address)},
identifiers={(DOMAIN, service_info.address)},
manufacturer="Fjäråskupan",
name="Fjäråskupan",
)
coordinator: FjaraskupanCoordinator = FjaraskupanCoordinator(
hass, entry, device, device_info
)
coordinator.detection_callback(service_info)
entry.runtime_data[service_info.address] = coordinator
async_dispatcher_send(
hass, f"{DISPATCH_DETECTION}.{entry.entry_id}", coordinator
)
entry.async_on_unload(
async_register_callback(
hass,
detection_callback,
data_callback,
BluetoothCallbackMatcher(
manufacturer_id=20296,
manufacturer_data_start=[79, 68, 70, 74, 65, 82],
@@ -86,6 +93,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: FjaraskupanConfigEntry)
)
)
entry.async_on_unload(
async_register_callback(
hass,
detect_callback,
BluetoothCallbackMatcher(
service_uuid=_UUID,
connectable=False,
),
BluetoothScanningMode.ACTIVE,
)
)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
@@ -1,6 +1,6 @@
"""Config flow for Fjäråskupan integration."""
from fjaraskupan import device_filter
from fjaraskupan import UUID_SERVICE
from homeassistant.components.bluetooth import async_discovered_service_info
from homeassistant.core import HomeAssistant
@@ -15,7 +15,8 @@ async def _async_has_devices(hass: HomeAssistant) -> bool:
service_infos = async_discovered_service_info(hass)
for service_info in service_infos:
if device_filter(service_info.device, service_info.advertisement):
uuids = service_info.service_uuids
if str(UUID_SERVICE) in uuids:
return True
return False
@@ -4,8 +4,7 @@
"bluetooth": [
{
"connectable": false,
"manufacturer_data_start": [79, 68, 70, 74, 65, 82],
"manufacturer_id": 20296
"service_uuid": "77a2bd49-1e5a-4961-bba1-21f34fa4bc7b"
}
],
"codeowners": ["@elupus"],
+1 -9
View File
@@ -144,15 +144,7 @@ BLUETOOTH: Final[list[dict[str, bool | str | int | list[int]]]] = [
{
"connectable": False,
"domain": "fjaraskupan",
"manufacturer_data_start": [
79,
68,
70,
74,
65,
82,
],
"manufacturer_id": 20296,
"service_uuid": "77a2bd49-1e5a-4961-bba1-21f34fa4bc7b",
},
{
"connectable": True,
+19 -2
View File
@@ -1,15 +1,17 @@
"""Tests for the Fjäråskupan integration."""
from fjaraskupan import ANNOUNCE_MANUFACTURER, DEVICE_NAME
from homeassistant.components.bluetooth import BluetoothServiceInfoBleak
from tests.components.bluetooth import generate_advertisement_data, generate_ble_device
COOKER_SERVICE_INFO = BluetoothServiceInfoBleak(
name="COOKERHOOD_FJAR",
name=DEVICE_NAME,
address="AA:BB:CC:DD:EE:FF",
rssi=-60,
manufacturer_data={},
service_uuids=[],
service_uuids=["77a2bd49-1e5a-4961-bba1-21f34fa4bc7b"],
service_data={},
source="local",
device=generate_ble_device(address="AA:BB:CC:DD:EE:FF", name="COOKERHOOD_FJAR"),
@@ -18,3 +20,18 @@ COOKER_SERVICE_INFO = BluetoothServiceInfoBleak(
connectable=True,
tx_power=-127,
)
COOKER_SERVICE_INFO_DATA = BluetoothServiceInfoBleak(
name=DEVICE_NAME,
address="AA:BB:CC:DD:EE:FF",
rssi=-60,
manufacturer_data={ANNOUNCE_MANUFACTURER: b"ODFJAR\x01\x02\x00\x00\x00\x30\x04"},
service_uuids=[],
service_data={},
source="local",
device=generate_ble_device(address="AA:BB:CC:DD:EE:FF", name=DEVICE_NAME),
advertisement=generate_advertisement_data(),
time=0,
connectable=True,
tx_power=-127,
)
@@ -10,7 +10,9 @@ from homeassistant.components.fjaraskupan.const import DOMAIN
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from . import COOKER_SERVICE_INFO
from . import COOKER_SERVICE_INFO, COOKER_SERVICE_INFO_DATA
from tests.components.bluetooth import inject_bluetooth_service_info
@pytest.fixture(name="mock_setup_entry", autouse=True)
@@ -60,3 +62,36 @@ async def test_scan_no_devices(hass: HomeAssistant) -> None:
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "no_devices_found"
async def test_discovery(hass: HomeAssistant) -> None:
"""Test we get the form."""
inject_bluetooth_service_info(
hass,
COOKER_SERVICE_INFO,
)
await hass.async_block_till_done(wait_background_tasks=True)
result = next(iter(hass.config_entries.flow.async_progress_by_handler(DOMAIN)))
assert result["step_id"] == "confirm"
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == "Fjäråskupan"
assert result["data"] == {}
async def test_discovery_ignored_without_service(hass: HomeAssistant) -> None:
"""Test we don't start discovery on only manufacturer data since that can be invalid."""
inject_bluetooth_service_info(
hass,
COOKER_SERVICE_INFO_DATA,
)
await hass.async_block_till_done(wait_background_tasks=True)
result = next(
iter(hass.config_entries.flow.async_progress_by_handler(DOMAIN)), None
)
assert result is None
+55 -6
View File
@@ -15,6 +15,17 @@ from tests.components.bluetooth import (
)
from tests.typing import WebSocketGenerator
MOCK_SERVICE_INFO_DISCOVERY = BluetoothServiceInfo(
address="11:11:11:11:11:11",
name=DEVICE_NAME,
service_uuids=["77a2bd49-1e5a-4961-bba1-21f34fa4bc7b"],
rssi=-60,
manufacturer_data={},
service_data={},
source="local",
)
MOCK_SERVICE_INFO = BluetoothServiceInfo(
address="11:11:11:11:11:11",
name=DEVICE_NAME,
@@ -38,6 +49,46 @@ async def test_setup(
config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id) is True
inject_bluetooth_service_info(
hass,
MOCK_SERVICE_INFO_DISCOVERY,
)
await hass.async_block_till_done()
device_entry = device_registry.async_get_device(
identifiers={(DOMAIN, MOCK_SERVICE_INFO_DISCOVERY.address)}
)
assert device_entry is not None
assert device_entry.manufacturer == "Fjäråskupan"
assert device_entry.name == "Fjäråskupan"
state = hass.states.get("fan.fjaraskupan")
assert state
assert state.state == "off"
inject_bluetooth_service_info(
hass,
MOCK_SERVICE_INFO,
)
await hass.async_block_till_done()
state = hass.states.get("fan.fjaraskupan")
assert state
assert state.state == "on"
async def test_setup_ignore_device(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
) -> None:
"""Test setup does not create a device before we see service."""
config_entry = MockConfigEntry(
domain=DOMAIN,
data={},
)
config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id) is True
inject_bluetooth_service_info(
hass,
MOCK_SERVICE_INFO,
@@ -45,11 +96,9 @@ async def test_setup(
await hass.async_block_till_done()
device_entry = device_registry.async_get_device(
identifiers={(DOMAIN, MOCK_SERVICE_INFO.address)}
identifiers={(DOMAIN, MOCK_SERVICE_INFO_DISCOVERY.address)}
)
assert device_entry is not None
assert device_entry.manufacturer == "Fjäråskupan"
assert device_entry.name == "Fjäråskupan"
assert device_entry is None
async def test_remove_device(
@@ -67,11 +116,11 @@ async def test_remove_device(
config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id) is True
inject_bluetooth_service_info(hass, MOCK_SERVICE_INFO)
inject_bluetooth_service_info(hass, MOCK_SERVICE_INFO_DISCOVERY)
await hass.async_block_till_done()
device_entry = device_registry.async_get_device(
identifiers={(DOMAIN, MOCK_SERVICE_INFO.address)}
identifiers={(DOMAIN, MOCK_SERVICE_INFO_DISCOVERY.address)}
)
assert device_entry