8000 Remove old Airthings devices by LaStrada · Pull Request #145914 · home-assistant/core · GitHub
[go: up one dir, main page]

Skip to content

Remove old Airthings devices #145914

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

Draft
wants to merge 9 commits into
base: dev
Choose a base branch
from
Draft
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
31 changes: 29 additions & 2 deletions homeassistant/components/airthings/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@
from datetime import timedelta
import logging

from airthings import Airthings
from airthings import Airthings, AirthingsDevice

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ID, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.aiohttp_client import async_get_clientsession

from .const import CONF_SECRET
from .const import CONF_SECRET, DOMAIN
from .coordinator import AirthingsDataUpdateCoordinator

_LOGGER = logging.getLogger(__name__)
Expand All @@ -39,9 +40,35 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirthingsConfigEntry) ->

await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)

_remove_old_devices(hass, entry, coordinator.data)

return True


async def async_unload_entry(hass: HomeAssistant, entry: AirthingsConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)


def _remove_old_devices(
hass: HomeAssistant,
entry: AirthingsConfigEntry,
airthings_devices: dict[str, AirthingsDevice],
) -> None:
device_registry = dr.async_get(hass)

for registered_device in device_registry.devices.get_devices_for_config_entry_id(
entry.entry_id
):
device_id = next(
(i[1] for i in registered_device.identifiers if i[0] == DOMAIN), None
)
if device_id and device_id not in airthings_devices:
_LOGGER.info(
"Removing device %s with ID %s because it no longer exists in your account",
registered_device.name,
device_id,
)
device_registry.async_update_device(
registered_device.id, remove_config_entry_id=entry.entry_id
)
35 changes: 35 additions & 0 deletions tests/components/airthings/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,36 @@
"""Tests for the Airthings integration."""

from airthings import Airthings, AirthingsDevice

from homeassistant.core import HomeAssistant

from .const import TEST_DATA

from tests.common import MockConfigEntry


async def setup_integration(hass: HomeAssistant) -> MockConfigEntry:
"""Set up the Airthings integration in Home Assistant."""
entry = MockConfigEntry(
domain="airthings",
data=TEST_DATA,
)
entry.add_to_hass(hass)

await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()

return entry


class MockAirthings(Airthings):
"""Mock Airthings class to simulate device data."""

def __init__(self, devices) -> None:
"""Initialize with a dictionary of devices."""
super().__init__(client_id="", secret="", websession=None)
self.devices = devices

async def update_devices(self) -> dict[str, AirthingsDevice]:
"""Mock method to return devices."""
return self.devices
Comment on lines +26 to +36
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 recommend to not create an extension like this at all, rather just use a

@pytest.fixture
def mock_overseerr_client() -> Generator[AsyncMock]:
    """Mock an Overseerr client."""
    with (
        patch(
            "homeassistant.components.overseerr.coordinator.OverseerrClient",
            autospec=True,
        ) as mock_client,
        patch(
            "homeassistant.components.overseerr.config_flow.OverseerrClient",
            new=mock_client,
        ),
    ):
        client = mock_client.return_value
        client.get_request_count.return_value = RequestCount.from_json(
            load_fixture("request_count.json", DOMAIN)
        )
        yield client

This way is way more versaitle and easier to update

84 changes: 84 additions & 0 deletions tests/components/airthings/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
"""Constants for Airthings integration tests."""

from homeassistant.components.airthings import AirthingsDevice
from homeassistant.components.airthings.const import CONF_SECRET
from homeassistant.const import CONF_ID

TEST_DATA = {
CONF_ID: "client_id",
CONF_SECRET: "secret",
}

WAVE_RADON: dict[str, AirthingsDevice] = {
"2950000001": AirthingsDevice(
device_id="2950000001",
name="Basement",
sensors={
"battery": 100,
"humidity": 75.0,
"radonShortTermAvg": 537.0,
"rssi": -76,
"temp": 16.6,
},
is_active=None,
location_name="Home",
device_type="WAVE_GEN2",
product_name="Wave",
)
}

WAVE_ENHANCE: dict[str, AirthingsDevice] = {
"3210000001": AirthingsDevice(
device_id="3210000001",
name="Bedroom",
sensors={
"battery": 35,
"co2": 551.0,
"humidity": 43.0,
"lux": 1.0,
"pressure": 985.0,
"rssi": -67,
"sla": 34.0,
"temp": 21.9,
"voc": 158.0,
},
is_active=None,
location_name="Home",
device_type="WAVE_ENHANCE",
product_name="Wave Enhance",
),
}
Comment on lines +30 to +50
Copy link
Member

Choose a reason for hiding this comment

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

Not sure if you have that, but if there's an easy way you go from JSON to an AirthingsDevice you could put all this data in a JSON fixture and load it


