Skip to content
Open
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
11 changes: 9 additions & 2 deletions homeassistant/components/airthings_ble/config_flow.py
Copy link
Member

Choose a reason for hiding this comment

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

I would assume these changes also have an effect to the test_config_flow.py

Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,12 @@ async def async_step_bluetooth_confirm(
):
return self.async_abort(reason="firmware_upgrade_required")

if self._discovered_device is None:
return self.async_abort(reason="no_devices_found")

return self.async_create_entry(
title=self.context["title_placeholders"]["name"], data={}
title=self.context["title_placeholders"]["name"],
data={"device_model": self._discovered_device.device.model.value},
)

self._set_confirm_only()
Expand Down Expand Up @@ -164,7 +168,10 @@ async def async_step_user(

self._discovered_device = discovery

return self.async_create_entry(title=discovery.name, data={})
return self.async_create_entry(
title=discovery.name,
data={"device_model": discovery.device.model.value},
)

current_addresses = self._async_current_ids(include_ignore=False)
devices: list[BluetoothServiceInfoBleak] = []
Expand Down
3 changes: 3 additions & 0 deletions homeassistant/components/airthings_ble/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
VOLUME_BECQUEREL = "Bq/m³"
VOLUME_PICOCURIE = "pCi/L"

DEVICE_MODEL = "device_model"

DEFAULT_SCAN_INTERVAL = 300
RADON_SCAN_INTERVAL = 1800

MAX_RETRIES_AFTER_STARTUP = 5
34 changes: 30 additions & 4 deletions homeassistant/components/airthings_ble/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@
from datetime import timedelta
import logging

from airthings_ble import AirthingsBluetoothDeviceData, AirthingsDevice
from airthings_ble import (
AirthingsBluetoothDeviceData,
AirthingsDevice,
AirthingsDeviceType,
)
from bleak.backends.device import BLEDevice
from bleak_retry_connector import close_stale_connections_by_address

Expand All @@ -16,7 +20,7 @@
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.util.unit_system import METRIC_SYSTEM

from .const import DEFAULT_SCAN_INTERVAL, DOMAIN
from .const import DEFAULT_SCAN_INTERVAL, DEVICE_MODEL, DOMAIN, RADON_SCAN_INTERVAL

_LOGGER = logging.getLogger(__name__)

Expand All @@ -34,12 +38,19 @@ def __init__(self, hass: HomeAssistant, entry: AirthingsBLEConfigEntry) -> None:
self.airthings = AirthingsBluetoothDeviceData(
_LOGGER, hass.config.units is METRIC_SYSTEM
)

device_model = entry.data.get(DEVICE_MODEL)
if device_model == AirthingsDeviceType.CORENTIUM_HOME_2.value:
interval = RADON_SCAN_INTERVAL
else:
interval = DEFAULT_SCAN_INTERVAL

super().__init__(
hass,
_LOGGER,
config_entry=entry,
name=DOMAIN,
update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL),
update_interval=timedelta(seconds=interval),
)

async def _async_setup(self) -> None:
Expand All @@ -58,11 +69,26 @@ async def _async_setup(self) -> None:
)
self.ble_device = ble_device

if DEVICE_MODEL not in self.config_entry.data:
_LOGGER.debug("Fetching device info for migration")
try:
data = await self.airthings.update_device(self.ble_device)
except Exception as err:
Copy link
Member

Choose a reason for hiding this comment

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

We should be more specific here, ideally we should also update the one in async_update_data but that is outside of the scope

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Noted. Will try to handle this (+ the other one) in a separate PR.

raise UpdateFailed(
f"Unable to fetch data for migration: {err}"
) from err

self.hass.config_entries.async_update_entry(
self.config_entry,
data={**self.config_entry.data, DEVICE_MODEL: data.model.value},
)
if data.model == AirthingsDeviceType.CORENTIUM_HOME_2:
self.update_interval = timedelta(seconds=RADON_SCAN_INTERVAL)

