Improve AccuWeather type annotations (#50616)

* Improve type annotations

* Remove unused argument

* Simplify state logic

* Fix uvindex state

* Fix type for logger

* Increase tests coverage

* Fix pylint arguments-differ error

* Suggested change

* Suggested change

* Remove unnecessary variable

* Remove unnecessary conditions

* Use int instead of list for forecast days

* Add enabled to sensor types dicts

* Fix request_remaining conversion and tests

* Run hassfest

* Suggested change

* Suggested change

* Do not use StateType
This commit is contained in:
Maciej Bieniek 2021-05-19 10:37:16 +02:00 committed by GitHub
parent 62386c8676
commit bce5f8ee05
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 473 additions and 337 deletions

View File

@ -4,6 +4,7 @@
homeassistant.components homeassistant.components
homeassistant.components.acer_projector.* homeassistant.components.acer_projector.*
homeassistant.components.accuweather.*
homeassistant.components.actiontec.* homeassistant.components.actiontec.*
homeassistant.components.aftership.* homeassistant.components.aftership.*
homeassistant.components.air_quality.* homeassistant.components.air_quality.*

View File

@ -1,12 +1,18 @@
"""The AccuWeather component.""" """The AccuWeather component."""
from __future__ import annotations
from datetime import timedelta from datetime import timedelta
import logging import logging
from typing import Any, Dict
from accuweather import AccuWeather, ApiError, InvalidApiKeyError, RequestsExceededError from accuweather import AccuWeather, ApiError, InvalidApiKeyError, RequestsExceededError
from aiohttp import ClientSession
from aiohttp.client_exceptions import ClientConnectorError from aiohttp.client_exceptions import ClientConnectorError
from async_timeout import timeout from async_timeout import timeout
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY from homeassistant.const import CONF_API_KEY
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
@ -23,11 +29,12 @@ _LOGGER = logging.getLogger(__name__)
PLATFORMS = ["sensor", "weather"] PLATFORMS = ["sensor", "weather"]
async def async_setup_entry(hass, config_entry) -> 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 = config_entry.data[CONF_API_KEY] api_key: str = entry.data[CONF_API_KEY]
location_key = config_entry.unique_id assert entry.unique_id is not None
forecast = config_entry.options.get(CONF_FORECAST, False) 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) _LOGGER.debug("Using location_key: %s, get forecast: %s", location_key, forecast)
@ -38,41 +45,46 @@ async def async_setup_entry(hass, config_entry) -> bool:
) )
await coordinator.async_config_entry_first_refresh() await coordinator.async_config_entry_first_refresh()
undo_listener = config_entry.add_update_listener(update_listener) undo_listener = entry.add_update_listener(update_listener)
hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = { hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {
COORDINATOR: coordinator, COORDINATOR: coordinator,
UNDO_UPDATE_LISTENER: undo_listener, UNDO_UPDATE_LISTENER: undo_listener,
} }
hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) hass.config_entries.async_setup_platforms(entry, PLATFORMS)
return True return True
async def async_unload_entry(hass, config_entry): async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry.""" """Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms( unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
config_entry, PLATFORMS
)
hass.data[DOMAIN][config_entry.entry_id][UNDO_UPDATE_LISTENER]() hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]()
if unload_ok: if unload_ok:
hass.data[DOMAIN].pop(config_entry.entry_id) hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok return unload_ok
async def update_listener(hass, config_entry): async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Update listener.""" """Update listener."""
await hass.config_entries.async_reload(config_entry.entry_id) await hass.config_entries.async_reload(entry.entry_id)
class AccuWeatherDataUpdateCoordinator(DataUpdateCoordinator): class AccuWeatherDataUpdateCoordinator(DataUpdateCoordinator[Dict[str, Any]]):
"""Class to manage fetching AccuWeather data API.""" """Class to manage fetching AccuWeather data API."""
def __init__(self, hass, session, api_key, location_key, forecast: bool): def __init__(
self,
hass: HomeAssistant,
session: ClientSession,
api_key: str,
location_key: str,
forecast: bool,
) -> None:
"""Initialize.""" """Initialize."""
self.location_key = location_key self.location_key = location_key
self.forecast = forecast self.forecast = forecast
@ -87,11 +99,11 @@ class AccuWeatherDataUpdateCoordinator(DataUpdateCoordinator):
update_interval = timedelta(minutes=40) update_interval = timedelta(minutes=40)
if self.forecast: if self.forecast:
update_interval *= 2 update_interval *= 2
_LOGGER.debug("Data will be update every %s", update_interval) _LOGGER.debug("Data will be update every %s", str(update_interval))
super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval) super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval)
async def _async_update_data(self): async def _async_update_data(self) -> dict[str, Any]:
"""Update data via library.""" """Update data via library."""
try: try:
async with timeout(10): async with timeout(10):
@ -108,5 +120,5 @@ class AccuWeatherDataUpdateCoordinator(DataUpdateCoordinator):
RequestsExceededError, RequestsExceededError,
) as error: ) as error:
raise UpdateFailed(error) from error raise UpdateFailed(error) from error
_LOGGER.debug("Requests remaining: %s", self.accuweather.requests_remaining) _LOGGER.debug("Requests remaining: %d", self.accuweather.requests_remaining)
return {**current, **{ATTR_FORECAST: forecast}} return {**current, **{ATTR_FORECAST: forecast}}

View File

@ -1,5 +1,8 @@
"""Adds config flow for AccuWeather.""" """Adds config flow for AccuWeather."""
from __future__ import annotations
import asyncio import asyncio
from typing import Any
from accuweather import AccuWeather, ApiError, InvalidApiKeyError, RequestsExceededError from accuweather import AccuWeather, ApiError, InvalidApiKeyError, RequestsExceededError
from aiohttp import ClientError from aiohttp import ClientError
@ -8,8 +11,10 @@ from async_timeout import timeout
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult
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
@ -21,7 +26,9 @@ class AccuWeatherFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
VERSION = 1 VERSION = 1
async def async_step_user(self, user_input=None): async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle a flow initialized by the user.""" """Handle a flow initialized by the user."""
# Under the terms of use of the API, one user can use one free API key. Due to # Under the terms of use of the API, one user can use one free API key. Due to
# the small number of requests allowed, we only allow one integration instance. # the small number of requests allowed, we only allow one integration instance.
@ -77,7 +84,9 @@ class AccuWeatherFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
@staticmethod @staticmethod
@callback @callback
def async_get_options_flow(config_entry): def async_get_options_flow(
config_entry: ConfigEntry,
) -> AccuWeatherOptionsFlowHandler:
"""Options callback for AccuWeather.""" """Options callback for AccuWeather."""
return AccuWeatherOptionsFlowHandler(config_entry) return AccuWeatherOptionsFlowHandler(config_entry)
@ -85,15 +94,19 @@ class AccuWeatherFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
class AccuWeatherOptionsFlowHandler(config_entries.OptionsFlow): class AccuWeatherOptionsFlowHandler(config_entries.OptionsFlow):
"""Config flow options for AccuWeather.""" """Config flow options for AccuWeather."""
def __init__(self, config_entry): def __init__(self, entry: ConfigEntry) -> None:
"""Initialize AccuWeather options flow.""" """Initialize AccuWeather options flow."""
self.config_entry = config_entry self.config_entry = entry
async def async_step_init(self, user_input=None): async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Manage the options.""" """Manage the options."""
return await self.async_step_user() return await self.async_step_user()
async def async_step_user(self, user_input=None): async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle a flow initialized by the user.""" """Handle a flow initialized by the user."""
if user_input is not None: if user_input is not None:
return self.async_create_entry(title="", data=user_input) return self.async_create_entry(title="", data=user_input)