VIEW_PLUS: dict[str, AirthingsDevice] = {
"2960000001": AirthingsDevice(
device_id="2960000001",
name="Office",
sensors={
"battery": 77,
"co2": 876.0,
"humidity": 42.0,
"pm1": 3.0,
"pm25": 3.0,
"pressure": 985.0,
"radonShortTermAvg": 15.0,
"rssi": 0,
"temp": 24.5,
"voc": 1842.0,
},
is_active=None,
location_name="Office",
device_type="VIEW_PLUS",
product_name="View Plus",
),
}

THREE_DEVICES: dict[str, AirthingsDevice] = {
**WAVE_RADON,
**WAVE_ENHANCE,
**VIEW_PLUS,
}

TWO_DEVICES: dict[str, AirthingsDevice] = {
**WAVE_RADON,
**VIEW_PLUS,
}
9 changes: 3 additions & 6 deletions tests/components/airthings/test_config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,15 @@
import pytest

from homeassistant import config_entries
from homeassistant.components.airthings.const import CONF_SECRET, DOMAIN
from homeassistant.components.airthings.const import DOMAIN
from homeassistant.const import CONF_ID
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo

from tests.common import MockConfigEntry
from .const import TEST_DATA

TEST_DATA = {
CONF_ID: "client_id",
CONF_SECRET: "secret",
}
from tests.common import MockConfigEntry

DHCP_SERVICE_INFO = [
DhcpServiceInfo(
Expand Down
127 changes: 127 additions & 0 deletions tests/components/airthings/test_remove_old_devices.py
Copy link
Member

Choose a reason for hiding this comment

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

move this to test_init.py

Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
"""Test Airthings devices, and ensure old devices are removed."""

from unittest.mock import patch

from airthings import AirthingsError

from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr

from . import MockAirthings, setup_integration
from .const import TEST_DATA, THREE_DEVICES, TWO_DEVICES


async def test_setup_integration(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
) -> None:
"""Test that the Airthings integration is set up correctly."""

with patch(
"homeassistant.components.airthings.Airthings",
return_value=MockAirthings(TWO_DEVICES),
):
entry = await setup_integration(hass)

assert entry is not None
assert entry.domain == "airthings"
assert entry.data == TEST_DATA
assert len(device_registry.devices) == len(TWO_DEVICES)


async def test_add_new_device(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
) -> None:
"""Test that a new Airthings device is added correctly."""

with patch(
"homeassistant.components.airthings.Airthings",
return_value=MockAirthings(TWO_DEVICES),
):
entry = await setup_integration(hass)

assert entry is not None
assert entry.domain == "airthings"
assert entry.data == TEST_DATA
assert len(device_registry.devices) == len(TWO_DEVICES)

# Add device
with patch(
"homeassistant.components.airthings.Airthings",
return_value=MockAirthings(THREE_DEVICES),
):
await hass.config_entries.async_reload(entry.entry_id)
await hass.async_block_till_done()

assert len(device_registry.devices) == len(THREE_DEVICES)


async def test_setup_integration_no_devices(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
) -> None:
"""Test that the Airthings integration handles no devices correctly."""

with patch(
"homeassistant.components.airthings.Airthings",
return_value=MockAirthings({}),
):
entry = await setup_integration(hass)

assert entry is not None
assert entry.domain == "airthings"
assert entry.data == TEST_DATA
assert len(device_registry.devices) == 0


async def test_remove_old_devices(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
) -> None:
"""Test that old devices are removed when new data is fetched."""

with patch(
"homeassistant.components.airthings.Airthings",
return_value=MockAirthings(THREE_DEVICES),
):
entry = await setup_integration(hass)

assert entry is not None
assert entry.domain == "airthings"
assert entry.data == TEST_DATA
assert len(device_registry.devices) == len(THREE_DEVICES)

with patch(
"homeassistant.components.airthings.Airthings",
return_value=MockAirthings(TWO_DEVICES),
):
await hass.config_entries.async_reload(entry.entry_id)
await hass.async_block_till_done()

assert len(device_registry.devices) == len(TWO_DEVICES)


async def test_failing_api_call(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
) -> None:
"""Test that the integration handles API call failures gracefully."""

with patch(
"homeassistant.components.airthings.Airthings",
return_value=MockAirthings(THREE_DEVICES),
):
entry = await setup_integration(hass)

assert len(device_registry.devices) == len(THREE_DEVICES)

# Simulate an API failure
with patch(
"homeassistant.components.airthings.Airthings.update_devices",
side_effect=AirthingsError("API call failed"),
):
await hass.config_entries.async_reload(entry.entry_id)
await hass.async_block_till_done()

assert len(device_registry.devices) == len(THREE_DEVICES)
0