-
-
Notifications
You must be signed in to change notification settings - Fork 30.5k
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
Initial xiaomi_ble integration #75618
Merged
Merged
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
fc7cca5
Initial xiaomi_ble integration
Jc2k 3298e4b
black
Jc2k 0888830
Update homeassistant/components/xiaomi_ble/config_flow.py
Jc2k eea73c6
Update homeassistant/components/xiaomi_ble/config_flow.py
Jc2k e43d6fe
Apply suggestions from code review
Jc2k 00b72c7
Update tests/components/xiaomi_ble/test_config_flow.py
Jc2k d0a5776
Update homeassistant/components/xiaomi_ble/sensor.py
Jc2k 7e4b5d8
Update tests/components/xiaomi_ble/test_config_flow.py
Jc2k 00d5322
Remove debug code
Jc2k f11cb8d
Need 'proper' MAC when running tests on linux
Jc2k f0a3606
Need to use proper MAC so validation passes
Jc2k 71219e0
Add tests for already_in_progress and already_configured
Jc2k 4c65bd9
Merge branch 'dev' into xiaomi_ble
bdraco 7ca6c58
copy test, add session fixture
bdraco 3a5c6e1
fix test
bdraco 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
Validating CODEOWNERS rules …
This file contains 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 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,56 @@ | ||
"""The Xiaomi Bluetooth integration.""" | ||
from __future__ import annotations | ||
|
||
import logging | ||
|
||
from xiaomi_ble import XiaomiBluetoothDeviceData | ||
|
||
from homeassistant.components.bluetooth.passive_update_coordinator import ( | ||
PassiveBluetoothDataUpdate, | ||
PassiveBluetoothDataUpdateCoordinator, | ||
) | ||
from homeassistant.config_entries import ConfigEntry | ||
from homeassistant.const import Platform | ||
from homeassistant.core import HomeAssistant, callback | ||
from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo | ||
|
||
from .const import DOMAIN | ||
from .sensor import sensor_update_to_bluetooth_data_update | ||
|
||
PLATFORMS: list[Platform] = [Platform.SENSOR] | ||
|
||
_LOGGER = logging.getLogger(__name__) | ||
|
||
|
||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: | ||
"""Set up Xiaomi BLE device from a config entry.""" | ||
address = entry.unique_id | ||
assert address is not None | ||
|
||
data = XiaomiBluetoothDeviceData() | ||
|
||
@callback | ||
def _async_update_data( | ||
service_info: BluetoothServiceInfo, | ||
) -> PassiveBluetoothDataUpdate: | ||
"""Update data from Xiaomi Bluetooth.""" | ||
return sensor_update_to_bluetooth_data_update(data.update(service_info)) | ||
|
||
hass.data.setdefault(DOMAIN, {})[ | ||
entry.entry_id | ||
] = PassiveBluetoothDataUpdateCoordinator( | ||
hass, | ||
_LOGGER, | ||
update_method=_async_update_data, | ||
address=address, | ||
) | ||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) | ||
return True | ||
|
||
|
||
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): | ||
hass.data[DOMAIN].pop(entry.entry_id) | ||
|
||
return unload_ok |
This file contains 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,93 @@ | ||
"""Config flow for Xiaomi Bluetooth integration.""" | ||
from __future__ import annotations | ||
|
||
from typing import Any | ||
|
||
import voluptuous as vol | ||
from xiaomi_ble import XiaomiBluetoothDeviceData as DeviceData | ||
|
||
from homeassistant.components.bluetooth import ( | ||
BluetoothServiceInfo, | ||
async_discovered_service_info, | ||
) | ||
from homeassistant.config_entries import ConfigFlow | ||
from homeassistant.const import CONF_ADDRESS | ||
from homeassistant.data_entry_flow import FlowResult | ||
|
||
from .const import DOMAIN | ||
|
||
|
||
class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): | ||
"""Handle a config flow for Xiaomi Bluetooth.""" | ||
|
||
VERSION = 1 | ||
|
||
def __init__(self) -> None: | ||
"""Initialize the config flow.""" | ||
self._discovery_info: BluetoothServiceInfo | None = None | ||
self._discovered_device: DeviceData | None = None | ||
self._discovered_devices: dict[str, str] = {} | ||
|
||
async def async_step_bluetooth( | ||
self, discovery_info: BluetoothServiceInfo | ||
) -> FlowResult: | ||
"""Handle the bluetooth discovery step.""" | ||
await self.async_set_unique_id(discovery_info.address) | ||
self._abort_if_unique_id_configured() | ||
device = DeviceData() | ||
if not device.supported(discovery_info): | ||
return self.async_abort(reason="not_supported") | ||
self._discovery_info = discovery_info | ||
self._discovered_device = device | ||
return await self.async_step_bluetooth_confirm() | ||
|
||
async def async_step_bluetooth_confirm( | ||
self, user_input: dict[str, Any] | None = None | ||
) -> FlowResult: | ||
"""Confirm discovery.""" | ||
assert self._discovered_device is not None | ||
device = self._discovered_device | ||
assert self._discovery_info is not None | ||
discovery_info = self._discovery_info | ||
title = device.title or device.get_device_name() or discovery_info.name | ||
if user_input is not None: | ||
return self.async_create_entry(title=title, data={}) | ||
|
||
self._set_confirm_only() | ||
placeholders = {"name": title} | ||
self.context["title_placeholders"] = placeholders | ||
return self.async_show_form( | ||
step_id="bluetooth_confirm", description_placeholders=placeholders | ||
) | ||
|
||
async def async_step_user( | ||
self, user_input: dict[str, Any] | None = None | ||
) -> FlowResult: | ||
"""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) | ||
return self.async_create_entry( | ||
title=self._discovered_devices[address], data={} | ||
) | ||
|
||
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 | ||
device = DeviceData() | ||
if device.supported(discovery_info): | ||
self._discovered_devices[address] = ( | ||
device.title or device.get_device_name() or discovery_info.name | ||
) | ||
|
||
if not self._discovered_devices: | ||
return self.async_abort(reason="no_devices_found") | ||
|
||
return self.async_show_form( | ||
step_id="user", | ||
data_schema=vol.Schema( | ||
{vol.Required(CONF_ADDRESS): vol.In(self._discovered_devices)} | ||
), | ||
) |
This file contains 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,3 @@ | ||
"""Constants for the Xiaomi Bluetooth integration.""" | ||
|
||
DOMAIN = "xiaomi_ble" |
This file contains 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,15 @@ | ||
{ | ||
"domain": "xiaomi_ble", | ||
"name": "Xiaomi BLE", | ||
"config_flow": true, | ||
"documentation": "https://www.home-assistant.io/integrations/xiaomi_ble", | ||
"bluetooth": [ | ||
{ | ||
"service_uuid": "0000fe95-0000-1000-8000-00805f9b34fb" | ||
} | ||
], | ||
"requirements": ["xiaomi-ble==0.1.0"], | ||
"dependencies": ["bluetooth"], | ||
"codeowners": ["@Jc2k", "@Ernst79"], | ||
"iot_class": "local_push" | ||
} |
This file contains 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,149 @@ | ||
"""Support for xiaomi ble sensors.""" | ||
from __future__ import annotations | ||
|
||
from typing import Optional, Union | ||
|
||
from xiaomi_ble import DeviceClass, DeviceKey, SensorDeviceInfo, SensorUpdate, Units | ||
|
||
from homeassistant import config_entries | ||
from homeassistant.components.bluetooth.passive_update_coordinator import ( | ||
PassiveBluetoothCoordinatorEntity, | ||
PassiveBluetoothDataUpdate, | ||
PassiveBluetoothDataUpdateCoordinator, | ||
PassiveBluetoothEntityKey, | ||
) | ||
from homeassistant.components.sensor import ( | ||
SensorDeviceClass, | ||
SensorEntity, | ||
SensorEntityDescription, | ||
SensorStateClass, | ||
) | ||
from homeassistant.const import ( | ||
ATTR_MANUFACTURER, | ||
ATTR_MODEL, | ||
ATTR_NAME, | ||
PERCENTAGE, | ||
PRESSURE_MBAR, | ||
SIGNAL_STRENGTH_DECIBELS_MILLIWATT, | ||
TEMP_CELSIUS, | ||
) | ||
from homeassistant.core import HomeAssistant | ||
from homeassistant.helpers.entity import DeviceInfo | ||
from homeassistant.helpers.entity_platform import AddEntitiesCallback | ||
|
||
from .const import DOMAIN | ||
|
||
SENSOR_DESCRIPTIONS = { | ||
(DeviceClass.TEMPERATURE, Units.TEMP_CELSIUS): SensorEntityDescription( | ||
key=f"{DeviceClass.TEMPERATURE}_{Units.TEMP_CELSIUS}", | ||
device_class=SensorDeviceClass.TEMPERATURE, | ||
native_unit_of_measurement=TEMP_CELSIUS, | ||
state_class=SensorStateClass.MEASUREMENT, | ||
), | ||
(DeviceClass.HUMIDITY, Units.PERCENTAGE): SensorEntityDescription( | ||
key=f"{DeviceClass.HUMIDITY}_{Units.PERCENTAGE}", | ||
device_class=SensorDeviceClass.HUMIDITY, | ||
native_unit_of_measurement=PERCENTAGE, | ||
state_class=SensorStateClass.MEASUREMENT, | ||
), | ||
(DeviceClass.PRESSURE, Units.PRESSURE_MBAR): SensorEntityDescription( | ||
key=f"{DeviceClass.PRESSURE}_{Units.PRESSURE_MBAR}", | ||
device_class=SensorDeviceClass.PRESSURE, | ||
native_unit_of_measurement=PRESSURE_MBAR, | ||
state_class=SensorStateClass.MEASUREMENT, | ||
), | ||
(DeviceClass.BATTERY, Units.PERCENTAGE): SensorEntityDescription( | ||
key=f"{DeviceClass.BATTERY}_{Units.PERCENTAGE}", | ||
device_class=SensorDeviceClass.BATTERY, | ||
native_unit_of_measurement=PERCENTAGE, | ||
state_class=SensorStateClass.MEASUREMENT, | ||
), | ||
( | ||
DeviceClass.SIGNAL_STRENGTH, | ||
Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, | ||
): SensorEntityDescription( | ||
key=f"{DeviceClass.SIGNAL_STRENGTH}_{Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT}", | ||
device_class=SensorDeviceClass.SIGNAL_STRENGTH, | ||
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, | ||
state_class=SensorStateClass.MEASUREMENT, | ||
entity_registry_enabled_default=False, | ||
), | ||
} | ||
|
||
|
||
def _device_key_to_bluetooth_entity_key( | ||
device_key: DeviceKey, | ||
) -> PassiveBluetoothEntityKey: | ||
"""Convert a device key to an entity key.""" | ||
return PassiveBluetoothEntityKey(device_key.key, device_key.device_id) | ||
|
||
|
||
def _sensor_device_info_to_hass( | ||
sensor_device_info: SensorDeviceInfo, | ||
) -> DeviceInfo: | ||
"""Convert a sensor device info to a sensor device info.""" | ||
hass_device_info = DeviceInfo({}) | ||
if sensor_device_info.name is not None: | ||
hass_device_info[ATTR_NAME] = sensor_device_info.name | ||
if sensor_device_info.manufacturer is not None: | ||
hass_device_info[ATTR_MANUFACTURER] = sensor_device_info.manufacturer | ||
if sensor_device_info.model is not None: | ||
hass_device_info[ATTR_MODEL] = sensor_device_info.model | ||
return hass_device_info | ||
|
||
|
||
def sensor_update_to_bluetooth_data_update( | ||
sensor_update: SensorUpdate, | ||
) -> PassiveBluetoothDataUpdate: | ||
"""Convert a sensor update to a bluetooth data update.""" | ||
return PassiveBluetoothDataUpdate( | ||
devices={ | ||
device_id: _sensor_device_info_to_hass(device_info) | ||
for device_id, device_info in sensor_update.devices.items() | ||
}, | ||
entity_descriptions={ | ||
_device_key_to_bluetooth_entity_key(device_key): SENSOR_DESCRIPTIONS[ | ||
(description.device_class, description.native_unit_of_measurement) | ||
] | ||
for device_key, description in sensor_update.entity_descriptions.items() | ||
if description.device_class and description.native_unit_of_measurement | ||
}, | ||
entity_data={ | ||
_device_key_to_bluetooth_entity_key(device_key): sensor_values.native_value | ||
for device_key, sensor_values in sensor_update.entity_values.items() | ||
}, | ||
entity_names={ | ||
_device_key_to_bluetooth_entity_key(device_key): sensor_values.name | ||
for device_key, sensor_values in sensor_update.entity_values.items() | ||
}, | ||
) | ||
|
||
|
||
async def async_setup_entry( | ||
hass: HomeAssistant, | ||
entry: config_entries.ConfigEntry, | ||
async_add_entities: AddEntitiesCallback, | ||
) -> None: | ||
"""Set up the Xiaomi BLE sensors.""" | ||
coordinator: PassiveBluetoothDataUpdateCoordinator = hass.data[DOMAIN][ | ||
entry.entry_id | ||
] | ||
entry.async_on_unload( | ||
coordinator.async_add_entities_listener( | ||
XiaomiBluetoothSensorEntity, async_add_entities | ||
) | ||
) | ||
|
||
|
||
class XiaomiBluetoothSensorEntity( | ||
PassiveBluetoothCoordinatorEntity[ | ||
PassiveBluetoothDataUpdateCoordinator[Optional[Union[float, int]]] | ||
], | ||
SensorEntity, | ||
): | ||
"""Representation of a xiaomi ble sensor.""" | ||
|
||
@property | ||
def native_value(self) -> int | float | None: | ||
"""Return the native value.""" | ||
return self.coordinator.entity_data.get(self.entity_key) |
This file contains 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,21 @@ | ||
{ | ||
"config": { | ||
"flow_title": "[%key:component::bluetooth::config::flow_title%]", | ||
"step": { | ||
"user": { | ||
"description": "[%key:component::bluetooth::config::step::user::description%]", | ||
"data": { | ||
"address": "[%key:component::bluetooth::config::step::user::data::address%]" | ||
} | ||
}, | ||
"bluetooth_confirm": { | ||
"description": "[%key:component::bluetooth::config::step::bluetooth_confirm::description%]" | ||
} | ||
}, | ||
"abort": { | ||
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", | ||
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", | ||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]" | ||
} | ||
} | ||
} |
This file contains 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,21 @@ | ||
{ | ||
"config": { | ||
"abort": { | ||
"already_configured": "Device is already configured", | ||
"already_in_progress": "Configuration flow is already in progress", | ||
"no_devices_found": "No devices found on the network" | ||
}, | ||
"flow_title": "{name}", | ||
"step": { | ||
"bluetooth_confirm": { | ||
"description": "Do you want to setup {name}?" | ||
}, | ||
"user": { | ||
"data": { | ||
"address": "Device" | ||
}, | ||
"description": "Choose a device to setup" | ||
} | ||
} | ||
} | ||
} |
This file contains 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 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
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I assume the integration now picks up all sensors that send temperature, humidity, pressure and battery. This might add all sensors, as almost all of them have battery messages. Does this mean that these sensors are added with only a battery entity? Not a problem, I guess, but we can also decide to comment out the sensors that aren't fully supported yet in the pypi package.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pressure isnt used yet, but yes. I've left all the parsers active and able to capture fields that aren't turned into entities so we can do a second pass and capture them.
The can ignore any devices they don't want to add in the UI, so i'd slightly prefer to leave as is I think.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I’m just a bit afraid of all the issues that are going to be created, when people notice that their device only shows a battery entity. There is also one device that has added a counter in the battery data. Like every tenth advertisement, it will send a counter (as battery data) that is increased with one. I had some filter in BLE monitor for that (not in the parser, but in sensor.py. It will look it up, which one it was…)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Eh, I'm less worried about that. It's been that way with homekit_controller for a few years.
We can add some boilerplate to the docs about what device classes are support and/or a whitelist of devices we expect to work.
(I actually have a dashboard just for batteries, so I'm totally up for battery only sensors).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The CGPR1 is the device with the odd battery sensor. I made a workaround in BLE monitor by storing the last 5 readings in a list, and only process the battery reading if it is in the list (only for the CGPR1, the others work fine without the workaround)
https://github.com/custom-components/ble_monitor/blob/de09d4396f2d9ce8f4d44c10c6ef90ebf6483c90/custom_components/ble_monitor/sensor.py#L259
The counter is actually counting downwards, this is what you get without the workaround.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've moved this into Bluetooth-Devices/xiaomi-ble#3 and will handle it seperately to this PR.