mirror of
https://github.com/home-assistant/core.git
synced 2025-07-10 06:47:09 +00:00
Add air quality sensor for Tradfri air purifier (#65070)
* Add air quality sensor for Tradfri fan platform * Refactor, use entity description * Fix typo * CHange init docstring * Let lambda handle special case * Remove unique id * Apply suggestions from code review Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Refactor to constants, add mixin * Rename lambda * Update homeassistant/components/tradfri/sensor.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update homeassistant/components/tradfri/sensor.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update homeassistant/components/tradfri/sensor.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Replace lambda with function * Refactor device init * Remove fixture scope Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
parent
be5ff87171
commit
3ca1b2fc6e
@ -2,13 +2,19 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from typing import Any
|
from dataclasses import dataclass
|
||||||
|
from typing import Any, cast
|
||||||
|
|
||||||
from pytradfri.command import Command
|
from pytradfri.command import Command
|
||||||
|
from pytradfri.device import Device
|
||||||
|
|
||||||
from homeassistant.components.sensor import SensorDeviceClass, SensorEntity
|
from homeassistant.components.sensor import (
|
||||||
|
SensorDeviceClass,
|
||||||
|
SensorEntity,
|
||||||
|
SensorEntityDescription,
|
||||||
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import PERCENTAGE
|
from homeassistant.const import CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, PERCENTAGE
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
@ -17,6 +23,46 @@ from .const import CONF_GATEWAY_ID, COORDINATOR, COORDINATOR_LIST, DOMAIN, KEY_A
|
|||||||
from .coordinator import TradfriDeviceDataUpdateCoordinator
|
from .coordinator import TradfriDeviceDataUpdateCoordinator
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TradfriSensorEntityDescriptionMixin:
|
||||||
|
"""Mixin for required keys."""
|
||||||
|
|
||||||
|
value: Callable[[Device], Any | None]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TradfriSensorEntityDescription(
|
||||||
|
SensorEntityDescription,
|
||||||
|
TradfriSensorEntityDescriptionMixin,
|
||||||
|
):
|
||||||
|
"""Class describing Tradfri sensor entities."""
|
||||||
|
|
||||||
|
|
||||||
|
def _get_air_quality(device: Device) -> int | None:
|
||||||
|
"""Fetch the air quality value."""
|
||||||
|
if (
|
||||||
|
device.air_purifier_control.air_purifiers[0].air_quality == 65535
|
||||||
|
): # The sensor returns 65535 if the fan is turned off
|
||||||
|
return None
|
||||||
|
|
||||||
|
return cast(int, device.air_purifier_control.air_purifiers[0].air_quality)
|
||||||
|
|
||||||
|
|
||||||
|
SENSOR_DESCRIPTION_AQI = TradfriSensorEntityDescription(
|
||||||
|
device_class=SensorDeviceClass.AQI,
|
||||||
|
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||||
|
key=SensorDeviceClass.AQI,
|
||||||
|
value=_get_air_quality,
|
||||||
|
)
|
||||||
|
|
||||||
|
SENSOR_DESCRIPTION_BATTERY = TradfriSensorEntityDescription(
|
||||||
|
device_class=SensorDeviceClass.BATTERY,
|
||||||
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
|
key=SensorDeviceClass.BATTERY,
|
||||||
|
value=lambda device: cast(int, device.device_info.battery_level),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config_entry: ConfigEntry,
|
config_entry: ConfigEntry,
|
||||||
@ -27,43 +73,56 @@ async def async_setup_entry(
|
|||||||
coordinator_data = hass.data[DOMAIN][config_entry.entry_id][COORDINATOR]
|
coordinator_data = hass.data[DOMAIN][config_entry.entry_id][COORDINATOR]
|
||||||
api = coordinator_data[KEY_API]
|
api = coordinator_data[KEY_API]
|
||||||
|
|
||||||
async_add_entities(
|
entities: list[TradfriSensor] = []
|
||||||
TradfriBatterySensor(
|
|
||||||
device_coordinator,
|
for device_coordinator in coordinator_data[COORDINATOR_LIST]:
|
||||||
api,
|
description = None
|
||||||
gateway_id,
|
|
||||||
)
|
|
||||||
for device_coordinator in coordinator_data[COORDINATOR_LIST]
|
|
||||||
if (
|
if (
|
||||||
not device_coordinator.device.has_light_control
|
not device_coordinator.device.has_light_control
|
||||||
and not device_coordinator.device.has_socket_control
|
and not device_coordinator.device.has_socket_control
|
||||||
and not device_coordinator.device.has_signal_repeater_control
|
and not device_coordinator.device.has_signal_repeater_control
|
||||||
and not device_coordinator.device.has_air_purifier_control
|
and not device_coordinator.device.has_air_purifier_control
|
||||||
)
|
):
|
||||||
)
|
description = SENSOR_DESCRIPTION_BATTERY
|
||||||
|
elif device_coordinator.device.has_air_purifier_control:
|
||||||
|
description = SENSOR_DESCRIPTION_AQI
|
||||||
|
|
||||||
|
if description:
|
||||||
|
entities.append(
|
||||||
|
TradfriSensor(
|
||||||
|
device_coordinator,
|
||||||
|
api,
|
||||||
|
gateway_id,
|
||||||
|
description=description,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
async_add_entities(entities)
|
||||||
|
|
||||||
|
|
||||||
class TradfriBatterySensor(TradfriBaseEntity, SensorEntity):
|
class TradfriSensor(TradfriBaseEntity, SensorEntity):
|
||||||
"""The platform class required by Home Assistant."""
|
"""The platform class required by Home Assistant."""
|
||||||
|
|
||||||
_attr_device_class = SensorDeviceClass.BATTERY
|
entity_description: TradfriSensorEntityDescription
|
||||||
_attr_native_unit_of_measurement = PERCENTAGE
|
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
device_coordinator: TradfriDeviceDataUpdateCoordinator,
|
device_coordinator: TradfriDeviceDataUpdateCoordinator,
|
||||||
api: Callable[[Command | list[Command]], Any],
|
api: Callable[[Command | list[Command]], Any],
|
||||||
gateway_id: str,
|
gateway_id: str,
|
||||||
|
description: TradfriSensorEntityDescription,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize a switch."""
|
"""Initialize a Tradfri sensor."""
|
||||||
super().__init__(
|
super().__init__(
|
||||||
device_coordinator=device_coordinator,
|
device_coordinator=device_coordinator,
|
||||||
api=api,
|
api=api,
|
||||||
gateway_id=gateway_id,
|
gateway_id=gateway_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self.entity_description = description
|
||||||
|
|
||||||
self._refresh() # Set initial state
|
self._refresh() # Set initial state
|
||||||
|
|
||||||
def _refresh(self) -> None:
|
def _refresh(self) -> None:
|
||||||
"""Refresh the device."""
|
"""Refresh the device."""
|
||||||
self._attr_native_value = self.coordinator.data.device_info.battery_level
|
self._attr_native_value = self.entity_description.value(self.coordinator.data)
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
"""Common tradfri test fixtures."""
|
"""Common tradfri test fixtures."""
|
||||||
from unittest.mock import Mock, patch
|
from unittest.mock import Mock, PropertyMock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@ -76,3 +76,20 @@ def mock_api_factory(mock_api):
|
|||||||
factory.init.return_value = factory.return_value
|
factory.init.return_value = factory.return_value
|
||||||
factory.return_value.request = mock_api
|
factory.return_value.request = mock_api
|
||||||
yield factory.return_value
|
yield factory.return_value
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def setup(request):
|
||||||
|
"""
|
||||||
|
Set up patches for pytradfri methods for the fan platform.
|
||||||
|
|
||||||
|
This is used in test_fan as well as in test_sensor.
|
||||||
|
"""
|
||||||
|
with patch(
|
||||||
|
"pytradfri.device.AirPurifierControl.raw",
|
||||||
|
new_callable=PropertyMock,
|
||||||
|
return_value=[{"mock": "mock"}],
|
||||||
|
), patch(
|
||||||
|
"pytradfri.device.AirPurifierControl.air_purifiers",
|
||||||
|
):
|
||||||
|
yield
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
"""Tradfri fan (recognised as air purifiers in the IKEA ecosystem) platform tests."""
|
"""Tradfri fan (recognised as air purifiers in the IKEA ecosystem) platform tests."""
|
||||||
|
|
||||||
from unittest.mock import MagicMock, Mock, PropertyMock, patch
|
from unittest.mock import MagicMock, Mock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from pytradfri.device import Device
|
from pytradfri.device import Device
|
||||||
@ -10,19 +10,6 @@ from pytradfri.device.air_purifier_control import AirPurifierControl
|
|||||||
from .common import setup_integration
|
from .common import setup_integration
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True, scope="module")
|
|
||||||
def setup(request):
|
|
||||||
"""Set up patches for pytradfri methods."""
|
|
||||||
with patch(
|
|
||||||
"pytradfri.device.AirPurifierControl.raw",
|
|
||||||
new_callable=PropertyMock,
|
|
||||||
return_value=[{"mock": "mock"}],
|
|
||||||
), patch(
|
|
||||||
"pytradfri.device.AirPurifierControl.air_purifiers",
|
|
||||||
):
|
|
||||||
yield
|
|
||||||
|
|
||||||
|
|
||||||
def mock_fan(test_features=None, test_state=None, device_number=0):
|
def mock_fan(test_features=None, test_state=None, device_number=0):
|
||||||
"""Mock a tradfri fan/air purifier."""
|
"""Mock a tradfri fan/air purifier."""
|
||||||
if test_features is None:
|
if test_features is None:
|
||||||
@ -57,9 +44,7 @@ def mock_fan(test_features=None, test_state=None, device_number=0):
|
|||||||
|
|
||||||
async def test_fan(hass, mock_gateway, mock_api_factory):
|
async def test_fan(hass, mock_gateway, mock_api_factory):
|
||||||
"""Test that fans are correctly added."""
|
"""Test that fans are correctly added."""
|
||||||
state = {
|
state = {"fan_speed": 10, "air_quality": 12}
|
||||||
"fan_speed": 10,
|
|
||||||
}
|
|
||||||
|
|
||||||
mock_gateway.mock_devices.append(mock_fan(test_state=state))
|
mock_gateway.mock_devices.append(mock_fan(test_state=state))
|
||||||
await setup_integration(hass)
|
await setup_integration(hass)
|
||||||
@ -74,9 +59,7 @@ async def test_fan(hass, mock_gateway, mock_api_factory):
|
|||||||
|
|
||||||
async def test_fan_observed(hass, mock_gateway, mock_api_factory):
|
async def test_fan_observed(hass, mock_gateway, mock_api_factory):
|
||||||
"""Test that fans are correctly observed."""
|
"""Test that fans are correctly observed."""
|
||||||
state = {
|
state = {"fan_speed": 10, "air_quality": 12}
|
||||||
"fan_speed": 10,
|
|
||||||
}
|
|
||||||
|
|
||||||
fan = mock_fan(test_state=state)
|
fan = mock_fan(test_state=state)
|
||||||
mock_gateway.mock_devices.append(fan)
|
mock_gateway.mock_devices.append(fan)
|
||||||
@ -87,10 +70,10 @@ async def test_fan_observed(hass, mock_gateway, mock_api_factory):
|
|||||||
async def test_fan_available(hass, mock_gateway, mock_api_factory):
|
async def test_fan_available(hass, mock_gateway, mock_api_factory):
|
||||||
"""Test fan available property."""
|
"""Test fan available property."""
|
||||||
|
|
||||||
fan = mock_fan(test_state={"fan_speed": 10}, device_number=1)
|
fan = mock_fan(test_state={"fan_speed": 10, "air_quality": 12}, device_number=1)
|
||||||
fan.reachable = True
|
fan.reachable = True
|
||||||
|
|
||||||
fan2 = mock_fan(test_state={"fan_speed": 10}, device_number=2)
|
fan2 = mock_fan(test_state={"fan_speed": 10, "air_quality": 12}, device_number=2)
|
||||||
fan2.reachable = False
|
fan2.reachable = False
|
||||||
|
|
||||||
mock_gateway.mock_devices.append(fan)
|
mock_gateway.mock_devices.append(fan)
|
||||||
@ -120,7 +103,7 @@ async def test_set_percentage(
|
|||||||
):
|
):
|
||||||
"""Test setting speed of a fan."""
|
"""Test setting speed of a fan."""
|
||||||
# Note pytradfri style, not hass. Values not really important.
|
# Note pytradfri style, not hass. Values not really important.
|
||||||
initial_state = {"percentage": 10, "fan_speed": 3}
|
initial_state = {"percentage": 10, "fan_speed": 3, "air_quality": 12}
|
||||||
# Setup the gateway with a mock fan.
|
# Setup the gateway with a mock fan.
|
||||||
fan = mock_fan(test_state=initial_state, device_number=0)
|
fan = mock_fan(test_state=initial_state, device_number=0)
|
||||||
mock_gateway.mock_devices.append(fan)
|
mock_gateway.mock_devices.append(fan)
|
||||||
@ -147,7 +130,7 @@ async def test_set_percentage(
|
|||||||
mock_gateway_response = responses[0]
|
mock_gateway_response = responses[0]
|
||||||
|
|
||||||
# A KeyError is raised if we don't add the 5908 response code
|
# A KeyError is raised if we don't add the 5908 response code
|
||||||
mock_gateway_response["15025"][0].update({"5908": 10})
|
mock_gateway_response["15025"][0].update({"5908": 10, "5907": 12})
|
||||||
|
|
||||||
# Use the callback function to update the fan state.
|
# Use the callback function to update the fan state.
|
||||||
dev = Device(mock_gateway_response)
|
dev = Device(mock_gateway_response)
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
from unittest.mock import MagicMock, Mock
|
from unittest.mock import MagicMock, Mock
|
||||||
|
|
||||||
from .common import setup_integration
|
from .common import setup_integration
|
||||||
|
from .test_fan import mock_fan
|
||||||
|
|
||||||
|
|
||||||
def mock_sensor(test_state: list, device_number=0):
|
def mock_sensor(test_state: list, device_number=0):
|
||||||
@ -65,6 +66,20 @@ async def test_cover_battery_sensor(hass, mock_gateway, mock_api_factory):
|
|||||||
assert sensor_1.attributes["device_class"] == "battery"
|
assert sensor_1.attributes["device_class"] == "battery"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_air_quality_sensor(hass, mock_gateway, mock_api_factory):
|
||||||
|
"""Test that a battery sensor is correctly added."""
|
||||||
|
mock_gateway.mock_devices.append(
|
||||||
|
mock_fan(test_state={"fan_speed": 10, "air_quality": 42})
|
||||||
|
)
|
||||||
|
await setup_integration(hass)
|
||||||
|
|
||||||
|
sensor_1 = hass.states.get("sensor.tradfri_fan_0")
|
||||||
|
assert sensor_1 is not None
|
||||||
|
assert sensor_1.state == "42"
|
||||||
|
assert sensor_1.attributes["unit_of_measurement"] == "µg/m³"
|
||||||
|
assert sensor_1.attributes["device_class"] == "aqi"
|
||||||
|
|
||||||
|
|
||||||
async def test_sensor_observed(hass, mock_gateway, mock_api_factory):
|
async def test_sensor_observed(hass, mock_gateway, mock_api_factory):
|
||||||
"""Test that sensors are correctly observed."""
|
"""Test that sensors are correctly observed."""
|
||||||
sensor = mock_sensor(test_state=[{"attribute": "battery_level", "value": 60}])
|
sensor = mock_sensor(test_state=[{"attribute": "battery_level", "value": 60}])
|
||||||
|
Loading…
x
Reference in New Issue
Block a user