diff --git a/CODEOWNERS b/CODEOWNERS index 606c71f0a1c7c6..ac160736202c55 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -533,6 +533,7 @@ homeassistant/components/tile/* @bachya homeassistant/components/time_date/* @fabaff homeassistant/components/tmb/* @alemuro homeassistant/components/todoist/* @boralyl +homeassistant/components/tomorrowio/* @raman325 homeassistant/components/toon/* @frenck homeassistant/components/totalconnect/* @austinmroczek homeassistant/components/tplink/* @rytilahti @thegardenmonkey diff --git a/homeassistant/components/tomorrowio/__init__.py b/homeassistant/components/tomorrowio/__init__.py new file mode 100644 index 00000000000000..3177a750a1d87d --- /dev/null +++ b/homeassistant/components/tomorrowio/__init__.py @@ -0,0 +1,268 @@ +"""The Tomorrow.io integration.""" +from __future__ import annotations + +from datetime import timedelta +import logging +from math import ceil +from typing import Any + +from pytomorrowio import TomorrowioV4 +from pytomorrowio.const import CURRENT, FORECASTS +from pytomorrowio.exceptions import ( + CantConnectException, + InvalidAPIKeyException, + RateLimitedException, + UnknownException, +) + +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.components.weather import DOMAIN as WEATHER_DOMAIN +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, + UpdateFailed, +) + +from .const import ( + ATTRIBUTION, + CONF_TIMESTEP, + DOMAIN, + MAX_REQUESTS_PER_DAY, + TMRW_ATTR_CARBON_MONOXIDE, + TMRW_ATTR_CHINA_AQI, + TMRW_ATTR_CHINA_HEALTH_CONCERN, + TMRW_ATTR_CHINA_PRIMARY_POLLUTANT, + TMRW_ATTR_CLOUD_BASE, + TMRW_ATTR_CLOUD_CEILING, + TMRW_ATTR_CLOUD_COVER, + TMRW_ATTR_CONDITION, + TMRW_ATTR_DEW_POINT, + TMRW_ATTR_EPA_AQI, + TMRW_ATTR_EPA_HEALTH_CONCERN, + TMRW_ATTR_EPA_PRIMARY_POLLUTANT, + TMRW_ATTR_FEELS_LIKE, + TMRW_ATTR_FIRE_INDEX, + TMRW_ATTR_HUMIDITY, + TMRW_ATTR_NITROGEN_DIOXIDE, + TMRW_ATTR_OZONE, + TMRW_ATTR_PARTICULATE_MATTER_10, + TMRW_ATTR_PARTICULATE_MATTER_25, + TMRW_ATTR_POLLEN_GRASS, + TMRW_ATTR_POLLEN_TREE, + TMRW_ATTR_POLLEN_WEED, + TMRW_ATTR_PRECIPITATION, + TMRW_ATTR_PRECIPITATION_PROBABILITY, + TMRW_ATTR_PRECIPITATION_TYPE, + TMRW_ATTR_PRESSURE, + TMRW_ATTR_PRESSURE_SURFACE_LEVEL, + TMRW_ATTR_SOLAR_GHI, + TMRW_ATTR_SULPHUR_DIOXIDE, + TMRW_ATTR_TEMPERATURE, + TMRW_ATTR_TEMPERATURE_HIGH, + TMRW_ATTR_TEMPERATURE_LOW, + TMRW_ATTR_VISIBILITY, + TMRW_ATTR_WIND_DIRECTION, + TMRW_ATTR_WIND_GUST, + TMRW_ATTR_WIND_SPEED, +) + +_LOGGER = logging.getLogger(__name__) + +PLATFORMS = [SENSOR_DOMAIN, WEATHER_DOMAIN] + + +def _set_update_interval(hass: HomeAssistant, current_entry: ConfigEntry) -> timedelta: + """Recalculate update_interval based on existing Tomorrow.io instances and update them.""" + api_calls = 2 + # We check how many Tomorrow.io 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 the number of API calls because 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 Tomorrow.io API from a config entry.""" + hass.data.setdefault(DOMAIN, {}) + + api = TomorrowioV4( + entry.data[CONF_API_KEY], + entry.data[CONF_LATITUDE], + entry.data[CONF_LONGITUDE], + session=async_get_clientsession(hass), + ) + + coordinator = TomorrowioDataUpdateCoordinator( + hass, + entry, + api, + _set_update_interval(hass, entry), + ) + + await coordinator.async_config_entry_first_refresh() + + hass.data[DOMAIN][entry.entry_id] = coordinator + + hass.config_entries.async_setup_platforms(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) + if not hass.data[DOMAIN]: + hass.data.pop(DOMAIN) + + return unload_ok + + +class TomorrowioDataUpdateCoordinator(DataUpdateCoordinator): + """Define an object to hold Tomorrow.io data.""" + + def __init__( + self, + hass: HomeAssistant, + config_entry: ConfigEntry, + api: TomorrowioV4, + update_interval: timedelta, + ) -> None: + """Initialize.""" + + self._config_entry = config_entry + 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.""" + try: + return await self._api.realtime_and_all_forecasts( + [ + TMRW_ATTR_TEMPERATURE, + TMRW_ATTR_HUMIDITY, + TMRW_ATTR_PRESSURE, + TMRW_ATTR_WIND_SPEED, + TMRW_ATTR_WIND_DIRECTION, + TMRW_ATTR_CONDITION, + TMRW_ATTR_VISIBILITY, + TMRW_ATTR_OZONE, + TMRW_ATTR_WIND_GUST, + TMRW_ATTR_CLOUD_COVER, + TMRW_ATTR_PRECIPITATION_TYPE, + *( + TMRW_ATTR_CARBON_MONOXIDE, + TMRW_ATTR_CHINA_AQI, + TMRW_ATTR_CHINA_HEALTH_CONCERN, + TMRW_ATTR_CHINA_PRIMARY_POLLUTANT, + TMRW_ATTR_CLOUD_BASE, + TMRW_ATTR_CLOUD_CEILING, + TMRW_ATTR_CLOUD_COVER, + TMRW_ATTR_DEW_POINT, + TMRW_ATTR_EPA_AQI, + TMRW_ATTR_EPA_HEALTH_CONCERN, + TMRW_ATTR_EPA_PRIMARY_POLLUTANT, + TMRW_ATTR_FEELS_LIKE, + TMRW_ATTR_FIRE_INDEX, + TMRW_ATTR_NITROGEN_DIOXIDE, + TMRW_ATTR_OZONE, + TMRW_ATTR_PARTICULATE_MATTER_10, + TMRW_ATTR_PARTICULATE_MATTER_25, + TMRW_ATTR_POLLEN_GRASS, + TMRW_ATTR_POLLEN_TREE, + TMRW_ATTR_POLLEN_WEED, + TMRW_ATTR_PRECIPITATION_TYPE, + TMRW_ATTR_PRESSURE_SURFACE_LEVEL, + TMRW_ATTR_SOLAR_GHI, + TMRW_ATTR_SULPHUR_DIOXIDE, + TMRW_ATTR_WIND_GUST, + ), + ], + [ + TMRW_ATTR_TEMPERATURE_LOW, + TMRW_ATTR_TEMPERATURE_HIGH, + TMRW_ATTR_WIND_SPEED, + TMRW_ATTR_WIND_DIRECTION, + TMRW_ATTR_CONDITION, + TMRW_ATTR_PRECIPITATION, + TMRW_ATTR_PRECIPITATION_PROBABILITY, + ], + nowcast_timestep=self._config_entry.options[CONF_TIMESTEP], + ) + except ( + CantConnectException, + InvalidAPIKeyException, + RateLimitedException, + UnknownException, + ) as error: + raise UpdateFailed from error + + +class TomorrowioEntity(CoordinatorEntity): + """Base Tomorrow.io Entity.""" + + def __init__( + self, + config_entry: ConfigEntry, + coordinator: TomorrowioDataUpdateCoordinator, + api_version: int, + ) -> None: + """Initialize Tomorrow.io Entity.""" + super().__init__(coordinator) + self.api_version = api_version + self._config_entry = config_entry + self._attr_device_info = { + "identifiers": {(DOMAIN, self._config_entry.data[CONF_API_KEY])}, + "name": "Tomorrow.io", + "manufacturer": "Tomorrow.io", + "sw_version": f"v{self.api_version}", + "entry_type": "service", + } + + def _get_current_property(self, property_name: str) -> int | str | float | None: + """ + Get property from current conditions. + + Used for V4 API. + """ + return self.coordinator.data.get(CURRENT, {}).get(property_name) + + @property + def attribution(self): + """Return the attribution.""" + return ATTRIBUTION diff --git a/homeassistant/components/tomorrowio/config_flow.py b/homeassistant/components/tomorrowio/config_flow.py new file mode 100644 index 00000000000000..b2d42ac27fba65 --- /dev/null +++ b/homeassistant/components/tomorrowio/config_flow.py @@ -0,0 +1,163 @@ +"""Config flow for Tomorrow.io integration.""" +from __future__ import annotations + +import logging +from typing import Any + +from pytomorrowio.exceptions import ( + CantConnectException, + InvalidAPIKeyException, + RateLimitedException, +) +from pytomorrowio.pytomorrowio import TomorrowioV4 +import voluptuous as vol + +from homeassistant import config_entries, core +from homeassistant.components.zone import async_active_zone +from homeassistant.const import ( + CONF_API_KEY, + CONF_FRIENDLY_NAME, + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_NAME, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv + +from .const import ( + CONF_TIMESTEP, + DEFAULT_NAME, + DEFAULT_TIMESTEP, + DOMAIN, + TMRW_ATTR_TEMPERATURE, +) + +_LOGGER = logging.getLogger(__name__) + + +def _get_config_schema( + hass: core.HomeAssistant, source: str | None, input_dict: dict[str, Any] = None +) -> vol.Schema: + """ + Return schema defaults for init step based on user input/config dict. + + Retain info already provided for future form views by setting them as + defaults in schema. + """ + if input_dict is None: + input_dict = {} + + api_key_schema = { + vol.Required(CONF_API_KEY, default=input_dict.get(CONF_API_KEY)): str, + } + + return vol.Schema( + { + **api_key_schema, + vol.Required( + CONF_LATITUDE, + "location", + default=input_dict.get(CONF_LATITUDE, hass.config.latitude), + ): cv.latitude, + vol.Required( + CONF_LONGITUDE, + "location", + default=input_dict.get(CONF_LONGITUDE, hass.config.longitude), + ): cv.longitude, + }, + ) + + +def _get_unique_id(hass: HomeAssistant, input_dict: dict[str, Any]): + """Return unique ID from config data.""" + return ( + f"{input_dict[CONF_API_KEY]}" + f"_{input_dict.get(CONF_LATITUDE, hass.config.latitude)}" + f"_{input_dict.get(CONF_LONGITUDE, hass.config.longitude)}" + ) + + +class TomorrowioOptionsConfigFlow(config_entries.OptionsFlow): + """Handle Tomorrow.io options.""" + + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + """Initialize Tomorrow.io options flow.""" + self._config_entry = config_entry + + async def async_step_init(self, user_input: dict[str, Any] = None) -> FlowResult: + """Manage the Tomorrow.io 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[CONF_TIMESTEP], + ): vol.In([1, 5, 15, 30]), + } + + return self.async_show_form( + step_id="init", data_schema=vol.Schema(options_schema) + ) + + +class TomorrowioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Tomorrow.io Weather API.""" + + VERSION = 1 + + @staticmethod + @callback + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> TomorrowioOptionsConfigFlow: + """Get the options flow for this handler.""" + return TomorrowioOptionsConfigFlow(config_entry) + + async def async_step_user(self, user_input: dict[str, Any] = None) -> FlowResult: + """Handle the initial step.""" + errors = {} + if user_input is not None: + await self.async_set_unique_id( + unique_id=_get_unique_id(self.hass, user_input) + ) + self._abort_if_unique_id_configured() + + latitude = user_input.get(CONF_LATITUDE, self.hass.config.latitude) + longitude = user_input.get(CONF_LONGITUDE, self.hass.config.longitude) + user_input[CONF_NAME] = DEFAULT_NAME + if zone_state := async_active_zone(self.hass, latitude, longitude): + user_input[ + CONF_NAME + ] += f" - {zone_state.attributes[CONF_FRIENDLY_NAME]}" + try: + await TomorrowioV4( + user_input[CONF_API_KEY], + str(latitude), + str(longitude), + session=async_get_clientsession(self.hass), + ).realtime([TMRW_ATTR_TEMPERATURE]) + except CantConnectException: + errors["base"] = "cannot_connect" + except InvalidAPIKeyException: + errors[CONF_API_KEY] = "invalid_api_key" + except RateLimitedException: + errors[CONF_API_KEY] = "rate_limited" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + + if not errors: + return self.async_create_entry( + title=user_input[CONF_NAME], + data=user_input, + options={CONF_TIMESTEP: DEFAULT_TIMESTEP}, + ) + + return self.async_show_form( + step_id="user", + data_schema=_get_config_schema(self.hass, self.source, user_input), + errors=errors, + ) diff --git a/homeassistant/components/tomorrowio/const.py b/homeassistant/components/tomorrowio/const.py new file mode 100644 index 00000000000000..c94e56707a2b61 --- /dev/null +++ b/homeassistant/components/tomorrowio/const.py @@ -0,0 +1,113 @@ +"""Constants for the Tomorrow.io integration.""" +from __future__ import annotations + +from pytomorrowio.const import DAILY, HOURLY, NOWCAST, WeatherCode + +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, +) + +CONF_TIMESTEP = "timestep" +FORECAST_TYPES = [DAILY, HOURLY, NOWCAST] + +DEFAULT_TIMESTEP = 15 +DEFAULT_FORECAST_TYPE = DAILY +DOMAIN = "tomorrowio" +INTEGRATION_NAME = "Tomorrow.io" +DEFAULT_NAME = INTEGRATION_NAME +ATTRIBUTION = "Powered by Tomorrow.io" + +MAX_REQUESTS_PER_DAY = 500 + +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" + +# 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, +} + +# Weather constants +TMRW_ATTR_TIMESTAMP = "startTime" +TMRW_ATTR_TEMPERATURE = "temperature" +TMRW_ATTR_TEMPERATURE_HIGH = "temperatureMax" +TMRW_ATTR_TEMPERATURE_LOW = "temperatureMin" +TMRW_ATTR_PRESSURE = "pressureSeaLevel" +TMRW_ATTR_HUMIDITY = "humidity" +TMRW_ATTR_WIND_SPEED = "windSpeed" +TMRW_ATTR_WIND_DIRECTION = "windDirection" +TMRW_ATTR_OZONE = "pollutantO3" +TMRW_ATTR_CONDITION = "weatherCode" +TMRW_ATTR_VISIBILITY = "visibility" +TMRW_ATTR_PRECIPITATION = "precipitationIntensityAvg" +TMRW_ATTR_PRECIPITATION_PROBABILITY = "precipitationProbability" +TMRW_ATTR_WIND_GUST = "windGust" +TMRW_ATTR_CLOUD_COVER = "cloudCover" +TMRW_ATTR_PRECIPITATION_TYPE = "precipitationType" + +# Sensor attributes +TMRW_ATTR_PARTICULATE_MATTER_25 = "particulateMatter25" +TMRW_ATTR_PARTICULATE_MATTER_10 = "particulateMatter10" +TMRW_ATTR_NITROGEN_DIOXIDE = "pollutantNO2" +TMRW_ATTR_CARBON_MONOXIDE = "pollutantCO" +TMRW_ATTR_SULPHUR_DIOXIDE = "pollutantSO2" +TMRW_ATTR_EPA_AQI = "epaIndex" +TMRW_ATTR_EPA_PRIMARY_POLLUTANT = "epaPrimaryPollutant" +TMRW_ATTR_EPA_HEALTH_CONCERN = "epaHealthConcern" +TMRW_ATTR_CHINA_AQI = "mepIndex" +TMRW_ATTR_CHINA_PRIMARY_POLLUTANT = "mepPrimaryPollutant" +TMRW_ATTR_CHINA_HEALTH_CONCERN = "mepHealthConcern" +TMRW_ATTR_POLLEN_TREE = "treeIndex" +TMRW_ATTR_POLLEN_WEED = "weedIndex" +TMRW_ATTR_POLLEN_GRASS = "grassIndex" +TMRW_ATTR_FIRE_INDEX = "fireIndex" +TMRW_ATTR_FEELS_LIKE = "temperatureApparent" +TMRW_ATTR_DEW_POINT = "dewPoint" +TMRW_ATTR_PRESSURE_SURFACE_LEVEL = "pressureSurfaceLevel" +TMRW_ATTR_SOLAR_GHI = "solarGHI" +TMRW_ATTR_CLOUD_BASE = "cloudBase" +TMRW_ATTR_CLOUD_CEILING = "cloudCeiling" diff --git a/homeassistant/components/tomorrowio/manifest.json b/homeassistant/components/tomorrowio/manifest.json new file mode 100644 index 00000000000000..6fc8a2f12ce5b3 --- /dev/null +++ b/homeassistant/components/tomorrowio/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "tomorrowio", + "name": "Tomorrow.io", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/tomorrowio", + "requirements": ["pytomorrowio==0.1.0"], + "codeowners": ["@raman325"], + "iot_class": "cloud_polling" +} diff --git a/homeassistant/components/tomorrowio/sensor.py b/homeassistant/components/tomorrowio/sensor.py new file mode 100644 index 00000000000000..42808674f148f5 --- /dev/null +++ b/homeassistant/components/tomorrowio/sensor.py @@ -0,0 +1,359 @@ +"""Sensor component that handles additional Tomorrowio data for your location.""" +from __future__ import annotations + +from abc import abstractmethod +from dataclasses import dataclass +import logging +from typing import Any, Callable + +from pytomorrowio.const import ( + HealthConcernType, + PollenIndex, + PrecipitationType, + PrimaryPollutantType, +) + +from homeassistant.components.sensor import SensorEntity, SensorEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + ATTR_ATTRIBUTION, + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + CONCENTRATION_PARTS_PER_MILLION, + CONF_NAME, + DEVICE_CLASS_AQI, + DEVICE_CLASS_CO, + DEVICE_CLASS_NITROGEN_DIOXIDE, + DEVICE_CLASS_OZONE, + DEVICE_CLASS_PM10, + DEVICE_CLASS_PM25, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_SULPHUR_DIOXIDE, + DEVICE_CLASS_TEMPERATURE, + IRRADIATION_BTUS_PER_HOUR_SQUARE_FOOT, + IRRADIATION_WATTS_PER_SQUARE_METER, + LENGTH_KILOMETERS, + LENGTH_METERS, + LENGTH_MILES, + PERCENTAGE, + PRESSURE_HPA, + PRESSURE_INHG, + SPEED_METERS_PER_SECOND, + SPEED_MILES_PER_HOUR, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.util import slugify +from homeassistant.util.distance import convert as distance_convert +from homeassistant.util.pressure import convert as pressure_convert +from homeassistant.util.temperature import convert as temp_convert + +from . import TomorrowioDataUpdateCoordinator, TomorrowioEntity +from .const import ( + DOMAIN, + TMRW_ATTR_CARBON_MONOXIDE, + TMRW_ATTR_CHINA_AQI, + TMRW_ATTR_CHINA_HEALTH_CONCERN, + TMRW_ATTR_CHINA_PRIMARY_POLLUTANT, + TMRW_ATTR_CLOUD_BASE, + TMRW_ATTR_CLOUD_CEILING, + TMRW_ATTR_CLOUD_COVER, + TMRW_ATTR_DEW_POINT, + TMRW_ATTR_EPA_AQI, + TMRW_ATTR_EPA_HEALTH_CONCERN, + TMRW_ATTR_EPA_PRIMARY_POLLUTANT, + TMRW_ATTR_FEELS_LIKE, + TMRW_ATTR_FIRE_INDEX, + TMRW_ATTR_NITROGEN_DIOXIDE, + TMRW_ATTR_OZONE, + TMRW_ATTR_PARTICULATE_MATTER_10, + TMRW_ATTR_PARTICULATE_MATTER_25, + TMRW_ATTR_POLLEN_GRASS, + TMRW_ATTR_POLLEN_TREE, + TMRW_ATTR_POLLEN_WEED, + TMRW_ATTR_PRECIPITATION_TYPE, + TMRW_ATTR_PRESSURE_SURFACE_LEVEL, + TMRW_ATTR_SOLAR_GHI, + TMRW_ATTR_SULPHUR_DIOXIDE, + TMRW_ATTR_WIND_GUST, +) + +_LOGGER = logging.getLogger(__name__) + + +@dataclass +class TomorrowioSensorEntityDescription(SensorEntityDescription): + """Describes a Tomorrow.io 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 + value_map: Any | None = None + + +SENSOR_TYPES = ( + TomorrowioSensorEntityDescription( + key=TMRW_ATTR_FEELS_LIKE, + name="Feels Like", + unit_imperial=TEMP_FAHRENHEIT, + unit_metric=TEMP_CELSIUS, + metric_conversion=lambda val: temp_convert(val, TEMP_FAHRENHEIT, TEMP_CELSIUS), + is_metric_check=True, + device_class=DEVICE_CLASS_TEMPERATURE, + ), + TomorrowioSensorEntityDescription( + key=TMRW_ATTR_DEW_POINT, + name="Dew Point", + unit_imperial=TEMP_FAHRENHEIT, + unit_metric=TEMP_CELSIUS, + metric_conversion=lambda val: temp_convert(val, TEMP_FAHRENHEIT, TEMP_CELSIUS), + is_metric_check=True, + device_class=DEVICE_CLASS_TEMPERATURE, + ), + TomorrowioSensorEntityDescription( + key=TMRW_ATTR_PRESSURE_SURFACE_LEVEL, + name="Pressure (Surface Level)", + unit_metric=PRESSURE_HPA, + metric_conversion=lambda val: pressure_convert( + val, PRESSURE_INHG, PRESSURE_HPA + ), + is_metric_check=True, + device_class=DEVICE_CLASS_PRESSURE, + ), + TomorrowioSensorEntityDescription( + key=TMRW_ATTR_SOLAR_GHI, + name="Global Horizontal Irradiance", + unit_imperial=IRRADIATION_BTUS_PER_HOUR_SQUARE_FOOT, + unit_metric=IRRADIATION_WATTS_PER_SQUARE_METER, + metric_conversion=3.15459, + is_metric_check=True, + ), + TomorrowioSensorEntityDescription( + key=TMRW_ATTR_CLOUD_BASE, + name="Cloud Base", + unit_imperial=LENGTH_MILES, + unit_metric=LENGTH_KILOMETERS, + metric_conversion=lambda val: distance_convert( + val, LENGTH_MILES, LENGTH_KILOMETERS + ), + is_metric_check=True, + ), + TomorrowioSensorEntityDescription( + key=TMRW_ATTR_CLOUD_CEILING, + name="Cloud Ceiling", + unit_imperial=LENGTH_MILES, + unit_metric=LENGTH_KILOMETERS, + metric_conversion=lambda val: distance_convert( + val, LENGTH_MILES, LENGTH_KILOMETERS + ), + is_metric_check=True, + ), + TomorrowioSensorEntityDescription( + key=TMRW_ATTR_CLOUD_COVER, + name="Cloud Cover", + unit_imperial=PERCENTAGE, + unit_metric=PERCENTAGE, + ), + TomorrowioSensorEntityDescription( + key=TMRW_ATTR_WIND_GUST, + name="Wind Gust", + unit_imperial=SPEED_MILES_PER_HOUR, + unit_metric=SPEED_METERS_PER_SECOND, + metric_conversion=lambda val: distance_convert(val, LENGTH_MILES, LENGTH_METERS) + / 3600, + is_metric_check=True, + ), + TomorrowioSensorEntityDescription( + key=TMRW_ATTR_PRECIPITATION_TYPE, + name="Precipitation Type", + value_map=PrecipitationType, + ), + TomorrowioSensorEntityDescription( + key=TMRW_ATTR_OZONE, + name="Ozone", + unit_metric=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + metric_conversion=2.03, + is_metric_check=True, + device_class=DEVICE_CLASS_OZONE, + ), + TomorrowioSensorEntityDescription( + key=TMRW_ATTR_PARTICULATE_MATTER_25, + name="Particulate Matter < 2.5 μm", + unit_metric=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + metric_conversion=3.2808399 ** 3, + is_metric_check=True, + device_class=DEVICE_CLASS_PM25, + ), + TomorrowioSensorEntityDescription( + key=TMRW_ATTR_PARTICULATE_MATTER_10, + name="Particulate Matter < 10 μm", + unit_metric=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + metric_conversion=3.2808399 ** 3, + is_metric_check=True, + device_class=DEVICE_CLASS_PM10, + ), + TomorrowioSensorEntityDescription( + key=TMRW_ATTR_NITROGEN_DIOXIDE, + name="Nitrogen Dioxide", + unit_metric=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + metric_conversion=1.95, + is_metric_check=True, + device_class=DEVICE_CLASS_NITROGEN_DIOXIDE, + ), + TomorrowioSensorEntityDescription( + key=TMRW_ATTR_CARBON_MONOXIDE, + name="Carbon Monoxide", + unit_imperial=CONCENTRATION_PARTS_PER_MILLION, + unit_metric=CONCENTRATION_PARTS_PER_MILLION, + device_class=DEVICE_CLASS_CO, + ), + TomorrowioSensorEntityDescription( + key=TMRW_ATTR_SULPHUR_DIOXIDE, + name="Sulphur Dioxide", + unit_metric=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + metric_conversion=2.71, + is_metric_check=True, + device_class=DEVICE_CLASS_SULPHUR_DIOXIDE, + ), + TomorrowioSensorEntityDescription( + key=TMRW_ATTR_EPA_AQI, + name="US EPA Air Quality Index", + device_class=DEVICE_CLASS_AQI, + ), + TomorrowioSensorEntityDescription( + key=TMRW_ATTR_EPA_PRIMARY_POLLUTANT, + name="US EPA Primary Pollutant", + value_map=PrimaryPollutantType, + ), + TomorrowioSensorEntityDescription( + key=TMRW_ATTR_EPA_HEALTH_CONCERN, + name="US EPA Health Concern", + value_map=HealthConcernType, + ), + TomorrowioSensorEntityDescription( + key=TMRW_ATTR_CHINA_AQI, + name="China MEP Air Quality Index", + device_class=DEVICE_CLASS_AQI, + ), + TomorrowioSensorEntityDescription( + key=TMRW_ATTR_CHINA_PRIMARY_POLLUTANT, + name="China MEP Primary Pollutant", + value_map=PrimaryPollutantType, + ), + TomorrowioSensorEntityDescription( + key=TMRW_ATTR_CHINA_HEALTH_CONCERN, + name="China MEP Health Concern", + value_map=HealthConcernType, + ), + TomorrowioSensorEntityDescription( + key=TMRW_ATTR_POLLEN_TREE, + name="Tree Pollen Index", + value_map=PollenIndex, + ), + TomorrowioSensorEntityDescription( + key=TMRW_ATTR_POLLEN_WEED, + name="Weed Pollen Index", + value_map=PollenIndex, + ), + TomorrowioSensorEntityDescription( + key=TMRW_ATTR_POLLEN_GRASS, + name="Grass Pollen Index", + value_map=PollenIndex, + ), + TomorrowioSensorEntityDescription( + TMRW_ATTR_FIRE_INDEX, + name="Fire Index", + ), +) + + +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] + entities = [ + TomorrowioSensorEntity(hass, config_entry, coordinator, 4, description) + for description in SENSOR_TYPES + ] + async_add_entities(entities) + + +class BaseTomorrowioSensorEntity(TomorrowioEntity, SensorEntity): + """Base Tomorrow.io sensor entity.""" + + entity_description: TomorrowioSensorEntityDescription + _attr_entity_registry_enabled_default = False + + def __init__( + self, + hass: HomeAssistant, + config_entry: ConfigEntry, + coordinator: TomorrowioDataUpdateCoordinator, + api_version: int, + description: TomorrowioSensorEntityDescription, + ) -> None: + """Initialize Tomorrow.io Sensor Entity.""" + super().__init__(config_entry, coordinator, api_version) + self.entity_description = description + 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} + # Fallback to metric always in case imperial isn't defined (for metric only + # sensors) + self._attr_native_unit_of_measurement = ( + description.unit_metric + if hass.config.units.is_metric + else description.unit_imperial + ) or description.unit_metric + + @property + @abstractmethod + def _state(self) -> str | int | float | None: + """Return the raw state.""" + + @property + def native_value(self) -> str | int | float | None: + """Return the state.""" + state = self._state + + # If an imperial unit isn't provided, we always want to convert to metric since + # that is what the UI expects + if state 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 + ) + or ( + self.entity_description.unit_imperial is None + and self.entity_description.unit_metric is not None + ) + ): + 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(float(state)), 2) + + return round(float(state) * conversion, 2) + + if self.entity_description.value_map is not None and state is not None: + return self.entity_description.value_map(state).name.lower() + + return state + + +class TomorrowioSensorEntity(BaseTomorrowioSensorEntity): + """Sensor entity that talks to Tomorrow.io v4 API to retrieve non-weather data.""" + + @property + def _state(self) -> str | int | float | None: + """Return the raw state.""" + return self._get_current_property(self.entity_description.key) diff --git a/homeassistant/components/tomorrowio/strings.json b/homeassistant/components/tomorrowio/strings.json new file mode 100644 index 00000000000000..b681dc4d043f44 --- /dev/null +++ b/homeassistant/components/tomorrowio/strings.json @@ -0,0 +1,32 @@ +{ + "config": { + "step": { + "user": { + "description": "To get an API key, sign up at [Tomorrow.io](https://app.tomorrow.io/signup).", + "data": { + "name": "[%key:common::config_flow::data::name%]", + "api_key": "[%key:common::config_flow::data::api_key%]", + "latitude": "[%key:common::config_flow::data::latitude%]", + "longitude": "[%key:common::config_flow::data::longitude%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]", + "unknown": "[%key:common::config_flow::error::unknown%]", + "rate_limited": "Currently rate limited, please try again later." + } + }, + "options": { + "step": { + "init": { + "title": "Update Tomorrow.io 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" + } + } + } + } +} diff --git a/homeassistant/components/tomorrowio/translations/en.json b/homeassistant/components/tomorrowio/translations/en.json new file mode 100644 index 00000000000000..7c653b005747f2 --- /dev/null +++ b/homeassistant/components/tomorrowio/translations/en.json @@ -0,0 +1,32 @@ +{ + "config": { + "error": { + "cannot_connect": "Failed to connect", + "invalid_api_key": "Invalid API key", + "rate_limited": "Currently rate limited, please try again later.", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "api_key": "API Key", + "latitude": "Latitude", + "longitude": "Longitude", + "name": "Name" + }, + "description": "To get an API key, sign up at [Tomorrow.io](https://app.tomorrow.io/signup)." + } + } + }, + "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 Tomorrow.io Options" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tomorrowio/weather.py b/homeassistant/components/tomorrowio/weather.py new file mode 100644 index 00000000000000..5d95b98e1096e9 --- /dev/null +++ b/homeassistant/components/tomorrowio/weather.py @@ -0,0 +1,321 @@ +"""Weather component that handles meteorological data for your location.""" +from __future__ import annotations + +from abc import abstractmethod +from datetime import datetime +import logging +from typing import Any + +from pytomorrowio.const import DAILY, FORECASTS, HOURLY, NOWCAST, WeatherCode + +from homeassistant.components.weather import ( + 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, + WeatherEntity, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_NAME, + LENGTH_FEET, + LENGTH_KILOMETERS, + LENGTH_METERS, + LENGTH_MILES, + PRESSURE_HPA, + PRESSURE_INHG, + 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.distance import convert as distance_convert +from homeassistant.util.pressure import convert as pressure_convert + +from . import TomorrowioDataUpdateCoordinator, TomorrowioEntity +from .const import ( + CLEAR_CONDITIONS, + CONDITIONS, + CONF_TIMESTEP, + DEFAULT_FORECAST_TYPE, + DOMAIN, + MAX_FORECASTS, + TMRW_ATTR_CONDITION, + TMRW_ATTR_HUMIDITY, + TMRW_ATTR_OZONE, + TMRW_ATTR_PRECIPITATION, + TMRW_ATTR_PRECIPITATION_PROBABILITY, + TMRW_ATTR_PRESSURE, + TMRW_ATTR_TEMPERATURE, + TMRW_ATTR_TEMPERATURE_HIGH, + TMRW_ATTR_TEMPERATURE_LOW, + TMRW_ATTR_TIMESTAMP, + TMRW_ATTR_VISIBILITY, + TMRW_ATTR_WIND_DIRECTION, + TMRW_ATTR_WIND_SPEED, +) + +_LOGGER = logging.getLogger(__name__) + + +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] + + entities = [ + TomorrowioWeatherEntity(config_entry, coordinator, 4, forecast_type) + for forecast_type in (DAILY, HOURLY, NOWCAST) + ] + async_add_entities(entities) + + +class BaseTomorrowioWeatherEntity(TomorrowioEntity, WeatherEntity): + """Base Tomorrow.io weather entity.""" + + def __init__( + self, + config_entry: ConfigEntry, + coordinator: TomorrowioDataUpdateCoordinator, + api_version: int, + forecast_type: str, + ) -> None: + """Initialize Tomorrow.io 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: int | None, sun_is_up: bool = True + ) -> str | None: + """Translate Tomorrow.io condition into an HA condition.""" + + def _forecast_dict( + self, + forecast_dt: datetime, + use_datetime: bool, + condition: int, + 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 Tomorrow.io 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) + + if self.hass.config.units.is_metric: + if precipitation: + precipitation = round( + distance_convert(precipitation / 12, LENGTH_FEET, LENGTH_METERS) + * 1000, + 4, + ) + if wind_speed: + wind_speed = round( + distance_convert(wind_speed, LENGTH_MILES, LENGTH_KILOMETERS), 4 + ) + + 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} + + @property + @abstractmethod + def _pressure(self): + """Return the raw pressure.""" + + @property + def pressure(self): + """Return the pressure.""" + if self.hass.config.units.is_metric and self._pressure: + return round( + pressure_convert(self._pressure, PRESSURE_INHG, PRESSURE_HPA), 4 + ) + return self._pressure + + @property + @abstractmethod + def _wind_speed(self): + """Return the raw wind speed.""" + + @property + def wind_speed(self): + """Return the wind speed.""" + if self.hass.config.units.is_metric and self._wind_speed: + return round( + distance_convert(self._wind_speed, LENGTH_MILES, LENGTH_KILOMETERS), 4 + ) + return self._wind_speed + + @property + @abstractmethod + def _visibility(self): + """Return the raw visibility.""" + + @property + def visibility(self): + """Return the visibility.""" + if self.hass.config.units.is_metric and self._visibility: + return round( + distance_convert(self._visibility, LENGTH_MILES, LENGTH_KILOMETERS), 4 + ) + return self._visibility + + +class TomorrowioWeatherEntity(BaseTomorrowioWeatherEntity): + """Entity that talks to Tomorrow.io v4 API to retrieve weather data.""" + + _attr_temperature_unit = TEMP_FAHRENHEIT + + @staticmethod + def _translate_condition( + condition: int | None, sun_is_up: bool = True + ) -> str | None: + """Translate Tomorrow.io 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] + + @property + def temperature(self): + """Return the platform temperature.""" + return self._get_current_property(TMRW_ATTR_TEMPERATURE) + + @property + def _pressure(self): + """Return the raw pressure.""" + return self._get_current_property(TMRW_ATTR_PRESSURE) + + @property + def humidity(self): + """Return the humidity.""" + return self._get_current_property(TMRW_ATTR_HUMIDITY) + + @property + def _wind_speed(self): + """Return the raw wind speed.""" + return self._get_current_property(TMRW_ATTR_WIND_SPEED) + + @property + def wind_bearing(self): + """Return the wind bearing.""" + return self._get_current_property(TMRW_ATTR_WIND_DIRECTION) + + @property + def ozone(self): + """Return the O3 (ozone) level.""" + return self._get_current_property(TMRW_ATTR_OZONE) + + @property + def condition(self): + """Return the condition.""" + return self._translate_condition( + self._get_current_property(TMRW_ATTR_CONDITION), + is_up(self.hass), + ) + + @property + def _visibility(self): + """Return the raw visibility.""" + return self._get_current_property(TMRW_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 = [] + 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[TMRW_ATTR_TIMESTAMP]) + + # Throw out past data + if forecast_dt.date() < dt_util.utcnow().date(): + continue + + values = forecast["values"] + use_datetime = True + + condition = values.get(TMRW_ATTR_CONDITION) + precipitation = values.get(TMRW_ATTR_PRECIPITATION) + precipitation_probability = values.get(TMRW_ATTR_PRECIPITATION_PROBABILITY) + + temp = values.get(TMRW_ATTR_TEMPERATURE_HIGH) + temp_low = values.get(TMRW_ATTR_TEMPERATURE_LOW) + wind_direction = values.get(TMRW_ATTR_WIND_DIRECTION) + wind_speed = values.get(TMRW_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 diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 2a04ec394780c6..8f7497c652e0f8 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -284,6 +284,7 @@ "tellduslive", "tibber", "tile", + "tomorrowio", "toon", "totalconnect", "tplink", diff --git a/requirements_all.txt b/requirements_all.txt index 363618c3e99fc5..6f526df24a6580 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1963,6 +1963,9 @@ pythonegardia==1.0.40 # homeassistant.components.tile pytile==5.2.3 +# homeassistant.components.tomorrowio +pytomorrowio==0.1.0 + # homeassistant.components.touchline pytouchline==0.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ab801c5f083c68..c4b0f984ba038d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1132,6 +1132,9 @@ python_awair==0.2.1 # homeassistant.components.tile pytile==5.2.3 +# homeassistant.components.tomorrowio +pytomorrowio==0.1.0 + # homeassistant.components.traccar pytraccar==0.9.0 diff --git a/tests/components/tomorrowio/__init__.py b/tests/components/tomorrowio/__init__.py new file mode 100644 index 00000000000000..de0ec5d135a037 --- /dev/null +++ b/tests/components/tomorrowio/__init__.py @@ -0,0 +1 @@ +"""Tests for the Tomorrow.io Weather API integration.""" diff --git a/tests/components/tomorrowio/conftest.py b/tests/components/tomorrowio/conftest.py new file mode 100644 index 00000000000000..433a532ee9a501 --- /dev/null +++ b/tests/components/tomorrowio/conftest.py @@ -0,0 +1,27 @@ +"""Configure py.test.""" +import json +from unittest.mock import patch + +import pytest + +from tests.common import load_fixture + + +@pytest.fixture(name="tomorrowio_config_flow_connect", autouse=True) +def tomorrowio_config_flow_connect(): + """Mock valid tomorrowio config flow setup.""" + with patch( + "homeassistant.components.tomorrowio.config_flow.TomorrowioV4.realtime", + return_value={}, + ): + yield + + +@pytest.fixture(name="tomorrowio_config_entry_update") +def tomorrowio_config_entry_update_fixture(): + """Mock valid tomorrowio config entry setup.""" + with patch( + "homeassistant.components.tomorrowio.TomorrowioV4.realtime_and_all_forecasts", + return_value=json.loads(load_fixture("tomorrowio/v4.json")), + ): + yield diff --git a/tests/components/tomorrowio/const.py b/tests/components/tomorrowio/const.py new file mode 100644 index 00000000000000..8cb00c6707ee15 --- /dev/null +++ b/tests/components/tomorrowio/const.py @@ -0,0 +1,21 @@ +"""Constants for tomorrowio tests.""" + +from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE + +API_KEY = "aa" + +MIN_CONFIG = { + CONF_API_KEY: API_KEY, +} + +V1_ENTRY_DATA = { + CONF_API_KEY: API_KEY, + CONF_LATITUDE: 80, + CONF_LONGITUDE: 80, +} + +API_V4_ENTRY_DATA = { + CONF_API_KEY: API_KEY, + CONF_LATITUDE: 80, + CONF_LONGITUDE: 80, +} diff --git a/tests/components/tomorrowio/test_config_flow.py b/tests/components/tomorrowio/test_config_flow.py new file mode 100644 index 00000000000000..51251532e84d0a --- /dev/null +++ b/tests/components/tomorrowio/test_config_flow.py @@ -0,0 +1,171 @@ +"""Test the Tomorrow.io config flow.""" +import logging +from unittest.mock import patch + +from pytomorrowio.exceptions import ( + CantConnectException, + InvalidAPIKeyException, + RateLimitedException, + UnknownException, +) + +from homeassistant import data_entry_flow +from homeassistant.components.tomorrowio.config_flow import ( + _get_config_schema, + _get_unique_id, +) +from homeassistant.components.tomorrowio.const import ( + CONF_TIMESTEP, + DEFAULT_NAME, + DEFAULT_TIMESTEP, + DOMAIN, +) +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME +from homeassistant.core import HomeAssistant + +from .const import API_KEY, MIN_CONFIG + +from tests.common import MockConfigEntry + +_LOGGER = logging.getLogger(__name__) + + +async def test_user_flow_minimum_fields(hass: HomeAssistant) -> None: + """Test user config flow with minimum fields.""" + 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" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=_get_config_schema(hass, SOURCE_USER, MIN_CONFIG)(MIN_CONFIG), + ) + + 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_LATITUDE] == hass.config.latitude + assert result["data"][CONF_LONGITUDE] == hass.config.longitude + + +async def test_user_flow_same_unique_ids(hass: HomeAssistant) -> None: + """Test user config flow with the same unique ID as an existing entry.""" + user_input = _get_config_schema(hass, SOURCE_USER, MIN_CONFIG)(MIN_CONFIG) + MockConfigEntry( + domain=DOMAIN, + data=user_input, + options={CONF_TIMESTEP: DEFAULT_TIMESTEP}, + 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( + DOMAIN, + context={"source": SOURCE_USER}, + data=user_input, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_user_flow_cannot_connect(hass: HomeAssistant) -> None: + """Test user config flow when Tomorrow.io can't connect.""" + with patch( + "homeassistant.components.tomorrowio.config_flow.TomorrowioV4.realtime", + side_effect=CantConnectException, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data=_get_config_schema(hass, SOURCE_USER, MIN_CONFIG)(MIN_CONFIG), + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "cannot_connect"} + + +async def test_user_flow_invalid_api(hass: HomeAssistant) -> None: + """Test user config flow when API key is invalid.""" + with patch( + "homeassistant.components.tomorrowio.config_flow.TomorrowioV4.realtime", + side_effect=InvalidAPIKeyException, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data=_get_config_schema(hass, SOURCE_USER, MIN_CONFIG)(MIN_CONFIG), + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {CONF_API_KEY: "invalid_api_key"} + + +async def test_user_flow_rate_limited(hass: HomeAssistant) -> None: + """Test user config flow when API key is rate limited.""" + with patch( + "homeassistant.components.tomorrowio.config_flow.TomorrowioV4.realtime", + side_effect=RateLimitedException, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data=_get_config_schema(hass, SOURCE_USER, MIN_CONFIG)(MIN_CONFIG), + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {CONF_API_KEY: "rate_limited"} + + +async def test_user_flow_unknown_exception(hass: HomeAssistant) -> None: + """Test user config flow when unknown error occurs.""" + with patch( + "homeassistant.components.tomorrowio.config_flow.TomorrowioV4.realtime", + side_effect=UnknownException, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data=_get_config_schema(hass, SOURCE_USER, MIN_CONFIG)(MIN_CONFIG), + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "unknown"} + + +async def test_options_flow(hass: HomeAssistant) -> None: + """Test options config flow for tomorrowio.""" + user_config = _get_config_schema(hass, SOURCE_USER)(MIN_CONFIG) + entry = MockConfigEntry( + domain=DOMAIN, + data=user_config, + options={CONF_TIMESTEP: DEFAULT_TIMESTEP}, + source=SOURCE_USER, + unique_id=_get_unique_id(hass, user_config), + 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.RESULT_TYPE_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.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "" + assert result["data"][CONF_TIMESTEP] == 1 + assert entry.options[CONF_TIMESTEP] == 1 diff --git a/tests/components/tomorrowio/test_init.py b/tests/components/tomorrowio/test_init.py new file mode 100644 index 00000000000000..95b09eaaf7d02e --- /dev/null +++ b/tests/components/tomorrowio/test_init.py @@ -0,0 +1,45 @@ +"""Tests for Tomorrow.io init.""" +import pytest + +from homeassistant.components.tomorrowio.config_flow import ( + _get_config_schema, + _get_unique_id, +) +from homeassistant.components.tomorrowio.const import ( + CONF_TIMESTEP, + DEFAULT_NAME, + DEFAULT_TIMESTEP, + DOMAIN, +) +from homeassistant.components.weather import DOMAIN as WEATHER_DOMAIN +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import CONF_NAME +from homeassistant.core import HomeAssistant + +from .const import MIN_CONFIG + +from tests.common import MockConfigEntry + + +async def test_load_and_unload( + hass: HomeAssistant, + tomorrowio_config_entry_update: pytest.fixture, +) -> None: + """Test loading and unloading entry.""" + data = _get_config_schema(hass, SOURCE_USER)(MIN_CONFIG) + data[CONF_NAME] = DEFAULT_NAME + config_entry = MockConfigEntry( + domain=DOMAIN, + data=data, + options={CONF_TIMESTEP: DEFAULT_TIMESTEP}, + 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 diff --git a/tests/components/tomorrowio/test_sensor.py b/tests/components/tomorrowio/test_sensor.py new file mode 100644 index 00000000000000..6f74f001b9e8a0 --- /dev/null +++ b/tests/components/tomorrowio/test_sensor.py @@ -0,0 +1,171 @@ +"""Tests for Tomorrow.io sensor entities.""" +from __future__ import annotations + +from datetime import datetime +from typing import Any +from unittest.mock import patch + +import pytest + +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.components.tomorrowio.config_flow import ( + _get_config_schema, + _get_unique_id, +) +from homeassistant.components.tomorrowio.const import ( + ATTRIBUTION, + CONF_TIMESTEP, + DEFAULT_NAME, + DEFAULT_TIMESTEP, + DOMAIN, +) +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import ATTR_ATTRIBUTION, CONF_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_V4_ENTRY_DATA + +from tests.common import MockConfigEntry + +CC_SENSOR_ENTITY_ID = "sensor.tomorrow_io_{}" + +O3 = "ozone" +CO = "carbon_monoxide" +NO2 = "nitrogen_dioxide" +SO2 = "sulphur_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), + ): + data = _get_config_schema(hass, SOURCE_USER)(config) + data[CONF_NAME] = DEFAULT_NAME + config_entry = MockConfigEntry( + domain=DOMAIN, + data=data, + options={CONF_TIMESTEP: DEFAULT_TIMESTEP}, + unique_id=_get_unique_id(hass, data), + version=1, + ) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + for entity_name in 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 Tomorrow.io 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_v4_sensor( + hass: HomeAssistant, + tomorrowio_config_entry_update: pytest.fixture, +) -> None: + """Test v4 sensor data.""" + await _setup(hass, V4_FIELDS, API_V4_ENTRY_DATA) + check_sensor_state(hass, O3, "94.46") + check_sensor_state(hass, CO, "0.63") + check_sensor_state(hass, NO2, "20.81") + check_sensor_state(hass, SO2, "4.47") + check_sensor_state(hass, PM25, "5.3") + check_sensor_state(hass, PM10, "20.13") + check_sensor_state(hass, MEP_AQI, "23") + check_sensor_state(hass, MEP_HEALTH_CONCERN, "good") + check_sensor_state(hass, MEP_PRIMARY_POLLUTANT, "pm10") + check_sensor_state(hass, EPA_AQI, "24") + check_sensor_state(hass, EPA_HEALTH_CONCERN, "good") + check_sensor_state(hass, EPA_PRIMARY_POLLUTANT, "pm25") + check_sensor_state(hass, FIRE_INDEX, "10") + check_sensor_state(hass, GRASS_POLLEN, "none") + check_sensor_state(hass, WEED_POLLEN, "none") + check_sensor_state(hass, TREE_POLLEN, "none") + check_sensor_state(hass, FEELS_LIKE, "38.5") + check_sensor_state(hass, DEW_POINT, "22.68") + check_sensor_state(hass, PRESSURE_SURFACE_LEVEL, "997.97") + check_sensor_state(hass, GHI, "0.0") + check_sensor_state(hass, CLOUD_BASE, "1.19") + check_sensor_state(hass, CLOUD_COVER, "100") + check_sensor_state(hass, CLOUD_CEILING, "1.19") + check_sensor_state(hass, WIND_GUST, "5.65") + check_sensor_state(hass, PRECIPITATION_TYPE, "rain") diff --git a/tests/components/tomorrowio/test_weather.py b/tests/components/tomorrowio/test_weather.py new file mode 100644 index 00000000000000..19031a7dc49894 --- /dev/null +++ b/tests/components/tomorrowio/test_weather.py @@ -0,0 +1,250 @@ +"""Tests for Tomorrow.io weather entity.""" +from __future__ import annotations + +from datetime import datetime +import logging +from typing import Any +from unittest.mock import patch + +import pytest + +from homeassistant.components.tomorrowio.config_flow import ( + _get_config_schema, + _get_unique_id, +) +from homeassistant.components.tomorrowio.const import ( + ATTRIBUTION, + CONF_TIMESTEP, + DEFAULT_NAME, + DEFAULT_TIMESTEP, + DOMAIN, +) +from homeassistant.components.weather import ( + 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.config_entries import SOURCE_USER +from homeassistant.const import ATTR_ATTRIBUTION, ATTR_FRIENDLY_NAME, CONF_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_V4_ENTRY_DATA + +from tests.common import MockConfigEntry + +_LOGGER = logging.getLogger(__name__) + + +@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), + ): + data = _get_config_schema(hass, SOURCE_USER)(config) + data[CONF_NAME] = DEFAULT_NAME + config_entry = MockConfigEntry( + domain=DOMAIN, + data=data, + options={CONF_TIMESTEP: DEFAULT_TIMESTEP}, + unique_id=_get_unique_id(hass, data), + version=1, + ) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + for entity_name in ("hourly", "nowcast"): + _enable_entity(hass, f"weather.tomorrow_io_{entity_name}") + await hass.async_block_till_done() + assert len(hass.states.async_entity_ids(WEATHER_DOMAIN)) == 3 + + return hass.states.get("weather.tomorrow_io_daily") + + +async def test_v4_weather( + hass: HomeAssistant, + tomorrowio_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.2727, + }, + { + 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.6517, + }, + { + 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.3459, + }, + { + 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.1234, + }, + { + 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.2506, + }, + { + ATTR_FORECAST_CONDITION: "rainy", + ATTR_FORECAST_TIME: "2021-03-12T11:00:00+00:00", + ATTR_FORECAST_PRECIPITATION: 0.1219, + ATTR_FORECAST_PRECIPITATION_PROBABILITY: 25, + ATTR_FORECAST_TEMP: 20, + ATTR_FORECAST_TEMP_LOW: 12, + ATTR_FORECAST_WIND_BEARING: 217.98, + ATTR_FORECAST_WIND_SPEED: 19.7949, + }, + { + 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.6428, + }, + { + ATTR_FORECAST_CONDITION: "snowy", + ATTR_FORECAST_TIME: "2021-03-14T10:00:00+00:00", + ATTR_FORECAST_PRECIPITATION: 23.9573, + ATTR_FORECAST_PRECIPITATION_PROBABILITY: 95, + ATTR_FORECAST_TEMP: 6, + ATTR_FORECAST_TEMP_LOW: 1, + ATTR_FORECAST_WIND_BEARING: 70.25, + ATTR_FORECAST_WIND_SPEED: 26.1518, + }, + { + ATTR_FORECAST_CONDITION: "snowy", + ATTR_FORECAST_TIME: "2021-03-15T10:00:00+00:00", + ATTR_FORECAST_PRECIPITATION: 1.4630, + ATTR_FORECAST_PRECIPITATION_PROBABILITY: 55, + ATTR_FORECAST_TEMP: 6, + ATTR_FORECAST_TEMP_LOW: -1, + ATTR_FORECAST_WIND_BEARING: 84.47, + ATTR_FORECAST_WIND_SPEED: 25.5725, + }, + { + 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.7987, + }, + { + 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.6999, + }, + { + 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.5895, + }, + { + ATTR_FORECAST_CONDITION: "rainy", + ATTR_FORECAST_TIME: "2021-03-19T10:00:00+00:00", + ATTR_FORECAST_PRECIPITATION: 2.9261, + ATTR_FORECAST_PRECIPITATION_PROBABILITY: 55, + ATTR_FORECAST_TEMP: 9, + ATTR_FORECAST_TEMP_LOW: 4, + ATTR_FORECAST_WIND_BEARING: 68.54, + ATTR_FORECAST_WIND_SPEED: 22.3860, + }, + { + 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.9221, + }, + ] + assert weather_state.attributes[ATTR_FRIENDLY_NAME] == "Tomorrow.io - 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.7691 + assert weather_state.attributes[ATTR_WEATHER_TEMPERATURE] == 7 + assert weather_state.attributes[ATTR_WEATHER_VISIBILITY] == 13.1162 + assert weather_state.attributes[ATTR_WEATHER_WIND_BEARING] == 315.14 + assert weather_state.attributes[ATTR_WEATHER_WIND_SPEED] == 15.0152 diff --git a/tests/fixtures/tomorrowio/v4.json b/tests/fixtures/tomorrowio/v4.json new file mode 100644 index 00000000000000..5cd86b5b60ebf8 --- /dev/null +++ b/tests/fixtures/tomorrowio/v4.json @@ -0,0 +1,2384 @@ +{ + "current": { + "temperature": 44.13, + "humidity": 22.71, + "pressureSeaLevel": 30.35, + "windSpeed": 9.33, + "windDirection": 315.14, + "weatherCode": 1000, + "visibility": 8.15, + "pollutantO3": 46.53, + "windGust": 12.64, + "cloudCover": 100, + "precipitationType": 1, + "particulateMatter25": 0.15, + "particulateMatter10": 0.57, + "pollutantNO2": 10.67, + "pollutantCO": 0.63, + "pollutantSO2": 1.65, + "epaIndex": 24, + "epaPrimaryPollutant": 0, + "epaHealthConcern": 0, + "mepIndex": 23, + "mepPrimaryPollutant": 1, + "mepHealthConcern": 0, + "treeIndex": 0, + "weedIndex": 0, + "grassIndex": 0, + "fireIndex": 10, + "temperatureApparent": 101.3, + "dewPoint": 72.82, + "pressureSurfaceLevel": 29.47, + "solarGHI": 0, + "cloudBase": 0.74, + "cloudCeiling": 0.74 + }, + "forecasts": { + "nowcast": [ + { + "startTime": "2021-03-07T17:48:00Z", + "values": { + "temperatureMin": 44.13, + "temperatureMax": 44.13, + "windSpeed": 9.33, + "windDirection": 315.14, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T17:53:00Z", + "values": { + "temperatureMin": 43.9, + "temperatureMax": 43.9, + "windSpeed": 9.31, + "windDirection": 315.14, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T17:58:00Z", + "values": { + "temperatureMin": 43.68, + "temperatureMax": 43.68, + "windSpeed": 9.28, + "windDirection": 315.14, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T18:03:00Z", + "values": { + "temperatureMin": 43.66, + "temperatureMax": 43.66, + "windSpeed": 9.26, + "windDirection": 315.14, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T18:08:00Z", + "values": { + "temperatureMin": 43.79, + "temperatureMax": 43.79, + "windSpeed": 9.22, + "windDirection": 315.14, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T18:13:00Z", + "values": { + "temperatureMin": 43.92, + "temperatureMax": 43.92, + "windSpeed": 9.17, + "windDirection": 315.14, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T18:18:00Z", + "values": { + "temperatureMin": 44.04, + "temperatureMax": 44.04, + "windSpeed": 9.13, + "windDirection": 315.14, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T18:23:00Z", + "values": { + "temperatureMin": 44.17, + "temperatureMax": 44.17, + "windSpeed": 9.06, + "windDirection": 315.14, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T18:28:00Z", + "values": { + "temperatureMin": 44.31, + "temperatureMax": 44.31, + "windSpeed": 9.02, + "windDirection": 315.14, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T18:33:00Z", + "values": { + "temperatureMin": 44.44, + "temperatureMax": 44.44, + "windSpeed": 8.97, + "windDirection": 321.71, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T18:38:00Z", + "values": { + "temperatureMin": 44.56, + "temperatureMax": 44.56, + "windSpeed": 8.93, + "windDirection": 321.71, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T18:43:00Z", + "values": { + "temperatureMin": 44.69, + "temperatureMax": 44.69, + "windSpeed": 8.88, + "windDirection": 321.71, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T18:48:00Z", + "values": { + "temperatureMin": 44.82, + "temperatureMax": 44.82, + "windSpeed": 8.84, + "windDirection": 321.71, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T18:53:00Z", + "values": { + "temperatureMin": 44.94, + "temperatureMax": 44.94, + "windSpeed": 8.79, + "windDirection": 321.71, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T18:58:00Z", + "values": { + "temperatureMin": 45.07, + "temperatureMax": 45.07, + "windSpeed": 8.75, + "windDirection": 321.71, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T19:03:00Z", + "values": { + "temperatureMin": 45.16, + "temperatureMax": 45.16, + "windSpeed": 8.75, + "windDirection": 321.71, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T19:08:00Z", + "values": { + "temperatureMin": 45.23, + "temperatureMax": 45.23, + "windSpeed": 8.75, + "windDirection": 321.71, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T19:13:00Z", + "values": { + "temperatureMin": 45.28, + "temperatureMax": 45.28, + "windSpeed": 8.77, + "windDirection": 321.71, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T19:18:00Z", + "values": { + "temperatureMin": 45.36, + "temperatureMax": 45.36, + "windSpeed": 8.79, + "windDirection": 321.71, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T19:23:00Z", + "values": { + "temperatureMin": 45.43, + "temperatureMax": 45.43, + "windSpeed": 8.81, + "windDirection": 321.71, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T19:28:00Z", + "values": { + "temperatureMin": 45.5, + "temperatureMax": 45.5, + "windSpeed": 8.81, + "windDirection": 321.71, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T19:33:00Z", + "values": { + "temperatureMin": 45.55, + "temperatureMax": 45.55, + "windSpeed": 8.84, + "windDirection": 323.38, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T19:38:00Z", + "values": { + "temperatureMin": 45.63, + "temperatureMax": 45.63, + "windSpeed": 8.86, + "windDirection": 323.38, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T19:43:00Z", + "values": { + "temperatureMin": 45.7, + "temperatureMax": 45.7, + "windSpeed": 8.88, + "windDirection": 323.38, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T19:48:00Z", + "values": { + "temperatureMin": 45.75, + "temperatureMax": 45.75, + "windSpeed": 8.9, + "windDirection": 323.38, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T19:53:00Z", + "values": { + "temperatureMin": 45.82, + "temperatureMax": 45.82, + "windSpeed": 8.9, + "windDirection": 323.38, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T19:58:00Z", + "values": { + "temperatureMin": 45.9, + "temperatureMax": 45.9, + "windSpeed": 8.93, + "windDirection": 323.38, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T20:03:00Z", + "values": { + "temperatureMin": 45.88, + "temperatureMax": 45.88, + "windSpeed": 8.97, + "windDirection": 323.38, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T20:08:00Z", + "values": { + "temperatureMin": 45.82, + "temperatureMax": 45.82, + "windSpeed": 9.02, + "windDirection": 323.38, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T20:13:00Z", + "values": { + "temperatureMin": 45.75, + "temperatureMax": 45.75, + "windSpeed": 9.06, + "windDirection": 323.38, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T20:18:00Z", + "values": { + "temperatureMin": 45.7, + "temperatureMax": 45.7, + "windSpeed": 9.1, + "windDirection": 323.38, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T20:23:00Z", + "values": { + "temperatureMin": 45.63, + "temperatureMax": 45.63, + "windSpeed": 9.15, + "windDirection": 323.38, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T20:28:00Z", + "values": { + "temperatureMin": 45.57, + "temperatureMax": 45.57, + "windSpeed": 9.19, + "windDirection": 323.38, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T20:33:00Z", + "values": { + "temperatureMin": 45.5, + "temperatureMax": 45.5, + "windSpeed": 9.24, + "windDirection": 318.43, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T20:38:00Z", + "values": { + "temperatureMin": 45.45, + "temperatureMax": 45.45, + "windSpeed": 9.28, + "windDirection": 318.43, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T20:43:00Z", + "values": { + "temperatureMin": 45.39, + "temperatureMax": 45.39, + "windSpeed": 9.33, + "windDirection": 318.43, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T20:48:00Z", + "values": { + "temperatureMin": 45.32, + "temperatureMax": 45.32, + "windSpeed": 9.37, + "windDirection": 318.43, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T20:53:00Z", + "values": { + "temperatureMin": 45.27, + "temperatureMax": 45.27, + "windSpeed": 9.42, + "windDirection": 318.43, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T20:58:00Z", + "values": { + "temperatureMin": 45.19, + "temperatureMax": 45.19, + "windSpeed": 9.46, + "windDirection": 318.43, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T21:03:00Z", + "values": { + "temperatureMin": 45.14, + "temperatureMax": 45.14, + "windSpeed": 9.4, + "windDirection": 318.43, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T21:08:00Z", + "values": { + "temperatureMin": 45.07, + "temperatureMax": 45.07, + "windSpeed": 9.24, + "windDirection": 318.43, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T21:13:00Z", + "values": { + "temperatureMin": 45.01, + "temperatureMax": 45.01, + "windSpeed": 9.08, + "windDirection": 318.43, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T21:18:00Z", + "values": { + "temperatureMin": 44.94, + "temperatureMax": 44.94, + "windSpeed": 8.95, + "windDirection": 318.43, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T21:23:00Z", + "values": { + "temperatureMin": 44.89, + "temperatureMax": 44.89, + "windSpeed": 8.79, + "windDirection": 318.43, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T21:28:00Z", + "values": { + "temperatureMin": 44.82, + "temperatureMax": 44.82, + "windSpeed": 8.63, + "windDirection": 318.43, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T21:33:00Z", + "values": { + "temperatureMin": 44.76, + "temperatureMax": 44.76, + "windSpeed": 8.5, + "windDirection": 320.9, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T21:38:00Z", + "values": { + "temperatureMin": 44.69, + "temperatureMax": 44.69, + "windSpeed": 8.34, + "windDirection": 320.9, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T21:43:00Z", + "values": { + "temperatureMin": 44.64, + "temperatureMax": 44.64, + "windSpeed": 8.19, + "windDirection": 320.9, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T21:48:00Z", + "values": { + "temperatureMin": 44.56, + "temperatureMax": 44.56, + "windSpeed": 8.05, + "windDirection": 320.9, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T21:53:00Z", + "values": { + "temperatureMin": 44.51, + "temperatureMax": 44.51, + "windSpeed": 7.9, + "windDirection": 320.9, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T21:58:00Z", + "values": { + "temperatureMin": 44.44, + "temperatureMax": 44.44, + "windSpeed": 7.74, + "windDirection": 320.9, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T22:03:00Z", + "values": { + "temperatureMin": 44.26, + "temperatureMax": 44.26, + "windSpeed": 7.47, + "windDirection": 320.9, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T22:08:00Z", + "values": { + "temperatureMin": 44.01, + "temperatureMax": 44.01, + "windSpeed": 7.14, + "windDirection": 320.9, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T22:13:00Z", + "values": { + "temperatureMin": 43.74, + "temperatureMax": 43.74, + "windSpeed": 6.78, + "windDirection": 320.9, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T22:18:00Z", + "values": { + "temperatureMin": 43.48, + "temperatureMax": 43.48, + "windSpeed": 6.44, + "windDirection": 320.9, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T22:23:00Z", + "values": { + "temperatureMin": 43.23, + "temperatureMax": 43.23, + "windSpeed": 6.08, + "windDirection": 320.9, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T22:28:00Z", + "values": { + "temperatureMin": 42.98, + "temperatureMax": 42.98, + "windSpeed": 5.75, + "windDirection": 320.9, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T22:33:00Z", + "values": { + "temperatureMin": 42.71, + "temperatureMax": 42.71, + "windSpeed": 5.39, + "windDirection": 322.11, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T22:38:00Z", + "values": { + "temperatureMin": 42.46, + "temperatureMax": 42.46, + "windSpeed": 5.06, + "windDirection": 322.11, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T22:43:00Z", + "values": { + "temperatureMin": 42.21, + "temperatureMax": 42.21, + "windSpeed": 4.7, + "windDirection": 322.11, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T22:48:00Z", + "values": { + "temperatureMin": 41.94, + "temperatureMax": 41.94, + "windSpeed": 4.36, + "windDirection": 322.11, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T22:53:00Z", + "values": { + "temperatureMin": 41.68, + "temperatureMax": 41.68, + "windSpeed": 4, + "windDirection": 322.11, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T22:58:00Z", + "values": { + "temperatureMin": 41.43, + "temperatureMax": 41.43, + "windSpeed": 3.67, + "windDirection": 322.11, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T23:03:00Z", + "values": { + "temperatureMin": 41.16, + "temperatureMax": 41.16, + "windSpeed": 3.6, + "windDirection": 322.11, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T23:08:00Z", + "values": { + "temperatureMin": 40.91, + "temperatureMax": 40.91, + "windSpeed": 3.76, + "windDirection": 322.11, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T23:13:00Z", + "values": { + "temperatureMin": 40.66, + "temperatureMax": 40.66, + "windSpeed": 3.91, + "windDirection": 322.11, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T23:18:00Z", + "values": { + "temperatureMin": 40.41, + "temperatureMax": 40.41, + "windSpeed": 4.05, + "windDirection": 322.11, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T23:23:00Z", + "values": { + "temperatureMin": 40.14, + "temperatureMax": 40.14, + "windSpeed": 4.21, + "windDirection": 322.11, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T23:28:00Z", + "values": { + "temperatureMin": 39.88, + "temperatureMax": 39.88, + "windSpeed": 4.36, + "windDirection": 322.11, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T23:33:00Z", + "values": { + "temperatureMin": 39.63, + "temperatureMax": 39.63, + "windSpeed": 4.5, + "windDirection": 295.94, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T23:38:00Z", + "values": { + "temperatureMin": 39.38, + "temperatureMax": 39.38, + "windSpeed": 4.65, + "windDirection": 295.94, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T23:43:00Z", + "values": { + "temperatureMin": 39.11, + "temperatureMax": 39.11, + "windSpeed": 4.79, + "windDirection": 295.94, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + } + ], + "hourly": [ + { + "startTime": "2021-03-07T17:48:00Z", + "values": { + "temperatureMin": 44.13, + "temperatureMax": 44.13, + "windSpeed": 9.33, + "windDirection": 315.14, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T18:48:00Z", + "values": { + "temperatureMin": 44.82, + "temperatureMax": 44.82, + "windSpeed": 8.84, + "windDirection": 321.71, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T19:48:00Z", + "values": { + "temperatureMin": 45.75, + "temperatureMax": 45.75, + "windSpeed": 8.9, + "windDirection": 323.38, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T20:48:00Z", + "values": { + "temperatureMin": 45.32, + "temperatureMax": 45.32, + "windSpeed": 9.37, + "windDirection": 318.43, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T21:48:00Z", + "values": { + "temperatureMin": 44.56, + "temperatureMax": 44.56, + "windSpeed": 8.05, + "windDirection": 320.9, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T22:48:00Z", + "values": { + "temperatureMin": 41.94, + "temperatureMax": 41.94, + "windSpeed": 4.36, + "windDirection": 322.11, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T23:48:00Z", + "values": { + "temperatureMin": 38.86, + "temperatureMax": 38.86, + "windSpeed": 4.94, + "windDirection": 295.94, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-08T00:48:00Z", + "values": { + "temperatureMin": 36.18, + "temperatureMax": 36.18, + "windSpeed": 5.59, + "windDirection": 11.94, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-08T01:48:00Z", + "values": { + "temperatureMin": 34.3, + "temperatureMax": 34.3, + "windSpeed": 5.57, + "windDirection": 13.68, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-08T02:48:00Z", + "values": { + "temperatureMin": 32.88, + "temperatureMax": 32.88, + "windSpeed": 5.41, + "windDirection": 14.93, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-08T03:48:00Z", + "values": { + "temperatureMin": 31.91, + "temperatureMax": 31.91, + "windSpeed": 4.61, + "windDirection": 26.07, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-08T04:48:00Z", + "values": { + "temperatureMin": 29.17, + "temperatureMax": 29.17, + "windSpeed": 2.59, + "windDirection": 51.27, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-08T05:48:00Z", + "values": { + "temperatureMin": 27.37, + "temperatureMax": 27.37, + "windSpeed": 3.31, + "windDirection": 343.25, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-08T06:48:00Z", + "values": { + "temperatureMin": 26.73, + "temperatureMax": 26.73, + "windSpeed": 4.27, + "windDirection": 341.46, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-08T07:48:00Z", + "values": { + "temperatureMin": 26.38, + "temperatureMax": 26.38, + "windSpeed": 3.53, + "windDirection": 322.34, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-08T08:48:00Z", + "values": { + "temperatureMin": 26.15, + "temperatureMax": 26.15, + "windSpeed": 3.65, + "windDirection": 294.69, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-08T09:48:00Z", + "values": { + "temperatureMin": 30.07, + "temperatureMax": 30.07, + "windSpeed": 3.2, + "windDirection": 325.32, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-08T10:48:00Z", + "values": { + "temperatureMin": 31.03, + "temperatureMax": 31.03, + "windSpeed": 2.84, + "windDirection": 322.27, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-08T11:48:00Z", + "values": { + "temperatureMin": 27.23, + "temperatureMax": 27.23, + "windSpeed": 5.59, + "windDirection": 310.14, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-08T12:48:00Z", + "values": { + "temperatureMin": 29.21, + "temperatureMax": 29.21, + "windSpeed": 7.05, + "windDirection": 324.8, + "weatherCode": 1100, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-08T13:48:00Z", + "values": { + "temperatureMin": 33.19, + "temperatureMax": 33.19, + "windSpeed": 6.46, + "windDirection": 335.16, + "weatherCode": 1101, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-08T14:48:00Z", + "values": { + "temperatureMin": 37.02, + "temperatureMax": 37.02, + "windSpeed": 5.88, + "windDirection": 324.49, + "weatherCode": 1001, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-08T15:48:00Z", + "values": { + "temperatureMin": 40.01, + "temperatureMax": 40.01, + "windSpeed": 5.55, + "windDirection": 310.68, + "weatherCode": 1001, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-08T16:48:00Z", + "values": { + "temperatureMin": 42.37, + "temperatureMax": 42.37, + "windSpeed": 5.46, + "windDirection": 304.18, + "weatherCode": 1101, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-08T17:48:00Z", + "values": { + "temperatureMin": 44.62, + "temperatureMax": 44.62, + "windSpeed": 4.99, + "windDirection": 301.19, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-08T18:48:00Z", + "values": { + "temperatureMin": 46.78, + "temperatureMax": 46.78, + "windSpeed": 4.72, + "windDirection": 295.05, + "weatherCode": 1100, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-08T19:48:00Z", + "values": { + "temperatureMin": 48.42, + "temperatureMax": 48.42, + "windSpeed": 4.81, + "windDirection": 287.4, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-08T20:48:00Z", + "values": { + "temperatureMin": 49.28, + "temperatureMax": 49.28, + "windSpeed": 4.74, + "windDirection": 282.48, + "weatherCode": 1100, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-08T21:48:00Z", + "values": { + "temperatureMin": 48.72, + "temperatureMax": 48.72, + "windSpeed": 2.51, + "windDirection": 268.74, + "weatherCode": 1100, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-08T22:48:00Z", + "values": { + "temperatureMin": 44.37, + "temperatureMax": 44.37, + "windSpeed": 3.56, + "windDirection": 180.04, + "weatherCode": 1101, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-08T23:48:00Z", + "values": { + "temperatureMin": 39.9, + "temperatureMax": 39.9, + "windSpeed": 4.68, + "windDirection": 177.89, + "weatherCode": 1101, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-09T00:48:00Z", + "values": { + "temperatureMin": 37.87, + "temperatureMax": 37.87, + "windSpeed": 5.21, + "windDirection": 197.47, + "weatherCode": 1101, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-09T01:48:00Z", + "values": { + "temperatureMin": 36.91, + "temperatureMax": 36.91, + "windSpeed": 5.46, + "windDirection": 209.77, + "weatherCode": 1100, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-09T02:48:00Z", + "values": { + "temperatureMin": 36.64, + "temperatureMax": 36.64, + "windSpeed": 6.11, + "windDirection": 210.14, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-09T03:48:00Z", + "values": { + "temperatureMin": 36.63, + "temperatureMax": 36.63, + "windSpeed": 6.4, + "windDirection": 216, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-09T04:48:00Z", + "values": { + "temperatureMin": 36.23, + "temperatureMax": 36.23, + "windSpeed": 6.22, + "windDirection": 223.92, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-09T05:48:00Z", + "values": { + "temperatureMin": 35.58, + "temperatureMax": 35.58, + "windSpeed": 5.75, + "windDirection": 229.68, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-09T06:48:00Z", + "values": { + "temperatureMin": 34.68, + "temperatureMax": 34.68, + "windSpeed": 5.21, + "windDirection": 235.24, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-09T07:48:00Z", + "values": { + "temperatureMin": 33.69, + "temperatureMax": 33.69, + "windSpeed": 4.81, + "windDirection": 237.24, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-09T08:48:00Z", + "values": { + "temperatureMin": 32.74, + "temperatureMax": 32.74, + "windSpeed": 4.52, + "windDirection": 239.35, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-09T09:48:00Z", + "values": { + "temperatureMin": 32.05, + "temperatureMax": 32.05, + "windSpeed": 4.32, + "windDirection": 245.68, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-09T10:48:00Z", + "values": { + "temperatureMin": 31.57, + "temperatureMax": 31.57, + "windSpeed": 4.14, + "windDirection": 248.11, + "weatherCode": 1100, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-09T11:48:00Z", + "values": { + "temperatureMin": 32.92, + "temperatureMax": 32.92, + "windSpeed": 4.32, + "windDirection": 249.54, + "weatherCode": 1100, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-09T12:48:00Z", + "values": { + "temperatureMin": 38.5, + "temperatureMax": 38.5, + "windSpeed": 4.7, + "windDirection": 253.3, + "weatherCode": 1100, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-09T13:48:00Z", + "values": { + "temperatureMin": 46.08, + "temperatureMax": 46.08, + "windSpeed": 4.41, + "windDirection": 258.49, + "weatherCode": 1100, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-09T14:48:00Z", + "values": { + "temperatureMin": 53.26, + "temperatureMax": 53.26, + "windSpeed": 4.9, + "windDirection": 260.49, + "weatherCode": 1101, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-09T15:48:00Z", + "values": { + "temperatureMin": 58.15, + "temperatureMax": 58.15, + "windSpeed": 5.55, + "windDirection": 261.29, + "weatherCode": 1100, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-09T16:48:00Z", + "values": { + "temperatureMin": 61.56, + "temperatureMax": 61.56, + "windSpeed": 6.35, + "windDirection": 264.3, + "weatherCode": 1101, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-09T17:48:00Z", + "values": { + "temperatureMin": 64, + "temperatureMax": 64, + "windSpeed": 6.6, + "windDirection": 257.54, + "weatherCode": 1101, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-09T18:48:00Z", + "values": { + "temperatureMin": 65.79, + "temperatureMax": 65.79, + "windSpeed": 6.96, + "windDirection": 253.12, + "weatherCode": 1100, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-09T19:48:00Z", + "values": { + "temperatureMin": 66.74, + "temperatureMax": 66.74, + "windSpeed": 6.8, + "windDirection": 259.46, + "weatherCode": 1100, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-09T20:48:00Z", + "values": { + "temperatureMin": 66.96, + "temperatureMax": 66.96, + "windSpeed": 6.33, + "windDirection": 294.25, + "weatherCode": 1101, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-09T21:48:00Z", + "values": { + "temperatureMin": 64.35, + "temperatureMax": 64.35, + "windSpeed": 3.91, + "windDirection": 279.37, + "weatherCode": 1100, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-09T22:48:00Z", + "values": { + "temperatureMin": 61.07, + "temperatureMax": 61.07, + "windSpeed": 3.65, + "windDirection": 218.19, + "weatherCode": 1100, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-09T23:48:00Z", + "values": { + "temperatureMin": 56.3, + "temperatureMax": 56.3, + "windSpeed": 4.09, + "windDirection": 208.3, + "weatherCode": 1101, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-10T00:48:00Z", + "values": { + "temperatureMin": 53.19, + "temperatureMax": 53.19, + "windSpeed": 4.21, + "windDirection": 216.42, + "weatherCode": 1102, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-10T01:48:00Z", + "values": { + "temperatureMin": 51.94, + "temperatureMax": 51.94, + "windSpeed": 3.38, + "windDirection": 257.19, + "weatherCode": 1001, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-10T02:48:00Z", + "values": { + "temperatureMin": 49.82, + "temperatureMax": 49.82, + "windSpeed": 2.71, + "windDirection": 288.85, + "weatherCode": 1001, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-10T03:48:00Z", + "values": { + "temperatureMin": 48.24, + "temperatureMax": 48.24, + "windSpeed": 2.8, + "windDirection": 334.41, + "weatherCode": 1001, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-10T04:48:00Z", + "values": { + "temperatureMin": 47.44, + "temperatureMax": 47.44, + "windSpeed": 2.26, + "windDirection": 342.01, + "weatherCode": 1001, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-10T05:48:00Z", + "values": { + "temperatureMin": 45.59, + "temperatureMax": 45.59, + "windSpeed": 2.35, + "windDirection": 2.43, + "weatherCode": 1001, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-10T06:48:00Z", + "values": { + "temperatureMin": 43.43, + "temperatureMax": 43.43, + "windSpeed": 2.3, + "windDirection": 336.56, + "weatherCode": 1001, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-10T07:48:00Z", + "values": { + "temperatureMin": 41.11, + "temperatureMax": 41.11, + "windSpeed": 2.71, + "windDirection": 4.41, + "weatherCode": 1001, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-10T08:48:00Z", + "values": { + "temperatureMin": 39.58, + "temperatureMax": 39.58, + "windSpeed": 3.4, + "windDirection": 21.26, + "weatherCode": 1001, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-10T09:48:00Z", + "values": { + "temperatureMin": 39.85, + "temperatureMax": 39.85, + "windSpeed": 3.31, + "windDirection": 22.76, + "weatherCode": 1001, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-10T10:48:00Z", + "values": { + "temperatureMin": 37.85, + "temperatureMax": 37.85, + "windSpeed": 4.03, + "windDirection": 29.3, + "weatherCode": 1101, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-10T11:48:00Z", + "values": { + "temperatureMin": 38.97, + "temperatureMax": 38.97, + "windSpeed": 3.15, + "windDirection": 21.82, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-10T12:48:00Z", + "values": { + "temperatureMin": 44.31, + "temperatureMax": 44.31, + "windSpeed": 3.53, + "windDirection": 14.25, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-10T13:48:00Z", + "values": { + "temperatureMin": 50.25, + "temperatureMax": 50.25, + "windSpeed": 2.82, + "windDirection": 42.41, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-10T14:48:00Z", + "values": { + "temperatureMin": 54.97, + "temperatureMax": 54.97, + "windSpeed": 2.53, + "windDirection": 87.81, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-10T15:48:00Z", + "values": { + "temperatureMin": 58.46, + "temperatureMax": 58.46, + "windSpeed": 3.09, + "windDirection": 125.82, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-10T16:48:00Z", + "values": { + "temperatureMin": 61.21, + "temperatureMax": 61.21, + "windSpeed": 4.03, + "windDirection": 157.54, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-10T17:48:00Z", + "values": { + "temperatureMin": 63.36, + "temperatureMax": 63.36, + "windSpeed": 5.21, + "windDirection": 166.66, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-10T18:48:00Z", + "values": { + "temperatureMin": 64.83, + "temperatureMax": 64.83, + "windSpeed": 6.93, + "windDirection": 189.24, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-10T19:48:00Z", + "values": { + "temperatureMin": 65.23, + "temperatureMax": 65.23, + "windSpeed": 8.95, + "windDirection": 194.58, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-10T20:48:00Z", + "values": { + "temperatureMin": 64.98, + "temperatureMax": 64.98, + "windSpeed": 9.4, + "windDirection": 193.22, + "weatherCode": 1100, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-10T21:48:00Z", + "values": { + "temperatureMin": 64.06, + "temperatureMax": 64.06, + "windSpeed": 8.55, + "windDirection": 186.39, + "weatherCode": 1100, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-10T22:48:00Z", + "values": { + "temperatureMin": 61.9, + "temperatureMax": 61.9, + "windSpeed": 7.49, + "windDirection": 171.81, + "weatherCode": 1100, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-10T23:48:00Z", + "values": { + "temperatureMin": 59.4, + "temperatureMax": 59.4, + "windSpeed": 7.54, + "windDirection": 165.51, + "weatherCode": 1100, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-11T00:48:00Z", + "values": { + "temperatureMin": 57.63, + "temperatureMax": 57.63, + "windSpeed": 8.12, + "windDirection": 171.94, + "weatherCode": 1102, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-11T01:48:00Z", + "values": { + "temperatureMin": 56.17, + "temperatureMax": 56.17, + "windSpeed": 8.7, + "windDirection": 176.84, + "weatherCode": 1001, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-11T02:48:00Z", + "values": { + "temperatureMin": 55.36, + "temperatureMax": 55.36, + "windSpeed": 9.42, + "windDirection": 184.14, + "weatherCode": 1001, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-11T03:48:00Z", + "values": { + "temperatureMin": 54.88, + "temperatureMax": 54.88, + "windSpeed": 10, + "windDirection": 195.54, + "weatherCode": 1001, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-11T04:48:00Z", + "values": { + "temperatureMin": 54.14, + "temperatureMax": 54.14, + "windSpeed": 10.4, + "windDirection": 200.56, + "weatherCode": 1001, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-11T05:48:00Z", + "values": { + "temperatureMin": 53.46, + "temperatureMax": 53.46, + "windSpeed": 10.04, + "windDirection": 198.08, + "weatherCode": 1001, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-11T06:48:00Z", + "values": { + "temperatureMin": 52.11, + "temperatureMax": 52.11, + "windSpeed": 10.02, + "windDirection": 199.54, + "weatherCode": 1101, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-11T07:48:00Z", + "values": { + "temperatureMin": 51.64, + "temperatureMax": 51.64, + "windSpeed": 10.51, + "windDirection": 202.73, + "weatherCode": 1102, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-11T08:48:00Z", + "values": { + "temperatureMin": 50.79, + "temperatureMax": 50.79, + "windSpeed": 10.38, + "windDirection": 203.35, + "weatherCode": 1001, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-11T09:48:00Z", + "values": { + "temperatureMin": 49.93, + "temperatureMax": 49.93, + "windSpeed": 9.51, + "windDirection": 210.36, + "weatherCode": 1001, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-11T10:48:00Z", + "values": { + "temperatureMin": 49.1, + "temperatureMax": 49.1, + "windSpeed": 8.61, + "windDirection": 210.6, + "weatherCode": 1001, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-11T11:48:00Z", + "values": { + "temperatureMin": 48.42, + "temperatureMax": 48.42, + "windSpeed": 9.15, + "windDirection": 211.29, + "weatherCode": 1001, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-11T12:48:00Z", + "values": { + "temperatureMin": 48.9, + "temperatureMax": 48.9, + "windSpeed": 10.25, + "windDirection": 215.59, + "weatherCode": 1001, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-11T13:48:00Z", + "values": { + "temperatureMin": 50.54, + "temperatureMax": 50.54, + "windSpeed": 10.18, + "windDirection": 215.48, + "weatherCode": 1102, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-11T14:48:00Z", + "values": { + "temperatureMin": 53.19, + "temperatureMax": 53.19, + "windSpeed": 9.4, + "windDirection": 208.76, + "weatherCode": 1101, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-11T15:48:00Z", + "values": { + "temperatureMin": 56.19, + "temperatureMax": 56.19, + "windSpeed": 9.73, + "windDirection": 197.59, + "weatherCode": 1101, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-11T16:48:00Z", + "values": { + "temperatureMin": 59.34, + "temperatureMax": 59.34, + "windSpeed": 10.69, + "windDirection": 204.29, + "weatherCode": 1100, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-11T17:48:00Z", + "values": { + "temperatureMin": 62.35, + "temperatureMax": 62.35, + "windSpeed": 11.81, + "windDirection": 204.56, + "weatherCode": 1100, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-11T18:48:00Z", + "values": { + "temperatureMin": 64.6, + "temperatureMax": 64.6, + "windSpeed": 13.09, + "windDirection": 206.85, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-11T19:48:00Z", + "values": { + "temperatureMin": 65.91, + "temperatureMax": 65.91, + "windSpeed": 13.82, + "windDirection": 204.82, + "weatherCode": 1100, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-11T20:48:00Z", + "values": { + "temperatureMin": 66.22, + "temperatureMax": 66.22, + "windSpeed": 14.54, + "windDirection": 208.43, + "weatherCode": 1100, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-11T21:48:00Z", + "values": { + "temperatureMin": 65.46, + "temperatureMax": 65.46, + "windSpeed": 13.2, + "windDirection": 208.3, + "weatherCode": 1101, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-11T22:48:00Z", + "values": { + "temperatureMin": 64.35, + "temperatureMax": 64.35, + "windSpeed": 12.35, + "windDirection": 208.58, + "weatherCode": 1101, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-11T23:48:00Z", + "values": { + "temperatureMin": 62.85, + "temperatureMax": 62.85, + "windSpeed": 12.86, + "windDirection": 205.39, + "weatherCode": 1101, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-12T00:48:00Z", + "values": { + "temperatureMin": 61.75, + "temperatureMax": 61.75, + "windSpeed": 14.7, + "windDirection": 209.51, + "weatherCode": 1102, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-12T01:48:00Z", + "values": { + "temperatureMin": 61.2, + "temperatureMax": 61.2, + "windSpeed": 15.57, + "windDirection": 211.47, + "weatherCode": 1001, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-12T02:48:00Z", + "values": { + "temperatureMin": 60.46, + "temperatureMax": 60.46, + "windSpeed": 14.94, + "windDirection": 211.57, + "weatherCode": 1001, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-12T03:48:00Z", + "values": { + "temperatureMin": 59.94, + "temperatureMax": 59.94, + "windSpeed": 14.29, + "windDirection": 208.93, + "weatherCode": 1001, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-12T04:48:00Z", + "values": { + "temperatureMin": 59.52, + "temperatureMax": 59.52, + "windSpeed": 14.36, + "windDirection": 217.91, + "weatherCode": 1001, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + } + ], + "daily": [ + { + "startTime": "2021-03-07T11:00:00Z", + "values": { + "temperatureMin": 26.11, + "temperatureMax": 45.93, + "windSpeed": 9.49, + "windDirection": 239.6, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-08T11:00:00Z", + "values": { + "temperatureMin": 26.28, + "temperatureMax": 49.42, + "windSpeed": 7.24, + "windDirection": 262.82, + "weatherCode": 1102, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-09T11:00:00Z", + "values": { + "temperatureMin": 31.48, + "temperatureMax": 66.98, + "windSpeed": 7.05, + "windDirection": 229.3, + "weatherCode": 1102, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-10T11:00:00Z", + "values": { + "temperatureMin": 37.32, + "temperatureMax": 65.28, + "windSpeed": 10.64, + "windDirection": 149.91, + "weatherCode": 1102, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-11T11:00:00Z", + "values": { + "temperatureMin": 48.29, + "temperatureMax": 66.25, + "windSpeed": 15.69, + "windDirection": 210.45, + "weatherCode": 1102, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-12T11:00:00Z", + "values": { + "temperatureMin": 53.83, + "temperatureMax": 67.91, + "windSpeed": 12.3, + "windDirection": 217.98, + "weatherCode": 4000, + "precipitationIntensityAvg": 0.0002, + "precipitationProbability": 25 + } + }, + { + "startTime": "2021-03-13T11:00:00Z", + "values": { + "temperatureMin": 42.91, + "temperatureMax": 54.48, + "windSpeed": 9.72, + "windDirection": 58.79, + "weatherCode": 1001, + "precipitationIntensityAvg": 0, + "precipitationProbability": 25 + } + }, + { + "startTime": "2021-03-14T10:00:00Z", + "values": { + "temperatureMin": 33.35, + "temperatureMax": 42.91, + "windSpeed": 16.25, + "windDirection": 70.25, + "weatherCode": 5101, + "precipitationIntensityAvg": 0.0393, + "precipitationProbability": 95 + } + }, + { + "startTime": "2021-03-15T10:00:00Z", + "values": { + "temperatureMin": 29.35, + "temperatureMax": 43.67, + "windSpeed": 15.89, + "windDirection": 84.47, + "weatherCode": 5001, + "precipitationIntensityAvg": 0.0024, + "precipitationProbability": 55 + } + }, + { + "startTime": "2021-03-16T10:00:00Z", + "values": { + "temperatureMin": 29.1, + "temperatureMax": 43, + "windSpeed": 6.71, + "windDirection": 103.85, + "weatherCode": 1102, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-17T10:00:00Z", + "values": { + "temperatureMin": 34.32, + "temperatureMax": 52.4, + "windSpeed": 7.27, + "windDirection": 145.41, + "weatherCode": 1102, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-18T10:00:00Z", + "values": { + "temperatureMin": 41.32, + "temperatureMax": 54.07, + "windSpeed": 6.58, + "windDirection": 62.99, + "weatherCode": 1001, + "precipitationIntensityAvg": 0, + "precipitationProbability": 10 + } + }, + { + "startTime": "2021-03-19T10:00:00Z", + "values": { + "temperatureMin": 39.4, + "temperatureMax": 48.94, + "windSpeed": 13.91, + "windDirection": 68.54, + "weatherCode": 4000, + "precipitationIntensityAvg": 0.0048, + "precipitationProbability": 55 + } + }, + { + "startTime": "2021-03-20T10:00:00Z", + "values": { + "temperatureMin": 35.06, + "temperatureMax": 40.12, + "windSpeed": 17.35, + "windDirection": 56.98, + "weatherCode": 5001, + "precipitationIntensityAvg": 0.002, + "precipitationProbability": 33.3 + } + }, + { + "startTime": "2021-03-21T10:00:00Z", + "values": { + "temperatureMin": 33.66, + "temperatureMax": 66.54, + "windSpeed": 15.93, + "windDirection": 82.57, + "weatherCode": 5001, + "precipitationIntensityAvg": 0.0004, + "precipitationProbability": 45 + } + } + ] + } + }