From 562aa74c77dd6b9e3f9ffcf44d8758e4399120e6 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Tue, 20 Jul 2021 00:22:41 -0400 Subject: [PATCH] Switch to dataclass from dictionary for climacell sensor definitions (#53168) * Switch to dataclass from dictionary for climacell sensor definitions * fix post_init * fix dataclass and add test * Update homeassistant/components/climacell/sensor.py Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> * Update homeassistant/components/climacell/const.py Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> * simplify logic * use tuple * simplify unit of measurement and use class attributes * Switch from UnitT to str Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> --- .../components/climacell/__init__.py | 8 +- homeassistant/components/climacell/const.py | 474 +++++++++--------- homeassistant/components/climacell/sensor.py | 105 ++-- tests/components/climacell/test_const.py | 12 + 4 files changed, 282 insertions(+), 317 deletions(-) create mode 100644 tests/components/climacell/test_const.py diff --git a/homeassistant/components/climacell/__init__.py b/homeassistant/components/climacell/__init__.py index db006b6fb68..9fd3e0b8340 100644 --- a/homeassistant/components/climacell/__init__.py +++ b/homeassistant/components/climacell/__init__.py @@ -35,7 +35,6 @@ from homeassistant.helpers.update_coordinator import ( ) from .const import ( - ATTR_FIELD, ATTRIBUTION, CC_ATTR_CLOUD_COVER, CC_ATTR_CONDITION, @@ -223,10 +222,7 @@ class ClimaCellDataUpdateCoordinator(DataUpdateCoordinator): CC_V3_ATTR_WIND_GUST, CC_V3_ATTR_CLOUD_COVER, CC_V3_ATTR_PRECIPITATION_TYPE, - *( - sensor_type[ATTR_FIELD] - for sensor_type in CC_V3_SENSOR_TYPES - ), + *(sensor_type.field for sensor_type in CC_V3_SENSOR_TYPES), ] ) data[FORECASTS][HOURLY] = await self._api.forecast_hourly( @@ -283,7 +279,7 @@ class ClimaCellDataUpdateCoordinator(DataUpdateCoordinator): CC_ATTR_WIND_GUST, CC_ATTR_CLOUD_COVER, CC_ATTR_PRECIPITATION_TYPE, - *(sensor_type[ATTR_FIELD] for sensor_type in CC_SENSOR_TYPES), + *(sensor_type.field for sensor_type in CC_SENSOR_TYPES), ], [ CC_ATTR_TEMPERATURE_LOW, diff --git a/homeassistant/components/climacell/const.py b/homeassistant/components/climacell/const.py index f5d2ac02696..f19724b002e 100644 --- a/homeassistant/components/climacell/const.py +++ b/homeassistant/components/climacell/const.py @@ -1,4 +1,10 @@ """Constants for the ClimaCell integration.""" +from __future__ import annotations + +from dataclasses import dataclass +from enum import IntEnum +from typing import Callable + from pyclimacell.const import ( DAILY, HOURLY, @@ -26,15 +32,10 @@ from homeassistant.components.weather import ( ATTR_CONDITION_WINDY, ) from homeassistant.const import ( - ATTR_DEVICE_CLASS, - ATTR_NAME, CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_BILLION, CONCENTRATION_PARTS_PER_MILLION, - CONF_UNIT_OF_MEASUREMENT, - CONF_UNIT_SYSTEM_IMPERIAL, - CONF_UNIT_SYSTEM_METRIC, DEVICE_CLASS_CO, DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, @@ -74,13 +75,6 @@ MAX_FORECASTS = { NOWCAST: 30, } -# Sensor type keys -ATTR_FIELD = "field" -ATTR_METRIC_CONVERSION = "metric_conversion" -ATTR_VALUE_MAP = "value_map" -ATTR_IS_METRIC_CHECK = "is_metric_check" -ATTR_SCALE = "scale" - # Additional attributes ATTR_WIND_GUST = "wind_gust" ATTR_CLOUD_COVER = "cloud_cover" @@ -155,165 +149,182 @@ CC_ATTR_SOLAR_GHI = "solarGHI" CC_ATTR_CLOUD_BASE = "cloudBase" CC_ATTR_CLOUD_CEILING = "cloudCeiling" -CC_SENSOR_TYPES = [ - { - ATTR_FIELD: CC_ATTR_FEELS_LIKE, - ATTR_NAME: "Feels Like", - CONF_UNIT_SYSTEM_IMPERIAL: TEMP_FAHRENHEIT, - CONF_UNIT_SYSTEM_METRIC: TEMP_CELSIUS, - ATTR_METRIC_CONVERSION: lambda val: temp_convert( - val, TEMP_FAHRENHEIT, TEMP_CELSIUS - ), - ATTR_IS_METRIC_CHECK: True, - ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, - }, - { - ATTR_FIELD: CC_ATTR_DEW_POINT, - ATTR_NAME: "Dew Point", - CONF_UNIT_SYSTEM_IMPERIAL: TEMP_FAHRENHEIT, - CONF_UNIT_SYSTEM_METRIC: TEMP_CELSIUS, - ATTR_METRIC_CONVERSION: lambda val: temp_convert( - val, TEMP_FAHRENHEIT, TEMP_CELSIUS - ), - ATTR_IS_METRIC_CHECK: True, - ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, - }, - { - ATTR_FIELD: CC_ATTR_PRESSURE_SURFACE_LEVEL, - ATTR_NAME: "Pressure (Surface Level)", - CONF_UNIT_SYSTEM_IMPERIAL: PRESSURE_INHG, - CONF_UNIT_SYSTEM_METRIC: PRESSURE_HPA, - ATTR_METRIC_CONVERSION: lambda val: pressure_convert( + +@dataclass +class ClimaCellSensorMetadata: + """Metadata about an individual ClimaCell sensor.""" + + field: str + name: str + unit_imperial: str | None = None + unit_metric: str | None = None + metric_conversion: Callable | float = 1.0 + is_metric_check: bool | None = None + device_class: str | None = None + value_map: IntEnum | None = None + + def __post_init__(self) -> None: + """Post initialization.""" + units = (self.unit_imperial, self.unit_metric) + if any(u is not None for u in units) and any(u is None for u in units): + raise RuntimeError( + "`unit_imperial` and `unit_metric` both need to be None or both need " + "to be defined." + ) + + +CC_SENSOR_TYPES = ( + ClimaCellSensorMetadata( + CC_ATTR_FEELS_LIKE, + "Feels Like", + unit_imperial=TEMP_FAHRENHEIT, + unit_metric=TEMP_CELSIUS, + metric_conversion=lambda val: temp_convert(val, TEMP_FAHRENHEIT, TEMP_CELSIUS), + is_metric_check=True, + device_class=DEVICE_CLASS_TEMPERATURE, + ), + ClimaCellSensorMetadata( + CC_ATTR_DEW_POINT, + "Dew Point", + unit_imperial=TEMP_FAHRENHEIT, + unit_metric=TEMP_CELSIUS, + metric_conversion=lambda val: temp_convert(val, TEMP_FAHRENHEIT, TEMP_CELSIUS), + is_metric_check=True, + device_class=DEVICE_CLASS_TEMPERATURE, + ), + ClimaCellSensorMetadata( + CC_ATTR_PRESSURE_SURFACE_LEVEL, + "Pressure (Surface Level)", + unit_imperial=PRESSURE_INHG, + unit_metric=PRESSURE_HPA, + metric_conversion=lambda val: pressure_convert( val, PRESSURE_INHG, PRESSURE_HPA ), - ATTR_IS_METRIC_CHECK: True, - ATTR_DEVICE_CLASS: DEVICE_CLASS_PRESSURE, - }, - { - ATTR_FIELD: CC_ATTR_SOLAR_GHI, - ATTR_NAME: "Global Horizontal Irradiance", - CONF_UNIT_SYSTEM_IMPERIAL: IRRADIATION_BTUS_PER_HOUR_SQUARE_FOOT, - CONF_UNIT_SYSTEM_METRIC: IRRADIATION_WATTS_PER_SQUARE_METER, - ATTR_METRIC_CONVERSION: 3.15459, - ATTR_IS_METRIC_CHECK: True, - }, - { - ATTR_FIELD: CC_ATTR_CLOUD_BASE, - ATTR_NAME: "Cloud Base", - CONF_UNIT_SYSTEM_IMPERIAL: LENGTH_MILES, - CONF_UNIT_SYSTEM_METRIC: LENGTH_KILOMETERS, - ATTR_METRIC_CONVERSION: lambda val: distance_convert( + is_metric_check=True, + device_class=DEVICE_CLASS_PRESSURE, + ), + ClimaCellSensorMetadata( + CC_ATTR_SOLAR_GHI, + "Global Horizontal Irradiance", + unit_imperial=IRRADIATION_BTUS_PER_HOUR_SQUARE_FOOT, + unit_metric=IRRADIATION_WATTS_PER_SQUARE_METER, + metric_conversion=3.15459, + is_metric_check=True, + ), + ClimaCellSensorMetadata( + CC_ATTR_CLOUD_BASE, + "Cloud Base", + unit_imperial=LENGTH_MILES, + unit_metric=LENGTH_KILOMETERS, + metric_conversion=lambda val: distance_convert( val, LENGTH_MILES, LENGTH_KILOMETERS ), - ATTR_IS_METRIC_CHECK: True, - }, - { - ATTR_FIELD: CC_ATTR_CLOUD_CEILING, - ATTR_NAME: "Cloud Ceiling", - CONF_UNIT_SYSTEM_IMPERIAL: LENGTH_MILES, - CONF_UNIT_SYSTEM_METRIC: LENGTH_KILOMETERS, - ATTR_METRIC_CONVERSION: lambda val: distance_convert( + is_metric_check=True, + ), + ClimaCellSensorMetadata( + CC_ATTR_CLOUD_CEILING, + "Cloud Ceiling", + unit_imperial=LENGTH_MILES, + unit_metric=LENGTH_KILOMETERS, + metric_conversion=lambda val: distance_convert( val, LENGTH_MILES, LENGTH_KILOMETERS ), - ATTR_IS_METRIC_CHECK: True, - }, - { - ATTR_FIELD: CC_ATTR_CLOUD_COVER, - ATTR_NAME: "Cloud Cover", - CONF_UNIT_OF_MEASUREMENT: PERCENTAGE, - }, - { - ATTR_FIELD: CC_ATTR_WIND_GUST, - ATTR_NAME: "Wind Gust", - CONF_UNIT_SYSTEM_IMPERIAL: SPEED_MILES_PER_HOUR, - CONF_UNIT_SYSTEM_METRIC: SPEED_METERS_PER_SECOND, - ATTR_METRIC_CONVERSION: lambda val: distance_convert( - val, LENGTH_MILES, LENGTH_METERS - ) + is_metric_check=True, + ), + ClimaCellSensorMetadata( + CC_ATTR_CLOUD_COVER, + "Cloud Cover", + unit_imperial=PERCENTAGE, + unit_metric=PERCENTAGE, + ), + ClimaCellSensorMetadata( + CC_ATTR_WIND_GUST, + "Wind Gust", + unit_imperial=SPEED_MILES_PER_HOUR, + unit_metric=SPEED_METERS_PER_SECOND, + metric_conversion=lambda val: distance_convert(val, LENGTH_MILES, LENGTH_METERS) / 3600, - ATTR_IS_METRIC_CHECK: True, - }, - { - ATTR_FIELD: CC_ATTR_PRECIPITATION_TYPE, - ATTR_NAME: "Precipitation Type", - ATTR_VALUE_MAP: PrecipitationType, - }, - { - ATTR_FIELD: CC_ATTR_OZONE, - ATTR_NAME: "Ozone", - CONF_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_BILLION, - }, - { - ATTR_FIELD: CC_ATTR_PARTICULATE_MATTER_25, - ATTR_NAME: "Particulate Matter < 2.5 μm", - CONF_UNIT_SYSTEM_IMPERIAL: CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT, - CONF_UNIT_SYSTEM_METRIC: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - ATTR_METRIC_CONVERSION: 3.2808399 ** 3, - ATTR_IS_METRIC_CHECK: True, - }, - { - ATTR_FIELD: CC_ATTR_PARTICULATE_MATTER_10, - ATTR_NAME: "Particulate Matter < 10 μm", - CONF_UNIT_SYSTEM_IMPERIAL: CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT, - CONF_UNIT_SYSTEM_METRIC: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - ATTR_METRIC_CONVERSION: 3.2808399 ** 3, - ATTR_IS_METRIC_CHECK: True, - }, - { - ATTR_FIELD: CC_ATTR_NITROGEN_DIOXIDE, - ATTR_NAME: "Nitrogen Dioxide", - CONF_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_BILLION, - }, - { - ATTR_FIELD: CC_ATTR_CARBON_MONOXIDE, - ATTR_NAME: "Carbon Monoxide", - CONF_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_BILLION, - ATTR_DEVICE_CLASS: DEVICE_CLASS_CO, - }, - { - ATTR_FIELD: CC_ATTR_SULFUR_DIOXIDE, - ATTR_NAME: "Sulfur Dioxide", - CONF_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_BILLION, - }, - {ATTR_FIELD: CC_ATTR_EPA_AQI, ATTR_NAME: "US EPA Air Quality Index"}, - { - ATTR_FIELD: CC_ATTR_EPA_PRIMARY_POLLUTANT, - ATTR_NAME: "US EPA Primary Pollutant", - ATTR_VALUE_MAP: PrimaryPollutantType, - }, - { - ATTR_FIELD: CC_ATTR_EPA_HEALTH_CONCERN, - ATTR_NAME: "US EPA Health Concern", - ATTR_VALUE_MAP: HealthConcernType, - }, - {ATTR_FIELD: CC_ATTR_CHINA_AQI, ATTR_NAME: "China MEP Air Quality Index"}, - { - ATTR_FIELD: CC_ATTR_CHINA_PRIMARY_POLLUTANT, - ATTR_NAME: "China MEP Primary Pollutant", - ATTR_VALUE_MAP: PrimaryPollutantType, - }, - { - ATTR_FIELD: CC_ATTR_CHINA_HEALTH_CONCERN, - ATTR_NAME: "China MEP Health Concern", - ATTR_VALUE_MAP: HealthConcernType, - }, - { - ATTR_FIELD: CC_ATTR_POLLEN_TREE, - ATTR_NAME: "Tree Pollen Index", - ATTR_VALUE_MAP: PollenIndex, - }, - { - ATTR_FIELD: CC_ATTR_POLLEN_WEED, - ATTR_NAME: "Weed Pollen Index", - ATTR_VALUE_MAP: PollenIndex, - }, - { - ATTR_FIELD: CC_ATTR_POLLEN_GRASS, - ATTR_NAME: "Grass Pollen Index", - ATTR_VALUE_MAP: PollenIndex, - }, - {ATTR_FIELD: CC_ATTR_FIRE_INDEX, ATTR_NAME: "Fire Index"}, -] + is_metric_check=True, + ), + ClimaCellSensorMetadata( + CC_ATTR_PRECIPITATION_TYPE, + "Precipitation Type", + value_map=PrecipitationType, + ), + ClimaCellSensorMetadata( + CC_ATTR_OZONE, + "Ozone", + unit_imperial=CONCENTRATION_PARTS_PER_BILLION, + unit_metric=CONCENTRATION_PARTS_PER_BILLION, + ), + ClimaCellSensorMetadata( + CC_ATTR_PARTICULATE_MATTER_25, + "Particulate Matter < 2.5 μm", + unit_imperial=CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT, + unit_metric=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + metric_conversion=3.2808399 ** 3, + is_metric_check=True, + ), + ClimaCellSensorMetadata( + CC_ATTR_PARTICULATE_MATTER_10, + "Particulate Matter < 10 μm", + unit_imperial=CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT, + unit_metric=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + metric_conversion=3.2808399 ** 3, + is_metric_check=True, + ), + ClimaCellSensorMetadata( + CC_ATTR_NITROGEN_DIOXIDE, + "Nitrogen Dioxide", + unit_imperial=CONCENTRATION_PARTS_PER_BILLION, + unit_metric=CONCENTRATION_PARTS_PER_BILLION, + ), + ClimaCellSensorMetadata( + CC_ATTR_CARBON_MONOXIDE, + "Carbon Monoxide", + unit_imperial=CONCENTRATION_PARTS_PER_MILLION, + unit_metric=CONCENTRATION_PARTS_PER_MILLION, + device_class=DEVICE_CLASS_CO, + ), + ClimaCellSensorMetadata( + CC_ATTR_SULFUR_DIOXIDE, + "Sulfur Dioxide", + unit_imperial=CONCENTRATION_PARTS_PER_BILLION, + unit_metric=CONCENTRATION_PARTS_PER_BILLION, + ), + ClimaCellSensorMetadata(CC_ATTR_EPA_AQI, "US EPA Air Quality Index"), + ClimaCellSensorMetadata( + CC_ATTR_EPA_PRIMARY_POLLUTANT, + "US EPA Primary Pollutant", + value_map=PrimaryPollutantType, + ), + ClimaCellSensorMetadata( + CC_ATTR_EPA_HEALTH_CONCERN, + "US EPA Health Concern", + value_map=HealthConcernType, + ), + ClimaCellSensorMetadata(CC_ATTR_CHINA_AQI, "China MEP Air Quality Index"), + ClimaCellSensorMetadata( + CC_ATTR_CHINA_PRIMARY_POLLUTANT, + "China MEP Primary Pollutant", + value_map=PrimaryPollutantType, + ), + ClimaCellSensorMetadata( + CC_ATTR_CHINA_HEALTH_CONCERN, + "China MEP Health Concern", + value_map=HealthConcernType, + ), + ClimaCellSensorMetadata( + CC_ATTR_POLLEN_TREE, "Tree Pollen Index", value_map=PollenIndex + ), + ClimaCellSensorMetadata( + CC_ATTR_POLLEN_WEED, "Weed Pollen Index", value_map=PollenIndex + ), + ClimaCellSensorMetadata( + CC_ATTR_POLLEN_GRASS, "Grass Pollen Index", value_map=PollenIndex + ), + ClimaCellSensorMetadata(CC_ATTR_FIRE_INDEX, "Fire Index"), +) # V3 constants CONDITIONS_V3 = { @@ -377,73 +388,68 @@ CC_V3_ATTR_POLLEN_WEED = "pollen_weed" CC_V3_ATTR_POLLEN_GRASS = "pollen_grass" CC_V3_ATTR_FIRE_INDEX = "fire_index" -CC_V3_SENSOR_TYPES = [ - { - ATTR_FIELD: CC_V3_ATTR_OZONE, - ATTR_NAME: "Ozone", - CONF_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_BILLION, - }, - { - ATTR_FIELD: CC_V3_ATTR_PARTICULATE_MATTER_25, - ATTR_NAME: "Particulate Matter < 2.5 μm", - CONF_UNIT_SYSTEM_IMPERIAL: "μg/ft³", - CONF_UNIT_SYSTEM_METRIC: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - ATTR_METRIC_CONVERSION: 3.2808399 ** 3, - ATTR_IS_METRIC_CHECK: False, - }, - { - ATTR_FIELD: CC_V3_ATTR_PARTICULATE_MATTER_10, - ATTR_NAME: "Particulate Matter < 10 μm", - CONF_UNIT_SYSTEM_IMPERIAL: "μg/ft³", - CONF_UNIT_SYSTEM_METRIC: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - ATTR_METRIC_CONVERSION: 3.2808399 ** 3, - ATTR_IS_METRIC_CHECK: False, - }, - { - ATTR_FIELD: CC_V3_ATTR_NITROGEN_DIOXIDE, - ATTR_NAME: "Nitrogen Dioxide", - CONF_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_BILLION, - }, - { - ATTR_FIELD: CC_V3_ATTR_CARBON_MONOXIDE, - ATTR_NAME: "Carbon Monoxide", - CONF_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_MILLION, - ATTR_DEVICE_CLASS: DEVICE_CLASS_CO, - }, - { - ATTR_FIELD: CC_V3_ATTR_SULFUR_DIOXIDE, - ATTR_NAME: "Sulfur Dioxide", - CONF_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_BILLION, - }, - {ATTR_FIELD: CC_V3_ATTR_EPA_AQI, ATTR_NAME: "US EPA Air Quality Index"}, - { - ATTR_FIELD: CC_V3_ATTR_EPA_PRIMARY_POLLUTANT, - ATTR_NAME: "US EPA Primary Pollutant", - }, - {ATTR_FIELD: CC_V3_ATTR_EPA_HEALTH_CONCERN, ATTR_NAME: "US EPA Health Concern"}, - {ATTR_FIELD: CC_V3_ATTR_CHINA_AQI, ATTR_NAME: "China MEP Air Quality Index"}, - { - ATTR_FIELD: CC_V3_ATTR_CHINA_PRIMARY_POLLUTANT, - ATTR_NAME: "China MEP Primary Pollutant", - }, - { - ATTR_FIELD: CC_V3_ATTR_CHINA_HEALTH_CONCERN, - ATTR_NAME: "China MEP Health Concern", - }, - { - ATTR_FIELD: CC_V3_ATTR_POLLEN_TREE, - ATTR_NAME: "Tree Pollen Index", - ATTR_VALUE_MAP: V3PollenIndex, - }, - { - ATTR_FIELD: CC_V3_ATTR_POLLEN_WEED, - ATTR_NAME: "Weed Pollen Index", - ATTR_VALUE_MAP: V3PollenIndex, - }, - { - ATTR_FIELD: CC_V3_ATTR_POLLEN_GRASS, - ATTR_NAME: "Grass Pollen Index", - ATTR_VALUE_MAP: V3PollenIndex, - }, - {ATTR_FIELD: CC_V3_ATTR_FIRE_INDEX, ATTR_NAME: "Fire Index"}, -] +CC_V3_SENSOR_TYPES = ( + ClimaCellSensorMetadata( + CC_V3_ATTR_OZONE, + "Ozone", + unit_imperial=CONCENTRATION_PARTS_PER_BILLION, + unit_metric=CONCENTRATION_PARTS_PER_BILLION, + ), + ClimaCellSensorMetadata( + CC_V3_ATTR_PARTICULATE_MATTER_25, + "Particulate Matter < 2.5 μm", + unit_imperial=CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT, + unit_metric=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + metric_conversion=3.2808399 ** 3, + is_metric_check=False, + ), + ClimaCellSensorMetadata( + CC_V3_ATTR_PARTICULATE_MATTER_10, + "Particulate Matter < 10 μm", + unit_imperial=CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT, + unit_metric=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + metric_conversion=3.2808399 ** 3, + is_metric_check=False, + ), + ClimaCellSensorMetadata( + CC_V3_ATTR_NITROGEN_DIOXIDE, + "Nitrogen Dioxide", + unit_imperial=CONCENTRATION_PARTS_PER_BILLION, + unit_metric=CONCENTRATION_PARTS_PER_BILLION, + ), + ClimaCellSensorMetadata( + CC_V3_ATTR_CARBON_MONOXIDE, + "Carbon Monoxide", + unit_imperial=CONCENTRATION_PARTS_PER_MILLION, + unit_metric=CONCENTRATION_PARTS_PER_MILLION, + device_class=DEVICE_CLASS_CO, + ), + ClimaCellSensorMetadata( + CC_V3_ATTR_SULFUR_DIOXIDE, + "Sulfur Dioxide", + unit_imperial=CONCENTRATION_PARTS_PER_BILLION, + unit_metric=CONCENTRATION_PARTS_PER_BILLION, + ), + ClimaCellSensorMetadata(CC_V3_ATTR_EPA_AQI, "US EPA Air Quality Index"), + ClimaCellSensorMetadata( + CC_V3_ATTR_EPA_PRIMARY_POLLUTANT, "US EPA Primary Pollutant" + ), + ClimaCellSensorMetadata(CC_V3_ATTR_EPA_HEALTH_CONCERN, "US EPA Health Concern"), + ClimaCellSensorMetadata(CC_V3_ATTR_CHINA_AQI, "China MEP Air Quality Index"), + ClimaCellSensorMetadata( + CC_V3_ATTR_CHINA_PRIMARY_POLLUTANT, "China MEP Primary Pollutant" + ), + ClimaCellSensorMetadata( + CC_V3_ATTR_CHINA_HEALTH_CONCERN, "China MEP Health Concern" + ), + ClimaCellSensorMetadata( + CC_V3_ATTR_POLLEN_TREE, "Tree Pollen Index", value_map=V3PollenIndex + ), + ClimaCellSensorMetadata( + CC_V3_ATTR_POLLEN_WEED, "Weed Pollen Index", value_map=V3PollenIndex + ), + ClimaCellSensorMetadata( + CC_V3_ATTR_POLLEN_GRASS, "Grass Pollen Index", value_map=V3PollenIndex + ), + ClimaCellSensorMetadata(CC_V3_ATTR_FIRE_INDEX, "Fire Index"), +) diff --git a/homeassistant/components/climacell/sensor.py b/homeassistant/components/climacell/sensor.py index e5d9a2cdb75..77fa6486013 100644 --- a/homeassistant/components/climacell/sensor.py +++ b/homeassistant/components/climacell/sensor.py @@ -2,39 +2,19 @@ from __future__ import annotations from abc import abstractmethod -from collections.abc import Mapping import logging -from typing import Any from pyclimacell.const import CURRENT from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_ATTRIBUTION, - ATTR_DEVICE_CLASS, - ATTR_NAME, - CONF_API_VERSION, - CONF_NAME, - CONF_UNIT_OF_MEASUREMENT, - CONF_UNIT_SYSTEM_IMPERIAL, - CONF_UNIT_SYSTEM_METRIC, -) +from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_VERSION, CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import slugify from . import ClimaCellDataUpdateCoordinator, ClimaCellEntity -from .const import ( - ATTR_FIELD, - ATTR_IS_METRIC_CHECK, - ATTR_METRIC_CONVERSION, - ATTR_SCALE, - ATTR_VALUE_MAP, - CC_SENSOR_TYPES, - CC_V3_SENSOR_TYPES, - DOMAIN, -) +from .const import CC_SENSOR_TYPES, CC_V3_SENSOR_TYPES, DOMAIN, ClimaCellSensorMetadata _LOGGER = logging.getLogger(__name__) @@ -55,7 +35,7 @@ async def async_setup_entry( api_class = ClimaCellSensorEntity sensor_types = CC_SENSOR_TYPES entities = [ - api_class(config_entry, coordinator, api_version, sensor_type) + api_class(hass, config_entry, coordinator, api_version, sensor_type) for sensor_type in sensor_types ] async_add_entities(entities) @@ -66,53 +46,29 @@ class BaseClimaCellSensorEntity(ClimaCellEntity, SensorEntity): def __init__( self, + hass: HomeAssistant, config_entry: ConfigEntry, coordinator: ClimaCellDataUpdateCoordinator, api_version: int, - sensor_type: dict[str, str | float], + sensor_type: ClimaCellSensorMetadata, ) -> None: """Initialize ClimaCell Sensor Entity.""" super().__init__(config_entry, coordinator, api_version) self.sensor_type = sensor_type - self._attr_device_class = self.sensor_type.get(ATTR_DEVICE_CLASS) - - @property - def entity_registry_enabled_default(self) -> bool: - """Return if the entity should be enabled when first added to the entity registry.""" - return False - - @property - def name(self) -> str: - """Return the name of the entity.""" - return f"{self._config_entry.data[CONF_NAME]} - {self.sensor_type[ATTR_NAME]}" - - @property - def unique_id(self) -> str: - """Return the unique id of the entity.""" - return f"{self._config_entry.unique_id}_{slugify(self.sensor_type[ATTR_NAME])}" - - @property - def extra_state_attributes(self) -> Mapping[str, Any] | None: - """Return entity specific state attributes.""" - return {ATTR_ATTRIBUTION: self.attribution} - - @property - def unit_of_measurement(self) -> str | None: - """Return the unit of measurement.""" - if CONF_UNIT_OF_MEASUREMENT in self.sensor_type: - return self.sensor_type[CONF_UNIT_OF_MEASUREMENT] - - if ( - CONF_UNIT_SYSTEM_IMPERIAL in self.sensor_type - and CONF_UNIT_SYSTEM_METRIC in self.sensor_type - ): - return ( - self.sensor_type[CONF_UNIT_SYSTEM_METRIC] - if self.hass.config.units.is_metric - else self.sensor_type[CONF_UNIT_SYSTEM_IMPERIAL] - ) - - return None + self._attr_device_class = self.sensor_type.device_class + self._attr_entity_registry_enabled_default = False + self._attr_name = ( + f"{self._config_entry.data[CONF_NAME]} - {self.sensor_type.name}" + ) + self._attr_unique_id = ( + f"{self._config_entry.unique_id}_{slugify(self.sensor_type.name)}" + ) + self._attr_extra_state_attributes = {ATTR_ATTRIBUTION: self.attribution} + self._attr_unit_of_measurement = ( + self.sensor_type.unit_metric + if hass.config.units.is_metric + else self.sensor_type.unit_imperial + ) @property @abstractmethod @@ -123,27 +79,22 @@ class BaseClimaCellSensorEntity(ClimaCellEntity, SensorEntity): def state(self) -> str | int | float | None: """Return the state.""" state = self._state - if state and ATTR_SCALE in self.sensor_type: - state *= self.sensor_type[ATTR_SCALE] - if ( state is not None - and CONF_UNIT_SYSTEM_IMPERIAL in self.sensor_type - and CONF_UNIT_SYSTEM_METRIC in self.sensor_type - and ATTR_METRIC_CONVERSION in self.sensor_type - and ATTR_IS_METRIC_CHECK in self.sensor_type - and self.hass.config.units.is_metric - == self.sensor_type[ATTR_IS_METRIC_CHECK] + and self.sensor_type.unit_imperial is not None + and self.sensor_type.metric_conversion != 1.0 + and self.sensor_type.is_metric_check is not None + and self.hass.config.units.is_metric == self.sensor_type.is_metric_check ): - conversion = self.sensor_type[ATTR_METRIC_CONVERSION] + conversion = self.sensor_type.metric_conversion # When conversion is a callable, we assume it's a single input function if callable(conversion): return round(conversion(state), 4) return round(state * conversion, 4) - if ATTR_VALUE_MAP in self.sensor_type and state is not None: - return self.sensor_type[ATTR_VALUE_MAP](state).name.lower() + if self.sensor_type.value_map is not None and state is not None: + return self.sensor_type.value_map(state).name.lower() return state @@ -154,7 +105,7 @@ class ClimaCellSensorEntity(BaseClimaCellSensorEntity): @property def _state(self) -> str | int | float | None: """Return the raw state.""" - return self._get_current_property(self.sensor_type[ATTR_FIELD]) + return self._get_current_property(self.sensor_type.field) class ClimaCellV3SensorEntity(BaseClimaCellSensorEntity): @@ -164,5 +115,5 @@ class ClimaCellV3SensorEntity(BaseClimaCellSensorEntity): def _state(self) -> str | int | float | None: """Return the raw state.""" return self._get_cc_value( - self.coordinator.data[CURRENT], self.sensor_type[ATTR_FIELD] + self.coordinator.data[CURRENT], self.sensor_type.field ) diff --git a/tests/components/climacell/test_const.py b/tests/components/climacell/test_const.py new file mode 100644 index 00000000000..ba8eb6d8f39 --- /dev/null +++ b/tests/components/climacell/test_const.py @@ -0,0 +1,12 @@ +"""Tests for ClimaCell const.""" +import pytest + +from homeassistant.components.climacell.const import ClimaCellSensorMetadata +from homeassistant.const import TEMP_FAHRENHEIT + + +async def test_post_init(): + """Test post initiailization check for ClimaCellSensorMetadata.""" + + with pytest.raises(RuntimeError): + ClimaCellSensorMetadata("a", "b", unit_imperial=TEMP_FAHRENHEIT)