Use separate data coordinators for AccuWeather observation and forecast (#115628)

* Remove forecast option

* Update strings

* Use separate DataUpdateCoordinator for observation and forecast

* Fix tests

* Remove unneeded variable

* Separate data coordinator classes

* Use list comprehension

* Separate coordinator clasess to add type annotations

* Test the availability of the forecast sensor entity

* Add DataUpdateCoordinator types

* Use snapshot for test_sensor()

---------

Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com>
This commit is contained in:
Maciej Bieniek 2024-04-17 00:00:16 +02:00 committed by GitHub
parent 81036967f0
commit e7076ac83f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 6913 additions and 852 deletions

View File

@ -2,14 +2,10 @@
from __future__ import annotations from __future__ import annotations
from asyncio import timeout from dataclasses import dataclass
from datetime import timedelta
import logging import logging
from typing import Any
from accuweather import AccuWeather, ApiError, InvalidApiKeyError, RequestsExceededError from accuweather import AccuWeather
from aiohttp import ClientSession
from aiohttp.client_exceptions import ClientConnectorError
from homeassistant.components.sensor import DOMAIN as SENSOR_PLATFORM from homeassistant.components.sensor import DOMAIN as SENSOR_PLATFORM
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
@ -17,43 +13,70 @@ from homeassistant.const import CONF_API_KEY, CONF_NAME, Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import ATTR_FORECAST, CONF_FORECAST, DOMAIN, MANUFACTURER from .const import DOMAIN, UPDATE_INTERVAL_DAILY_FORECAST, UPDATE_INTERVAL_OBSERVATION
from .coordinator import (
AccuWeatherDailyForecastDataUpdateCoordinator,
AccuWeatherObservationDataUpdateCoordinator,
)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
PLATFORMS = [Platform.SENSOR, Platform.WEATHER] PLATFORMS = [Platform.SENSOR, Platform.WEATHER]
@dataclass
class AccuWeatherData:
"""Data for AccuWeather integration."""
coordinator_observation: AccuWeatherObservationDataUpdateCoordinator
coordinator_daily_forecast: AccuWeatherDailyForecastDataUpdateCoordinator
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up AccuWeather as config entry.""" """Set up AccuWeather as config entry."""
api_key: str = entry.data[CONF_API_KEY] api_key: str = entry.data[CONF_API_KEY]
name: str = entry.data[CONF_NAME] name: str = entry.data[CONF_NAME]
assert entry.unique_id is not None
location_key = entry.unique_id
forecast: bool = entry.options.get(CONF_FORECAST, False)
_LOGGER.debug("Using location_key: %s, get forecast: %s", location_key, forecast) location_key = entry.unique_id
_LOGGER.debug("Using location_key: %s", location_key)
websession = async_get_clientsession(hass) websession = async_get_clientsession(hass)
accuweather = AccuWeather(api_key, websession, location_key=location_key)
coordinator = AccuWeatherDataUpdateCoordinator( coordinator_observation = AccuWeatherObservationDataUpdateCoordinator(
hass, websession, api_key, location_key, forecast, name hass,
accuweather,
name,
"observation",
UPDATE_INTERVAL_OBSERVATION,
) )
await coordinator.async_config_entry_first_refresh()
coordinator_daily_forecast = AccuWeatherDailyForecastDataUpdateCoordinator(
hass,
accuweather,
name,
"daily forecast",
UPDATE_INTERVAL_DAILY_FORECAST,
)
await coordinator_observation.async_config_entry_first_refresh()
await coordinator_daily_forecast.async_config_entry_first_refresh()
entry.async_on_unload(entry.add_update_listener(update_listener)) entry.async_on_unload(entry.add_update_listener(update_listener))
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator hass.data.setdefault(DOMAIN, {})[entry.entry_id] = AccuWeatherData(
coordinator_observation=coordinator_observation,
coordinator_daily_forecast=coordinator_daily_forecast,
)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
# Remove ozone sensors from registry if they exist # Remove ozone sensors from registry if they exist
ent_reg = er.async_get(hass) ent_reg = er.async_get(hass)
for day in range(5): for day in range(5):
unique_id = f"{coordinator.location_key}-ozone-{day}" unique_id = f"{location_key}-ozone-{day}"
if entity_id := ent_reg.async_get_entity_id(SENSOR_PLATFORM, DOMAIN, unique_id): if entity_id := ent_reg.async_get_entity_id(SENSOR_PLATFORM, DOMAIN, unique_id):
_LOGGER.debug("Removing ozone sensor entity %s", entity_id) _LOGGER.debug("Removing ozone sensor entity %s", entity_id)
ent_reg.async_remove(entity_id) ent_reg.async_remove(entity_id)
@ -74,65 +97,3 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Update listener.""" """Update listener."""
await hass.config_entries.async_reload(entry.entry_id) await hass.config_entries.async_reload(entry.entry_id)
class AccuWeatherDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): # pylint: disable=hass-enforce-coordinator-module
"""Class to manage fetching AccuWeather data API."""
def __init__(
self,
hass: HomeAssistant,
session: ClientSession,
api_key: str,
location_key: str,
forecast: bool,
name: str,
) -> None:
"""Initialize."""
self.location_key = location_key
self.forecast = forecast
self.accuweather = AccuWeather(api_key, session, location_key=location_key)
self.device_info = DeviceInfo(
entry_type=DeviceEntryType.SERVICE,
identifiers={(DOMAIN, location_key)},
manufacturer=MANUFACTURER,
name=name,
# You don't need to provide specific details for the URL,
# so passing in _ characters is fine if the location key
# is correct
configuration_url=(
"http://accuweather.com/en/"
f"_/_/{location_key}/"
f"weather-forecast/{location_key}/"
),
)
# Enabling the forecast download increases the number of requests per data
# update, we use 40 minutes for current condition only and 80 minutes for
# current condition and forecast as update interval to not exceed allowed number
# of requests. We have 50 requests allowed per day, so we use 36 and leave 14 as
# a reserve for restarting HA.
update_interval = timedelta(minutes=40)
if self.forecast:
update_interval *= 2
_LOGGER.debug("Data will be update every %s", update_interval)
super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval)
async def _async_update_data(self) -> dict[str, Any]:
"""Update data via library."""
forecast: list[dict[str, Any]] = []
try:
async with timeout(10):
current = await self.accuweather.async_get_current_conditions()
if self.forecast:
forecast = await self.accuweather.async_get_daily_forecast()
except (
ApiError,
ClientConnectorError,
InvalidApiKeyError,
RequestsExceededError,
) as error:
raise UpdateFailed(error) from error
_LOGGER.debug("Requests remaining: %d", self.accuweather.requests_remaining)
return {**current, ATTR_FORECAST: forecast}

View File

@ -10,26 +10,12 @@ from aiohttp import ClientError
from aiohttp.client_exceptions import ClientConnectorError from aiohttp.client_exceptions import ClientConnectorError
import voluptuous as vol import voluptuous as vol
from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
from homeassistant.core import callback
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.schema_config_entry_flow import (
SchemaFlowFormStep,
SchemaOptionsFlowHandler,
)
from .const import CONF_FORECAST, DOMAIN from .const import DOMAIN
OPTIONS_SCHEMA = vol.Schema(
{
vol.Optional(CONF_FORECAST, default=False): bool,
}
)
OPTIONS_FLOW = {
"init": SchemaFlowFormStep(OPTIONS_SCHEMA),
}
class AccuWeatherFlowHandler(ConfigFlow, domain=DOMAIN): class AccuWeatherFlowHandler(ConfigFlow, domain=DOMAIN):
@ -87,9 +73,3 @@ class AccuWeatherFlowHandler(ConfigFlow, domain=DOMAIN):
), ),
errors=errors, errors=errors,
) )
@staticmethod
@callback
def async_get_options_flow(config_entry: ConfigEntry) -> SchemaOptionsFlowHandler:
"""Options callback for AccuWeather."""
return SchemaOptionsFlowHandler(config_entry, OPTIONS_FLOW)

View File

