-
-
Notifications
You must be signed in to change notification settings - Fork 37.5k
Add LD2410 BLE integration #83883
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Add LD2410 BLE integration #83883
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
ff023f3
Add LD2410 BLE integration
930913 963f907
Fix pylint
930913 d5c26ca
Fix types
930913 7e4cf3b
Fix typos
930913 534af31
Use BinarySensorEntityDescription
930913 17be51f
Refactor to one class
930913 a34fbda
Comprehend sensors
930913 5322f85
Fix manifest.documentation
930913 34e4a6b
Remove unused constants
930913 6162fe8
Pack entity description
930913 4cf70b6
Refactor availibility
930913 af869ca
Add coordinator file
930913 f41a846
Add coordinator file to coverage exclusion
930913 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,94 @@ | ||
| """The LD2410 BLE integration.""" | ||
|
|
||
| import logging | ||
|
|
||
| from bleak_retry_connector import BleakError, get_device | ||
| from ld2410_ble import LD2410BLE | ||
|
|
||
| from homeassistant.components import bluetooth | ||
| from homeassistant.components.bluetooth.match import ADDRESS, BluetoothCallbackMatcher | ||
| from homeassistant.config_entries import ConfigEntry | ||
| from homeassistant.const import CONF_ADDRESS, EVENT_HOMEASSISTANT_STOP, Platform | ||
| from homeassistant.core import Event, HomeAssistant, callback | ||
| from homeassistant.exceptions import ConfigEntryNotReady | ||
|
|
||
| from .const import DOMAIN | ||
| from .coordinator import LD2410BLECoordinator | ||
| from .models import LD2410BLEData | ||
|
|
||
| PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR] | ||
|
|
||
| _LOGGER = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: | ||
| """Set up LD2410 BLE from a config entry.""" | ||
| address: str = entry.data[CONF_ADDRESS] | ||
| ble_device = bluetooth.async_ble_device_from_address( | ||
| hass, address.upper(), True | ||
| ) or await get_device(address) | ||
| if not ble_device: | ||
| raise ConfigEntryNotReady( | ||
| f"Could not find LD2410B device with address {address}" | ||
| ) | ||
| ld2410_ble = LD2410BLE(ble_device) | ||
|
|
||
| coordinator = LD2410BLECoordinator(hass, ld2410_ble) | ||
|
|
||
| try: | ||
| await ld2410_ble.initialise() | ||
| except BleakError as exc: | ||
| raise ConfigEntryNotReady( | ||
| f"Could not initialise LD2410B device with address {address}" | ||
| ) from exc | ||
|
|
||
| @callback | ||
| def _async_update_ble( | ||
| service_info: bluetooth.BluetoothServiceInfoBleak, | ||
| change: bluetooth.BluetoothChange, | ||
| ) -> None: | ||
| """Update from a ble callback.""" | ||
| ld2410_ble.set_ble_device_and_advertisement_data( | ||
| service_info.device, service_info.advertisement | ||
| ) | ||
|
|
||
| entry.async_on_unload( | ||
| bluetooth.async_register_callback( | ||
| hass, | ||
| _async_update_ble, | ||
| BluetoothCallbackMatcher({ADDRESS: address}), | ||
| bluetooth.BluetoothScanningMode.ACTIVE, | ||
| ) | ||
| ) | ||
|
|
||
| hass.data.setdefault(DOMAIN, {})[entry.entry_id] = LD2410BLEData( | ||
| entry.title, ld2410_ble, coordinator | ||
| ) | ||
|
|
||
| await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) | ||
| entry.async_on_unload(entry.add_update_listener(_async_update_listener)) | ||
|
|
||
| async def _async_stop(event: Event) -> None: | ||
| """Close the connection.""" | ||
| await ld2410_ble.stop() | ||
|
|
||
| entry.async_on_unload( | ||
| hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_stop) | ||
| ) | ||
| return True | ||
|
|
||
|
|
||
| async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: | ||
| """Handle options update.""" | ||
| data: LD2410BLEData = hass.data[DOMAIN][entry.entry_id] | ||
| if entry.title != data.title: | ||
| await hass.config_entries.async_reload(entry.entry_id) | ||
|
|
||
|
|
||
| async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: | ||
| """Unload a config entry.""" | ||
| if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): | ||
| data: LD2410BLEData = hass.data[DOMAIN].pop(entry.entry_id) | ||
| await data.device.stop() | ||
|
|
||
| return unload_ok |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| """LD2410 BLE integration binary sensor platform.""" | ||
|
|
||
|
|
||
| from homeassistant.components.binary_sensor import ( | ||
| BinarySensorDeviceClass, | ||
| BinarySensorEntity, | ||
| BinarySensorEntityDescription, | ||
| ) | ||
| from homeassistant.config_entries import ConfigEntry | ||
| from homeassistant.core import HomeAssistant, callback | ||
| from homeassistant.helpers import device_registry as dr | ||
| from homeassistant.helpers.entity import DeviceInfo | ||
| from homeassistant.helpers.entity_platform import AddEntitiesCallback | ||
| from homeassistant.helpers.update_coordinator import CoordinatorEntity | ||
|
|
||
| from . import LD2410BLE, LD2410BLECoordinator | ||
| from .const import DOMAIN | ||
| from .models import LD2410BLEData | ||
|
|
||
| ENTITY_DESCRIPTIONS = [ | ||
| BinarySensorEntityDescription( | ||
| key="is_moving", | ||
| device_class=BinarySensorDeviceClass.MOTION, | ||
| has_entity_name=True, | ||
| name="Motion", | ||
| ), | ||
| BinarySensorEntityDescription( | ||
| key="is_static", | ||
| device_class=BinarySensorDeviceClass.OCCUPANCY, | ||
| has_entity_name=True, | ||
| name="Occupancy", | ||
| ), | ||
| ] | ||
|
Comment on lines
+20
to
+33
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. MINOR: This could be a tuple since it never changes
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Noted. I might change it in a subsequent MR. |
||
|
|
||
|
|
||
| async def async_setup_entry( | ||
| hass: HomeAssistant, | ||
| entry: ConfigEntry, | ||
| async_add_entities: AddEntitiesCallback, | ||
| ) -> None: | ||
| """Set up the platform for LD2410BLE.""" | ||
| data: LD2410BLEData = hass.data[DOMAIN][entry.entry_id] | ||
| async_add_entities( | ||
| LD2410BLEBinarySensor(data.coordinator, data.device, entry.title, description) | ||
| for description in ENTITY_DESCRIPTIONS | ||
| ) | ||
|
|
||
|
|
||
| class LD2410BLEBinarySensor(CoordinatorEntity, BinarySensorEntity): | ||
| """Moving/static sensor for LD2410BLE.""" | ||
|
|
||
| def __init__( | ||
| self, | ||
| coordinator: LD2410BLECoordinator, | ||
| device: LD2410BLE, | ||
| name: str, | ||
| description: BinarySensorEntityDescription, | ||
| ) -> None: | ||
| """Initialize the sensor.""" | ||
| super().__init__(coordinator) | ||
| self._coordinator = coordinator | ||
| self._key = description.key | ||
| self._device = device | ||
| self.entity_description = description | ||
| self._attr_unique_id = f"{device.address}_{self._key}" | ||
| self._attr_device_info = DeviceInfo( | ||
| name=name, | ||
| connections={(dr.CONNECTION_BLUETOOTH, device.address)}, | ||
| ) | ||
| self._attr_is_on = getattr(self._device, self._key) | ||
|
bdraco marked this conversation as resolved.
|
||
|
|
||
| @callback | ||
| def _handle_coordinator_update(self) -> None: | ||
| """Handle updated data from the coordinator.""" | ||
| self._attr_is_on = getattr(self._device, self._key) | ||
|
bdraco marked this conversation as resolved.
|
||
| self.async_write_ha_state() | ||
|
|
||
| @property | ||
| def available(self) -> bool: | ||
| """Unavailable if coordinator isn't connected.""" | ||
| return self._coordinator.connected and super().available | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,112 @@ | ||
| """Config flow for LD2410BLE integration.""" | ||
| from __future__ import annotations | ||
|
|
||
| import logging | ||
| from typing import Any | ||
|
|
||
| from bluetooth_data_tools import human_readable_name | ||
| from ld2410_ble import BLEAK_EXCEPTIONS, LD2410BLE | ||
| import voluptuous as vol | ||
|
|
||
| from homeassistant import config_entries | ||
| from homeassistant.components.bluetooth import ( | ||
| BluetoothServiceInfoBleak, | ||
| async_discovered_service_info, | ||
| ) | ||
| from homeassistant.const import CONF_ADDRESS | ||
| from homeassistant.data_entry_flow import FlowResult | ||
|
|
||
| from .const import DOMAIN, LOCAL_NAMES | ||
|
|
||
| _LOGGER = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): | ||
| """Handle a config flow for LD2410 BLE.""" | ||
|
|
||
| VERSION = 1 | ||
|
|
||
| def __init__(self) -> None: | ||
| """Initialize the config flow.""" | ||
| self._discovery_info: BluetoothServiceInfoBleak | None = None | ||
| self._discovered_devices: dict[str, BluetoothServiceInfoBleak] = {} | ||
|
|
||
| async def async_step_bluetooth( | ||
| self, discovery_info: BluetoothServiceInfoBleak | ||
| ) -> FlowResult: | ||
| """Handle the bluetooth discovery step.""" | ||
| await self.async_set_unique_id(discovery_info.address) | ||
| self._abort_if_unique_id_configured() | ||
| self._discovery_info = discovery_info | ||
| self.context["title_placeholders"] = { | ||
| "name": human_readable_name( | ||
| None, discovery_info.name, discovery_info.address | ||
| ) | ||
| } | ||
| return await self.async_step_user() | ||
|
|
||
| async def async_step_user( | ||
| self, user_input: dict[str, Any] | None = None | ||
| ) -> FlowResult: | ||
| """Handle the user step to pick discovered device.""" | ||
| errors: dict[str, str] = {} | ||
|
|
||
| if user_input is not None: | ||
| address = user_input[CONF_ADDRESS] | ||
| discovery_info = self._discovered_devices[address] | ||
| local_name = discovery_info.name | ||
| await self.async_set_unique_id( | ||
| discovery_info.address, raise_on_progress=False | ||
| ) | ||
| self._abort_if_unique_id_configured() | ||
| ld2410_ble = LD2410BLE(discovery_info.device) | ||
| try: | ||
| await ld2410_ble.initialise() | ||
| except BLEAK_EXCEPTIONS: | ||
| errors["base"] = "cannot_connect" | ||
| except Exception: # pylint: disable=broad-except | ||
| _LOGGER.exception("Unexpected error") | ||
| errors["base"] = "unknown" | ||
| else: | ||
| await ld2410_ble.stop() | ||
| return self.async_create_entry( | ||
| title=local_name, | ||
| data={ | ||
| CONF_ADDRESS: discovery_info.address, | ||
| }, | ||
| ) | ||
|
|
||
| if discovery := self._discovery_info: | ||
| self._discovered_devices[discovery.address] = discovery | ||
| else: | ||
| current_addresses = self._async_current_ids() | ||
| for discovery in async_discovered_service_info(self.hass): | ||
| if ( | ||
| discovery.address in current_addresses | ||
| or discovery.address in self._discovered_devices | ||
| or not any( | ||
| discovery.name.startswith(local_name) | ||
| for local_name in LOCAL_NAMES | ||
| ) | ||
| ): | ||
| continue | ||
| self._discovered_devices[discovery.address] = discovery | ||
|
|
||
| if not self._discovered_devices: | ||
| return self.async_abort(reason="no_unconfigured_devices") | ||
|
|
||
| data_schema = vol.Schema( | ||
| { | ||
| vol.Required(CONF_ADDRESS): vol.In( | ||
| { | ||
| service_info.address: f"{service_info.name} ({service_info.address})" | ||
| for service_info in self._discovered_devices.values() | ||
| } | ||
| ), | ||
| } | ||
| ) | ||
| return self.async_show_form( | ||
| step_id="user", | ||
| data_schema=data_schema, | ||
| errors=errors, | ||
| ) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| """Constants for the LD2410 BLE integration.""" | ||
|
|
||
| DOMAIN = "ld2410_ble" | ||
|
|
||
| LOCAL_NAMES = {"HLK-LD2410B"} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| """Data coordinator for receiving LD2410B updates.""" | ||
|
|
||
| import logging | ||
|
|
||
| from ld2410_ble import LD2410BLE, LD2410BLEState | ||
|
|
||
| from homeassistant.core import HomeAssistant, callback | ||
| from homeassistant.helpers.update_coordinator import DataUpdateCoordinator | ||
|
|
||
| from .const import DOMAIN | ||
|
|
||
| _LOGGER = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| class LD2410BLECoordinator(DataUpdateCoordinator): | ||
| """Data coordinator for receiving LD2410B updates.""" | ||
|
|
||
| def __init__(self, hass: HomeAssistant, ld2410_ble: LD2410BLE) -> None: | ||
| """Initialise the coordinator.""" | ||
| super().__init__( | ||
| hass, | ||
| _LOGGER, | ||
| name=DOMAIN, | ||
| ) | ||
| self._ld2410_ble = ld2410_ble | ||
| ld2410_ble.register_callback(self._async_handle_update) | ||
| ld2410_ble.register_disconnected_callback(self._async_handle_disconnect) | ||
| self.connected = False | ||
|
|
||
| @callback | ||
| def _async_handle_update(self, state: LD2410BLEState) -> None: | ||
| """Just trigger the callbacks.""" | ||
| self.connected = True | ||
| self.async_set_updated_data(True) | ||
|
|
||
| @callback | ||
| def _async_handle_disconnect(self) -> None: | ||
| """Trigger the callbacks for disconnected.""" | ||
| self.connected = False | ||
| self.async_update_listeners() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| { | ||
| "domain": "ld2410_ble", | ||
| "name": "LD2410 BLE", | ||
| "config_flow": true, | ||
| "documentation": "https://www.home-assistant.io/integrations/ld2410_ble/", | ||
| "requirements": ["bluetooth-data-tools==0.3.0", "ld2410-ble==0.1.1"], | ||
| "dependencies": ["bluetooth"], | ||
| "codeowners": ["@930913"], | ||
| "bluetooth": [{ "local_name": "HLK-LD2410B_*" }], | ||
|
bdraco marked this conversation as resolved.
|
||
| "integration_type": "device", | ||
| "iot_class": "local_push" | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| """The ld2410 ble integration models.""" | ||
| from __future__ import annotations | ||
|
|
||
| from dataclasses import dataclass | ||
|
|
||
| from ld2410_ble import LD2410BLE | ||
|
|
||
| from .coordinator import LD2410BLECoordinator | ||
|
|
||
|
|
||
| @dataclass | ||
| class LD2410BLEData: | ||
| """Data for the ld2410 ble integration.""" | ||
|
|
||
| title: str | ||
| device: LD2410BLE | ||
| coordinator: LD2410BLECoordinator |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.