async def _async_update_data(self) -> AirthingsDevice:
"""Get data from Airthings BLE."""
try:
data = await self.airthings.update_device(self.ble_device)
except Exception as err:
raise UpdateFailed(f"Unable to fetch data: {err}") from err

return data
39 changes: 39 additions & 0 deletions tests/components/airthings_ble/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,27 @@ def patch_airthings_device_update():
tx_power=0,
)

CORENTIUM_HOME_2_SERVICE_INFO = BluetoothServiceInfoBleak(
name="cc-cc-cc-cc-cc-cc",
address="cc:cc:cc:cc:cc:cc",
device=generate_ble_device(
address="cc:cc:cc:cc:cc:cc",
name="Airthings Corentium Home 2",
),
rssi=-61,
manufacturer_data={820: b"\xe4/\xa5\xae\t\x00"},
service_data={},
service_uuids=[],
source="local",
advertisement=generate_advertisement_data(
manufacturer_data={820: b"\xe4/\xa5\xae\t\x00"},
service_uuids=[],
),
connectable=True,
time=0,
tx_power=0,
)

VIEW_PLUS_SERVICE_INFO = BluetoothServiceInfoBleak(
name="cc-cc-cc-cc-cc-cc",
address="cc:cc:cc:cc:cc:cc",
Expand Down Expand Up @@ -265,6 +286,24 @@ def patch_airthings_device_update():
address="cc:cc:cc:cc:cc:cc",
)

CORENTIUM_HOME_2_DEVICE_INFO = AirthingsDevice(
manufacturer="Airthings AS",
hw_version="REV X",
sw_version="R-SUB-1.3.4-master+0",
model=AirthingsDeviceType.CORENTIUM_HOME_2,
name="Airthings Corentium Home 2",
identifier="123456",
sensors={
"connectivity_mode": "Bluetooth",
"battery": 90,
"temperature": 20.0,
"humidity": 55.0,
"radon_1day_avg": 45,
"radon_1day_level": "low",
},
address="cc:cc:cc:cc:cc:cc",
)

