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>
This commit is contained in:
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, UnknownException,
) )
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.components.weather import DOMAIN as WEATHER_DOMAIN from homeassistant.components.weather import DOMAIN as WEATHER_DOMAIN
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
@ -34,6 +35,7 @@ from homeassistant.helpers.update_coordinator import (
) )
from .const import ( from .const import (
ATTR_FIELD,
ATTRIBUTION, ATTRIBUTION,
CC_ATTR_CLOUD_COVER, CC_ATTR_CLOUD_COVER,
CC_ATTR_CONDITION, CC_ATTR_CONDITION,
@ -50,6 +52,7 @@ from .const import (
CC_ATTR_WIND_DIRECTION, CC_ATTR_WIND_DIRECTION,
CC_ATTR_WIND_GUST, CC_ATTR_WIND_GUST,
CC_ATTR_WIND_SPEED, CC_ATTR_WIND_SPEED,
CC_SENSOR_TYPES,
CC_V3_ATTR_CLOUD_COVER, CC_V3_ATTR_CLOUD_COVER,
CC_V3_ATTR_CONDITION, CC_V3_ATTR_CONDITION,
CC_V3_ATTR_HUMIDITY, CC_V3_ATTR_HUMIDITY,
@ -64,8 +67,8 @@ from .const import (
CC_V3_ATTR_WIND_DIRECTION, CC_V3_ATTR_WIND_DIRECTION,
CC_V3_ATTR_WIND_GUST, CC_V3_ATTR_WIND_GUST,
CC_V3_ATTR_WIND_SPEED, CC_V3_ATTR_WIND_SPEED,
CC_V3_SENSOR_TYPES,
CONF_TIMESTEP, CONF_TIMESTEP,
DEFAULT_FORECAST_TYPE,
DEFAULT_TIMESTEP, DEFAULT_TIMESTEP,
DOMAIN, DOMAIN,
MAX_REQUESTS_PER_DAY, MAX_REQUESTS_PER_DAY,
@ -73,7 +76,7 @@ from .const import (
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
PLATFORMS = [WEATHER_DOMAIN] PLATFORMS = [SENSOR_DOMAIN, WEATHER_DOMAIN]
def _set_update_interval( def _set_update_interval(
@ -232,6 +235,10 @@ class ClimaCellDataUpdateCoordinator(DataUpdateCoordinator):
CC_V3_ATTR_WIND_GUST, CC_V3_ATTR_WIND_GUST,
CC_V3_ATTR_CLOUD_COVER, CC_V3_ATTR_CLOUD_COVER,
CC_V3_ATTR_PRECIPITATION_TYPE, 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( data[FORECASTS][HOURLY] = await self._api.forecast_hourly(
@ -288,6 +295,7 @@ class ClimaCellDataUpdateCoordinator(DataUpdateCoordinator):
CC_ATTR_WIND_GUST, CC_ATTR_WIND_GUST,
CC_ATTR_CLOUD_COVER, CC_ATTR_CLOUD_COVER,
CC_ATTR_PRECIPITATION_TYPE, CC_ATTR_PRECIPITATION_TYPE,
*[sensor_type[ATTR_FIELD] for sensor_type in CC_SENSOR_TYPES],
], ],
[ [
CC_ATTR_TEMPERATURE_LOW, CC_ATTR_TEMPERATURE_LOW,
@ -317,20 +325,22 @@ class ClimaCellEntity(CoordinatorEntity):
self, self,
config_entry: ConfigEntry, config_entry: ConfigEntry,
coordinator: ClimaCellDataUpdateCoordinator, coordinator: ClimaCellDataUpdateCoordinator,
forecast_type: str,
api_version: int, api_version: int,
) -> None: ) -> None:
"""Initialize ClimaCell Entity.""" """Initialize ClimaCell Entity."""
super().__init__(coordinator) super().__init__(coordinator)
self.api_version = api_version self.api_version = api_version
self.forecast_type = forecast_type
self._config_entry = config_entry self._config_entry = config_entry
@staticmethod @staticmethod
def _get_cc_value( def _get_cc_value(
weather_dict: dict[str, Any], key: str weather_dict: dict[str, Any], key: str
) -> int | float | str | None: ) -> int | float | str | None:
"""Return property from weather_dict.""" """
Return property from weather_dict.
Used for V3 API.
"""
items = weather_dict.get(key, {}) items = weather_dict.get(key, {})
# Handle cases where value returned is a list. # Handle cases where value returned is a list.
# Optimistically find the best value to return. # Optimistically find the best value to return.
@ -347,23 +357,13 @@ class ClimaCellEntity(CoordinatorEntity):
return items.get("value") return items.get("value")
@property def _get_current_property(self, property_name: str) -> int | str | float | None:
def entity_registry_enabled_default(self) -> bool: """
"""Return if the entity should be enabled when first added to the entity registry.""" Get property from current conditions.
if self.forecast_type == DEFAULT_FORECAST_TYPE:
return True
return False Used for V4 API.
"""
@property return self.coordinator.data.get(CURRENT, {}).get(property_name)
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}"
@property @property
def attribution(self): def attribution(self):
@ -377,6 +377,6 @@ class ClimaCellEntity(CoordinatorEntity):
"identifiers": {(DOMAIN, self._config_entry.data[CONF_API_KEY])}, "identifiers": {(DOMAIN, self._config_entry.data[CONF_API_KEY])},
"name": "ClimaCell", "name": "ClimaCell",
"manufacturer": "ClimaCell", "manufacturer": "ClimaCell",
"sw_version": "v3", "sw_version": f"v{self.api_version}",
"entry_type": "service", "entry_type": "service",
} }

View File

@ -1,5 +1,13 @@
"""Constants for the ClimaCell integration.""" """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 ( from homeassistant.components.weather import (
ATTR_CONDITION_CLEAR_NIGHT, ATTR_CONDITION_CLEAR_NIGHT,
@ -15,6 +23,15 @@ from homeassistant.components.weather import (
ATTR_CONDITION_SUNNY, ATTR_CONDITION_SUNNY,
ATTR_CONDITION_WINDY, 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" CONF_TIMESTEP = "timestep"
FORECAST_TYPES = [DAILY, HOURLY, NOWCAST] FORECAST_TYPES = [DAILY, HOURLY, NOWCAST]
@ -35,6 +52,12 @@ MAX_FORECASTS = {
NOWCAST: 30, 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 # Additional attributes
ATTR_WIND_GUST = "wind_gust" ATTR_WIND_GUST = "wind_gust"
ATTR_CLOUD_COVER = "cloud_cover" ATTR_CLOUD_COVER = "cloud_cover"
@ -68,6 +91,7 @@ CONDITIONS = {
WeatherCode.PARTLY_CLOUDY: ATTR_CONDITION_PARTLYCLOUDY, WeatherCode.PARTLY_CLOUDY: ATTR_CONDITION_PARTLYCLOUDY,
} }
# Weather constants
CC_ATTR_TIMESTAMP = "startTime" CC_ATTR_TIMESTAMP = "startTime"
CC_ATTR_TEMPERATURE = "temperature" CC_ATTR_TEMPERATURE = "temperature"
CC_ATTR_TEMPERATURE_HIGH = "temperatureMax" CC_ATTR_TEMPERATURE_HIGH = "temperatureMax"
@ -85,6 +109,95 @@ CC_ATTR_WIND_GUST = "windGust"
CC_ATTR_CLOUD_COVER = "cloudCover" CC_ATTR_CLOUD_COVER = "cloudCover"
CC_ATTR_PRECIPITATION_TYPE = "precipitationType" 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 # V3 constants
CONDITIONS_V3 = { CONDITIONS_V3 = {
"breezy": ATTR_CONDITION_WINDY, "breezy": ATTR_CONDITION_WINDY,
@ -111,6 +224,7 @@ CONDITIONS_V3 = {
"partly_cloudy": ATTR_CONDITION_PARTLYCLOUDY, "partly_cloudy": ATTR_CONDITION_PARTLYCLOUDY,
} }
# Weather attributes
CC_V3_ATTR_TIMESTAMP = "observation_time" CC_V3_ATTR_TIMESTAMP = "observation_time"
CC_V3_ATTR_TEMPERATURE = "temp" CC_V3_ATTR_TEMPERATURE = "temp"
CC_V3_ATTR_TEMPERATURE_HIGH = "max" 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_WIND_GUST = "wind_gust"
CC_V3_ATTR_CLOUD_COVER = "cloud_cover" CC_V3_ATTR_CLOUD_COVER = "cloud_cover"
CC_V3_ATTR_PRECIPITATION_TYPE = "precipitation_type" 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.""" """Weather component that handles meteorological data for your location."""
from __future__ import annotations from __future__ import annotations
from abc import abstractmethod
from datetime import datetime from datetime import datetime
import logging import logging
from typing import Any, Callable, Mapping from typing import Any, Callable, Mapping
@ -29,6 +30,7 @@ from homeassistant.components.weather import (
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
CONF_API_VERSION, CONF_API_VERSION,
CONF_NAME,
LENGTH_FEET, LENGTH_FEET,
LENGTH_KILOMETERS, LENGTH_KILOMETERS,
LENGTH_METERS, 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.distance import convert as distance_convert
from homeassistant.util.pressure import convert as pressure_convert from homeassistant.util.pressure import convert as pressure_convert
from . import ClimaCellEntity from . import ClimaCellDataUpdateCoordinator, ClimaCellEntity
from .const import ( from .const import (
ATTR_CLOUD_COVER, ATTR_CLOUD_COVER,
ATTR_PRECIPITATION_TYPE, ATTR_PRECIPITATION_TYPE,
@ -86,12 +88,11 @@ from .const import (
CONDITIONS, CONDITIONS,
CONDITIONS_V3, CONDITIONS_V3,
CONF_TIMESTEP, CONF_TIMESTEP,
DEFAULT_FORECAST_TYPE,
DOMAIN, DOMAIN,
MAX_FORECASTS, MAX_FORECASTS,
) )
# mypy: allow-untyped-defs, no-check-untyped-defs
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -106,7 +107,7 @@ async def async_setup_entry(
api_class = ClimaCellV3WeatherEntity if api_version == 3 else ClimaCellWeatherEntity api_class = ClimaCellV3WeatherEntity if api_version == 3 else ClimaCellWeatherEntity
entities = [ 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] for forecast_type in [DAILY, HOURLY, NOWCAST]
] ]
async_add_entities(entities) async_add_entities(entities)
@ -115,12 +116,41 @@ async def async_setup_entry(
class BaseClimaCellWeatherEntity(ClimaCellEntity, WeatherEntity): class BaseClimaCellWeatherEntity(ClimaCellEntity, WeatherEntity):
"""Base ClimaCell weather entity.""" """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 @staticmethod
@abstractmethod
def _translate_condition( def _translate_condition(
condition: int | None, sun_is_up: bool = True condition: int | None, sun_is_up: bool = True
) -> str | None: ) -> str | None:
"""Translate ClimaCell condition into an HA condition.""" """Translate ClimaCell condition into an HA condition."""
raise NotImplementedError()
def _forecast_dict( def _forecast_dict(
self, self,
@ -144,13 +174,14 @@ class BaseClimaCellWeatherEntity(ClimaCellEntity, WeatherEntity):
if self.hass.config.units.is_metric: if self.hass.config.units.is_metric:
if precipitation: if precipitation:
precipitation = ( precipitation = round(
distance_convert(precipitation / 12, LENGTH_FEET, LENGTH_METERS) distance_convert(precipitation / 12, LENGTH_FEET, LENGTH_METERS)
* 1000 * 1000,
4,
) )
if wind_speed: if wind_speed:
wind_speed = distance_convert( wind_speed = round(
wind_speed, LENGTH_MILES, LENGTH_KILOMETERS distance_convert(wind_speed, LENGTH_MILES, LENGTH_KILOMETERS), 4
) )
data = { data = {
@ -171,8 +202,8 @@ class BaseClimaCellWeatherEntity(ClimaCellEntity, WeatherEntity):
"""Return additional state attributes.""" """Return additional state attributes."""
wind_gust = self.wind_gust wind_gust = self.wind_gust
if wind_gust and self.hass.config.units.is_metric: if wind_gust and self.hass.config.units.is_metric:
wind_gust = distance_convert( wind_gust = round(
self.wind_gust, LENGTH_MILES, LENGTH_KILOMETERS distance_convert(self.wind_gust, LENGTH_MILES, LENGTH_KILOMETERS), 4
) )
cloud_cover = self.cloud_cover cloud_cover = self.cloud_cover
if cloud_cover is not None: if cloud_cover is not None:
@ -184,19 +215,61 @@ class BaseClimaCellWeatherEntity(ClimaCellEntity, WeatherEntity):
} }
@property @property
@abstractmethod
def cloud_cover(self): def cloud_cover(self):
"""Return cloud cover.""" """Return cloud cover."""
raise NotImplementedError
@property @property
@abstractmethod
def wind_gust(self): def wind_gust(self):
"""Return wind gust speed.""" """Return wind gust speed."""
raise NotImplementedError
@property @property
@abstractmethod
def precipitation_type(self): def precipitation_type(self):
"""Return precipitation type.""" """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): class ClimaCellWeatherEntity(BaseClimaCellWeatherEntity):
@ -217,10 +290,6 @@ class ClimaCellWeatherEntity(BaseClimaCellWeatherEntity):
return CLEAR_CONDITIONS["night"] return CLEAR_CONDITIONS["night"]
return CONDITIONS[condition] 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 @property
def temperature(self): def temperature(self):
"""Return the platform temperature.""" """Return the platform temperature."""
@ -232,12 +301,9 @@ class ClimaCellWeatherEntity(BaseClimaCellWeatherEntity):
return TEMP_FAHRENHEIT return TEMP_FAHRENHEIT
@property @property
def pressure(self): def _pressure(self):
"""Return the pressure.""" """Return the raw pressure."""
pressure = self._get_current_property(CC_ATTR_PRESSURE) return 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
@property @property
def humidity(self): def humidity(self):
@ -263,12 +329,9 @@ class ClimaCellWeatherEntity(BaseClimaCellWeatherEntity):
return PrecipitationType(precipitation_type).name.lower() return PrecipitationType(precipitation_type).name.lower()
@property @property
def wind_speed(self): def _wind_speed(self):
"""Return the wind speed.""" """Return the raw wind speed."""
wind_speed = self._get_current_property(CC_ATTR_WIND_SPEED) return 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
@property @property
def wind_bearing(self): def wind_bearing(self):
@ -289,12 +352,9 @@ class ClimaCellWeatherEntity(BaseClimaCellWeatherEntity):
) )
@property @property
def visibility(self): def _visibility(self):
"""Return the visibility.""" """Return the raw visibility."""
visibility = self._get_current_property(CC_ATTR_VISIBILITY) return 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
@property @property
def forecast(self): def forecast(self):
@ -391,14 +451,9 @@ class ClimaCellV3WeatherEntity(BaseClimaCellWeatherEntity):
return TEMP_FAHRENHEIT return TEMP_FAHRENHEIT
@property @property
def pressure(self): def _pressure(self):
"""Return the pressure.""" """Return the raw pressure."""
pressure = self._get_cc_value( return self._get_cc_value(self.coordinator.data[CURRENT], CC_V3_ATTR_PRESSURE)
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
@property @property
def humidity(self): def humidity(self):
@ -425,14 +480,9 @@ class ClimaCellV3WeatherEntity(BaseClimaCellWeatherEntity):
) )
@property @property
def wind_speed(self): def _wind_speed(self):
"""Return the wind speed.""" """Return the raw wind speed."""
wind_speed = self._get_cc_value( return self._get_cc_value(self.coordinator.data[CURRENT], CC_V3_ATTR_WIND_SPEED)
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
@property @property
def wind_bearing(self): def wind_bearing(self):
@ -455,14 +505,9 @@ class ClimaCellV3WeatherEntity(BaseClimaCellWeatherEntity):
) )
@property @property
def visibility(self): def _visibility(self):
"""Return the visibility.""" """Return the raw visibility."""
visibility = self._get_cc_value( return self._get_cc_value(self.coordinator.data[CURRENT], CC_V3_ATTR_VISIBILITY)
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
@property @property
def forecast(self): 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, DOMAIN as WEATHER_DOMAIN,
) )
from homeassistant.const import ATTR_ATTRIBUTION, ATTR_FRIENDLY_NAME 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.entity_registry import async_get
from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.typing import HomeAssistantType
@ -55,7 +55,8 @@ from tests.common import MockConfigEntry
_LOGGER = logging.getLogger(__name__) _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.""" """Enable disabled entity."""
ent_reg = async_get(hass) ent_reg = async_get(hass)
entry = ent_reg.async_get(entity_name) 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) config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id) assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
await _enable_entity(hass, "weather.climacell_hourly") for entity_name in ("hourly", "nowcast"):
await _enable_entity(hass, "weather.climacell_nowcast") _enable_entity(hass, f"weather.climacell_{entity_name}")
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(hass.states.async_entity_ids(WEATHER_DOMAIN)) == 3 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_CONDITION: ATTR_CONDITION_CLOUDY,
ATTR_FORECAST_TIME: "2021-03-12T00:00:00+00:00", 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_PRECIPITATION_PROBABILITY: 25,
ATTR_FORECAST_TEMP: 20, ATTR_FORECAST_TEMP: 20,
ATTR_FORECAST_TEMP_LOW: 12, ATTR_FORECAST_TEMP_LOW: 12,
@ -158,7 +159,7 @@ async def test_v3_weather(
{ {
ATTR_FORECAST_CONDITION: ATTR_CONDITION_RAINY, ATTR_FORECAST_CONDITION: ATTR_CONDITION_RAINY,
ATTR_FORECAST_TIME: "2021-03-14T00:00:00+00:00", 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_PRECIPITATION_PROBABILITY: 75,
ATTR_FORECAST_TEMP: 6, ATTR_FORECAST_TEMP: 6,
ATTR_FORECAST_TEMP_LOW: 3, ATTR_FORECAST_TEMP_LOW: 3,
@ -166,7 +167,7 @@ async def test_v3_weather(
{ {
ATTR_FORECAST_CONDITION: ATTR_CONDITION_SNOWY, ATTR_FORECAST_CONDITION: ATTR_CONDITION_SNOWY,
ATTR_FORECAST_TIME: "2021-03-15T00:00:00+00:00", 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_PRECIPITATION_PROBABILITY: 95,
ATTR_FORECAST_TEMP: 1, ATTR_FORECAST_TEMP: 1,
ATTR_FORECAST_TEMP_LOW: 0, ATTR_FORECAST_TEMP_LOW: 0,
@ -174,7 +175,7 @@ async def test_v3_weather(
{ {
ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY,
ATTR_FORECAST_TIME: "2021-03-16T00:00:00+00:00", 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_PRECIPITATION_PROBABILITY: 5,
ATTR_FORECAST_TEMP: 6, ATTR_FORECAST_TEMP: 6,
ATTR_FORECAST_TEMP_LOW: -2, ATTR_FORECAST_TEMP_LOW: -2,
@ -214,7 +215,7 @@ async def test_v3_weather(
{ {
ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY,
ATTR_FORECAST_TIME: "2021-03-21T00:00:00+00:00", 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_PRECIPITATION_PROBABILITY: 20,
ATTR_FORECAST_TEMP: 7, ATTR_FORECAST_TEMP: 7,
ATTR_FORECAST_TEMP_LOW: 1, 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_FRIENDLY_NAME] == "ClimaCell - Daily"
assert weather_state.attributes[ATTR_WEATHER_HUMIDITY] == 24 assert weather_state.attributes[ATTR_WEATHER_HUMIDITY] == 24
assert weather_state.attributes[ATTR_WEATHER_OZONE] == 52.625 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_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_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_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" assert weather_state.attributes[ATTR_PRECIPITATION_TYPE] == "rain"
@ -250,7 +251,7 @@ async def test_v4_weather(
ATTR_FORECAST_TEMP: 8, ATTR_FORECAST_TEMP: 8,
ATTR_FORECAST_TEMP_LOW: -3, ATTR_FORECAST_TEMP_LOW: -3,
ATTR_FORECAST_WIND_BEARING: 239.6, ATTR_FORECAST_WIND_BEARING: 239.6,
ATTR_FORECAST_WIND_SPEED: 15.272674560000002, ATTR_FORECAST_WIND_SPEED: 15.2727,
}, },
{ {
ATTR_FORECAST_CONDITION: "cloudy", ATTR_FORECAST_CONDITION: "cloudy",
@ -260,7 +261,7 @@ async def test_v4_weather(
ATTR_FORECAST_TEMP: 10, ATTR_FORECAST_TEMP: 10,
ATTR_FORECAST_TEMP_LOW: -3, ATTR_FORECAST_TEMP_LOW: -3,
ATTR_FORECAST_WIND_BEARING: 262.82, ATTR_FORECAST_WIND_BEARING: 262.82,
ATTR_FORECAST_WIND_SPEED: 11.65165056, ATTR_FORECAST_WIND_SPEED: 11.6517,
}, },
{ {
ATTR_FORECAST_CONDITION: "cloudy", ATTR_FORECAST_CONDITION: "cloudy",
@ -270,7 +271,7 @@ async def test_v4_weather(
ATTR_FORECAST_TEMP: 19, ATTR_FORECAST_TEMP: 19,
ATTR_FORECAST_TEMP_LOW: 0, ATTR_FORECAST_TEMP_LOW: 0,
ATTR_FORECAST_WIND_BEARING: 229.3, ATTR_FORECAST_WIND_BEARING: 229.3,
ATTR_FORECAST_WIND_SPEED: 11.3458752, ATTR_FORECAST_WIND_SPEED: 11.3459,
}, },
{ {
ATTR_FORECAST_CONDITION: "cloudy", ATTR_FORECAST_CONDITION: "cloudy",
@ -280,7 +281,7 @@ async def test_v4_weather(
ATTR_FORECAST_TEMP: 18, ATTR_FORECAST_TEMP: 18,
ATTR_FORECAST_TEMP_LOW: 3, ATTR_FORECAST_TEMP_LOW: 3,
ATTR_FORECAST_WIND_BEARING: 149.91, ATTR_FORECAST_WIND_BEARING: 149.91,
ATTR_FORECAST_WIND_SPEED: 17.123420160000002, ATTR_FORECAST_WIND_SPEED: 17.1234,
}, },
{ {
ATTR_FORECAST_CONDITION: "cloudy", ATTR_FORECAST_CONDITION: "cloudy",
@ -290,17 +291,17 @@ async def test_v4_weather(
ATTR_FORECAST_TEMP: 19, ATTR_FORECAST_TEMP: 19,
ATTR_FORECAST_TEMP_LOW: 9, ATTR_FORECAST_TEMP_LOW: 9,
ATTR_FORECAST_WIND_BEARING: 210.45, ATTR_FORECAST_WIND_BEARING: 210.45,
ATTR_FORECAST_WIND_SPEED: 25.250607360000004, ATTR_FORECAST_WIND_SPEED: 25.2506,
}, },
{ {
ATTR_FORECAST_CONDITION: "rainy", ATTR_FORECAST_CONDITION: "rainy",
ATTR_FORECAST_TIME: "2021-03-12T11:00:00+00:00", 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_PRECIPITATION_PROBABILITY: 25,
ATTR_FORECAST_TEMP: 20, ATTR_FORECAST_TEMP: 20,
ATTR_FORECAST_TEMP_LOW: 12, ATTR_FORECAST_TEMP_LOW: 12,
ATTR_FORECAST_WIND_BEARING: 217.98, ATTR_FORECAST_WIND_BEARING: 217.98,
ATTR_FORECAST_WIND_SPEED: 19.794931200000004, ATTR_FORECAST_WIND_SPEED: 19.7949,
}, },
{ {
ATTR_FORECAST_CONDITION: "cloudy", ATTR_FORECAST_CONDITION: "cloudy",
@ -310,27 +311,27 @@ async def test_v4_weather(
ATTR_FORECAST_TEMP: 12, ATTR_FORECAST_TEMP: 12,
ATTR_FORECAST_TEMP_LOW: 6, ATTR_FORECAST_TEMP_LOW: 6,
ATTR_FORECAST_WIND_BEARING: 58.79, ATTR_FORECAST_WIND_BEARING: 58.79,
ATTR_FORECAST_WIND_SPEED: 15.642823680000001, ATTR_FORECAST_WIND_SPEED: 15.6428,
}, },
{ {
ATTR_FORECAST_CONDITION: "snowy", ATTR_FORECAST_CONDITION: "snowy",
ATTR_FORECAST_TIME: "2021-03-14T10:00:00+00:00", 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_PRECIPITATION_PROBABILITY: 95,
ATTR_FORECAST_TEMP: 6, ATTR_FORECAST_TEMP: 6,
ATTR_FORECAST_TEMP_LOW: 1, ATTR_FORECAST_TEMP_LOW: 1,
ATTR_FORECAST_WIND_BEARING: 70.25, ATTR_FORECAST_WIND_BEARING: 70.25,
ATTR_FORECAST_WIND_SPEED: 26.15184, ATTR_FORECAST_WIND_SPEED: 26.1518,
}, },
{ {
ATTR_FORECAST_CONDITION: "snowy", ATTR_FORECAST_CONDITION: "snowy",
ATTR_FORECAST_TIME: "2021-03-15T10:00:00+00:00", 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_PRECIPITATION_PROBABILITY: 55,
ATTR_FORECAST_TEMP: 6, ATTR_FORECAST_TEMP: 6,
ATTR_FORECAST_TEMP_LOW: -1, ATTR_FORECAST_TEMP_LOW: -1,
ATTR_FORECAST_WIND_BEARING: 84.47, ATTR_FORECAST_WIND_BEARING: 84.47,
ATTR_FORECAST_WIND_SPEED: 25.57247616, ATTR_FORECAST_WIND_SPEED: 25.5725,
}, },
{ {
ATTR_FORECAST_CONDITION: "cloudy", ATTR_FORECAST_CONDITION: "cloudy",
@ -340,7 +341,7 @@ async def test_v4_weather(
ATTR_FORECAST_TEMP: 6, ATTR_FORECAST_TEMP: 6,
ATTR_FORECAST_TEMP_LOW: -2, ATTR_FORECAST_TEMP_LOW: -2,
ATTR_FORECAST_WIND_BEARING: 103.85, ATTR_FORECAST_WIND_BEARING: 103.85,
ATTR_FORECAST_WIND_SPEED: 10.79869824, ATTR_FORECAST_WIND_SPEED: 10.7987,
}, },
{ {
ATTR_FORECAST_CONDITION: "cloudy", ATTR_FORECAST_CONDITION: "cloudy",
@ -350,7 +351,7 @@ async def test_v4_weather(
ATTR_FORECAST_TEMP: 11, ATTR_FORECAST_TEMP: 11,
ATTR_FORECAST_TEMP_LOW: 1, ATTR_FORECAST_TEMP_LOW: 1,
ATTR_FORECAST_WIND_BEARING: 145.41, ATTR_FORECAST_WIND_BEARING: 145.41,
ATTR_FORECAST_WIND_SPEED: 11.69993088, ATTR_FORECAST_WIND_SPEED: 11.6999,
}, },
{ {
ATTR_FORECAST_CONDITION: "cloudy", ATTR_FORECAST_CONDITION: "cloudy",
@ -360,17 +361,17 @@ async def test_v4_weather(
ATTR_FORECAST_TEMP: 12, ATTR_FORECAST_TEMP: 12,
ATTR_FORECAST_TEMP_LOW: 5, ATTR_FORECAST_TEMP_LOW: 5,
ATTR_FORECAST_WIND_BEARING: 62.99, ATTR_FORECAST_WIND_BEARING: 62.99,
ATTR_FORECAST_WIND_SPEED: 10.58948352, ATTR_FORECAST_WIND_SPEED: 10.5895,
}, },
{ {
ATTR_FORECAST_CONDITION: "rainy", ATTR_FORECAST_CONDITION: "rainy",
ATTR_FORECAST_TIME: "2021-03-19T10:00:00+00:00", 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_PRECIPITATION_PROBABILITY: 55,
ATTR_FORECAST_TEMP: 9, ATTR_FORECAST_TEMP: 9,
ATTR_FORECAST_TEMP_LOW: 4, ATTR_FORECAST_TEMP_LOW: 4,
ATTR_FORECAST_WIND_BEARING: 68.54, ATTR_FORECAST_WIND_BEARING: 68.54,
ATTR_FORECAST_WIND_SPEED: 22.38597504, ATTR_FORECAST_WIND_SPEED: 22.3860,
}, },
{ {
ATTR_FORECAST_CONDITION: "snowy", ATTR_FORECAST_CONDITION: "snowy",
@ -380,17 +381,17 @@ async def test_v4_weather(
ATTR_FORECAST_TEMP: 5, ATTR_FORECAST_TEMP: 5,
ATTR_FORECAST_TEMP_LOW: 2, ATTR_FORECAST_TEMP_LOW: 2,
ATTR_FORECAST_WIND_BEARING: 56.98, 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_FRIENDLY_NAME] == "ClimaCell - Daily"
assert weather_state.attributes[ATTR_WEATHER_HUMIDITY] == 23 assert weather_state.attributes[ATTR_WEATHER_HUMIDITY] == 23
assert weather_state.attributes[ATTR_WEATHER_OZONE] == 46.53 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_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_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_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" assert weather_state.attributes[ATTR_PRECIPITATION_TYPE] == "rain"

View File

@ -43,6 +43,59 @@
"value": 100, "value": 100,
"units": "%" "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": { "observation_time": {
"value": "2021-03-07T18:54:06.055Z" "value": "2021-03-07T18:54:06.055Z"
} }

View File

@ -10,7 +10,22 @@
"pollutantO3": 46.53, "pollutantO3": 46.53,
"windGust": 12.64, "windGust": 12.64,
"cloudCover": 100, "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": { "forecasts": {
"nowcast": [ "nowcast": [