Retire climacell entirely (#78901)

* Retire climacell entirely

* remove fixtures

* remove const file

* Remove all traces of climacell integration

* missed some
This commit is contained in:
Raman Gupta 2022-09-23 03:05:55 -04:00 committed by GitHub
parent d1da6ea04d
commit 95e3572277
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
78 changed files with 9 additions and 5610 deletions

View File

@ -177,8 +177,6 @@ build.json @home-assistant/supervisor
/homeassistant/components/cisco_ios/ @fbradyirl
/homeassistant/components/cisco_mobility_express/ @fbradyirl
/homeassistant/components/cisco_webex_teams/ @fbradyirl
/homeassistant/components/climacell/ @raman325
/tests/components/climacell/ @raman325
/homeassistant/components/climate/ @home-assistant/core
/tests/components/climate/ @home-assistant/core
/homeassistant/components/cloud/ @home-assistant/cloud

View File

@ -1,329 +0,0 @@
"""The ClimaCell integration."""
from __future__ import annotations
from datetime import timedelta
import logging
from math import ceil
from typing import Any
from pyclimacell import ClimaCellV3, ClimaCellV4
from pyclimacell.const import CURRENT, DAILY, FORECASTS, HOURLY, NOWCAST
from pyclimacell.exceptions import (
CantConnectException,
InvalidAPIKeyException,
RateLimitedException,
UnknownException,
)
from homeassistant.components.tomorrowio import DOMAIN as TOMORROW_DOMAIN
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import (
CONF_API_KEY,
CONF_API_VERSION,
CONF_LATITUDE,
CONF_LONGITUDE,
CONF_NAME,
Platform,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.device_registry import DeviceEntryType
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
UpdateFailed,
)
from .const import (
ATTRIBUTION,
CC_V3_ATTR_CLOUD_COVER,
CC_V3_ATTR_CONDITION,
CC_V3_ATTR_HUMIDITY,
CC_V3_ATTR_OZONE,
CC_V3_ATTR_PRECIPITATION,
CC_V3_ATTR_PRECIPITATION_DAILY,
CC_V3_ATTR_PRECIPITATION_PROBABILITY,
CC_V3_ATTR_PRECIPITATION_TYPE,
CC_V3_ATTR_PRESSURE,
CC_V3_ATTR_TEMPERATURE,
CC_V3_ATTR_VISIBILITY,
CC_V3_ATTR_WIND_DIRECTION,
CC_V3_ATTR_WIND_GUST,
CC_V3_ATTR_WIND_SPEED,
CC_V3_SENSOR_TYPES,
CONF_TIMESTEP,
DEFAULT_TIMESTEP,
DOMAIN,
MAX_REQUESTS_PER_DAY,
)
_LOGGER = logging.getLogger(__name__)
PLATFORMS = [Platform.SENSOR, Platform.WEATHER]
def _set_update_interval(hass: HomeAssistant, current_entry: ConfigEntry) -> timedelta:
"""Recalculate update_interval based on existing ClimaCell instances and update them."""
api_calls = 4 if current_entry.data[CONF_API_VERSION] == 3 else 2
# We check how many ClimaCell configured instances are using the same API key and
# calculate interval to not exceed allowed numbers of requests. Divide 90% of
# MAX_REQUESTS_PER_DAY by 4 because every update requires four API calls and we want
# a buffer in the number of API calls left at the end of the day.
other_instance_entry_ids = [
entry.entry_id
for entry in hass.config_entries.async_entries(DOMAIN)
if entry.entry_id != current_entry.entry_id
and entry.data[CONF_API_KEY] == current_entry.data[CONF_API_KEY]
]
interval = timedelta(
minutes=(
ceil(
(24 * 60 * (len(other_instance_entry_ids) + 1) * api_calls)
/ (MAX_REQUESTS_PER_DAY * 0.9)
)
)
)
for entry_id in other_instance_entry_ids:
if entry_id in hass.data[DOMAIN]:
hass.data[DOMAIN][entry_id].update_interval = interval
return interval
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up ClimaCell API from a config entry."""
hass.data.setdefault(DOMAIN, {})
params: dict[str, Any] = {}
# If config entry options not set up, set them up
if not entry.options:
params["options"] = {
CONF_TIMESTEP: DEFAULT_TIMESTEP,
}
else:
# Use valid timestep if it's invalid
timestep = entry.options[CONF_TIMESTEP]
if timestep not in (1, 5, 15, 30):
if timestep <= 2:
timestep = 1
elif timestep <= 7:
timestep = 5
elif timestep <= 20:
timestep = 15
else:
timestep = 30
new_options = entry.options.copy()
new_options[CONF_TIMESTEP] = timestep
params["options"] = new_options
# Add API version if not found
if CONF_API_VERSION not in entry.data:
new_data = entry.data.copy()
new_data[CONF_API_VERSION] = 3
params["data"] = new_data
if params:
hass.config_entries.async_update_entry(entry, **params)
hass.async_create_task(
hass.config_entries.flow.async_init(
TOMORROW_DOMAIN,
context={"source": SOURCE_IMPORT, "old_config_entry_id": entry.entry_id},
data=entry.data,
)
)
# Eventually we will remove the code that sets up the platforms and force users to
# migrate. This will only impact users still on the V3 API because we can't
# automatically migrate them, but for V4 users, we can skip the platform setup.
if entry.data[CONF_API_VERSION] == 4:
return True
api = ClimaCellV3(
entry.data[CONF_API_KEY],
entry.data.get(CONF_LATITUDE, hass.config.latitude),
entry.data.get(CONF_LONGITUDE, hass.config.longitude),
session=async_get_clientsession(hass),
)
coordinator = ClimaCellDataUpdateCoordinator(
hass,
entry,
api,
_set_update_interval(hass, entry),
)
await coordinator.async_config_entry_first_refresh()
hass.data[DOMAIN][entry.entry_id] = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(
config_entry, PLATFORMS
)
hass.data[DOMAIN].pop(config_entry.entry_id, None)
if not hass.data[DOMAIN]:
hass.data.pop(DOMAIN)
return unload_ok
class ClimaCellDataUpdateCoordinator(DataUpdateCoordinator):
"""Define an object to hold ClimaCell data."""
def __init__(
self,
hass: HomeAssistant,
config_entry: ConfigEntry,
api: ClimaCellV3 | ClimaCellV4,
update_interval: timedelta,
) -> None:
"""Initialize."""
self._config_entry = config_entry
self._api_version = config_entry.data[CONF_API_VERSION]
self._api = api
self.name = config_entry.data[CONF_NAME]
self.data = {CURRENT: {}, FORECASTS: {}}
super().__init__(
hass,
_LOGGER,
name=config_entry.data[CONF_NAME],
update_interval=update_interval,
)
async def _async_update_data(self) -> dict[str, Any]:
"""Update data via library."""
data: dict[str, Any] = {FORECASTS: {}}
try:
data[CURRENT] = await self._api.realtime(
[
CC_V3_ATTR_TEMPERATURE,
CC_V3_ATTR_HUMIDITY,
CC_V3_ATTR_PRESSURE,
CC_V3_ATTR_WIND_SPEED,
CC_V3_ATTR_WIND_DIRECTION,
CC_V3_ATTR_CONDITION,
CC_V3_ATTR_VISIBILITY,
CC_V3_ATTR_OZONE,
CC_V3_ATTR_WIND_GUST,
CC_V3_ATTR_CLOUD_COVER,
CC_V3_ATTR_PRECIPITATION_TYPE,
*(sensor_type.key for sensor_type in CC_V3_SENSOR_TYPES),
]
)
data[FORECASTS][HOURLY] = await self._api.forecast_hourly(
[
CC_V3_ATTR_TEMPERATURE,
CC_V3_ATTR_WIND_SPEED,
CC_V3_ATTR_WIND_DIRECTION,
CC_V3_ATTR_CONDITION,
CC_V3_ATTR_PRECIPITATION,
CC_V3_ATTR_PRECIPITATION_PROBABILITY,
],
None,
timedelta(hours=24),
)
data[FORECASTS][DAILY] = await self._api.forecast_daily(
[
CC_V3_ATTR_TEMPERATURE,
CC_V3_ATTR_WIND_SPEED,
CC_V3_ATTR_WIND_DIRECTION,
CC_V3_ATTR_CONDITION,
CC_V3_ATTR_PRECIPITATION_DAILY,
CC_V3_ATTR_PRECIPITATION_PROBABILITY,
],
None,
timedelta(days=14),
)
data[FORECASTS][NOWCAST] = await self._api.forecast_nowcast(
[
CC_V3_ATTR_TEMPERATURE,
CC_V3_ATTR_WIND_SPEED,
CC_V3_ATTR_WIND_DIRECTION,
CC_V3_ATTR_CONDITION,
CC_V3_ATTR_PRECIPITATION,
],
None,
timedelta(
minutes=min(300, self._config_entry.options[CONF_TIMESTEP] * 30)
),
self._config_entry.options[CONF_TIMESTEP],
)
except (
CantConnectException,
InvalidAPIKeyException,
RateLimitedException,
UnknownException,
) as error:
raise UpdateFailed from error
return data
class ClimaCellEntity(CoordinatorEntity[ClimaCellDataUpdateCoordinator]):
"""Base ClimaCell Entity."""
def __init__(
self,
config_entry: ConfigEntry,
coordinator: ClimaCellDataUpdateCoordinator,
api_version: int,
) -> None:
"""Initialize ClimaCell Entity."""
super().__init__(coordinator)
self.api_version = api_version
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.
Used for V3 API.
"""
items = weather_dict.get(key, {})
# Handle cases where value returned is a list.
# Optimistically find the best value to return.
if isinstance(items, list):
if len(items) == 1:
return items[0].get("value")
return next(
(item.get("value") for item in items if "max" in item),
next(
(item.get("value") for item in items if "min" in item),
items[0].get("value", None),
),
)
return items.get("value")
@property
def attribution(self):
"""Return the attribution."""
return ATTRIBUTION
@property
def device_info(self) -> DeviceInfo:
"""Return device registry information."""
return DeviceInfo(
entry_type=DeviceEntryType.SERVICE,
identifiers={(DOMAIN, self._config_entry.data[CONF_API_KEY])},
manufacturer="ClimaCell",
name="ClimaCell",
sw_version=f"v{self.api_version}",
)

View File

@ -1,52 +0,0 @@
"""Config flow for ClimaCell integration."""
from __future__ import annotations
from typing import Any
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult
from .const import CONF_TIMESTEP, DEFAULT_TIMESTEP, DOMAIN
class ClimaCellOptionsConfigFlow(config_entries.OptionsFlow):
"""Handle ClimaCell options."""
def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
"""Initialize ClimaCell options flow."""
self._config_entry = config_entry
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Manage the ClimaCell options."""
if user_input is not None:
return self.async_create_entry(title="", data=user_input)
options_schema = {
vol.Required(
CONF_TIMESTEP,
default=self._config_entry.options.get(CONF_TIMESTEP, DEFAULT_TIMESTEP),
): vol.In([1, 5, 15, 30]),
}
return self.async_show_form(
step_id="init", data_schema=vol.Schema(options_schema)
)
class ClimaCellConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for ClimaCell Weather API."""
VERSION = 1
@staticmethod
@callback
def async_get_options_flow(
config_entry: config_entries.ConfigEntry,
) -> ClimaCellOptionsConfigFlow:
"""Get the options flow for this handler."""
return ClimaCellOptionsConfigFlow(config_entry)

View File

@ -1,225 +0,0 @@
"""Constants for the ClimaCell integration."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from enum import IntEnum
from pyclimacell.const import DAILY, HOURLY, NOWCAST, V3PollenIndex
from homeassistant.components.sensor import SensorDeviceClass, SensorEntityDescription
from homeassistant.components.weather import (
ATTR_CONDITION_CLEAR_NIGHT,
ATTR_CONDITION_CLOUDY,
ATTR_CONDITION_FOG,
ATTR_CONDITION_HAIL,
ATTR_CONDITION_LIGHTNING,
ATTR_CONDITION_PARTLYCLOUDY,
ATTR_CONDITION_POURING,
ATTR_CONDITION_RAINY,
ATTR_CONDITION_SNOWY,
ATTR_CONDITION_SNOWY_RAINY,
ATTR_CONDITION_SUNNY,
ATTR_CONDITION_WINDY,
)
from homeassistant.const import (
CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_BILLION,
CONCENTRATION_PARTS_PER_MILLION,
)
CONF_TIMESTEP = "timestep"
FORECAST_TYPES = [DAILY, HOURLY, NOWCAST]
DEFAULT_NAME = "ClimaCell"
DEFAULT_TIMESTEP = 15
DEFAULT_FORECAST_TYPE = DAILY
DOMAIN = "climacell"
ATTRIBUTION = "Powered by ClimaCell"
MAX_REQUESTS_PER_DAY = 100
CLEAR_CONDITIONS = {"night": ATTR_CONDITION_CLEAR_NIGHT, "day": ATTR_CONDITION_SUNNY}
MAX_FORECASTS = {
DAILY: 14,
HOURLY: 24,
NOWCAST: 30,
}
# Additional attributes
ATTR_WIND_GUST = "wind_gust"
ATTR_CLOUD_COVER = "cloud_cover"
ATTR_PRECIPITATION_TYPE = "precipitation_type"
@dataclass
class ClimaCellSensorEntityDescription(SensorEntityDescription):
"""Describes a ClimaCell sensor entity."""
unit_imperial: str | None = None
unit_metric: str | None = None
metric_conversion: Callable[[float], float] | float = 1.0
is_metric_check: bool | None = None
device_class: str | None = None
value_map: IntEnum | None = None
def __post_init__(self) -> None:
"""Post initialization."""
units = (self.unit_imperial, self.unit_metric)
if any(u is not None for u in units) and any(u is None for u in units):
raise RuntimeError(
"`unit_imperial` and `unit_metric` both need to be None or both need "
"to be defined."
)
# V3 constants
CONDITIONS_V3 = {
"breezy": ATTR_CONDITION_WINDY,
"freezing_rain_heavy": ATTR_CONDITION_SNOWY_RAINY,
"freezing_rain": ATTR_CONDITION_SNOWY_RAINY,
"freezing_rain_light": ATTR_CONDITION_SNOWY_RAINY,
"freezing_drizzle": ATTR_CONDITION_SNOWY_RAINY,
"ice_pellets_heavy": ATTR_CONDITION_HAIL,
"ice_pellets": ATTR_CONDITION_HAIL,
"ice_pellets_light": ATTR_CONDITION_HAIL,
"snow_heavy": ATTR_CONDITION_SNOWY,
"snow": ATTR_CONDITION_SNOWY,
"snow_light": ATTR_CONDITION_SNOWY,
"flurries": ATTR_CONDITION_SNOWY,
"tstorm": ATTR_CONDITION_LIGHTNING,
"rain_heavy": ATTR_CONDITION_POURING,
"rain": ATTR_CONDITION_RAINY,
"rain_light": ATTR_CONDITION_RAINY,
"drizzle": ATTR_CONDITION_RAINY,
"fog_light": ATTR_CONDITION_FOG,
"fog": ATTR_CONDITION_FOG,
"cloudy": ATTR_CONDITION_CLOUDY,
"mostly_cloudy": ATTR_CONDITION_CLOUDY,
"partly_cloudy": ATTR_CONDITION_PARTLYCLOUDY,
}
# Weather attributes
CC_V3_ATTR_TIMESTAMP = "observation_time"
CC_V3_ATTR_TEMPERATURE = "temp"
CC_V3_ATTR_TEMPERATURE_HIGH = "max"
CC_V3_ATTR_TEMPERATURE_LOW = "min"
CC_V3_ATTR_PRESSURE = "baro_pressure"
CC_V3_ATTR_HUMIDITY = "humidity"
CC_V3_ATTR_WIND_SPEED = "wind_speed"
CC_V3_ATTR_WIND_DIRECTION = "wind_direction"
CC_V3_ATTR_OZONE = "o3"
CC_V3_ATTR_CONDITION = "weather_code"
CC_V3_ATTR_VISIBILITY = "visibility"
CC_V3_ATTR_PRECIPITATION = "precipitation"
CC_V3_ATTR_PRECIPITATION_DAILY = "precipitation_accumulation"
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 = (
ClimaCellSensorEntityDescription(
key=CC_V3_ATTR_OZONE,
name="Ozone",
unit_imperial=CONCENTRATION_PARTS_PER_BILLION,
unit_metric=CONCENTRATION_PARTS_PER_BILLION,
),
ClimaCellSensorEntityDescription(
key=CC_V3_ATTR_PARTICULATE_MATTER_25,
name="Particulate Matter < 2.5 μm",
unit_imperial=CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT,
unit_metric=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
metric_conversion=3.2808399**3,
is_metric_check=False,
),
ClimaCellSensorEntityDescription(
key=CC_V3_ATTR_PARTICULATE_MATTER_10,
name="Particulate Matter < 10 μm",
unit_imperial=CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT,
unit_metric=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
metric_conversion=3.2808399**3,
is_metric_check=False,
),
ClimaCellSensorEntityDescription(
key=CC_V3_ATTR_NITROGEN_DIOXIDE,
name="Nitrogen Dioxide",
unit_imperial=CONCENTRATION_PARTS_PER_BILLION,
unit_metric=CONCENTRATION_PARTS_PER_BILLION,
),
ClimaCellSensorEntityDescription(
key=CC_V3_ATTR_CARBON_MONOXIDE,
name="Carbon Monoxide",
unit_imperial=CONCENTRATION_PARTS_PER_MILLION,
unit_metric=CONCENTRATION_PARTS_PER_MILLION,
device_class=SensorDeviceClass.CO,
),
ClimaCellSensorEntityDescription(
key=CC_V3_ATTR_SULFUR_DIOXIDE,
name="Sulfur Dioxide",
unit_imperial=CONCENTRATION_PARTS_PER_BILLION,
unit_metric=CONCENTRATION_PARTS_PER_BILLION,
),
ClimaCellSensorEntityDescription(
key=CC_V3_ATTR_EPA_AQI,
name="US EPA Air Quality Index",
),
ClimaCellSensorEntityDescription(
key=CC_V3_ATTR_EPA_PRIMARY_POLLUTANT,
name="US EPA Primary Pollutant",
),
ClimaCellSensorEntityDescription(
key=CC_V3_ATTR_EPA_HEALTH_CONCERN,
name="US EPA Health Concern",
),
ClimaCellSensorEntityDescription(
key=CC_V3_ATTR_CHINA_AQI,
name="China MEP Air Quality Index",
),
ClimaCellSensorEntityDescription(
key=CC_V3_ATTR_CHINA_PRIMARY_POLLUTANT,
name="China MEP Primary Pollutant",
),
ClimaCellSensorEntityDescription(
key=CC_V3_ATTR_CHINA_HEALTH_CONCERN,
name="China MEP Health Concern",
),
ClimaCellSensorEntityDescription(
key=CC_V3_ATTR_POLLEN_TREE,
name="Tree Pollen Index",
value_map=V3PollenIndex,
),
ClimaCellSensorEntityDescription(
key=CC_V3_ATTR_POLLEN_WEED,
name="Weed Pollen Index",
value_map=V3PollenIndex,
),
ClimaCellSensorEntityDescription(
key=CC_V3_ATTR_POLLEN_GRASS,
name="Grass Pollen Index",
value_map=V3PollenIndex,
),
ClimaCellSensorEntityDescription(
key=CC_V3_ATTR_FIRE_INDEX,
name="Fire Index",
),
)

View File

@ -1,11 +0,0 @@
{
"domain": "climacell",
"name": "ClimaCell",
"config_flow": false,
"documentation": "https://www.home-assistant.io/integrations/climacell",
"requirements": ["pyclimacell==0.18.2"],
"after_dependencies": ["tomorrowio"],
"codeowners": ["@raman325"],
"iot_class": "cloud_polling",
"loggers": ["pyclimacell"]
}

View File

@ -1,88 +0,0 @@
"""Sensor component that handles additional ClimaCell data for your location."""
from __future__ import annotations
from pyclimacell.const import CURRENT
from homeassistant.components.sensor import SensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_VERSION, CONF_NAME
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util import slugify
from . import ClimaCellDataUpdateCoordinator, ClimaCellEntity
from .const import CC_V3_SENSOR_TYPES, DOMAIN, ClimaCellSensorEntityDescription
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up a config entry."""
coordinator = hass.data[DOMAIN][config_entry.entry_id]
api_version = config_entry.data[CONF_API_VERSION]
entities = [
ClimaCellV3SensorEntity(
hass, config_entry, coordinator, api_version, description
)
for description in CC_V3_SENSOR_TYPES
]
async_add_entities(entities)
class ClimaCellV3SensorEntity(ClimaCellEntity, SensorEntity):
"""Sensor entity that talks to ClimaCell v3 API to retrieve non-weather data."""
entity_description: ClimaCellSensorEntityDescription
def __init__(
self,
hass: HomeAssistant,
config_entry: ConfigEntry,
coordinator: ClimaCellDataUpdateCoordinator,
api_version: int,
description: ClimaCellSensorEntityDescription,
) -> None:
"""Initialize ClimaCell Sensor Entity."""
super().__init__(config_entry, coordinator, api_version)
self.entity_description = description
self._attr_entity_registry_enabled_default = False
self._attr_name = f"{self._config_entry.data[CONF_NAME]} - {description.name}"
self._attr_unique_id = (
f"{self._config_entry.unique_id}_{slugify(description.name)}"
)
self._attr_extra_state_attributes = {ATTR_ATTRIBUTION: self.attribution}
self._attr_native_unit_of_measurement = (
description.unit_metric
if hass.config.units.is_metric
else description.unit_imperial
)
@property
def native_value(self) -> str | int | float | None:
"""Return the state."""
state = self._get_cc_value(
self.coordinator.data[CURRENT], self.entity_description.key
)
if (
state is not None
and not isinstance(state, str)
and self.entity_description.unit_imperial is not None
and self.entity_description.metric_conversion != 1.0
and self.entity_description.is_metric_check is not None
and self.hass.config.units.is_metric
== self.entity_description.is_metric_check
):
conversion = self.entity_description.metric_conversion
# When conversion is a callable, we assume it's a single input function
if callable(conversion):
return round(conversion(state), 4)
return round(state * conversion, 4)
if self.entity_description.value_map is not None and state is not None:
# mypy bug: "Literal[IntEnum.value]" not callable
return self.entity_description.value_map(state).name.lower() # type: ignore[misc]
return state

View File

@ -1,13 +0,0 @@
{
"options": {
"step": {
"init": {
"title": "Update ClimaCell Options",
"description": "If you choose to enable the `nowcast` forecast entity, you can configure the number of minutes between each forecast. The number of forecasts provided depends on the number of minutes chosen between forecasts.",
"data": {
"timestep": "Min. Between NowCast Forecasts"
}
}
}
}
}

View File

@ -1,27 +0,0 @@
{
"state": {
"climacell__pollen_index": {
"none": "None",
"very_low": "Very Low",
"low": "Low",
"medium": "Medium",
"high": "High",
"very_high": "Very High"
},
"climacell__health_concern": {
"good": "Good",
"moderate": "Moderate",
"unhealthy_for_sensitive_groups": "Unhealthy for Sensitive Groups",
"unhealthy": "Unhealthy",
"very_unhealthy": "Very Unhealthy",
"hazardous": "Hazardous"
},
"climacell__precipitation_type": {
"none": "None",
"rain": "Rain",
"snow": "Snow",
"freezing_rain": "Freezing Rain",
"ice_pellets": "Ice Pellets"
}
}
}

View File

@ -1,9 +0,0 @@
{
"options": {
"step": {
"init": {
"title": "Update [%key:component::climacell::title%] opties"
}
}
}
}

View File

@ -1,13 +0,0 @@
{
"options": {
"step": {
"init": {
"data": {
"timestep": "Minuts entre previsions NowCast"
},
"description": "Si decideixes activar l'entitat de previsi\u00f3 `nowcast`, podr\u00e0s configurar l'interval en minuts entre cada previsi\u00f3. El nombre de previsions proporcionades dep\u00e8n d'aquest interval de minuts.",
"title": "Actualitzaci\u00f3 d'opcions de ClimaCell"
}
}
}
}

View File

@ -1,13 +0,0 @@
{
"options": {
"step": {
"init": {
"data": {
"timestep": "Minuten zwischen den NowCast Kurzvorhersagen"
},
"description": "Wenn du die Vorhersage-Entitit\u00e4t \"Kurzvorhersage\" aktivierst, kannst du die Anzahl der Minuten zwischen den einzelnen Vorhersagen konfigurieren. Die Anzahl der bereitgestellten Vorhersagen h\u00e4ngt von der Anzahl der zwischen den Vorhersagen gew\u00e4hlten Minuten ab.",
"title": "ClimaCell-Optionen aktualisieren"
}
}
}
}

View File

@ -1,13 +0,0 @@
{
"options": {
"step": {
"init": {
"data": {
"timestep": "\u039b\u03b5\u03c0\u03c4\u03ac \u03bc\u03b5\u03c4\u03b1\u03be\u03cd \u03c4\u03c9\u03bd \u03b4\u03b5\u03bb\u03c4\u03af\u03c9\u03bd NowCast"
},
"description": "\u0395\u03ac\u03bd \u03b5\u03c0\u03b9\u03bb\u03ad\u03be\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1 \u03b4\u03b5\u03bb\u03c4\u03af\u03c9\u03bd 'nowcast', \u03bc\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc \u03c4\u03c9\u03bd \u03bb\u03b5\u03c0\u03c4\u03ce\u03bd \u03bc\u03b5\u03c4\u03b1\u03be\u03cd \u03ba\u03ac\u03b8\u03b5 \u03b4\u03b5\u03bb\u03c4\u03af\u03bf\u03c5. \u039f \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 \u03c4\u03c9\u03bd \u03b4\u03b5\u03bb\u03c4\u03af\u03c9\u03bd \u03c0\u03bf\u03c5 \u03c0\u03b1\u03c1\u03ad\u03c7\u03bf\u03bd\u03c4\u03b1\u03b9 \u03b5\u03be\u03b1\u03c1\u03c4\u03ac\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03bf\u03bd \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc \u03c4\u03c9\u03bd \u03bb\u03b5\u03c0\u03c4\u03ce\u03bd \u03c0\u03bf\u03c5 \u03b5\u03c0\u03b9\u03bb\u03ad\u03b3\u03bf\u03bd\u03c4\u03b1\u03b9 \u03bc\u03b5\u03c4\u03b1\u03be\u03cd \u03c4\u03c9\u03bd \u03b4\u03b5\u03bb\u03c4\u03af\u03c9\u03bd.",
"title": "\u0395\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ce\u03bd ClimaCell"
}
}
}
}

View File

@ -1,13 +0,0 @@
{
"options": {
"step": {
"init": {
"data": {
"timestep": "Min. Between NowCast Forecasts"
},
"description": "If you choose to enable the `nowcast` forecast entity, you can configure the number of minutes between each forecast. The number of forecasts provided depends on the number of minutes chosen between forecasts.",
"title": "Update ClimaCell Options"
}
}
}
}

View File

@ -1,13 +0,0 @@
{
"options": {
"step": {
"init": {
"data": {
"timestep": "Min. entre pron\u00f3sticos de NowCast"
},
"description": "Si elige habilitar la entidad de pron\u00f3stico \"nowcast\", puede configurar el n\u00famero de minutos entre cada pron\u00f3stico. El n\u00famero de pron\u00f3sticos proporcionados depende del n\u00famero de minutos elegidos entre los pron\u00f3sticos.",
"title": "Actualizar opciones de ClimaCell"
}
}
}
}

View File

@ -1,13 +0,0 @@
{
"options": {
"step": {
"init": {
"data": {
"timestep": "Min. entre pron\u00f3sticos de NowCast"
},
"description": "Si eliges habilitar la entidad de pron\u00f3stico `nowcast`, puedes configurar la cantidad de minutos entre cada pron\u00f3stico. La cantidad de pron\u00f3sticos proporcionados depende de la cantidad de minutos elegidos entre los pron\u00f3sticos.",
"title": "Actualizar opciones de ClimaCell"
}
}
}
}

View File

@ -1,13 +0,0 @@
{
"options": {
"step": {
"init": {
"data": {
"timestep": "Minuteid NowCasti prognooside vahel"
},
"description": "Kui otsustad lubada \"nowcast\" prognoosi\u00fcksuse, saad seadistada minutite arvu iga prognoosi vahel. Esitatavate prognooside arv s\u00f5ltub prognooside vahel valitud minutite arvust.",
"title": "V\u00e4rskenda [%key:component::climacell::title%] suvandeid"
}
}
}
}

View File

@ -1,13 +0,0 @@
{
"options": {
"step": {
"init": {
"data": {
"timestep": "Min. entre les pr\u00e9visions NowCast"
},
"description": "Si vous choisissez d'activer l'entit\u00e9 de pr\u00e9vision \u00ab\u00a0nowcast\u00a0\u00bb, vous pouvez configurer le nombre de minutes entre chaque pr\u00e9vision. Le nombre de pr\u00e9visions fournies d\u00e9pend du nombre de minutes choisies entre les pr\u00e9visions.",
"title": "Mettre \u00e0 jour les options ClimaCell"
}
}
}
}

View File

@ -1,13 +0,0 @@
{
"options": {
"step": {
"init": {
"data": {
"timestep": "Min. A NowCast el\u0151rejelz\u00e9sek k\u00f6z\u00f6tt"
},
"description": "Ha a `nowcast` el\u0151rejelz\u00e9si entit\u00e1s enged\u00e9lyez\u00e9s\u00e9t v\u00e1lasztja, be\u00e1ll\u00edthatja az egyes el\u0151rejelz\u00e9sek k\u00f6z\u00f6tti percek sz\u00e1m\u00e1t. A megadott el\u0151rejelz\u00e9sek sz\u00e1ma az el\u0151rejelz\u00e9sek k\u00f6z\u00f6tt kiv\u00e1lasztott percek sz\u00e1m\u00e1t\u00f3l f\u00fcgg.",
"title": "ClimaCell be\u00e1ll\u00edt\u00e1sok friss\u00edt\u00e9se"
}
}
}
}

View File

@ -1,13 +0,0 @@
{
"options": {
"step": {
"init": {
"data": {
"timestep": "Jarak Interval Prakiraan NowCast dalam Menit"
},
"description": "Jika Anda memilih untuk mengaktifkan entitas prakiraan `nowcast`, Anda dapat mengonfigurasi jarak interval prakiraan dalam menit. Jumlah prakiraan yang diberikan tergantung pada nilai interval yang dipilih.",
"title": "Perbarui Opsi ClimaCell"
}
}
}
}

View File

@ -1,13 +0,0 @@
{
"options": {
"step": {
"init": {
"data": {
"timestep": "Minuti tra le previsioni di NowCast"
},
"description": "Se scegli di abilitare l'entit\u00e0 di previsione `nowcast`, puoi configurare il numero di minuti tra ogni previsione. Il numero di previsioni fornite dipende dal numero di minuti scelti tra le previsioni.",
"title": "Aggiorna le opzioni di ClimaCell"
}
}
}
}

View File

@ -1,13 +0,0 @@
{
"options": {
"step": {
"init": {
"data": {
"timestep": "\u6700\u5c0f\u3002 NowCast \u4e88\u6e2c\u306e\u9593"
},
"description": "`nowcast` forecast(\u4e88\u6e2c) \u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u3092\u6709\u52b9\u306b\u3059\u308b\u3053\u3068\u3092\u9078\u629e\u3057\u305f\u5834\u5408\u3001\u5404\u4e88\u6e2c\u9593\u306e\u5206\u6570\u3092\u8a2d\u5b9a\u3059\u308b\u3053\u3068\u304c\u3067\u304d\u307e\u3059\u3002\u63d0\u4f9b\u3055\u308c\u308bforecast(\u4e88\u6e2c)\u306e\u6570\u306f\u3001forecast(\u4e88\u6e2c)\u306e\u9593\u306b\u9078\u629e\u3057\u305f\u5206\u6570\u306b\u4f9d\u5b58\u3057\u307e\u3059\u3002",
"title": "ClimaCell \u30aa\u30d7\u30b7\u30e7\u30f3\u306e\u66f4\u65b0"
}
}
}
}

View File

@ -1,13 +0,0 @@
{
"options": {
"step": {
"init": {
"data": {
"timestep": "\ub2e8\uae30\uc608\uce21 \uc77c\uae30\uc608\ubcf4 \uac04 \ucd5c\uc18c \uc2dc\uac04"
},
"description": "`nowcast` \uc77c\uae30\uc608\ubcf4 \uad6c\uc131\uc694\uc18c\ub97c \uc0ac\uc6a9\ud558\ub3c4\ub85d \uc120\ud0dd\ud55c \uacbd\uc6b0 \uac01 \uc77c\uae30\uc608\ubcf4 \uc0ac\uc774\uc758 \uc2dc\uac04(\ubd84)\uc744 \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \uc81c\uacf5\ub41c \uc77c\uae30\uc608\ubcf4 \ud69f\uc218\ub294 \uc608\uce21 \uac04 \uc120\ud0dd\ud55c \uc2dc\uac04(\ubd84)\uc5d0 \ub530\ub77c \ub2ec\ub77c\uc9d1\ub2c8\ub2e4.",
"title": "[%key:component::climacell::title%] \uc635\uc158 \uc5c5\ub370\uc774\ud2b8\ud558\uae30"
}
}
}
}

View File

@ -1,13 +0,0 @@
{
"options": {
"step": {
"init": {
"data": {
"timestep": "Min. Tussen NowCast-voorspellingen"
},
"description": "Als u ervoor kiest om de `nowcast` voorspellingsentiteit in te schakelen, kan u het aantal minuten tussen elke voorspelling configureren. Het aantal voorspellingen hangt af van het aantal gekozen minuten tussen de voorspellingen.",
"title": "Update ClimaCell Opties"
}
}
}
}

View File

@ -1,13 +0,0 @@
{
"options": {
"step": {
"init": {
"data": {
"timestep": "Min. mellom NowCast prognoser"
},
"description": "Hvis du velger \u00e5 aktivere \u00abnowcast\u00bb -varselentiteten, kan du konfigurere antall minutter mellom hver prognose. Antall angitte prognoser avhenger av antall minutter som er valgt mellom prognosene.",
"title": "Oppdater ClimaCell-alternativer"
}
}
}
}

View File

@ -1,13 +0,0 @@
{
"options": {
"step": {
"init": {
"data": {
"timestep": "Czas (min) mi\u0119dzy prognozami NowCast"
},
"description": "Je\u015bli zdecydujesz si\u0119 w\u0142\u0105czy\u0107 encj\u0119 prognozy \u201enowcast\u201d, mo\u017cesz skonfigurowa\u0107 liczb\u0119 minut mi\u0119dzy ka\u017cd\u0105 prognoz\u0105. Liczba dostarczonych prognoz zale\u017cy od liczby minut wybranych mi\u0119dzy prognozami.",
"title": "Opcje aktualizacji ClimaCell"
}
}
}
}

View File

@ -1,13 +0,0 @@
{
"options": {
"step": {
"init": {
"data": {
"timestep": "M\u00ednimo entre previs\u00f5es NowCast"
},
"description": "Se voc\u00ea optar por ativar a entidade de previs\u00e3o `nowcast`, poder\u00e1 configurar o n\u00famero de minutos entre cada previs\u00e3o. O n\u00famero de previs\u00f5es fornecidas depende do n\u00famero de minutos escolhidos entre as previs\u00f5es.",
"title": "Atualizar as op\u00e7\u00f5es do ClimaCell"
}
}
}
}

View File

@ -1,13 +0,0 @@
{
"options": {
"step": {
"init": {
"data": {
"timestep": "\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f (\u0432 \u043c\u0438\u043d\u0443\u0442\u0430\u0445)"
},
"description": "\u0415\u0441\u043b\u0438 \u0412\u044b \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u0443\u0435\u0442\u0435 \u043e\u0431\u044a\u0435\u043a\u0442 \u043f\u0440\u043e\u0433\u043d\u043e\u0437\u0430 'nowcast', \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u0438\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f \u043f\u0440\u043e\u0433\u043d\u043e\u0437\u0430.",
"title": "\u041e\u0431\u043d\u043e\u0432\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 ClimaCell"
}
}
}
}

View File

@ -1,8 +0,0 @@
{
"state": {
"climacell__precipitation_type": {
"rain": "\u0414\u044a\u0436\u0434",
"snow": "\u0421\u043d\u044f\u0433"
}
}
}

View File

@ -1,27 +0,0 @@
{
"state": {
"climacell__health_concern": {
"good": "Bo",
"hazardous": "Perill\u00f3s",
"moderate": "Moderat",
"unhealthy": "Poc saludable",
"unhealthy_for_sensitive_groups": "No saludable per a grups sensibles",
"very_unhealthy": "Gens saludable"
},
"climacell__pollen_index": {
"high": "Alt",
"low": "Baix",
"medium": "Mitj\u00e0",
"none": "Cap",
"very_high": "Molt alt",
"very_low": "Molt baix"
},
"climacell__precipitation_type": {
"freezing_rain": "Pluja congelada",
"ice_pellets": "Gran\u00eds",
"none": "Cap",
"rain": "Pluja",
"snow": "Neu"
}
}
}

View File

@ -1,27 +0,0 @@
{
"state": {
"climacell__health_concern": {
"good": "Gut",
"hazardous": "Gef\u00e4hrlich",
"moderate": "M\u00e4\u00dfig",
"unhealthy": "Ungesund",
"unhealthy_for_sensitive_groups": "Ungesund f\u00fcr sensible Gruppen",
"very_unhealthy": "Sehr ungesund"
},
"climacell__pollen_index": {
"high": "Hoch",
"low": "Niedrig",
"medium": "Mittel",
"none": "Keine",
"very_high": "Sehr hoch",
"very_low": "Sehr niedrig"
},
"climacell__precipitation_type": {
"freezing_rain": "Gefrierender Regen",
"ice_pellets": "Graupel",
"none": "Keine",
"rain": "Regen",
"snow": "Schnee"
}
}
}

View File

@ -1,27 +0,0 @@
{
"state": {
"climacell__health_concern": {
"good": "\u039a\u03b1\u03bb\u03cc",
"hazardous": "\u0395\u03c0\u03b9\u03ba\u03af\u03bd\u03b4\u03c5\u03bd\u03bf",
"moderate": "\u039c\u03ad\u03c4\u03c1\u03b9\u03bf",
"unhealthy": "\u0391\u03bd\u03b8\u03c5\u03b3\u03b9\u03b5\u03b9\u03bd\u03cc",
"unhealthy_for_sensitive_groups": "\u0391\u03bd\u03b8\u03c5\u03b3\u03b9\u03b5\u03b9\u03bd\u03cc \u03b3\u03b9\u03b1 \u03b5\u03c5\u03b1\u03af\u03c3\u03b8\u03b7\u03c4\u03b5\u03c2 \u03bf\u03bc\u03ac\u03b4\u03b5\u03c2",
"very_unhealthy": "\u03a0\u03bf\u03bb\u03cd \u0391\u03bd\u03b8\u03c5\u03b3\u03b9\u03b5\u03b9\u03bd\u03cc"
},
"climacell__pollen_index": {
"high": "\u03a5\u03c8\u03b7\u03bb\u03cc",
"low": "\u03a7\u03b1\u03bc\u03b7\u03bb\u03cc",
"medium": "\u039c\u03b5\u03c3\u03b1\u03af\u03bf",
"none": "\u03a4\u03af\u03c0\u03bf\u03c4\u03b1",
"very_high": "\u03a0\u03bf\u03bb\u03cd \u03c5\u03c8\u03b7\u03bb\u03cc",
"very_low": "\u03a0\u03bf\u03bb\u03cd \u03c7\u03b1\u03bc\u03b7\u03bb\u03cc"
},
"climacell__precipitation_type": {
"freezing_rain": "\u03a0\u03b1\u03b3\u03c9\u03bc\u03ad\u03bd\u03b7 \u03b2\u03c1\u03bf\u03c7\u03ae",
"ice_pellets": "\u03a0\u03ad\u03bb\u03bb\u03b5\u03c4 \u03c0\u03ac\u03b3\u03bf\u03c5",
"none": "\u03a4\u03af\u03c0\u03bf\u03c4\u03b1",
"rain": "\u0392\u03c1\u03bf\u03c7\u03ae",
"snow": "\u03a7\u03b9\u03cc\u03bd\u03b9"
}
}
}

View File

@ -1,27 +0,0 @@
{
"state": {
"climacell__health_concern": {
"good": "Good",
"hazardous": "Hazardous",
"moderate": "Moderate",
"unhealthy": "Unhealthy",
"unhealthy_for_sensitive_groups": "Unhealthy for Sensitive Groups",
"very_unhealthy": "Very Unhealthy"
},
"climacell__pollen_index": {
"high": "High",
"low": "Low",
"medium": "Medium",
"none": "None",
"very_high": "Very High",
"very_low": "Very Low"
},
"climacell__precipitation_type": {
"freezing_rain": "Freezing Rain",
"ice_pellets": "Ice Pellets",
"none": "None",
"rain": "Rain",
"snow": "Snow"
}
}
}

View File

@ -1,27 +0,0 @@
{
"state": {
"climacell__health_concern": {
"good": "Bueno",
"hazardous": "Peligroso",
"moderate": "Moderado",
"unhealthy": "Insalubre",
"unhealthy_for_sensitive_groups": "Insalubre para grupos sensibles",
"very_unhealthy": "Muy poco saludable"
},
"climacell__pollen_index": {
"high": "Alto",
"low": "Bajo",
"medium": "Medio",
"none": "Ninguno",
"very_high": "Muy alto",
"very_low": "Muy bajo"
},
"climacell__precipitation_type": {
"freezing_rain": "Lluvia helada",
"ice_pellets": "Gr\u00e1nulos de hielo",
"none": "Ninguno",
"rain": "Lluvia",
"snow": "Nieve"
}
}
}

View File

@ -1,27 +0,0 @@
{
"state": {
"climacell__health_concern": {
"good": "Bueno",
"hazardous": "Peligroso",
"moderate": "Moderado",
"unhealthy": "No saludable",
"unhealthy_for_sensitive_groups": "No es saludable para grupos sensibles",
"very_unhealthy": "Muy poco saludable"
},
"climacell__pollen_index": {
"high": "Alto",
"low": "Bajo",
"medium": "Medio",
"none": "Ninguno",
"very_high": "Muy alto",
"very_low": "Muy bajo"
},
"climacell__precipitation_type": {
"freezing_rain": "Lluvia helada",
"ice_pellets": "Granizo",
"none": "Ninguna",
"rain": "Lluvia",
"snow": "Nieve"
}
}
}

View File

@ -1,27 +0,0 @@
{
"state": {
"climacell__health_concern": {
"good": "Normaalne",
"hazardous": "Ohtlik",
"moderate": "M\u00f5\u00f5dukas",
"unhealthy": "Ebatervislik",
"unhealthy_for_sensitive_groups": "Ebatervislik riskir\u00fchmale",
"very_unhealthy": "V\u00e4ga ebatervislik"
},
"climacell__pollen_index": {
"high": "K\u00f5rge",
"low": "Madal",
"medium": "Keskmine",
"none": "Puudub",
"very_high": "V\u00e4ga k\u00f5rge",
"very_low": "V\u00e4ga madal"
},
"climacell__precipitation_type": {
"freezing_rain": "J\u00e4\u00e4vihm",
"ice_pellets": "J\u00e4\u00e4kruubid",
"none": "Sademeid pole",
"rain": "Vihm",
"snow": "Lumi"
}
}
}

View File

@ -1,27 +0,0 @@
{
"state": {
"climacell__health_concern": {
"good": "Bon",
"hazardous": "Dangereux",
"moderate": "Mod\u00e9r\u00e9",
"unhealthy": "Mauvais pour la sant\u00e9",
"unhealthy_for_sensitive_groups": "Mauvaise qualit\u00e9 pour les groupes sensibles",
"very_unhealthy": "Tr\u00e8s mauvais pour la sant\u00e9"
},
"climacell__pollen_index": {
"high": "Haut",
"low": "Faible",
"medium": "Moyen",
"none": "Aucun",
"very_high": "Tr\u00e8s \u00e9lev\u00e9",
"very_low": "Tr\u00e8s faible"
},
"climacell__precipitation_type": {
"freezing_rain": "Pluie vergla\u00e7ante",
"ice_pellets": "Gr\u00e9sil",
"none": "Aucun",
"rain": "Pluie",
"snow": "Neige"
}
}
}

View File

@ -1,7 +0,0 @@
{
"state": {
"climacell__health_concern": {
"unhealthy_for_sensitive_groups": "\u05dc\u05d0 \u05d1\u05e8\u05d9\u05d0 \u05dc\u05e7\u05d1\u05d5\u05e6\u05d5\u05ea \u05e8\u05d2\u05d9\u05e9\u05d5\u05ea"
}
}
}

View File

@ -1,27 +0,0 @@
{
"state": {
"climacell__health_concern": {
"good": "J\u00f3",
"hazardous": "Vesz\u00e9lyes",
"moderate": "M\u00e9rs\u00e9kelt",
"unhealthy": "Eg\u00e9szs\u00e9gtelen",
"unhealthy_for_sensitive_groups": "Eg\u00e9szs\u00e9gtelen \u00e9rz\u00e9keny csoportok sz\u00e1m\u00e1ra",
"very_unhealthy": "Nagyon eg\u00e9szs\u00e9gtelen"
},
"climacell__pollen_index": {
"high": "Magas",
"low": "Alacsony",
"medium": "K\u00f6zepes",
"none": "Nincs",
"very_high": "Nagyon magas",
"very_low": "Nagyon alacsony"
},
"climacell__precipitation_type": {
"freezing_rain": "Havas es\u0151",
"ice_pellets": "\u00d3nos es\u0151",
"none": "Nincs",
"rain": "Es\u0151",
"snow": "Havaz\u00e1s"
}
}
}

View File

@ -1,27 +0,0 @@
{
"state": {
"climacell__health_concern": {
"good": "Bagus",
"hazardous": "Berbahaya",
"moderate": "Sedang",
"unhealthy": "Tidak Sehat",
"unhealthy_for_sensitive_groups": "Tidak Sehat untuk Kelompok Sensitif",
"very_unhealthy": "Sangat Tidak Sehat"
},
"climacell__pollen_index": {
"high": "Tinggi",
"low": "Rendah",
"medium": "Sedang",
"none": "Tidak Ada",
"very_high": "Sangat Tinggi",
"very_low": "Sangat Rendah"
},
"climacell__precipitation_type": {
"freezing_rain": "Hujan Beku",
"ice_pellets": "Hujan Es",
"none": "Tidak Ada",
"rain": "Hujan",
"snow": "Salju"
}
}
}

View File

@ -1,12 +0,0 @@
{
"state": {
"climacell__health_concern": {
"hazardous": "H\u00e6ttulegt",
"unhealthy": "\u00d3hollt"
},
"climacell__precipitation_type": {
"rain": "Rigning",
"snow": "Snj\u00f3r"
}
}
}

View File

@ -1,27 +0,0 @@
{
"state": {
"climacell__health_concern": {
"good": "Buono",
"hazardous": "Pericoloso",
"moderate": "Moderato",
"unhealthy": "Malsano",
"unhealthy_for_sensitive_groups": "Malsano per gruppi sensibili",
"very_unhealthy": "Molto malsano"
},
"climacell__pollen_index": {
"high": "Alto",
"low": "Basso",
"medium": "Medio",
"none": "Nessuno",
"very_high": "Molto alto",
"very_low": "Molto basso"
},
"climacell__precipitation_type": {
"freezing_rain": "Grandine",
"ice_pellets": "Pioggia gelata",
"none": "Nessuno",
"rain": "Pioggia",
"snow": "Neve"
}
}
}

View File

@ -1,27 +0,0 @@
{
"state": {
"climacell__health_concern": {
"good": "\u826f\u597d",
"hazardous": "\u5371\u967a",
"moderate": "\u7a4f\u3084\u304b\u306a",
"unhealthy": "\u4e0d\u5065\u5eb7",
"unhealthy_for_sensitive_groups": "\u654f\u611f\u306a\u30b0\u30eb\u30fc\u30d7\u306b\u3068\u3063\u3066\u306f\u4e0d\u5065\u5eb7",
"very_unhealthy": "\u975e\u5e38\u306b\u4e0d\u5065\u5eb7"
},
"climacell__pollen_index": {
"high": "\u9ad8\u3044",
"low": "\u4f4e\u3044",
"medium": "\u4e2d",
"none": "\u306a\u3057",
"very_high": "\u975e\u5e38\u306b\u9ad8\u3044",
"very_low": "\u3068\u3066\u3082\u4f4e\u3044"
},
"climacell__precipitation_type": {
"freezing_rain": "\u51cd\u3066\u3064\u304f\u96e8",
"ice_pellets": "\u51cd\u96e8",
"none": "\u306a\u3057",
"rain": "\u96e8",
"snow": "\u96ea"
}
}
}

View File

@ -1,7 +0,0 @@
{
"state": {
"climacell__precipitation_type": {
"snow": "\ub208"
}
}
}

View File

@ -1,27 +0,0 @@
{
"state": {
"climacell__health_concern": {
"good": "Labs",
"hazardous": "B\u012bstams",
"moderate": "M\u0113rens",
"unhealthy": "Nevesel\u012bgs",
"unhealthy_for_sensitive_groups": "Nevesel\u012bgs jut\u012bg\u0101m grup\u0101m",
"very_unhealthy": "\u013boti nevesel\u012bgs"
},
"climacell__pollen_index": {
"high": "Augsts",
"low": "Zems",
"medium": "Vid\u0113js",
"none": "Nav",
"very_high": "\u013boti augsts",
"very_low": "\u013boti zems"
},
"climacell__precipitation_type": {
"freezing_rain": "Sasalsto\u0161s lietus",
"ice_pellets": "Krusa",
"none": "Nav",
"rain": "Lietus",
"snow": "Sniegs"
}
}
}

View File

@ -1,27 +0,0 @@
{
"state": {
"climacell__health_concern": {
"good": "Goed",
"hazardous": "Gevaarlijk",
"moderate": "Gematigd",
"unhealthy": "Ongezond",
"unhealthy_for_sensitive_groups": "Ongezond voor gevoelige groepen",
"very_unhealthy": "Zeer ongezond"
},
"climacell__pollen_index": {
"high": "Hoog",
"low": "Laag",
"medium": "Medium",
"none": "Geen",
"very_high": "Zeer Hoog",
"very_low": "Zeer Laag"
},
"climacell__precipitation_type": {
"freezing_rain": "IJzel",
"ice_pellets": "IJskorrels",
"none": "Geen",
"rain": "Regen",
"snow": "Sneeuw"
}
}
}

View File

@ -1,27 +0,0 @@
{
"state": {
"climacell__health_concern": {
"good": "Bra",
"hazardous": "Farlig",
"moderate": "Moderat",
"unhealthy": "Usunt",
"unhealthy_for_sensitive_groups": "Usunt for sensitive grupper",
"very_unhealthy": "Veldig usunt"
},
"climacell__pollen_index": {
"high": "H\u00f8y",
"low": "Lav",
"medium": "Medium",
"none": "Ingen",
"very_high": "Veldig h\u00f8y",
"very_low": "Veldig lav"
},
"climacell__precipitation_type": {
"freezing_rain": "Underkj\u00f8lt regn",
"ice_pellets": "Is tapper",
"none": "Ingen",
"rain": "Regn",
"snow": "Sn\u00f8"
}
}
}

View File

@ -1,27 +0,0 @@
{
"state": {
"climacell__health_concern": {
"good": "dobre",
"hazardous": "niebezpieczne",
"moderate": "umiarkowane",
"unhealthy": "niezdrowe",
"unhealthy_for_sensitive_groups": "niezdrowe dla grup wra\u017cliwych",
"very_unhealthy": "bardzo niezdrowe"
},
"climacell__pollen_index": {
"high": "wysokie",
"low": "niskie",
"medium": "\u015brednie",
"none": "brak",
"very_high": "bardzo wysokie",
"very_low": "bardzo niskie"
},
"climacell__precipitation_type": {
"freezing_rain": "marzn\u0105cy deszcz",
"ice_pellets": "granulki lodu",
"none": "brak",
"rain": "deszcz",
"snow": "\u015bnieg"
}
}
}

View File

@ -1,27 +0,0 @@
{
"state": {
"climacell__health_concern": {
"good": "Bom",
"hazardous": "Perigosos",
"moderate": "Moderado",
"unhealthy": "Pouco saud\u00e1vel",
"unhealthy_for_sensitive_groups": "Insalubre para grupos sens\u00edveis",
"very_unhealthy": "Muito prejudicial \u00e0 sa\u00fade"
},
"climacell__pollen_index": {
"high": "Alto",
"low": "Baixo",
"medium": "M\u00e9dio",
"none": "Nenhum",
"very_high": "Muito alto",
"very_low": "Muito baixo"
},
"climacell__precipitation_type": {
"freezing_rain": "Chuva congelante",
"ice_pellets": "Granizo",
"none": "Nenhum",
"rain": "Chuva",
"snow": "Neve"
}
}
}

View File

@ -1,7 +0,0 @@
{
"state": {
"climacell__health_concern": {
"unhealthy_for_sensitive_groups": "Pouco saud\u00e1vel para grupos sens\u00edveis"
}
}
}

View File

@ -1,27 +0,0 @@
{
"state": {
"climacell__health_concern": {
"good": "\u0425\u043e\u0440\u043e\u0448\u043e",
"hazardous": "\u041e\u043f\u0430\u0441\u043d\u043e",
"moderate": "\u0421\u0440\u0435\u0434\u043d\u0435",
"unhealthy": "\u0412\u0440\u0435\u0434\u043d\u043e",
"unhealthy_for_sensitive_groups": "\u0412\u0440\u0435\u0434\u043d\u043e \u0434\u043b\u044f \u0443\u044f\u0437\u0432\u0438\u043c\u044b\u0445 \u0433\u0440\u0443\u043f\u043f",
"very_unhealthy": "\u041e\u0447\u0435\u043d\u044c \u0432\u0440\u0435\u0434\u043d\u043e"
},
"climacell__pollen_index": {
"high": "\u0412\u044b\u0441\u043e\u043a\u0438\u0439",
"low": "\u041d\u0438\u0437\u043a\u0438\u0439",
"medium": "\u0421\u0440\u0435\u0434\u043d\u0438\u0439",
"none": "\u041e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u0435\u0442",
"very_high": "\u041e\u0447\u0435\u043d\u044c \u0432\u044b\u0441\u043e\u043a\u0438\u0439",
"very_low": "\u041e\u0447\u0435\u043d\u044c \u043d\u0438\u0437\u043a\u0438\u0439"
},
"climacell__precipitation_type": {
"freezing_rain": "\u041b\u0435\u0434\u044f\u043d\u043e\u0439 \u0434\u043e\u0436\u0434\u044c",
"ice_pellets": "\u041b\u0435\u0434\u044f\u043d\u0430\u044f \u043a\u0440\u0443\u043f\u0430",
"none": "\u041e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u0435\u0442",
"rain": "\u0414\u043e\u0436\u0434\u044c",
"snow": "\u0421\u043d\u0435\u0433"
}
}
}

View File

@ -1,7 +0,0 @@
{
"state": {
"climacell__health_concern": {
"unhealthy": "Nezdrav\u00e9"
}
}
}

View File

@ -1,27 +0,0 @@
{
"state": {
"climacell__health_concern": {
"good": "Bra",
"hazardous": "Farligt",
"moderate": "M\u00e5ttligt",
"unhealthy": "Oh\u00e4lsosamt",
"unhealthy_for_sensitive_groups": "Oh\u00e4lsosamt f\u00f6r k\u00e4nsliga grupper",
"very_unhealthy": "Mycket oh\u00e4lsosamt"
},
"climacell__pollen_index": {
"high": "H\u00f6gt",
"low": "L\u00e5gt",
"medium": "Medium",
"none": "Inget",
"very_high": "V\u00e4ldigt h\u00f6gt",
"very_low": "V\u00e4ldigt l\u00e5gt"
},
"climacell__precipitation_type": {
"freezing_rain": "Underkylt regn",
"ice_pellets": "Hagel",
"none": "Ingen",
"rain": "Regn",
"snow": "Sn\u00f6"
}
}
}

View File

@ -1,27 +0,0 @@
{
"state": {
"climacell__health_concern": {
"good": "\u0130yi",
"hazardous": "Tehlikeli",
"moderate": "Il\u0131ml\u0131",
"unhealthy": "Sa\u011fl\u0131ks\u0131z",
"unhealthy_for_sensitive_groups": "Hassas Gruplar \u0130\u00e7in Sa\u011fl\u0131ks\u0131z",
"very_unhealthy": "\u00c7ok Sa\u011fl\u0131ks\u0131z"
},
"climacell__pollen_index": {
"high": "Y\u00fcksek",
"low": "D\u00fc\u015f\u00fck",
"medium": "Orta",
"none": "Hi\u00e7biri",
"very_high": "\u00c7ok Y\u00fcksek",
"very_low": "\u00c7ok D\u00fc\u015f\u00fck"
},
"climacell__precipitation_type": {
"freezing_rain": "Dondurucu Ya\u011fmur",
"ice_pellets": "Buz Peletleri",
"none": "Hi\u00e7biri",
"rain": "Ya\u011fmur",
"snow": "Kar"
}
}
}

View File

@ -1,27 +0,0 @@
{
"state": {
"climacell__health_concern": {
"good": "\u826f\u597d",
"hazardous": "\u5371\u96aa",
"moderate": "\u4e2d\u7b49",
"unhealthy": "\u4e0d\u5065\u5eb7",
"unhealthy_for_sensitive_groups": "\u5c0d\u654f\u611f\u65cf\u7fa4\u4e0d\u5065\u5eb7",
"very_unhealthy": "\u975e\u5e38\u4e0d\u5065\u5eb7"
},
"climacell__pollen_index": {
"high": "\u9ad8",
"low": "\u4f4e",
"medium": "\u4e2d",
"none": "\u7121",
"very_high": "\u6975\u9ad8",
"very_low": "\u6975\u4f4e"
},
"climacell__precipitation_type": {
"freezing_rain": "\u51cd\u96e8",
"ice_pellets": "\u51b0\u73e0",
"none": "\u7121",
"rain": "\u4e0b\u96e8",
"snow": "\u4e0b\u96ea"
}
}
}

View File

@ -1,13 +0,0 @@
{
"options": {
"step": {
"init": {
"data": {
"timestep": "Min. Mellan NowCast-prognoser"
},
"description": "Om du v\u00e4ljer att aktivera \"nowcast\"-prognosentiteten kan du konfigurera antalet minuter mellan varje prognos. Antalet prognoser som tillhandah\u00e5lls beror p\u00e5 antalet minuter som v\u00e4ljs mellan prognoserna.",
"title": "Uppdatera ClimaCell-alternativ"
}
}
}
}

View File

@ -1,13 +0,0 @@
{
"options": {
"step": {
"init": {
"data": {
"timestep": "Min. NowCast Tahminleri Aras\u0131nda"
},
"description": "'Nowcast' tahmin varl\u0131\u011f\u0131n\u0131 etkinle\u015ftirmeyi se\u00e7erseniz, her tahmin aras\u0131ndaki dakika say\u0131s\u0131n\u0131 yap\u0131land\u0131rabilirsiniz. Sa\u011flanan tahmin say\u0131s\u0131, tahminler aras\u0131nda se\u00e7ilen dakika say\u0131s\u0131na ba\u011fl\u0131d\u0131r.",
"title": "ClimaCell Se\u00e7eneklerini G\u00fcncelleyin"
}
}
}
}

View File

@ -1,13 +0,0 @@
{
"options": {
"step": {
"init": {
"data": {
"timestep": "NowCast \u9810\u5831\u9593\u9694\u5206\u9418"
},
"description": "\u5047\u5982\u9078\u64c7\u958b\u555f `nowcast` \u9810\u5831\u5be6\u9ad4\u3001\u5c07\u53ef\u4ee5\u8a2d\u5b9a\u9810\u5831\u983b\u7387\u9593\u9694\u5206\u9418\u6578\u3002\u6839\u64da\u6240\u8f38\u5165\u7684\u9593\u9694\u6642\u9593\u5c07\u6c7a\u5b9a\u9810\u5831\u7684\u6578\u76ee\u3002",
"title": "\u66f4\u65b0 ClimaCell \u9078\u9805"
}
}
}
}

View File

@ -1,361 +0,0 @@
"""Weather component that handles meteorological data for your location."""
from __future__ import annotations
from abc import abstractmethod
from collections.abc import Mapping
from datetime import datetime
from typing import Any, cast
from pyclimacell.const import CURRENT, DAILY, FORECASTS, HOURLY, NOWCAST
from homeassistant.components.weather import (
ATTR_FORECAST_CONDITION,
ATTR_FORECAST_NATIVE_PRECIPITATION,
ATTR_FORECAST_NATIVE_TEMP,
ATTR_FORECAST_NATIVE_TEMP_LOW,
ATTR_FORECAST_NATIVE_WIND_SPEED,
ATTR_FORECAST_PRECIPITATION_PROBABILITY,
ATTR_FORECAST_TIME,
ATTR_FORECAST_WIND_BEARING,
WeatherEntity,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_API_VERSION,
CONF_NAME,
LENGTH_INCHES,
LENGTH_MILES,
PRESSURE_INHG,
SPEED_MILES_PER_HOUR,
TEMP_FAHRENHEIT,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.sun import is_up
from homeassistant.util import dt as dt_util
from homeassistant.util.speed import convert as speed_convert
from . import ClimaCellDataUpdateCoordinator, ClimaCellEntity
from .const import (
ATTR_CLOUD_COVER,
ATTR_PRECIPITATION_TYPE,
ATTR_WIND_GUST,
CC_V3_ATTR_CLOUD_COVER,
CC_V3_ATTR_CONDITION,
CC_V3_ATTR_HUMIDITY,
CC_V3_ATTR_OZONE,
CC_V3_ATTR_PRECIPITATION,
CC_V3_ATTR_PRECIPITATION_DAILY,
CC_V3_ATTR_PRECIPITATION_PROBABILITY,
CC_V3_ATTR_PRECIPITATION_TYPE,
CC_V3_ATTR_PRESSURE,
CC_V3_ATTR_TEMPERATURE,
CC_V3_ATTR_TEMPERATURE_HIGH,
CC_V3_ATTR_TEMPERATURE_LOW,
CC_V3_ATTR_TIMESTAMP,
CC_V3_ATTR_VISIBILITY,
CC_V3_ATTR_WIND_DIRECTION,
CC_V3_ATTR_WIND_GUST,
CC_V3_ATTR_WIND_SPEED,
CLEAR_CONDITIONS,
CONDITIONS_V3,
CONF_TIMESTEP,
DEFAULT_FORECAST_TYPE,
DOMAIN,
)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up a config entry."""
coordinator = hass.data[DOMAIN][config_entry.entry_id]
api_version = config_entry.data[CONF_API_VERSION]
entities = [
ClimaCellV3WeatherEntity(config_entry, coordinator, api_version, forecast_type)
for forecast_type in (DAILY, HOURLY, NOWCAST)
]
async_add_entities(entities)
class BaseClimaCellWeatherEntity(ClimaCellEntity, WeatherEntity):
"""Base ClimaCell weather entity."""
_attr_native_precipitation_unit = LENGTH_INCHES
_attr_native_pressure_unit = PRESSURE_INHG
_attr_native_temperature_unit = TEMP_FAHRENHEIT
_attr_native_visibility_unit = LENGTH_MILES
_attr_native_wind_speed_unit = SPEED_MILES_PER_HOUR
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
self._attr_entity_registry_enabled_default = (
forecast_type == DEFAULT_FORECAST_TYPE
)
self._attr_name = f"{config_entry.data[CONF_NAME]} - {forecast_type.title()}"
self._attr_unique_id = f"{config_entry.unique_id}_{forecast_type}"
@staticmethod
@abstractmethod
def _translate_condition(
condition: str | int | None, sun_is_up: bool = True
) -> str | None:
"""Translate ClimaCell condition into an HA condition."""
def _forecast_dict(
self,
forecast_dt: datetime,
use_datetime: bool,
condition: int | str,
precipitation: float | None,
precipitation_probability: float | None,
temp: float | None,
temp_low: float | None,
wind_direction: float | None,
wind_speed: float | None,
) -> dict[str, Any]:
"""Return formatted Forecast dict from ClimaCell forecast data."""
if use_datetime:
translated_condition = self._translate_condition(
condition, is_up(self.hass, forecast_dt)
)
else:
translated_condition = self._translate_condition(condition, True)
data = {
ATTR_FORECAST_TIME: forecast_dt.isoformat(),
ATTR_FORECAST_CONDITION: translated_condition,
ATTR_FORECAST_NATIVE_PRECIPITATION: precipitation,
ATTR_FORECAST_PRECIPITATION_PROBABILITY: precipitation_probability,
ATTR_FORECAST_NATIVE_TEMP: temp,
ATTR_FORECAST_NATIVE_TEMP_LOW: temp_low,
ATTR_FORECAST_WIND_BEARING: wind_direction,
ATTR_FORECAST_NATIVE_WIND_SPEED: wind_speed,
}
return {k: v for k, v in data.items() if v is not None}
@property
def extra_state_attributes(self) -> Mapping[str, Any] | None:
"""Return additional state attributes."""
cloud_cover = self.cloud_cover
attrs = {
ATTR_CLOUD_COVER: cloud_cover,
ATTR_PRECIPITATION_TYPE: self.precipitation_type,
}
if (wind_gust := self.wind_gust) is not None:
attrs[ATTR_WIND_GUST] = round(
speed_convert(wind_gust, SPEED_MILES_PER_HOUR, self._wind_speed_unit), 4
)
return attrs
@property
@abstractmethod
def cloud_cover(self):
"""Return cloud cover."""
@property
@abstractmethod
def wind_gust(self):
"""Return wind gust speed."""
@property
@abstractmethod
def precipitation_type(self):
"""Return precipitation type."""
@property
@abstractmethod
def _pressure(self):
"""Return the raw pressure."""
@property
def native_pressure(self):
"""Return the pressure."""
return self._pressure
@property
@abstractmethod
def _wind_speed(self):
"""Return the raw wind speed."""
@property
def native_wind_speed(self):
"""Return the wind speed."""
return self._wind_speed
@property
@abstractmethod
def _visibility(self):
"""Return the raw visibility."""
@property
def native_visibility(self):
"""Return the visibility."""
return self._visibility
class ClimaCellV3WeatherEntity(BaseClimaCellWeatherEntity):
"""Entity that talks to ClimaCell v3 API to retrieve weather data."""
@staticmethod
def _translate_condition(
condition: int | str | None, sun_is_up: bool = True
) -> str | None:
"""Translate ClimaCell condition into an HA condition."""
if not condition:
return None
condition = cast(str, condition)
if "clear" in condition.lower():
if sun_is_up:
return CLEAR_CONDITIONS["day"]
return CLEAR_CONDITIONS["night"]
return CONDITIONS_V3[condition]
@property
def native_temperature(self):
"""Return the platform temperature."""
return self._get_cc_value(
self.coordinator.data[CURRENT], CC_V3_ATTR_TEMPERATURE
)
@property
def _pressure(self):
"""Return the raw pressure."""
return self._get_cc_value(self.coordinator.data[CURRENT], CC_V3_ATTR_PRESSURE)
@property
def humidity(self):
"""Return the humidity."""
return self._get_cc_value(self.coordinator.data[CURRENT], CC_V3_ATTR_HUMIDITY)
@property
def wind_gust(self):
"""Return the wind gust speed."""
return self._get_cc_value(self.coordinator.data[CURRENT], CC_V3_ATTR_WIND_GUST)
@property
def cloud_cover(self):
"""Return the cloud cover."""
return self._get_cc_value(
self.coordinator.data[CURRENT], CC_V3_ATTR_CLOUD_COVER
)
@property
def precipitation_type(self):
"""Return precipitation type."""
return self._get_cc_value(
self.coordinator.data[CURRENT], CC_V3_ATTR_PRECIPITATION_TYPE
)
@property
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):
"""Return the wind bearing."""
return self._get_cc_value(
self.coordinator.data[CURRENT], CC_V3_ATTR_WIND_DIRECTION
)
@property
def ozone(self):
"""Return the O3 (ozone) level."""
return self._get_cc_value(self.coordinator.data[CURRENT], CC_V3_ATTR_OZONE)
@property
def condition(self):
"""Return the condition."""
return self._translate_condition(
self._get_cc_value(self.coordinator.data[CURRENT], CC_V3_ATTR_CONDITION),
is_up(self.hass),
)
@property
def _visibility(self):
"""Return the raw visibility."""
return self._get_cc_value(self.coordinator.data[CURRENT], CC_V3_ATTR_VISIBILITY)
@property
def forecast(self):
"""Return the forecast."""
# Check if forecasts are available
raw_forecasts = self.coordinator.data.get(FORECASTS, {}).get(self.forecast_type)
if not raw_forecasts:
return None
forecasts = []
# Set default values (in cases where keys don't exist), None will be
# returned. Override properties per forecast type as needed
for forecast in raw_forecasts:
forecast_dt = dt_util.parse_datetime(
self._get_cc_value(forecast, CC_V3_ATTR_TIMESTAMP)
)
use_datetime = True
condition = self._get_cc_value(forecast, CC_V3_ATTR_CONDITION)
precipitation = self._get_cc_value(forecast, CC_V3_ATTR_PRECIPITATION)
precipitation_probability = self._get_cc_value(
forecast, CC_V3_ATTR_PRECIPITATION_PROBABILITY
)
temp = self._get_cc_value(forecast, CC_V3_ATTR_TEMPERATURE)
temp_low = None
wind_direction = self._get_cc_value(forecast, CC_V3_ATTR_WIND_DIRECTION)
wind_speed = self._get_cc_value(forecast, CC_V3_ATTR_WIND_SPEED)
if self.forecast_type == DAILY:
use_datetime = False
forecast_dt = dt_util.start_of_local_day(forecast_dt)
precipitation = self._get_cc_value(
forecast, CC_V3_ATTR_PRECIPITATION_DAILY
)
temp = next(
(
self._get_cc_value(item, CC_V3_ATTR_TEMPERATURE_HIGH)
for item in forecast[CC_V3_ATTR_TEMPERATURE]
if "max" in item
),
temp,
)
temp_low = next(
(
self._get_cc_value(item, CC_V3_ATTR_TEMPERATURE_LOW)
for item in forecast[CC_V3_ATTR_TEMPERATURE]
if "min" in item
),
temp_low,
)
elif self.forecast_type == NOWCAST and precipitation:
# Precipitation is forecasted in CONF_TIMESTEP increments but in a
# per hour rate, so value needs to be converted to an amount.
precipitation = (
precipitation / 60 * self._config_entry.options[CONF_TIMESTEP]
)
forecasts.append(
self._forecast_dict(
forecast_dt,
use_datetime,
condition,
precipitation,
precipitation_probability,
temp,
temp_low,
wind_direction,
wind_speed,
)
)
return forecasts

View File

@ -17,7 +17,7 @@ from pytomorrowio.exceptions import (
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.components.weather import DOMAIN as WEATHER_DOMAIN
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_API_KEY,
CONF_LATITUDE,
@ -25,8 +25,8 @@ from homeassistant.const import (
CONF_LONGITUDE,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.device_registry import DeviceEntryType
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
@ -123,86 +123,10 @@ def async_set_update_interval(
return timedelta(minutes=minutes)
@callback
def async_migrate_entry_from_climacell(
hass: HomeAssistant,
dev_reg: dr.DeviceRegistry,
entry: ConfigEntry,
device: dr.DeviceEntry,
) -> None:
"""Migrate a config entry from a Climacell entry."""
# Remove the old config entry ID from the entry data so we don't try this again
# on the next setup
data = entry.data.copy()
old_config_entry_id = data.pop("old_config_entry_id")
hass.config_entries.async_update_entry(entry, data=data)
LOGGER.debug(
(
"Setting up imported climacell entry %s for the first time as "
"tomorrowio entry %s"
),
old_config_entry_id,
entry.entry_id,
)
ent_reg = er.async_get(hass)
for entity_entry in er.async_entries_for_config_entry(ent_reg, old_config_entry_id):
old_platform = entity_entry.platform
# In case the API key has changed due to a V3 -> V4 change, we need to
# generate the new entity's unique ID
new_unique_id = (
f"{entry.data[CONF_API_KEY]}_"
f"{'_'.join(entity_entry.unique_id.split('_')[1:])}"
)
ent_reg.async_update_entity_platform(
entity_entry.entity_id,
DOMAIN,
new_unique_id=new_unique_id,
new_config_entry_id=entry.entry_id,
new_device_id=device.id,
)
assert entity_entry
LOGGER.debug(
"Migrated %s from %s to %s",
entity_entry.entity_id,
old_platform,
DOMAIN,
)
# We only have one device in the registry but we will do a loop just in case
for old_device in dr.async_entries_for_config_entry(dev_reg, old_config_entry_id):
if old_device.name_by_user:
dev_reg.async_update_device(device.id, name_by_user=old_device.name_by_user)
# Remove the old config entry and now the entry is fully migrated
hass.async_create_task(hass.config_entries.async_remove(old_config_entry_id))
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Tomorrow.io API from a config entry."""
hass.data.setdefault(DOMAIN, {})
# Let's precreate the device so that if this is a first time setup for a config
# entry imported from a ClimaCell entry, we can apply customizations from the old
# device.
dev_reg = dr.async_get(hass)
device = dev_reg.async_get_or_create(
config_entry_id=entry.entry_id,
identifiers={(DOMAIN, entry.data[CONF_API_KEY])},
name=INTEGRATION_NAME,
manufacturer=INTEGRATION_NAME,
sw_version="v4",
entry_type=dr.DeviceEntryType.SERVICE,
)
# If this is an import and we still have the old config entry ID in the entry data,
# it means we are setting this entry up for the first time after a migration from
# ClimaCell to Tomorrow.io. In order to preserve any customizations on the ClimaCell
# entities, we need to remove each old entity, creating a new entity in its place
# but attached to this entry.
if entry.source == SOURCE_IMPORT and "old_config_entry_id" in entry.data:
async_migrate_entry_from_climacell(hass, dev_reg, entry, device)
api_key = entry.data[CONF_API_KEY]
# If coordinator already exists for this API key, we'll use that, otherwise
# we have to create a new one
@ -408,10 +332,10 @@ class TomorrowioEntity(CoordinatorEntity[TomorrowioDataUpdateCoordinator]):
self._config_entry = config_entry
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, self._config_entry.data[CONF_API_KEY])},
name="Tomorrow.io",
manufacturer="Tomorrow.io",
name=INTEGRATION_NAME,
manufacturer=INTEGRATION_NAME,
sw_version=f"v{self.api_version}",
entry_type=dr.DeviceEntryType.SERVICE,
entry_type=DeviceEntryType.SERVICE,
)
def _get_current_property(self, property_name: str) -> int | str | float | None:

