Add sensors for other ClimaCell data (#49259)
* Add sensors for other ClimaCell data * add tests and add rounding * docstrings * fix pressure * Update homeassistant/components/climacell/sensor.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update homeassistant/components/climacell/sensor.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * review comments * add another abstractmethod * use superscript * remove mypy ignore Co-authored-by: Martin Hjelmare <marhje52@gmail.com>pull/49274/head
parent
5fb36ad9e1
commit
898a1a17be
|
@ -16,6 +16,7 @@ from pyclimacell.exceptions import (
|
|||
UnknownException,
|
||||
)
|
||||
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||
from homeassistant.components.weather import DOMAIN as WEATHER_DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
|
@ -34,6 +35,7 @@ from homeassistant.helpers.update_coordinator import (
|
|||
)
|
||||
|
||||
from .const import (
|
||||
ATTR_FIELD,
|
||||
ATTRIBUTION,
|
||||
CC_ATTR_CLOUD_COVER,
|
||||
CC_ATTR_CONDITION,
|
||||
|
@ -50,6 +52,7 @@ from .const import (
|
|||
CC_ATTR_WIND_DIRECTION,
|
||||
CC_ATTR_WIND_GUST,
|
||||
CC_ATTR_WIND_SPEED,
|
||||
CC_SENSOR_TYPES,
|
||||
CC_V3_ATTR_CLOUD_COVER,
|
||||
CC_V3_ATTR_CONDITION,
|
||||
CC_V3_ATTR_HUMIDITY,
|
||||
|
@ -64,8 +67,8 @@ from .const import (
|
|||
CC_V3_ATTR_WIND_DIRECTION,
|
||||
CC_V3_ATTR_WIND_GUST,
|
||||
CC_V3_ATTR_WIND_SPEED,
|
||||
CC_V3_SENSOR_TYPES,
|
||||
CONF_TIMESTEP,
|
||||
DEFAULT_FORECAST_TYPE,
|
||||
DEFAULT_TIMESTEP,
|
||||
DOMAIN,
|
||||
MAX_REQUESTS_PER_DAY,
|
||||
|
@ -73,7 +76,7 @@ from .const import (
|
|||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORMS = [WEATHER_DOMAIN]
|
||||
PLATFORMS = [SENSOR_DOMAIN, WEATHER_DOMAIN]
|
||||
|
||||
|
||||
def _set_update_interval(
|
||||
|
@ -232,6 +235,10 @@ 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
|
||||
],
|
||||
]
|
||||
)
|
||||
data[FORECASTS][HOURLY] = await self._api.forecast_hourly(
|
||||
|
@ -288,6 +295,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],
|
||||
],
|
||||
[
|
||||
CC_ATTR_TEMPERATURE_LOW,
|
||||
|
@ -317,20 +325,22 @@ class ClimaCellEntity(CoordinatorEntity):
|
|||
self,
|
||||
config_entry: ConfigEntry,
|
||||
coordinator: ClimaCellDataUpdateCoordinator,
|
||||
forecast_type: str,
|
||||
api_version: int,
|
||||
) -> None:
|
||||
"""Initialize ClimaCell Entity."""
|
||||
super().__init__(coordinator)
|
||||
self.api_version = api_version
|
||||
self.forecast_type = forecast_type
|
||||
self._config_entry = config_entry
|
||||
|
||||
@staticmethod
|
||||
def _get_cc_value(
|
||||
weather_dict: dict[str, Any], key: str
|
||||
) -> int | float | str | None:
|
||||
"""Return property from weather_dict."""
|
||||
"""
|
||||
Return property from weather_dict.
|
||||
|
||||
Used for V3 API.
|
||||
"""
|
||||
items = weather_dict.get(key, {})
|
||||
# Handle cases where value returned is a list.
|
||||
# Optimistically find the best value to return.
|
||||
|
@ -347,23 +357,13 @@ class ClimaCellEntity(CoordinatorEntity):
|
|||
|
||||
return items.get("value")
|
||||
|
||||
@property
|
||||
def entity_registry_enabled_default(self) -> bool:
|
||||
"""Return if the entity should be enabled when first added to the entity registry."""
|
||||
if self.forecast_type == DEFAULT_FORECAST_TYPE:
|
||||
return True
|
||||
def _get_current_property(self, property_name: str) -> int | str | float | None:
|
||||
"""
|
||||
Get property from current conditions.
|
||||
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return the name of the entity."""
|
||||
return f"{self._config_entry.data[CONF_NAME]} - {self.forecast_type.title()}"
|
||||
|
||||
@property
|
||||
def unique_id(self) -> str:
|
||||
"""Return the unique id of the entity."""
|
||||
return f"{self._config_entry.unique_id}_{self.forecast_type}"
|
||||
Used for V4 API.
|
||||
"""
|
||||
return self.coordinator.data.get(CURRENT, {}).get(property_name)
|
||||
|
||||
@property
|
||||
def attribution(self):
|
||||
|
@ -377,6 +377,6 @@ class ClimaCellEntity(CoordinatorEntity):
|
|||
"identifiers": {(DOMAIN, self._config_entry.data[CONF_API_KEY])},
|
||||
"name": "ClimaCell",
|
||||
"manufacturer": "ClimaCell",
|
||||
"sw_version": "v3",
|
||||
"sw_version": f"v{self.api_version}",
|
||||
"entry_type": "service",
|
||||
}
|
||||
|
|
|
@ -1,5 +1,13 @@
|
|||
"""Constants for the ClimaCell integration."""
|
||||
from pyclimacell.const import DAILY, HOURLY, NOWCAST, WeatherCode
|
||||
from pyclimacell.const import (
|
||||
DAILY,
|
||||
HOURLY,
|
||||
NOWCAST,
|
||||
HealthConcernType,
|
||||
PollenIndex,
|
||||
PrimaryPollutantType,
|
||||
WeatherCode,
|
||||
)
|
||||
|
||||
from homeassistant.components.weather import (
|
||||
ATTR_CONDITION_CLEAR_NIGHT,
|
||||
|
@ -15,6 +23,15 @@ from homeassistant.components.weather import (
|
|||
ATTR_CONDITION_SUNNY,
|
||||
ATTR_CONDITION_WINDY,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
ATTR_NAME,
|
||||
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,
|
||||
)
|
||||
|
||||
CONF_TIMESTEP = "timestep"
|
||||
FORECAST_TYPES = [DAILY, HOURLY, NOWCAST]
|
||||
|
@ -35,6 +52,12 @@ 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"
|
||||
|
||||
# Additional attributes
|
||||
ATTR_WIND_GUST = "wind_gust"
|
||||
ATTR_CLOUD_COVER = "cloud_cover"
|
||||
|
@ -68,6 +91,7 @@ CONDITIONS = {
|
|||
WeatherCode.PARTLY_CLOUDY: ATTR_CONDITION_PARTLYCLOUDY,
|
||||
}
|
||||
|
||||
# Weather constants
|
||||
CC_ATTR_TIMESTAMP = "startTime"
|
||||
CC_ATTR_TEMPERATURE = "temperature"
|
||||
CC_ATTR_TEMPERATURE_HIGH = "temperatureMax"
|
||||
|
@ -85,6 +109,95 @@ CC_ATTR_WIND_GUST = "windGust"
|
|||
CC_ATTR_CLOUD_COVER = "cloudCover"
|
||||
CC_ATTR_PRECIPITATION_TYPE = "precipitationType"
|
||||
|
||||
# Sensor attributes
|
||||
CC_ATTR_PARTICULATE_MATTER_25 = "particulateMatter25"
|
||||
CC_ATTR_PARTICULATE_MATTER_10 = "particulateMatter10"
|
||||
CC_ATTR_NITROGEN_DIOXIDE = "pollutantNO2"
|
||||
CC_ATTR_CARBON_MONOXIDE = "pollutantCO"
|
||||
CC_ATTR_SULFUR_DIOXIDE = "pollutantSO2"
|
||||
CC_ATTR_EPA_AQI = "epaIndex"
|
||||
CC_ATTR_EPA_PRIMARY_POLLUTANT = "epaPrimaryPollutant"
|
||||
CC_ATTR_EPA_HEALTH_CONCERN = "epaHealthConcern"
|
||||
CC_ATTR_CHINA_AQI = "mepIndex"
|
||||
CC_ATTR_CHINA_PRIMARY_POLLUTANT = "mepPrimaryPollutant"
|
||||
CC_ATTR_CHINA_HEALTH_CONCERN = "mepHealthConcern"
|
||||
CC_ATTR_POLLEN_TREE = "treeIndex"
|
||||
CC_ATTR_POLLEN_WEED = "weedIndex"
|
||||
CC_ATTR_POLLEN_GRASS = "grassIndex"
|
||||
CC_ATTR_FIRE_INDEX = "fireIndex"
|
||||
|
||||
CC_SENSOR_TYPES = [
|
||||
{
|
||||
ATTR_FIELD: CC_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: True,
|
||||
},
|
||||
{
|
||||
ATTR_FIELD: CC_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: 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_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"},
|
||||
]
|
||||
|
||||
# V3 constants
|
||||
CONDITIONS_V3 = {
|
||||
"breezy": ATTR_CONDITION_WINDY,
|
||||
|
@ -111,6 +224,7 @@ CONDITIONS_V3 = {
|
|||
"partly_cloudy": ATTR_CONDITION_PARTLYCLOUDY,
|
||||
}
|
||||
|
||||
# Weather attributes
|
||||
CC_V3_ATTR_TIMESTAMP = "observation_time"
|
||||
CC_V3_ATTR_TEMPERATURE = "temp"
|
||||
CC_V3_ATTR_TEMPERATURE_HIGH = "max"
|
||||
|
@ -128,3 +242,73 @@ CC_V3_ATTR_PRECIPITATION_PROBABILITY = "precipitation_probability"
|
|||
CC_V3_ATTR_WIND_GUST = "wind_gust"
|
||||
CC_V3_ATTR_CLOUD_COVER = "cloud_cover"
|
||||
CC_V3_ATTR_PRECIPITATION_TYPE = "precipitation_type"
|
||||
|
||||
# Sensor attributes
|
||||
CC_V3_ATTR_PARTICULATE_MATTER_25 = "pm25"
|
||||
CC_V3_ATTR_PARTICULATE_MATTER_10 = "pm10"
|
||||
CC_V3_ATTR_NITROGEN_DIOXIDE = "no2"
|
||||
CC_V3_ATTR_CARBON_MONOXIDE = "co"
|
||||
CC_V3_ATTR_SULFUR_DIOXIDE = "so2"
|
||||
CC_V3_ATTR_EPA_AQI = "epa_aqi"
|
||||
CC_V3_ATTR_EPA_PRIMARY_POLLUTANT = "epa_primary_pollutant"
|
||||
CC_V3_ATTR_EPA_HEALTH_CONCERN = "epa_health_concern"
|
||||
CC_V3_ATTR_CHINA_AQI = "china_aqi"
|
||||
CC_V3_ATTR_CHINA_PRIMARY_POLLUTANT = "china_primary_pollutant"
|
||||
CC_V3_ATTR_CHINA_HEALTH_CONCERN = "china_health_concern"
|
||||
CC_V3_ATTR_POLLEN_TREE = "pollen_tree"
|
||||
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_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: 1 / (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: 1 / (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_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_FIELD: CC_V3_ATTR_POLLEN_WEED, ATTR_NAME: "Weed Pollen Index"},
|
||||
{ATTR_FIELD: CC_V3_ATTR_POLLEN_GRASS, ATTR_NAME: "Grass Pollen Index"},
|
||||
{ATTR_FIELD: CC_V3_ATTR_FIRE_INDEX, ATTR_NAME: "Fire Index"},
|
||||
]
|
||||
|
|
|
@ -0,0 +1,152 @@
|
|||
"""Sensor component that handles additional ClimaCell data for your location."""
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import abstractmethod
|
||||
import logging
|
||||
from typing import Any, Callable, Mapping
|
||||
|
||||
from pyclimacell.const import CURRENT
|
||||
|
||||
from homeassistant.components.sensor import SensorEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
ATTR_ATTRIBUTION,
|
||||
ATTR_NAME,
|
||||
CONF_API_VERSION,
|
||||
CONF_NAME,
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
CONF_UNIT_SYSTEM_IMPERIAL,
|
||||
CONF_UNIT_SYSTEM_METRIC,
|
||||
)
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.typing import HomeAssistantType
|
||||
from homeassistant.util import slugify
|
||||
|
||||
from . import ClimaCellDataUpdateCoordinator, ClimaCellEntity
|
||||
from .const import (
|
||||
ATTR_FIELD,
|
||||
ATTR_IS_METRIC_CHECK,
|
||||
ATTR_METRIC_CONVERSION,
|
||||
ATTR_VALUE_MAP,
|
||||
CC_SENSOR_TYPES,
|
||||
CC_V3_SENSOR_TYPES,
|
||||
DOMAIN,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistantType,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: Callable[[list[Entity], bool], None],
|
||||
) -> None:
|
||||
"""Set up a config entry."""
|
||||
coordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||
api_version = config_entry.data[CONF_API_VERSION]
|
||||
|
||||
if api_version == 3:
|
||||
api_class = ClimaCellV3SensorEntity
|
||||
sensor_types = CC_V3_SENSOR_TYPES
|
||||
else:
|
||||
api_class = ClimaCellSensorEntity
|
||||
sensor_types = CC_SENSOR_TYPES
|
||||
entities = [
|
||||
api_class(config_entry, coordinator, api_version, sensor_type)
|
||||
for sensor_type in sensor_types
|
||||
]
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class BaseClimaCellSensorEntity(ClimaCellEntity, SensorEntity):
|
||||
"""Base ClimaCell sensor entity."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config_entry: ConfigEntry,
|
||||
coordinator: ClimaCellDataUpdateCoordinator,
|
||||
api_version: int,
|
||||
sensor_type: dict[str, str | float],
|
||||
) -> None:
|
||||
"""Initialize ClimaCell Sensor Entity."""
|
||||
super().__init__(config_entry, coordinator, api_version)
|
||||
self.sensor_type = sensor_type
|
||||
|
||||
@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
|
||||
):
|
||||
if self.hass.config.units.is_metric:
|
||||
return self.sensor_type[CONF_UNIT_SYSTEM_METRIC]
|
||||
return self.sensor_type[CONF_UNIT_SYSTEM_IMPERIAL]
|
||||
|
||||
return None
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def _state(self) -> str | int | float | None:
|
||||
"""Return the raw state."""
|
||||
|
||||
@property
|
||||
def state(self) -> str | int | float | None:
|
||||
"""Return the state."""
|
||||
if (
|
||||
self._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]
|
||||
):
|
||||
return round(self._state * self.sensor_type[ATTR_METRIC_CONVERSION], 4)
|
||||
|
||||
if ATTR_VALUE_MAP in self.sensor_type:
|
||||
return self.sensor_type[ATTR_VALUE_MAP](self._state).name.lower()
|
||||
return self._state
|
||||
|
||||
|
||||
class ClimaCellSensorEntity(BaseClimaCellSensorEntity):
|
||||
"""Sensor entity that talks to ClimaCell v4 API to retrieve non-weather data."""
|
||||
|
||||
@property
|
||||
def _state(self) -> str | int | float | None:
|
||||
"""Return the raw state."""
|
||||
return self._get_current_property(self.sensor_type[ATTR_FIELD])
|
||||
|
||||
|
||||
class ClimaCellV3SensorEntity(BaseClimaCellSensorEntity):
|
||||
"""Sensor entity that talks to ClimaCell v3 API to retrieve non-weather data."""
|
||||
|
||||
@property
|
||||
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]
|
||||
)
|
|
@ -1,6 +1,7 @@
|
|||
"""Weather component that handles meteorological data for your location."""
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import abstractmethod
|
||||
from datetime import datetime
|
||||
import logging
|
||||
from typing import Any, Callable, Mapping
|
||||
|
@ -29,6 +30,7 @@ from homeassistant.components.weather import (
|
|||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_API_VERSION,
|
||||
CONF_NAME,
|
||||
LENGTH_FEET,
|
||||
LENGTH_KILOMETERS,
|
||||
LENGTH_METERS,
|
||||
|
@ -44,7 +46,7 @@ from homeassistant.util import dt as dt_util
|
|||
from homeassistant.util.distance import convert as distance_convert
|
||||
from homeassistant.util.pressure import convert as pressure_convert
|
||||
|
||||
from . import ClimaCellEntity
|
||||
from . import ClimaCellDataUpdateCoordinator, ClimaCellEntity
|
||||
from .const import (
|
||||
ATTR_CLOUD_COVER,
|
||||
ATTR_PRECIPITATION_TYPE,
|
||||
|
@ -86,12 +88,11 @@ from .const import (
|
|||
CONDITIONS,
|
||||
CONDITIONS_V3,
|
||||
CONF_TIMESTEP,
|
||||
DEFAULT_FORECAST_TYPE,
|
||||
DOMAIN,
|
||||
MAX_FORECASTS,
|
||||
)
|
||||
|
||||
# mypy: allow-untyped-defs, no-check-untyped-defs
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
@ -106,7 +107,7 @@ async def async_setup_entry(
|
|||
|
||||
api_class = ClimaCellV3WeatherEntity if api_version == 3 else ClimaCellWeatherEntity
|
||||
entities = [
|
||||
api_class(config_entry, coordinator, forecast_type, api_version)
|
||||
api_class(config_entry, coordinator, api_version, forecast_type)
|
||||
for forecast_type in [DAILY, HOURLY, NOWCAST]
|
||||
]
|
||||
async_add_entities(entities)
|
||||
|
@ -115,12 +116,41 @@ async def async_setup_entry(
|
|||
class BaseClimaCellWeatherEntity(ClimaCellEntity, WeatherEntity):
|
||||
"""Base ClimaCell weather entity."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config_entry: ConfigEntry,
|
||||
coordinator: ClimaCellDataUpdateCoordinator,
|
||||
api_version: int,
|
||||
forecast_type: str,
|
||||
) -> None:
|
||||
"""Initialize ClimaCell Weather Entity."""
|
||||
super().__init__(config_entry, coordinator, api_version)
|
||||
self.forecast_type = forecast_type
|
||||
|
||||
@property
|
||||
def entity_registry_enabled_default(self) -> bool:
|
||||
"""Return if the entity should be enabled when first added to the entity registry."""
|
||||
if self.forecast_type == DEFAULT_FORECAST_TYPE:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return the name of the entity."""
|
||||
return f"{self._config_entry.data[CONF_NAME]} - {self.forecast_type.title()}"
|
||||
|
||||
@property
|
||||
def unique_id(self) -> str:
|
||||
"""Return the unique id of the entity."""
|
||||
return f"{self._config_entry.unique_id}_{self.forecast_type}"
|
||||
|
||||
@staticmethod
|
||||
@abstractmethod
|
||||
def _translate_condition(
|
||||
condition: int | None, sun_is_up: bool = True
|
||||
) -> str | None:
|
||||
"""Translate ClimaCell condition into an HA condition."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def _forecast_dict(
|
||||
self,
|
||||
|
@ -144,13 +174,14 @@ class BaseClimaCellWeatherEntity(ClimaCellEntity, WeatherEntity):
|
|||
|
||||
if self.hass.config.units.is_metric:
|
||||
if precipitation:
|
||||
precipitation = (
|
||||
precipitation = round(
|
||||
distance_convert(precipitation / 12, LENGTH_FEET, LENGTH_METERS)
|
||||
* 1000
|
||||
* 1000,
|
||||
4,
|
||||
)
|
||||
if wind_speed:
|
||||
wind_speed = distance_convert(
|
||||
wind_speed, LENGTH_MILES, LENGTH_KILOMETERS
|
||||
wind_speed = round(
|
||||
distance_convert(wind_speed, LENGTH_MILES, LENGTH_KILOMETERS), 4
|
||||
)
|
||||
|
||||
data = {
|
||||
|
@ -171,8 +202,8 @@ class BaseClimaCellWeatherEntity(ClimaCellEntity, WeatherEntity):
|
|||
"""Return additional state attributes."""
|
||||
wind_gust = self.wind_gust
|
||||
if wind_gust and self.hass.config.units.is_metric:
|
||||
wind_gust = distance_convert(
|
||||
self.wind_gust, LENGTH_MILES, LENGTH_KILOMETERS
|
||||
wind_gust = round(
|
||||
distance_convert(self.wind_gust, LENGTH_MILES, LENGTH_KILOMETERS), 4
|
||||
)
|
||||
cloud_cover = self.cloud_cover
|
||||
if cloud_cover is not None:
|
||||
|
@ -184,19 +215,61 @@ class BaseClimaCellWeatherEntity(ClimaCellEntity, WeatherEntity):
|
|||
}
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def cloud_cover(self):
|
||||
"""Return cloud cover."""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def wind_gust(self):
|
||||
"""Return wind gust speed."""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def precipitation_type(self):
|
||||
"""Return precipitation type."""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def _pressure(self):
|
||||
"""Return the raw pressure."""
|
||||
|
||||
@property
|
||||
def pressure(self):
|
||||
"""Return the pressure."""
|
||||
if self.hass.config.units.is_metric and self._pressure:
|
||||
return round(
|
||||
pressure_convert(self._pressure, PRESSURE_INHG, PRESSURE_HPA), 4
|
||||
)
|
||||
return self._pressure
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def _wind_speed(self):
|
||||
"""Return the raw wind speed."""
|
||||
|
||||
@property
|
||||
def wind_speed(self):
|
||||
"""Return the wind speed."""
|
||||
if self.hass.config.units.is_metric and self._wind_speed:
|
||||
return round(
|
||||
distance_convert(self._wind_speed, LENGTH_MILES, LENGTH_KILOMETERS), 4
|
||||
)
|
||||
return self._wind_speed
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def _visibility(self):
|
||||
"""Return the raw visibility."""
|
||||
|
||||
@property
|
||||
def visibility(self):
|
||||
"""Return the visibility."""
|
||||
if self.hass.config.units.is_metric and self._visibility:
|
||||
return round(
|
||||
distance_convert(self._visibility, LENGTH_MILES, LENGTH_KILOMETERS), 4
|
||||
)
|
||||
return self._visibility
|
||||
|
||||
|
||||
class ClimaCellWeatherEntity(BaseClimaCellWeatherEntity):
|
||||
|
@ -217,10 +290,6 @@ class ClimaCellWeatherEntity(BaseClimaCellWeatherEntity):
|
|||
return CLEAR_CONDITIONS["night"]
|
||||
return CONDITIONS[condition]
|
||||
|
||||
def _get_current_property(self, property_name: str) -> int | str | float | None:
|
||||
"""Get property from current conditions."""
|
||||
return self.coordinator.data.get(CURRENT, {}).get(property_name)
|
||||
|
||||
@property
|
||||
def temperature(self):
|
||||
"""Return the platform temperature."""
|
||||
|
@ -232,12 +301,9 @@ class ClimaCellWeatherEntity(BaseClimaCellWeatherEntity):
|
|||
return TEMP_FAHRENHEIT
|
||||
|
||||
@property
|
||||
def pressure(self):
|
||||
"""Return the pressure."""
|
||||
pressure = self._get_current_property(CC_ATTR_PRESSURE)
|
||||
if self.hass.config.units.is_metric and pressure:
|
||||
return pressure_convert(pressure, PRESSURE_INHG, PRESSURE_HPA)
|
||||
return pressure
|
||||
def _pressure(self):
|
||||
"""Return the raw pressure."""
|
||||
return self._get_current_property(CC_ATTR_PRESSURE)
|
||||
|
||||
@property
|
||||
def humidity(self):
|
||||
|
@ -263,12 +329,9 @@ class ClimaCellWeatherEntity(BaseClimaCellWeatherEntity):
|
|||
return PrecipitationType(precipitation_type).name.lower()
|
||||
|
||||
@property
|
||||
def wind_speed(self):
|
||||
"""Return the wind speed."""
|
||||
wind_speed = self._get_current_property(CC_ATTR_WIND_SPEED)
|
||||
if self.hass.config.units.is_metric and wind_speed:
|
||||
return distance_convert(wind_speed, LENGTH_MILES, LENGTH_KILOMETERS)
|
||||
return wind_speed
|
||||
def _wind_speed(self):
|
||||
"""Return the raw wind speed."""
|
||||
return self._get_current_property(CC_ATTR_WIND_SPEED)
|
||||
|
||||
@property
|
||||
def wind_bearing(self):
|
||||
|
@ -289,12 +352,9 @@ class ClimaCellWeatherEntity(BaseClimaCellWeatherEntity):
|
|||
)
|
||||
|
||||
@property
|
||||
def visibility(self):
|
||||
"""Return the visibility."""
|
||||
visibility = self._get_current_property(CC_ATTR_VISIBILITY)
|
||||
if self.hass.config.units.is_metric and visibility:
|
||||
return distance_convert(visibility, LENGTH_MILES, LENGTH_KILOMETERS)
|
||||
return visibility
|
||||
def _visibility(self):
|
||||
"""Return the raw visibility."""
|
||||
return self._get_current_property(CC_ATTR_VISIBILITY)
|
||||
|
||||
@property
|
||||
def forecast(self):
|
||||
|
@ -391,14 +451,9 @@ class ClimaCellV3WeatherEntity(BaseClimaCellWeatherEntity):
|
|||
return TEMP_FAHRENHEIT
|
||||
|
||||
@property
|
||||
def pressure(self):
|
||||
"""Return the pressure."""
|
||||
pressure = self._get_cc_value(
|
||||
self.coordinator.data[CURRENT], CC_V3_ATTR_PRESSURE
|
||||
)
|
||||
if self.hass.config.units.is_metric and pressure:
|
||||
return pressure_convert(pressure, PRESSURE_INHG, PRESSURE_HPA)
|
||||
return pressure
|
||||
def _pressure(self):
|
||||
"""Return the raw pressure."""
|
||||
return self._get_cc_value(self.coordinator.data[CURRENT], CC_V3_ATTR_PRESSURE)
|
||||
|
||||
@property
|
||||
def humidity(self):
|
||||
|
@ -425,14 +480,9 @@ class ClimaCellV3WeatherEntity(BaseClimaCellWeatherEntity):
|
|||
)
|
||||
|
||||
@property
|
||||
def wind_speed(self):
|
||||
"""Return the wind speed."""
|
||||
wind_speed = self._get_cc_value(
|
||||
self.coordinator.data[CURRENT], CC_V3_ATTR_WIND_SPEED
|
||||
)
|
||||
if self.hass.config.units.is_metric and wind_speed:
|
||||
return distance_convert(wind_speed, LENGTH_MILES, LENGTH_KILOMETERS)
|
||||
return wind_speed
|
||||
def _wind_speed(self):
|
||||
"""Return the raw wind speed."""
|
||||
return self._get_cc_value(self.coordinator.data[CURRENT], CC_V3_ATTR_WIND_SPEED)
|
||||
|
||||
@property
|
||||
def wind_bearing(self):
|
||||
|
@ -455,14 +505,9 @@ class ClimaCellV3WeatherEntity(BaseClimaCellWeatherEntity):
|
|||
)
|
||||
|
||||
@property
|
||||
def visibility(self):
|
||||
"""Return the visibility."""
|
||||
visibility = self._get_cc_value(
|
||||
self.coordinator.data[CURRENT], CC_V3_ATTR_VISIBILITY
|
||||
)
|
||||
if self.hass.config.units.is_metric and visibility:
|
||||
return distance_convert(visibility, LENGTH_MILES, LENGTH_KILOMETERS)
|
||||
return visibility
|
||||
def _visibility(self):
|
||||
"""Return the raw visibility."""
|
||||
return self._get_cc_value(self.coordinator.data[CURRENT], CC_V3_ATTR_VISIBILITY)
|
||||
|
||||
@property
|
||||
def forecast(self):
|
||||
|
|
|
@ -0,0 +1,148 @@
|
|||
"""Tests for Climacell sensor entities."""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
import logging
|
||||
from typing import Any
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
import pytz
|
||||
|
||||
from homeassistant.components.climacell.config_flow import (
|
||||
_get_config_schema,
|
||||
_get_unique_id,
|
||||
)
|
||||
from homeassistant.components.climacell.const import ATTRIBUTION, DOMAIN
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||
from homeassistant.const import ATTR_ATTRIBUTION
|
||||
from homeassistant.core import State, callback
|
||||
from homeassistant.helpers.entity_registry import async_get
|
||||
from homeassistant.helpers.typing import HomeAssistantType
|
||||
|
||||
from .const import API_V3_ENTRY_DATA, API_V4_ENTRY_DATA
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
CC_SENSOR_ENTITY_ID = "sensor.climacell_{}"
|
||||
|
||||
CO = "carbon_monoxide"
|
||||
NO2 = "nitrogen_dioxide"
|
||||
SO2 = "sulfur_dioxide"
|
||||
PM25 = "particulate_matter_2_5_mm"
|
||||
PM10 = "particulate_matter_10_mm"
|
||||
MEP_AQI = "china_mep_air_quality_index"
|
||||
MEP_HEALTH_CONCERN = "china_mep_health_concern"
|
||||
MEP_PRIMARY_POLLUTANT = "china_mep_primary_pollutant"
|
||||
EPA_AQI = "us_epa_air_quality_index"
|
||||
EPA_HEALTH_CONCERN = "us_epa_health_concern"
|
||||
EPA_PRIMARY_POLLUTANT = "us_epa_primary_pollutant"
|
||||
FIRE_INDEX = "fire_index"
|
||||
GRASS_POLLEN = "grass_pollen_index"
|
||||
WEED_POLLEN = "weed_pollen_index"
|
||||
TREE_POLLEN = "tree_pollen_index"
|
||||
|
||||
|
||||
@callback
|
||||
def _enable_entity(hass: HomeAssistantType, entity_name: str) -> None:
|
||||
"""Enable disabled entity."""
|
||||
ent_reg = async_get(hass)
|
||||
entry = ent_reg.async_get(entity_name)
|
||||
updated_entry = ent_reg.async_update_entity(
|
||||
entry.entity_id, **{"disabled_by": None}
|
||||
)
|
||||
assert updated_entry != entry
|
||||
assert updated_entry.disabled is False
|
||||
|
||||
|
||||
async def _setup(hass: HomeAssistantType, config: dict[str, Any]) -> State:
|
||||
"""Set up entry and return entity state."""
|
||||
with patch(
|
||||
"homeassistant.util.dt.utcnow",
|
||||
return_value=datetime(2021, 3, 6, 23, 59, 59, tzinfo=pytz.UTC),
|
||||
):
|
||||
data = _get_config_schema(hass)(config)
|
||||
config_entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data=data,
|
||||
unique_id=_get_unique_id(hass, data),
|
||||
version=1,
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
for entity_name in (
|
||||
CO,
|
||||
NO2,
|
||||
SO2,
|
||||
PM25,
|
||||
PM10,
|
||||
MEP_AQI,
|
||||
MEP_HEALTH_CONCERN,
|
||||
MEP_PRIMARY_POLLUTANT,
|
||||
EPA_AQI,
|
||||
EPA_HEALTH_CONCERN,
|
||||
EPA_PRIMARY_POLLUTANT,
|
||||
FIRE_INDEX,
|
||||
GRASS_POLLEN,
|
||||
WEED_POLLEN,
|
||||
TREE_POLLEN,
|
||||
):
|
||||
_enable_entity(hass, CC_SENSOR_ENTITY_ID.format(entity_name))
|
||||
await hass.async_block_till_done()
|
||||
assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 15
|
||||
|
||||
|
||||
def check_sensor_state(hass: HomeAssistantType, entity_name: str, value: str):
|
||||
"""Check the state of a ClimaCell sensor."""
|
||||
state = hass.states.get(CC_SENSOR_ENTITY_ID.format(entity_name))
|
||||
assert state
|
||||
assert state.state == value
|
||||
assert state.attributes[ATTR_ATTRIBUTION] == ATTRIBUTION
|
||||
|
||||
|
||||
async def test_v3_sensor(
|
||||
hass: HomeAssistantType,
|
||||
climacell_config_entry_update: pytest.fixture,
|
||||
) -> None:
|
||||
"""Test v3 sensor data."""
|
||||
await _setup(hass, API_V3_ENTRY_DATA)
|
||||
check_sensor_state(hass, CO, "0.875")
|
||||
check_sensor_state(hass, NO2, "14.1875")
|
||||
check_sensor_state(hass, SO2, "2")
|
||||
check_sensor_state(hass, PM25, "5.3125")
|
||||
check_sensor_state(hass, PM10, "27")
|
||||
check_sensor_state(hass, MEP_AQI, "27")
|
||||
check_sensor_state(hass, MEP_HEALTH_CONCERN, "Good")
|
||||
check_sensor_state(hass, MEP_PRIMARY_POLLUTANT, "pm10")
|
||||
check_sensor_state(hass, EPA_AQI, "22.3125")
|
||||
check_sensor_state(hass, EPA_HEALTH_CONCERN, "Good")
|
||||
check_sensor_state(hass, EPA_PRIMARY_POLLUTANT, "pm25")
|
||||
check_sensor_state(hass, FIRE_INDEX, "9")
|
||||
check_sensor_state(hass, GRASS_POLLEN, "0")
|
||||
check_sensor_state(hass, WEED_POLLEN, "0")
|
||||
check_sensor_state(hass, TREE_POLLEN, "0")
|
||||
|
||||
|
||||
async def test_v4_sensor(
|
||||
hass: HomeAssistantType,
|
||||
climacell_config_entry_update: pytest.fixture,
|
||||
) -> None:
|
||||
"""Test v4 sensor data."""
|
||||
await _setup(hass, API_V4_ENTRY_DATA)
|
||||
check_sensor_state(hass, CO, "0.63")
|
||||
check_sensor_state(hass, NO2, "10.67")
|
||||
check_sensor_state(hass, SO2, "1.65")
|
||||
check_sensor_state(hass, PM25, "5.2972")
|
||||
check_sensor_state(hass, PM10, "20.1294")
|
||||
check_sensor_state(hass, MEP_AQI, "23")
|
||||
check_sensor_state(hass, MEP_HEALTH_CONCERN, "good")
|
||||
check_sensor_state(hass, MEP_PRIMARY_POLLUTANT, "pm10")
|
||||
check_sensor_state(hass, EPA_AQI, "24")
|
||||
check_sensor_state(hass, EPA_HEALTH_CONCERN, "good")
|
||||
check_sensor_state(hass, EPA_PRIMARY_POLLUTANT, "pm25")
|
||||
check_sensor_state(hass, FIRE_INDEX, "10")
|
||||
check_sensor_state(hass, GRASS_POLLEN, "none")
|
||||
check_sensor_state(hass, WEED_POLLEN, "none")
|
||||
check_sensor_state(hass, TREE_POLLEN, "none")
|
|
@ -44,7 +44,7 @@ from homeassistant.components.weather import (
|
|||
DOMAIN as WEATHER_DOMAIN,
|
||||
)
|
||||
from homeassistant.const import ATTR_ATTRIBUTION, ATTR_FRIENDLY_NAME
|
||||
from homeassistant.core import State
|
||||
from homeassistant.core import State, callback
|
||||
from homeassistant.helpers.entity_registry import async_get
|
||||
from homeassistant.helpers.typing import HomeAssistantType
|
||||
|
||||
|
@ -55,7 +55,8 @@ from tests.common import MockConfigEntry
|
|||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def _enable_entity(hass: HomeAssistantType, entity_name: str) -> None:
|
||||
@callback
|
||||
def _enable_entity(hass: HomeAssistantType, entity_name: str) -> None:
|
||||
"""Enable disabled entity."""
|
||||
ent_reg = async_get(hass)
|
||||
entry = ent_reg.async_get(entity_name)
|
||||
|
@ -82,8 +83,8 @@ async def _setup(hass: HomeAssistantType, config: dict[str, Any]) -> State:
|
|||
config_entry.add_to_hass(hass)
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
await _enable_entity(hass, "weather.climacell_hourly")
|
||||
await _enable_entity(hass, "weather.climacell_nowcast")
|
||||
for entity_name in ("hourly", "nowcast"):
|
||||
_enable_entity(hass, f"weather.climacell_{entity_name}")
|
||||
await hass.async_block_till_done()
|
||||
assert len(hass.states.async_entity_ids(WEATHER_DOMAIN)) == 3
|
||||
|
||||
|
@ -142,7 +143,7 @@ async def test_v3_weather(
|
|||
{
|
||||
ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY,
|
||||
ATTR_FORECAST_TIME: "2021-03-12T00:00:00+00:00",
|
||||
ATTR_FORECAST_PRECIPITATION: 0.04572,
|
||||
ATTR_FORECAST_PRECIPITATION: 0.0457,
|
||||
ATTR_FORECAST_PRECIPITATION_PROBABILITY: 25,
|
||||
ATTR_FORECAST_TEMP: 20,
|
||||
ATTR_FORECAST_TEMP_LOW: 12,
|
||||
|
@ -158,7 +159,7 @@ async def test_v3_weather(
|
|||
{
|
||||
ATTR_FORECAST_CONDITION: ATTR_CONDITION_RAINY,
|
||||
ATTR_FORECAST_TIME: "2021-03-14T00:00:00+00:00",
|
||||
ATTR_FORECAST_PRECIPITATION: 1.07442,
|
||||
ATTR_FORECAST_PRECIPITATION: 1.0744,
|
||||
ATTR_FORECAST_PRECIPITATION_PROBABILITY: 75,
|
||||
ATTR_FORECAST_TEMP: 6,
|
||||
ATTR_FORECAST_TEMP_LOW: 3,
|
||||
|
@ -166,7 +167,7 @@ async def test_v3_weather(
|
|||
{
|
||||
ATTR_FORECAST_CONDITION: ATTR_CONDITION_SNOWY,
|
||||
ATTR_FORECAST_TIME: "2021-03-15T00:00:00+00:00",
|
||||
ATTR_FORECAST_PRECIPITATION: 7.305040000000001,
|
||||
ATTR_FORECAST_PRECIPITATION: 7.3050,
|
||||
ATTR_FORECAST_PRECIPITATION_PROBABILITY: 95,
|
||||
ATTR_FORECAST_TEMP: 1,
|
||||
ATTR_FORECAST_TEMP_LOW: 0,
|
||||
|
@ -174,7 +175,7 @@ async def test_v3_weather(
|
|||
{
|
||||
ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY,
|
||||
ATTR_FORECAST_TIME: "2021-03-16T00:00:00+00:00",
|
||||
ATTR_FORECAST_PRECIPITATION: 0.00508,
|
||||
ATTR_FORECAST_PRECIPITATION: 0.0051,
|
||||
ATTR_FORECAST_PRECIPITATION_PROBABILITY: 5,
|
||||
ATTR_FORECAST_TEMP: 6,
|
||||
ATTR_FORECAST_TEMP_LOW: -2,
|
||||
|
@ -214,7 +215,7 @@ async def test_v3_weather(
|
|||
{
|
||||
ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY,
|
||||
ATTR_FORECAST_TIME: "2021-03-21T00:00:00+00:00",
|
||||
ATTR_FORECAST_PRECIPITATION: 0.043179999999999996,
|
||||
ATTR_FORECAST_PRECIPITATION: 0.0432,
|
||||
ATTR_FORECAST_PRECIPITATION_PROBABILITY: 20,
|
||||
ATTR_FORECAST_TEMP: 7,
|
||||
ATTR_FORECAST_TEMP_LOW: 1,
|
||||
|
@ -223,13 +224,13 @@ async def test_v3_weather(
|
|||
assert weather_state.attributes[ATTR_FRIENDLY_NAME] == "ClimaCell - Daily"
|
||||
assert weather_state.attributes[ATTR_WEATHER_HUMIDITY] == 24
|
||||
assert weather_state.attributes[ATTR_WEATHER_OZONE] == 52.625
|
||||
assert weather_state.attributes[ATTR_WEATHER_PRESSURE] == 1028.124632345
|
||||
assert weather_state.attributes[ATTR_WEATHER_PRESSURE] == 1028.1246
|
||||
assert weather_state.attributes[ATTR_WEATHER_TEMPERATURE] == 7
|
||||
assert weather_state.attributes[ATTR_WEATHER_VISIBILITY] == 9.994026240000002
|
||||
assert weather_state.attributes[ATTR_WEATHER_VISIBILITY] == 9.9940
|
||||
assert weather_state.attributes[ATTR_WEATHER_WIND_BEARING] == 320.31
|
||||
assert weather_state.attributes[ATTR_WEATHER_WIND_SPEED] == 14.62893696
|
||||
assert weather_state.attributes[ATTR_WEATHER_WIND_SPEED] == 14.6289
|
||||
assert weather_state.attributes[ATTR_CLOUD_COVER] == 1
|
||||
assert weather_state.attributes[ATTR_WIND_GUST] == 24.075786240000003
|
||||
assert weather_state.attributes[ATTR_WIND_GUST] == 24.0758
|
||||
assert weather_state.attributes[ATTR_PRECIPITATION_TYPE] == "rain"
|
||||
|
||||
|
||||
|
@ -250,7 +251,7 @@ async def test_v4_weather(
|
|||
ATTR_FORECAST_TEMP: 8,
|
||||
ATTR_FORECAST_TEMP_LOW: -3,
|
||||
ATTR_FORECAST_WIND_BEARING: 239.6,
|
||||
ATTR_FORECAST_WIND_SPEED: 15.272674560000002,
|
||||
ATTR_FORECAST_WIND_SPEED: 15.2727,
|
||||
},
|
||||
{
|
||||
ATTR_FORECAST_CONDITION: "cloudy",
|
||||
|
@ -260,7 +261,7 @@ async def test_v4_weather(
|
|||
ATTR_FORECAST_TEMP: 10,
|
||||
ATTR_FORECAST_TEMP_LOW: -3,
|
||||
ATTR_FORECAST_WIND_BEARING: 262.82,
|
||||
ATTR_FORECAST_WIND_SPEED: 11.65165056,
|
||||
ATTR_FORECAST_WIND_SPEED: 11.6517,
|
||||
},
|
||||
{
|
||||
ATTR_FORECAST_CONDITION: "cloudy",
|
||||
|
@ -270,7 +271,7 @@ async def test_v4_weather(
|
|||
ATTR_FORECAST_TEMP: 19,
|
||||
ATTR_FORECAST_TEMP_LOW: 0,
|
||||
ATTR_FORECAST_WIND_BEARING: 229.3,
|
||||
ATTR_FORECAST_WIND_SPEED: 11.3458752,
|
||||
ATTR_FORECAST_WIND_SPEED: 11.3459,
|
||||
},
|
||||
{
|
||||
ATTR_FORECAST_CONDITION: "cloudy",
|
||||
|
@ -280,7 +281,7 @@ async def test_v4_weather(
|
|||
ATTR_FORECAST_TEMP: 18,
|
||||
ATTR_FORECAST_TEMP_LOW: 3,
|
||||
ATTR_FORECAST_WIND_BEARING: 149.91,
|
||||
ATTR_FORECAST_WIND_SPEED: 17.123420160000002,
|
||||
ATTR_FORECAST_WIND_SPEED: 17.1234,
|
||||
},
|
||||
{
|
||||
ATTR_FORECAST_CONDITION: "cloudy",
|
||||
|
@ -290,17 +291,17 @@ async def test_v4_weather(
|
|||
ATTR_FORECAST_TEMP: 19,
|
||||
ATTR_FORECAST_TEMP_LOW: 9,
|
||||
ATTR_FORECAST_WIND_BEARING: 210.45,
|
||||
ATTR_FORECAST_WIND_SPEED: 25.250607360000004,
|
||||
ATTR_FORECAST_WIND_SPEED: 25.2506,
|
||||
},
|
||||
{
|
||||
ATTR_FORECAST_CONDITION: "rainy",
|
||||
ATTR_FORECAST_TIME: "2021-03-12T11:00:00+00:00",
|
||||
ATTR_FORECAST_PRECIPITATION: 0.12192000000000001,
|
||||
ATTR_FORECAST_PRECIPITATION: 0.1219,
|
||||
ATTR_FORECAST_PRECIPITATION_PROBABILITY: 25,
|
||||
ATTR_FORECAST_TEMP: 20,
|
||||
ATTR_FORECAST_TEMP_LOW: 12,
|
||||
ATTR_FORECAST_WIND_BEARING: 217.98,
|
||||
ATTR_FORECAST_WIND_SPEED: 19.794931200000004,
|
||||
ATTR_FORECAST_WIND_SPEED: 19.7949,
|
||||
},
|
||||
{
|
||||
ATTR_FORECAST_CONDITION: "cloudy",
|
||||
|
@ -310,27 +311,27 @@ async def test_v4_weather(
|
|||
ATTR_FORECAST_TEMP: 12,
|
||||
ATTR_FORECAST_TEMP_LOW: 6,
|
||||
ATTR_FORECAST_WIND_BEARING: 58.79,
|
||||
ATTR_FORECAST_WIND_SPEED: 15.642823680000001,
|
||||
ATTR_FORECAST_WIND_SPEED: 15.6428,
|
||||
},
|
||||
{
|
||||
ATTR_FORECAST_CONDITION: "snowy",
|
||||
ATTR_FORECAST_TIME: "2021-03-14T10:00:00+00:00",
|
||||
ATTR_FORECAST_PRECIPITATION: 23.95728,
|
||||
ATTR_FORECAST_PRECIPITATION: 23.9573,
|
||||
ATTR_FORECAST_PRECIPITATION_PROBABILITY: 95,
|
||||
ATTR_FORECAST_TEMP: 6,
|
||||
ATTR_FORECAST_TEMP_LOW: 1,
|
||||
ATTR_FORECAST_WIND_BEARING: 70.25,
|
||||
ATTR_FORECAST_WIND_SPEED: 26.15184,
|
||||
ATTR_FORECAST_WIND_SPEED: 26.1518,
|
||||
},
|
||||
{
|
||||
ATTR_FORECAST_CONDITION: "snowy",
|
||||
ATTR_FORECAST_TIME: "2021-03-15T10:00:00+00:00",
|
||||
ATTR_FORECAST_PRECIPITATION: 1.46304,
|
||||
ATTR_FORECAST_PRECIPITATION: 1.4630,
|
||||
ATTR_FORECAST_PRECIPITATION_PROBABILITY: 55,
|
||||
ATTR_FORECAST_TEMP: 6,
|
||||
ATTR_FORECAST_TEMP_LOW: -1,
|
||||
ATTR_FORECAST_WIND_BEARING: 84.47,
|
||||
ATTR_FORECAST_WIND_SPEED: 25.57247616,
|
||||
ATTR_FORECAST_WIND_SPEED: 25.5725,
|
||||
},
|
||||
{
|
||||
ATTR_FORECAST_CONDITION: "cloudy",
|
||||
|
@ -340,7 +341,7 @@ async def test_v4_weather(
|
|||
ATTR_FORECAST_TEMP: 6,
|
||||
ATTR_FORECAST_TEMP_LOW: -2,
|
||||
ATTR_FORECAST_WIND_BEARING: 103.85,
|
||||
ATTR_FORECAST_WIND_SPEED: 10.79869824,
|
||||
ATTR_FORECAST_WIND_SPEED: 10.7987,
|
||||
},
|
||||
{
|
||||
ATTR_FORECAST_CONDITION: "cloudy",
|
||||
|
@ -350,7 +351,7 @@ async def test_v4_weather(
|
|||
ATTR_FORECAST_TEMP: 11,
|
||||
ATTR_FORECAST_TEMP_LOW: 1,
|
||||
ATTR_FORECAST_WIND_BEARING: 145.41,
|
||||
ATTR_FORECAST_WIND_SPEED: 11.69993088,
|
||||
ATTR_FORECAST_WIND_SPEED: 11.6999,
|
||||
},
|
||||
{
|
||||
ATTR_FORECAST_CONDITION: "cloudy",
|
||||
|
@ -360,17 +361,17 @@ async def test_v4_weather(
|
|||
ATTR_FORECAST_TEMP: 12,
|
||||
ATTR_FORECAST_TEMP_LOW: 5,
|
||||
ATTR_FORECAST_WIND_BEARING: 62.99,
|
||||
ATTR_FORECAST_WIND_SPEED: 10.58948352,
|
||||
ATTR_FORECAST_WIND_SPEED: 10.5895,
|
||||
},
|
||||
{
|
||||
ATTR_FORECAST_CONDITION: "rainy",
|
||||
ATTR_FORECAST_TIME: "2021-03-19T10:00:00+00:00",
|
||||
ATTR_FORECAST_PRECIPITATION: 2.92608,
|
||||
ATTR_FORECAST_PRECIPITATION: 2.9261,
|
||||
ATTR_FORECAST_PRECIPITATION_PROBABILITY: 55,
|
||||
ATTR_FORECAST_TEMP: 9,
|
||||
ATTR_FORECAST_TEMP_LOW: 4,
|
||||
ATTR_FORECAST_WIND_BEARING: 68.54,
|
||||
ATTR_FORECAST_WIND_SPEED: 22.38597504,
|
||||
ATTR_FORECAST_WIND_SPEED: 22.3860,
|
||||
},
|
||||
{
|
||||
ATTR_FORECAST_CONDITION: "snowy",
|
||||
|
@ -380,17 +381,17 @@ async def test_v4_weather(
|
|||
ATTR_FORECAST_TEMP: 5,
|
||||
ATTR_FORECAST_TEMP_LOW: 2,
|
||||
ATTR_FORECAST_WIND_BEARING: 56.98,
|
||||
ATTR_FORECAST_WIND_SPEED: 27.922118400000002,
|
||||
ATTR_FORECAST_WIND_SPEED: 27.9221,
|
||||
},
|
||||
]
|
||||
assert weather_state.attributes[ATTR_FRIENDLY_NAME] == "ClimaCell - Daily"
|
||||
assert weather_state.attributes[ATTR_WEATHER_HUMIDITY] == 23
|
||||
assert weather_state.attributes[ATTR_WEATHER_OZONE] == 46.53
|
||||
assert weather_state.attributes[ATTR_WEATHER_PRESSURE] == 1027.7690615000001
|
||||
assert weather_state.attributes[ATTR_WEATHER_PRESSURE] == 1027.7691
|
||||
assert weather_state.attributes[ATTR_WEATHER_TEMPERATURE] == 7
|
||||
assert weather_state.attributes[ATTR_WEATHER_VISIBILITY] == 13.116153600000002
|
||||
assert weather_state.attributes[ATTR_WEATHER_VISIBILITY] == 13.1162
|
||||
assert weather_state.attributes[ATTR_WEATHER_WIND_BEARING] == 315.14
|
||||
assert weather_state.attributes[ATTR_WEATHER_WIND_SPEED] == 15.01517952
|
||||
assert weather_state.attributes[ATTR_WEATHER_WIND_SPEED] == 15.0152
|
||||
assert weather_state.attributes[ATTR_CLOUD_COVER] == 1
|
||||
assert weather_state.attributes[ATTR_WIND_GUST] == 20.34210816
|
||||
assert weather_state.attributes[ATTR_WIND_GUST] == 20.3421
|
||||
assert weather_state.attributes[ATTR_PRECIPITATION_TYPE] == "rain"
|
||||
|
|
|
@ -43,6 +43,59 @@
|
|||
"value": 100,
|
||||
"units": "%"
|
||||
},
|
||||
"fire_index": {
|
||||
"value": 9
|
||||
},
|
||||
"epa_aqi": {
|
||||
"value": 22.3125
|
||||
},
|
||||
"epa_primary_pollutant": {
|
||||
"value": "pm25"
|
||||
},
|
||||
"china_aqi": {
|
||||
"value": 27
|
||||
},
|
||||
"china_primary_pollutant": {
|
||||
"value": "pm10"
|
||||
},
|
||||
"pm25": {
|
||||
"value": 5.3125,
|
||||
"units": "\u00b5g/m3"
|
||||
},
|
||||
"pm10": {
|
||||
"value": 27,
|
||||
"units": "\u00b5g/m3"
|
||||
},
|
||||
"no2": {
|
||||
"value": 14.1875,
|
||||
"units": "ppb"
|
||||
},
|
||||
"co": {
|
||||
"value": 0.875,
|
||||
"units": "ppm"
|
||||
},
|
||||
"so2": {
|
||||
"value": 2,
|
||||
"units": "ppb"
|
||||
},
|
||||
"epa_health_concern": {
|
||||
"value": "Good"
|
||||
},
|
||||
"china_health_concern": {
|
||||
"value": "Good"
|
||||
},
|
||||
"pollen_tree": {
|
||||
"value": 0,
|
||||
"units": "Climacell Pollen Index"
|
||||
},
|
||||
"pollen_weed": {
|
||||
"value": 0,
|
||||
"units": "Climacell Pollen Index"
|
||||
},
|
||||
"pollen_grass": {
|
||||
"value": 0,
|
||||
"units": "Climacell Pollen Index"
|
||||
},
|
||||
"observation_time": {
|
||||
"value": "2021-03-07T18:54:06.055Z"
|
||||
}
|
||||
|
|
|
@ -10,7 +10,22 @@
|
|||
"pollutantO3": 46.53,
|
||||
"windGust": 12.64,
|
||||
"cloudCover": 100,
|
||||
"precipitationType": 1
|
||||
"precipitationType": 1,
|
||||
"particulateMatter25": 0.15,
|
||||
"particulateMatter10": 0.57,
|
||||
"pollutantNO2": 10.67,
|
||||
"pollutantCO": 0.63,
|
||||
"pollutantSO2": 1.65,
|
||||
"epaIndex": 24,
|
||||
"epaPrimaryPollutant": 0,
|
||||
"epaHealthConcern": 0,
|
||||
"mepIndex": 23,
|
||||
"mepPrimaryPollutant": 1,
|
||||
"mepHealthConcern": 0,
|
||||
"treeIndex": 0,
|
||||
"weedIndex": 0,
|
||||
"grassIndex": 0,
|
||||
"fireIndex": 10
|
||||
},
|
||||
"forecasts": {
|
||||
"nowcast": [
|
||||
|
|
Loading…
Reference in New Issue