Use EntityDescription - awair (#55747)

This commit is contained in:
Marc Mueller 2021-09-06 09:40:41 +02:00 committed by GitHub
parent 99ef2ae54d
commit cc6a0d2f8d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 154 additions and 136 deletions

View File

@ -1,4 +1,5 @@
"""Constants for the Awair component."""
from __future__ import annotations
from dataclasses import dataclass
from datetime import timedelta
@ -6,9 +7,8 @@ import logging
from python_awair.devices import AwairDevice
from homeassistant.components.sensor import SensorEntityDescription
from homeassistant.const import (
ATTR_DEVICE_CLASS,
ATTR_ICON,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_BILLION,
CONCENTRATION_PARTS_PER_MILLION,
@ -36,10 +36,6 @@ API_VOC = "volatile_organic_compounds"
ATTRIBUTION = "Awair air quality sensor"
ATTR_LABEL = "label"
ATTR_UNIT = "unit"
ATTR_UNIQUE_ID = "unique_id"
DOMAIN = "awair"
DUST_ALIASES = [API_PM25, API_PM10]
@ -48,71 +44,89 @@ LOGGER = logging.getLogger(__package__)
UPDATE_INTERVAL = timedelta(minutes=5)
SENSOR_TYPES = {
API_SCORE: {
ATTR_DEVICE_CLASS: None,
ATTR_ICON: "mdi:blur",
ATTR_UNIT: PERCENTAGE,
ATTR_LABEL: "Awair score",
ATTR_UNIQUE_ID: "score", # matches legacy format
},
API_HUMID: {
ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY,
ATTR_ICON: None,
ATTR_UNIT: PERCENTAGE,
ATTR_LABEL: "Humidity",
ATTR_UNIQUE_ID: "HUMID", # matches legacy format
},
API_LUX: {
ATTR_DEVICE_CLASS: DEVICE_CLASS_ILLUMINANCE,
ATTR_ICON: None,
ATTR_UNIT: LIGHT_LUX,
ATTR_LABEL: "Illuminance",
ATTR_UNIQUE_ID: "illuminance",
},
API_SPL_A: {
ATTR_DEVICE_CLASS: None,
ATTR_ICON: "mdi:ear-hearing",
ATTR_UNIT: SOUND_PRESSURE_WEIGHTED_DBA,
ATTR_LABEL: "Sound level",
ATTR_UNIQUE_ID: "sound_level",
},
API_VOC: {
ATTR_DEVICE_CLASS: None,
ATTR_ICON: "mdi:cloud",
ATTR_UNIT: CONCENTRATION_PARTS_PER_BILLION,
ATTR_LABEL: "Volatile organic compounds",
ATTR_UNIQUE_ID: "VOC", # matches legacy format
},
API_TEMP: {
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
ATTR_ICON: None,
ATTR_UNIT: TEMP_CELSIUS,
ATTR_LABEL: "Temperature",
ATTR_UNIQUE_ID: "TEMP", # matches legacy format
},
API_PM25: {
ATTR_DEVICE_CLASS: None,
ATTR_ICON: "mdi:blur",
ATTR_UNIT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
ATTR_LABEL: "PM2.5",
ATTR_UNIQUE_ID: "PM25", # matches legacy format
},
API_PM10: {
ATTR_DEVICE_CLASS: None,
ATTR_ICON: "mdi:blur",
ATTR_UNIT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
ATTR_LABEL: "PM10",
ATTR_UNIQUE_ID: "PM10", # matches legacy format
},
API_CO2: {
ATTR_DEVICE_CLASS: DEVICE_CLASS_CO2,
ATTR_ICON: "mdi:cloud",
ATTR_UNIT: CONCENTRATION_PARTS_PER_MILLION,
ATTR_LABEL: "Carbon dioxide",
ATTR_UNIQUE_ID: "CO2", # matches legacy format
},
}
@dataclass
class AwairRequiredKeysMixin:
"""Mixinf for required keys."""
unique_id_tag: str
@dataclass
class AwairSensorEntityDescription(SensorEntityDescription, AwairRequiredKeysMixin):
"""Describes Awair sensor entity."""
SENSOR_TYPE_SCORE = AwairSensorEntityDescription(
key=API_SCORE,
icon="mdi:blur",
native_unit_of_measurement=PERCENTAGE,
name="Awair score",
unique_id_tag="score", # matches legacy format
)
SENSOR_TYPES: tuple[AwairSensorEntityDescription, ...] = (
AwairSensorEntityDescription(
key=API_HUMID,
device_class=DEVICE_CLASS_HUMIDITY,
native_unit_of_measurement=PERCENTAGE,
name="Humidity",
unique_id_tag="HUMID", # matches legacy format
),
AwairSensorEntityDescription(
key=API_LUX,
device_class=DEVICE_CLASS_ILLUMINANCE,
native_unit_of_measurement=LIGHT_LUX,
name="Illuminance",
unique_id_tag="illuminance",
),
AwairSensorEntityDescription(
key=API_SPL_A,
icon="mdi:ear-hearing",
native_unit_of_measurement=SOUND_PRESSURE_WEIGHTED_DBA,
name="Sound level",
unique_id_tag="sound_level",
),
AwairSensorEntityDescription(
key=API_VOC,
icon="mdi:cloud",
native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION,
name="Volatile organic compounds",
unique_id_tag="VOC", # matches legacy format
),
AwairSensorEntityDescription(
key=API_TEMP,
device_class=DEVICE_CLASS_TEMPERATURE,
native_unit_of_measurement=TEMP_CELSIUS,
name="Temperature",
unique_id_tag="TEMP", # matches legacy format
),
AwairSensorEntityDescription(
key=API_CO2,
device_class=DEVICE_CLASS_CO2,
icon="mdi:cloud",
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
name="Carbon dioxide",
unique_id_tag="CO2", # matches legacy format
),
)
SENSOR_TYPES_DUST: tuple[AwairSensorEntityDescription, ...] = (
AwairSensorEntityDescription(
key=API_PM25,
icon="mdi:blur",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
name="PM2.5",
unique_id_tag="PM25", # matches legacy format
),
AwairSensorEntityDescription(
key=API_PM10,
icon="mdi:blur",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
name="PM10",
unique_id_tag="PM10", # matches legacy format
),
)
@dataclass

View File

@ -7,7 +7,7 @@ import voluptuous as vol
from homeassistant.components.awair import AwairDataUpdateCoordinator, AwairResult
from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.const import ATTR_ATTRIBUTION, ATTR_DEVICE_CLASS, CONF_ACCESS_TOKEN
from homeassistant.const import ATTR_ATTRIBUTION, CONF_ACCESS_TOKEN
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
import homeassistant.helpers.config_validation as cv
@ -22,15 +22,14 @@ from .const import (
API_SCORE,
API_TEMP,
API_VOC,
ATTR_ICON,
ATTR_LABEL,
ATTR_UNIQUE_ID,
ATTR_UNIT,
ATTRIBUTION,
DOMAIN,
DUST_ALIASES,
LOGGER,
SENSOR_TYPE_SCORE,
SENSOR_TYPES,
SENSOR_TYPES_DUST,
AwairSensorEntityDescription,
)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
@ -60,16 +59,20 @@ async def async_setup_entry(
):
"""Set up Awair sensor entity based on a config entry."""
coordinator = hass.data[DOMAIN][config_entry.entry_id]
sensors = []
entities = []
data: list[AwairResult] = coordinator.data.values()
for result in data:
if result.air_data:
sensors.append(AwairSensor(API_SCORE, result.device, coordinator))
entities.append(AwairSensor(result.device, coordinator, SENSOR_TYPE_SCORE))
device_sensors = result.air_data.sensors.keys()
for sensor in device_sensors:
if sensor in SENSOR_TYPES:
sensors.append(AwairSensor(sensor, result.device, coordinator))
entities.extend(
[
AwairSensor(result.device, coordinator, description)
for description in (*SENSOR_TYPES, *SENSOR_TYPES_DUST)
if description.key in device_sensors
]
)
# The "DUST" sensor for Awair is a combo pm2.5/pm10 sensor only
# present on first-gen devices in lieu of separate pm2.5/pm10 sensors.
@ -78,45 +81,53 @@ async def async_setup_entry(
# that data - because we can't really tell what kind of particles the
# "DUST" sensor actually detected. However, it's still useful data.
if API_DUST in device_sensors:
for alias_kind in DUST_ALIASES:
sensors.append(AwairSensor(alias_kind, result.device, coordinator))
entities.extend(
[
AwairSensor(result.device, coordinator, description)
for description in SENSOR_TYPES_DUST
]
)
async_add_entities(sensors)
async_add_entities(entities)
class AwairSensor(CoordinatorEntity, SensorEntity):
"""Defines an Awair sensor entity."""
entity_description: AwairSensorEntityDescription
def __init__(
self,
kind: str,
device: AwairDevice,
coordinator: AwairDataUpdateCoordinator,
description: AwairSensorEntityDescription,
) -> None:
"""Set up an individual AwairSensor."""
super().__init__(coordinator)
self._kind = kind
self.entity_description = description
self._device = device
@property
def name(self) -> str:
def name(self) -> str | None:
"""Return the name of the sensor."""
name = SENSOR_TYPES[self._kind][ATTR_LABEL]
if self._device.name:
name = f"{self._device.name} {name}"
return f"{self._device.name} {self.entity_description.name}"
return name
return self.entity_description.name
@property
def unique_id(self) -> str:
"""Return the uuid as the unique_id."""
unique_id_tag = SENSOR_TYPES[self._kind][ATTR_UNIQUE_ID]
unique_id_tag = self.entity_description.unique_id_tag
# This integration used to create a sensor that was labelled as a "PM2.5"
# sensor for first-gen Awair devices, but its unique_id reflected the truth:
# under the hood, it was a "DUST" sensor. So we preserve that specific unique_id
# for users with first-gen devices that are upgrading.
if self._kind == API_PM25 and API_DUST in self._air_data.sensors:
if (
self.entity_description.key == API_PM25
and API_DUST in self._air_data.sensors
):
unique_id_tag = "DUST"
return f"{self._device.uuid}_{unique_id_tag}"
@ -127,16 +138,17 @@ class AwairSensor(CoordinatorEntity, SensorEntity):
# If the last update was successful...
if self.coordinator.last_update_success and self._air_data:
# and the results included our sensor type...
if self._kind in self._air_data.sensors:
sensor_type = self.entity_description.key
if sensor_type in self._air_data.sensors:
# then we are available.
return True
# or, we're a dust alias
if self._kind in DUST_ALIASES and API_DUST in self._air_data.sensors:
if sensor_type in DUST_ALIASES and API_DUST in self._air_data.sensors:
return True
# or we are API_SCORE
if self._kind == API_SCORE:
if sensor_type == API_SCORE:
# then we are available.
return True
@ -147,38 +159,24 @@ class AwairSensor(CoordinatorEntity, SensorEntity):
def native_value(self) -> float:
"""Return the state, rounding off to reasonable values."""
state: float
sensor_type = self.entity_description.key
# Special-case for "SCORE", which we treat as the AQI
if self._kind == API_SCORE:
if sensor_type == API_SCORE:
state = self._air_data.score
elif self._kind in DUST_ALIASES and API_DUST in self._air_data.sensors:
elif sensor_type in DUST_ALIASES and API_DUST in self._air_data.sensors:
state = self._air_data.sensors.dust
else:
state = self._air_data.sensors[self._kind]
state = self._air_data.sensors[sensor_type]
if self._kind == API_VOC or self._kind == API_SCORE:
if sensor_type in {API_VOC, API_SCORE}:
return round(state)
if self._kind == API_TEMP:
if sensor_type == API_TEMP:
return round(state, 1)
return round(state, 2)
@property
def icon(self) -> str:
"""Return the icon."""
return SENSOR_TYPES[self._kind][ATTR_ICON]
@property
def device_class(self) -> str:
"""Return the device_class."""
return SENSOR_TYPES[self._kind][ATTR_DEVICE_CLASS]
@property
def native_unit_of_measurement(self) -> str:
"""Return the unit the value is expressed in."""
return SENSOR_TYPES[self._kind][ATTR_UNIT]
@property
def extra_state_attributes(self) -> dict:
"""Return the Awair Index alongside state attributes.
@ -201,10 +199,11 @@ class AwairSensor(CoordinatorEntity, SensorEntity):
https://docs.developer.getawair.com/?version=latest#awair-score-and-index
"""
sensor_type = self.entity_description.key
attrs = {ATTR_ATTRIBUTION: ATTRIBUTION}
if self._kind in self._air_data.indices:
attrs["awair_index"] = abs(self._air_data.indices[self._kind])
elif self._kind in DUST_ALIASES and API_DUST in self._air_data.indices:
if sensor_type in self._air_data.indices:
attrs["awair_index"] = abs(self._air_data.indices[sensor_type])
elif sensor_type in DUST_ALIASES and API_DUST in self._air_data.indices:
attrs["awair_index"] = abs(self._air_data.indices.dust)
return attrs

View File

@ -11,9 +11,10 @@ from homeassistant.components.awair.const import (
API_SPL_A,
API_TEMP,
API_VOC,
ATTR_UNIQUE_ID,
DOMAIN,
SENSOR_TYPE_SCORE,
SENSOR_TYPES,
SENSOR_TYPES_DUST,
)
from homeassistant.const import (
ATTR_ICON,
@ -44,6 +45,10 @@ from .const import (
from tests.common import MockConfigEntry
SENSOR_TYPES_MAP = {
desc.key: desc for desc in (SENSOR_TYPE_SCORE, *SENSOR_TYPES, *SENSOR_TYPES_DUST)
}
async def setup_awair(hass, fixtures):
"""Add Awair devices to hass, using specified fixtures for data."""
@ -80,7 +85,7 @@ async def test_awair_gen1_sensors(hass):
hass,
registry,
"sensor.living_room_awair_score",
f"{AWAIR_UUID}_{SENSOR_TYPES[API_SCORE][ATTR_UNIQUE_ID]}",
f"{AWAIR_UUID}_{SENSOR_TYPES_MAP[API_SCORE].unique_id_tag}",
"88",
{ATTR_ICON: "mdi:blur"},
)
@ -89,7 +94,7 @@ async def test_awair_gen1_sensors(hass):
hass,
registry,
"sensor.living_room_temperature",
f"{AWAIR_UUID}_{SENSOR_TYPES[API_TEMP][ATTR_UNIQUE_ID]}",
f"{AWAIR_UUID}_{SENSOR_TYPES_MAP[API_TEMP].unique_id_tag}",
"21.8",
{ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, "awair_index": 1.0},
)
@ -98,7 +103,7 @@ async def test_awair_gen1_sensors(hass):
hass,
registry,
"sensor.living_room_humidity",
f"{AWAIR_UUID}_{SENSOR_TYPES[API_HUMID][ATTR_UNIQUE_ID]}",
f"{AWAIR_UUID}_{SENSOR_TYPES_MAP[API_HUMID].unique_id_tag}",
"41.59",
{ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, "awair_index": 0.0},
)
@ -107,7 +112,7 @@ async def test_awair_gen1_sensors(hass):
hass,
registry,
"sensor.living_room_carbon_dioxide",
f"{AWAIR_UUID}_{SENSOR_TYPES[API_CO2][ATTR_UNIQUE_ID]}",
f"{AWAIR_UUID}_{SENSOR_TYPES_MAP[API_CO2].unique_id_tag}",
"654.0",
{
ATTR_ICON: "mdi:cloud",
@ -120,7 +125,7 @@ async def test_awair_gen1_sensors(hass):
hass,
registry,
"sensor.living_room_volatile_organic_compounds",
f"{AWAIR_UUID}_{SENSOR_TYPES[API_VOC][ATTR_UNIQUE_ID]}",
f"{AWAIR_UUID}_{SENSOR_TYPES_MAP[API_VOC].unique_id_tag}",
"366",
{
ATTR_ICON: "mdi:cloud",
@ -147,7 +152,7 @@ async def test_awair_gen1_sensors(hass):
hass,
registry,
"sensor.living_room_pm10",
f"{AWAIR_UUID}_{SENSOR_TYPES[API_PM10][ATTR_UNIQUE_ID]}",
f"{AWAIR_UUID}_{SENSOR_TYPES_MAP[API_PM10].unique_id_tag}",
"14.3",
{
ATTR_ICON: "mdi:blur",
@ -176,7 +181,7 @@ async def test_awair_gen2_sensors(hass):
hass,
registry,
"sensor.living_room_awair_score",
f"{AWAIR_UUID}_{SENSOR_TYPES[API_SCORE][ATTR_UNIQUE_ID]}",
f"{AWAIR_UUID}_{SENSOR_TYPES_MAP[API_SCORE].unique_id_tag}",
"97",
{ATTR_ICON: "mdi:blur"},
)
@ -185,7 +190,7 @@ async def test_awair_gen2_sensors(hass):
hass,
registry,
"sensor.living_room_pm2_5",
f"{AWAIR_UUID}_{SENSOR_TYPES[API_PM25][ATTR_UNIQUE_ID]}",
f"{AWAIR_UUID}_{SENSOR_TYPES_MAP[API_PM25].unique_id_tag}",
"2.0",
{
ATTR_ICON: "mdi:blur",
@ -210,7 +215,7 @@ async def test_awair_mint_sensors(hass):
hass,
registry,
"sensor.living_room_awair_score",
f"{AWAIR_UUID}_{SENSOR_TYPES[API_SCORE][ATTR_UNIQUE_ID]}",
f"{AWAIR_UUID}_{SENSOR_TYPES_MAP[API_SCORE].unique_id_tag}",
"98",
{ATTR_ICON: "mdi:blur"},
)
@ -219,7 +224,7 @@ async def test_awair_mint_sensors(hass):
hass,
registry,
"sensor.living_room_pm2_5",
f"{AWAIR_UUID}_{SENSOR_TYPES[API_PM25][ATTR_UNIQUE_ID]}",
f"{AWAIR_UUID}_{SENSOR_TYPES_MAP[API_PM25].unique_id_tag}",
"1.0",
{
ATTR_ICON: "mdi:blur",
@ -232,7 +237,7 @@ async def test_awair_mint_sensors(hass):
hass,
registry,
"sensor.living_room_illuminance",
f"{AWAIR_UUID}_{SENSOR_TYPES[API_LUX][ATTR_UNIQUE_ID]}",
f"{AWAIR_UUID}_{SENSOR_TYPES_MAP[API_LUX].unique_id_tag}",
"441.7",
{ATTR_UNIT_OF_MEASUREMENT: LIGHT_LUX},
)
@ -252,7 +257,7 @@ async def test_awair_glow_sensors(hass):
hass,
registry,
"sensor.living_room_awair_score",
f"{AWAIR_UUID}_{SENSOR_TYPES[API_SCORE][ATTR_UNIQUE_ID]}",
f"{AWAIR_UUID}_{SENSOR_TYPES_MAP[API_SCORE].unique_id_tag}",
"93",
{ATTR_ICON: "mdi:blur"},
)
@ -272,7 +277,7 @@ async def test_awair_omni_sensors(hass):
hass,
registry,
"sensor.living_room_awair_score",
f"{AWAIR_UUID}_{SENSOR_TYPES[API_SCORE][ATTR_UNIQUE_ID]}",
f"{AWAIR_UUID}_{SENSOR_TYPES_MAP[API_SCORE].unique_id_tag}",
"99",
{ATTR_ICON: "mdi:blur"},
)
@ -281,7 +286,7 @@ async def test_awair_omni_sensors(hass):
hass,
registry,
"sensor.living_room_sound_level",
f"{AWAIR_UUID}_{SENSOR_TYPES[API_SPL_A][ATTR_UNIQUE_ID]}",
f"{AWAIR_UUID}_{SENSOR_TYPES_MAP[API_SPL_A].unique_id_tag}",
"47.0",
{ATTR_ICON: "mdi:ear-hearing", ATTR_UNIT_OF_MEASUREMENT: "dBa"},
)
@ -290,7 +295,7 @@ async def test_awair_omni_sensors(hass):
hass,
registry,
"sensor.living_room_illuminance",
f"{AWAIR_UUID}_{SENSOR_TYPES[API_LUX][ATTR_UNIQUE_ID]}",
f"{AWAIR_UUID}_{SENSOR_TYPES_MAP[API_LUX].unique_id_tag}",
"804.9",
{ATTR_UNIT_OF_MEASUREMENT: LIGHT_LUX},
)
@ -325,7 +330,7 @@ async def test_awair_unavailable(hass):
hass,
registry,
"sensor.living_room_awair_score",
f"{AWAIR_UUID}_{SENSOR_TYPES[API_SCORE][ATTR_UNIQUE_ID]}",
f"{AWAIR_UUID}_{SENSOR_TYPES_MAP[API_SCORE].unique_id_tag}",
"88",
{ATTR_ICON: "mdi:blur"},
)
@ -338,7 +343,7 @@ async def test_awair_unavailable(hass):
hass,
registry,
"sensor.living_room_awair_score",
f"{AWAIR_UUID}_{SENSOR_TYPES[API_SCORE][ATTR_UNIQUE_ID]}",
f"{AWAIR_UUID}_{SENSOR_TYPES_MAP[API_SCORE].unique_id_tag}",
STATE_UNAVAILABLE,
{ATTR_ICON: "mdi:blur"},
)