Add new Probe Plus integration (#143424)

* Add probe_plus integration

* Changes for quality scale

* sentence-casing

* Update homeassistant/components/probe_plus/config_flow.py

Co-authored-by: Erwin Douna <e.douna@gmail.com>

* Update homeassistant/components/probe_plus/config_flow.py

Co-authored-by: Erwin Douna <e.douna@gmail.com>

* Update tests/components/probe_plus/test_config_flow.py

Co-authored-by: Erwin Douna <e.douna@gmail.com>

* Update tests/components/probe_plus/test_config_flow.py

Co-authored-by: Erwin Douna <e.douna@gmail.com>

* remove version from configflow

* remove address var from async_step_bluetooth_confirm

* move timedelta to SCAN_INTERVAL in coordinator

* update tests

* updates from review

* add voltage device class

* remove unused logger

* remove names

* update tests

* Update config flow tests

* Update unit tests

* Reorder successful tests

* Update config entry typing

* Remove icons

* ruff

* Update async_add_entities logic

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* sensor platform formatting

---------

Co-authored-by: Erwin Douna <e.douna@gmail.com>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
Jordan Harvey
2025-05-19 21:50:09 +01:00
committed by GitHub
parent df3688ef08
commit 20ce879471
19 changed files with 785 additions and 0 deletions

2
CODEOWNERS generated
View File

@ -1178,6 +1178,8 @@ build.json @home-assistant/supervisor
/tests/components/powerwall/ @bdraco @jrester @daniel-simpson
/homeassistant/components/private_ble_device/ @Jc2k
/tests/components/private_ble_device/ @Jc2k
/homeassistant/components/probe_plus/ @pantherale0
/tests/components/probe_plus/ @pantherale0
/homeassistant/components/profiler/ @bdraco
/tests/components/profiler/ @bdraco
/homeassistant/components/progettihwsw/ @ardaseremet

View File

@ -0,0 +1,24 @@
"""The Probe Plus integration."""
from __future__ import annotations
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from .coordinator import ProbePlusConfigEntry, ProbePlusDataUpdateCoordinator
PLATFORMS: list[Platform] = [Platform.SENSOR]
async def async_setup_entry(hass: HomeAssistant, entry: ProbePlusConfigEntry) -> bool:
"""Set up Probe Plus from a config entry."""
coordinator = ProbePlusDataUpdateCoordinator(hass, entry)
await coordinator.async_config_entry_first_refresh()
entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ProbePlusConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

View File

@ -0,0 +1,125 @@
"""Config flow for probe_plus integration."""
from __future__ import annotations
import dataclasses
import logging
from typing import Any
import voluptuous as vol
from homeassistant.components.bluetooth import (
BluetoothServiceInfo,
async_discovered_service_info,
)
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_ADDRESS
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
@dataclasses.dataclass(frozen=True)
class Discovery:
"""Represents a discovered Bluetooth device.
Attributes:
title: The name or title of the discovered device.
discovery_info: Information about the discovered device.
"""
title: str
discovery_info: BluetoothServiceInfo
def title(discovery_info: BluetoothServiceInfo) -> str:
"""Return a title for the discovered device."""
return f"{discovery_info.name} {discovery_info.address}"
class ProbeConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for BT Probe."""
def __init__(self) -> None:
"""Initialize the config flow."""
self._discovered_devices: dict[str, Discovery] = {}
async def async_step_bluetooth(
self, discovery_info: BluetoothServiceInfo
) -> ConfigFlowResult:
"""Handle the bluetooth discovery step."""
_LOGGER.debug("Discovered BT device: %s", discovery_info)
await self.async_set_unique_id(discovery_info.address)
self._abort_if_unique_id_configured()
self.context["title_placeholders"] = {"name": title(discovery_info)}
self._discovered_devices[discovery_info.address] = Discovery(
title(discovery_info), discovery_info
)
return await self.async_step_bluetooth_confirm()
async def async_step_bluetooth_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the bluetooth confirmation step."""
if user_input is not None:
assert self.unique_id
self._abort_if_unique_id_configured()
discovery = self._discovered_devices[self.unique_id]
return self.async_create_entry(
title=discovery.title,
data={
CONF_ADDRESS: discovery.discovery_info.address,
},
)
self._set_confirm_only()
assert self.unique_id
return self.async_show_form(
step_id="bluetooth_confirm",
description_placeholders={
"name": title(self._discovered_devices[self.unique_id].discovery_info)
},
)
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the user step to pick discovered device."""
if user_input is not None:
address = user_input[CONF_ADDRESS]
await self.async_set_unique_id(address, raise_on_progress=False)
self._abort_if_unique_id_configured()
discovery = self._discovered_devices[address]
return self.async_create_entry(
title=discovery.title,
data=user_input,
)
current_addresses = self._async_current_ids()
for discovery_info in async_discovered_service_info(self.hass):
address = discovery_info.address
if address in current_addresses or address in self._discovered_devices:
continue
self._discovered_devices[address] = Discovery(
title(discovery_info), discovery_info
)
if not self._discovered_devices:
return self.async_abort(reason="no_devices_found")
titles = {
address: discovery.title
for (address, discovery) in self._discovered_devices.items()
}
return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
{
vol.Required(CONF_ADDRESS): vol.In(titles),
}
),
)

View File

@ -0,0 +1,3 @@
"""Constants for the Probe Plus integration."""
DOMAIN = "probe_plus"

View File

@ -0,0 +1,68 @@
"""Coordinator for the probe_plus integration."""
from __future__ import annotations
from datetime import timedelta
import logging
from pyprobeplus import ProbePlusDevice
from pyprobeplus.exceptions import ProbePlusDeviceNotFound, ProbePlusError
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ADDRESS
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
type ProbePlusConfigEntry = ConfigEntry[ProbePlusDataUpdateCoordinator]
_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(seconds=15)
class ProbePlusDataUpdateCoordinator(DataUpdateCoordinator[None]):
"""Coordinator to manage data updates for a probe device.
This class handles the communication with Probe Plus devices.
Data is updated by the device itself.
"""
config_entry: ProbePlusConfigEntry
def __init__(self, hass: HomeAssistant, entry: ProbePlusConfigEntry) -> None:
"""Initialize coordinator."""
super().__init__(
hass,
_LOGGER,
name="ProbePlusDataUpdateCoordinator",
update_interval=SCAN_INTERVAL,
config_entry=entry,
)
self.device: ProbePlusDevice = ProbePlusDevice(
address_or_ble_device=entry.data[CONF_ADDRESS],
name=entry.title,
notify_callback=self.async_update_listeners,
)
async def _async_update_data(self) -> None:
"""Connect to the Probe Plus device on a set interval.
This method is called periodically to reconnect to the device
Data updates are handled by the device itself.
"""
# Already connected, no need to update any data as the device streams this.
if self.device.connected:
return
# Probe is not connected, try to connect
try:
await self.device.connect()
except (ProbePlusError, ProbePlusDeviceNotFound, TimeoutError) as e:
_LOGGER.debug(
"Could not connect to scale: %s, Error: %s",
self.config_entry.data[CONF_ADDRESS],
e,
)
self.device.device_disconnected_handler(notify=False)
return

View File

@ -0,0 +1,54 @@
"""Probe Plus base entity type."""
from dataclasses import dataclass
from pyprobeplus import ProbePlusDevice
from homeassistant.helpers.device_registry import (
CONNECTION_BLUETOOTH,
DeviceInfo,
format_mac,
)
from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
from .coordinator import ProbePlusDataUpdateCoordinator
@dataclass
class ProbePlusEntity(CoordinatorEntity[ProbePlusDataUpdateCoordinator]):
"""Base class for Probe Plus entities."""
_attr_has_entity_name = True
def __init__(
self,
coordinator: ProbePlusDataUpdateCoordinator,
entity_description: EntityDescription,
) -> None:
"""Initialize the entity."""
super().__init__(coordinator)
self.entity_description = entity_description
# Set the unique ID for the entity
self._attr_unique_id = (
f"{format_mac(coordinator.device.mac)}_{entity_description.key}"
)
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, format_mac(coordinator.device.mac))},
name=coordinator.device.name,
manufacturer="Probe Plus",
suggested_area="Kitchen",
connections={(CONNECTION_BLUETOOTH, coordinator.device.mac)},
)
@property
def available(self) -> bool:
"""Return True if the entity is available."""
return super().available and self.coordinator.device.connected
@property
def device(self) -> ProbePlusDevice:
"""Return the device associated with this entity."""
return self.coordinator.device

View File

@ -0,0 +1,9 @@
{
"entity": {
"sensor": {
"probe_temperature": {
"default": "mdi:thermometer-bluetooth"
}
}
}
}

View File

@ -0,0 +1,19 @@
{
"domain": "probe_plus",
"name": "Probe Plus",
"bluetooth": [
{
"connectable": true,
"manufacturer_id": 36606,
"local_name": "FM2*"
}
],
"codeowners": ["@pantherale0"],
"config_flow": true,
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/probe_plus",
"integration_type": "device",
"iot_class": "local_push",
"quality_scale": "bronze",
"requirements": ["pyprobeplus==1.0.0"]
}

View File

@ -0,0 +1,100 @@
rules:
# Bronze
action-setup:
status: exempt
comment: |
No custom actions are defined.
appropriate-polling: done
brands: done
common-modules: done
config-flow-test-coverage: done
config-flow: done
dependency-transparency: done
docs-actions:
status: exempt
comment: |
No custom actions are defined.
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
entity-event-setup:
status: exempt
comment: |
No explicit event subscriptions.
entity-unique-id: done
has-entity-name: done
runtime-data: done
test-before-configure: done
test-before-setup:
status: exempt
comment: |
Device is expected to be offline most of the time, but needs to connect quickly once available.
unique-config-entry: done
# Silver
action-exceptions:
status: exempt
comment: |
No custom actions are defined.
config-entry-unloading: done
docs-configuration-parameters: done
docs-installation-parameters: done
entity-unavailable: done
integration-owner: done
log-when-unavailable:
status: done
comment: |
Handled by coordinator.
parallel-updates: done
reauthentication-flow:
status: exempt
comment: |
No authentication required.
test-coverage: todo
# Gold
devices: done
diagnostics: todo
discovery-update-info:
status: exempt
comment: |
No IP discovery.
discovery:
status: done
comment: |
The integration uses Bluetooth discovery to find devices.
docs-data-update: done
docs-examples: todo
docs-known-limitations: done
docs-supported-devices: done
docs-supported-functions: done
docs-troubleshooting: done
docs-use-cases: done
dynamic-devices: done
entity-category: done
entity-device-class: done
entity-disabled-by-default: done
entity-translations: done
exception-translations:
status: exempt
comment: |
No custom exceptions are defined.
icon-translations: done
reconfiguration-flow:
status: exempt
comment: |
No reconfiguration flow is needed as the only thing that could be changed is the MAC, which is already hardcoded on the device itself.
repair-issues:
status: exempt
comment: |
No repair issues.
stale-devices:
status: exempt
comment: |
The device itself is the integration.
# Platinum
async-dependency: done
inject-websession:
status: exempt
comment: |
No web session is used.
strict-typing: todo

View File

@ -0,0 +1,106 @@
"""Support for Probe Plus BLE sensors."""
from collections.abc import Callable
from dataclasses import dataclass
from homeassistant.components.sensor import (
RestoreSensor,
SensorDeviceClass,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.const import (
PERCENTAGE,
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
EntityCategory,
UnitOfElectricPotential,
UnitOfTemperature,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import ProbePlusConfigEntry, ProbePlusDevice
from .entity import ProbePlusEntity
# Coordinator is used to centralize the data updates
PARALLEL_UPDATES = 0
@dataclass(kw_only=True, frozen=True)
class ProbePlusSensorEntityDescription(SensorEntityDescription):
"""Description for Probe Plus sensor entities."""
value_fn: Callable[[ProbePlusDevice], int | float | None]
SENSOR_DESCRIPTIONS: tuple[ProbePlusSensorEntityDescription, ...] = (
ProbePlusSensorEntityDescription(
key="probe_temperature",
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
value_fn=lambda device: device.device_state.probe_temperature,
device_class=SensorDeviceClass.TEMPERATURE,
),
ProbePlusSensorEntityDescription(
key="probe_battery",
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=PERCENTAGE,
value_fn=lambda device: device.device_state.probe_battery,
device_class=SensorDeviceClass.BATTERY,
),
ProbePlusSensorEntityDescription(
key="relay_battery",
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=PERCENTAGE,
value_fn=lambda device: device.device_state.relay_battery,
device_class=SensorDeviceClass.BATTERY,
),
ProbePlusSensorEntityDescription(
key="probe_rssi",
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda device: device.device_state.probe_rssi,
entity_registry_enabled_default=False,
),
ProbePlusSensorEntityDescription(
key="relay_voltage",
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
entity_category=EntityCategory.DIAGNOSTIC,
device_class=SensorDeviceClass.VOLTAGE,
value_fn=lambda device: device.device_state.relay_voltage,
entity_registry_enabled_default=False,
),
ProbePlusSensorEntityDescription(
key="probe_voltage",
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
entity_category=EntityCategory.DIAGNOSTIC,
device_class=SensorDeviceClass.VOLTAGE,
value_fn=lambda device: device.device_state.probe_voltage,
entity_registry_enabled_default=False,
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: ProbePlusConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Probe Plus sensors."""
coordinator = entry.runtime_data
async_add_entities(ProbeSensor(coordinator, desc) for desc in SENSOR_DESCRIPTIONS)
class ProbeSensor(ProbePlusEntity, RestoreSensor):
"""Representation of a Probe Plus sensor."""
entity_description: ProbePlusSensorEntityDescription
@property
def native_value(self) -> int | float | None:
"""Return the state of the sensor."""
return self.entity_description.value_fn(self.device)

View File

@ -0,0 +1,49 @@
{
"config": {
"flow_title": "{name}",
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]"
},
"error": {
"device_not_found": "Device could not be found.",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"step": {
"bluetooth_confirm": {
"description": "[%key:component::bluetooth::config::step::bluetooth_confirm::description%]"
},
"user": {
"description": "[%key:component::bluetooth::config::step::user::description%]",
"data": {
"address": "[%key:common::config_flow::data::device%]"
},
"data_description": {
"address": "Select BLE probe you want to set up"
}
}
}
},
"entity": {
"sensor": {
"probe_battery": {
"name": "Probe battery"
},
"probe_temperature": {
"name": "Probe temperature"
},
"probe_rssi": {
"name": "Probe RSSI"
},
"probe_voltage": {
"name": "Probe voltage"
},
"relay_battery": {
"name": "Relay battery"
},
"relay_voltage": {
"name": "Relay voltage"
}
}
}
}