@ -2,6 +2,7 @@
from __future__ import annotations from __future__ import annotations
from datetime import timedelta
from typing import Final from typing import Final
from homeassistant.components.weather import ( from homeassistant.components.weather import (
@ -27,10 +28,8 @@ ATTR_CATEGORY: Final = "Category"
ATTR_DIRECTION: Final = "Direction" ATTR_DIRECTION: Final = "Direction"
ATTR_ENGLISH: Final = "English" ATTR_ENGLISH: Final = "English"
ATTR_LEVEL: Final = "level" ATTR_LEVEL: Final = "level"
ATTR_FORECAST: Final = "forecast"
ATTR_SPEED: Final = "Speed" ATTR_SPEED: Final = "Speed"
ATTR_VALUE: Final = "Value" ATTR_VALUE: Final = "Value"
CONF_FORECAST: Final = "forecast"
DOMAIN: Final = "accuweather" DOMAIN: Final = "accuweather"
MANUFACTURER: Final = "AccuWeather, Inc." MANUFACTURER: Final = "AccuWeather, Inc."
MAX_FORECAST_DAYS: Final = 4 MAX_FORECAST_DAYS: Final = 4
@ -56,3 +55,5 @@ CONDITION_MAP = {
for cond_ha, cond_codes in CONDITION_CLASSES.items() for cond_ha, cond_codes in CONDITION_CLASSES.items()
for cond_code in cond_codes for cond_code in cond_codes
} }
UPDATE_INTERVAL_OBSERVATION = timedelta(minutes=40)
UPDATE_INTERVAL_DAILY_FORECAST = timedelta(hours=6)

View File

@ -0,0 +1,124 @@
"""The AccuWeather coordinator."""
from asyncio import timeout
from datetime import timedelta
import logging
from typing import TYPE_CHECKING, Any
from accuweather import AccuWeather, ApiError, InvalidApiKeyError, RequestsExceededError
from aiohttp.client_exceptions import ClientConnectorError
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.update_coordinator import (
DataUpdateCoordinator,
TimestampDataUpdateCoordinator,
UpdateFailed,
)
from .const import DOMAIN, MANUFACTURER
EXCEPTIONS = (ApiError, ClientConnectorError, InvalidApiKeyError, RequestsExceededError)
_LOGGER = logging.getLogger(__name__)
class AccuWeatherObservationDataUpdateCoordinator(
DataUpdateCoordinator[dict[str, Any]]
):
"""Class to manage fetching AccuWeather data API."""
def __init__(
self,
hass: HomeAssistant,
accuweather: AccuWeather,
name: str,
coordinator_type: str,
update_interval: timedelta,
) -> None:
"""Initialize."""
self.accuweather = accuweather
self.location_key = accuweather.location_key
if TYPE_CHECKING:
assert self.location_key is not None
self.device_info = _get_device_info(self.location_key, name)
super().__init__(
hass,
_LOGGER,
name=f"{name} ({coordinator_type})",
update_interval=update_interval,
)
async def _async_update_data(self) -> dict[str, Any]:
"""Update data via library."""
try:
async with timeout(10):
result = await self.accuweather.async_get_current_conditions()
except EXCEPTIONS as error:
raise UpdateFailed(error) from error
_LOGGER.debug("Requests remaining: %d", self.accuweather.requests_remaining)
return result
class AccuWeatherDailyForecastDataUpdateCoordinator(
TimestampDataUpdateCoordinator[list[dict[str, Any]]]
):
"""Class to manage fetching AccuWeather data API."""
def __init__(
self,
hass: HomeAssistant,
accuweather: AccuWeather,
name: str,
coordinator_type: str,
update_interval: timedelta,
) -> None:
"""Initialize."""
self.accuweather = accuweather
self.location_key = accuweather.location_key
if TYPE_CHECKING:
assert self.location_key is not None
self.device_info = _get_device_info(self.location_key, name)
super().__init__(
hass,
_LOGGER,
name=f"{name} ({coordinator_type})",
update_interval=update_interval,
)
async def _async_update_data(self) -> list[dict[str, Any]]:
"""Update data via library."""
try:
async with timeout(10):
result = await self.accuweather.async_get_daily_forecast()
except EXCEPTIONS as error:
raise UpdateFailed(error) from error
_LOGGER.debug("Requests remaining: %d", self.accuweather.requests_remaining)
return result
def _get_device_info(location_key: str, name: str) -> DeviceInfo:
"""Get device info."""
return DeviceInfo(
entry_type=DeviceEntryType.SERVICE,
identifiers={(DOMAIN, location_key)},
manufacturer=MANUFACTURER,
name=name,
# You don't need to provide specific details for the URL,
# so passing in _ characters is fine if the location key
# is correct
configuration_url=(
"http://accuweather.com/en/"
f"_/_/{location_key}/weather-forecast/{location_key}/"
),
)

View File

@ -9,7 +9,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from . import AccuWeatherDataUpdateCoordinator from . import AccuWeatherData
from .const import DOMAIN from .const import DOMAIN
TO_REDACT = {CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE} TO_REDACT = {CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE}
@ -19,11 +19,9 @@ async def async_get_config_entry_diagnostics(
hass: HomeAssistant, config_entry: ConfigEntry hass: HomeAssistant, config_entry: ConfigEntry
) -> dict[str, Any]: ) -> dict[str, Any]:
"""Return diagnostics for a config entry.""" """Return diagnostics for a config entry."""
coordinator: AccuWeatherDataUpdateCoordinator = hass.data[DOMAIN][ accuweather_data: AccuWeatherData = hass.data[DOMAIN][config_entry.entry_id]
config_entry.entry_id
]
return { return {
"config_entry_data": async_redact_data(dict(config_entry.data), TO_REDACT), "config_entry_data": async_redact_data(dict(config_entry.data), TO_REDACT),
"coordinator_data": coordinator.data, "observation_data": accuweather_data.coordinator_observation.data,
} }

View File

