Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -670,6 +670,9 @@ omit =
homeassistant/components/lcn/helpers.py
homeassistant/components/lcn/scene.py
homeassistant/components/lcn/services.py
homeassistant/components/ld2410_ble/__init__.py
homeassistant/components/ld2410_ble/binary_sensor.py
homeassistant/components/ld2410_ble/coordinator.py
homeassistant/components/led_ble/__init__.py
homeassistant/components/led_ble/light.py
homeassistant/components/lg_netcast/media_player.py
Expand Down
1 change: 1 addition & 0 deletions .strict-typing
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ homeassistant.components.lacrosse_view.*
homeassistant.components.lametric.*
homeassistant.components.laundrify.*
homeassistant.components.lcn.*
homeassistant.components.ld2410_ble.*
homeassistant.components.lidarr.*
homeassistant.components.lifx.*
homeassistant.components.light.*
Expand Down
2 changes: 2 additions & 0 deletions CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -625,6 +625,8 @@ build.json @home-assistant/supervisor
/tests/components/laundrify/ @xLarry
/homeassistant/components/lcn/ @alengwenus
/tests/components/lcn/ @alengwenus
/homeassistant/components/ld2410_ble/ @930913
/tests/components/ld2410_ble/ @930913
/homeassistant/components/led_ble/ @bdraco
/tests/components/led_ble/ @bdraco
/homeassistant/components/lg_netcast/ @Drafteed
Expand Down
94 changes: 94 additions & 0 deletions homeassistant/components/ld2410_ble/__init__.py
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
81 changes: 81 additions & 0 deletions homeassistant/components/ld2410_ble/binary_sensor.py
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

Comment thread
bdraco marked this conversation as resolved.
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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MINOR: This could be a tuple since it never changes

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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)
Comment thread
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)
Comment thread
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
112 changes: 112 additions & 0 deletions homeassistant/components/ld2410_ble/config_flow.py
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,
)
5 changes: 5 additions & 0 deletions homeassistant/components/ld2410_ble/const.py
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"}
40 changes: 40 additions & 0 deletions homeassistant/components/ld2410_ble/coordinator.py
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()
12 changes: 12 additions & 0 deletions homeassistant/components/ld2410_ble/manifest.json
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_*" }],
Comment thread
bdraco marked this conversation as resolved.
"integration_type": "device",
"iot_class": "local_push"
}
17 changes: 17 additions & 0 deletions homeassistant/components/ld2410_ble/models.py
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
Loading