Add ClimaCell v4 API support (#47575)

* Add ClimaCell v4 API support

* fix tests

* use constants

* fix logic and update tests

* revert accidental changes and enable hourly and nowcast forecast entities in test

* use variable instead of accessing dictionary multiple times

* only grab necessary fields

* add _translate_condition method ot base class

* bump pyclimacell again to fix bug

* switch typehints back to new format

* more typehint fixes

* fix tests

* revert merge conflict change

* handle 'migration' in async_setup_entry so we don't have to bump config entry versions

* parametrize timestep test
This commit is contained in:
Raman Gupta 2021-04-05 13:39:39 -04:00 committed by GitHub
parent 0f757c3db2
commit d0b3f76a6f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 5973 additions and 219 deletions

View File

@ -145,7 +145,6 @@ omit =
homeassistant/components/clickatell/notify.py
homeassistant/components/clicksend/notify.py
homeassistant/components/clicksend_tts/notify.py
homeassistant/components/climacell/weather.py
homeassistant/components/cmus/media_player.py
homeassistant/components/co2signal/*
homeassistant/components/coinbase/*

View File

@ -7,14 +7,9 @@ import logging
from math import ceil
from typing import Any
from pyclimacell import ClimaCell
from pyclimacell.const import (
FORECAST_DAILY,
FORECAST_HOURLY,
FORECAST_NOWCAST,
REALTIME,
)
from pyclimacell.pyclimacell import (
from pyclimacell import ClimaCellV3, ClimaCellV4
from pyclimacell.const import CURRENT, DAILY, FORECASTS, HOURLY, NOWCAST
from pyclimacell.exceptions import (
CantConnectException,
InvalidAPIKeyException,
RateLimitedException,
@ -23,7 +18,13 @@ from pyclimacell.pyclimacell import (
from homeassistant.components.weather import DOMAIN as WEATHER_DOMAIN
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
from homeassistant.const import (
CONF_API_KEY,
CONF_API_VERSION,
CONF_LATITUDE,
CONF_LONGITUDE,
CONF_NAME,
)
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.helpers.update_coordinator import (
@ -34,15 +35,34 @@ from homeassistant.helpers.update_coordinator import (
from .const import (
ATTRIBUTION,
CC_ATTR_CONDITION,
CC_ATTR_HUMIDITY,
CC_ATTR_OZONE,
CC_ATTR_PRECIPITATION,
CC_ATTR_PRECIPITATION_PROBABILITY,
CC_ATTR_PRESSURE,
CC_ATTR_TEMPERATURE,
CC_ATTR_TEMPERATURE_HIGH,
CC_ATTR_TEMPERATURE_LOW,
CC_ATTR_VISIBILITY,
CC_ATTR_WIND_DIRECTION,
CC_ATTR_WIND_SPEED,
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_PRESSURE,
CC_V3_ATTR_TEMPERATURE,
CC_V3_ATTR_VISIBILITY,
CC_V3_ATTR_WIND_DIRECTION,
CC_V3_ATTR_WIND_SPEED,
CONF_TIMESTEP,
CURRENT,
DAILY,
DEFAULT_FORECAST_TYPE,
DEFAULT_TIMESTEP,
DOMAIN,
FORECASTS,
HOURLY,
MAX_REQUESTS_PER_DAY,
NOWCAST,
)
_LOGGER = logging.getLogger(__name__)
@ -54,6 +74,7 @@ def _set_update_interval(
hass: HomeAssistantType, 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
@ -68,7 +89,7 @@ def _set_update_interval(
interval = timedelta(
minutes=(
ceil(
(24 * 60 * (len(other_instance_entry_ids) + 1) * 4)
(24 * 60 * (len(other_instance_entry_ids) + 1) * api_calls)
/ (MAX_REQUESTS_PER_DAY * 0.9)
)
)
@ -85,24 +106,48 @@ async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry)
"""Set up ClimaCell API from a config entry."""
hass.data.setdefault(DOMAIN, {})
params = {}
# If config entry options not set up, set them up
if not config_entry.options:
hass.config_entries.async_update_entry(
config_entry,
options={
CONF_TIMESTEP: DEFAULT_TIMESTEP,
},
)
params["options"] = {
CONF_TIMESTEP: DEFAULT_TIMESTEP,
}
else:
# Use valid timestep if it's invalid
timestep = config_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 = config_entry.options.copy()
new_options[CONF_TIMESTEP] = timestep
params["options"] = new_options
# Add API version if not found
if CONF_API_VERSION not in config_entry.data:
new_data = config_entry.data.copy()
new_data[CONF_API_VERSION] = 3
params["data"] = new_data
if params:
hass.config_entries.async_update_entry(config_entry, **params)
api_class = ClimaCellV3 if config_entry.data[CONF_API_VERSION] == 3 else ClimaCellV4
api = api_class(
config_entry.data[CONF_API_KEY],
config_entry.data.get(CONF_LATITUDE, hass.config.latitude),
config_entry.data.get(CONF_LONGITUDE, hass.config.longitude),
session=async_get_clientsession(hass),
)
coordinator = ClimaCellDataUpdateCoordinator(
hass,
config_entry,
ClimaCell(
config_entry.data[CONF_API_KEY],
config_entry.data.get(CONF_LATITUDE, hass.config.latitude),
config_entry.data.get(CONF_LONGITUDE, hass.config.longitude),
session=async_get_clientsession(hass),
),
api,
_set_update_interval(hass, config_entry),
)
@ -145,12 +190,13 @@ class ClimaCellDataUpdateCoordinator(DataUpdateCoordinator):
self,
hass: HomeAssistantType,
config_entry: ConfigEntry,
api: ClimaCell,
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: {}}
@ -166,27 +212,81 @@ class ClimaCellDataUpdateCoordinator(DataUpdateCoordinator):
"""Update data via library."""
data = {FORECASTS: {}}
try:
data[CURRENT] = await self._api.realtime(
self._api.available_fields(REALTIME)
)
data[FORECASTS][HOURLY] = await self._api.forecast_hourly(
self._api.available_fields(FORECAST_HOURLY),
None,
timedelta(hours=24),
)
if self._api_version == 3:
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,
]
)
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(
self._api.available_fields(FORECAST_DAILY), None, timedelta(days=14)
)
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(
self._api.available_fields(FORECAST_NOWCAST),
None,
timedelta(
minutes=min(300, self._config_entry.options[CONF_TIMESTEP] * 30)
),
self._config_entry.options[CONF_TIMESTEP],
)
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],
)
else:
return await self._api.realtime_and_all_forecasts(
[
CC_ATTR_TEMPERATURE,
CC_ATTR_HUMIDITY,
CC_ATTR_PRESSURE,
CC_ATTR_WIND_SPEED,
CC_ATTR_WIND_DIRECTION,
CC_ATTR_CONDITION,
CC_ATTR_VISIBILITY,
CC_ATTR_OZONE,
],
[
CC_ATTR_TEMPERATURE_LOW,
CC_ATTR_TEMPERATURE_HIGH,
CC_ATTR_WIND_SPEED,
CC_ATTR_WIND_DIRECTION,
CC_ATTR_CONDITION,
CC_ATTR_PRECIPITATION,
CC_ATTR_PRECIPITATION_PROBABILITY,
],
)
except (
CantConnectException,
InvalidAPIKeyException,
@ -202,10 +302,16 @@ class ClimaCellEntity(CoordinatorEntity):
"""Base ClimaCell Entity."""
def __init__(
self, config_entry: ConfigEntry, coordinator: ClimaCellDataUpdateCoordinator
self,
config_entry: ConfigEntry,
coordinator: ClimaCellDataUpdateCoordinator,
forecast_type: str,
api_version: int,
) -> None:
"""Initialize ClimaCell Entity."""
super().__init__(coordinator)
self.api_version = api_version
self.forecast_type = forecast_type
self._config_entry = config_entry
@staticmethod
@ -229,15 +335,23 @@ class ClimaCellEntity(CoordinatorEntity):
return items.get("value")
@property
def entity_registry_enabled_default(self) -> bool:
"""Return if the entity should be enabled when first added to the entity registry."""
if self.forecast_type == DEFAULT_FORECAST_TYPE:
return True
return False
@property
def name(self) -> str:
"""Return the name of the entity."""
return self._config_entry.data[CONF_NAME]
return f"{self._config_entry.data[CONF_NAME]} - {self.forecast_type.title()}"
@property
def unique_id(self) -> str:
"""Return the unique id of the entity."""
return self._config_entry.unique_id
return f"{self._config_entry.unique_id}_{self.forecast_type}"
@property
def attribution(self):

View File

@ -4,23 +4,36 @@ from __future__ import annotations
import logging
from typing import Any
from pyclimacell import ClimaCell
from pyclimacell.const import REALTIME
from pyclimacell import ClimaCellV3
from pyclimacell.exceptions import (
CantConnectException,
InvalidAPIKeyException,
RateLimitedException,
)
from pyclimacell.pyclimacell import ClimaCellV4
import voluptuous as vol
from homeassistant import config_entries, core
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
from homeassistant.const import (
CONF_API_KEY,
CONF_API_VERSION,
CONF_LATITUDE,
CONF_LONGITUDE,
CONF_NAME,
)
from homeassistant.core import callback
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.typing import HomeAssistantType
from .const import CONF_TIMESTEP, DEFAULT_NAME, DEFAULT_TIMESTEP, DOMAIN
from .const import (
CC_ATTR_TEMPERATURE,
CC_V3_ATTR_TEMPERATURE,
CONF_TIMESTEP,
DEFAULT_NAME,
DEFAULT_TIMESTEP,
DOMAIN,
)
_LOGGER = logging.getLogger(__name__)
@ -43,6 +56,7 @@ def _get_config_schema(
CONF_NAME, default=input_dict.get(CONF_NAME, DEFAULT_NAME)
): str,
vol.Required(CONF_API_KEY, default=input_dict.get(CONF_API_KEY)): str,
vol.Required(CONF_API_VERSION, default=4): vol.In([3, 4]),
vol.Inclusive(
CONF_LATITUDE,
"location",
@ -85,7 +99,7 @@ class ClimaCellOptionsConfigFlow(config_entries.OptionsFlow):
vol.Required(
CONF_TIMESTEP,
default=self._config_entry.options.get(CONF_TIMESTEP, DEFAULT_TIMESTEP),
): vol.All(vol.Coerce(int), vol.Range(min=1, max=60)),
): vol.In([1, 5, 15, 30]),
}
return self.async_show_form(
@ -119,12 +133,18 @@ class ClimaCellConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
self._abort_if_unique_id_configured()
try:
await ClimaCell(
if user_input[CONF_API_VERSION] == 3:
api_class = ClimaCellV3
field = CC_V3_ATTR_TEMPERATURE
else:
api_class = ClimaCellV4
field = CC_ATTR_TEMPERATURE
await api_class(
user_input[CONF_API_KEY],
str(user_input.get(CONF_LATITUDE, self.hass.config.latitude)),
str(user_input.get(CONF_LONGITUDE, self.hass.config.longitude)),
session=async_get_clientsession(self.hass),
).realtime(ClimaCell.first_field(REALTIME))
).realtime([field])
return self.async_create_entry(
title=user_input[CONF_NAME], data=user_input

View File

@ -1,4 +1,5 @@
"""Constants for the ClimaCell integration."""
from pyclimacell.const import DAILY, HOURLY, NOWCAST, WeatherCode
from homeassistant.components.weather import (
ATTR_CONDITION_CLEAR_NIGHT,
@ -16,15 +17,8 @@ from homeassistant.components.weather import (
)
CONF_TIMESTEP = "timestep"
DAILY = "daily"
HOURLY = "hourly"
NOWCAST = "nowcast"
FORECAST_TYPES = [DAILY, HOURLY, NOWCAST]
CURRENT = "current"
FORECASTS = "forecasts"
DEFAULT_NAME = "ClimaCell"
DEFAULT_TIMESTEP = 15
DEFAULT_FORECAST_TYPE = DAILY
@ -33,7 +27,58 @@ ATTRIBUTION = "Powered by ClimaCell"
MAX_REQUESTS_PER_DAY = 1000
CLEAR_CONDITIONS = {"night": ATTR_CONDITION_CLEAR_NIGHT, "day": ATTR_CONDITION_SUNNY}
MAX_FORECASTS = {
DAILY: 14,
HOURLY: 24,
NOWCAST: 30,
}
# V4 constants
CONDITIONS = {
WeatherCode.WIND: ATTR_CONDITION_WINDY,
WeatherCode.LIGHT_WIND: ATTR_CONDITION_WINDY,
WeatherCode.STRONG_WIND: ATTR_CONDITION_WINDY,
WeatherCode.FREEZING_RAIN: ATTR_CONDITION_SNOWY_RAINY,
WeatherCode.HEAVY_FREEZING_RAIN: ATTR_CONDITION_SNOWY_RAINY,
WeatherCode.LIGHT_FREEZING_RAIN: ATTR_CONDITION_SNOWY_RAINY,
WeatherCode.FREEZING_DRIZZLE: ATTR_CONDITION_SNOWY_RAINY,
WeatherCode.ICE_PELLETS: ATTR_CONDITION_HAIL,
WeatherCode.HEAVY_ICE_PELLETS: ATTR_CONDITION_HAIL,
WeatherCode.LIGHT_ICE_PELLETS: ATTR_CONDITION_HAIL,
WeatherCode.SNOW: ATTR_CONDITION_SNOWY,
WeatherCode.HEAVY_SNOW: ATTR_CONDITION_SNOWY,
WeatherCode.LIGHT_SNOW: ATTR_CONDITION_SNOWY,
WeatherCode.FLURRIES: ATTR_CONDITION_SNOWY,
WeatherCode.THUNDERSTORM: ATTR_CONDITION_LIGHTNING,
WeatherCode.RAIN: ATTR_CONDITION_POURING,
WeatherCode.HEAVY_RAIN: ATTR_CONDITION_RAINY,
WeatherCode.LIGHT_RAIN: ATTR_CONDITION_RAINY,
WeatherCode.DRIZZLE: ATTR_CONDITION_RAINY,
WeatherCode.FOG: ATTR_CONDITION_FOG,
WeatherCode.LIGHT_FOG: ATTR_CONDITION_FOG,
WeatherCode.CLOUDY: ATTR_CONDITION_CLOUDY,
WeatherCode.MOSTLY_CLOUDY: ATTR_CONDITION_CLOUDY,
WeatherCode.PARTLY_CLOUDY: ATTR_CONDITION_PARTLYCLOUDY,
}
CC_ATTR_TIMESTAMP = "startTime"
CC_ATTR_TEMPERATURE = "temperature"
CC_ATTR_TEMPERATURE_HIGH = "temperatureMax"
CC_ATTR_TEMPERATURE_LOW = "temperatureMin"
CC_ATTR_PRESSURE = "pressureSeaLevel"
CC_ATTR_HUMIDITY = "humidity"
CC_ATTR_WIND_SPEED = "windSpeed"
CC_ATTR_WIND_DIRECTION = "windDirection"
CC_ATTR_OZONE = "pollutantO3"
CC_ATTR_CONDITION = "weatherCode"
CC_ATTR_VISIBILITY = "visibility"
CC_ATTR_PRECIPITATION = "precipitationIntensityAvg"
CC_ATTR_PRECIPITATION_PROBABILITY = "precipitationProbability"
# V3 constants
CONDITIONS_V3 = {
"breezy": ATTR_CONDITION_WINDY,
"freezing_rain_heavy": ATTR_CONDITION_SNOWY_RAINY,
"freezing_rain": ATTR_CONDITION_SNOWY_RAINY,
@ -58,24 +103,17 @@ CONDITIONS = {
"partly_cloudy": ATTR_CONDITION_PARTLYCLOUDY,
}
CLEAR_CONDITIONS = {"night": ATTR_CONDITION_CLEAR_NIGHT, "day": ATTR_CONDITION_SUNNY}
CC_ATTR_TIMESTAMP = "observation_time"
CC_ATTR_TEMPERATURE = "temp"
CC_ATTR_TEMPERATURE_HIGH = "max"
CC_ATTR_TEMPERATURE_LOW = "min"
CC_ATTR_PRESSURE = "baro_pressure"
CC_ATTR_HUMIDITY = "humidity"
CC_ATTR_WIND_SPEED = "wind_speed"
CC_ATTR_WIND_DIRECTION = "wind_direction"
CC_ATTR_OZONE = "o3"
CC_ATTR_CONDITION = "weather_code"
CC_ATTR_VISIBILITY = "visibility"
CC_ATTR_PRECIPITATION = "precipitation"
CC_ATTR_PRECIPITATION_DAILY = "precipitation_accumulation"
CC_ATTR_PRECIPITATION_PROBABILITY = "precipitation_probability"
CC_ATTR_PM_2_5 = "pm25"
CC_ATTR_PM_10 = "pm10"
CC_ATTR_CARBON_MONOXIDE = "co"
CC_ATTR_SULPHUR_DIOXIDE = "so2"
CC_ATTR_NITROGEN_DIOXIDE = "no2"
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"

View File

@ -3,6 +3,6 @@
"name": "ClimaCell",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/climacell",
"requirements": ["pyclimacell==0.14.0"],
"requirements": ["pyclimacell==0.18.0"],
"codeowners": ["@raman325"]
}

View File

@ -7,6 +7,7 @@
"data": {
"name": "[%key:common::config_flow::data::name%]",
"api_key": "[%key:common::config_flow::data::api_key%]",
"api_version": "API Version",
"latitude": "[%key:common::config_flow::data::latitude%]",
"longitude": "[%key:common::config_flow::data::longitude%]"
}
@ -25,8 +26,7 @@
"title": "Update [%key:component::climacell::title%] 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",
"forecast_types": "Forecast Type(s)"
"timestep": "Min. Between NowCast Forecasts"
}
}
}

View File

@ -5,6 +5,8 @@ from datetime import datetime
import logging
from typing import Any, Callable
from pyclimacell.const import CURRENT, DAILY, FORECASTS, HOURLY, NOWCAST, WeatherCode
from homeassistant.components.weather import (
ATTR_FORECAST_CONDITION,
ATTR_FORECAST_PRECIPITATION,
@ -18,6 +20,7 @@ from homeassistant.components.weather import (
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_API_VERSION,
LENGTH_FEET,
LENGTH_KILOMETERS,
LENGTH_METERS,
@ -33,13 +36,12 @@ from homeassistant.util import dt as dt_util
from homeassistant.util.distance import convert as distance_convert
from homeassistant.util.pressure import convert as pressure_convert
from . import ClimaCellDataUpdateCoordinator, ClimaCellEntity
from . import ClimaCellEntity
from .const import (
CC_ATTR_CONDITION,
CC_ATTR_HUMIDITY,
CC_ATTR_OZONE,
CC_ATTR_PRECIPITATION,
CC_ATTR_PRECIPITATION_DAILY,
CC_ATTR_PRECIPITATION_PROBABILITY,
CC_ATTR_PRESSURE,
CC_ATTR_TEMPERATURE,
@ -49,16 +51,26 @@ from .const import (
CC_ATTR_VISIBILITY,
CC_ATTR_WIND_DIRECTION,
CC_ATTR_WIND_SPEED,
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_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_SPEED,
CLEAR_CONDITIONS,
CONDITIONS,
CONDITIONS_V3,
CONF_TIMESTEP,
CURRENT,
DAILY,
DEFAULT_FORECAST_TYPE,
DOMAIN,
FORECASTS,
HOURLY,
NOWCAST,
MAX_FORECASTS,
)
# mypy: allow-untyped-defs, no-check-untyped-defs
@ -66,57 +78,6 @@ from .const import (
_LOGGER = logging.getLogger(__name__)
def _translate_condition(condition: str | None, sun_is_up: bool = True) -> str | None:
"""Translate ClimaCell condition into an HA condition."""
if not condition:
return None
if "clear" in condition.lower():
if sun_is_up:
return CLEAR_CONDITIONS["day"]
return CLEAR_CONDITIONS["night"]
return CONDITIONS[condition]
def _forecast_dict(
hass: HomeAssistantType,
forecast_dt: datetime,
use_datetime: bool,
condition: 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 = _translate_condition(condition, is_up(hass, forecast_dt))
else:
translated_condition = _translate_condition(condition, True)
if hass.config.units.is_metric:
if precipitation:
precipitation = (
distance_convert(precipitation / 12, LENGTH_FEET, LENGTH_METERS) * 1000
)
if wind_speed:
wind_speed = distance_convert(wind_speed, LENGTH_MILES, LENGTH_KILOMETERS)
data = {
ATTR_FORECAST_TIME: forecast_dt.isoformat(),
ATTR_FORECAST_CONDITION: translated_condition,
ATTR_FORECAST_PRECIPITATION: precipitation,
ATTR_FORECAST_PRECIPITATION_PROBABILITY: precipitation_probability,
ATTR_FORECAST_TEMP: temp,
ATTR_FORECAST_TEMP_LOW: temp_low,
ATTR_FORECAST_WIND_BEARING: wind_direction,
ATTR_FORECAST_WIND_SPEED: wind_speed,
}
return {k: v for k, v in data.items() if v is not None}
async def async_setup_entry(
hass: HomeAssistantType,
config_entry: ConfigEntry,
@ -124,49 +85,97 @@ async def async_setup_entry(
) -> None:
"""Set up a config entry."""
coordinator = hass.data[DOMAIN][config_entry.entry_id]
api_version = config_entry.data[CONF_API_VERSION]
api_class = ClimaCellV3WeatherEntity if api_version == 3 else ClimaCellWeatherEntity
entities = [
ClimaCellWeatherEntity(config_entry, coordinator, forecast_type)
api_class(config_entry, coordinator, forecast_type, api_version)
for forecast_type in [DAILY, HOURLY, NOWCAST]
]
async_add_entities(entities)
class ClimaCellWeatherEntity(ClimaCellEntity, WeatherEntity):
"""Entity that talks to ClimaCell API to retrieve weather data."""
class BaseClimaCellWeatherEntity(ClimaCellEntity, WeatherEntity):
"""Base ClimaCell weather entity."""
def __init__(
@staticmethod
def _translate_condition(
condition: int | None, sun_is_up: bool = True
) -> str | None:
"""Translate ClimaCell condition into an HA condition."""
raise NotImplementedError()
def _forecast_dict(
self,
config_entry: ConfigEntry,
coordinator: ClimaCellDataUpdateCoordinator,
forecast_type: str,
) -> None:
"""Initialize ClimaCell weather entity."""
super().__init__(config_entry, coordinator)
self.forecast_type = forecast_type
forecast_dt: datetime,
use_datetime: bool,
condition: 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)
@property
def entity_registry_enabled_default(self) -> bool:
"""Return if the entity should be enabled when first added to the entity registry."""
if self.forecast_type == DEFAULT_FORECAST_TYPE:
return True
if self.hass.config.units.is_metric:
if precipitation:
precipitation = (
distance_convert(precipitation / 12, LENGTH_FEET, LENGTH_METERS)
* 1000
)
if wind_speed:
wind_speed = distance_convert(
wind_speed, LENGTH_MILES, LENGTH_KILOMETERS
)
return False
data = {
ATTR_FORECAST_TIME: forecast_dt.isoformat(),
ATTR_FORECAST_CONDITION: translated_condition,
ATTR_FORECAST_PRECIPITATION: precipitation,
ATTR_FORECAST_PRECIPITATION_PROBABILITY: precipitation_probability,
ATTR_FORECAST_TEMP: temp,
ATTR_FORECAST_TEMP_LOW: temp_low,
ATTR_FORECAST_WIND_BEARING: wind_direction,
ATTR_FORECAST_WIND_SPEED: wind_speed,
}
@property
def name(self) -> str:
"""Return the name of the entity."""
return f"{super().name} - {self.forecast_type.title()}"
return {k: v for k, v in data.items() if v is not None}
@property
def unique_id(self) -> str:
"""Return the unique id of the entity."""
return f"{super().unique_id}_{self.forecast_type}"
class ClimaCellWeatherEntity(BaseClimaCellWeatherEntity):
"""Entity that talks to ClimaCell v4 API to retrieve weather data."""
@staticmethod
def _translate_condition(
condition: int | None, sun_is_up: bool = True
) -> str | None:
"""Translate ClimaCell condition into an HA condition."""
if condition is None:
return None
# We won't guard here, instead we will fail hard
condition = WeatherCode(condition)
if condition in (WeatherCode.CLEAR, WeatherCode.MOSTLY_CLEAR):
if sun_is_up:
return CLEAR_CONDITIONS["day"]
return CLEAR_CONDITIONS["night"]
return CONDITIONS[condition]
def _get_current_property(self, property_name: str) -> int | str | float | None:
"""Get property from current conditions."""
return self.coordinator.data.get(CURRENT, {}).get(property_name)
@property
def temperature(self):
"""Return the platform temperature."""
return self._get_cc_value(self.coordinator.data[CURRENT], CC_ATTR_TEMPERATURE)
return self._get_current_property(CC_ATTR_TEMPERATURE)
@property
def temperature_unit(self):
@ -176,7 +185,7 @@ class ClimaCellWeatherEntity(ClimaCellEntity, WeatherEntity):
@property
def pressure(self):
"""Return the pressure."""
pressure = self._get_cc_value(self.coordinator.data[CURRENT], CC_ATTR_PRESSURE)
pressure = self._get_current_property(CC_ATTR_PRESSURE)
if self.hass.config.units.is_metric and pressure:
return pressure_convert(pressure, PRESSURE_INHG, PRESSURE_HPA)
return pressure
@ -184,13 +193,156 @@ class ClimaCellWeatherEntity(ClimaCellEntity, WeatherEntity):
@property
def humidity(self):
"""Return the humidity."""
return self._get_cc_value(self.coordinator.data[CURRENT], CC_ATTR_HUMIDITY)
return self._get_current_property(CC_ATTR_HUMIDITY)
@property
def wind_speed(self):
"""Return the wind speed."""
wind_speed = self._get_current_property(CC_ATTR_WIND_SPEED)
if self.hass.config.units.is_metric and wind_speed:
return distance_convert(wind_speed, LENGTH_MILES, LENGTH_KILOMETERS)
return wind_speed
@property
def wind_bearing(self):
"""Return the wind bearing."""
return self._get_current_property(CC_ATTR_WIND_DIRECTION)
@property
def ozone(self):
"""Return the O3 (ozone) level."""
return self._get_current_property(CC_ATTR_OZONE)
@property
def condition(self):
"""Return the condition."""
return self._translate_condition(
self._get_current_property(CC_ATTR_CONDITION),
is_up(self.hass),
)
@property
def visibility(self):
"""Return the visibility."""
visibility = self._get_current_property(CC_ATTR_VISIBILITY)
if self.hass.config.units.is_metric and visibility:
return distance_convert(visibility, LENGTH_MILES, LENGTH_KILOMETERS)
return visibility
@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 = []
max_forecasts = MAX_FORECASTS[self.forecast_type]
forecast_count = 0
# 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(forecast[CC_ATTR_TIMESTAMP])
# Throw out past data
if forecast_dt.date() < dt_util.utcnow().date():
continue
values = forecast["values"]
use_datetime = True
condition = values.get(CC_ATTR_CONDITION)
precipitation = values.get(CC_ATTR_PRECIPITATION)
precipitation_probability = values.get(CC_ATTR_PRECIPITATION_PROBABILITY)
temp = values.get(CC_ATTR_TEMPERATURE_HIGH)
temp_low = values.get(CC_ATTR_TEMPERATURE_LOW)
wind_direction = values.get(CC_ATTR_WIND_DIRECTION)
wind_speed = values.get(CC_ATTR_WIND_SPEED)
if self.forecast_type == DAILY:
use_datetime = False
if precipitation:
precipitation = precipitation * 24
elif self.forecast_type == NOWCAST:
# Precipitation is forecasted in CONF_TIMESTEP increments but in a
# per hour rate, so value needs to be converted to an amount.
if precipitation:
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,
)
)
forecast_count += 1
if forecast_count == max_forecasts:
break
return forecasts
class ClimaCellV3WeatherEntity(BaseClimaCellWeatherEntity):
"""Entity that talks to ClimaCell v3 API to retrieve weather data."""
@staticmethod
def _translate_condition(
condition: str | None, sun_is_up: bool = True
) -> str | None:
"""Translate ClimaCell condition into an HA condition."""
if not condition:
return None
if "clear" in condition.lower():
if sun_is_up:
return CLEAR_CONDITIONS["day"]
return CLEAR_CONDITIONS["night"]
return CONDITIONS_V3[condition]
@property
def temperature(self):
"""Return the platform temperature."""
return self._get_cc_value(
self.coordinator.data[CURRENT], CC_V3_ATTR_TEMPERATURE
)
@property
def temperature_unit(self):
"""Return the unit of measurement."""
return TEMP_FAHRENHEIT
@property
def pressure(self):
"""Return the pressure."""
pressure = self._get_cc_value(
self.coordinator.data[CURRENT], CC_V3_ATTR_PRESSURE
)
if self.hass.config.units.is_metric and pressure:
return pressure_convert(pressure, PRESSURE_INHG, PRESSURE_HPA)
return pressure
@property
def humidity(self):
"""Return the humidity."""
return self._get_cc_value(self.coordinator.data[CURRENT], CC_V3_ATTR_HUMIDITY)
@property
def wind_speed(self):
"""Return the wind speed."""
wind_speed = self._get_cc_value(
self.coordinator.data[CURRENT], CC_ATTR_WIND_SPEED
self.coordinator.data[CURRENT], CC_V3_ATTR_WIND_SPEED
)
if self.hass.config.units.is_metric and wind_speed:
return distance_convert(wind_speed, LENGTH_MILES, LENGTH_KILOMETERS)
@ -200,19 +352,19 @@ class ClimaCellWeatherEntity(ClimaCellEntity, WeatherEntity):
def wind_bearing(self):
"""Return the wind bearing."""
return self._get_cc_value(
self.coordinator.data[CURRENT], CC_ATTR_WIND_DIRECTION
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_ATTR_OZONE)
return self._get_cc_value(self.coordinator.data[CURRENT], CC_V3_ATTR_OZONE)
@property
def condition(self):
"""Return the condition."""
return _translate_condition(
self._get_cc_value(self.coordinator.data[CURRENT], CC_ATTR_CONDITION),
return self._translate_condition(
self._get_cc_value(self.coordinator.data[CURRENT], CC_V3_ATTR_CONDITION),
is_up(self.hass),
)
@ -220,7 +372,7 @@ class ClimaCellWeatherEntity(ClimaCellEntity, WeatherEntity):
def visibility(self):
"""Return the visibility."""
visibility = self._get_cc_value(
self.coordinator.data[CURRENT], CC_ATTR_VISIBILITY
self.coordinator.data[CURRENT], CC_V3_ATTR_VISIBILITY
)
if self.hass.config.units.is_metric and visibility:
return distance_convert(visibility, LENGTH_MILES, LENGTH_KILOMETERS)
@ -230,46 +382,47 @@ class ClimaCellWeatherEntity(ClimaCellEntity, WeatherEntity):
def forecast(self):
"""Return the forecast."""
# Check if forecasts are available
if not self.coordinator.data[FORECASTS].get(self.forecast_type):
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 self.coordinator.data[FORECASTS][self.forecast_type]:
for forecast in raw_forecasts:
forecast_dt = dt_util.parse_datetime(
self._get_cc_value(forecast, CC_ATTR_TIMESTAMP)
self._get_cc_value(forecast, CC_V3_ATTR_TIMESTAMP)
)
use_datetime = True
condition = self._get_cc_value(forecast, CC_ATTR_CONDITION)
precipitation = self._get_cc_value(forecast, CC_ATTR_PRECIPITATION)
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_ATTR_PRECIPITATION_PROBABILITY
forecast, CC_V3_ATTR_PRECIPITATION_PROBABILITY
)
temp = self._get_cc_value(forecast, CC_ATTR_TEMPERATURE)
temp = self._get_cc_value(forecast, CC_V3_ATTR_TEMPERATURE)
temp_low = None
wind_direction = self._get_cc_value(forecast, CC_ATTR_WIND_DIRECTION)
wind_speed = self._get_cc_value(forecast, CC_ATTR_WIND_SPEED)
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_ATTR_PRECIPITATION_DAILY
forecast, CC_V3_ATTR_PRECIPITATION_DAILY
)
temp = next(
(
self._get_cc_value(item, CC_ATTR_TEMPERATURE_HIGH)
for item in forecast[CC_ATTR_TEMPERATURE]
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_ATTR_TEMPERATURE_LOW)
for item in forecast[CC_ATTR_TEMPERATURE]
self._get_cc_value(item, CC_V3_ATTR_TEMPERATURE_LOW)
for item in forecast[CC_V3_ATTR_TEMPERATURE]
if "min" in item
),
temp_low,
@ -282,8 +435,7 @@ class ClimaCellWeatherEntity(ClimaCellEntity, WeatherEntity):
)
forecasts.append(
_forecast_dict(
self.hass,
self._forecast_dict(
forecast_dt,
use_datetime,
condition,

View File

@ -1310,7 +1310,7 @@ pychromecast==9.1.1
pycketcasts==1.0.0
# homeassistant.components.climacell
pyclimacell==0.14.0
pyclimacell==0.18.0
# homeassistant.components.cmus
pycmus==0.1.1

View File

@ -702,7 +702,7 @@ pycfdns==1.2.1
pychromecast==9.1.1
# homeassistant.components.climacell
pyclimacell==0.14.0
pyclimacell==0.18.0
# homeassistant.components.comfoconnect
pycomfoconnect==0.4

View File

@ -1,8 +1,11 @@
"""Configure py.test."""
import json
from unittest.mock import patch
import pytest
from tests.common import load_fixture
@pytest.fixture(name="skip_notifications", autouse=True)
def skip_notifications_fixture():
@ -17,7 +20,10 @@ def skip_notifications_fixture():
def climacell_config_flow_connect():
"""Mock valid climacell config flow setup."""
with patch(
"homeassistant.components.climacell.config_flow.ClimaCell.realtime",
"homeassistant.components.climacell.config_flow.ClimaCellV3.realtime",
return_value={},
), patch(
"homeassistant.components.climacell.config_flow.ClimaCellV4.realtime",
return_value={},
):
yield
@ -27,16 +33,19 @@ def climacell_config_flow_connect():
def climacell_config_entry_update_fixture():
"""Mock valid climacell config entry setup."""
with patch(
"homeassistant.components.climacell.ClimaCell.realtime",
return_value={},
"homeassistant.components.climacell.ClimaCellV3.realtime",
return_value=json.loads(load_fixture("climacell/v3_realtime.json")),
), patch(
"homeassistant.components.climacell.ClimaCell.forecast_hourly",
return_value=[],
"homeassistant.components.climacell.ClimaCellV3.forecast_hourly",
return_value=json.loads(load_fixture("climacell/v3_forecast_hourly.json")),
), patch(
"homeassistant.components.climacell.ClimaCell.forecast_daily",
return_value=[],
"homeassistant.components.climacell.ClimaCellV3.forecast_daily",
return_value=json.loads(load_fixture("climacell/v3_forecast_daily.json")),
), patch(
"homeassistant.components.climacell.ClimaCell.forecast_nowcast",
return_value=[],
"homeassistant.components.climacell.ClimaCellV3.forecast_nowcast",
return_value=json.loads(load_fixture("climacell/v3_forecast_nowcast.json")),
), patch(
"homeassistant.components.climacell.ClimaCellV4.realtime_and_all_forecasts",
return_value=json.loads(load_fixture("climacell/v4.json")),
):
yield

View File

@ -1,9 +1,38 @@
"""Constants for climacell tests."""
from homeassistant.const import CONF_API_KEY
from homeassistant.const import (
CONF_API_KEY,
CONF_API_VERSION,
CONF_LATITUDE,
CONF_LONGITUDE,
CONF_NAME,
)
API_KEY = "aa"
MIN_CONFIG = {
CONF_API_KEY: API_KEY,
}
V1_ENTRY_DATA = {
CONF_NAME: "ClimaCell",
CONF_API_KEY: API_KEY,
CONF_LATITUDE: 80,
CONF_LONGITUDE: 80,
}
API_V3_ENTRY_DATA = {
CONF_NAME: "ClimaCell",
CONF_API_KEY: API_KEY,
CONF_LATITUDE: 80,
CONF_LONGITUDE: 80,
CONF_API_VERSION: 3,
}
API_V4_ENTRY_DATA = {
CONF_NAME: "ClimaCell",
CONF_API_KEY: API_KEY,
CONF_LATITUDE: 80,
CONF_LONGITUDE: 80,
CONF_API_VERSION: 4,
}

View File

@ -21,7 +21,13 @@ from homeassistant.components.climacell.const import (
DOMAIN,
)
from homeassistant.config_entries import SOURCE_USER
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
from homeassistant.const import (
CONF_API_KEY,
CONF_API_VERSION,
CONF_LATITUDE,
CONF_LONGITUDE,
CONF_NAME,
)
from homeassistant.helpers.typing import HomeAssistantType
from .const import API_KEY, MIN_CONFIG
@ -48,6 +54,32 @@ async def test_user_flow_minimum_fields(hass: HomeAssistantType) -> None:
assert result["title"] == DEFAULT_NAME
assert result["data"][CONF_NAME] == DEFAULT_NAME
assert result["data"][CONF_API_KEY] == API_KEY
assert result["data"][CONF_API_VERSION] == 4
assert result["data"][CONF_LATITUDE] == hass.config.latitude
assert result["data"][CONF_LONGITUDE] == hass.config.longitude
async def test_user_flow_v3(hass: HomeAssistantType) -> None:
"""Test user config flow with v3 API."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "user"
data = _get_config_schema(hass, MIN_CONFIG)(MIN_CONFIG)
data[CONF_API_VERSION] = 3
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input=data,
)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["title"] == DEFAULT_NAME
assert result["data"][CONF_NAME] == DEFAULT_NAME
assert result["data"][CONF_API_KEY] == API_KEY
assert result["data"][CONF_API_VERSION] == 3
assert result["data"][CONF_LATITUDE] == hass.config.latitude
assert result["data"][CONF_LONGITUDE] == hass.config.longitude
@ -60,6 +92,7 @@ async def test_user_flow_same_unique_ids(hass: HomeAssistantType) -> None:
data=user_input,
source=SOURCE_USER,
unique_id=_get_unique_id(hass, user_input),
version=2,
).add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
@ -75,7 +108,7 @@ async def test_user_flow_same_unique_ids(hass: HomeAssistantType) -> None:
async def test_user_flow_cannot_connect(hass: HomeAssistantType) -> None:
"""Test user config flow when ClimaCell can't connect."""
with patch(
"homeassistant.components.climacell.config_flow.ClimaCell.realtime",
"homeassistant.components.climacell.config_flow.ClimaCellV4.realtime",
side_effect=CantConnectException,
):
result = await hass.config_entries.flow.async_init(
@ -91,7 +124,7 @@ async def test_user_flow_cannot_connect(hass: HomeAssistantType) -> None:
async def test_user_flow_invalid_api(hass: HomeAssistantType) -> None:
"""Test user config flow when API key is invalid."""
with patch(
"homeassistant.components.climacell.config_flow.ClimaCell.realtime",
"homeassistant.components.climacell.config_flow.ClimaCellV4.realtime",
side_effect=InvalidAPIKeyException,
):
result = await hass.config_entries.flow.async_init(
@ -107,7 +140,7 @@ async def test_user_flow_invalid_api(hass: HomeAssistantType) -> None:
async def test_user_flow_rate_limited(hass: HomeAssistantType) -> None:
"""Test user config flow when API key is rate limited."""
with patch(
"homeassistant.components.climacell.config_flow.ClimaCell.realtime",
"homeassistant.components.climacell.config_flow.ClimaCellV4.realtime",
side_effect=RateLimitedException,
):
result = await hass.config_entries.flow.async_init(
@ -123,7 +156,7 @@ async def test_user_flow_rate_limited(hass: HomeAssistantType) -> None:
async def test_user_flow_unknown_exception(hass: HomeAssistantType) -> None:
"""Test user config flow when unknown error occurs."""
with patch(
"homeassistant.components.climacell.config_flow.ClimaCell.realtime",
"homeassistant.components.climacell.config_flow.ClimaCellV4.realtime",
side_effect=UnknownException,
):
result = await hass.config_entries.flow.async_init(
@ -144,6 +177,7 @@ async def test_options_flow(hass: HomeAssistantType) -> None:
data=user_config,
source=SOURCE_USER,
unique_id=_get_unique_id(hass, user_config),
version=1,
)
entry.add_to_hass(hass)

View File

@ -7,11 +7,12 @@ from homeassistant.components.climacell.config_flow import (
_get_config_schema,
_get_unique_id,
)
from homeassistant.components.climacell.const import DOMAIN
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.helpers.typing import HomeAssistantType
from .const import MIN_CONFIG
from .const import API_V3_ENTRY_DATA, MIN_CONFIG, V1_ENTRY_DATA
from tests.common import MockConfigEntry
@ -23,10 +24,12 @@ async def test_load_and_unload(
climacell_config_entry_update: pytest.fixture,
) -> None:
"""Test loading and unloading entry."""
data = _get_config_schema(hass)(MIN_CONFIG)
config_entry = MockConfigEntry(
domain=DOMAIN,
data=_get_config_schema(hass)(MIN_CONFIG),
unique_id=_get_unique_id(hass, _get_config_schema(hass)(MIN_CONFIG)),
data=data,
unique_id=_get_unique_id(hass, data),
version=1,
)
config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id)
@ -36,3 +39,53 @@ async def test_load_and_unload(
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: HomeAssistantType,
climacell_config_entry_update: pytest.fixture,
) -> None:
"""Test loading and unloading v3 entry."""
data = _get_config_schema(hass)(API_V3_ENTRY_DATA)
config_entry = MockConfigEntry(
domain=DOMAIN,
data=data,
unique_id=_get_unique_id(hass, data),
version=1,
)
config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
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
@pytest.mark.parametrize(
"old_timestep, new_timestep", [(2, 1), (7, 5), (20, 15), (21, 30)]
)
async def test_migrate_timestep(
hass: HomeAssistantType,
climacell_config_entry_update: pytest.fixture,
old_timestep: int,
new_timestep: int,
) -> None:
"""Test migration to standardized timestep."""
config_entry = MockConfigEntry(
domain=DOMAIN,
data=V1_ENTRY_DATA,
options={CONF_TIMESTEP: old_timestep},
unique_id=_get_unique_id(hass, V1_ENTRY_DATA),
version=1,
)
config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
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

@ -0,0 +1,382 @@
"""Tests for Climacell weather entity."""
from datetime import datetime
import logging
from typing import Any, Dict
from unittest.mock import patch
import pytest
import pytz
from homeassistant.components.climacell.config_flow import (
_get_config_schema,
_get_unique_id,
)
from homeassistant.components.climacell.const import ATTRIBUTION, DOMAIN
from homeassistant.components.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_FORECAST_WIND_BEARING,
ATTR_FORECAST_WIND_SPEED,
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 State
from homeassistant.helpers.entity_registry import async_get
from homeassistant.helpers.typing import HomeAssistantType
from .const import API_V3_ENTRY_DATA, API_V4_ENTRY_DATA
from tests.common import MockConfigEntry
_LOGGER = logging.getLogger(__name__)
async def _enable_entity(hass: HomeAssistantType, entity_name: str) -> None:
"""Enable disabled entity."""
ent_reg = async_get(hass)
entry = ent_reg.async_get(entity_name)
updated_entry = ent_reg.async_update_entity(
entry.entity_id, **{"disabled_by": None}
)
assert updated_entry != entry
assert updated_entry.disabled is False
async def _setup(hass: HomeAssistantType, config: Dict[str, Any]) -> State:
"""Set up entry and return entity state."""
with patch(
"homeassistant.util.dt.utcnow",
return_value=datetime(2021, 3, 6, 23, 59, 59, tzinfo=pytz.UTC),
):
data = _get_config_schema(hass)(config)
config_entry = MockConfigEntry(
domain=DOMAIN,
data=data,
unique_id=_get_unique_id(hass, data),
version=1,
)
config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
await _enable_entity(hass, "weather.climacell_hourly")
await _enable_entity(hass, "weather.climacell_nowcast")
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: HomeAssistantType,
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+00:00",
ATTR_FORECAST_PRECIPITATION: 0,
ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0,
ATTR_FORECAST_TEMP: 7,
ATTR_FORECAST_TEMP_LOW: -5,
},
{
ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY,
ATTR_FORECAST_TIME: "2021-03-08T00:00:00+00:00",
ATTR_FORECAST_PRECIPITATION: 0,
ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0,
ATTR_FORECAST_TEMP: 10,
ATTR_FORECAST_TEMP_LOW: -4,
},
{
ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY,
ATTR_FORECAST_TIME: "2021-03-09T00:00:00+00:00",
ATTR_FORECAST_PRECIPITATION: 0,
ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0,
ATTR_FORECAST_TEMP: 19,
ATTR_FORECAST_TEMP_LOW: 0,
},
{
ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY,
ATTR_FORECAST_TIME: "2021-03-10T00:00:00+00:00",
ATTR_FORECAST_PRECIPITATION: 0,
ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0,
ATTR_FORECAST_TEMP: 18,
ATTR_FORECAST_TEMP_LOW: 3,
},
{
ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY,
ATTR_FORECAST_TIME: "2021-03-11T00:00:00+00:00",
ATTR_FORECAST_PRECIPITATION: 0,
ATTR_FORECAST_PRECIPITATION_PROBABILITY: 5,
ATTR_FORECAST_TEMP: 20,
ATTR_FORECAST_TEMP_LOW: 9,
},
{
ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY,
ATTR_FORECAST_TIME: "2021-03-12T00:00:00+00:00",
ATTR_FORECAST_PRECIPITATION: 0.04572,
ATTR_FORECAST_PRECIPITATION_PROBABILITY: 25,
ATTR_FORECAST_TEMP: 20,
ATTR_FORECAST_TEMP_LOW: 12,
},
{
ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY,
ATTR_FORECAST_TIME: "2021-03-13T00:00:00+00:00",
ATTR_FORECAST_PRECIPITATION: 0,
ATTR_FORECAST_PRECIPITATION_PROBABILITY: 25,
ATTR_FORECAST_TEMP: 16,
ATTR_FORECAST_TEMP_LOW: 7,
},
{
ATTR_FORECAST_CONDITION: ATTR_CONDITION_RAINY,
ATTR_FORECAST_TIME: "2021-03-14T00:00:00+00:00",
ATTR_FORECAST_PRECIPITATION: 1.07442,
ATTR_FORECAST_PRECIPITATION_PROBABILITY: 75,
ATTR_FORECAST_TEMP: 6,
ATTR_FORECAST_TEMP_LOW: 3,
},
{
ATTR_FORECAST_CONDITION: ATTR_CONDITION_SNOWY,
ATTR_FORECAST_TIME: "2021-03-15T00:00:00+00:00",
ATTR_FORECAST_PRECIPITATION: 7.305040000000001,
ATTR_FORECAST_PRECIPITATION_PROBABILITY: 95,
ATTR_FORECAST_TEMP: 1,
ATTR_FORECAST_TEMP_LOW: 0,
},
{
ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY,
ATTR_FORECAST_TIME: "2021-03-16T00:00:00+00:00",
ATTR_FORECAST_PRECIPITATION: 0.00508,
ATTR_FORECAST_PRECIPITATION_PROBABILITY: 5,
ATTR_FORECAST_TEMP: 6,
ATTR_FORECAST_TEMP_LOW: -2,
},
{
ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY,
ATTR_FORECAST_TIME: "2021-03-17T00:00:00+00:00",
ATTR_FORECAST_PRECIPITATION: 0,
ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0,
ATTR_FORECAST_TEMP: 11,
ATTR_FORECAST_TEMP_LOW: 1,
},
{
ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY,
ATTR_FORECAST_TIME: "2021-03-18T00:00:00+00:00",
ATTR_FORECAST_PRECIPITATION: 0,
ATTR_FORECAST_PRECIPITATION_PROBABILITY: 5,
ATTR_FORECAST_TEMP: 12,
ATTR_FORECAST_TEMP_LOW: 6,
},
{
ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY,
ATTR_FORECAST_TIME: "2021-03-19T00:00:00+00:00",
ATTR_FORECAST_PRECIPITATION: 0.1778,
ATTR_FORECAST_PRECIPITATION_PROBABILITY: 45,
ATTR_FORECAST_TEMP: 9,
ATTR_FORECAST_TEMP_LOW: 5,
},
{
ATTR_FORECAST_CONDITION: ATTR_CONDITION_RAINY,
ATTR_FORECAST_TIME: "2021-03-20T00:00:00+00:00",
ATTR_FORECAST_PRECIPITATION: 1.2319,
ATTR_FORECAST_PRECIPITATION_PROBABILITY: 55,
ATTR_FORECAST_TEMP: 5,
ATTR_FORECAST_TEMP_LOW: 3,
},
{
ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY,
ATTR_FORECAST_TIME: "2021-03-21T00:00:00+00:00",
ATTR_FORECAST_PRECIPITATION: 0.043179999999999996,
ATTR_FORECAST_PRECIPITATION_PROBABILITY: 20,
ATTR_FORECAST_TEMP: 7,
ATTR_FORECAST_TEMP_LOW: 1,
},
]
assert weather_state.attributes[ATTR_FRIENDLY_NAME] == "ClimaCell - Daily"
assert weather_state.attributes[ATTR_WEATHER_HUMIDITY] == 24
assert weather_state.attributes[ATTR_WEATHER_OZONE] == 52.625
assert weather_state.attributes[ATTR_WEATHER_PRESSURE] == 1028.124632345
assert weather_state.attributes[ATTR_WEATHER_TEMPERATURE] == 7
assert weather_state.attributes[ATTR_WEATHER_VISIBILITY] == 9.994026240000002
assert weather_state.attributes[ATTR_WEATHER_WIND_BEARING] == 320.31
assert weather_state.attributes[ATTR_WEATHER_WIND_SPEED] == 14.62893696
async def test_v4_weather(
hass: HomeAssistantType,
climacell_config_entry_update: pytest.fixture,
) -> None:
"""Test v4 weather data."""
weather_state = await _setup(hass, API_V4_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-07T11:00:00+00:00",
ATTR_FORECAST_PRECIPITATION: 0,
ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0,
ATTR_FORECAST_TEMP: 8,
ATTR_FORECAST_TEMP_LOW: -3,
ATTR_FORECAST_WIND_BEARING: 239.6,
ATTR_FORECAST_WIND_SPEED: 15.272674560000002,
},
{
ATTR_FORECAST_CONDITION: "cloudy",
ATTR_FORECAST_TIME: "2021-03-08T11:00:00+00:00",
ATTR_FORECAST_PRECIPITATION: 0,
ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0,
ATTR_FORECAST_TEMP: 10,
ATTR_FORECAST_TEMP_LOW: -3,
ATTR_FORECAST_WIND_BEARING: 262.82,
ATTR_FORECAST_WIND_SPEED: 11.65165056,
},
{
ATTR_FORECAST_CONDITION: "cloudy",
ATTR_FORECAST_TIME: "2021-03-09T11:00:00+00:00",
ATTR_FORECAST_PRECIPITATION: 0,
ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0,
ATTR_FORECAST_TEMP: 19,
ATTR_FORECAST_TEMP_LOW: 0,
ATTR_FORECAST_WIND_BEARING: 229.3,
ATTR_FORECAST_WIND_SPEED: 11.3458752,
},
{
ATTR_FORECAST_CONDITION: "cloudy",
ATTR_FORECAST_TIME: "2021-03-10T11:00:00+00:00",
ATTR_FORECAST_PRECIPITATION: 0,
ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0,
ATTR_FORECAST_TEMP: 18,
ATTR_FORECAST_TEMP_LOW: 3,
ATTR_FORECAST_WIND_BEARING: 149.91,
ATTR_FORECAST_WIND_SPEED: 17.123420160000002,
},
{
ATTR_FORECAST_CONDITION: "cloudy",
ATTR_FORECAST_TIME: "2021-03-11T11:00:00+00:00",
ATTR_FORECAST_PRECIPITATION: 0,
ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0,
ATTR_FORECAST_TEMP: 19,
ATTR_FORECAST_TEMP_LOW: 9,
ATTR_FORECAST_WIND_BEARING: 210.45,
ATTR_FORECAST_WIND_SPEED: 25.250607360000004,
},
{
ATTR_FORECAST_CONDITION: "rainy",
ATTR_FORECAST_TIME: "2021-03-12T11:00:00+00:00",
ATTR_FORECAST_PRECIPITATION: 0.12192000000000001,
ATTR_FORECAST_PRECIPITATION_PROBABILITY: 25,
ATTR_FORECAST_TEMP: 20,
ATTR_FORECAST_TEMP_LOW: 12,
ATTR_FORECAST_WIND_BEARING: 217.98,
ATTR_FORECAST_WIND_SPEED: 19.794931200000004,
},
{
ATTR_FORECAST_CONDITION: "cloudy",
ATTR_FORECAST_TIME: "2021-03-13T11:00:00+00:00",
ATTR_FORECAST_PRECIPITATION: 0,
ATTR_FORECAST_PRECIPITATION_PROBABILITY: 25,
ATTR_FORECAST_TEMP: 12,
ATTR_FORECAST_TEMP_LOW: 6,
ATTR_FORECAST_WIND_BEARING: 58.79,
ATTR_FORECAST_WIND_SPEED: 15.642823680000001,
},
{
ATTR_FORECAST_CONDITION: "snowy",
ATTR_FORECAST_TIME: "2021-03-14T10:00:00+00:00",
ATTR_FORECAST_PRECIPITATION: 23.95728,
ATTR_FORECAST_PRECIPITATION_PROBABILITY: 95,
ATTR_FORECAST_TEMP: 6,
ATTR_FORECAST_TEMP_LOW: 1,
ATTR_FORECAST_WIND_BEARING: 70.25,
ATTR_FORECAST_WIND_SPEED: 26.15184,
},
{
ATTR_FORECAST_CONDITION: "snowy",
ATTR_FORECAST_TIME: "2021-03-15T10:00:00+00:00",
ATTR_FORECAST_PRECIPITATION: 1.46304,
ATTR_FORECAST_PRECIPITATION_PROBABILITY: 55,
ATTR_FORECAST_TEMP: 6,
ATTR_FORECAST_TEMP_LOW: -1,
ATTR_FORECAST_WIND_BEARING: 84.47,
ATTR_FORECAST_WIND_SPEED: 25.57247616,
},
{
ATTR_FORECAST_CONDITION: "cloudy",
ATTR_FORECAST_TIME: "2021-03-16T10:00:00+00:00",
ATTR_FORECAST_PRECIPITATION: 0,
ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0,
ATTR_FORECAST_TEMP: 6,
ATTR_FORECAST_TEMP_LOW: -2,
ATTR_FORECAST_WIND_BEARING: 103.85,
ATTR_FORECAST_WIND_SPEED: 10.79869824,
},
{
ATTR_FORECAST_CONDITION: "cloudy",
ATTR_FORECAST_TIME: "2021-03-17T10:00:00+00:00",
ATTR_FORECAST_PRECIPITATION: 0,
ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0,
ATTR_FORECAST_TEMP: 11,
ATTR_FORECAST_TEMP_LOW: 1,
ATTR_FORECAST_WIND_BEARING: 145.41,
ATTR_FORECAST_WIND_SPEED: 11.69993088,
},
{
ATTR_FORECAST_CONDITION: "cloudy",
ATTR_FORECAST_TIME: "2021-03-18T10:00:00+00:00",
ATTR_FORECAST_PRECIPITATION: 0,
ATTR_FORECAST_PRECIPITATION_PROBABILITY: 10,
ATTR_FORECAST_TEMP: 12,
ATTR_FORECAST_TEMP_LOW: 5,
ATTR_FORECAST_WIND_BEARING: 62.99,
ATTR_FORECAST_WIND_SPEED: 10.58948352,
},
{
ATTR_FORECAST_CONDITION: "rainy",
ATTR_FORECAST_TIME: "2021-03-19T10:00:00+00:00",
ATTR_FORECAST_PRECIPITATION: 2.92608,
ATTR_FORECAST_PRECIPITATION_PROBABILITY: 55,
ATTR_FORECAST_TEMP: 9,
ATTR_FORECAST_TEMP_LOW: 4,
ATTR_FORECAST_WIND_BEARING: 68.54,
ATTR_FORECAST_WIND_SPEED: 22.38597504,
},
{
ATTR_FORECAST_CONDITION: "snowy",
ATTR_FORECAST_TIME: "2021-03-20T10:00:00+00:00",
ATTR_FORECAST_PRECIPITATION: 1.2192,
ATTR_FORECAST_PRECIPITATION_PROBABILITY: 33.3,
ATTR_FORECAST_TEMP: 5,
ATTR_FORECAST_TEMP_LOW: 2,
ATTR_FORECAST_WIND_BEARING: 56.98,
ATTR_FORECAST_WIND_SPEED: 27.922118400000002,
},
]
assert weather_state.attributes[ATTR_FRIENDLY_NAME] == "ClimaCell - Daily"
assert weather_state.attributes[ATTR_WEATHER_HUMIDITY] == 23
assert weather_state.attributes[ATTR_WEATHER_OZONE] == 46.53
assert weather_state.attributes[ATTR_WEATHER_PRESSURE] == 1027.7690615000001
assert weather_state.attributes[ATTR_WEATHER_TEMPERATURE] == 7
assert weather_state.attributes[ATTR_WEATHER_VISIBILITY] == 13.116153600000002
assert weather_state.attributes[ATTR_WEATHER_WIND_BEARING] == 315.14
assert weather_state.attributes[ATTR_WEATHER_WIND_SPEED] == 15.01517952

View File

@ -0,0 +1,992 @@
[
{
"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

@ -0,0 +1,752 @@
[
{
"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

@ -0,0 +1,782 @@
[
{
"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

@ -0,0 +1,38 @@
{
"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"
},
"observation_time": {
"value": "2021-03-07T18:54:06.055Z"
}
}

2360
tests/fixtures/climacell/v4.json vendored Normal file

File diff suppressed because it is too large Load Diff