@ -28,13 +28,12 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import AccuWeatherDataUpdateCoordinator from . import AccuWeatherData
from .const import ( from .const import (
API_METRIC, API_METRIC,
ATTR_CATEGORY, ATTR_CATEGORY,
ATTR_DIRECTION, ATTR_DIRECTION,
ATTR_ENGLISH, ATTR_ENGLISH,
ATTR_FORECAST,
ATTR_LEVEL, ATTR_LEVEL,
ATTR_SPEED, ATTR_SPEED,
ATTR_VALUE, ATTR_VALUE,
@ -42,6 +41,10 @@ from .const import (
DOMAIN, DOMAIN,
MAX_FORECAST_DAYS, MAX_FORECAST_DAYS,
) )
from .coordinator import (
AccuWeatherDailyForecastDataUpdateCoordinator,
AccuWeatherObservationDataUpdateCoordinator,
)
PARALLEL_UPDATES = 1 PARALLEL_UPDATES = 1
@ -52,12 +55,18 @@ class AccuWeatherSensorDescription(SensorEntityDescription):
value_fn: Callable[[dict[str, Any]], str | int | float | None] value_fn: Callable[[dict[str, Any]], str | int | float | None]
attr_fn: Callable[[dict[str, Any]], dict[str, Any]] = lambda _: {} attr_fn: Callable[[dict[str, Any]], dict[str, Any]] = lambda _: {}
day: int | None = None
FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( @dataclass(frozen=True, kw_only=True)
class AccuWeatherForecastSensorDescription(AccuWeatherSensorDescription):
"""Class describing AccuWeather sensor entities."""
day: int
FORECAST_SENSOR_TYPES: tuple[AccuWeatherForecastSensorDescription, ...] = (
*( *(
AccuWeatherSensorDescription( AccuWeatherForecastSensorDescription(
key="AirQuality", key="AirQuality",
icon="mdi:air-filter", icon="mdi:air-filter",
value_fn=lambda data: cast(str, data[ATTR_CATEGORY]), value_fn=lambda data: cast(str, data[ATTR_CATEGORY]),
@ -69,7 +78,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = (
for day in range(MAX_FORECAST_DAYS + 1) for day in range(MAX_FORECAST_DAYS + 1)
), ),
*( *(
AccuWeatherSensorDescription( AccuWeatherForecastSensorDescription(
key="CloudCoverDay", key="CloudCoverDay",
icon="mdi:weather-cloudy", icon="mdi:weather-cloudy",
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
@ -81,7 +90,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = (
for day in range(MAX_FORECAST_DAYS + 1) for day in range(MAX_FORECAST_DAYS + 1)
), ),
*( *(
AccuWeatherSensorDescription( AccuWeatherForecastSensorDescription(
key="CloudCoverNight", key="CloudCoverNight",
icon="mdi:weather-cloudy", icon="mdi:weather-cloudy",
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
@ -93,7 +102,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = (
for day in range(MAX_FORECAST_DAYS + 1) for day in range(MAX_FORECAST_DAYS + 1)
), ),
*( *(
AccuWeatherSensorDescription( AccuWeatherForecastSensorDescription(
key="Grass", key="Grass",
icon="mdi:grass", icon="mdi:grass",
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
@ -106,7 +115,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = (
for day in range(MAX_FORECAST_DAYS + 1) for day in range(MAX_FORECAST_DAYS + 1)
), ),
*( *(
AccuWeatherSensorDescription( AccuWeatherForecastSensorDescription(
key="HoursOfSun", key="HoursOfSun",
icon="mdi:weather-partly-cloudy", icon="mdi:weather-partly-cloudy",
native_unit_of_measurement=UnitOfTime.HOURS, native_unit_of_measurement=UnitOfTime.HOURS,
@ -117,7 +126,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = (
for day in range(MAX_FORECAST_DAYS + 1) for day in range(MAX_FORECAST_DAYS + 1)
), ),
*( *(
AccuWeatherSensorDescription( AccuWeatherForecastSensorDescription(
key="LongPhraseDay", key="LongPhraseDay",
value_fn=lambda data: cast(str, data), value_fn=lambda data: cast(str, data),
translation_key=f"condition_day_{day}d", translation_key=f"condition_day_{day}d",
@ -126,7 +135,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = (
for day in range(MAX_FORECAST_DAYS + 1) for day in range(MAX_FORECAST_DAYS + 1)
), ),
*( *(
AccuWeatherSensorDescription( AccuWeatherForecastSensorDescription(
key="LongPhraseNight", key="LongPhraseNight",
value_fn=lambda data: cast(str, data), value_fn=lambda data: cast(str, data),
translation_key=f"condition_night_{day}d", translation_key=f"condition_night_{day}d",
@ -135,7 +144,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = (
for day in range(MAX_FORECAST_DAYS + 1) for day in range(MAX_FORECAST_DAYS + 1)
), ),
*( *(
AccuWeatherSensorDescription( AccuWeatherForecastSensorDescription(
key="Mold", key="Mold",
icon="mdi:blur", icon="mdi:blur",
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
@ -148,7 +157,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = (
for day in range(MAX_FORECAST_DAYS + 1) for day in range(MAX_FORECAST_DAYS + 1)
), ),
*( *(
AccuWeatherSensorDescription( AccuWeatherForecastSensorDescription(
key="Ragweed", key="Ragweed",
icon="mdi:sprout", icon="mdi:sprout",
native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER,
@ -161,7 +170,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = (
for day in range(MAX_FORECAST_DAYS + 1) for day in range(MAX_FORECAST_DAYS + 1)
), ),
*( *(
AccuWeatherSensorDescription( AccuWeatherForecastSensorDescription(
key="RealFeelTemperatureMax", key="RealFeelTemperatureMax",
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS, native_unit_of_measurement=UnitOfTemperature.CELSIUS,
@ -172,7 +181,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = (
for day in range(MAX_FORECAST_DAYS + 1) for day in range(MAX_FORECAST_DAYS + 1)
), ),
*( *(
AccuWeatherSensorDescription( AccuWeatherForecastSensorDescription(
key="RealFeelTemperatureMin", key="RealFeelTemperatureMin",
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS, native_unit_of_measurement=UnitOfTemperature.CELSIUS,
@ -183,7 +192,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = (
for day in range(MAX_FORECAST_DAYS + 1) for day in range(MAX_FORECAST_DAYS + 1)
), ),
*( *(
AccuWeatherSensorDescription( AccuWeatherForecastSensorDescription(
key="RealFeelTemperatureShadeMax", key="RealFeelTemperatureShadeMax",
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
@ -195,7 +204,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = (
for day in range(MAX_FORECAST_DAYS + 1) for day in range(MAX_FORECAST_DAYS + 1)
), ),
*( *(
AccuWeatherSensorDescription( AccuWeatherForecastSensorDescription(
key="RealFeelTemperatureShadeMin", key="RealFeelTemperatureShadeMin",
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
@ -207,7 +216,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = (
for day in range(MAX_FORECAST_DAYS + 1) for day in range(MAX_FORECAST_DAYS + 1)
), ),
*( *(
AccuWeatherSensorDescription( AccuWeatherForecastSensorDescription(
key="SolarIrradianceDay", key="SolarIrradianceDay",
icon="mdi:weather-sunny", icon="mdi:weather-sunny",
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
@ -219,7 +228,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = (
for day in range(MAX_FORECAST_DAYS + 1) for day in range(MAX_FORECAST_DAYS + 1)
), ),
*( *(
AccuWeatherSensorDescription( AccuWeatherForecastSensorDescription(
key="SolarIrradianceNight", key="SolarIrradianceNight",
icon="mdi:weather-sunny", icon="mdi:weather-sunny",
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
@ -231,7 +240,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = (
for day in range(MAX_FORECAST_DAYS + 1) for day in range(MAX_FORECAST_DAYS + 1)
), ),
*( *(
AccuWeatherSensorDescription( AccuWeatherForecastSensorDescription(
key="ThunderstormProbabilityDay", key="ThunderstormProbabilityDay",
icon="mdi:weather-lightning", icon="mdi:weather-lightning",
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
@ -242,7 +251,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = (
for day in range(MAX_FORECAST_DAYS + 1) for day in range(MAX_FORECAST_DAYS + 1)
), ),
*( *(
AccuWeatherSensorDescription( AccuWeatherForecastSensorDescription(
key="ThunderstormProbabilityNight", key="ThunderstormProbabilityNight",
icon="mdi:weather-lightning", icon="mdi:weather-lightning",
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
@ -253,7 +262,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = (
for day in range(MAX_FORECAST_DAYS + 1) for day in range(MAX_FORECAST_DAYS + 1)
), ),
*( *(
AccuWeatherSensorDescription( AccuWeatherForecastSensorDescription(
key="Tree", key="Tree",
icon="mdi:tree-outline", icon="mdi:tree-outline",
native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER,
@ -266,7 +275,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = (
for day in range(MAX_FORECAST_DAYS + 1) for day in range(MAX_FORECAST_DAYS + 1)
), ),
*( *(
AccuWeatherSensorDescription( AccuWeatherForecastSensorDescription(
key="UVIndex", key="UVIndex",
icon="mdi:weather-sunny", icon="mdi:weather-sunny",
native_unit_of_measurement=UV_INDEX, native_unit_of_measurement=UV_INDEX,
@ -278,7 +287,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = (
for day in range(MAX_FORECAST_DAYS + 1) for day in range(MAX_FORECAST_DAYS + 1)
), ),
*( *(
AccuWeatherSensorDescription( AccuWeatherForecastSensorDescription(
key="WindGustDay", key="WindGustDay",
device_class=SensorDeviceClass.WIND_SPEED, device_class=SensorDeviceClass.WIND_SPEED,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
@ -291,7 +300,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = (
for day in range(MAX_FORECAST_DAYS + 1) for day in range(MAX_FORECAST_DAYS + 1)
), ),
*( *(
AccuWeatherSensorDescription( AccuWeatherForecastSensorDescription(
key="WindGustNight", key="WindGustNight",
device_class=SensorDeviceClass.WIND_SPEED, device_class=SensorDeviceClass.WIND_SPEED,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
@ -304,7 +313,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = (
for day in range(MAX_FORECAST_DAYS + 1) for day in range(MAX_FORECAST_DAYS + 1)
), ),
*( *(
AccuWeatherSensorDescription( AccuWeatherForecastSensorDescription(
key="WindDay", key="WindDay",
device_class=SensorDeviceClass.WIND_SPEED, device_class=SensorDeviceClass.WIND_SPEED,
native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR, native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
@ -316,7 +325,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = (
for day in range(MAX_FORECAST_DAYS + 1) for day in range(MAX_FORECAST_DAYS + 1)
), ),
*( *(
AccuWeatherSensorDescription( AccuWeatherForecastSensorDescription(
key="WindNight", key="WindNight",
device_class=SensorDeviceClass.WIND_SPEED, device_class=SensorDeviceClass.WIND_SPEED,
native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR, native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
@ -453,25 +462,33 @@ async def async_setup_entry(
) -> None: ) -> None:
"""Add AccuWeather entities from a config_entry.""" """Add AccuWeather entities from a config_entry."""
coordinator: AccuWeatherDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] accuweather_data: AccuWeatherData = hass.data[DOMAIN][entry.entry_id]
sensors = [ observation_coordinator: AccuWeatherObservationDataUpdateCoordinator = (
AccuWeatherSensor(coordinator, description) for description in SENSOR_TYPES accuweather_data.coordinator_observation
)
forecast_daily_coordinator: AccuWeatherDailyForecastDataUpdateCoordinator = (
accuweather_data.coordinator_daily_forecast
)
sensors: list[AccuWeatherSensor | AccuWeatherForecastSensor] = [
AccuWeatherSensor(observation_coordinator, description)
for description in SENSOR_TYPES
] ]
if coordinator.forecast: sensors.extend(
for description in FORECAST_SENSOR_TYPES: [
# Some air quality/allergy sensors are only available for certain AccuWeatherForecastSensor(forecast_daily_coordinator, description)
# locations. for description in FORECAST_SENSOR_TYPES
if description.key not in coordinator.data[ATTR_FORECAST][description.day]: if description.key in forecast_daily_coordinator.data[description.day]
continue ]
sensors.append(AccuWeatherSensor(coordinator, description)) )
async_add_entities(sensors) async_add_entities(sensors)
class AccuWeatherSensor( class AccuWeatherSensor(
CoordinatorEntity[AccuWeatherDataUpdateCoordinator], SensorEntity CoordinatorEntity[AccuWeatherObservationDataUpdateCoordinator], SensorEntity
): ):
"""Define an AccuWeather entity.""" """Define an AccuWeather entity."""
@ -481,22 +498,15 @@ class AccuWeatherSensor(
def __init__( def __init__(
self, self,
coordinator: AccuWeatherDataUpdateCoordinator, coordinator: AccuWeatherObservationDataUpdateCoordinator,
description: AccuWeatherSensorDescription, description: AccuWeatherSensorDescription,
) -> None: ) -> None:
"""Initialize.""" """Initialize."""
super().__init__(coordinator) super().__init__(coordinator)
self.forecast_day = description.day
self.entity_description = description self.entity_description = description
self._sensor_data = _get_sensor_data( self._sensor_data = self._get_sensor_data(coordinator.data, description.key)
coordinator.data, description.key, self.forecast_day self._attr_unique_id = f"{coordinator.location_key}-{description.key}".lower()
)
if self.forecast_day is not None:
self._attr_unique_id = f"{coordinator.location_key}-{description.key}-{self.forecast_day}".lower()
else:
self._attr_unique_id = (
f"{coordinator.location_key}-{description.key}".lower()
)
self._attr_device_info = coordinator.device_info self._attr_device_info = coordinator.device_info
@property @property
@ -507,30 +517,78 @@ class AccuWeatherSensor(
@property @property
def extra_state_attributes(self) -> dict[str, Any]: def extra_state_attributes(self) -> dict[str, Any]:
"""Return the state attributes.""" """Return the state attributes."""
if self.forecast_day is not None:
return self.entity_description.attr_fn(self._sensor_data)
return self.entity_description.attr_fn(self.coordinator.data) return self.entity_description.attr_fn(self.coordinator.data)
@callback @callback
def _handle_coordinator_update(self) -> None: def _handle_coordinator_update(self) -> None:
"""Handle data update.""" """Handle data update."""
self._sensor_data = _get_sensor_data( self._sensor_data = self._get_sensor_data(
self.coordinator.data, self.entity_description.key
)
self.async_write_ha_state()
@staticmethod
def _get_sensor_data(
sensors: dict[str, Any],
kind: str,
) -> Any:
"""Get sensor data."""
if kind == "Precipitation":
return sensors["PrecipitationSummary"]["PastHour"]
return sensors[kind]
class AccuWeatherForecastSensor(
CoordinatorEntity[AccuWeatherDailyForecastDataUpdateCoordinator], SensorEntity
):
"""Define an AccuWeather entity."""
_attr_attribution = ATTRIBUTION
_attr_has_entity_name = True
entity_description: AccuWeatherForecastSensorDescription
def __init__(
self,
coordinator: AccuWeatherDailyForecastDataUpdateCoordinator,
description: AccuWeatherForecastSensorDescription,
) -> None:
"""Initialize."""
super().__init__(coordinator)
self.forecast_day = description.day
self.entity_description = description
self._sensor_data = self._get_sensor_data(
coordinator.data, description.key, self.forecast_day
)
self._attr_unique_id = (
f"{coordinator.location_key}-{description.key}-{self.forecast_day}".lower()
)
self._attr_device_info = coordinator.device_info
@property
def native_value(self) -> str | int | float | None:
"""Return the state."""
return self.entity_description.value_fn(self._sensor_data)
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return the state attributes."""
return self.entity_description.attr_fn(self._sensor_data)
@callback
def _handle_coordinator_update(self) -> None:
"""Handle data update."""
self._sensor_data = self._get_sensor_data(
self.coordinator.data, self.entity_description.key, self.forecast_day self.coordinator.data, self.entity_description.key, self.forecast_day
) )
self.async_write_ha_state() self.async_write_ha_state()
@staticmethod
def _get_sensor_data( def _get_sensor_data(
sensors: dict[str, Any], sensors: list[dict[str, Any]],
kind: str, kind: str,
forecast_day: int | None = None, forecast_day: int,
) -> Any: ) -> Any:
"""Get sensor data.""" """Get sensor data."""
if forecast_day is not None: return sensors[forecast_day][kind]
return sensors[ATTR_FORECAST][forecast_day][kind]
if kind == "Precipitation":
return sensors["PrecipitationSummary"]["PastHour"]
return sensors[kind]

View File

@ -11,7 +11,7 @@
} }
}, },
"create_entry": { "create_entry": {
"default": "Some sensors are not enabled by default. You can enable them in the entity registry after the integration configuration.\nWeather forecast is not enabled by default. You can enable it in the integration options." "default": "Some sensors are not enabled by default. You can enable them in the entity registry after the integration configuration."
}, },
"error": { "error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
@ -790,16 +790,6 @@
} }
} }
}, },
"options": {
"step": {
"init": {
"description": "Due to the limitations of the free version of the AccuWeather API key, when you enable weather forecast, data updates will be performed every 80 minutes instead of every 40 minutes.",
"data": {
"forecast": "Weather forecast"
}
}
}
},
"system_health": { "system_health": {
"info": { "info": {
"can_reach_server": "Reach AccuWeather server", "can_reach_server": "Reach AccuWeather server",

View File

@ -17,8 +17,8 @@ from homeassistant.components.weather import (
ATTR_FORECAST_TIME, ATTR_FORECAST_TIME,
ATTR_FORECAST_UV_INDEX, ATTR_FORECAST_UV_INDEX,
ATTR_FORECAST_WIND_BEARING, ATTR_FORECAST_WIND_BEARING,
CoordinatorWeatherEntity,
Forecast, Forecast,
SingleCoordinatorWeatherEntity,
WeatherEntityFeature, WeatherEntityFeature,
) )
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
@ -31,19 +31,23 @@ from homeassistant.const import (
) )
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import TimestampDataUpdateCoordinator
from homeassistant.util.dt import utc_from_timestamp from homeassistant.util.dt import utc_from_timestamp
from . import AccuWeatherDataUpdateCoordinator from . import AccuWeatherData
from .const import ( from .const import (
API_METRIC, API_METRIC,
ATTR_DIRECTION, ATTR_DIRECTION,
ATTR_FORECAST,
ATTR_SPEED, ATTR_SPEED,
ATTR_VALUE, ATTR_VALUE,
ATTRIBUTION, ATTRIBUTION,
CONDITION_MAP, CONDITION_MAP,
DOMAIN, DOMAIN,
) )
from .coordinator import (
AccuWeatherDailyForecastDataUpdateCoordinator,
AccuWeatherObservationDataUpdateCoordinator,
)
PARALLEL_UPDATES = 1 PARALLEL_UPDATES = 1
@ -52,106 +56,134 @@ async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None: ) -> None:
"""Add a AccuWeather weather entity from a config_entry.""" """Add a AccuWeather weather entity from a config_entry."""
accuweather_data: AccuWeatherData = hass.data[DOMAIN][entry.entry_id]
coordinator: AccuWeatherDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] async_add_entities([AccuWeatherEntity(accuweather_data)])
async_add_entities([AccuWeatherEntity(coordinator)])
class AccuWeatherEntity( class AccuWeatherEntity(
SingleCoordinatorWeatherEntity[AccuWeatherDataUpdateCoordinator] CoordinatorWeatherEntity[
AccuWeatherObservationDataUpdateCoordinator,
AccuWeatherDailyForecastDataUpdateCoordinator,
TimestampDataUpdateCoordinator,
TimestampDataUpdateCoordinator,
]
): ):
"""Define an AccuWeather entity.""" """Define an AccuWeather entity."""
_attr_has_entity_name = True _attr_has_entity_name = True
_attr_name = None _attr_name = None
def __init__(self, coordinator: AccuWeatherDataUpdateCoordinator) -> None: def __init__(self, accuweather_data: AccuWeatherData) -> None:
"""Initialize.""" """Initialize."""
super().__init__(coordinator) super().__init__(
observation_coordinator=accuweather_data.coordinator_observation,
daily_coordinator=accuweather_data.coordinator_daily_forecast,
)
self._attr_native_precipitation_unit = UnitOfPrecipitationDepth.MILLIMETERS self._attr_native_precipitation_unit = UnitOfPrecipitationDepth.MILLIMETERS
self._attr_native_pressure_unit = UnitOfPressure.HPA self._attr_native_pressure_unit = UnitOfPressure.HPA
self._attr_native_temperature_unit = UnitOfTemperature.CELSIUS self._attr_native_temperature_unit = UnitOfTemperature.CELSIUS
self._attr_native_visibility_unit = UnitOfLength.KILOMETERS self._attr_native_visibility_unit = UnitOfLength.KILOMETERS
self._attr_native_wind_speed_unit = UnitOfSpeed.KILOMETERS_PER_HOUR self._attr_native_wind_speed_unit = UnitOfSpeed.KILOMETERS_PER_HOUR
self._attr_unique_id = coordinator.location_key self._attr_unique_id = accuweather_data.coordinator_observation.location_key
self._attr_attribution = ATTRIBUTION self._attr_attribution = ATTRIBUTION
self._attr_device_info = coordinator.device_info self._attr_device_info = accuweather_data.coordinator_observation.device_info
if self.coordinator.forecast: self._attr_supported_features = WeatherEntityFeature.FORECAST_DAILY
self._attr_supported_features = WeatherEntityFeature.FORECAST_DAILY
self.observation_coordinator = accuweather_data.coordinator_observation
self.daily_coordinator = accuweather_data.coordinator_daily_forecast
@property @property
def condition(self) -> str | None: def condition(self) -> str | None:
"""Return the current condition.""" """Return the current condition."""
return CONDITION_MAP.get(self.coordinator.data["WeatherIcon"]) return CONDITION_MAP.get(self.observation_coordinator.data["WeatherIcon"])
@property @property
def cloud_coverage(self) -> float: def cloud_coverage(self) -> float:
"""Return the Cloud coverage in %.""" """Return the Cloud coverage in %."""
return cast(float, self.coordinator.data["CloudCover"]) return cast(float, self.observation_coordinator.data["CloudCover"])
@property @property
def native_apparent_temperature(self) -> float: def native_apparent_temperature(self) -> float:
"""Return the apparent temperature.""" """Return the apparent temperature."""
return cast( return cast(
float, self.coordinator.data["ApparentTemperature"][API_METRIC][ATTR_VALUE] float,
self.observation_coordinator.data["ApparentTemperature"][API_METRIC][
ATTR_VALUE
],
) )
@property @property
def native_temperature(self) -> float: def native_temperature(self) -> float:
"""Return the temperature.""" """Return the temperature."""
return cast(float, self.coordinator.data["Temperature"][API_METRIC][ATTR_VALUE]) return cast(
float,
self.observation_coordinator.data["Temperature"][API_METRIC][ATTR_VALUE],
)
@property @property
def native_pressure(self) -> float: def native_pressure(self) -> float:
"""Return the pressure.""" """Return the pressure."""
return cast(float, self.coordinator.data["Pressure"][API_METRIC][ATTR_VALUE]) return cast(
float, self.observation_coordinator.data["Pressure"][API_METRIC][ATTR_VALUE]
)
@property @property
def native_dew_point(self) -> float: def native_dew_point(self) -> float:
"""Return the dew point.""" """Return the dew point."""
return cast(float, self.coordinator.data["DewPoint"][API_METRIC][ATTR_VALUE]) return cast(
float, self.observation_coordinator.data["DewPoint"][API_METRIC][ATTR_VALUE]
)
@property @property
def humidity(self) -> int: def humidity(self) -> int:
"""Return the humidity.""" """Return the humidity."""
return cast(int, self.coordinator.data["RelativeHumidity"]) return cast(int, self.observation_coordinator.data["RelativeHumidity"])
@property @property
def native_wind_gust_speed(self) -> float: def native_wind_gust_speed(self) -> float:
"""Return the wind gust speed.""" """Return the wind gust speed."""
return cast( return cast(
float, self.coordinator.data["WindGust"][ATTR_SPEED][API_METRIC][ATTR_VALUE] float,
self.observation_coordinator.data["WindGust"][ATTR_SPEED][API_METRIC][
ATTR_VALUE
],
) )
@property @property
def native_wind_speed(self) -> float: def native_wind_speed(self) -> float:
"""Return the wind speed.""" """Return the wind speed."""
return cast( return cast(
float, self.coordinator.data["Wind"][ATTR_SPEED][API_METRIC][ATTR_VALUE] float,
self.observation_coordinator.data["Wind"][ATTR_SPEED][API_METRIC][
ATTR_VALUE
],
) )
@property @property
def wind_bearing(self) -> int: def wind_bearing(self) -> int:
"""Return the wind bearing.""" """Return the wind bearing."""
return cast(int, self.coordinator.data["Wind"][ATTR_DIRECTION]["Degrees"]) return cast(
int, self.observation_coordinator.data["Wind"][ATTR_DIRECTION]["Degrees"]
)
@property @property
def native_visibility(self) -> float: def native_visibility(self) -> float:
"""Return the visibility.""" """Return the visibility."""
return cast(float, self.coordinator.data["Visibility"][API_METRIC][ATTR_VALUE]) return cast(
float,
self.observation_coordinator.data["Visibility"][API_METRIC][ATTR_VALUE],
)
@property @property
def uv_index(self) -> float: def uv_index(self) -> float:
"""Return the UV index.""" """Return the UV index."""
return cast(float, self.coordinator.data["UVIndex"]) return cast(float, self.observation_coordinator.data["UVIndex"])
@callback @callback
def _async_forecast_daily(self) -> list[Forecast] | None: def _async_forecast_daily(self) -> list[Forecast] | None:
"""Return the daily forecast in native units.""" """Return the daily forecast in native units."""
if not self.coordinator.forecast:
return None
# remap keys from library to keys understood by the weather component
return [ return [
{ {
ATTR_FORECAST_TIME: utc_from_timestamp(item["EpochDate"]).isoformat(), ATTR_FORECAST_TIME: utc_from_timestamp(item["EpochDate"]).isoformat(),
@ -175,5 +207,5 @@ class AccuWeatherEntity(
ATTR_FORECAST_WIND_BEARING: item["WindDay"][ATTR_DIRECTION]["Degrees"], ATTR_FORECAST_WIND_BEARING: item["WindDay"][ATTR_DIRECTION]["Degrees"],
ATTR_FORECAST_CONDITION: CONDITION_MAP.get(item["IconDay"]), ATTR_FORECAST_CONDITION: CONDITION_MAP.get(item["IconDay"]),
} }
for item in self.coordinator.data[ATTR_FORECAST] for item in self.daily_coordinator.data
] ]

View File

@ -11,14 +11,8 @@ from tests.common import (
) )
async def init_integration( async def init_integration(hass, unsupported_icon=False) -> MockConfigEntry:
hass, forecast=False, unsupported_icon=False
) -> MockConfigEntry:
"""Set up the AccuWeather integration in Home Assistant.""" """Set up the AccuWeather integration in Home Assistant."""
options = {}
if forecast:
options["forecast"] = True
entry = MockConfigEntry( entry = MockConfigEntry(
domain=DOMAIN, domain=DOMAIN,
title="Home", title="Home",
@ -29,7 +23,6 @@ async def init_integration(
"longitude": 122.12, "longitude": 122.12,
"name": "Home", "name": "Home",
}, },
options=options,
) )
current = load_json_object_fixture("accuweather/current_conditions_data.json") current = load_json_object_fixture("accuweather/current_conditions_data.json")

View File

@ -7,7 +7,7 @@
'longitude': '**REDACTED**', 'longitude': '**REDACTED**',
'name': 'Home', 'name': 'Home',
}), }),
'coordinator_data': dict({ 'observation_data': dict({
'ApparentTemperature': dict({ 'ApparentTemperature': dict({
'Imperial': dict({ 'Imperial': dict({
'Unit': 'F', 'Unit': 'F',
@ -297,8 +297,6 @@
}), }),
}), }),
}), }),
'forecast': list([
]),
}), }),
}) })
# --- # ---

File diff suppressed because it is too large Load Diff

View File

@ -1,10 +1,10 @@
"""Define tests for the AccuWeather config flow.""" """Define tests for the AccuWeather config flow."""
from unittest.mock import PropertyMock, patch from unittest.mock import patch
from accuweather import ApiError, InvalidApiKeyError, RequestsExceededError from accuweather import ApiError, InvalidApiKeyError, RequestsExceededError
from homeassistant.components.accuweather.const import CONF_FORECAST, DOMAIN from homeassistant.components.accuweather.const import DOMAIN
from homeassistant.config_entries import SOURCE_USER from homeassistant.config_entries import SOURCE_USER
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
@ -140,52 +140,3 @@ async def test_create_entry(hass: HomeAssistant) -> None:
assert result["data"][CONF_LATITUDE] == 55.55 assert result["data"][CONF_LATITUDE] == 55.55
assert result["data"][CONF_LONGITUDE] == 122.12 assert result["data"][CONF_LONGITUDE] == 122.12
assert result["data"][CONF_API_KEY] == "32-character-string-1234567890qw" assert result["data"][CONF_API_KEY] == "32-character-string-1234567890qw"
async def test_options_flow(hass: HomeAssistant) -> None:
"""Test config flow options."""
config_entry = MockConfigEntry(
domain=DOMAIN,
unique_id="123456",
data=VALID_CONFIG,
)
config_entry.add_to_hass(hass)
with (
patch(
"homeassistant.components.accuweather.AccuWeather._async_get_data",
return_value=load_json_object_fixture("accuweather/location_data.json"),
),
patch(
"homeassistant.components.accuweather.AccuWeather.async_get_current_conditions",
return_value=load_json_object_fixture(
"accuweather/current_conditions_data.json"
),
),
patch(
"homeassistant.components.accuweather.AccuWeather.async_get_daily_forecast"
),
patch(
"homeassistant.components.accuweather.AccuWeather.requests_remaining",
new_callable=PropertyMock,
return_value=10,
),
):
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
result = await hass.config_entries.options.async_init(config_entry.entry_id)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "init"
result = await hass.config_entries.options.async_configure(
result["flow_id"], user_input={CONF_FORECAST: True}
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert config_entry.options == {CONF_FORECAST: True}
await hass.async_block_till_done()
assert await hass.config_entries.async_unload(config_entry.entry_id)
await hass.async_block_till_done()

View File

@ -6,7 +6,6 @@ from homeassistant.core import HomeAssistant
from . import init_integration from . import init_integration
from tests.common import load_json_object_fixture
from tests.components.diagnostics import get_diagnostics_for_config_entry from tests.components.diagnostics import get_diagnostics_for_config_entry
from tests.typing import ClientSessionGenerator from tests.typing import ClientSessionGenerator
@ -19,12 +18,6 @@ async def test_entry_diagnostics(
"""Test config entry diagnostics.""" """Test config entry diagnostics."""
entry = await init_integration(hass) entry = await init_integration(hass)
coordinator_data = load_json_object_fixture(
"current_conditions_data.json", "accuweather"
)
coordinator_data["forecast"] = []
result = await get_diagnostics_for_config_entry(hass, hass_client, entry) result = await get_diagnostics_for_config_entry(hass, hass_client, entry)
assert result == snapshot assert result == snapshot

View File

@ -1,11 +1,14 @@
"""Test init of AccuWeather integration.""" """Test init of AccuWeather integration."""
from datetime import timedelta
from unittest.mock import patch from unittest.mock import patch
from accuweather import ApiError from accuweather import ApiError
from homeassistant.components.accuweather.const import DOMAIN from homeassistant.components.accuweather.const import (
DOMAIN,
UPDATE_INTERVAL_DAILY_FORECAST,
UPDATE_INTERVAL_OBSERVATION,
)
from homeassistant.components.sensor import DOMAIN as SENSOR_PLATFORM from homeassistant.components.sensor import DOMAIN as SENSOR_PLATFORM
from homeassistant.config_entries import ConfigEntryState from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import STATE_UNAVAILABLE from homeassistant.const import STATE_UNAVAILABLE
@ -76,30 +79,8 @@ async def test_update_interval(hass: HomeAssistant) -> None:
assert entry.state is ConfigEntryState.LOADED assert entry.state is ConfigEntryState.LOADED
current = load_json_object_fixture("accuweather/current_conditions_data.json")
future = utcnow() + timedelta(minutes=40)
with patch(
"homeassistant.components.accuweather.AccuWeather.async_get_current_conditions",
return_value=current,
) as mock_current:
assert mock_current.call_count == 0
async_fire_time_changed(hass, future)
await hass.async_block_till_done()
assert mock_current.call_count == 1
async def test_update_interval_forecast(hass: HomeAssistant) -> None:
"""Test correct update interval when forecast is True."""
entry = await init_integration(hass, forecast=True)
assert entry.state is ConfigEntryState.LOADED
current = load_json_object_fixture("accuweather/current_conditions_data.json") current = load_json_object_fixture("accuweather/current_conditions_data.json")
forecast = load_json_array_fixture("accuweather/forecast_data.json") forecast = load_json_array_fixture("accuweather/forecast_data.json")
future = utcnow() + timedelta(minutes=80)
with ( with (
patch( patch(
@ -114,10 +95,14 @@ async def test_update_interval_forecast(hass: HomeAssistant) -> None:
assert mock_current.call_count == 0 assert mock_current.call_count == 0
assert mock_forecast.call_count == 0 assert mock_forecast.call_count == 0
async_fire_time_changed(hass, future) async_fire_time_changed(hass, utcnow() + UPDATE_INTERVAL_OBSERVATION)
await hass.async_block_till_done() await hass.async_block_till_done()
assert mock_current.call_count == 1 assert mock_current.call_count == 1
async_fire_time_changed(hass, utcnow() + UPDATE_INTERVAL_DAILY_FORECAST)
await hass.async_block_till_done()
assert mock_forecast.call_count == 1 assert mock_forecast.call_count == 1

View File

@ -3,29 +3,20 @@
from datetime import timedelta from datetime import timedelta
from unittest.mock import PropertyMock, patch from unittest.mock import PropertyMock, patch
from homeassistant.components.accuweather.const import ATTRIBUTION from accuweather import ApiError, InvalidApiKeyError, RequestsExceededError
from homeassistant.components.sensor import ( from aiohttp.client_exceptions import ClientConnectorError
ATTR_OPTIONS, import pytest
ATTR_STATE_CLASS, from syrupy import SnapshotAssertion
SensorDeviceClass,
SensorStateClass, from homeassistant.components.accuweather.const import UPDATE_INTERVAL_DAILY_FORECAST
)
from homeassistant.const import ( from homeassistant.const import (
ATTR_ATTRIBUTION,
ATTR_DEVICE_CLASS,
ATTR_ENTITY_ID, ATTR_ENTITY_ID,
ATTR_ICON,
ATTR_UNIT_OF_MEASUREMENT, ATTR_UNIT_OF_MEASUREMENT,
CONCENTRATION_PARTS_PER_CUBIC_METER,
PERCENTAGE,
STATE_UNAVAILABLE, STATE_UNAVAILABLE,
UV_INDEX, Platform,
UnitOfIrradiance,
UnitOfLength, UnitOfLength,
UnitOfSpeed, UnitOfSpeed,
UnitOfTemperature, UnitOfTemperature,
UnitOfTime,
UnitOfVolumetricFlux,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
@ -42,517 +33,23 @@ from tests.common import (
) )
async def test_sensor_without_forecast( async def test_sensor(
hass: HomeAssistant, hass: HomeAssistant,
entity_registry_enabled_by_default: None, entity_registry_enabled_by_default: None,
entity_registry: er.EntityRegistry, entity_registry: er.EntityRegistry,
snapshot: SnapshotAssertion,
) -> None: ) -> None:
"""Test states of the sensor without forecast.""" """Test states of the sensor."""
await init_integration(hass) with patch("homeassistant.components.accuweather.PLATFORMS", [Platform.SENSOR]):
entry = await init_integration(hass)
state = hass.states.get("sensor.home_cloud_ceiling") entity_entries = er.async_entries_for_config_entry(entity_registry, entry.entry_id)
assert state
assert state.state == "3200.0"
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert state.attributes.get(ATTR_ICON) == "mdi:weather-fog"
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfLength.METERS
assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT
assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.DISTANCE
entry = entity_registry.async_get("sensor.home_cloud_ceiling") assert entity_entries
assert entry for entity_entry in entity_entries:
assert entry.unique_id == "0123456-ceiling" assert entity_entry == snapshot(name=f"{entity_entry.entity_id}-entry")
assert entry.options["sensor"] == {"suggested_display_precision": 0} assert (state := hass.states.get(entity_entry.entity_id))
assert state == snapshot(name=f"{entity_entry.entity_id}-state")
state = hass.states.get("sensor.home_precipitation")
assert state
assert state.state == "0.0"
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert (
state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
== UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR
)
assert state.attributes.get(ATTR_ICON) is None
assert state.attributes.get("type") is None
assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT
assert (
state.attributes.get(ATTR_DEVICE_CLASS)
== SensorDeviceClass.PRECIPITATION_INTENSITY
)
entry = entity_registry.async_get("sensor.home_precipitation")
assert entry
assert entry.unique_id == "0123456-precipitation"
state = hass.states.get("sensor.home_pressure_tendency")
assert state
assert state.state == "falling"
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert state.attributes.get(ATTR_ICON) == "mdi:gauge"
assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENUM
assert state.attributes.get(ATTR_STATE_CLASS) is None
assert state.attributes.get(ATTR_OPTIONS) == ["falling", "rising", "steady"]
entry = entity_registry.async_get("sensor.home_pressure_tendency")
assert entry
assert entry.unique_id == "0123456-pressuretendency"
assert entry.translation_key == "pressure_tendency"
state = hass.states.get("sensor.home_realfeel_temperature")
assert state
assert state.state == "25.1"
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS
assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE
assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT
entry = entity_registry.async_get("sensor.home_realfeel_temperature")
assert entry
assert entry.unique_id == "0123456-realfeeltemperature"
state = hass.states.get("sensor.home_uv_index")
assert state
assert state.state == "6"
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UV_INDEX
assert state.attributes.get("level") == "High"
assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT
entry = entity_registry.async_get("sensor.home_uv_index")
assert entry
assert entry.unique_id == "0123456-uvindex"
state = hass.states.get("sensor.home_apparent_temperature")
assert state
assert state.state == "22.8"
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS
assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE
assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT
entry = entity_registry.async_get("sensor.home_apparent_temperature")
assert entry
assert entry.unique_id == "0123456-apparenttemperature"
state = hass.states.get("sensor.home_cloud_cover")
assert state
assert state.state == "10"
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE
assert state.attributes.get(ATTR_ICON) == "mdi:weather-cloudy"
assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT
entry = entity_registry.async_get("sensor.home_cloud_cover")
assert entry
assert entry.unique_id == "0123456-cloudcover"
state = hass.states.get("sensor.home_dew_point")
assert state
assert state.state == "16.2"
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS
assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE
assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT
entry = entity_registry.async_get("sensor.home_dew_point")
assert entry
assert entry.unique_id == "0123456-dewpoint"
state = hass.states.get("sensor.home_realfeel_temperature_shade")
assert state
assert state.state == "21.1"
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS
assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE
assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT
entry = entity_registry.async_get("sensor.home_realfeel_temperature_shade")
assert entry
assert entry.unique_id == "0123456-realfeeltemperatureshade"
state = hass.states.get("sensor.home_wet_bulb_temperature")
assert state
assert state.state == "18.6"
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS
assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE
assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT
entry = entity_registry.async_get("sensor.home_wet_bulb_temperature")
assert entry
assert entry.unique_id == "0123456-wetbulbtemperature"
state = hass.states.get("sensor.home_wind_chill_temperature")
assert state
assert state.state == "22.8"
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS
assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE
assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT
entry = entity_registry.async_get("sensor.home_wind_chill_temperature")
assert entry
assert entry.unique_id == "0123456-windchilltemperature"
state = hass.states.get("sensor.home_wind_gust_speed")
assert state
assert state.state == "20.3"
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert (
state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
== UnitOfSpeed.KILOMETERS_PER_HOUR
)
assert state.attributes.get(ATTR_ICON) is None
assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT
assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.WIND_SPEED
entry = entity_registry.async_get("sensor.home_wind_gust_speed")
assert entry
assert entry.unique_id == "0123456-windgust"
state = hass.states.get("sensor.home_wind_speed")
assert state
assert state.state == "14.5"
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert (
state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
== UnitOfSpeed.KILOMETERS_PER_HOUR
)
assert state.attributes.get(ATTR_ICON) is None
assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT
assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.WIND_SPEED
entry = entity_registry.async_get("sensor.home_wind_speed")
assert entry
assert entry.unique_id == "0123456-wind"
async def test_sensor_with_forecast(
hass: HomeAssistant,
entity_registry_enabled_by_default: None,
entity_registry: er.EntityRegistry,
) -> None:
"""Test states of the sensor with forecast."""
await init_integration(hass, forecast=True)
state = hass.states.get("sensor.home_hours_of_sun_today")
assert state
assert state.state == "7.2"
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert state.attributes.get(ATTR_ICON) == "mdi:weather-partly-cloudy"
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTime.HOURS
assert state.attributes.get(ATTR_STATE_CLASS) is None
entry = entity_registry.async_get("sensor.home_hours_of_sun_today")
assert entry
assert entry.unique_id == "0123456-hoursofsun-0"
state = hass.states.get("sensor.home_realfeel_temperature_max_today")
assert state
assert state.state == "29.8"
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS
assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE
assert state.attributes.get(ATTR_STATE_CLASS) is None
entry = entity_registry.async_get("sensor.home_realfeel_temperature_max_today")
assert entry
state = hass.states.get("sensor.home_realfeel_temperature_min_today")
assert state
assert state.state == "15.1"
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS
assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE
assert state.attributes.get(ATTR_STATE_CLASS) is None
entry = entity_registry.async_get("sensor.home_realfeel_temperature_min_today")
assert entry
assert entry.unique_id == "0123456-realfeeltemperaturemin-0"
state = hass.states.get("sensor.home_thunderstorm_probability_today")
assert state
assert state.state == "40"
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert state.attributes.get(ATTR_ICON) == "mdi:weather-lightning"
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE
assert state.attributes.get(ATTR_STATE_CLASS) is None
entry = entity_registry.async_get("sensor.home_thunderstorm_probability_today")
assert entry
assert entry.unique_id == "0123456-thunderstormprobabilityday-0"
state = hass.states.get("sensor.home_thunderstorm_probability_tonight")
assert state
assert state.state == "40"
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert state.attributes.get(ATTR_ICON) == "mdi:weather-lightning"
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE
assert state.attributes.get(ATTR_STATE_CLASS) is None
entry = entity_registry.async_get("sensor.home_thunderstorm_probability_tonight")
assert entry
assert entry.unique_id == "0123456-thunderstormprobabilitynight-0"
state = hass.states.get("sensor.home_uv_index_today")
assert state
assert state.state == "5"
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert state.attributes.get(ATTR_ICON) == "mdi:weather-sunny"
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UV_INDEX
assert state.attributes.get("level") == "moderate"
assert state.attributes.get(ATTR_STATE_CLASS) is None
entry = entity_registry.async_get("sensor.home_uv_index_today")
assert entry
assert entry.unique_id == "0123456-uvindex-0"
state = hass.states.get("sensor.home_air_quality_today")
assert state
assert state.state == "good"
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert state.attributes.get(ATTR_ICON) == "mdi:air-filter"
assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENUM
assert state.attributes.get(ATTR_OPTIONS) == [
"good",
"hazardous",
"high",
"low",
"moderate",
"unhealthy",
]
state = hass.states.get("sensor.home_cloud_cover_today")
assert state
assert state.state == "58"
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE
assert state.attributes.get(ATTR_ICON) == "mdi:weather-cloudy"
assert state.attributes.get(ATTR_STATE_CLASS) is None
entry = entity_registry.async_get("sensor.home_cloud_cover_today")
assert entry
assert entry.unique_id == "0123456-cloudcoverday-0"
state = hass.states.get("sensor.home_cloud_cover_tonight")
assert state
assert state.state == "65"
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE
assert state.attributes.get(ATTR_ICON) == "mdi:weather-cloudy"
assert state.attributes.get(ATTR_STATE_CLASS) is None
entry = entity_registry.async_get("sensor.home_cloud_cover_tonight")
assert entry
state = hass.states.get("sensor.home_grass_pollen_today")
assert state
assert state.state == "0"
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert (
state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
== CONCENTRATION_PARTS_PER_CUBIC_METER
)
assert state.attributes.get("level") == "low"
assert state.attributes.get(ATTR_ICON) == "mdi:grass"
assert state.attributes.get(ATTR_STATE_CLASS) is None
entry = entity_registry.async_get("sensor.home_grass_pollen_today")
assert entry
assert entry.unique_id == "0123456-grass-0"
state = hass.states.get("sensor.home_mold_pollen_today")
assert state
assert state.state == "0"
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert (
state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
== CONCENTRATION_PARTS_PER_CUBIC_METER
)
assert state.attributes.get("level") == "low"
assert state.attributes.get(ATTR_ICON) == "mdi:blur"
entry = entity_registry.async_get("sensor.home_mold_pollen_today")
assert entry
assert entry.unique_id == "0123456-mold-0"
state = hass.states.get("sensor.home_ragweed_pollen_today")
assert state
assert state.state == "0"
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert (
state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
== CONCENTRATION_PARTS_PER_CUBIC_METER
)
assert state.attributes.get("level") == "low"
assert state.attributes.get(ATTR_ICON) == "mdi:sprout"
entry = entity_registry.async_get("sensor.home_ragweed_pollen_today")
assert entry
assert entry.unique_id == "0123456-ragweed-0"
state = hass.states.get("sensor.home_realfeel_temperature_shade_max_today")
assert state
assert state.state == "28.0"
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS
assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE
assert state.attributes.get(ATTR_STATE_CLASS) is None
entry = entity_registry.async_get(
"sensor.home_realfeel_temperature_shade_max_today"
)
assert entry
assert entry.unique_id == "0123456-realfeeltemperatureshademax-0"
state = hass.states.get("sensor.home_realfeel_temperature_shade_min_today")
assert state
assert state.state == "15.1"
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS
assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE
entry = entity_registry.async_get(
"sensor.home_realfeel_temperature_shade_min_today"
)
assert entry
assert entry.unique_id == "0123456-realfeeltemperatureshademin-0"
state = hass.states.get("sensor.home_tree_pollen_today")
assert state
assert state.state == "0"
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert (
state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
== CONCENTRATION_PARTS_PER_CUBIC_METER
)
assert state.attributes.get("level") == "low"
assert state.attributes.get(ATTR_ICON) == "mdi:tree-outline"
assert state.attributes.get(ATTR_STATE_CLASS) is None
entry = entity_registry.async_get("sensor.home_tree_pollen_today")
assert entry
assert entry.unique_id == "0123456-tree-0"
state = hass.states.get("sensor.home_wind_speed_today")
assert state
assert state.state == "13.0"
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert (
state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
== UnitOfSpeed.KILOMETERS_PER_HOUR
)
assert state.attributes.get("direction") == "SSE"
assert state.attributes.get(ATTR_ICON) is None
assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.WIND_SPEED
entry = entity_registry.async_get("sensor.home_wind_speed_today")
assert entry
assert entry.unique_id == "0123456-windday-0"
state = hass.states.get("sensor.home_wind_speed_tonight")
assert state
assert state.state == "7.4"
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert (
state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
== UnitOfSpeed.KILOMETERS_PER_HOUR
)
assert state.attributes.get("direction") == "WNW"
assert state.attributes.get(ATTR_ICON) is None
assert state.attributes.get(ATTR_STATE_CLASS) is None
assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.WIND_SPEED
entry = entity_registry.async_get("sensor.home_wind_speed_tonight")
assert entry
assert entry.unique_id == "0123456-windnight-0"
state = hass.states.get("sensor.home_wind_gust_speed_today")
assert state
assert state.state == "29.6"
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert (
state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
== UnitOfSpeed.KILOMETERS_PER_HOUR
)
assert state.attributes.get("direction") == "S"
assert state.attributes.get(ATTR_ICON) is None
assert state.attributes.get(ATTR_STATE_CLASS) is None
assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.WIND_SPEED
entry = entity_registry.async_get("sensor.home_wind_gust_speed_today")
assert entry
assert entry.unique_id == "0123456-windgustday-0"
state = hass.states.get("sensor.home_wind_gust_speed_tonight")
assert state
assert state.state == "18.5"
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert (
state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
== UnitOfSpeed.KILOMETERS_PER_HOUR
)
assert state.attributes.get("direction") == "WSW"
assert state.attributes.get(ATTR_ICON) is None
assert state.attributes.get(ATTR_STATE_CLASS) is None
assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.WIND_SPEED
entry = entity_registry.async_get("sensor.home_wind_gust_speed_tonight")
assert entry
assert entry.unique_id == "0123456-windgustnight-0"
entry = entity_registry.async_get("sensor.home_air_quality_today")
assert entry
assert entry.unique_id == "0123456-airquality-0"
state = hass.states.get("sensor.home_solar_irradiance_today")
assert state
assert state.state == "7447.1"
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert state.attributes.get(ATTR_ICON) == "mdi:weather-sunny"
assert (
state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
== UnitOfIrradiance.WATTS_PER_SQUARE_METER
)
entry = entity_registry.async_get("sensor.home_solar_irradiance_today")
assert entry
assert entry.unique_id == "0123456-solarirradianceday-0"
state = hass.states.get("sensor.home_solar_irradiance_tonight")
assert state
assert state.state == "271.6"
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert state.attributes.get(ATTR_ICON) == "mdi:weather-sunny"
assert (
state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
== UnitOfIrradiance.WATTS_PER_SQUARE_METER
)
entry = entity_registry.async_get("sensor.home_solar_irradiance_tonight")
assert entry
assert entry.unique_id == "0123456-solarirradiancenight-0"
state = hass.states.get("sensor.home_condition_today")
assert state
assert (
state.state
== "Clouds and sunshine with a couple of showers and a thunderstorm around late this afternoon"
)
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
entry = entity_registry.async_get("sensor.home_condition_today")
assert entry
assert entry.unique_id == "0123456-longphraseday-0"
state = hass.states.get("sensor.home_condition_tonight")
assert state
assert state.state == "Partly cloudy"
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
entry = entity_registry.async_get("sensor.home_condition_tonight")
assert entry
assert entry.unique_id == "0123456-longphrasenight-0"
async def test_availability(hass: HomeAssistant) -> None: async def test_availability(hass: HomeAssistant) -> None:
@ -599,24 +96,88 @@ async def test_availability(hass: HomeAssistant) -> None:
assert state.state == "3200.0" assert state.state == "3200.0"
@pytest.mark.parametrize(
"exception",
[
ApiError,
ConnectionError,
ClientConnectorError,
InvalidApiKeyError,
RequestsExceededError,
],
)
async def test_availability_forecast(hass: HomeAssistant, exception: Exception) -> None:
"""Ensure that we mark the entities unavailable correctly when service is offline."""
current = load_json_object_fixture("accuweather/current_conditions_data.json")
forecast = load_json_array_fixture("accuweather/forecast_data.json")
entity_id = "sensor.home_hours_of_sun_day_2"
await init_integration(hass)
state = hass.states.get(entity_id)
assert state
assert state.state != STATE_UNAVAILABLE
assert state.state == "5.7"
with (
patch(
"homeassistant.components.accuweather.AccuWeather.async_get_current_conditions",
return_value=current,
),
patch(
"homeassistant.components.accuweather.AccuWeather.async_get_daily_forecast",
side_effect=exception,
),
patch(
"homeassistant.components.accuweather.AccuWeather.requests_remaining",
new_callable=PropertyMock,
return_value=10,
),
):
async_fire_time_changed(hass, utcnow() + UPDATE_INTERVAL_DAILY_FORECAST)
await hass.async_block_till_done()
state = hass.states.get(entity_id)
assert state
assert state.state == STATE_UNAVAILABLE
with (
patch(
"homeassistant.components.accuweather.AccuWeather.async_get_current_conditions",
return_value=current,
),
patch(
"homeassistant.components.accuweather.AccuWeather.async_get_daily_forecast",
return_value=forecast,
),
patch(
"homeassistant.components.accuweather.AccuWeather.requests_remaining",
new_callable=PropertyMock,
return_value=10,
),
):
async_fire_time_changed(hass, utcnow() + UPDATE_INTERVAL_DAILY_FORECAST * 2)
await hass.async_block_till_done()
state = hass.states.get(entity_id)
assert state
assert state.state != STATE_UNAVAILABLE
assert state.state == "5.7"
async def test_manual_update_entity(hass: HomeAssistant) -> None: async def test_manual_update_entity(hass: HomeAssistant) -> None:
"""Test manual update entity via service homeassistant/update_entity.""" """Test manual update entity via service homeassistant/update_entity."""
await init_integration(hass, forecast=True) await init_integration(hass)
await async_setup_component(hass, "homeassistant", {}) await async_setup_component(hass, "homeassistant", {})
current = load_json_object_fixture("accuweather/current_conditions_data.json") current = load_json_object_fixture("accuweather/current_conditions_data.json")
forecast = load_json_array_fixture("accuweather/forecast_data.json")
with ( with (
patch( patch(
"homeassistant.components.accuweather.AccuWeather.async_get_current_conditions", "homeassistant.components.accuweather.AccuWeather.async_get_current_conditions",
return_value=current, return_value=current,
) as mock_current, ) as mock_current,
patch(
"homeassistant.components.accuweather.AccuWeather.async_get_daily_forecast",
return_value=forecast,
) as mock_forecast,
patch( patch(
"homeassistant.components.accuweather.AccuWeather.requests_remaining", "homeassistant.components.accuweather.AccuWeather.requests_remaining",
new_callable=PropertyMock, new_callable=PropertyMock,
@ -629,8 +190,7 @@ async def test_manual_update_entity(hass: HomeAssistant) -> None:
{ATTR_ENTITY_ID: ["sensor.home_cloud_ceiling"]}, {ATTR_ENTITY_ID: ["sensor.home_cloud_ceiling"]},
blocking=True, blocking=True,
) )
assert mock_current.call_count == 1 assert mock_current.call_count == 1
assert mock_forecast.call_count == 1
async def test_sensor_imperial_units(hass: HomeAssistant) -> None: async def test_sensor_imperial_units(hass: HomeAssistant) -> None:

View File

@ -7,7 +7,10 @@ from freezegun.api import FrozenDateTimeFactory
import pytest import pytest
from syrupy.assertion import SnapshotAssertion from syrupy.assertion import SnapshotAssertion
from homeassistant.components.accuweather.const import ATTRIBUTION from homeassistant.components.accuweather.const import (
ATTRIBUTION,
UPDATE_INTERVAL_DAILY_FORECAST,
)
from homeassistant.components.weather import ( from homeassistant.components.weather import (
ATTR_FORECAST_CONDITION, ATTR_FORECAST_CONDITION,
ATTR_WEATHER_APPARENT_TEMPERATURE, ATTR_WEATHER_APPARENT_TEMPERATURE,
@ -24,6 +27,7 @@ from homeassistant.components.weather import (
DOMAIN as WEATHER_DOMAIN, DOMAIN as WEATHER_DOMAIN,
LEGACY_SERVICE_GET_FORECAST, LEGACY_SERVICE_GET_FORECAST,
SERVICE_GET_FORECASTS, SERVICE_GET_FORECASTS,
WeatherEntityFeature,
) )
from homeassistant.const import ( from homeassistant.const import (
ATTR_ATTRIBUTION, ATTR_ATTRIBUTION,
@ -65,7 +69,10 @@ async def test_weather(hass: HomeAssistant, entity_registry: er.EntityRegistry)
assert state.attributes.get(ATTR_WEATHER_WIND_GUST_SPEED) == 20.3 assert state.attributes.get(ATTR_WEATHER_WIND_GUST_SPEED) == 20.3
assert state.attributes.get(ATTR_WEATHER_UV_INDEX) == 6 assert state.attributes.get(ATTR_WEATHER_UV_INDEX) == 6
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert ATTR_SUPPORTED_FEATURES not in state.attributes assert (
state.attributes.get(ATTR_SUPPORTED_FEATURES)
is WeatherEntityFeature.FORECAST_DAILY
)
entry = entity_registry.async_get("weather.home") entry = entity_registry.async_get("weather.home")
assert entry assert entry
@ -118,22 +125,17 @@ async def test_availability(hass: HomeAssistant) -> None:
async def test_manual_update_entity(hass: HomeAssistant) -> None: async def test_manual_update_entity(hass: HomeAssistant) -> None:
"""Test manual update entity via service homeassistant/update_entity.""" """Test manual update entity via service homeassistant/update_entity."""
await init_integration(hass, forecast=True) await init_integration(hass)
await async_setup_component(hass, "homeassistant", {}) await async_setup_component(hass, "homeassistant", {})
current = load_json_object_fixture("accuweather/current_conditions_data.json") current = load_json_object_fixture("accuweather/current_conditions_data.json")
forecast = load_json_array_fixture("accuweather/forecast_data.json")
with ( with (
patch( patch(
"homeassistant.components.accuweather.AccuWeather.async_get_current_conditions", "homeassistant.components.accuweather.AccuWeather.async_get_current_conditions",
return_value=current, return_value=current,
) as mock_current, ) as mock_current,
patch(
"homeassistant.components.accuweather.AccuWeather.async_get_daily_forecast",
return_value=forecast,
) as mock_forecast,
patch( patch(
"homeassistant.components.accuweather.AccuWeather.requests_remaining", "homeassistant.components.accuweather.AccuWeather.requests_remaining",
new_callable=PropertyMock, new_callable=PropertyMock,
@ -147,12 +149,11 @@ async def test_manual_update_entity(hass: HomeAssistant) -> None:
blocking=True, blocking=True,
) )
assert mock_current.call_count == 1 assert mock_current.call_count == 1
assert mock_forecast.call_count == 1
async def test_unsupported_condition_icon_data(hass: HomeAssistant) -> None: async def test_unsupported_condition_icon_data(hass: HomeAssistant) -> None:
"""Test with unsupported condition icon data.""" """Test with unsupported condition icon data."""
await init_integration(hass, forecast=True, unsupported_icon=True) await init_integration(hass, unsupported_icon=True)
state = hass.states.get("weather.home") state = hass.states.get("weather.home")
assert state.attributes.get(ATTR_FORECAST_CONDITION) is None assert state.attributes.get(ATTR_FORECAST_CONDITION) is None
@ -171,7 +172,7 @@ async def test_forecast_service(
service: str, service: str,
) -> None: ) -> None:
"""Test multiple forecast.""" """Test multiple forecast."""
await init_integration(hass, forecast=True) await init_integration(hass)
response = await hass.services.async_call( response = await hass.services.async_call(
WEATHER_DOMAIN, WEATHER_DOMAIN,
@ -195,7 +196,7 @@ async def test_forecast_subscription(
"""Test multiple forecast.""" """Test multiple forecast."""
client = await hass_ws_client(hass) client = await hass_ws_client(hass)
await init_integration(hass, forecast=True) await init_integration(hass)
await client.send_json_auto_id( await client.send_json_auto_id(
{ {
@ -235,7 +236,7 @@ async def test_forecast_subscription(
return_value=10, return_value=10,
), ),
): ):
freezer.tick(timedelta(minutes=80) + timedelta(seconds=1)) freezer.tick(UPDATE_INTERVAL_DAILY_FORECAST + timedelta(seconds=1))
await hass.async_block_till_done() await hass.async_block_till_done()
msg = await client.receive_json() msg = await client.receive_json()