TEMPERATURE_V1 = MockEntity(
unique_id="Airthings Wave Plus 123456_temperature",
name="Airthings Wave Plus 123456 Temperature",
Expand Down
6 changes: 6 additions & 0 deletions tests/components/airthings_ble/test_config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ async def test_bluetooth_discovery(hass: HomeAssistant) -> None:
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == "Airthings Wave Plus (2930123456)"
assert result["result"].unique_id == "cc:cc:cc:cc:cc:cc"
assert result["data"] == {"device_model": "2930"}
assert result["result"].data == {"device_model": "2930"}


async def test_bluetooth_discovery_no_BLEDevice(hass: HomeAssistant) -> None:
Expand Down Expand Up @@ -158,6 +160,8 @@ async def test_user_setup(hass: HomeAssistant) -> None:
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == "Airthings Wave Plus (2930123456)"
assert result["result"].unique_id == "cc:cc:cc:cc:cc:cc"
assert result["data"] == {"device_model": "2930"}
assert result["result"].data == {"device_model": "2930"}


async def test_user_setup_replaces_ignored_device(hass: HomeAssistant) -> None:
Expand Down Expand Up @@ -208,6 +212,8 @@ async def test_user_setup_replaces_ignored_device(hass: HomeAssistant) -> None:
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == "Airthings Wave Plus (2930123456)"
assert result["result"].unique_id == "cc:cc:cc:cc:cc:cc"
assert result["data"] == {"device_model": "2930"}
assert result["result"].data == {"device_model": "2930"}


async def test_user_setup_no_device(hass: HomeAssistant) -> None:
Expand Down
147 changes: 147 additions & 0 deletions tests/components/airthings_ble/test_init.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
"""Test the Airthings BLE integration init."""

from datetime import timedelta

import pytest

from homeassistant.components.airthings_ble.const import (
DEFAULT_SCAN_INTERVAL,
DOMAIN,
RADON_SCAN_INTERVAL,
)
from homeassistant.core import HomeAssistant

from . import (
CORENTIUM_HOME_2_DEVICE_INFO,
CORENTIUM_HOME_2_SERVICE_INFO,
WAVE_DEVICE_INFO,
WAVE_ENHANCE_DEVICE_INFO,
WAVE_ENHANCE_SERVICE_INFO,
WAVE_SERVICE_INFO,
patch_airthings_ble,
patch_async_ble_device_from_address,
)

from tests.common import MockConfigEntry
from tests.components.bluetooth import inject_bluetooth_service_info


@pytest.mark.parametrize(
("service_info", "device_info"),
[
(WAVE_SERVICE_INFO, WAVE_DEVICE_INFO),
(WAVE_ENHANCE_SERVICE_INFO, WAVE_ENHANCE_DEVICE_INFO),
(CORENTIUM_HOME_2_SERVICE_INFO, CORENTIUM_HOME_2_DEVICE_INFO),
],
)
async def test_migration_existing_entries(
hass: HomeAssistant,
service_info,
device_info,
) -> None:
"""Test migration of existing config entry without device model."""
entry = MockConfigEntry(
domain=DOMAIN,
unique_id=service_info.address,
data={},
)
entry.add_to_hass(hass)

inject_bluetooth_service_info(hass, service_info)

assert "device_model" not in entry.data

with (
patch_async_ble_device_from_address(service_info.device),
patch_airthings_ble(device_info),
):
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()

# Migration should have added device_model to entry data
assert "device_model" in entry.data
assert entry.data["device_model"] == device_info.model.value


async def test_no_migration_when_device_model_exists(
hass: HomeAssistant,
) -> None:
"""Test that migration does not run when device_model already exists."""
entry = MockConfigEntry(
domain=DOMAIN,
unique_id=WAVE_SERVICE_INFO.address,
data={"device_model": WAVE_DEVICE_INFO.model.value},
)
entry.add_to_hass(hass)

inject_bluetooth_service_info(hass, WAVE_SERVICE_INFO)

with (
patch_async_ble_device_from_address(WAVE_SERVICE_INFO.device),
patch_airthings_ble(WAVE_DEVICE_INFO) as mock_update,
):
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()

# Should have only 1 call for initial refresh (no migration call)
assert mock_update.call_count == 1
assert entry.data["device_model"] == WAVE_DEVICE_INFO.model.value


async def test_scan_interval_corentium_home_2(
hass: HomeAssistant,
) -> None:
"""Test that coordinator uses radon scan interval for Corentium Home 2."""
entry = MockConfigEntry(
domain=DOMAIN,
unique_id=WAVE_SERVICE_INFO.address,
data={"device_model": CORENTIUM_HOME_2_DEVICE_INFO.model.value},
)
entry.add_to_hass(hass)

inject_bluetooth_service_info(hass, WAVE_SERVICE_INFO)

with (
patch_async_ble_device_from_address(WAVE_SERVICE_INFO.device),
patch_airthings_ble(CORENTIUM_HOME_2_DEVICE_INFO),
):
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()

# Coordinator should have radon scan interval
coordinator = entry.runtime_data
assert coordinator.update_interval == timedelta(seconds=RADON_SCAN_INTERVAL)


@pytest.mark.parametrize(
("service_info", "device_info"),
[
(WAVE_SERVICE_INFO, WAVE_DEVICE_INFO),
(WAVE_ENHANCE_SERVICE_INFO, WAVE_ENHANCE_DEVICE_INFO),
],
)
async def test_coordinator_default_scan_interval(
hass: HomeAssistant,
service_info,
device_info,
) -> None:
"""Test that coordinator uses default scan interval."""
entry = MockConfigEntry(
domain=DOMAIN,
unique_id=service_info.address,
data={"device_model": device_info.model.value},
)
entry.add_to_hass(hass)

inject_bluetooth_service_info(hass, service_info)

with (
patch_async_ble_device_from_address(service_info.device),
patch_airthings_ble(device_info),
):
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()

# Coordinator should have default scan interval
coordinator = entry.runtime_data
assert coordinator.update_interval == timedelta(seconds=DEFAULT_SCAN_INTERVAL)
Loading
Loading