View File

@ -17,7 +17,6 @@ from homeassistant import config_entries, core
from homeassistant.components.zone import async_active_zone
from homeassistant.const import (
CONF_API_KEY,
CONF_API_VERSION,
CONF_FRIENDLY_NAME,
CONF_LATITUDE,
CONF_LOCATION,
@ -30,14 +29,10 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.selector import LocationSelector, LocationSelectorConfig
from .const import (
AUTO_MIGRATION_MESSAGE,
CC_DOMAIN,
CONF_TIMESTEP,
DEFAULT_NAME,
DEFAULT_TIMESTEP,
DOMAIN,
INTEGRATION_NAME,
MANUAL_MIGRATION_MESSAGE,
TMRW_ATTR_TEMPERATURE,
)
@ -62,10 +57,6 @@ def _get_config_schema(
vol.Required(CONF_API_KEY, default=input_dict.get(CONF_API_KEY)): str,
}
# For imports we just need to ask for the API key
if source == config_entries.SOURCE_IMPORT:
return vol.Schema(api_key_schema, extra=vol.REMOVE_EXTRA)
default_location = (
input_dict[CONF_LOCATION]
if CONF_LOCATION in input_dict
@ -125,11 +116,6 @@ class TomorrowioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
VERSION = 1
def __init__(self) -> None:
"""Initialize config flow."""
self._showed_import_message = 0
self._import_config: dict[str, Any] | None = None
@staticmethod
@callback
def async_get_options_flow(
@ -144,18 +130,6 @@ class TomorrowioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle the initial step."""
errors = {}
if user_input is not None:
# Grab the API key and add it to the rest of the config before continuing
if self._import_config:
self._import_config[CONF_API_KEY] = user_input[CONF_API_KEY]
self._import_config[CONF_LOCATION] = {
CONF_LATITUDE: self._import_config.pop(
CONF_LATITUDE, self.hass.config.latitude
),
CONF_LONGITUDE: self._import_config.pop(
CONF_LONGITUDE, self.hass.config.longitude
),
}
user_input = self._import_config.copy()
await self.async_set_unique_id(
unique_id=_get_unique_id(self.hass, user_input)
)
@ -189,15 +163,6 @@ class TomorrowioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
if not errors:
options: Mapping[str, Any] = {CONF_TIMESTEP: DEFAULT_TIMESTEP}
# Store the old config entry ID and retrieve options to recreate the entry
if self.source == config_entries.SOURCE_IMPORT:
old_config_entry_id = self.context["old_config_entry_id"]
old_config_entry = self.hass.config_entries.async_get_entry(
old_config_entry_id
)
assert old_config_entry
options = dict(old_config_entry.options)
user_input["old_config_entry_id"] = old_config_entry_id
return self.async_create_entry(
title=user_input[CONF_NAME],
data=user_input,
@ -209,24 +174,3 @@ class TomorrowioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
data_schema=_get_config_schema(self.hass, self.source, user_input),
errors=errors,
)
async def async_step_import(self, import_config: dict) -> FlowResult:
"""Import from config."""
# Store import config for later
self._import_config = dict(import_config)
if self._import_config.pop(CONF_API_VERSION, 3) == 3:
# Clear API key from import config
self._import_config[CONF_API_KEY] = ""
self.hass.components.persistent_notification.async_create(
MANUAL_MIGRATION_MESSAGE,
INTEGRATION_NAME,
f"{CC_DOMAIN}_to_{DOMAIN}_new_api_key_needed",
)
return await self.async_step_user()
self.hass.components.persistent_notification.async_create(
AUTO_MIGRATION_MESSAGE,
INTEGRATION_NAME,
f"{CC_DOMAIN}_to_{DOMAIN}",
)
return await self.async_step_user(self._import_config)

View File

@ -27,7 +27,6 @@ FORECAST_TYPES = [DAILY, HOURLY, NOWCAST]
DEFAULT_TIMESTEP = 15
DEFAULT_FORECAST_TYPE = DAILY
CC_DOMAIN = "climacell"
DOMAIN = "tomorrowio"
INTEGRATION_NAME = "Tomorrow.io"
DEFAULT_NAME = INTEGRATION_NAME
@ -116,32 +115,3 @@ TMRW_ATTR_PRESSURE_SURFACE_LEVEL = "pressureSurfaceLevel"
TMRW_ATTR_SOLAR_GHI = "solarGHI"
TMRW_ATTR_CLOUD_BASE = "cloudBase"
TMRW_ATTR_CLOUD_CEILING = "cloudCeiling"
MANUAL_MIGRATION_MESSAGE = (
"As part of [ClimaCell's rebranding to Tomorrow.io](https://www.tomorrow.io/blog/my-last-day-as-ceo-of-climacell/) "
"we will migrate your existing ClimaCell config entry (or config "
"entries) to the new Tomorrow.io integration, but because **the "
" V3 API is now deprecated**, you will need to get a new V4 API "
"key from [Tomorrow.io](https://app.tomorrow.io/development/keys)."
" Once that is done, visit the "
"[Integrations Configuration](/config/integrations) page and "
"click Configure on the Tomorrow.io card(s) to submit the new "
"key. Once your key has been validated, your config entry will "
"automatically be migrated. The new integration is a drop in "
"replacement and your existing entities will be migrated over, "
"just note that the location of the integration card on the "
"[Integrations Configuration](/config/integrations) page has changed "
"since the integration name has changed."
)
AUTO_MIGRATION_MESSAGE = (
"As part of [ClimaCell's rebranding to Tomorrow.io](https://www.tomorrow.io/blog/my-last-day-as-ceo-of-climacell/) "
"we have automatically migrated your existing ClimaCell config entry "
"(or as many of your ClimaCell config entries as we could) to the new "
"Tomorrow.io integration. There is nothing you need to do since the "
"new integration is a drop in replacement and your existing entities "
"have been migrated over, just note that the location of the "
"integration card on the "
"[Integrations Configuration](/config/integrations) page has changed "
"since the integration name has changed."
)

View File

@ -1480,9 +1480,6 @@ pychromecast==12.1.4
# homeassistant.components.pocketcasts
pycketcasts==1.0.1
# homeassistant.components.climacell
pyclimacell==0.18.2
# homeassistant.components.cmus
pycmus==0.1.1

View File

@ -1044,9 +1044,6 @@ pycfdns==1.2.2
# homeassistant.components.cast
pychromecast==12.1.4
# homeassistant.components.climacell
pyclimacell==0.18.2
# homeassistant.components.comfoconnect
pycomfoconnect==0.4

View File

@ -1 +0,0 @@
"""Tests for the ClimaCell Weather API integration."""

View File

@ -1,26 +0,0 @@
"""Configure py.test."""
import json
from unittest.mock import patch
import pytest
from tests.common import load_fixture
@pytest.fixture(name="climacell_config_entry_update")
def climacell_config_entry_update_fixture():
"""Mock valid climacell config entry setup."""
with patch(
"homeassistant.components.climacell.ClimaCellV3.realtime",
return_value=json.loads(load_fixture("v3_realtime.json", "climacell")),
), patch(
"homeassistant.components.climacell.ClimaCellV3.forecast_hourly",
return_value=json.loads(load_fixture("v3_forecast_hourly.json", "climacell")),
), patch(
"homeassistant.components.climacell.ClimaCellV3.forecast_daily",
return_value=json.loads(load_fixture("v3_forecast_daily.json", "climacell")),
), patch(
"homeassistant.components.climacell.ClimaCellV3.forecast_nowcast",
return_value=json.loads(load_fixture("v3_forecast_nowcast.json", "climacell")),
):
yield

View File

@ -1,19 +0,0 @@
"""Constants for climacell tests."""
from homeassistant.const import (
CONF_API_KEY,
CONF_API_VERSION,
CONF_LATITUDE,
CONF_LONGITUDE,
CONF_NAME,
)
API_KEY = "aa"
API_V3_ENTRY_DATA = {
CONF_NAME: "ClimaCell",
CONF_API_KEY: API_KEY,
CONF_LATITUDE: 80.0,
CONF_LONGITUDE: 80.0,
CONF_API_VERSION: 3,
}

View File

@ -1,992 +0,0 @@
[
{
"temp": [
{
"observation_time": "2021-03-07T11:00:00Z",
"min": {
"value": 23.47,
"units": "F"
}
},
{
"observation_time": "2021-03-07T21:00:00Z",
"max": {
"value": 44.88,
"units": "F"
}
}
],
"precipitation_accumulation": {
"value": 0,
"units": "in"
},
"precipitation_probability": {
"value": 0,
"units": "%"
},
"wind_speed": [
{
"observation_time": "2021-03-08T00:00:00Z",
"min": {
"value": 2.58,
"units": "mph"
}
},
{
"observation_time": "2021-03-07T19:00:00Z",
"max": {
"value": 7.67,
"units": "mph"
}
}
],
"wind_direction": [
{
"observation_time": "2021-03-08T00:00:00Z",
"min": {
"value": 72.1,
"units": "degrees"
}
},
{
"observation_time": "2021-03-07T19:00:00Z",
"max": {
"value": 313.49,
"units": "degrees"
}
}
],
"weather_code": {
"value": "clear"
},
"observation_time": {
"value": "2021-03-07"
},
"lat": 38.90694,
"lon": -77.03012
},
{
"temp": [
{
"observation_time": "2021-03-08T11:00:00Z",
"min": {
"value": 24.79,
"units": "F"
}
},
{
"observation_time": "2021-03-08T21:00:00Z",
"max": {
"value": 49.42,
"units": "F"
}
}
],
"precipitation_accumulation": {
"value": 0,
"units": "in"
},
"precipitation_probability": {
"value": 0,
"units": "%"
},
"wind_speed": [
{
"observation_time": "2021-03-08T22:00:00Z",
"min": {
"value": 1.97,
"units": "mph"
}
},
{
"observation_time": "2021-03-08T13:00:00Z",
"max": {
"value": 7.24,
"units": "mph"
}
}
],
"wind_direction": [
{
"observation_time": "2021-03-08T22:00:00Z",
"min": {
"value": 268.74,
"units": "degrees"
}
},
{
"observation_time": "2021-03-08T13:00:00Z",
"max": {
"value": 324.8,
"units": "degrees"
}
}
],
"weather_code": {
"value": "cloudy"
},
"observation_time": {
"value": "2021-03-08"
},
"lat": 38.90694,
"lon": -77.03012
},
{
"temp": [
{
"observation_time": "2021-03-09T11:00:00Z",
"min": {
"value": 31.48,
"units": "F"
}
},
{
"observation_time": "2021-03-09T21:00:00Z",
"max": {
"value": 66.98,
"units": "F"
}
}
],
"precipitation_accumulation": {
"value": 0,
"units": "in"
},
"precipitation_probability": {
"value": 0,
"units": "%"
},
"wind_speed": [
{
"observation_time": "2021-03-09T22:00:00Z",
"min": {
"value": 3.35,
"units": "mph"
}
},
{
"observation_time": "2021-03-09T19:00:00Z",
"max": {
"value": 7.05,
"units": "mph"
}
}
],
"wind_direction": [
{
"observation_time": "2021-03-09T22:00:00Z",
"min": {
"value": 279.37,
"units": "degrees"
}
},
{
"observation_time": "2021-03-09T19:00:00Z",
"max": {
"value": 253.12,
"units": "degrees"
}
}
],
"weather_code": {
"value": "mostly_cloudy"
},
"observation_time": {
"value": "2021-03-09"
},
"lat": 38.90694,
"lon": -77.03012
},
{
"temp": [
{
"observation_time": "2021-03-10T11:00:00Z",
"min": {
"value": 37.32,
"units": "F"
}
},
{
"observation_time": "2021-03-10T20:00:00Z",
"max": {
"value": 65.28,
"units": "F"
}
}
],
"precipitation_accumulation": {
"value": 0,
"units": "in"
},
"precipitation_probability": {
"value": 0,
"units": "%"
},
"wind_speed": [
{
"observation_time": "2021-03-10T05:00:00Z",
"min": {
"value": 2.13,
"units": "mph"
}
},
{
"observation_time": "2021-03-10T21:00:00Z",
"max": {
"value": 9.42,
"units": "mph"
}
}
],
"wind_direction": [
{
"observation_time": "2021-03-10T05:00:00Z",
"min": {
"value": 342.01,
"units": "degrees"
}
},
{
"observation_time": "2021-03-10T21:00:00Z",
"max": {
"value": 193.22,
"units": "degrees"
}
}
],
"weather_code": {
"value": "cloudy"
},
"observation_time": {
"value": "2021-03-10"
},
"lat": 38.90694,
"lon": -77.03012
},
{
"temp": [
{
"observation_time": "2021-03-11T12:00:00Z",
"min": {
"value": 48.69,
"units": "F"
}
},
{
"observation_time": "2021-03-11T21:00:00Z",
"max": {
"value": 67.37,
"units": "F"
}
}
],
"precipitation_accumulation": {
"value": 0,
"units": "in"
},
"precipitation_probability": {
"value": 5,
"units": "%"
},
"wind_speed": [
{
"observation_time": "2021-03-11T02:00:00Z",
"min": {
"value": 8.82,
"units": "mph"
}
},
{
"observation_time": "2021-03-12T01:00:00Z",
"max": {
"value": 14.47,
"units": "mph"
}
}
],
"wind_direction": [
{
"observation_time": "2021-03-11T02:00:00Z",
"min": {
"value": 176.84,
"units": "degrees"
}
},
{
"observation_time": "2021-03-12T01:00:00Z",
"max": {
"value": 210.63,
"units": "degrees"
}
}
],
"weather_code": {
"value": "cloudy"
},
"observation_time": {
"value": "2021-03-11"
},
"lat": 38.90694,
"lon": -77.03012
},
{
"temp": [
{
"observation_time": "2021-03-12T12:00:00Z",
"min": {
"value": 53.83,
"units": "F"
}
},
{
"observation_time": "2021-03-12T18:00:00Z",
"max": {
"value": 67.91,
"units": "F"
}
}
],
"precipitation_accumulation": {
"value": 0.0018,
"units": "in"
},
"precipitation_probability": {
"value": 25,
"units": "%"
},
"wind_speed": [
{
"observation_time": "2021-03-13T00:00:00Z",
"min": {
"value": 4.98,
"units": "mph"
}
},
{
"observation_time": "2021-03-12T02:00:00Z",
"max": {
"value": 15.69,
"units": "mph"
}
}
],
"wind_direction": [
{
"observation_time": "2021-03-13T00:00:00Z",
"min": {
"value": 329.35,
"units": "degrees"
}
},
{
"observation_time": "2021-03-12T02:00:00Z",
"max": {
"value": 211.47,
"units": "degrees"
}
}
],
"weather_code": {
"value": "cloudy"
},
"observation_time": {
"value": "2021-03-12"
},
"lat": 38.90694,
"lon": -77.03012
},
{
"temp": [
{
"observation_time": "2021-03-14T00:00:00Z",
"min": {
"value": 45.48,
"units": "F"
}
},
{
"observation_time": "2021-03-13T03:00:00Z",
"max": {
"value": 60.42,
"units": "F"
}
}
],
"precipitation_accumulation": {
"value": 0,
"units": "in"
},
"precipitation_probability": {
"value": 25,
"units": "%"
},
"wind_speed": [
{
"observation_time": "2021-03-13T03:00:00Z",
"min": {
"value": 2.91,
"units": "mph"
}
},
{
"observation_time": "2021-03-13T21:00:00Z",
"max": {
"value": 9.72,
"units": "mph"
}
}
],
"wind_direction": [
{
"observation_time": "2021-03-13T03:00:00Z",
"min": {
"value": 202.04,
"units": "degrees"
}
},
{
"observation_time": "2021-03-13T21:00:00Z",
"max": {
"value": 64.38,
"units": "degrees"
}
}
],
"weather_code": {
"value": "cloudy"
},
"observation_time": {
"value": "2021-03-13"
},
"lat": 38.90694,
"lon": -77.03012
},
{
"temp": [
{
"observation_time": "2021-03-15T00:00:00Z",
"min": {
"value": 37.81,
"units": "F"
}
},
{
"observation_time": "2021-03-14T03:00:00Z",
"max": {
"value": 43.58,
"units": "F"
}
}
],
"precipitation_accumulation": {
"value": 0.0423,
"units": "in"
},
"precipitation_probability": {
"value": 75,
"units": "%"
},
"wind_speed": [
{
"observation_time": "2021-03-14T06:00:00Z",
"min": {
"value": 5.34,
"units": "mph"
}
},
{
"observation_time": "2021-03-14T21:00:00Z",
"max": {
"value": 16.25,
"units": "mph"
}
}
],
"wind_direction": [
{
"observation_time": "2021-03-14T06:00:00Z",
"min": {
"value": 57.52,
"units": "degrees"
}
},
{
"observation_time": "2021-03-14T21:00:00Z",
"max": {
"value": 83.23,
"units": "degrees"
}
}
],
"weather_code": {
"value": "rain_light"
},
"observation_time": {
"value": "2021-03-14"
},
"lat": 38.90694,
"lon": -77.03012
},
{
"temp": [
{
"observation_time": "2021-03-16T00:00:00Z",
"min": {
"value": 32.31,
"units": "F"
}
},
{
"observation_time": "2021-03-15T09:00:00Z",
"max": {
"value": 34.21,
"units": "F"
}
}
],
"precipitation_accumulation": {
"value": 0.2876,
"units": "in"
},
"precipitation_probability": {
"value": 95,
"units": "%"
},
"wind_speed": [
{
"observation_time": "2021-03-16T00:00:00Z",
"min": {
"value": 11.7,
"units": "mph"
}
},
{
"observation_time": "2021-03-15T18:00:00Z",
"max": {
"value": 15.89,
"units": "mph"
}
}
],
"wind_direction": [
{
"observation_time": "2021-03-16T00:00:00Z",
"min": {
"value": 63.67,
"units": "degrees"
}
},
{
"observation_time": "2021-03-15T18:00:00Z",
"max": {
"value": 59.49,
"units": "degrees"
}
}
],
"weather_code": {
"value": "snow_heavy"
},
"observation_time": {
"value": "2021-03-15"
},
"lat": 38.90694,
"lon": -77.03012
},
{
"temp": [
{
"observation_time": "2021-03-16T12:00:00Z",
"min": {
"value": 29.1,
"units": "F"
}
},
{
"observation_time": "2021-03-16T21:00:00Z",
"max": {
"value": 43,
"units": "F"
}
}
],
"precipitation_accumulation": {
"value": 0.0002,
"units": "in"
},
"precipitation_probability": {
"value": 5,
"units": "%"
},
"wind_speed": [
{
"observation_time": "2021-03-16T18:00:00Z",
"min": {
"value": 4.98,
"units": "mph"
}
},
{
"observation_time": "2021-03-16T03:00:00Z",
"max": {
"value": 9.77,
"units": "mph"
}
}
],
"wind_direction": [
{
"observation_time": "2021-03-16T18:00:00Z",
"min": {
"value": 80.47,
"units": "degrees"
}
},
{
"observation_time": "2021-03-16T03:00:00Z",
"max": {
"value": 58.98,
"units": "degrees"
}
}
],
"weather_code": {
"value": "cloudy"
},
"observation_time": {
"value": "2021-03-16"
},
"lat": 38.90694,
"lon": -77.03012
},
{
"temp": [
{
"observation_time": "2021-03-17T12:00:00Z",
"min": {
"value": 34.32,
"units": "F"
}
},
{
"observation_time": "2021-03-17T21:00:00Z",
"max": {
"value": 52.4,
"units": "F"
}
}
],
"precipitation_accumulation": {
"value": 0,
"units": "in"
},
"precipitation_probability": {
"value": 0,
"units": "%"
},
"wind_speed": [
{
"observation_time": "2021-03-18T00:00:00Z",
"min": {
"value": 4.49,
"units": "mph"
}
},
{
"observation_time": "2021-03-17T03:00:00Z",
"max": {
"value": 6.71,
"units": "mph"
}
}
],
"wind_direction": [
{
"observation_time": "2021-03-18T00:00:00Z",
"min": {
"value": 116.64,
"units": "degrees"
}
},
{
"observation_time": "2021-03-17T03:00:00Z",
"max": {
"value": 111.51,
"units": "degrees"
}
}
],
"weather_code": {
"value": "cloudy"
},
"observation_time": {
"value": "2021-03-17"
},
"lat": 38.90694,
"lon": -77.03012
},
{
"temp": [
{
"observation_time": "2021-03-18T12:00:00Z",
"min": {
"value": 41.99,
"units": "F"
}
},
{
"observation_time": "2021-03-18T21:00:00Z",
"max": {
"value": 54.07,
"units": "F"
}
}
],
"precipitation_accumulation": {
"value": 0,
"units": "in"
},
"precipitation_probability": {
"value": 5,
"units": "%"
},
"wind_speed": [
{
"observation_time": "2021-03-18T06:00:00Z",
"min": {
"value": 2.77,
"units": "mph"
}
},
{
"observation_time": "2021-03-18T03:00:00Z",
"max": {
"value": 5.22,
"units": "mph"
}
}
],
"wind_direction": [
{
"observation_time": "2021-03-18T06:00:00Z",
"min": {
"value": 119.5,
"units": "degrees"
}
},
{
"observation_time": "2021-03-18T03:00:00Z",
"max": {
"value": 135.5,
"units": "degrees"
}
}
],
"weather_code": {
"value": "cloudy"
},
"observation_time": {
"value": "2021-03-18"
},
"lat": 38.90694,
"lon": -77.03012
},
{
"temp": [
{
"observation_time": "2021-03-19T12:00:00Z",
"min": {
"value": 40.48,
"units": "F"
}
},
{
"observation_time": "2021-03-19T18:00:00Z",
"max": {
"value": 48.94,
"units": "F"
}
}
],
"precipitation_accumulation": {
"value": 0.007,
"units": "in"
},
"precipitation_probability": {
"value": 45,
"units": "%"
},
"wind_speed": [
{
"observation_time": "2021-03-19T03:00:00Z",
"min": {
"value": 5.43,
"units": "mph"
}
},
{
"observation_time": "2021-03-20T00:00:00Z",
"max": {
"value": 11.1,
"units": "mph"
}
}
],
"wind_direction": [
{
"observation_time": "2021-03-19T03:00:00Z",
"min": {
"value": 50.18,
"units": "degrees"
}
},
{
"observation_time": "2021-03-20T00:00:00Z",
"max": {
"value": 86.96,
"units": "degrees"
}
}
],
"weather_code": {
"value": "cloudy"
},
"observation_time": {
"value": "2021-03-19"
},
"lat": 38.90694,
"lon": -77.03012
},
{
"temp": [
{
"observation_time": "2021-03-21T00:00:00Z",
"min": {
"value": 37.56,
"units": "F"
}
},
{
"observation_time": "2021-03-20T03:00:00Z",
"max": {
"value": 41.05,
"units": "F"
}
}
],
"precipitation_accumulation": {
"value": 0.0485,
"units": "in"
},
"precipitation_probability": {
"value": 55,
"units": "%"
},
"wind_speed": [
{
"observation_time": "2021-03-20T03:00:00Z",
"min": {
"value": 10.9,
"units": "mph"
}
},
{
"observation_time": "2021-03-20T21:00:00Z",
"max": {
"value": 17.35,
"units": "mph"
}
}
],
"wind_direction": [
{
"observation_time": "2021-03-20T03:00:00Z",
"min": {
"value": 70.56,
"units": "degrees"
}
},
{
"observation_time": "2021-03-20T21:00:00Z",
"max": {
"value": 58.55,
"units": "degrees"
}
}
],
"weather_code": {
"value": "drizzle"
},
"observation_time": {
"value": "2021-03-20"
},
"lat": 38.90694,
"lon": -77.03012
},
{
"temp": [
{
"observation_time": "2021-03-21T12:00:00Z",
"min": {
"value": 33.66,
"units": "F"
}
},
{
"observation_time": "2021-03-21T21:00:00Z",
"max": {
"value": 44.3,
"units": "F"
}
}
],
"precipitation_accumulation": {
"value": 0.0017,
"units": "in"
},
"precipitation_probability": {
"value": 20,
"units": "%"
},
"wind_speed": [
{
"observation_time": "2021-03-22T00:00:00Z",
"min": {
"value": 8.65,
"units": "mph"
}
},
{
"observation_time": "2021-03-21T03:00:00Z",
"max": {
"value": 16.53,
"units": "mph"
}
}
],
"wind_direction": [
{
"observation_time": "2021-03-22T00:00:00Z",
"min": {
"value": 64.92,
"units": "degrees"
}
},
{
"observation_time": "2021-03-21T03:00:00Z",
"max": {
"value": 57.74,
"units": "degrees"
}
}
],
"weather_code": {
"value": "cloudy"
},
"observation_time": {
"value": "2021-03-21"
},
"lat": 38.90694,
"lon": -77.03012
}
]

View File

@ -1,752 +0,0 @@
[
{
"lon": -77.03012,
"lat": 38.90694,
"temp": {
"value": 42.75,
"units": "F"
},
"precipitation": {
"value": 0,
"units": "in/hr"
},
"precipitation_probability": {
"value": 0,
"units": "%"
},
"wind_speed": {
"value": 8.99,
"units": "mph"
},
"wind_direction": {
"value": 320.22,
"units": "degrees"
},
"weather_code": {
"value": "clear"
},
"observation_time": {
"value": "2021-03-07T18:00:00.000Z"
}
},
{
"lon": -77.03012,
"lat": 38.90694,
"temp": {
"value": 44.29,
"units": "F"
},
"precipitation": {
"value": 0,
"units": "in/hr"
},
"precipitation_probability": {
"value": 0,
"units": "%"
},
"wind_speed": {
"value": 9.65,
"units": "mph"
},
"wind_direction": {
"value": 326.14,
"units": "degrees"
},
"weather_code": {
"value": "clear"
},
"observation_time": {
"value": "2021-03-07T19:00:00.000Z"
}
},
{
"lon": -77.03012,
"lat": 38.90694,
"temp": {
"value": 45.3,
"units": "F"
},
"precipitation": {
"value": 0,
"units": "in/hr"
},
"precipitation_probability": {
"value": 0,
"units": "%"
},
"wind_speed": {
"value": 9.28,
"units": "mph"
},
"wind_direction": {
"value": 322.01,
"units": "degrees"
},
"weather_code": {
"value": "clear"
},
"observation_time": {
"value": "2021-03-07T20:00:00.000Z"
}
},
{
"lon": -77.03012,
"lat": 38.90694,
"temp": {
"value": 45.26,
"units": "F"
},
"precipitation": {
"value": 0,
"units": "in/hr"
},
"precipitation_probability": {
"value": 0,
"units": "%"
},
"wind_speed": {
"value": 9.12,
"units": "mph"
},
"wind_direction": {
"value": 323.71,
"units": "degrees"
},
"weather_code": {
"value": "clear"
},
"observation_time": {
"value": "2021-03-07T21:00:00.000Z"
}
},
{
"lon": -77.03012,
"lat": 38.90694,
"temp": {
"value": 44.83,
"units": "F"
},
"precipitation": {
"value": 0,
"units": "in/hr"
},
"precipitation_probability": {
"value": 0,
"units": "%"
},
"wind_speed": {
"value": 7.27,
"units": "mph"
},
"wind_direction": {
"value": 319.88,
"units": "degrees"
},
"weather_code": {
"value": "clear"
},
"observation_time": {
"value": "2021-03-07T22:00:00.000Z"
}
},
{
"lon": -77.03012,
"lat": 38.90694,
"temp": {
"value": 41.7,
"units": "F"
},
"precipitation": {
"value": 0,
"units": "in/hr"
},
"precipitation_probability": {
"value": 0,
"units": "%"
},
"wind_speed": {
"value": 4.37,
"units": "mph"
},
"wind_direction": {
"value": 320.69,
"units": "degrees"
},
"weather_code": {
"value": "clear"
},
"observation_time": {
"value": "2021-03-07T23:00:00.000Z"
}
},
{
"lon": -77.03012,
"lat": 38.90694,
"temp": {
"value": 38.04,
"units": "F"
},
"precipitation": {
"value": 0,
"units": "in/hr"
},
"precipitation_probability": {
"value": 0,
"units": "%"
},
"wind_speed": {
"value": 5.45,
"units": "mph"
},
"wind_direction": {
"value": 351.54,
"units": "degrees"
},
"weather_code": {
"value": "clear"
},
"observation_time": {
"value": "2021-03-08T00:00:00.000Z"
}
},
{
"lon": -77.03012,
"lat": 38.90694,
"temp": {
"value": 35.88,
"units": "F"
},
"precipitation": {
"value": 0,
"units": "in/hr"
},
"precipitation_probability": {
"value": 0,
"units": "%"
},
"wind_speed": {
"value": 5.31,
"units": "mph"
},
"wind_direction": {
"value": 20.6,
"units": "degrees"
},
"weather_code": {
"value": "clear"
},
"observation_time": {
"value": "2021-03-08T01:00:00.000Z"
}
},
{
"lon": -77.03012,
"lat": 38.90694,
"temp": {
"value": 34.34,
"units": "F"
},
"precipitation": {
"value": 0,
"units": "in/hr"
},
"precipitation_probability": {
"value": 0,
"units": "%"
},
"wind_speed": {
"value": 5.78,
"units": "mph"
},
"wind_direction": {
"value": 11.22,
"units": "degrees"
},
"weather_code": {
"value": "clear"
},
"observation_time": {
"value": "2021-03-08T02:00:00.000Z"
}
},
{
"lon": -77.03012,
"lat": 38.90694,
"temp": {
"value": 33.3,
"units": "F"
},
"precipitation": {
"value": 0,
"units": "in/hr"
},
"precipitation_probability": {
"value": 0,
"units": "%"
},
"wind_speed": {
"value": 5.73,
"units": "mph"
},
"wind_direction": {
"value": 15.46,
"units": "degrees"
},
"weather_code": {
"value": "clear"
},
"observation_time": {
"value": "2021-03-08T03:00:00.000Z"
}
},
{
"lon": -77.03012,
"lat": 38.90694,
"temp": {
"value": 31.74,
"units": "F"
},
"precipitation": {
"value": 0,
"units": "in/hr"
},
"precipitation_probability": {
"value": 0,
"units": "%"
},
"wind_speed": {
"value": 4.44,
"units": "mph"
},
"wind_direction": {
"value": 26.07,
"units": "degrees"
},
"weather_code": {
"value": "clear"
},
"observation_time": {
"value": "2021-03-08T04:00:00.000Z"
}
},
{
"lon": -77.03012,
"lat": 38.90694,
"temp": {
"value": 29.98,
"units": "F"
},
"precipitation": {
"value": 0,
"units": "in/hr"
},
"precipitation_probability": {
"value": 0,
"units": "%"
},
"wind_speed": {
"value": 4.33,
"units": "mph"
},
"wind_direction": {
"value": 23.7,
"units": "degrees"
},
"weather_code": {
"value": "clear"
},
"observation_time": {
"value": "2021-03-08T05:00:00.000Z"
}
},
{
"lon": -77.03012,
"lat": 38.90694,
"temp": {
"value": 27.34,
"units": "F"
},
"precipitation": {
"value": 0,
"units": "in/hr"
},
"precipitation_probability": {
"value": 0,
"units": "%"
},
"wind_speed": {
"value": 4.7,
"units": "mph"
},
"wind_direction": {
"value": 354.56,
"units": "degrees"
},
"weather_code": {
"value": "clear"
},
"observation_time": {
"value": "2021-03-08T06:00:00.000Z"
}
},
{
"lon": -77.03012,
"lat": 38.90694,
"temp": {
"value": 26.61,
"units": "F"
},
"precipitation": {
"value": 0,
"units": "in/hr"
},
"precipitation_probability": {
"value": 0,
"units": "%"
},
"wind_speed": {
"value": 4.94,
"units": "mph"
},
"wind_direction": {
"value": 349.63,
"units": "degrees"
},
"weather_code": {
"value": "clear"
},
"observation_time": {
"value": "2021-03-08T07:00:00.000Z"
}
},
{
"lon": -77.03012,
"lat": 38.90694,
"temp": {
"value": 25.96,
"units": "F"
},
"precipitation": {
"value": 0,
"units": "in/hr"
},
"precipitation_probability": {
"value": 0,
"units": "%"
},
"wind_speed": {
"value": 4.61,
"units": "mph"
},
"wind_direction": {
"value": 336.74,
"units": "degrees"
},
"weather_code": {
"value": "clear"
},
"observation_time": {
"value": "2021-03-08T08:00:00.000Z"
}
},
{
"lon": -77.03012,
"lat": 38.90694,
"temp": {
"value": 25.72,
"units": "F"
},
"precipitation": {
"value": 0,
"units": "in/hr"
},
"precipitation_probability": {
"value": 0,
"units": "%"
},
"wind_speed": {
"value": 4.22,
"units": "mph"
},
"wind_direction": {
"value": 332.71,
"units": "degrees"
},
"weather_code": {
"value": "clear"
},
"observation_time": {
"value": "2021-03-08T09:00:00.000Z"
}
},
{
"lon": -77.03012,
"lat": 38.90694,
"temp": {
"value": 25.68,
"units": "F"
},
"precipitation": {
"value": 0,
"units": "in/hr"
},
"precipitation_probability": {
"value": 0,
"units": "%"
},
"wind_speed": {
"value": 4.56,
"units": "mph"
},
"wind_direction": {
"value": 328.58,
"units": "degrees"
},
"weather_code": {
"value": "clear"
},
"observation_time": {
"value": "2021-03-08T10:00:00.000Z"
}
},
{
"lon": -77.03012,
"lat": 38.90694,
"temp": {
"value": 31.02,
"units": "F"
},
"precipitation": {
"value": 0,
"units": "in/hr"
},
"precipitation_probability": {
"value": 0,
"units": "%"
},
"wind_speed": {
"value": 2.8,
"units": "mph"
},
"wind_direction": {
"value": 322.27,
"units": "degrees"
},
"weather_code": {
"value": "clear"
},
"observation_time": {
"value": "2021-03-08T11:00:00.000Z"
}
},
{
"lon": -77.03012,
"lat": 38.90694,
"temp": {
"value": 31.04,
"units": "F"
},
"precipitation": {
"value": 0,
"units": "in/hr"
},
"precipitation_probability": {
"value": 0,
"units": "%"
},
"wind_speed": {
"value": 2.82,
"units": "mph"
},
"wind_direction": {
"value": 325.27,
"units": "degrees"
},
"weather_code": {
"value": "clear"
},
"observation_time": {
"value": "2021-03-08T12:00:00.000Z"
}
},
{
"lat": 38.90694,
"lon": -77.03012,
"temp": {
"value": 29.95,
"units": "F"
},
"precipitation": {
"value": 0,
"units": "in/hr"
},
"precipitation_probability": {
"value": 0,
"units": "%"
},
"wind_speed": {
"value": 7.24,
"units": "mph"
},
"wind_direction": {
"value": 324.8,
"units": "degrees"
},
"weather_code": {
"value": "mostly_clear"
},
"observation_time": {
"value": "2021-03-08T13:00:00.000Z"
}
},
{
"lat": 38.90694,
"lon": -77.03012,
"temp": {
"value": 34.02,
"units": "F"
},
"precipitation": {
"value": 0,
"units": "in/hr"
},
"precipitation_probability": {
"value": 0,
"units": "%"
},
"wind_speed": {
"value": 6.28,
"units": "mph"
},
"wind_direction": {
"value": 335.16,
"units": "degrees"
},
"weather_code": {
"value": "partly_cloudy"
},
"observation_time": {
"value": "2021-03-08T14:00:00.000Z"
}
},
{
"lat": 38.90694,
"lon": -77.03012,
"temp": {
"value": 37.78,
"units": "F"
},
"precipitation": {
"value": 0,
"units": "in/hr"
},
"precipitation_probability": {
"value": 0,
"units": "%"
},
"wind_speed": {
"value": 5.8,
"units": "mph"
},
"wind_direction": {
"value": 324.49,
"units": "degrees"
},
"weather_code": {
"value": "cloudy"
},
"observation_time": {
"value": "2021-03-08T15:00:00.000Z"
}
},
{
"lat": 38.90694,
"lon": -77.03012,
"temp": {
"value": 40.57,
"units": "F"
},
"precipitation": {
"value": 0,
"units": "in/hr"
},
"precipitation_probability": {
"value": 0,
"units": "%"
},
"wind_speed": {
"value": 5.5,
"units": "mph"
},
"wind_direction": {
"value": 310.68,
"units": "degrees"
},
"weather_code": {
"value": "mostly_cloudy"
},
"observation_time": {
"value": "2021-03-08T16:00:00.000Z"
}
},
{
"lat": 38.90694,
"lon": -77.03012,
"temp": {
"value": 42.83,
"units": "F"
},
"precipitation": {
"value": 0,
"units": "in/hr"
},
"precipitation_probability": {
"value": 0,
"units": "%"
},
"wind_speed": {
"value": 5.47,
"units": "mph"
},
"wind_direction": {
"value": 304.18,
"units": "degrees"
},
"weather_code": {
"value": "mostly_clear"
},
"observation_time": {
"value": "2021-03-08T17:00:00.000Z"
}
},
{
"lat": 38.90694,
"lon": -77.03012,
"temp": {
"value": 45.07,
"units": "F"
},
"precipitation": {
"value": 0,
"units": "in/hr"
},
"precipitation_probability": {
"value": 0,
"units": "%"
},
"wind_speed": {
"value": 4.88,
"units": "mph"
},
"wind_direction": {
"value": 301.19,
"units": "degrees"
},
"weather_code": {
"value": "clear"
},
"observation_time": {
"value": "2021-03-08T18:00:00.000Z"
}
}
]

View File

@ -1,782 +0,0 @@
[
{
"lat": 38.90694,
"lon": -77.03012,
"temp": {
"value": 44.14,
"units": "F"
},
"wind_speed": {
"value": 9.58,
"units": "mph"
},
"precipitation": {
"value": 0,
"units": "in/hr"
},
"wind_direction": {
"value": 320.22,
"units": "degrees"
},
"observation_time": {
"value": "2021-03-07T18:54:06.493Z"
},
"weather_code": {
"value": "clear"
}
},
{
"lat": 38.90694,
"lon": -77.03012,
"temp": {
"value": 44.17,
"units": "F"
},
"wind_speed": {
"value": 9.59,
"units": "mph"
},
"precipitation": {
"value": 0,
"units": "in/hr"
},
"wind_direction": {
"value": 320.22,
"units": "degrees"
},
"observation_time": {
"value": "2021-03-07T18:55:06.493Z"
},
"weather_code": {
"value": "clear"
}
},
{
"lat": 38.90694,
"lon": -77.03012,
"temp": {
"value": 44.19,
"units": "F"
},
"wind_speed": {
"value": 9.6,
"units": "mph"
},
"precipitation": {
"value": 0,
"units": "in/hr"
},
"wind_direction": {
"value": 320.22,
"units": "degrees"
},
"observation_time": {
"value": "2021-03-07T18:56:06.493Z"
},
"weather_code": {
"value": "clear"
}
},
{
"lat": 38.90694,
"lon": -77.03012,
"temp": {
"value": 44.22,
"units": "F"
},
"wind_speed": {
"value": 9.61,
"units": "mph"
},
"precipitation": {
"value": 0,
"units": "in/hr"
},
"wind_direction": {
"value": 320.22,
"units": "degrees"
},
"observation_time": {
"value": "2021-03-07T18:57:06.493Z"
},
"weather_code": {
"value": "clear"
}
},
{
"lat": 38.90694,
"lon": -77.03012,
"temp": {
"value": 44.24,
"units": "F"
},
"wind_speed": {
"value": 9.62,
"units": "mph"
},
"precipitation": {
"value": 0,
"units": "in/hr"
},
"wind_direction": {
"value": 320.22,
"units": "degrees"
},
"observation_time": {
"value": "2021-03-07T18:58:06.493Z"
},
"weather_code": {
"value": "clear"
}
},
{
"lat": 38.90694,
"lon": -77.03012,
"temp": {
"value": 44.27,
"units": "F"
},
"wind_speed": {
"value": 9.64,
"units": "mph"
},
"precipitation": {
"value": 0,
"units": "in/hr"
},
"wind_direction": {
"value": 320.22,
"units": "degrees"
},
"observation_time": {
"value": "2021-03-07T18:59:06.493Z"
},
"weather_code": {
"value": "clear"
}
},
{
"lat": 38.90694,
"lon": -77.03012,
"temp": {
"value": 44.29,
"units": "F"
},
"wind_speed": {
"value": 9.65,
"units": "mph"
},
"precipitation": {
"value": 0,
"units": "in/hr"
},
"wind_direction": {
"value": 326.14,
"units": "degrees"
},
"observation_time": {
"value": "2021-03-07T19:00:06.493Z"
},
"weather_code": {
"value": "clear"
}
},
{
"lat": 38.90694,
"lon": -77.03012,
"temp": {
"value": 44.31,
"units": "F"
},
"wind_speed": {
"value": 9.64,
"units": "mph"
},
"precipitation": {
"value": 0,
"units": "in/hr"
},
"wind_direction": {
"value": 326.14,
"units": "degrees"
},
"observation_time": {
"value": "2021-03-07T19:01:06.493Z"
},
"weather_code": {
"value": "clear"
}
},
{
"lat": 38.90694,
"lon": -77.03012,
"temp": {
"value": 44.33,
"units": "F"
},
"wind_speed": {
"value": 9.63,
"units": "mph"
},
"precipitation": {
"value": 0,
"units": "in/hr"
},
"wind_direction": {
"value": 326.14,
"units": "degrees"
},
"observation_time": {
"value": "2021-03-07T19:02:06.493Z"
},
"weather_code": {
"value": "clear"
}
},
{
"lat": 38.90694,
"lon": -77.03012,
"temp": {
"value": 44.34,
"units": "F"
},
"wind_speed": {
"value": 9.63,
"units": "mph"
},
"precipitation": {
"value": 0,
"units": "in/hr"
},
"wind_direction": {
"value": 326.14,
"units": "degrees"
},
"observation_time": {
"value": "2021-03-07T19:03:06.493Z"
},
"weather_code": {
"value": "clear"
}
},
{
"lat": 38.90694,
"lon": -77.03012,
"temp": {
"value": 44.36,
"units": "F"
},
"wind_speed": {
"value": 9.62,
"units": "mph"
},
"precipitation": {
"value": 0,
"units": "in/hr"
},
"wind_direction": {
"value": 326.14,
"units": "degrees"
},
"observation_time": {
"value": "2021-03-07T19:04:06.493Z"
},
"weather_code": {
"value": "clear"
}
},
{
"lat": 38.90694,
"lon": -77.03012,
"temp": {
"value": 44.38,
"units": "F"
},
"wind_speed": {
"value": 9.61,
"units": "mph"
},
"precipitation": {
"value": 0,
"units": "in/hr"
},
"wind_direction": {
"value": 326.14,
"units": "degrees"
},
"observation_time": {
"value": "2021-03-07T19:05:06.493Z"
},
"weather_code": {
"value": "clear"
}
},
{
"lat": 38.90694,
"lon": -77.03012,
"temp": {
"value": 44.4,
"units": "F"
},
"wind_speed": {
"value": 9.61,
"units": "mph"
},
"precipitation": {
"value": 0,
"units": "in/hr"
},
"wind_direction": {
"value": 326.14,
"units": "degrees"
},
"observation_time": {
"value": "2021-03-07T19:06:06.493Z"
},
"weather_code": {
"value": "clear"
}
},
{
"lat": 38.90694,
"lon": -77.03012,
"temp": {
"value": 44.41,
"units": "F"
},
"wind_speed": {
"value": 9.6,
"units": "mph"
},
"precipitation": {
"value": 0,
"units": "in/hr"
},
"wind_direction": {
"value": 326.14,
"units": "degrees"
},
"observation_time": {
"value": "2021-03-07T19:07:06.493Z"
},
"weather_code": {
"value": "clear"
}
},
{
"lat": 38.90694,
"lon": -77.03012,
"temp": {
"value": 44.43,
"units": "F"
},
"wind_speed": {
"value": 9.6,
"units": "mph"
},
"precipitation": {
"value": 0,
"units": "in/hr"
},
"wind_direction": {
"value": 326.14,
"units": "degrees"
},
"observation_time": {
"value": "2021-03-07T19:08:06.493Z"
},
"weather_code": {
"value": "clear"
}
},
{
"lat": 38.90694,
"lon": -77.03012,
"temp": {
"value": 44.45,
"units": "F"
},
"wind_speed": {
"value": 9.59,
"units": "mph"
},
"precipitation": {
"value": 0,
"units": "in/hr"
},
"wind_direction": {
"value": 326.14,
"units": "degrees"
},
"observation_time": {
"value": "2021-03-07T19:09:06.493Z"
},
"weather_code": {
"value": "clear"
}
},
{
"lat": 38.90694,
"lon": -77.03012,
"temp": {
"value": 44.46,
"units": "F"
},
"wind_speed": {
"value": 9.58,
"units": "mph"
},
"precipitation": {
"value": 0,
"units": "in/hr"
},
"wind_direction": {
"value": 326.14,
"units": "degrees"
},
"observation_time": {
"value": "2021-03-07T19:10:06.493Z"
},
"weather_code": {
"value": "clear"
}
},
{
"lat": 38.90694,
"lon": -77.03012,
"temp": {
"value": 44.48,
"units": "F"
},
"wind_speed": {
"value": 9.58,
"units": "mph"
},
"precipitation": {
"value": 0,
"units": "in/hr"
},
"wind_direction": {
"value": 326.14,
"units": "degrees"
},
"observation_time": {
"value": "2021-03-07T19:11:06.493Z"
},
"weather_code": {
"value": "clear"
}
},
{
"lat": 38.90694,
"lon": -77.03012,
"temp": {
"value": 44.5,
"units": "F"
},
"wind_speed": {
"value": 9.57,
"units": "mph"
},
"precipitation": {
"value": 0,
"units": "in/hr"
},
"wind_direction": {
"value": 326.14,
"units": "degrees"
},
"observation_time": {
"value": "2021-03-07T19:12:06.493Z"
},
"weather_code": {
"value": "clear"
}
},
{
"lat": 38.90694,
"lon": -77.03012,
"temp": {
"value": 44.51,
"units": "F"
},
"wind_speed": {
"value": 9.57,
"units": "mph"
},
"precipitation": {
"value": 0,
"units": "in/hr"
},
"wind_direction": {
"value": 326.14,
"units": "degrees"
},
"observation_time": {
"value": "2021-03-07T19:13:06.493Z"
},
"weather_code": {
"value": "clear"
}
},
{
"lat": 38.90694,
"lon": -77.03012,
"temp": {
"value": 44.53,
"units": "F"
},
"wind_speed": {
"value": 9.56,
"units": "mph"
},
"precipitation": {
"value": 0,
"units": "in/hr"
},
"wind_direction": {
"value": 326.14,
"units": "degrees"
},
"observation_time": {
"value": "2021-03-07T19:14:06.493Z"
},
"weather_code": {
"value": "clear"
}
},
{
"lat": 38.90694,
"lon": -77.03012,
"temp": {
"value": 44.55,
"units": "F"
},
"wind_speed": {
"value": 9.55,
"units": "mph"
},
"precipitation": {
"value": 0,
"units": "in/hr"
},
"wind_direction": {
"value": 326.14,
"units": "degrees"
},
"observation_time": {
"value": "2021-03-07T19:15:06.493Z"
},
"weather_code": {
"value": "clear"
}
},
{
"lat": 38.90694,
"lon": -77.03012,
"temp": {
"value": 44.56,
"units": "F"
},
"wind_speed": {
"value": 9.55,
"units": "mph"
},
"precipitation": {
"value": 0,
"units": "in/hr"
},
"wind_direction": {
"value": 326.14,
"units": "degrees"
},
"observation_time": {
"value": "2021-03-07T19:16:06.493Z"
},
"weather_code": {
"value": "clear"
}
},
{
"lat": 38.90694,
"lon": -77.03012,
"temp": {
"value": 44.58,
"units": "F"
},
"wind_speed": {
"value": 9.54,
"units": "mph"
},
"precipitation": {
"value": 0,
"units": "in/hr"
},
"wind_direction": {
"value": 326.14,
"units": "degrees"
},
"observation_time": {
"value": "2021-03-07T19:17:06.493Z"
},
"weather_code": {
"value": "clear"
}
},
{
"lat": 38.90694,
"lon": -77.03012,
"temp": {
"value": 44.6,
"units": "F"
},
"wind_speed": {
"value": 9.54,
"units": "mph"
},
"precipitation": {
"value": 0,
"units": "in/hr"
},
"wind_direction": {
"value": 326.14,
"units": "degrees"
},
"observation_time": {
"value": "2021-03-07T19:18:06.493Z"
},
"weather_code": {
"value": "clear"
}
},
{
"lat": 38.90694,
"lon": -77.03012,
"temp": {
"value": 44.61,
"units": "F"
},
"wind_speed": {
"value": 9.53,
"units": "mph"
},
"precipitation": {
"value": 0,
"units": "in/hr"
},
"wind_direction": {
"value": 326.14,
"units": "degrees"
},
"observation_time": {
"value": "2021-03-07T19:19:06.493Z"
},
"weather_code": {
"value": "clear"
}
},
{
"lat": 38.90694,
"lon": -77.03012,
"temp": {
"value": 44.63,
"units": "F"
},
"wind_speed": {
"value": 9.52,
"units": "mph"
},
"precipitation": {
"value": 0,
"units": "in/hr"
},
"wind_direction": {
"value": 326.14,
"units": "degrees"
},
"observation_time": {
"value": "2021-03-07T19:20:06.493Z"
},
"weather_code": {
"value": "clear"
}
},
{
"lat": 38.90694,
"lon": -77.03012,
"temp": {
"value": 44.65,
"units": "F"
},
"wind_speed": {
"value": 9.52,
"units": "mph"
},
"precipitation": {
"value": 0,
"units": "in/hr"
},
"wind_direction": {
"value": 326.14,
"units": "degrees"
},
"observation_time": {
"value": "2021-03-07T19:21:06.493Z"
},
"weather_code": {
"value": "clear"
}
},
{
"lat": 38.90694,
"lon": -77.03012,
"temp": {
"value": 44.66,
"units": "F"
},
"wind_speed": {
"value": 9.51,
"units": "mph"
},
"precipitation": {
"value": 0,
"units": "in/hr"
},
"wind_direction": {
"value": 326.14,
"units": "degrees"
},
"observation_time": {
"value": "2021-03-07T19:22:06.493Z"
},
"weather_code": {
"value": "clear"
}
},
{
"lat": 38.90694,
"lon": -77.03012,
"temp": {
"value": 44.68,
"units": "F"
},
"wind_speed": {
"value": 9.51,
"units": "mph"
},
"precipitation": {
"value": 0,
"units": "in/hr"
},
"wind_direction": {
"value": 326.14,
"units": "degrees"
},
"observation_time": {
"value": "2021-03-07T19:23:06.493Z"
},
"weather_code": {
"value": "clear"
}
}
]

View File

@ -1,102 +0,0 @@
{
"lat": 38.90694,
"lon": -77.03012,
"temp": {
"value": 43.93,
"units": "F"
},
"wind_speed": {
"value": 9.09,
"units": "mph"
},
"baro_pressure": {
"value": 30.3605,
"units": "inHg"
},
"visibility": {
"value": 6.21,
"units": "mi"
},
"humidity": {
"value": 24.5,
"units": "%"
},
"wind_direction": {
"value": 320.31,
"units": "degrees"
},
"weather_code": {
"value": "clear"
},
"o3": {
"value": 52.625,
"units": "ppb"
},
"wind_gust": {
"value": 14.96,
"units": "mph"
},
"precipitation_type": {
"value": "rain"
},
"cloud_cover": {
"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

@ -1,46 +0,0 @@
"""Test the ClimaCell config flow."""
from homeassistant import data_entry_flow
from homeassistant.components.climacell.const import (
CONF_TIMESTEP,
DEFAULT_TIMESTEP,
DOMAIN,
)
from homeassistant.config_entries import SOURCE_USER
from homeassistant.core import HomeAssistant
from .const import API_V3_ENTRY_DATA
from tests.common import MockConfigEntry
async def test_options_flow(
hass: HomeAssistant, climacell_config_entry_update: None
) -> None:
"""Test options config flow for climacell."""
entry = MockConfigEntry(
domain=DOMAIN,
data=API_V3_ENTRY_DATA,
source=SOURCE_USER,
unique_id="test",
version=1,
)
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
assert entry.options[CONF_TIMESTEP] == DEFAULT_TIMESTEP
assert CONF_TIMESTEP not in entry.data
result = await hass.config_entries.options.async_init(entry.entry_id, data=None)
assert result["type"] == data_entry_flow.FlowResultType.FORM
assert result["step_id"] == "init"
result = await hass.config_entries.options.async_configure(
result["flow_id"], user_input={CONF_TIMESTEP: 1}
)
assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
assert result["title"] == ""
assert result["data"][CONF_TIMESTEP] == 1
assert entry.options[CONF_TIMESTEP] == 1

View File

@ -1,14 +0,0 @@
"""Tests for ClimaCell const."""
import pytest
from homeassistant.components.climacell.const import ClimaCellSensorEntityDescription
from homeassistant.const import TEMP_FAHRENHEIT
async def test_post_init():
"""Test post initialization check for ClimaCellSensorEntityDescription."""
with pytest.raises(RuntimeError):
ClimaCellSensorEntityDescription(
key="a", name="b", unit_imperial=TEMP_FAHRENHEIT
)

View File

@ -1,106 +0,0 @@
"""Tests for Climacell init."""
from unittest.mock import patch
import pytest
from homeassistant.components.climacell.const import CONF_TIMESTEP, DOMAIN
from homeassistant.components.weather import DOMAIN as WEATHER_DOMAIN
from homeassistant.const import CONF_API_VERSION
from homeassistant.core import HomeAssistant
from .const import API_V3_ENTRY_DATA
from tests.common import MockConfigEntry
async def test_load_and_unload(
hass: HomeAssistant,
climacell_config_entry_update: pytest.fixture,
) -> None:
"""Test loading and unloading entry."""
config_entry = MockConfigEntry(
domain=DOMAIN,
data=API_V3_ENTRY_DATA,
unique_id="test",
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()
assert len(hass.states.async_entity_ids(WEATHER_DOMAIN)) == 1
assert await hass.config_entries.async_remove(config_entry.entry_id)
await hass.async_block_till_done()
assert len(hass.states.async_entity_ids(WEATHER_DOMAIN)) == 0
async def test_v3_load_and_unload(
hass: HomeAssistant,
climacell_config_entry_update: pytest.fixture,
) -> None:
"""Test loading and unloading v3 entry."""
config_entry = MockConfigEntry(
domain=DOMAIN,
data={k: v for k, v in API_V3_ENTRY_DATA.items() if k != CONF_API_VERSION},
unique_id="test",
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()
assert len(hass.states.async_entity_ids(WEATHER_DOMAIN)) == 1
assert await hass.config_entries.async_remove(config_entry.entry_id)
await hass.async_block_till_done()
assert len(hass.states.async_entity_ids(WEATHER_DOMAIN)) == 0
async def test_v4_load_and_unload(
hass: HomeAssistant,
climacell_config_entry_update: pytest.fixture,
) -> None:
"""Test loading and unloading v3 entry."""
config_entry = MockConfigEntry(
domain=DOMAIN,
data={
CONF_API_VERSION: 4,
**{k: v for k, v in API_V3_ENTRY_DATA.items() if k != CONF_API_VERSION},
},
unique_id="test",
version=1,
)
config_entry.add_to_hass(hass)
with patch(
"homeassistant.components.tomorrowio.async_setup_entry", return_value=True
):
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert len(hass.states.async_entity_ids(WEATHER_DOMAIN)) == 0
@pytest.mark.parametrize(
"old_timestep, new_timestep", [(2, 1), (7, 5), (20, 15), (21, 30)]
)
async def test_migrate_timestep(
hass: HomeAssistant,
climacell_config_entry_update: pytest.fixture,
old_timestep: int,
new_timestep: int,
) -> None:
"""Test migration to standardized timestep."""
config_entry = MockConfigEntry(
domain=DOMAIN,
data=API_V3_ENTRY_DATA,
options={CONF_TIMESTEP: old_timestep},
unique_id="test",
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()
assert config_entry.version == 1
assert (
CONF_API_VERSION in config_entry.data
and config_entry.data[CONF_API_VERSION] == 3
)
assert config_entry.options[CONF_TIMESTEP] == new_timestep

View File

@ -1,148 +0,0 @@
"""Tests for Climacell sensor entities."""
from __future__ import annotations
from datetime import datetime
from typing import Any
from unittest.mock import patch
import pytest
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 HomeAssistant, State, callback
from homeassistant.helpers.entity_registry import async_get
from homeassistant.util import dt as dt_util
from .const import API_V3_ENTRY_DATA
from tests.common import MockConfigEntry
CC_SENSOR_ENTITY_ID = "sensor.climacell_{}"
O3 = "ozone"
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"
FEELS_LIKE = "feels_like"
DEW_POINT = "dew_point"
PRESSURE_SURFACE_LEVEL = "pressure_surface_level"
SNOW_ACCUMULATION = "snow_accumulation"
ICE_ACCUMULATION = "ice_accumulation"
GHI = "global_horizontal_irradiance"
CLOUD_BASE = "cloud_base"
CLOUD_COVER = "cloud_cover"
CLOUD_CEILING = "cloud_ceiling"
WIND_GUST = "wind_gust"
PRECIPITATION_TYPE = "precipitation_type"
V3_FIELDS = [
O3,
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,
]
V4_FIELDS = [
*V3_FIELDS,
FEELS_LIKE,
DEW_POINT,
PRESSURE_SURFACE_LEVEL,
GHI,
CLOUD_BASE,
CLOUD_COVER,
CLOUD_CEILING,
WIND_GUST,
PRECIPITATION_TYPE,
]
@callback
def _enable_entity(hass: HomeAssistant, 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: HomeAssistant, sensors: list[str], 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=dt_util.UTC),
):
config_entry = MockConfigEntry(
domain=DOMAIN,
data=config,
unique_id="test",
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 sensors:
_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)) == len(sensors)
def check_sensor_state(hass: HomeAssistant, 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: HomeAssistant,
climacell_config_entry_update: pytest.fixture,
) -> None:
"""Test v3 sensor data."""
await _setup(hass, V3_FIELDS, API_V3_ENTRY_DATA)
check_sensor_state(hass, O3, "52.625")
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, "minimal_to_none")
check_sensor_state(hass, WEED_POLLEN, "minimal_to_none")
check_sensor_state(hass, TREE_POLLEN, "minimal_to_none")

View File

@ -1,223 +0,0 @@
"""Tests for Climacell weather entity."""
from __future__ import annotations
from datetime import datetime
from typing import Any
from unittest.mock import patch
import pytest
from homeassistant.components.climacell.const import (
ATTR_CLOUD_COVER,
ATTR_PRECIPITATION_TYPE,
ATTR_WIND_GUST,
ATTRIBUTION,
DOMAIN,
)
from homeassistant.components.weather import (
ATTR_CONDITION_CLOUDY,
ATTR_CONDITION_RAINY,
ATTR_CONDITION_SNOWY,
ATTR_CONDITION_SUNNY,
ATTR_FORECAST,
ATTR_FORECAST_CONDITION,
ATTR_FORECAST_PRECIPITATION,
ATTR_FORECAST_PRECIPITATION_PROBABILITY,
ATTR_FORECAST_TEMP,
ATTR_FORECAST_TEMP_LOW,
ATTR_FORECAST_TIME,
ATTR_WEATHER_HUMIDITY,
ATTR_WEATHER_OZONE,
ATTR_WEATHER_PRESSURE,
ATTR_WEATHER_TEMPERATURE,
ATTR_WEATHER_VISIBILITY,
ATTR_WEATHER_WIND_BEARING,
ATTR_WEATHER_WIND_SPEED,
DOMAIN as WEATHER_DOMAIN,
)
from homeassistant.const import ATTR_ATTRIBUTION, ATTR_FRIENDLY_NAME
from homeassistant.core import HomeAssistant, State, callback
from homeassistant.helpers.entity_registry import async_get
from homeassistant.util import dt as dt_util
from .const import API_V3_ENTRY_DATA
from tests.common import MockConfigEntry
@callback
def _enable_entity(hass: HomeAssistant, 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: HomeAssistant, 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=dt_util.UTC),
):
config_entry = MockConfigEntry(
domain=DOMAIN,
data=config,
unique_id="test",
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 ("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
return hass.states.get("weather.climacell_daily")
async def test_v3_weather(
hass: HomeAssistant,
climacell_config_entry_update: pytest.fixture,
) -> None:
"""Test v3 weather data."""
weather_state = await _setup(hass, API_V3_ENTRY_DATA)
assert weather_state.state == ATTR_CONDITION_SUNNY
assert weather_state.attributes[ATTR_ATTRIBUTION] == ATTRIBUTION
assert weather_state.attributes[ATTR_FORECAST] == [
{
ATTR_FORECAST_CONDITION: ATTR_CONDITION_SUNNY,
ATTR_FORECAST_TIME: "2021-03-07T00:00:00-08:00",
ATTR_FORECAST_PRECIPITATION: 0,
ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0,
ATTR_FORECAST_TEMP: 7.2,
ATTR_FORECAST_TEMP_LOW: -4.7,
},
{
ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY,
ATTR_FORECAST_TIME: "2021-03-08T00:00:00-08:00",
ATTR_FORECAST_PRECIPITATION: 0,
ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0,
ATTR_FORECAST_TEMP: 9.7,
ATTR_FORECAST_TEMP_LOW: -4.0,
},
{
ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY,
ATTR_FORECAST_TIME: "2021-03-09T00:00:00-08:00",
ATTR_FORECAST_PRECIPITATION: 0,
ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0,
ATTR_FORECAST_TEMP: 19.4,
ATTR_FORECAST_TEMP_LOW: -0.3,
},
{
ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY,
ATTR_FORECAST_TIME: "2021-03-10T00:00:00-08:00",
ATTR_FORECAST_PRECIPITATION: 0,
ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0,
ATTR_FORECAST_TEMP: 18.5,
ATTR_FORECAST_TEMP_LOW: 3.0,
},
{
ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY,
ATTR_FORECAST_TIME: "2021-03-11T00:00:00-08:00",
ATTR_FORECAST_PRECIPITATION: 0,
ATTR_FORECAST_PRECIPITATION_PROBABILITY: 5,
ATTR_FORECAST_TEMP: 19.7,
ATTR_FORECAST_TEMP_LOW: 9.3,
},
{
ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY,
ATTR_FORECAST_TIME: "2021-03-12T00:00:00-08:00",
ATTR_FORECAST_PRECIPITATION: 0.05,
ATTR_FORECAST_PRECIPITATION_PROBABILITY: 25,
ATTR_FORECAST_TEMP: 19.9,
ATTR_FORECAST_TEMP_LOW: 12.1,
},
{
ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY,
ATTR_FORECAST_TIME: "2021-03-13T00:00:00-08:00",
ATTR_FORECAST_PRECIPITATION: 0,
ATTR_FORECAST_PRECIPITATION_PROBABILITY: 25,
ATTR_FORECAST_TEMP: 15.8,
ATTR_FORECAST_TEMP_LOW: 7.5,
},
{
ATTR_FORECAST_CONDITION: ATTR_CONDITION_RAINY,
ATTR_FORECAST_TIME: "2021-03-14T00:00:00-08:00",
ATTR_FORECAST_PRECIPITATION: 1.07,
ATTR_FORECAST_PRECIPITATION_PROBABILITY: 75,
ATTR_FORECAST_TEMP: 6.4,
ATTR_FORECAST_TEMP_LOW: 3.2,
},
{
ATTR_FORECAST_CONDITION: ATTR_CONDITION_SNOWY,
ATTR_FORECAST_TIME: "2021-03-15T00:00:00-07:00", # DST starts
ATTR_FORECAST_PRECIPITATION: 7.31,
ATTR_FORECAST_PRECIPITATION_PROBABILITY: 95,
ATTR_FORECAST_TEMP: 1.2,
ATTR_FORECAST_TEMP_LOW: 0.2,
},
{
ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY,
ATTR_FORECAST_TIME: "2021-03-16T00:00:00-07:00",
ATTR_FORECAST_PRECIPITATION: 0.01,
ATTR_FORECAST_PRECIPITATION_PROBABILITY: 5,
ATTR_FORECAST_TEMP: 6.1,
ATTR_FORECAST_TEMP_LOW: -1.6,
},
{
ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY,
ATTR_FORECAST_TIME: "2021-03-17T00:00:00-07:00",
ATTR_FORECAST_PRECIPITATION: 0,
ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0,
ATTR_FORECAST_TEMP: 11.3,
ATTR_FORECAST_TEMP_LOW: 1.3,
},
{
ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY,
ATTR_FORECAST_TIME: "2021-03-18T00:00:00-07:00",
ATTR_FORECAST_PRECIPITATION: 0,
ATTR_FORECAST_PRECIPITATION_PROBABILITY: 5,
ATTR_FORECAST_TEMP: 12.3,
ATTR_FORECAST_TEMP_LOW: 5.6,
},
{
ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY,
ATTR_FORECAST_TIME: "2021-03-19T00:00:00-07:00",
ATTR_FORECAST_PRECIPITATION: 0.18,
ATTR_FORECAST_PRECIPITATION_PROBABILITY: 45,
ATTR_FORECAST_TEMP: 9.4,
ATTR_FORECAST_TEMP_LOW: 4.7,
},
{
ATTR_FORECAST_CONDITION: ATTR_CONDITION_RAINY,
ATTR_FORECAST_TIME: "2021-03-20T00:00:00-07:00",
ATTR_FORECAST_PRECIPITATION: 1.23,
ATTR_FORECAST_PRECIPITATION_PROBABILITY: 55,
ATTR_FORECAST_TEMP: 5.0,
ATTR_FORECAST_TEMP_LOW: 3.1,
},
{
ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY,
ATTR_FORECAST_TIME: "2021-03-21T00:00:00-07:00",
ATTR_FORECAST_PRECIPITATION: 0.04,
ATTR_FORECAST_PRECIPITATION_PROBABILITY: 20,
ATTR_FORECAST_TEMP: 6.8,
ATTR_FORECAST_TEMP_LOW: 0.9,
},
]
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.12
assert weather_state.attributes[ATTR_WEATHER_TEMPERATURE] == 6.6
assert weather_state.attributes[ATTR_WEATHER_VISIBILITY] == 9.99
assert weather_state.attributes[ATTR_WEATHER_WIND_BEARING] == 320.31
assert weather_state.attributes[ATTR_WEATHER_WIND_SPEED] == 14.63
assert weather_state.attributes[ATTR_CLOUD_COVER] == 100
assert weather_state.attributes[ATTR_WIND_GUST] == 24.0758
assert weather_state.attributes[ATTR_PRECIPITATION_TYPE] == "rain"

View File

@ -33,22 +33,3 @@ def tomorrowio_config_entry_update_fixture():
mock_max_requests_per_day.return_value = 100
mock_num_api_requests.return_value = 2
yield mock_update
@pytest.fixture(name="climacell_config_entry_update")
def climacell_config_entry_update_fixture():
"""Mock valid climacell config entry setup."""
with patch(
"homeassistant.components.climacell.ClimaCellV3.realtime",
return_value={},
), patch(
"homeassistant.components.climacell.ClimaCellV3.forecast_hourly",
return_value={},
), patch(
"homeassistant.components.climacell.ClimaCellV3.forecast_daily",
return_value={},
), patch(
"homeassistant.components.climacell.ClimaCellV3.forecast_nowcast",
return_value={},
):
yield

View File

@ -9,7 +9,6 @@ from pytomorrowio.exceptions import (
)
from homeassistant import data_entry_flow
from homeassistant.components.climacell import DOMAIN as CC_DOMAIN
from homeassistant.components.tomorrowio.config_flow import (
_get_config_schema,
_get_unique_id,
@ -20,10 +19,9 @@ from homeassistant.components.tomorrowio.const import (
DEFAULT_TIMESTEP,
DOMAIN,
)
from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER, ConfigEntryState
from homeassistant.config_entries import SOURCE_USER
from homeassistant.const import (
CONF_API_KEY,
CONF_API_VERSION,
CONF_LATITUDE,
CONF_LOCATION,
CONF_LONGITUDE,
@ -36,7 +34,6 @@ from homeassistant.setup import async_setup_component
from .const import API_KEY, MIN_CONFIG
from tests.common import MockConfigEntry
from tests.components.climacell.const import API_V3_ENTRY_DATA
async def test_user_flow_minimum_fields(hass: HomeAssistant) -> None:
@ -210,73 +207,3 @@ async def test_options_flow(hass: HomeAssistant) -> None:
assert result["title"] == ""
assert result["data"][CONF_TIMESTEP] == 1
assert entry.options[CONF_TIMESTEP] == 1
async def test_import_flow_v4(hass: HomeAssistant) -> None:
"""Test import flow for climacell v4 config entry."""
user_config = API_V3_ENTRY_DATA.copy()
user_config[CONF_API_VERSION] = 4
old_entry = MockConfigEntry(
domain=CC_DOMAIN,
data=user_config,
source=SOURCE_USER,
unique_id="test",
version=1,
)
old_entry.add_to_hass(hass)
await hass.config_entries.async_setup(old_entry.entry_id)
await hass.async_block_till_done()
assert old_entry.state != ConfigEntryState.LOADED
assert len(hass.config_entries.async_entries(CC_DOMAIN)) == 0
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
entry = hass.config_entries.async_entries(DOMAIN)[0]
assert "old_config_entry_id" not in entry.data
assert CONF_API_VERSION not in entry.data
async def test_import_flow_v3(
hass: HomeAssistant, climacell_config_entry_update
) -> None:
"""Test import flow for climacell v3 config entry."""
user_config = API_V3_ENTRY_DATA
old_entry = MockConfigEntry(
domain=CC_DOMAIN,
data=user_config,
source=SOURCE_USER,
unique_id="test",
version=1,
)
old_entry.add_to_hass(hass)
await hass.config_entries.async_setup(old_entry.entry_id)
assert old_entry.state == ConfigEntryState.LOADED
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_IMPORT, "old_config_entry_id": old_entry.entry_id},
data=old_entry.data,
)
assert result["type"] == data_entry_flow.FlowResultType.FORM
assert result["step_id"] == "user"
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_API_KEY: "this is a test"}
)
assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
assert result["data"] == {
CONF_API_KEY: "this is a test",
CONF_LOCATION: {
CONF_LATITUDE: 80.0,
CONF_LONGITUDE: 80.0,
},
CONF_NAME: "ClimaCell",
"old_config_entry_id": old_entry.entry_id,
}
await hass.async_block_till_done()
assert len(hass.config_entries.async_entries(CC_DOMAIN)) == 0
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
entry = hass.config_entries.async_entries(DOMAIN)[0]
assert "old_config_entry_id" not in entry.data
assert CONF_API_VERSION not in entry.data

View File

@ -2,30 +2,20 @@
from datetime import timedelta
from unittest.mock import patch
from homeassistant.components.climacell import CONF_TIMESTEP, DOMAIN as CC_DOMAIN
from homeassistant.components.tomorrowio.config_flow import (
_get_config_schema,
_get_unique_id,
)
from homeassistant.components.tomorrowio.const import DOMAIN
from homeassistant.components.tomorrowio.const import CONF_TIMESTEP, DOMAIN
from homeassistant.components.weather import DOMAIN as WEATHER_DOMAIN
from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER
from homeassistant.const import (
CONF_API_KEY,
CONF_API_VERSION,
CONF_LATITUDE,
CONF_LOCATION,
CONF_LONGITUDE,
CONF_NAME,
)
from homeassistant.config_entries import SOURCE_USER
from homeassistant.const import CONF_API_KEY, CONF_NAME
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.util import dt as dt_util
from .const import MIN_CONFIG
from tests.common import MockConfigEntry, async_fire_time_changed
from tests.components.climacell.const import API_V3_ENTRY_DATA
NEW_NAME = "New Name"
@ -126,118 +116,3 @@ async def test_update_intervals(
assert len(tomorrowio_config_entry_update.call_args_list) == 2
tomorrowio_config_entry_update.reset_mock()
async def test_climacell_migration_logic(
hass: HomeAssistant, climacell_config_entry_update
) -> None:
"""Test that climacell config entry is properly migrated."""
old_data = API_V3_ENTRY_DATA.copy()
old_data[CONF_API_KEY] = "v3apikey"
old_config_entry = MockConfigEntry(
domain=CC_DOMAIN,
data=old_data,
unique_id="v3apikey_80.0_80.0",
version=1,
)
old_config_entry.add_to_hass(hass)
# Let's create a device and update its name
dev_reg = dr.async_get(hass)
old_device = dev_reg.async_get_or_create(
config_entry_id=old_config_entry.entry_id,
identifiers={(CC_DOMAIN, old_data[CONF_API_KEY])},
manufacturer="ClimaCell",
sw_version="v4",
entry_type="service",
name="ClimaCell",
)
dev_reg.async_update_device(old_device.id, name_by_user=NEW_NAME)
# Now let's create some entity and update some things to see if everything migrates
# over
ent_reg = er.async_get(hass)
old_entity_daily = ent_reg.async_get_or_create(
"weather",
CC_DOMAIN,
"v3apikey_80.0_80.0_daily",
config_entry=old_config_entry,
original_name="ClimaCell - Daily",
suggested_object_id="climacell_daily",
device_id=old_device.id,
)
old_entity_hourly = ent_reg.async_get_or_create(
"weather",
CC_DOMAIN,
"v3apikey_80.0_80.0_hourly",
config_entry=old_config_entry,
original_name="ClimaCell - Hourly",
suggested_object_id="climacell_hourly",
device_id=old_device.id,
disabled_by=er.RegistryEntryDisabler.USER,
)
old_entity_nowcast = ent_reg.async_get_or_create(
"weather",
CC_DOMAIN,
"v3apikey_80.0_80.0_nowcast",
config_entry=old_config_entry,
original_name="ClimaCell - Nowcast",
suggested_object_id="climacell_nowcast",
device_id=old_device.id,
)
ent_reg.async_update_entity(old_entity_daily.entity_id, name=NEW_NAME)
# Now let's create a new tomorrowio config entry that is supposedly created from a
# climacell import and see what happens - we are also changing the API key to ensure
# that things work as expected
new_data = API_V3_ENTRY_DATA.copy()
new_data[CONF_LOCATION] = {
CONF_LATITUDE: float(new_data.pop(CONF_LATITUDE)),
CONF_LONGITUDE: float(new_data.pop(CONF_LONGITUDE)),
}
new_data[CONF_API_VERSION] = 4
new_data["old_config_entry_id"] = old_config_entry.entry_id
config_entry = MockConfigEntry(
domain=DOMAIN,
data=new_data,
unique_id=_get_unique_id(hass, new_data),
version=1,
source=SOURCE_IMPORT,
)
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
# Check that the old device no longer exists
assert dev_reg.async_get(old_device.id) is None
# Check that the new device was created and that it has the correct name
assert (
dr.async_entries_for_config_entry(dev_reg, config_entry.entry_id)[
0
].name_by_user
== NEW_NAME
)
# Check that the new entities match the old ones (minus the default name)
new_entity_daily = ent_reg.async_get(old_entity_daily.entity_id)
assert new_entity_daily.platform == DOMAIN
assert new_entity_daily.name == NEW_NAME
assert new_entity_daily.original_name == "ClimaCell - Daily"
assert new_entity_daily.device_id != old_device.id
assert new_entity_daily.unique_id == f"{_get_unique_id(hass, new_data)}_daily"
assert new_entity_daily.disabled_by is None
new_entity_hourly = ent_reg.async_get(old_entity_hourly.entity_id)
assert new_entity_hourly.platform == DOMAIN
assert new_entity_hourly.name is None
assert new_entity_hourly.original_name == "ClimaCell - Hourly"
assert new_entity_hourly.device_id != old_device.id
assert new_entity_hourly.unique_id == f"{_get_unique_id(hass, new_data)}_hourly"
assert new_entity_hourly.disabled_by == er.RegistryEntryDisabler.USER
new_entity_nowcast = ent_reg.async_get(old_entity_nowcast.entity_id)
assert new_entity_nowcast.platform == DOMAIN
assert new_entity_nowcast.name is None
assert new_entity_nowcast.original_name == "ClimaCell - Nowcast"
assert new_entity_nowcast.device_id != old_device.id
assert new_entity_nowcast.unique_id == f"{_get_unique_id(hass, new_data)}_nowcast"
assert new_entity_nowcast.disabled_by is None