Add support for Wave Enhance and Corentium Home 2 in Airthings BLE integration (#153780)

This commit is contained in:
Ståle Storø Hauknes
2025-10-06 14:59:21 +02:00
committed by GitHub
parent 645f32fd65
commit 9640ebb593
6 changed files with 107 additions and 18 deletions

View File

@@ -6,7 +6,11 @@ import dataclasses
import logging
from typing import Any
from airthings_ble import AirthingsBluetoothDeviceData, AirthingsDevice
from airthings_ble import (
AirthingsBluetoothDeviceData,
AirthingsDevice,
UnsupportedDeviceError,
)
from bleak import BleakError
from habluetooth import BluetoothServiceInfoBleak
import voluptuous as vol
@@ -28,6 +32,7 @@ SERVICE_UUIDS = [
"b42e4a8e-ade7-11e4-89d3-123b93f75cba",
"b42e1c08-ade7-11e4-89d3-123b93f75cba",
"b42e3882-ade7-11e4-89d3-123b93f75cba",
"b42e90a2-ade7-11e4-89d3-123b93f75cba",
]
@@ -38,6 +43,7 @@ class Discovery:
name: str
discovery_info: BluetoothServiceInfo
device: AirthingsDevice
data: AirthingsBluetoothDeviceData
def get_name(device: AirthingsDevice) -> str:
@@ -63,8 +69,8 @@ class AirthingsConfigFlow(ConfigFlow, domain=DOMAIN):
self._discovered_device: Discovery | None = None
self._discovered_devices: dict[str, Discovery] = {}
async def _get_device_data(
self, discovery_info: BluetoothServiceInfo
async def _get_device(
self, data: AirthingsBluetoothDeviceData, discovery_info: BluetoothServiceInfo
) -> AirthingsDevice:
ble_device = bluetooth.async_ble_device_from_address(
self.hass, discovery_info.address
@@ -73,10 +79,8 @@ class AirthingsConfigFlow(ConfigFlow, domain=DOMAIN):
_LOGGER.debug("no ble_device in _get_device_data")
raise AirthingsDeviceUpdateError("No ble_device")
airthings = AirthingsBluetoothDeviceData(_LOGGER)
try:
data = await airthings.update_device(ble_device)
device = await data.update_device(ble_device)
except BleakError as err:
_LOGGER.error(
"Error connecting to and getting data from %s: %s",
@@ -84,12 +88,15 @@ class AirthingsConfigFlow(ConfigFlow, domain=DOMAIN):
err,
)
raise AirthingsDeviceUpdateError("Failed getting device data") from err
except UnsupportedDeviceError:
_LOGGER.debug("Skipping unsupported device: %s", discovery_info.name)
raise
except Exception as err:
_LOGGER.error(
"Unknown error occurred from %s: %s", discovery_info.address, err
)
raise
return data
return device
async def async_step_bluetooth(
self, discovery_info: BluetoothServiceInfo
@@ -99,17 +106,21 @@ class AirthingsConfigFlow(ConfigFlow, domain=DOMAIN):
await self.async_set_unique_id(discovery_info.address)
self._abort_if_unique_id_configured()
data = AirthingsBluetoothDeviceData(logger=_LOGGER)
try:
device = await self._get_device_data(discovery_info)
device = await self._get_device(data=data, discovery_info=discovery_info)
except AirthingsDeviceUpdateError:
return self.async_abort(reason="cannot_connect")
except UnsupportedDeviceError:
return self.async_abort(reason="unsupported_device")
except Exception:
_LOGGER.exception("Unknown error occurred")
return self.async_abort(reason="unknown")
name = get_name(device)
self.context["title_placeholders"] = {"name": name}
self._discovered_device = Discovery(name, discovery_info, device)
self._discovered_device = Discovery(name, discovery_info, device, data=data)
return await self.async_step_bluetooth_confirm()
@@ -164,16 +175,28 @@ class AirthingsConfigFlow(ConfigFlow, domain=DOMAIN):
if MFCT_ID not in discovery_info.manufacturer_data:
continue
if not any(uuid in SERVICE_UUIDS for uuid in discovery_info.service_uuids):
_LOGGER.debug(
"Skipping unsupported device: %s (%s)", discovery_info.name, address
)
continue
devices.append(discovery_info)
for discovery_info in devices:
address = discovery_info.address
data = AirthingsBluetoothDeviceData(logger=_LOGGER)
try:
device = await self._get_device_data(discovery_info)
device = await self._get_device(data, discovery_info)
except AirthingsDeviceUpdateError:
_LOGGER.error(
"Error connecting to and getting data from %s",
"Error connecting to and getting data from %s (%s)",
discovery_info.name,
discovery_info.address,
)
continue
except UnsupportedDeviceError:
_LOGGER.debug(
"Skipping unsupported device: %s (%s)",
discovery_info.name,
discovery_info.address,
)
continue
@@ -181,7 +204,10 @@ class AirthingsConfigFlow(ConfigFlow, domain=DOMAIN):
_LOGGER.exception("Unknown error occurred")
return self.async_abort(reason="unknown")
name = get_name(device)
self._discovered_devices[address] = Discovery(name, discovery_info, device)
_LOGGER.debug("Discovered Airthings device: %s (%s)", name, address)
self._discovered_devices[address] = Discovery(
name, discovery_info, device, data
)
if not self._discovered_devices:
return self.async_abort(reason="no_devices_found")

View File

@@ -17,6 +17,10 @@
{
"manufacturer_id": 820,
"service_uuid": "b42e3882-ade7-11e4-89d3-123b93f75cba"
},
{
"manufacturer_id": 820,
"service_uuid": "b42e90a2-ade7-11e4-89d3-123b93f75cba"
}
],
"codeowners": ["@vincegio", "@LaStrada"],

View File

@@ -21,6 +21,7 @@
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"firmware_upgrade_required": "Your device requires a firmware upgrade. Please use the Airthings app (Android/iOS) to upgrade it.",
"unsupported_device": "Unsupported device",
"unknown": "[%key:common::config_flow::error::unknown%]"
}
},

View File

@@ -48,6 +48,11 @@ BLUETOOTH: Final[list[dict[str, bool | str | int | list[int]]]] = [
"manufacturer_id": 820,
"service_uuid": "b42e3882-ade7-11e4-89d3-123b93f75cba",
},
{
"domain": "airthings_ble",
"manufacturer_id": 820,
"service_uuid": "b42e90a2-ade7-11e4-89d3-123b93f75cba",
},
{
"connectable": False,
"domain": "aranet",

View File

@@ -146,6 +146,27 @@ VIEW_PLUS_SERVICE_INFO = BluetoothServiceInfoBleak(
tx_power=0,
)
UNKNOWN_AIRTHINGS_SERVICE_INFO = BluetoothServiceInfoBleak(
name="unknown",
address="00:cc:cc:cc:cc:cc",
rssi=-61,
manufacturer_data={820: b"\xe4/\xa5\xae\t\x00"},
service_data={},
service_uuids=[],
source="local",
device=generate_ble_device(
"cc:cc:cc:cc:cc:cc",
"unknown",
),
advertisement=generate_advertisement_data(
manufacturer_data={},
service_uuids=[],
),
connectable=True,
time=0,
tx_power=0,
)
UNKNOWN_SERVICE_INFO = BluetoothServiceInfoBleak(
name="unknown",
address="00:cc:cc:cc:cc:cc",

View File

@@ -2,8 +2,9 @@
from unittest.mock import patch
from airthings_ble import AirthingsDevice, AirthingsDeviceType
from airthings_ble import AirthingsDevice, AirthingsDeviceType, UnsupportedDeviceError
from bleak import BleakError
from home_assistant_bluetooth import BluetoothServiceInfoBleak
import pytest
from homeassistant.components.airthings_ble.const import DOMAIN
@@ -13,6 +14,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from . import (
UNKNOWN_AIRTHINGS_SERVICE_INFO,
UNKNOWN_SERVICE_INFO,
VIEW_PLUS_SERVICE_INFO,
WAVE_DEVICE_INFO,
@@ -73,7 +75,12 @@ async def test_bluetooth_discovery_no_BLEDevice(hass: HomeAssistant) -> None:
@pytest.mark.parametrize(
("exc", "reason"), [(Exception(), "unknown"), (BleakError(), "cannot_connect")]
("exc", "reason"),
[
(Exception(), "unknown"),
(BleakError(), "cannot_connect"),
(UnsupportedDeviceError(), "unsupported_device"),
],
)
async def test_bluetooth_discovery_airthings_ble_update_failed(
hass: HomeAssistant, exc: Exception, reason: str
@@ -234,22 +241,34 @@ async def test_user_setup_existing_and_unknown_device(hass: HomeAssistant) -> No
assert result["reason"] == "no_devices_found"
async def test_user_setup_unknown_error(hass: HomeAssistant) -> None:
@pytest.mark.parametrize(
("exc", "reason", "service_info"),
[
(Exception(), "unknown", WAVE_SERVICE_INFO),
(UnsupportedDeviceError(), "no_devices_found", UNKNOWN_AIRTHINGS_SERVICE_INFO),
],
)
async def test_user_setup_unknown_error(
hass: HomeAssistant,
exc: Exception,
reason: str,
service_info: BluetoothServiceInfoBleak,
) -> None:
"""Test the user initiated form with an unknown error."""
with (
patch(
"homeassistant.components.airthings_ble.config_flow.async_discovered_service_info",
return_value=[WAVE_SERVICE_INFO],
),
patch_async_ble_device_from_address(WAVE_SERVICE_INFO),
patch_airthings_ble(None, Exception()),
patch_async_ble_device_from_address(service_info),
patch_airthings_ble(None, exc),
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "unknown"
assert result["reason"] == reason
async def test_user_setup_unable_to_connect(hass: HomeAssistant) -> None:
@@ -350,3 +369,16 @@ async def test_step_user_firmware_required(hass: HomeAssistant) -> None:
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "firmware_upgrade_required"
async def test_discovering_unsupported_devices(hass: HomeAssistant) -> None:
"""Test discovering unsupported devices."""
with patch(
"homeassistant.components.airthings_ble.config_flow.async_discovered_service_info",
return_value=[UNKNOWN_AIRTHINGS_SERVICE_INFO, UNKNOWN_SERVICE_INFO],
):
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"