mirror of
https://github.com/home-assistant/core.git
synced 2025-04-22 16:27:56 +00:00
Add sensor for filter time left on Tradfri fan platform (#65877)
* Add support for filter time left * Fix test for fan platform * Remove debug code * Add unique id migration tool * Convert to hours * Fix tests * Apply suggestions from code review Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Add comment, check migration * Refactor migration helper * Refactor migration helper * Move definition of new unique id * Return after warning * Add test for unique id migration Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
parent
f943f30492
commit
afd0005a31
@ -1,4 +1,6 @@
|
||||
"""Consts used by Tradfri."""
|
||||
from typing import Final
|
||||
|
||||
from homeassistant.components.light import SUPPORT_BRIGHTNESS, SUPPORT_TRANSITION
|
||||
from homeassistant.const import ( # noqa: F401 pylint: disable=unused-import
|
||||
CONF_HOST,
|
||||
@ -43,3 +45,5 @@ SCAN_INTERVAL = 60 # Interval for updating the coordinator
|
||||
COORDINATOR = "coordinator"
|
||||
COORDINATOR_LIST = "coordinator_list"
|
||||
GROUPS_LIST = "groups_list"
|
||||
|
||||
ATTR_FILTER_LIFE_REMAINING: Final = "filter_life_remaining"
|
||||
|
@ -3,6 +3,7 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from typing import Any, cast
|
||||
|
||||
from pytradfri.command import Command
|
||||
@ -12,16 +13,32 @@ from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, PERCENTAGE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.const import (
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
PERCENTAGE,
|
||||
TIME_HOURS,
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import entity_registry
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .base_class import TradfriBaseEntity
|
||||
from .const import CONF_GATEWAY_ID, COORDINATOR, COORDINATOR_LIST, DOMAIN, KEY_API
|
||||
from .const import (
|
||||
ATTR_FILTER_LIFE_REMAINING,
|
||||
CONF_GATEWAY_ID,
|
||||
COORDINATOR,
|
||||
COORDINATOR_LIST,
|
||||
DOMAIN,
|
||||
KEY_API,
|
||||
)
|
||||
from .coordinator import TradfriDeviceDataUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class TradfriSensorEntityDescriptionMixin:
|
||||
@ -48,21 +65,71 @@ def _get_air_quality(device: Device) -> int | 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,
|
||||
def _get_filter_time_left(device: Device) -> int:
|
||||
"""Fetch the filter's remaining life (in hours)."""
|
||||
return round(
|
||||
device.air_purifier_control.air_purifiers[0].filter_lifetime_remaining / 60
|
||||
)
|
||||
|
||||
|
||||
SENSOR_DESCRIPTIONS_BATTERY: tuple[TradfriSensorEntityDescription, ...] = (
|
||||
TradfriSensorEntityDescription(
|
||||
key="battery_level",
|
||||
device_class=SensorDeviceClass.BATTERY,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
value=lambda device: cast(int, device.device_info.battery_level),
|
||||
),
|
||||
)
|
||||
|
||||
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),
|
||||
|
||||
SENSOR_DESCRIPTIONS_FAN: tuple[TradfriSensorEntityDescription, ...] = (
|
||||
TradfriSensorEntityDescription(
|
||||
key="aqi",
|
||||
name="air quality",
|
||||
device_class=SensorDeviceClass.AQI,
|
||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
value=_get_air_quality,
|
||||
),
|
||||
TradfriSensorEntityDescription(
|
||||
key=ATTR_FILTER_LIFE_REMAINING,
|
||||
name="filter time left",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=TIME_HOURS,
|
||||
icon="mdi:clock-outline",
|
||||
value=_get_filter_time_left,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
def _migrate_old_unique_ids(hass: HomeAssistant, old_unique_id: str, key: str) -> None:
|
||||
"""Migrate unique IDs to the new format."""
|
||||
ent_reg = entity_registry.async_get(hass)
|
||||
|
||||
entity_id = ent_reg.async_get_entity_id(Platform.SENSOR, DOMAIN, old_unique_id)
|
||||
|
||||
if entity_id is None:
|
||||
return
|
||||
|
||||
new_unique_id = f"{old_unique_id}-{key}"
|
||||
|
||||
try:
|
||||
ent_reg.async_update_entity(entity_id, new_unique_id=new_unique_id)
|
||||
except ValueError:
|
||||
_LOGGER.warning(
|
||||
"Skip migration of id [%s] to [%s] because it already exists",
|
||||
old_unique_id,
|
||||
new_unique_id,
|
||||
)
|
||||
return
|
||||
|
||||
_LOGGER.debug(
|
||||
"Migrating unique_id from [%s] to [%s]",
|
||||
old_unique_id,
|
||||
new_unique_id,
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
@ -76,18 +143,26 @@ async def async_setup_entry(
|
||||
entities: list[TradfriSensor] = []
|
||||
|
||||
for device_coordinator in coordinator_data[COORDINATOR_LIST]:
|
||||
description = None
|
||||
if (
|
||||
not device_coordinator.device.has_light_control
|
||||
and not device_coordinator.device.has_socket_control
|
||||
and not device_coordinator.device.has_signal_repeater_control
|
||||
and not device_coordinator.device.has_air_purifier_control
|
||||
):
|
||||
description = SENSOR_DESCRIPTION_BATTERY
|
||||
descriptions = SENSOR_DESCRIPTIONS_BATTERY
|
||||
elif device_coordinator.device.has_air_purifier_control:
|
||||
description = SENSOR_DESCRIPTION_AQI
|
||||
descriptions = SENSOR_DESCRIPTIONS_FAN
|
||||
else:
|
||||
continue
|
||||
|
||||
for description in descriptions:
|
||||
# Added in Home assistant 2022.3
|
||||
_migrate_old_unique_ids(
|
||||
hass=hass,
|
||||
old_unique_id=f"{gateway_id}-{device_coordinator.device.id}",
|
||||
key=description.key,
|
||||
)
|
||||
|
||||
if description:
|
||||
entities.append(
|
||||
TradfriSensor(
|
||||
device_coordinator,
|
||||
@ -121,6 +196,11 @@ class TradfriSensor(TradfriBaseEntity, SensorEntity):
|
||||
|
||||
self.entity_description = description
|
||||
|
||||
self._attr_unique_id = f"{self._attr_unique_id}-{description.key}"
|
||||
|
||||
if description.name:
|
||||
self._attr_name = f"{self._attr_name}: {description.name}"
|
||||
|
||||
self._refresh() # Set initial state
|
||||
|
||||
def _refresh(self) -> None:
|
||||
|
@ -129,8 +129,8 @@ async def test_set_percentage(
|
||||
responses = mock_gateway.mock_responses
|
||||
mock_gateway_response = responses[0]
|
||||
|
||||
# A KeyError is raised if we don't add the 5908 response code
|
||||
mock_gateway_response["15025"][0].update({"5908": 10, "5907": 12})
|
||||
# A KeyError is raised if we don't this to the response code
|
||||
mock_gateway_response["15025"][0].update({"5908": 10, "5907": 12, "5910": 20})
|
||||
|
||||
# Use the callback function to update the fan state.
|
||||
dev = Device(mock_gateway_response)
|
||||
|
@ -1,10 +1,17 @@
|
||||
"""Tradfri sensor platform tests."""
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import MagicMock, Mock
|
||||
|
||||
from homeassistant.components import tradfri
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from . import GATEWAY_ID
|
||||
from .common import setup_integration
|
||||
from .test_fan import mock_fan
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
def mock_sensor(test_state: list, device_number=0):
|
||||
"""Mock a tradfri sensor."""
|
||||
@ -69,17 +76,42 @@ async def test_cover_battery_sensor(hass, mock_gateway, mock_api_factory):
|
||||
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})
|
||||
mock_fan(
|
||||
test_state={
|
||||
"fan_speed": 10,
|
||||
"air_quality": 42,
|
||||
"filter_lifetime_remaining": 120,
|
||||
}
|
||||
)
|
||||
)
|
||||
await setup_integration(hass)
|
||||
|
||||
sensor_1 = hass.states.get("sensor.tradfri_fan_0")
|
||||
sensor_1 = hass.states.get("sensor.tradfri_fan_0_air_quality")
|
||||
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_filter_time_left_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,
|
||||
"filter_lifetime_remaining": 120,
|
||||
}
|
||||
)
|
||||
)
|
||||
await setup_integration(hass)
|
||||
|
||||
sensor_1 = hass.states.get("sensor.tradfri_fan_0_filter_time_left")
|
||||
assert sensor_1 is not None
|
||||
assert sensor_1.state == "2"
|
||||
assert sensor_1.attributes["unit_of_measurement"] == "h"
|
||||
|
||||
|
||||
async def test_sensor_observed(hass, mock_gateway, mock_api_factory):
|
||||
"""Test that sensors are correctly observed."""
|
||||
sensor = mock_sensor(test_state=[{"attribute": "battery_level", "value": 60}])
|
||||
@ -106,3 +138,51 @@ async def test_sensor_available(hass, mock_gateway, mock_api_factory):
|
||||
|
||||
assert hass.states.get("sensor.tradfri_sensor_1").state == "60"
|
||||
assert hass.states.get("sensor.tradfri_sensor_2").state == "unavailable"
|
||||
|
||||
|
||||
async def test_unique_id_migration(hass, mock_gateway, mock_api_factory):
|
||||
"""Test unique ID is migrated from old format to new."""
|
||||
ent_reg = er.async_get(hass)
|
||||
old_unique_id = f"{GATEWAY_ID}-mock-sensor-id-0"
|
||||
entry = MockConfigEntry(
|
||||
domain=tradfri.DOMAIN,
|
||||
data={
|
||||
"host": "mock-host",
|
||||
"identity": "mock-identity",
|
||||
"key": "mock-key",
|
||||
"import_groups": False,
|
||||
"gateway_id": GATEWAY_ID,
|
||||
},
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
# Version 1
|
||||
sensor_name = "sensor.tradfri_sensor_0"
|
||||
entity_name = sensor_name.split(".")[1]
|
||||
|
||||
entity_entry = ent_reg.async_get_or_create(
|
||||
"sensor",
|
||||
tradfri.DOMAIN,
|
||||
old_unique_id,
|
||||
suggested_object_id=entity_name,
|
||||
config_entry=entry,
|
||||
original_name=entity_name,
|
||||
)
|
||||
|
||||
assert entity_entry.entity_id == sensor_name
|
||||
assert entity_entry.unique_id == old_unique_id
|
||||
|
||||
# Add a sensor to the gateway so that it populates coordinator list
|
||||
sensor = mock_sensor(
|
||||
test_state=[{"attribute": "battery_level", "value": 60}],
|
||||
)
|
||||
mock_gateway.mock_devices.append(sensor)
|
||||
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Check that new RegistryEntry is using new unique ID format
|
||||
entity_entry = ent_reg.async_get(sensor_name)
|
||||
new_unique_id = f"{GATEWAY_ID}-mock-sensor-id-0-battery_level"
|
||||
assert entity_entry.unique_id == new_unique_id
|
||||
assert ent_reg.async_get_entity_id("sensor", tradfri.DOMAIN, old_unique_id) is None
|
||||
|
Loading…
x
Reference in New Issue
Block a user