View File

@ -593,6 +593,12 @@ BLUETOOTH: Final[list[dict[str, bool | str | int | list[int]]]] = [
"domain": "oralb",
"manufacturer_id": 220,
},
{
"connectable": True,
"domain": "probe_plus",
"local_name": "FM2*",
"manufacturer_id": 36606,
},
{
"connectable": False,
"domain": "qingping",

View File

@ -487,6 +487,7 @@ FLOWS = {
"powerfox",
"powerwall",
"private_ble_device",
"probe_plus",
"profiler",
"progettihwsw",
"prosegur",

View File

@ -5048,6 +5048,12 @@
"config_flow": true,
"iot_class": "local_push"
},
"probe_plus": {
"name": "Probe Plus",
"integration_type": "device",
"config_flow": true,
"iot_class": "local_push"
},
"profiler": {
"name": "Profiler",
"integration_type": "hub",

3
requirements_all.txt generated
View File

@ -2244,6 +2244,9 @@ pyplaato==0.0.19
# homeassistant.components.point
pypoint==3.0.0
# homeassistant.components.probe_plus
pyprobeplus==1.0.0
# homeassistant.components.profiler
pyprof2calltree==1.4.5

View File

@ -1838,6 +1838,9 @@ pyplaato==0.0.19
# homeassistant.components.point
pypoint==3.0.0
# homeassistant.components.probe_plus
pyprobeplus==1.0.0
# homeassistant.components.profiler
pyprof2calltree==1.4.5

View File

@ -0,0 +1,14 @@
"""Tests for the Probe Plus integration."""
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
async def setup_integration(
hass: HomeAssistant, mock_config_entry: MockConfigEntry
) -> None:
"""Set up the Probe Plus integration for testing."""
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()

View File

@ -0,0 +1,60 @@
"""Common fixtures for the Probe Plus tests."""
from collections.abc import Generator
from unittest.mock import AsyncMock, MagicMock, patch
from pyprobeplus.parser import ParserBase, ProbePlusData
import pytest
from homeassistant.components.probe_plus.const import DOMAIN
from homeassistant.const import CONF_ADDRESS
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
@pytest.fixture
def mock_setup_entry() -> Generator[AsyncMock]:
"""Override async_setup_entry."""
with patch(
"homeassistant.components.probe_plus.async_setup_entry", return_value=True
) as mock_setup_entry:
yield mock_setup_entry
@pytest.fixture
def mock_config_entry(hass: HomeAssistant) -> MockConfigEntry:
"""Return the default mocked config entry."""
return MockConfigEntry(
title="FM210 aa:bb:cc:dd:ee:ff",
domain=DOMAIN,
version=1,
data={
CONF_ADDRESS: "aa:bb:cc:dd:ee:ff",
},
unique_id="aa:bb:cc:dd:ee:ff",
)
@pytest.fixture
def mock_probe_plus() -> MagicMock:
"""Mock the Probe Plus device."""
with patch(
"homeassistant.components.probe_plus.coordinator.ProbePlusDevice",
autospec=True,
) as mock_device:
device = mock_device.return_value
device.connected = True
device.name = "FM210 aa:bb:cc:dd:ee:ff"
mock_state = ParserBase()
mock_state.state = ProbePlusData(
relay_battery=50,
probe_battery=50,
probe_temperature=25.0,
probe_rssi=200,
probe_voltage=3.7,
relay_status=1,
relay_voltage=9.0,
)
device._device_state = mock_state
yield device

View File

@ -0,0 +1,133 @@
"""Test the config flow for the Probe Plus."""
from collections.abc import Generator
from unittest.mock import AsyncMock, patch
import pytest
from homeassistant.components.probe_plus.const import DOMAIN
from homeassistant.config_entries import SOURCE_BLUETOOTH, SOURCE_USER
from homeassistant.const import CONF_ADDRESS
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo
from tests.common import MockConfigEntry
service_info = BluetoothServiceInfo(
name="FM210",
address="aa:bb:cc:dd:ee:ff",
rssi=-63,
manufacturer_data={},
service_data={},
service_uuids=[],
source="local",
)
@pytest.fixture
def mock_discovered_service_info() -> Generator[AsyncMock]:
"""Override getting Bluetooth service info."""
with patch(
"homeassistant.components.probe_plus.config_flow.async_discovered_service_info",
return_value=[service_info],
) as mock_discovered_service_info:
yield mock_discovered_service_info
async def test_user_config_flow_creates_entry(
hass: HomeAssistant,
mock_setup_entry: AsyncMock,
mock_discovered_service_info: AsyncMock,
) -> None:
"""Test the user configuration flow successfully creates a config entry."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user"
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={
CONF_ADDRESS: "aa:bb:cc:dd:ee:ff",
},
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["result"].unique_id == "aa:bb:cc:dd:ee:ff"
assert result["title"] == "FM210 aa:bb:cc:dd:ee:ff"
assert result["data"] == {CONF_ADDRESS: "aa:bb:cc:dd:ee:ff"}
async def test_user_flow_already_configured(
hass: HomeAssistant,
mock_discovered_service_info: AsyncMock,
mock_config_entry: MockConfigEntry,
mock_setup_entry: AsyncMock,
) -> None:
"""Test that the user flow aborts when the entry is already configured."""
mock_config_entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
# this aborts with no devices found as the config flow
# already checks for existing config entries when validating the discovered devices
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "no_devices_found"
async def test_bluetooth_discovery(
hass: HomeAssistant,
mock_setup_entry: AsyncMock,
mock_discovered_service_info: AsyncMock,
) -> None:
"""Test we can discover a device."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_BLUETOOTH}, data=service_info
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "bluetooth_confirm"
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={}
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == "FM210 aa:bb:cc:dd:ee:ff"
assert result["result"].unique_id == "aa:bb:cc:dd:ee:ff"
assert result["data"] == {
CONF_ADDRESS: service_info.address,
}
async def test_already_configured_bluetooth_discovery(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
) -> None:
"""Ensure configure device is not discovered again."""
mock_config_entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_BLUETOOTH}, data=service_info
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured"
async def test_no_bluetooth_devices(
hass: HomeAssistant,
mock_setup_entry: AsyncMock,
mock_discovered_service_info: AsyncMock,
) -> None:
"""Test flow aborts on unsupported device."""
mock_discovered_service_info.return_value = []
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "no_devices_found"