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
Raman Gupta 2021-04-15 16:31:59 -04:00 committed by GitHub
parent 5fb36ad9e1
commit 898a1a17be
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 717 additions and 119 deletions

View File

@ -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",
}

View File

@ -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"},
]

View File

@ -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]
)

View File

@ -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):

View File

@ -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")

View File

@ -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"

View File

@ -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"
}

View File

@ -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": [