Repository: home-assistant/core
Pull request: Add bluetooth api to get the count of connectable and non-connectable scanners
~~TODO:~~

- [x] dev docs https://github.com/home-assistant/developers.home-assistant/pull/1452

## Proposed change
<!--
  Describe the big picture of your changes here to communicate to the
  maintainers why we should accept this pull request. If it fixes a bug
  or resolves a feature request, be sure to link to that issue in the
  additional information section.
-->

Some integrations need to raise an error if there are no connectable scanners available or no scanners at all.

closes #76281


## Type of change
<!--
  What type of change does your PR introduce to Home Assistant?
  NOTE: Please, check only 1! box!
  If your PR requires multiple boxes to be checked, you'll most likely need to
  split it into multiple PRs. This makes things easier and faster to code review.
-->

- [ ] Dependency upgrade
- [ ] Bugfix (non-breaking change which fixes an issue)
- [ ] New integration (thank you!)
- [x] New feature (which adds functionality to an existing integration)
- [ ] Deprecation (breaking change to happen in the future)
- [ ] Breaking change (fix/feature causing existing functionality to break)
- [ ] Code quality improvements to existing code or addition of tests

## Additional information
<!--
  Details are important, and help maintainers processing your PR.
  Please be sure to fill out additional details, if applicable.
-->

- This PR fixes or closes issue: fixes #
- This PR is related to issue: 
- Link to documentation pull request: 

## Checklist
<!--
  Put an `x` in the boxes that apply. You can also fill these out after
  creating the PR. If you're unsure about any of them, don't hesitate to ask.
  We're here to help! This is simply a reminder of what we are going to look
  for before merging your code.
-->

- [x] The code change is tested and works locally.
- [ ] Local tests pass. **Your PR cannot be merged unless tests pass**
- [ ] There is no commented out code in this PR.
- [ ] I have followed the [development checklist][dev-checklist]
- [ ] The code has been formatted using Black (`black --fast homeassistant tests`)
- [x] Tests have been added to verify that the new code works.

If user exposed functionality or configuration variables are added/changed:

- [ ] Documentation added/updated for [www.home-assistant.io][docs-repository]

If the code communicates with devices, web services, or third-party tools:

- [ ] The [manifest file][manifest-docs] has all fields filled out correctly.  
      Updated and included derived files by running: `python3 -m script.hassfest`.
- [ ] New or updated dependencies have been added to `requirements_all.txt`.  
      Updated by running `python3 -m script.gen_requirements_all`.
- [ ] For the updated dependencies - a link to the changelog, or at minimum a diff between library versions is added to the PR description.
- [ ] Untested files have been added to `.coveragerc`.

The integration reached or maintains the following [Integration Quality Scale][quality-scale]:
<!--
  The Integration Quality Scale scores an integration on the code quality
  and user experience. Each level of the quality scale consists of a list
  of requirements. We highly recommend getting your integration scored!
-->

- [ ] No score or internal
- [ ] 🥈 Silver
- [ ] 🥇 Gold
- [ ] 🏆 Platinum

<!--
  This project is very active and we have a high turnover of pull requests.

  Unfortunately, the number of incoming pull requests is higher than what our
  reviewers can review and merge so there is a long backlog of pull requests
  waiting for review. You can help here!
  
  By reviewing another pull request, you will help raise the code quality of
  that pull request and the final review will be faster. This way the general
  pace of pull request reviews will go up and your wait time will go down.
  
  When picking a pull request to review, try to choose one that hasn't yet
  been reviewed.

  Thanks for helping out!
-->

To help with the load of incoming pull requests:

- [ ] I have reviewed two other [open pull requests][prs] in this repository.

[prs]: https://github.com/home-assistant/core/pulls?q=is%3Aopen+is%3Apr+-author%3A%40me+-draft%3Atrue+-label%3Awaiting-for-upstream+sort%3Acreated-desc+review%3Anone+-status%3Afailure

<!--
  Thank you for contributing <3

  Below, some useful links you could explore:
-->
[dev-checklist]: https://developers.home-assistant.io/docs/en/development_checklist.html
[manifest-docs]: https://developers.home-assistant.io/docs/en/creating_integration_manifest.html
[quality-scale]: https://developers.home-assistant.io/docs/en/next/integration_quality_scale_index.html
[docs-repository]: https://github.com/home-assistant/home-assistant.io


Problem statement:
bluetooth.async_get_scanner() returns broken scanner (not None) if bluetooth integration discovered but not configured
### The problem

I am trying to adapt my custom integration with the new bluetooth component.
In the config_flow, I have a step to scan for available devices.

I was trying to ensure that if the bluetooth integration is enabled, then my custom integration would use HAbleakscanner, but if it is not, it would use the bleak scanner directly.
But it turns out that if the bluetooth is discovered but not enabled, the call to bluetooth.async_get_scanner() returns a scanner object that is failing to discover any devices and raising an exception.  
Is it that it is not created completely since we did not configured `bluetooth`?
I would have expected a `None` return so that I can then just create a `BleakScanner`.