View File

@ -1,4 +1,8 @@
"""Constants for AccuWeather integration.""" """Constants for AccuWeather integration."""
from __future__ import annotations
from typing import Final
from homeassistant.components.weather import ( from homeassistant.components.weather import (
ATTR_CONDITION_CLEAR_NIGHT, ATTR_CONDITION_CLEAR_NIGHT,
ATTR_CONDITION_CLOUDY, ATTR_CONDITION_CLOUDY,
@ -16,8 +20,6 @@ from homeassistant.components.weather import (
ATTR_CONDITION_WINDY, ATTR_CONDITION_WINDY,
) )
from homeassistant.const import ( from homeassistant.const import (
ATTR_DEVICE_CLASS,
ATTR_ICON,
CONCENTRATION_PARTS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_CUBIC_METER,
DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_TEMPERATURE,
LENGTH_FEET, LENGTH_FEET,
@ -33,18 +35,19 @@ from homeassistant.const import (
UV_INDEX, UV_INDEX,
) )
ATTRIBUTION = "Data provided by AccuWeather" from .model import SensorDescription
ATTR_FORECAST = CONF_FORECAST = "forecast"
ATTR_LABEL = "label"
ATTR_UNIT_IMPERIAL = "Imperial"
ATTR_UNIT_METRIC = "Metric"
COORDINATOR = "coordinator"
DOMAIN = "accuweather"
MANUFACTURER = "AccuWeather, Inc."
NAME = "AccuWeather"
UNDO_UPDATE_LISTENER = "undo_update_listener"
CONDITION_CLASSES = { ATTRIBUTION: Final = "Data provided by AccuWeather"
ATTR_FORECAST: Final = "forecast"
CONF_FORECAST: Final = "forecast"
COORDINATOR: Final = "coordinator"
DOMAIN: Final = "accuweather"
MANUFACTURER: Final = "AccuWeather, Inc."
MAX_FORECAST_DAYS: Final = 4
NAME: Final = "AccuWeather"
UNDO_UPDATE_LISTENER: Final = "undo_update_listener"
CONDITION_CLASSES: Final[dict[str, list[int]]] = {
ATTR_CONDITION_CLEAR_NIGHT: [33, 34, 37], ATTR_CONDITION_CLEAR_NIGHT: [33, 34, 37],
ATTR_CONDITION_CLOUDY: [7, 8, 38], ATTR_CONDITION_CLOUDY: [7, 8, 38],
ATTR_CONDITION_EXCEPTIONAL: [24, 30, 31], ATTR_CONDITION_EXCEPTIONAL: [24, 30, 31],
@ -61,255 +64,264 @@ CONDITION_CLASSES = {
ATTR_CONDITION_WINDY: [32], ATTR_CONDITION_WINDY: [32],
} }
FORECAST_DAYS = [0, 1, 2, 3, 4] FORECAST_SENSOR_TYPES: Final[dict[str, SensorDescription]] = {
FORECAST_SENSOR_TYPES = {
"CloudCoverDay": { "CloudCoverDay": {
ATTR_DEVICE_CLASS: None, "device_class": None,
ATTR_ICON: "mdi:weather-cloudy", "icon": "mdi:weather-cloudy",
ATTR_LABEL: "Cloud Cover Day", "label": "Cloud Cover Day",
ATTR_UNIT_METRIC: PERCENTAGE, "unit_metric": PERCENTAGE,
ATTR_UNIT_IMPERIAL: PERCENTAGE, "unit_imperial": PERCENTAGE,
"enabled": False,
}, },
"CloudCoverNight": { "CloudCoverNight": {
ATTR_DEVICE_CLASS: None, "device_class": None,
ATTR_ICON: "mdi:weather-cloudy", "icon": "mdi:weather-cloudy",
ATTR_LABEL: "Cloud Cover Night", "label": "Cloud Cover Night",
ATTR_UNIT_METRIC: PERCENTAGE, "unit_metric": PERCENTAGE,
ATTR_UNIT_IMPERIAL: PERCENTAGE, "unit_imperial": PERCENTAGE,
"enabled": False,
}, },
"Grass": { "Grass": {
ATTR_DEVICE_CLASS: None, "device_class": None,
ATTR_ICON: "mdi:grass", "icon": "mdi:grass",
ATTR_LABEL: "Grass Pollen", "label": "Grass Pollen",
ATTR_UNIT_METRIC: CONCENTRATION_PARTS_PER_CUBIC_METER, "unit_metric": CONCENTRATION_PARTS_PER_CUBIC_METER,
ATTR_UNIT_IMPERIAL: CONCENTRATION_PARTS_PER_CUBIC_METER, "unit_imperial": CONCENTRATION_PARTS_PER_CUBIC_METER,
"enabled": False,
}, },
"HoursOfSun": { "HoursOfSun": {
ATTR_DEVICE_CLASS: None, "device_class": None,
ATTR_ICON: "mdi:weather-partly-cloudy", "icon": "mdi:weather-partly-cloudy",
ATTR_LABEL: "Hours Of Sun", "label": "Hours Of Sun",
ATTR_UNIT_METRIC: TIME_HOURS, "unit_metric": TIME_HOURS,
ATTR_UNIT_IMPERIAL: TIME_HOURS, "unit_imperial": TIME_HOURS,
"enabled": True,
}, },
"Mold": { "Mold": {
ATTR_DEVICE_CLASS: None, "device_class": None,
ATTR_ICON: "mdi:blur", "icon": "mdi:blur",
ATTR_LABEL: "Mold Pollen", "label": "Mold Pollen",
ATTR_UNIT_METRIC: CONCENTRATION_PARTS_PER_CUBIC_METER, "unit_metric": CONCENTRATION_PARTS_PER_CUBIC_METER,
ATTR_UNIT_IMPERIAL: CONCENTRATION_PARTS_PER_CUBIC_METER, "unit_imperial": CONCENTRATION_PARTS_PER_CUBIC_METER,
"enabled": False,
}, },
"Ozone": { "Ozone": {
ATTR_DEVICE_CLASS: None, "device_class": None,
ATTR_ICON: "mdi:vector-triangle", "icon": "mdi:vector-triangle",
ATTR_LABEL: "Ozone", "label": "Ozone",
ATTR_UNIT_METRIC: None, "unit_metric": None,
ATTR_UNIT_IMPERIAL: None, "unit_imperial": None,
"enabled": False,
}, },
"Ragweed": { "Ragweed": {
ATTR_DEVICE_CLASS: None, "device_class": None,
ATTR_ICON: "mdi:sprout", "icon": "mdi:sprout",
ATTR_LABEL: "Ragweed Pollen", "label": "Ragweed Pollen",
ATTR_UNIT_METRIC: CONCENTRATION_PARTS_PER_CUBIC_METER, "unit_metric": CONCENTRATION_PARTS_PER_CUBIC_METER,
ATTR_UNIT_IMPERIAL: CONCENTRATION_PARTS_PER_CUBIC_METER, "unit_imperial": CONCENTRATION_PARTS_PER_CUBIC_METER,
"enabled": False,
}, },
"RealFeelTemperatureMax": { "RealFeelTemperatureMax": {
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, "device_class": DEVICE_CLASS_TEMPERATURE,
ATTR_ICON: None, "icon": None,
ATTR_LABEL: "RealFeel Temperature Max", "label": "RealFeel Temperature Max",
ATTR_UNIT_METRIC: TEMP_CELSIUS, "unit_metric": TEMP_CELSIUS,
ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT, "unit_imperial": TEMP_FAHRENHEIT,
"enabled": True,
}, },
"RealFeelTemperatureMin": { "RealFeelTemperatureMin": {
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, "device_class": DEVICE_CLASS_TEMPERATURE,
ATTR_ICON: None, "icon": None,
ATTR_LABEL: "RealFeel Temperature Min", "label": "RealFeel Temperature Min",
ATTR_UNIT_METRIC: TEMP_CELSIUS, "unit_metric": TEMP_CELSIUS,
ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT, "unit_imperial": TEMP_FAHRENHEIT,
"enabled": True,
}, },
"RealFeelTemperatureShadeMax": { "RealFeelTemperatureShadeMax": {
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, "device_class": DEVICE_CLASS_TEMPERATURE,
ATTR_ICON: None, "icon": None,
ATTR_LABEL: "RealFeel Temperature Shade Max", "label": "RealFeel Temperature Shade Max",
ATTR_UNIT_METRIC: TEMP_CELSIUS, "unit_metric": TEMP_CELSIUS,
ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT, "unit_imperial": TEMP_FAHRENHEIT,
"enabled": False,
}, },
"RealFeelTemperatureShadeMin": { "RealFeelTemperatureShadeMin": {
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, "device_class": DEVICE_CLASS_TEMPERATURE,
ATTR_ICON: None, "icon": None,
ATTR_LABEL: "RealFeel Temperature Shade Min", "label": "RealFeel Temperature Shade Min",
ATTR_UNIT_METRIC: TEMP_CELSIUS, "unit_metric": TEMP_CELSIUS,
ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT, "unit_imperial": TEMP_FAHRENHEIT,
"enabled": False,
}, },
"ThunderstormProbabilityDay": { "ThunderstormProbabilityDay": {
ATTR_DEVICE_CLASS: None, "device_class": None,
ATTR_ICON: "mdi:weather-lightning", "icon": "mdi:weather-lightning",
ATTR_LABEL: "Thunderstorm Probability Day", "label": "Thunderstorm Probability Day",
ATTR_UNIT_METRIC: PERCENTAGE, "unit_metric": PERCENTAGE,
ATTR_UNIT_IMPERIAL: PERCENTAGE, "unit_imperial": PERCENTAGE,
"enabled": True,
}, },
"ThunderstormProbabilityNight": { "ThunderstormProbabilityNight": {
ATTR_DEVICE_CLASS: None, "device_class": None,
ATTR_ICON: "mdi:weather-lightning", "icon": "mdi:weather-lightning",
ATTR_LABEL: "Thunderstorm Probability Night", "label": "Thunderstorm Probability Night",
ATTR_UNIT_METRIC: PERCENTAGE, "unit_metric": PERCENTAGE,
ATTR_UNIT_IMPERIAL: PERCENTAGE, "unit_imperial": PERCENTAGE,
"enabled": True,
}, },
"Tree": { "Tree": {
ATTR_DEVICE_CLASS: None, "device_class": None,
ATTR_ICON: "mdi:tree-outline", "icon": "mdi:tree-outline",
ATTR_LABEL: "Tree Pollen", "label": "Tree Pollen",
ATTR_UNIT_METRIC: CONCENTRATION_PARTS_PER_CUBIC_METER, "unit_metric": CONCENTRATION_PARTS_PER_CUBIC_METER,
ATTR_UNIT_IMPERIAL: CONCENTRATION_PARTS_PER_CUBIC_METER, "unit_imperial": CONCENTRATION_PARTS_PER_CUBIC_METER,
"enabled": False,
}, },
"UVIndex": { "UVIndex": {
ATTR_DEVICE_CLASS: None, "device_class": None,
ATTR_ICON: "mdi:weather-sunny", "icon": "mdi:weather-sunny",
ATTR_LABEL: "UV Index", "label": "UV Index",
ATTR_UNIT_METRIC: UV_INDEX, "unit_metric": UV_INDEX,
ATTR_UNIT_IMPERIAL: UV_INDEX, "unit_imperial": UV_INDEX,
"enabled": True,
}, },
"WindGustDay": { "WindGustDay": {
ATTR_DEVICE_CLASS: None, "device_class": None,
ATTR_ICON: "mdi:weather-windy", "icon": "mdi:weather-windy",
ATTR_LABEL: "Wind Gust Day", "label": "Wind Gust Day",
ATTR_UNIT_METRIC: SPEED_KILOMETERS_PER_HOUR, "unit_metric": SPEED_KILOMETERS_PER_HOUR,
ATTR_UNIT_IMPERIAL: SPEED_MILES_PER_HOUR, "unit_imperial": SPEED_MILES_PER_HOUR,
"enabled": False,
}, },
"WindGustNight": { "WindGustNight": {
ATTR_DEVICE_CLASS: None, "device_class": None,
ATTR_ICON: "mdi:weather-windy", "icon": "mdi:weather-windy",
ATTR_LABEL: "Wind Gust Night", "label": "Wind Gust Night",
ATTR_UNIT_METRIC: SPEED_KILOMETERS_PER_HOUR, "unit_metric": SPEED_KILOMETERS_PER_HOUR,
ATTR_UNIT_IMPERIAL: SPEED_MILES_PER_HOUR, "unit_imperial": SPEED_MILES_PER_HOUR,
"enabled": False,
}, },
"WindDay": { "WindDay": {
ATTR_DEVICE_CLASS: None, "device_class": None,
ATTR_ICON: "mdi:weather-windy", "icon": "mdi:weather-windy",
ATTR_LABEL: "Wind Day", "label": "Wind Day",
ATTR_UNIT_METRIC: SPEED_KILOMETERS_PER_HOUR, "unit_metric": SPEED_KILOMETERS_PER_HOUR,
ATTR_UNIT_IMPERIAL: SPEED_MILES_PER_HOUR, "unit_imperial": SPEED_MILES_PER_HOUR,
"enabled": True,
}, },
"WindNight": { "WindNight": {
ATTR_DEVICE_CLASS: None, "device_class": None,
ATTR_ICON: "mdi:weather-windy", "icon": "mdi:weather-windy",
ATTR_LABEL: "Wind Night", "label": "Wind Night",
ATTR_UNIT_METRIC: SPEED_KILOMETERS_PER_HOUR, "unit_metric": SPEED_KILOMETERS_PER_HOUR,
ATTR_UNIT_IMPERIAL: SPEED_MILES_PER_HOUR, "unit_imperial": SPEED_MILES_PER_HOUR,
"enabled": True,
}, },
} }
OPTIONAL_SENSORS = ( SENSOR_TYPES: Final[dict[str, SensorDescription]] = {
"ApparentTemperature",
"CloudCover",
"CloudCoverDay",
"CloudCoverNight",
"DewPoint",
"Grass",
"Mold",
"Ozone",
"Ragweed",
"RealFeelTemperatureShade",
"RealFeelTemperatureShadeMax",
"RealFeelTemperatureShadeMin",
"Tree",
"WetBulbTemperature",
"WindChillTemperature",
"WindGust",
"WindGustDay",
"WindGustNight",
)
SENSOR_TYPES = {
"ApparentTemperature": { "ApparentTemperature": {
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, "device_class": DEVICE_CLASS_TEMPERATURE,
ATTR_ICON: None, "icon": None,
ATTR_LABEL: "Apparent Temperature", "label": "Apparent Temperature",
ATTR_UNIT_METRIC: TEMP_CELSIUS, "unit_metric": TEMP_CELSIUS,
ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT, "unit_imperial": TEMP_FAHRENHEIT,
"enabled": False,
}, },
"Ceiling": { "Ceiling": {
ATTR_DEVICE_CLASS: None, "device_class": None,
ATTR_ICON: "mdi:weather-fog", "icon": "mdi:weather-fog",
ATTR_LABEL: "Cloud Ceiling", "label": "Cloud Ceiling",
ATTR_UNIT_METRIC: LENGTH_METERS, "unit_metric": LENGTH_METERS,
ATTR_UNIT_IMPERIAL: LENGTH_FEET, "unit_imperial": LENGTH_FEET,
"enabled": True,
}, },
"CloudCover": { "CloudCover": {
ATTR_DEVICE_CLASS: None, "device_class": None,
ATTR_ICON: "mdi:weather-cloudy", "icon": "mdi:weather-cloudy",
ATTR_LABEL: "Cloud Cover", "label": "Cloud Cover",
ATTR_UNIT_METRIC: PERCENTAGE, "unit_metric": PERCENTAGE,
ATTR_UNIT_IMPERIAL: PERCENTAGE, "unit_imperial": PERCENTAGE,
"enabled": False,
}, },
"DewPoint": { "DewPoint": {
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, "device_class": DEVICE_CLASS_TEMPERATURE,
ATTR_ICON: None, "icon": None,
ATTR_LABEL: "Dew Point", "label": "Dew Point",
ATTR_UNIT_METRIC: TEMP_CELSIUS, "unit_metric": TEMP_CELSIUS,
ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT, "unit_imperial": TEMP_FAHRENHEIT,
"enabled": False,
}, },
"RealFeelTemperature": { "RealFeelTemperature": {
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, "device_class": DEVICE_CLASS_TEMPERATURE,
ATTR_ICON: None, "icon": None,
ATTR_LABEL: "RealFeel Temperature", "label": "RealFeel Temperature",
ATTR_UNIT_METRIC: TEMP_CELSIUS, "unit_metric": TEMP_CELSIUS,
ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT, "unit_imperial": TEMP_FAHRENHEIT,
"enabled": True,
}, },
"RealFeelTemperatureShade": { "RealFeelTemperatureShade": {
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, "device_class": DEVICE_CLASS_TEMPERATURE,
ATTR_ICON: None, "icon": None,
ATTR_LABEL: "RealFeel Temperature Shade", "label": "RealFeel Temperature Shade",
ATTR_UNIT_METRIC: TEMP_CELSIUS, "unit_metric": TEMP_CELSIUS,
ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT, "unit_imperial": TEMP_FAHRENHEIT,
"enabled": False,
}, },
"Precipitation": { "Precipitation": {
ATTR_DEVICE_CLASS: None, "device_class": None,
ATTR_ICON: "mdi:weather-rainy", "icon": "mdi:weather-rainy",
ATTR_LABEL: "Precipitation", "label": "Precipitation",
ATTR_UNIT_METRIC: LENGTH_MILLIMETERS, "unit_metric": LENGTH_MILLIMETERS,
ATTR_UNIT_IMPERIAL: LENGTH_INCHES, "unit_imperial": LENGTH_INCHES,
"enabled": True,
}, },
"PressureTendency": { "PressureTendency": {
ATTR_DEVICE_CLASS: "accuweather__pressure_tendency", "device_class": "accuweather__pressure_tendency",
ATTR_ICON: "mdi:gauge", "icon": "mdi:gauge",
ATTR_LABEL: "Pressure Tendency", "label": "Pressure Tendency",
ATTR_UNIT_METRIC: None, "unit_metric": None,
ATTR_UNIT_IMPERIAL: None, "unit_imperial": None,
"enabled": True,
}, },
"UVIndex": { "UVIndex": {
ATTR_DEVICE_CLASS: None, "device_class": None,
ATTR_ICON: "mdi:weather-sunny", "icon": "mdi:weather-sunny",
ATTR_LABEL: "UV Index", "label": "UV Index",
ATTR_UNIT_METRIC: UV_INDEX, "unit_metric": UV_INDEX,
ATTR_UNIT_IMPERIAL: UV_INDEX, "unit_imperial": UV_INDEX,
"enabled": True,
}, },
"WetBulbTemperature": { "WetBulbTemperature": {
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, "device_class": DEVICE_CLASS_TEMPERATURE,
ATTR_ICON: None, "icon": None,
ATTR_LABEL: "Wet Bulb Temperature", "label": "Wet Bulb Temperature",
ATTR_UNIT_METRIC: TEMP_CELSIUS, "unit_metric": TEMP_CELSIUS,
ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT, "unit_imperial": TEMP_FAHRENHEIT,
"enabled": False,
}, },
"WindChillTemperature": { "WindChillTemperature": {
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, "device_class": DEVICE_CLASS_TEMPERATURE,
ATTR_ICON: None, "icon": None,
ATTR_LABEL: "Wind Chill Temperature", "label": "Wind Chill Temperature",
ATTR_UNIT_METRIC: TEMP_CELSIUS, "unit_metric": TEMP_CELSIUS,
ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT, "unit_imperial": TEMP_FAHRENHEIT,
"enabled": False,
}, },
"Wind": { "Wind": {
ATTR_DEVICE_CLASS: None, "device_class": None,
ATTR_ICON: "mdi:weather-windy", "icon": "mdi:weather-windy",
ATTR_LABEL: "Wind", "label": "Wind",
ATTR_UNIT_METRIC: SPEED_KILOMETERS_PER_HOUR, "unit_metric": SPEED_KILOMETERS_PER_HOUR,
ATTR_UNIT_IMPERIAL: SPEED_MILES_PER_HOUR, "unit_imperial": SPEED_MILES_PER_HOUR,
"enabled": True,
}, },
"WindGust": { "WindGust": {
ATTR_DEVICE_CLASS: None, "device_class": None,
ATTR_ICON: "mdi:weather-windy", "icon": "mdi:weather-windy",
ATTR_LABEL: "Wind Gust", "label": "Wind Gust",
ATTR_UNIT_METRIC: SPEED_KILOMETERS_PER_HOUR, "unit_metric": SPEED_KILOMETERS_PER_HOUR,
ATTR_UNIT_IMPERIAL: SPEED_MILES_PER_HOUR, "unit_imperial": SPEED_MILES_PER_HOUR,
"enabled": False,
}, },
} }

View File

@ -0,0 +1,15 @@
"""Type definitions for AccuWeather integration."""
from __future__ import annotations
from typing import TypedDict
class SensorDescription(TypedDict):
"""Sensor description class."""
device_class: str | None
icon: str | None
label: str
unit_metric: str | None
unit_imperial: str | None
enabled: bool

View File

@ -1,44 +1,50 @@
"""Support for the AccuWeather service.""" """Support for the AccuWeather service."""
from __future__ import annotations
from typing import Any, cast
from homeassistant.components.sensor import SensorEntity from homeassistant.components.sensor import SensorEntity
from homeassistant.const import ( from homeassistant.config_entries import ConfigEntry
ATTR_ATTRIBUTION, from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME, DEVICE_CLASS_TEMPERATURE
ATTR_DEVICE_CLASS, from homeassistant.core import HomeAssistant
CONF_NAME, from homeassistant.helpers.entity import DeviceInfo
DEVICE_CLASS_TEMPERATURE, from homeassistant.helpers.entity_platform import AddEntitiesCallback
) from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import AccuWeatherDataUpdateCoordinator
from .const import ( from .const import (
ATTR_FORECAST, ATTR_FORECAST,
ATTR_ICON,
ATTR_LABEL,
ATTRIBUTION, ATTRIBUTION,
COORDINATOR, COORDINATOR,
DOMAIN, DOMAIN,
FORECAST_DAYS,
FORECAST_SENSOR_TYPES, FORECAST_SENSOR_TYPES,
MANUFACTURER, MANUFACTURER,
MAX_FORECAST_DAYS,
NAME, NAME,
OPTIONAL_SENSORS,
SENSOR_TYPES, SENSOR_TYPES,
) )
PARALLEL_UPDATES = 1 PARALLEL_UPDATES = 1
async def async_setup_entry(hass, config_entry, async_add_entities): async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Add AccuWeather entities from a config_entry.""" """Add AccuWeather entities from a config_entry."""
name = config_entry.data[CONF_NAME] name: str = entry.data[CONF_NAME]
coordinator = hass.data[DOMAIN][config_entry.entry_id][COORDINATOR] coordinator: AccuWeatherDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][
COORDINATOR
]
sensors = [] sensors: list[AccuWeatherSensor] = []
for sensor in SENSOR_TYPES: for sensor in SENSOR_TYPES:
sensors.append(AccuWeatherSensor(name, sensor, coordinator)) sensors.append(AccuWeatherSensor(name, sensor, coordinator))
if coordinator.forecast: if coordinator.forecast:
for sensor in FORECAST_SENSOR_TYPES: for sensor in FORECAST_SENSOR_TYPES:
for day in FORECAST_DAYS: for day in range(MAX_FORECAST_DAYS + 1):
# Some air quality/allergy sensors are only available for certain # Some air quality/allergy sensors are only available for certain
# locations. # locations.
if sensor in coordinator.data[ATTR_FORECAST][0]: if sensor in coordinator.data[ATTR_FORECAST][0]:
@ -46,38 +52,56 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
AccuWeatherSensor(name, sensor, coordinator, forecast_day=day) AccuWeatherSensor(name, sensor, coordinator, forecast_day=day)
) )
async_add_entities(sensors, False) async_add_entities(sensors)
class AccuWeatherSensor(CoordinatorEntity, SensorEntity): class AccuWeatherSensor(CoordinatorEntity, SensorEntity):
"""Define an AccuWeather entity.""" """Define an AccuWeather entity."""
def __init__(self, name, kind, coordinator, forecast_day=None): coordinator: AccuWeatherDataUpdateCoordinator
def __init__(
self,
name: str,
kind: str,
coordinator: AccuWeatherDataUpdateCoordinator,
forecast_day: int | None = None,
) -> None:
"""Initialize.""" """Initialize."""
super().__init__(coordinator) super().__init__(coordinator)
if forecast_day is None:
self._description = SENSOR_TYPES[kind]
self._sensor_data: dict[str, Any]
if kind == "Precipitation":
self._sensor_data = coordinator.data["PrecipitationSummary"][kind]
else:
self._sensor_data = coordinator.data[kind]
else:
self._description = FORECAST_SENSOR_TYPES[kind]
self._sensor_data = coordinator.data[ATTR_FORECAST][forecast_day][kind]
self._unit_system = "Metric" if coordinator.is_metric else "Imperial"
self._name = name self._name = name
self.kind = kind self.kind = kind
self._device_class = None self._device_class = None
self._attrs = {ATTR_ATTRIBUTION: ATTRIBUTION} self._attrs = {ATTR_ATTRIBUTION: ATTRIBUTION}
self._unit_system = "Metric" if self.coordinator.is_metric else "Imperial"
self.forecast_day = forecast_day self.forecast_day = forecast_day
@property @property
def name(self): def name(self) -> str:
"""Return the name.""" """Return the name."""
if self.forecast_day is not None: if self.forecast_day is not None:
return f"{self._name} {FORECAST_SENSOR_TYPES[self.kind][ATTR_LABEL]} {self.forecast_day}d" return f"{self._name} {self._description['label']} {self.forecast_day}d"
return f"{self._name} {SENSOR_TYPES[self.kind][ATTR_LABEL]}" return f"{self._name} {self._description['label']}"
@property @property
def unique_id(self): def unique_id(self) -> str:
"""Return a unique_id for this entity.""" """Return a unique_id for this entity."""
if self.forecast_day is not None: if self.forecast_day is not None:
return f"{self.coordinator.location_key}-{self.kind}-{self.forecast_day}".lower() return f"{self.coordinator.location_key}-{self.kind}-{self.forecast_day}".lower()
return f"{self.coordinator.location_key}-{self.kind}".lower() return f"{self.coordinator.location_key}-{self.kind}".lower()
@property @property
def device_info(self): def device_info(self) -> DeviceInfo:
"""Return the device info.""" """Return the device info."""
return { return {
"identifiers": {(DOMAIN, self.coordinator.location_key)}, "identifiers": {(DOMAIN, self.coordinator.location_key)},
@ -87,72 +111,54 @@ class AccuWeatherSensor(CoordinatorEntity, SensorEntity):
} }
@property @property
def state(self): def state(self) -> StateType:
"""Return the state.""" """Return the state."""
if self.forecast_day is not None: if self.forecast_day is not None:
if ( if self._description["device_class"] == DEVICE_CLASS_TEMPERATURE:
FORECAST_SENSOR_TYPES[self.kind][ATTR_DEVICE_CLASS] return cast(float, self._sensor_data["Value"])
== DEVICE_CLASS_TEMPERATURE if self.kind == "UVIndex":
): return cast(int, self._sensor_data["Value"])
return self.coordinator.data[ATTR_FORECAST][self.forecast_day][ if self.kind in ["Grass", "Mold", "Ragweed", "Tree", "Ozone"]:
self.kind return cast(int, self._sensor_data["Value"])
]["Value"]
if self.kind in ["WindDay", "WindNight", "WindGustDay", "WindGustNight"]:
return self.coordinator.data[ATTR_FORECAST][self.forecast_day][
self.kind
]["Speed"]["Value"]
if self.kind in ["Grass", "Mold", "Ragweed", "Tree", "UVIndex", "Ozone"]:
return self.coordinator.data[ATTR_FORECAST][self.forecast_day][
self.kind
]["Value"]
return self.coordinator.data[ATTR_FORECAST][self.forecast_day][self.kind]
if self.kind == "Ceiling": if self.kind == "Ceiling":
return round(self.coordinator.data[self.kind][self._unit_system]["Value"]) return round(self._sensor_data[self._unit_system]["Value"])
if self.kind == "PressureTendency": if self.kind == "PressureTendency":
return self.coordinator.data[self.kind]["LocalizedText"].lower() return cast(str, self._sensor_data["LocalizedText"].lower())
if SENSOR_TYPES[self.kind][ATTR_DEVICE_CLASS] == DEVICE_CLASS_TEMPERATURE: if self._description["device_class"] == DEVICE_CLASS_TEMPERATURE:
return self.coordinator.data[self.kind][self._unit_system]["Value"] return cast(float, self._sensor_data[self._unit_system]["Value"])
if self.kind == "Precipitation": if self.kind == "Precipitation":
return self.coordinator.data["PrecipitationSummary"][self.kind][ return cast(float, self._sensor_data[self._unit_system]["Value"])
self._unit_system
]["Value"]
if self.kind in ["Wind", "WindGust"]: if self.kind in ["Wind", "WindGust"]:
return self.coordinator.data[self.kind]["Speed"][self._unit_system]["Value"] return cast(float, self._sensor_data["Speed"][self._unit_system]["Value"])
return self.coordinator.data[self.kind] if self.kind in ["WindDay", "WindNight", "WindGustDay", "WindGustNight"]:
return cast(StateType, self._sensor_data["Speed"]["Value"])
return cast(StateType, self._sensor_data)
@property @property
def icon(self): def icon(self) -> str | None:
"""Return the icon.""" """Return the icon."""
if self.forecast_day is not None: return self._description["icon"]
return FORECAST_SENSOR_TYPES[self.kind][ATTR_ICON]
return SENSOR_TYPES[self.kind][ATTR_ICON]
@property @property
def device_class(self): def device_class(self) -> str | None:
"""Return the device_class.""" """Return the device_class."""
if self.forecast_day is not None: return self._description["device_class"]
return FORECAST_SENSOR_TYPES[self.kind][ATTR_DEVICE_CLASS]
return SENSOR_TYPES[self.kind][ATTR_DEVICE_CLASS]
@property @property
def unit_of_measurement(self): def unit_of_measurement(self) -> str | None:
"""Return the unit the value is expressed in.""" """Return the unit the value is expressed in."""
if self.forecast_day is not None: if self.coordinator.is_metric:
return FORECAST_SENSOR_TYPES[self.kind][self._unit_system] return self._description["unit_metric"]
return SENSOR_TYPES[self.kind][self._unit_system] return self._description["unit_imperial"]
@property @property
def extra_state_attributes(self): def extra_state_attributes(self) -> dict[str, Any]:
"""Return the state attributes.""" """Return the state attributes."""
if self.forecast_day is not None: if self.forecast_day is not None:
if self.kind in ["WindDay", "WindNight", "WindGustDay", "WindGustNight"]: if self.kind in ["WindDay", "WindNight", "WindGustDay", "WindGustNight"]:
self._attrs["direction"] = self.coordinator.data[ATTR_FORECAST][ self._attrs["direction"] = self._sensor_data["Direction"]["English"]
self.forecast_day
][self.kind]["Direction"]["English"]
elif self.kind in ["Grass", "Mold", "Ragweed", "Tree", "UVIndex", "Ozone"]: elif self.kind in ["Grass", "Mold", "Ragweed", "Tree", "UVIndex", "Ozone"]:
self._attrs["level"] = self.coordinator.data[ATTR_FORECAST][ self._attrs["level"] = self._sensor_data["Category"]
self.forecast_day
][self.kind]["Category"]
return self._attrs return self._attrs
if self.kind == "UVIndex": if self.kind == "UVIndex":
self._attrs["level"] = self.coordinator.data["UVIndexText"] self._attrs["level"] = self.coordinator.data["UVIndexText"]
@ -161,6 +167,6 @@ class AccuWeatherSensor(CoordinatorEntity, SensorEntity):
return self._attrs return self._attrs
@property @property
def entity_registry_enabled_default(self): def entity_registry_enabled_default(self) -> bool:
"""Return if the entity should be enabled when first added to the entity registry.""" """Return if the entity should be enabled when first added to the entity registry."""
return bool(self.kind not in OPTIONAL_SENSORS) return self._description["enabled"]

View File

@ -1,4 +1,8 @@
"""Provide info to system health.""" """Provide info to system health."""
from __future__ import annotations
from typing import Any
from accuweather.const import ENDPOINT from accuweather.const import ENDPOINT
from homeassistant.components import system_health from homeassistant.components import system_health
@ -15,7 +19,7 @@ def async_register(
register.async_register_info(system_health_info) register.async_register_info(system_health_info)
async def system_health_info(hass): async def system_health_info(hass: HomeAssistant) -> dict[str, Any]:
"""Get info for the info page.""" """Get info for the info page."""
remaining_requests = list(hass.data[DOMAIN].values())[0][ remaining_requests = list(hass.data[DOMAIN].values())[0][
COORDINATOR COORDINATOR

View File

@ -1,5 +1,8 @@
"""Support for the AccuWeather service.""" """Support for the AccuWeather service."""
from __future__ import annotations
from statistics import mean from statistics import mean
from typing import Any, cast
from homeassistant.components.weather import ( from homeassistant.components.weather import (
ATTR_FORECAST_CONDITION, ATTR_FORECAST_CONDITION,
@ -12,10 +15,15 @@ from homeassistant.components.weather import (
ATTR_FORECAST_WIND_SPEED, ATTR_FORECAST_WIND_SPEED,
WeatherEntity, WeatherEntity,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_NAME, TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.const import CONF_NAME, TEMP_CELSIUS, TEMP_FAHRENHEIT
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util.dt import utc_from_timestamp from homeassistant.util.dt import utc_from_timestamp
from . import AccuWeatherDataUpdateCoordinator
from .const import ( from .const import (
ATTR_FORECAST, ATTR_FORECAST,
ATTRIBUTION, ATTRIBUTION,
@ -29,42 +37,49 @@ from .const import (
PARALLEL_UPDATES = 1 PARALLEL_UPDATES = 1
async def async_setup_entry(hass, config_entry, async_add_entities): async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Add a AccuWeather weather entity from a config_entry.""" """Add a AccuWeather weather entity from a config_entry."""
name = config_entry.data[CONF_NAME] name: str = entry.data[CONF_NAME]
coordinator = hass.data[DOMAIN][config_entry.entry_id][COORDINATOR] coordinator: AccuWeatherDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][
COORDINATOR
]
async_add_entities([AccuWeatherEntity(name, coordinator)], False) async_add_entities([AccuWeatherEntity(name, coordinator)])
class AccuWeatherEntity(CoordinatorEntity, WeatherEntity): class AccuWeatherEntity(CoordinatorEntity, WeatherEntity):
"""Define an AccuWeather entity.""" """Define an AccuWeather entity."""
def __init__(self, name, coordinator): coordinator: AccuWeatherDataUpdateCoordinator
def __init__(
self, name: str, coordinator: AccuWeatherDataUpdateCoordinator
) -> None:
"""Initialize.""" """Initialize."""
super().__init__(coordinator) super().__init__(coordinator)
self._name = name self._name = name
self._attrs = {}
self._unit_system = "Metric" if self.coordinator.is_metric else "Imperial" self._unit_system = "Metric" if self.coordinator.is_metric else "Imperial"
@property @property
def name(self): def name(self) -> str:
"""Return the name.""" """Return the name."""
return self._name return self._name
@property @property
def attribution(self): def attribution(self) -> str:
"""Return the attribution.""" """Return the attribution."""
return ATTRIBUTION return ATTRIBUTION
@property @property
def unique_id(self): def unique_id(self) -> str:
"""Return a unique_id for this entity.""" """Return a unique_id for this entity."""
return self.coordinator.location_key return self.coordinator.location_key
@property @property
def device_info(self): def device_info(self) -> DeviceInfo:
"""Return the device info.""" """Return the device info."""
return { return {
"identifiers": {(DOMAIN, self.coordinator.location_key)}, "identifiers": {(DOMAIN, self.coordinator.location_key)},
@ -74,7 +89,7 @@ class AccuWeatherEntity(CoordinatorEntity, WeatherEntity):
} }
@property @property
def condition(self): def condition(self) -> str | None:
"""Return the current condition.""" """Return the current condition."""
try: try:
return [ return [
@ -86,52 +101,60 @@ class AccuWeatherEntity(CoordinatorEntity, WeatherEntity):
return None return None
@property @property
def temperature(self): def temperature(self) -> float:
"""Return the temperature.""" """Return the temperature."""
return self.coordinator.data["Temperature"][self._unit_system]["Value"] return cast(
float, self.coordinator.data["Temperature"][self._unit_system]["Value"]
)
@property @property
def temperature_unit(self): def temperature_unit(self) -> str:
"""Return the unit of measurement.""" """Return the unit of measurement."""
return TEMP_CELSIUS if self.coordinator.is_metric else TEMP_FAHRENHEIT return TEMP_CELSIUS if self.coordinator.is_metric else TEMP_FAHRENHEIT
@property @property
def pressure(self): def pressure(self) -> float:
"""Return the pressure.""" """Return the pressure."""
return self.coordinator.data["Pressure"][self._unit_system]["Value"] return cast(
float, self.coordinator.data["Pressure"][self._unit_system]["Value"]
)
@property @property
def humidity(self): def humidity(self) -> int:
"""Return the humidity.""" """Return the humidity."""
return self.coordinator.data["RelativeHumidity"] return cast(int, self.coordinator.data["RelativeHumidity"])
@property @property
def wind_speed(self): def wind_speed(self) -> float:
"""Return the wind speed.""" """Return the wind speed."""
return self.coordinator.data["Wind"]["Speed"][self._unit_system]["Value"] return cast(
float, self.coordinator.data["Wind"]["Speed"][self._unit_system]["Value"]
)
@property @property
def wind_bearing(self): def wind_bearing(self) -> int:
"""Return the wind bearing.""" """Return the wind bearing."""
return self.coordinator.data["Wind"]["Direction"]["Degrees"] return cast(int, self.coordinator.data["Wind"]["Direction"]["Degrees"])
@property @property
def visibility(self): def visibility(self) -> float:
"""Return the visibility.""" """Return the visibility."""
return self.coordinator.data["Visibility"][self._unit_system]["Value"] return cast(
float, self.coordinator.data["Visibility"][self._unit_system]["Value"]
)
@property @property
def ozone(self): def ozone(self) -> int | None:
"""Return the ozone level.""" """Return the ozone level."""
# We only have ozone data for certain locations and only in the forecast data. # We only have ozone data for certain locations and only in the forecast data.
if self.coordinator.forecast and self.coordinator.data[ATTR_FORECAST][0].get( if self.coordinator.forecast and self.coordinator.data[ATTR_FORECAST][0].get(
"Ozone" "Ozone"
): ):
return self.coordinator.data[ATTR_FORECAST][0]["Ozone"]["Value"] return cast(int, self.coordinator.data[ATTR_FORECAST][0]["Ozone"]["Value"])
return None return None
@property @property
def forecast(self): def forecast(self) -> list[dict[str, Any]] | None:
"""Return the forecast array.""" """Return the forecast array."""
if not self.coordinator.forecast: if not self.coordinator.forecast:
return None return None
@ -161,7 +184,7 @@ class AccuWeatherEntity(CoordinatorEntity, WeatherEntity):
return forecast return forecast
@staticmethod @staticmethod
def _calc_precipitation(day: dict) -> float: def _calc_precipitation(day: dict[str, Any]) -> float:
"""Return sum of the precipitation.""" """Return sum of the precipitation."""
precip_sum = 0 precip_sum = 0
precip_types = ["Rain", "Snow", "Ice"] precip_types = ["Rain", "Snow", "Ice"]

View File

@ -55,6 +55,17 @@ no_implicit_optional = true
warn_return_any = true warn_return_any = true
warn_unreachable = true warn_unreachable = true
[mypy-homeassistant.components.accuweather.*]
check_untyped_defs = true
disallow_incomplete_defs = true
disallow_subclassing_any = true
disallow_untyped_calls = true
disallow_untyped_decorators = true
disallow_untyped_defs = true
no_implicit_optional = true
warn_return_any = true
warn_unreachable = true
[mypy-homeassistant.components.actiontec.*] [mypy-homeassistant.components.actiontec.*]
check_untyped_defs = true check_untyped_defs = true
disallow_incomplete_defs = true disallow_incomplete_defs = true

View File

@ -1,6 +1,6 @@
"""Tests for AccuWeather.""" """Tests for AccuWeather."""
import json import json
from unittest.mock import patch from unittest.mock import PropertyMock, patch
from homeassistant.components.accuweather.const import DOMAIN from homeassistant.components.accuweather.const import DOMAIN
@ -40,6 +40,10 @@ async def init_integration(
), patch( ), patch(
"homeassistant.components.accuweather.AccuWeather.async_get_forecast", "homeassistant.components.accuweather.AccuWeather.async_get_forecast",
return_value=forecast, return_value=forecast,
), patch(
"homeassistant.components.accuweather.AccuWeather.requests_remaining",
new_callable=PropertyMock,
return_value=10,
): ):
entry.add_to_hass(hass) entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id) await hass.config_entries.async_setup(entry.entry_id)

View File

@ -1,6 +1,6 @@
"""Define tests for the AccuWeather config flow.""" """Define tests for the AccuWeather config flow."""
import json import json
from unittest.mock import patch from unittest.mock import PropertyMock, patch
from accuweather import ApiError, InvalidApiKeyError, RequestsExceededError from accuweather import ApiError, InvalidApiKeyError, RequestsExceededError
@ -50,7 +50,7 @@ async def test_api_key_too_short(hass):
async def test_invalid_api_key(hass): async def test_invalid_api_key(hass):
"""Test that errors are shown when API key is invalid.""" """Test that errors are shown when API key is invalid."""
with patch( with patch(
"accuweather.AccuWeather._async_get_data", "homeassistant.components.accuweather.AccuWeather._async_get_data",
side_effect=InvalidApiKeyError("Invalid API key"), side_effect=InvalidApiKeyError("Invalid API key"),
): ):
@ -66,7 +66,7 @@ async def test_invalid_api_key(hass):
async def test_api_error(hass): async def test_api_error(hass):
"""Test API error.""" """Test API error."""
with patch( with patch(
"accuweather.AccuWeather._async_get_data", "homeassistant.components.accuweather.AccuWeather._async_get_data",
side_effect=ApiError("Invalid response from AccuWeather API"), side_effect=ApiError("Invalid response from AccuWeather API"),
): ):
@ -82,7 +82,7 @@ async def test_api_error(hass):
async def test_requests_exceeded_error(hass): async def test_requests_exceeded_error(hass):
"""Test requests exceeded error.""" """Test requests exceeded error."""
with patch( with patch(
"accuweather.AccuWeather._async_get_data", "homeassistant.components.accuweather.AccuWeather._async_get_data",
side_effect=RequestsExceededError( side_effect=RequestsExceededError(
"The allowed number of requests has been exceeded" "The allowed number of requests has been exceeded"
), ),
@ -100,7 +100,7 @@ async def test_requests_exceeded_error(hass):
async def test_integration_already_exists(hass): async def test_integration_already_exists(hass):
"""Test we only allow a single config flow.""" """Test we only allow a single config flow."""
with patch( with patch(
"accuweather.AccuWeather._async_get_data", "homeassistant.components.accuweather.AccuWeather._async_get_data",
return_value=json.loads(load_fixture("accuweather/location_data.json")), return_value=json.loads(load_fixture("accuweather/location_data.json")),
): ):
MockConfigEntry( MockConfigEntry(
@ -122,7 +122,7 @@ async def test_integration_already_exists(hass):
async def test_create_entry(hass): async def test_create_entry(hass):
"""Test that the user step works.""" """Test that the user step works."""
with patch( with patch(
"accuweather.AccuWeather._async_get_data", "homeassistant.components.accuweather.AccuWeather._async_get_data",
return_value=json.loads(load_fixture("accuweather/location_data.json")), return_value=json.loads(load_fixture("accuweather/location_data.json")),
), patch( ), patch(
"homeassistant.components.accuweather.async_setup_entry", return_value=True "homeassistant.components.accuweather.async_setup_entry", return_value=True
@ -152,15 +152,19 @@ async def test_options_flow(hass):
config_entry.add_to_hass(hass) config_entry.add_to_hass(hass)
with patch( with patch(
"accuweather.AccuWeather._async_get_data", "homeassistant.components.accuweather.AccuWeather._async_get_data",
return_value=json.loads(load_fixture("accuweather/location_data.json")), return_value=json.loads(load_fixture("accuweather/location_data.json")),
), patch( ), patch(
"accuweather.AccuWeather.async_get_current_conditions", "homeassistant.components.accuweather.AccuWeather.async_get_current_conditions",
return_value=json.loads( return_value=json.loads(
load_fixture("accuweather/current_conditions_data.json") load_fixture("accuweather/current_conditions_data.json")
), ),
), patch( ), patch(
"accuweather.AccuWeather.async_get_forecast" "homeassistant.components.accuweather.AccuWeather.async_get_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) assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()

View File

@ -1,7 +1,7 @@
"""Test sensor of AccuWeather integration.""" """Test sensor of AccuWeather integration."""
from datetime import timedelta from datetime import timedelta
import json import json
from unittest.mock import patch from unittest.mock import PropertyMock, patch
from homeassistant.components.accuweather.const import ATTRIBUTION, DOMAIN from homeassistant.components.accuweather.const import ATTRIBUTION, DOMAIN
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
@ -13,6 +13,7 @@ from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT, ATTR_UNIT_OF_MEASUREMENT,
CONCENTRATION_PARTS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_CUBIC_METER,
DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_TEMPERATURE,
LENGTH_FEET,
LENGTH_METERS, LENGTH_METERS,
LENGTH_MILLIMETERS, LENGTH_MILLIMETERS,
PERCENTAGE, PERCENTAGE,
@ -25,6 +26,7 @@ from homeassistant.const import (
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from homeassistant.util.dt import utcnow from homeassistant.util.dt import utcnow
from homeassistant.util.unit_system import IMPERIAL_SYSTEM
from tests.common import async_fire_time_changed, load_fixture from tests.common import async_fire_time_changed, load_fixture
from tests.components.accuweather import init_integration from tests.components.accuweather import init_integration
@ -616,6 +618,10 @@ async def test_availability(hass):
return_value=json.loads( return_value=json.loads(
load_fixture("accuweather/current_conditions_data.json") load_fixture("accuweather/current_conditions_data.json")
), ),
), patch(
"homeassistant.components.accuweather.AccuWeather.requests_remaining",
new_callable=PropertyMock,
return_value=10,
): ):
async_fire_time_changed(hass, future) async_fire_time_changed(hass, future)
await hass.async_block_till_done() await hass.async_block_till_done()
@ -641,7 +647,11 @@ async def test_manual_update_entity(hass):
) as mock_current, patch( ) as mock_current, patch(
"homeassistant.components.accuweather.AccuWeather.async_get_forecast", "homeassistant.components.accuweather.AccuWeather.async_get_forecast",
return_value=forecast, return_value=forecast,
) as mock_forecast: ) as mock_forecast, patch(
"homeassistant.components.accuweather.AccuWeather.requests_remaining",
new_callable=PropertyMock,
return_value=10,
):
await hass.services.async_call( await hass.services.async_call(
"homeassistant", "homeassistant",
"update_entity", "update_entity",
@ -650,3 +660,16 @@ async def test_manual_update_entity(hass):
) )
assert mock_current.call_count == 1 assert mock_current.call_count == 1
assert mock_forecast.call_count == 1 assert mock_forecast.call_count == 1
async def test_sensor_imperial_units(hass):
"""Test states of the sensor without forecast."""
hass.config.units = IMPERIAL_SYSTEM
await init_integration(hass)
state = hass.states.get("sensor.home_cloud_ceiling")
assert state
assert state.state == "10500"
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert state.attributes.get(ATTR_ICON) == "mdi:weather-fog"
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == LENGTH_FEET

View File

@ -1,7 +1,7 @@
"""Test weather of AccuWeather integration.""" """Test weather of AccuWeather integration."""
from datetime import timedelta from datetime import timedelta
import json import json
from unittest.mock import patch from unittest.mock import PropertyMock, patch
from homeassistant.components.accuweather.const import ATTRIBUTION from homeassistant.components.accuweather.const import ATTRIBUTION
from homeassistant.components.weather import ( from homeassistant.components.weather import (
@ -112,6 +112,10 @@ async def test_availability(hass):
return_value=json.loads( return_value=json.loads(
load_fixture("accuweather/current_conditions_data.json") load_fixture("accuweather/current_conditions_data.json")
), ),
), patch(
"homeassistant.components.accuweather.AccuWeather.requests_remaining",
new_callable=PropertyMock,
return_value=10,
): ):
async_fire_time_changed(hass, future) async_fire_time_changed(hass, future)
await hass.async_block_till_done() await hass.async_block_till_done()
@ -137,7 +141,11 @@ async def test_manual_update_entity(hass):
) as mock_current, patch( ) as mock_current, patch(
"homeassistant.components.accuweather.AccuWeather.async_get_forecast", "homeassistant.components.accuweather.AccuWeather.async_get_forecast",
return_value=forecast, return_value=forecast,
) as mock_forecast: ) as mock_forecast, patch(
"homeassistant.components.accuweather.AccuWeather.requests_remaining",
new_callable=PropertyMock,
return_value=10,
):
await hass.services.async_call( await hass.services.async_call(
"homeassistant", "homeassistant",
"update_entity", "update_entity",