If `bluetooth` is configured, then the HAbleakscanner provided is working perfectly, discovering the devices.

More general question, I assume going forward is it recommended to have the `bluetooth` as a dependency (so that all integrations are using the scanner managed by HA), however, that does not "force" the user to configure it.
And if it is not configured, the HAbleakscanner does not work...  
So how to tackle properly the cases where bluetooth is discovered but not fully configured, even though I put `bluetooth` as a dependency`?


### What version of Home Assistant Core has the issue?

2022.8.0

### What was the last working version of Home Assistant Core?

n/a

### What type of installation are you running?

Home Assistant Container

### Integration causing the issue

bluetooth

### Link to integration documentation on our website

_No response_

### Diagnostics information

In the config_flow I have the fllowing testing code:

```python
 scanner = bluetooth.async_get_scanner(self.hass)
  _LOGGER.debug(f"scanner is {scanner}")
  devices = await discover_yeelight_lamps(scanner) # expecting HAscanner or None and will instanciate a BleakScanenr
```

```log
2022-08-05 16:25:36.121 DEBUG (MainThread) [custom_components.yeelight_bt.config_flow] Starting a scan for Yeelight Bt devices
2022-08-05 16:25:36.121 DEBUG (MainThread) [custom_components.yeelight_bt.config_flow] scanner is <homeassistant.components.bluetooth.models.HaBleakScannerWrapper object at 0x7f474bf920e0>
2022-08-05 16:25:41.122 ERROR (MainThread) [aiohttp.server] Error handling request
Traceback (most recent call last):
  File "/usr/local/lib/python3.10/site-packages/aiohttp/web_protocol.py", line 435, in _handle_request
    resp = await request_handler(request)
  File "/usr/local/lib/python3.10/site-packages/aiohttp/web_app.py", line 504, in _handle
    resp = await handler(request)
  File "/usr/local/lib/python3.10/site-packages/aiohttp/web_middlewares.py", line 117, in impl
    return await handler(request)
  File "/usr/src/homeassistant/homeassistant/components/http/security_filter.py", line 60, in security_filter_middleware
    return await handler(request)
  File "/usr/src/homeassistant/homeassistant/components/http/forwarded.py", line 222, in forwarded_middleware
    return await handler(request)
  File "/usr/src/homeassistant/homeassistant/components/http/request_context.py", line 28, in request_context_middleware
    return await handler(request)
  File "/usr/src/homeassistant/homeassistant/components/http/ban.py", line 82, in ban_middleware
    return await handler(request)
  File "/usr/src/homeassistant/homeassistant/components/http/auth.py", line 236, in auth_middleware
    return await handler(request)
  File "/usr/src/homeassistant/homeassistant/components/http/view.py", line 136, in handle
    result = await result
  File "/usr/src/homeassistant/homeassistant/components/config/config_entries.py", line 177, in post
    return await super().post(request, flow_id)
  File "/usr/src/homeassistant/homeassistant/components/http/data_validator.py", line 73, in wrapper
    result = await method(view, request, data, *args, **kwargs)
  File "/usr/src/homeassistant/homeassistant/helpers/data_entry_flow.py", line 109, in post
    result = await self._flow_mgr.async_configure(flow_id, data)
  File "/usr/src/homeassistant/homeassistant/data_entry_flow.py", line 277, in async_configure
    result = await self._async_handle_step(
  File "/usr/src/homeassistant/homeassistant/data_entry_flow.py", line 359, in _async_handle_step
    result: FlowResult = await getattr(flow, method)(user_input)
  File "/config/custom_components/yeelight_bt/config_flow.py", line 58, in async_step_scan
    ble_devices = await discover_yeelight_lamps(scanner)
  File "/config/custom_components/yeelight_bt/yeelightbt.py", line 390, in discover_yeelight_lamps
    devices = await scanner.discover()
  File "/usr/local/lib/python3.10/site-packages/bleak/backends/scanner.py", line 118, in discover
    devices = scanner.discovered_devices
  File "/usr/src/homeassistant/homeassistant/components/bluetooth/models.py", line 172, in discovered_devices
    return HA_BLEAK_SCANNER.discovered_devices
  File "/usr/local/lib/python3.10/site-packages/bleak/backends/bluezdbus/scanner.py", line 183, in discovered_devices
    for path, props in self._devices.items():
AttributeError: 'HaBleakScanner' object has no attribute '_devices'
```

### Example YAML snippet

_No response_

### Anything in the logs that might be useful for us?

_No response_

### Additional information

_No response_

Hints from issue discussion:
[bluetooth documentation](https://www.home-assistant.io/integrations/bluetooth)
[bluetooth source](https://github.com/home-assistant/core/tree/dev/homeassistant/components/bluetooth)
<sub><sup>(message by IssueLinks)</sup></sub>
Hey there @bdraco, mind taking a look at this issue as it has been labeled with an integration (`bluetooth`) you are listed as a [code owner](https://github.com/home-assistant/core/blob/dev/CODEOWNERS#L141) for? Thanks!
<sub><sup>(message by CodeOwnersMention)</sup></sub>
The intent is that the scanner will start working as soon as the user configures bluetooth so they don't have to restart to enable it
Adding `bluetooth` as a dep is a good practice.  We talked about adding a new API `async_enabled` or the like that would allow you to know if its enabled.  Nothing has really needed it yet so we didn't move forward with doing that. 
I see, thanks for clarifying.
I think knowing when it is bluetooth is enabled and ready to scan would be beneficial, especially for setting up integration (if user has not configured the bluetooth).  
So at the moment there is no easy way to check if user has configure the `bluetooth`?

I suppose the alternative is a try/except a scan with the HAscanner and if errors, message the user to configure bluetooth first. 
> So at the moment there is no easy way to check if user has configure the `bluetooth`?

You could check if a non-ignored config entry exists, but thats an implementation detail we want to hide behind an API since it might change in the future.

Commit message: Add bluetooth api to get the count of connectable and non-connectable scanners

closes #76281

Patch to review:
diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py
index 632635f7dbcd42..8ba3a503a30257 100644
--- a/homeassistant/components/bluetooth/__init__.py
+++ b/homeassistant/components/bluetooth/__init__.py
@@ -60,6 +60,7 @@
     "async_rediscover_address",
     "async_register_callback",
     "async_track_unavailable",
+    "async_scanner_count",
     "BaseHaScanner",
     "BluetoothServiceInfo",
     "BluetoothServiceInfoBleak",
@@ -86,6 +87,12 @@ def async_get_scanner(hass: HomeAssistant) -> HaBleakScannerWrapper:
     return HaBleakScannerWrapper()
 
 
+@hass_callback
+def async_scanner_count(hass: HomeAssistant, connectable: bool = True) -> int:
+    """Return the number of scanners currently in use."""
+    return _get_manager(hass).async_scanner_count(connectable)
+
+
 @hass_callback
 def async_discovered_service_info(
     hass: HomeAssistant, connectable: bool = True
diff --git a/homeassistant/components/bluetooth/manager.py b/homeassistant/components/bluetooth/manager.py
index b1193c47245dbc..259c3d76cfbeeb 100644
--- a/homeassistant/components/bluetooth/manager.py
+++ b/homeassistant/components/bluetooth/manager.py
@@ -153,6 +153,12 @@ def supports_passive_scan(self) -> bool:
         """Return if passive scan is supported."""
         return any(adapter[ADAPTER_PASSIVE_SCAN] for adapter in self._adapters.values())
 
+    def async_scanner_count(self, connectable: bool = True) -> int:
+        """Return the number of scanners."""
+        if connectable:
+            return len(self._connectable_scanners)
+        return len(self._connectable_scanners) + len(self._scanners)
+
     async def async_diagnostics(self) -> dict[str, Any]:
         """Diagnostics for the manager."""
         scanner_diagnostics = await asyncio.gather(
diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py
index a005a71f048a4b..ade68fdb94da25 100644
--- a/tests/components/bluetooth/test_init.py
+++ b/tests/components/bluetooth/test_init.py
@@ -2333,6 +2333,22 @@ async def test_getting_the_scanner_returns_the_wrapped_instance(hass, enable_blu
     assert isinstance(scanner, models.HaBleakScannerWrapper)
 
 
+async def test_scanner_count_connectable(hass, enable_bluetooth):
+    """Test getting the connectable scanner count."""
+    scanner = models.BaseHaScanner()
+    cancel = bluetooth.async_register_scanner(hass, scanner, False)
+    assert bluetooth.async_scanner_count(hass, connectable=True) == 1
+    cancel()
+
+
+async def test_scanner_count(hass, enable_bluetooth):
+    """Test getting the connectable and non-connectable scanner count."""
+    scanner = models.BaseHaScanner()
+    cancel = bluetooth.async_register_scanner(hass, scanner, False)
+    assert bluetooth.async_scanner_count(hass, connectable=False) == 2
+    cancel()
+
+
 async def test_migrate_single_entry_macos(
     hass, mock_bleak_scanner_start, macos_adapter
 ):


Code review comments:
diff_hunk: @@ -153,6 +153,12 @@ def supports_passive_scan(self) -> bool:
         """Return if passive scan is supported."""
         return any(adapter[ADAPTER_PASSIVE_SCAN] for adapter in self._adapters.values())
 
+    def async_scanner_count(self, connectable: bool = True) -> int:
+        """Return the number of scanners."""
+        if connectable:
+            return len(self._connectable_scanners)
+        return len(self._connectable_scanners) + len(self._scanners)
path: homeassistant/components/bluetooth/manager.py
text: @user1:
Maybe not related to this PR, but the the attribute naming seems confusing if it's correct that `self._connectable_scanners` are connectable scanners and `self._scanners` are non-connectable scanners. Based on the naming, I would expect `self._scanners` to be all scanners, not only the non-connectable ones.

@author:
Renamed in 96534fa086