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
13 changed files with 473 additions and 337 deletions

View File

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

View File

@ -1,12 +1,18 @@
"""The AccuWeather component."""
from __future__ import annotations
from datetime import timedelta
import logging
from typing import Any, Dict
from accuweather import AccuWeather, ApiError, InvalidApiKeyError, RequestsExceededError
from aiohttp import ClientSession
from aiohttp.client_exceptions import ClientConnectorError
from async_timeout import timeout
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
@ -23,11 +29,12 @@ _LOGGER = logging.getLogger(__name__)
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."""
api_key = config_entry.data[CONF_API_KEY]
location_key = config_entry.unique_id
forecast = config_entry.options.get(CONF_FORECAST, False)
api_key: str = entry.data[CONF_API_KEY]
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)
@ -38,41 +45,46 @@ async def async_setup_entry(hass, config_entry) -> bool:
)
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,
UNDO_UPDATE_LISTENER: undo_listener,
}
hass.config_entries.async_setup_platforms(config_entry, PLATFORMS)
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
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_ok = await hass.config_entries.async_unload_platforms(
config_entry, PLATFORMS
)
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
hass.data[DOMAIN][config_entry.entry_id][UNDO_UPDATE_LISTENER]()
hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]()
if unload_ok:
hass.data[DOMAIN].pop(config_entry.entry_id)
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
async def update_listener(hass, config_entry):
async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""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."""
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."""
self.location_key = location_key
self.forecast = forecast
@ -87,11 +99,11 @@ class AccuWeatherDataUpdateCoordinator(DataUpdateCoordinator):
update_interval = timedelta(minutes=40)
if self.forecast:
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)
async def _async_update_data(self):
async def _async_update_data(self) -> dict[str, Any]:
"""Update data via library."""
try:
async with timeout(10):
@ -108,5 +120,5 @@ class AccuWeatherDataUpdateCoordinator(DataUpdateCoordinator):
RequestsExceededError,
) as 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}}

View File

@ -1,5 +1,8 @@
"""Adds config flow for AccuWeather."""
from __future__ import annotations
import asyncio
from typing import Any
from accuweather import AccuWeather, ApiError, InvalidApiKeyError, RequestsExceededError
from aiohttp import ClientError
@ -8,8 +11,10 @@ from async_timeout import timeout
import voluptuous as vol
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.core import callback
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
@ -21,7 +26,9 @@ class AccuWeatherFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
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."""
# 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.
@ -77,7 +84,9 @@ class AccuWeatherFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
@staticmethod
@callback
def async_get_options_flow(config_entry):
def async_get_options_flow(
config_entry: ConfigEntry,
) -> AccuWeatherOptionsFlowHandler:
"""Options callback for AccuWeather."""
return AccuWeatherOptionsFlowHandler(config_entry)
@ -85,15 +94,19 @@ class AccuWeatherFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
class AccuWeatherOptionsFlowHandler(config_entries.OptionsFlow):
"""Config flow options for AccuWeather."""
def __init__(self, config_entry):
def __init__(self, entry: ConfigEntry) -> None:
"""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."""
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."""
if user_input is not None:
return self.async_create_entry(title="", data=user_input)

View File

@ -1,4 +1,8 @@
"""Constants for AccuWeather integration."""
from __future__ import annotations
from typing import Final
from homeassistant.components.weather import (
ATTR_CONDITION_CLEAR_NIGHT,
ATTR_CONDITION_CLOUDY,
@ -16,8 +20,6 @@ from homeassistant.components.weather import (
ATTR_CONDITION_WINDY,
)
from homeassistant.const import (
ATTR_DEVICE_CLASS,
ATTR_ICON,
CONCENTRATION_PARTS_PER_CUBIC_METER,
DEVICE_CLASS_TEMPERATURE,
LENGTH_FEET,
@ -33,18 +35,19 @@ from homeassistant.const import (
UV_INDEX,
)
ATTRIBUTION = "Data provided by AccuWeather"
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"
from .model import SensorDescription
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_CLOUDY: [7, 8, 38],
ATTR_CONDITION_EXCEPTIONAL: [24, 30, 31],
@ -61,255 +64,264 @@ CONDITION_CLASSES = {
ATTR_CONDITION_WINDY: [32],
}
FORECAST_DAYS = [0, 1, 2, 3, 4]
FORECAST_SENSOR_TYPES = {
FORECAST_SENSOR_TYPES: Final[dict[str, SensorDescription]] = {
"CloudCoverDay": {
ATTR_DEVICE_CLASS: None,
ATTR_ICON: "mdi:weather-cloudy",
ATTR_LABEL: "Cloud Cover Day",
ATTR_UNIT_METRIC: PERCENTAGE,
ATTR_UNIT_IMPERIAL: PERCENTAGE,
"device_class": None,
"icon": "mdi:weather-cloudy",
"label": "Cloud Cover Day",
"unit_metric": PERCENTAGE,
"unit_imperial": PERCENTAGE,
"enabled": False,
},
"CloudCoverNight": {
ATTR_DEVICE_CLASS: None,
ATTR_ICON: "mdi:weather-cloudy",
ATTR_LABEL: "Cloud Cover Night",
ATTR_UNIT_METRIC: PERCENTAGE,
ATTR_UNIT_IMPERIAL: PERCENTAGE,
"device_class": None,
"icon": "mdi:weather-cloudy",
"label": "Cloud Cover Night",
"unit_metric": PERCENTAGE,
"unit_imperial": PERCENTAGE,
"enabled": False,
},
"Grass": {
ATTR_DEVICE_CLASS: None,
ATTR_ICON: "mdi:grass",
ATTR_LABEL: "Grass Pollen",
ATTR_UNIT_METRIC: CONCENTRATION_PARTS_PER_CUBIC_METER,
ATTR_UNIT_IMPERIAL: CONCENTRATION_PARTS_PER_CUBIC_METER,
"device_class": None,
"icon": "mdi:grass",
"label": "Grass Pollen",
"unit_metric": CONCENTRATION_PARTS_PER_CUBIC_METER,
"unit_imperial": CONCENTRATION_PARTS_PER_CUBIC_METER,
"enabled": False,
},
"HoursOfSun": {
ATTR_DEVICE_CLASS: None,
ATTR_ICON: "mdi:weather-partly-cloudy",
ATTR_LABEL: "Hours Of Sun",
ATTR_UNIT_METRIC: TIME_HOURS,
ATTR_UNIT_IMPERIAL: TIME_HOURS,
"device_class": None,
"icon": "mdi:weather-partly-cloudy",
"label": "Hours Of Sun",
"unit_metric": TIME_HOURS,
"unit_imperial": TIME_HOURS,
"enabled": True,
},
"Mold": {
ATTR_DEVICE_CLASS: None,
ATTR_ICON: "mdi:blur",
ATTR_LABEL: "Mold Pollen",
ATTR_UNIT_METRIC: CONCENTRATION_PARTS_PER_CUBIC_METER,
ATTR_UNIT_IMPERIAL: CONCENTRATION_PARTS_PER_CUBIC_METER,
"device_class": None,
"icon": "mdi:blur",
"label": "Mold Pollen",
"unit_metric": CONCENTRATION_PARTS_PER_CUBIC_METER,
"unit_imperial": CONCENTRATION_PARTS_PER_CUBIC_METER,
"enabled": False,
},
"Ozone": {
ATTR_DEVICE_CLASS: None,
ATTR_ICON: "mdi:vector-triangle",
ATTR_LABEL: "Ozone",
ATTR_UNIT_METRIC: None,
ATTR_UNIT_IMPERIAL: None,
"device_class": None,
"icon": "mdi:vector-triangle",
"label": "Ozone",
"unit_metric": None,
"unit_imperial": None,
"enabled": False,
},
"Ragweed": {
ATTR_DEVICE_CLASS: None,
ATTR_ICON: "mdi:sprout",
ATTR_LABEL: "Ragweed Pollen",
ATTR_UNIT_METRIC: CONCENTRATION_PARTS_PER_CUBIC_METER,
ATTR_UNIT_IMPERIAL: CONCENTRATION_PARTS_PER_CUBIC_METER,
"device_class": None,
"icon": "mdi:sprout",
"label": "Ragweed Pollen",
"unit_metric": CONCENTRATION_PARTS_PER_CUBIC_METER,
"unit_imperial": CONCENTRATION_PARTS_PER_CUBIC_METER,
"enabled": False,
},
"RealFeelTemperatureMax": {
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
ATTR_ICON: None,
ATTR_LABEL: "RealFeel Temperature Max",
ATTR_UNIT_METRIC: TEMP_CELSIUS,
ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT,
"device_class": DEVICE_CLASS_TEMPERATURE,
"icon": None,
"label": "RealFeel Temperature Max",
"unit_metric": TEMP_CELSIUS,
"unit_imperial": TEMP_FAHRENHEIT,
"enabled": True,
},
"RealFeelTemperatureMin": {
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
ATTR_ICON: None,
ATTR_LABEL: "RealFeel Temperature Min",
ATTR_UNIT_METRIC: TEMP_CELSIUS,
ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT,
"device_class": DEVICE_CLASS_TEMPERATURE,
"icon": None,
"label": "RealFeel Temperature Min",
"unit_metric": TEMP_CELSIUS,
"unit_imperial": TEMP_FAHRENHEIT,
"enabled": True,
},
"RealFeelTemperatureShadeMax": {
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
ATTR_ICON: None,
ATTR_LABEL: "RealFeel Temperature Shade Max",
ATTR_UNIT_METRIC: TEMP_CELSIUS,
ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT,
"device_class": DEVICE_CLASS_TEMPERATURE,
"icon": None,
"label": "RealFeel Temperature Shade Max",
"unit_metric": TEMP_CELSIUS,
"unit_imperial": TEMP_FAHRENHEIT,
"enabled": False,
},
"RealFeelTemperatureShadeMin": {
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
ATTR_ICON: None,
ATTR_LABEL: "RealFeel Temperature Shade Min",
ATTR_UNIT_METRIC: TEMP_CELSIUS,
ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT,
"device_class": DEVICE_CLASS_TEMPERATURE,
"icon": None,
"label": "RealFeel Temperature Shade Min",
"unit_metric": TEMP_CELSIUS,
"unit_imperial": TEMP_FAHRENHEIT,
"enabled": False,
},
"ThunderstormProbabilityDay": {
ATTR_DEVICE_CLASS: None,
ATTR_ICON: "mdi:weather-lightning",
ATTR_LABEL: "Thunderstorm Probability Day",
ATTR_UNIT_METRIC: PERCENTAGE,
ATTR_UNIT_IMPERIAL: PERCENTAGE,
"device_class": None,
"icon": "mdi:weather-lightning",
"label": "Thunderstorm Probability Day",
"unit_metric": PERCENTAGE,
"unit_imperial": PERCENTAGE,
"enabled": True,
},
"ThunderstormProbabilityNight": {
ATTR_DEVICE_CLASS: None,
ATTR_ICON: "mdi:weather-lightning",
ATTR_LABEL: "Thunderstorm Probability Night",
ATTR_UNIT_METRIC: PERCENTAGE,
ATTR_UNIT_IMPERIAL: PERCENTAGE,
"device_class": None,
"icon": "mdi:weather-lightning",
"label": "Thunderstorm Probability Night",
"unit_metric": PERCENTAGE,
"unit_imperial": PERCENTAGE,
"enabled": True,
},
"Tree": {
ATTR_DEVICE_CLASS: None,
ATTR_ICON: "mdi:tree-outline",
ATTR_LABEL: "Tree Pollen",
ATTR_UNIT_METRIC: CONCENTRATION_PARTS_PER_CUBIC_METER,
ATTR_UNIT_IMPERIAL: CONCENTRATION_PARTS_PER_CUBIC_METER,
"device_class": None,
"icon": "mdi:tree-outline",
"label": "Tree Pollen",
"unit_metric": CONCENTRATION_PARTS_PER_CUBIC_METER,
"unit_imperial": CONCENTRATION_PARTS_PER_CUBIC_METER,
"enabled": False,
},
"UVIndex": {
ATTR_DEVICE_CLASS: None,
ATTR_ICON: "mdi:weather-sunny",
ATTR_LABEL: "UV Index",
ATTR_UNIT_METRIC: UV_INDEX,
ATTR_UNIT_IMPERIAL: UV_INDEX,
"device_class": None,
"icon": "mdi:weather-sunny",
"label": "UV Index",
"unit_metric": UV_INDEX,
"unit_imperial": UV_INDEX,
"enabled": True,
},
"WindGustDay": {
ATTR_DEVICE_CLASS: None,
ATTR_ICON: "mdi:weather-windy",
ATTR_LABEL: "Wind Gust Day",
ATTR_UNIT_METRIC: SPEED_KILOMETERS_PER_HOUR,
ATTR_UNIT_IMPERIAL: SPEED_MILES_PER_HOUR,
"device_class": None,
"icon": "mdi:weather-windy",
"label": "Wind Gust Day",
"unit_metric": SPEED_KILOMETERS_PER_HOUR,
"unit_imperial": SPEED_MILES_PER_HOUR,
"enabled": False,
},
"WindGustNight": {
ATTR_DEVICE_CLASS: None,
ATTR_ICON: "mdi:weather-windy",
ATTR_LABEL: "Wind Gust Night",
ATTR_UNIT_METRIC: SPEED_KILOMETERS_PER_HOUR,
ATTR_UNIT_IMPERIAL: SPEED_MILES_PER_HOUR,
"device_class": None,
"icon": "mdi:weather-windy",
"label": "Wind Gust Night",
"unit_metric": SPEED_KILOMETERS_PER_HOUR,
"unit_imperial": SPEED_MILES_PER_HOUR,
"enabled": False,
},
"WindDay": {
ATTR_DEVICE_CLASS: None,
ATTR_ICON: "mdi:weather-windy",
ATTR_LABEL: "Wind Day",
ATTR_UNIT_METRIC: SPEED_KILOMETERS_PER_HOUR,
ATTR_UNIT_IMPERIAL: SPEED_MILES_PER_HOUR,
"device_class": None,
"icon": "mdi:weather-windy",
"label": "Wind Day",
"unit_metric": SPEED_KILOMETERS_PER_HOUR,
"unit_imperial": SPEED_MILES_PER_HOUR,
"enabled": True,
},
"WindNight": {
ATTR_DEVICE_CLASS: None,
ATTR_ICON: "mdi:weather-windy",
ATTR_LABEL: "Wind Night",
ATTR_UNIT_METRIC: SPEED_KILOMETERS_PER_HOUR,
ATTR_UNIT_IMPERIAL: SPEED_MILES_PER_HOUR,
"device_class": None,
"icon": "mdi:weather-windy",
"label": "Wind Night",
"unit_metric": SPEED_KILOMETERS_PER_HOUR,
"unit_imperial": SPEED_MILES_PER_HOUR,
"enabled": True,
},
}
OPTIONAL_SENSORS = (
"ApparentTemperature",
"CloudCover",
"CloudCoverDay",
"CloudCoverNight",
"DewPoint",
"Grass",
"Mold",
"Ozone",
"Ragweed",
"RealFeelTemperatureShade",
"RealFeelTemperatureShadeMax",
"RealFeelTemperatureShadeMin",
"Tree",
"WetBulbTemperature",
"WindChillTemperature",
"WindGust",
"WindGustDay",
"WindGustNight",
)
SENSOR_TYPES = {
SENSOR_TYPES: Final[dict[str, SensorDescription]] = {
"ApparentTemperature": {
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
ATTR_ICON: None,
ATTR_LABEL: "Apparent Temperature",
ATTR_UNIT_METRIC: TEMP_CELSIUS,
ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT,
"device_class": DEVICE_CLASS_TEMPERATURE,
"icon": None,
"label": "Apparent Temperature",
"unit_metric": TEMP_CELSIUS,
"unit_imperial": TEMP_FAHRENHEIT,
"enabled": False,
},
"Ceiling": {
ATTR_DEVICE_CLASS: None,
ATTR_ICON: "mdi:weather-fog",
ATTR_LABEL: "Cloud Ceiling",
ATTR_UNIT_METRIC: LENGTH_METERS,
ATTR_UNIT_IMPERIAL: LENGTH_FEET,
"device_class": None,
"icon": "mdi:weather-fog",
"label": "Cloud Ceiling",
"unit_metric": LENGTH_METERS,
"unit_imperial": LENGTH_FEET,
"enabled": True,
},
"CloudCover": {
ATTR_DEVICE_CLASS: None,
ATTR_ICON: "mdi:weather-cloudy",
ATTR_LABEL: "Cloud Cover",
ATTR_UNIT_METRIC: PERCENTAGE,
ATTR_UNIT_IMPERIAL: PERCENTAGE,
"device_class": None,
"icon": "mdi:weather-cloudy",
"label": "Cloud Cover",
"unit_metric": PERCENTAGE,
"unit_imperial": PERCENTAGE,
"enabled": False,
},
"DewPoint": {
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
ATTR_ICON: None,
ATTR_LABEL: "Dew Point",
ATTR_UNIT_METRIC: TEMP_CELSIUS,
ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT,
"device_class": DEVICE_CLASS_TEMPERATURE,
"icon": None,
"label": "Dew Point",
"unit_metric": TEMP_CELSIUS,
"unit_imperial": TEMP_FAHRENHEIT,
"enabled": False,
},
"RealFeelTemperature": {
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
ATTR_ICON: None,
ATTR_LABEL: "RealFeel Temperature",
ATTR_UNIT_METRIC: TEMP_CELSIUS,
ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT,
"device_class": DEVICE_CLASS_TEMPERATURE,
"icon": None,
"label": "RealFeel Temperature",
"unit_metric": TEMP_CELSIUS,
"unit_imperial": TEMP_FAHRENHEIT,
"enabled": True,
},
"RealFeelTemperatureShade": {
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
ATTR_ICON: None,
ATTR_LABEL: "RealFeel Temperature Shade",
ATTR_UNIT_METRIC: TEMP_CELSIUS,
ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT,
"device_class": DEVICE_CLASS_TEMPERATURE,
"icon": None,
"label": "RealFeel Temperature Shade",
"unit_metric": TEMP_CELSIUS,
"unit_imperial": TEMP_FAHRENHEIT,
"enabled": False,
},
"Precipitation": {
ATTR_DEVICE_CLASS: None,
ATTR_ICON: "mdi:weather-rainy",
ATTR_LABEL: "Precipitation",
ATTR_UNIT_METRIC: LENGTH_MILLIMETERS,
ATTR_UNIT_IMPERIAL: LENGTH_INCHES,
"device_class": None,
"icon": "mdi:weather-rainy",
"label": "Precipitation",
"unit_metric": LENGTH_MILLIMETERS,
"unit_imperial": LENGTH_INCHES,
"enabled": True,
},
"PressureTendency": {
ATTR_DEVICE_CLASS: "accuweather__pressure_tendency",
ATTR_ICON: "mdi:gauge",
ATTR_LABEL: "Pressure Tendency",
ATTR_UNIT_METRIC: None,
ATTR_UNIT_IMPERIAL: None,
"device_class": "accuweather__pressure_tendency",
"icon": "mdi:gauge",
"label": "Pressure Tendency",
"unit_metric": None,
"unit_imperial": None,
"enabled": True,
},
"UVIndex": {
ATTR_DEVICE_CLASS: None,
ATTR_ICON: "mdi:weather-sunny",
ATTR_LABEL: "UV Index",
ATTR_UNIT_METRIC: UV_INDEX,
ATTR_UNIT_IMPERIAL: UV_INDEX,
"device_class": None,
"icon": "mdi:weather-sunny",
"label": "UV Index",
"unit_metric": UV_INDEX,
"unit_imperial": UV_INDEX,
"enabled": True,
},
"WetBulbTemperature": {
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
ATTR_ICON: None,
ATTR_LABEL: "Wet Bulb Temperature",
ATTR_UNIT_METRIC: TEMP_CELSIUS,
ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT,
"device_class": DEVICE_CLASS_TEMPERATURE,
"icon": None,
"label": "Wet Bulb Temperature",
"unit_metric": TEMP_CELSIUS,
"unit_imperial": TEMP_FAHRENHEIT,
"enabled": False,
},
"WindChillTemperature": {
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
ATTR_ICON: None,
ATTR_LABEL: "Wind Chill Temperature",
ATTR_UNIT_METRIC: TEMP_CELSIUS,
ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT,
"device_class": DEVICE_CLASS_TEMPERATURE,
"icon": None,
"label": "Wind Chill Temperature",
"unit_metric": TEMP_CELSIUS,
"unit_imperial": TEMP_FAHRENHEIT,
"enabled": False,
},
"Wind": {
ATTR_DEVICE_CLASS: None,
ATTR_ICON: "mdi:weather-windy",
ATTR_LABEL: "Wind",
ATTR_UNIT_METRIC: SPEED_KILOMETERS_PER_HOUR,
ATTR_UNIT_IMPERIAL: SPEED_MILES_PER_HOUR,
"device_class": None,
"icon": "mdi:weather-windy",
"label": "Wind",
"unit_metric": SPEED_KILOMETERS_PER_HOUR,
"unit_imperial": SPEED_MILES_PER_HOUR,
"enabled": True,
},
"WindGust": {
ATTR_DEVICE_CLASS: None,
ATTR_ICON: "mdi:weather-windy",
ATTR_LABEL: "Wind Gust",
ATTR_UNIT_METRIC: SPEED_KILOMETERS_PER_HOUR,
ATTR_UNIT_IMPERIAL: SPEED_MILES_PER_HOUR,
"device_class": None,
"icon": "mdi:weather-windy",
"label": "Wind Gust",
"unit_metric": SPEED_KILOMETERS_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."""
from __future__ import annotations
from typing import Any, cast
from homeassistant.components.sensor import SensorEntity
from homeassistant.const import (
ATTR_ATTRIBUTION,
ATTR_DEVICE_CLASS,
CONF_NAME,
DEVICE_CLASS_TEMPERATURE,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME, DEVICE_CLASS_TEMPERATURE
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import AccuWeatherDataUpdateCoordinator
from .const import (
ATTR_FORECAST,
ATTR_ICON,
ATTR_LABEL,
ATTRIBUTION,
COORDINATOR,
DOMAIN,
FORECAST_DAYS,
FORECAST_SENSOR_TYPES,
MANUFACTURER,
MAX_FORECAST_DAYS,
NAME,
OPTIONAL_SENSORS,
SENSOR_TYPES,
)
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."""
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:
sensors.append(AccuWeatherSensor(name, sensor, coordinator))
if coordinator.forecast:
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
# locations.
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)
)
async_add_entities(sensors, False)
async_add_entities(sensors)
class AccuWeatherSensor(CoordinatorEntity, SensorEntity):
"""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."""
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.kind = kind
self._device_class = None
self._attrs = {ATTR_ATTRIBUTION: ATTRIBUTION}
self._unit_system = "Metric" if self.coordinator.is_metric else "Imperial"
self.forecast_day = forecast_day
@property
def name(self):
def name(self) -> str:
"""Return the name."""
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} {SENSOR_TYPES[self.kind][ATTR_LABEL]}"
return f"{self._name} {self._description['label']} {self.forecast_day}d"
return f"{self._name} {self._description['label']}"
@property
def unique_id(self):
def unique_id(self) -> str:
"""Return a unique_id for this entity."""
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}".lower()
@property
def device_info(self):
def device_info(self) -> DeviceInfo:
"""Return the device info."""
return {
"identifiers": {(DOMAIN, self.coordinator.location_key)},
@ -87,72 +111,54 @@ class AccuWeatherSensor(CoordinatorEntity, SensorEntity):
}
@property
def state(self):
def state(self) -> StateType:
"""Return the state."""
if self.forecast_day is not None:
if (
FORECAST_SENSOR_TYPES[self.kind][ATTR_DEVICE_CLASS]
== DEVICE_CLASS_TEMPERATURE
):
return self.coordinator.data[ATTR_FORECAST][self.forecast_day][
self.kind
]["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._description["device_class"] == DEVICE_CLASS_TEMPERATURE:
return cast(float, self._sensor_data["Value"])
if self.kind == "UVIndex":
return cast(int, self._sensor_data["Value"])
if self.kind in ["Grass", "Mold", "Ragweed", "Tree", "Ozone"]:
return cast(int, self._sensor_data["Value"])
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":
return self.coordinator.data[self.kind]["LocalizedText"].lower()
if SENSOR_TYPES[self.kind][ATTR_DEVICE_CLASS] == DEVICE_CLASS_TEMPERATURE:
return self.coordinator.data[self.kind][self._unit_system]["Value"]
return cast(str, self._sensor_data["LocalizedText"].lower())
if self._description["device_class"] == DEVICE_CLASS_TEMPERATURE:
return cast(float, self._sensor_data[self._unit_system]["Value"])
if self.kind == "Precipitation":
return self.coordinator.data["PrecipitationSummary"][self.kind][
self._unit_system
]["Value"]
return cast(float, self._sensor_data[self._unit_system]["Value"])
if self.kind in ["Wind", "WindGust"]:
return self.coordinator.data[self.kind]["Speed"][self._unit_system]["Value"]
return self.coordinator.data[self.kind]
return cast(float, self._sensor_data["Speed"][self._unit_system]["Value"])
if self.kind in ["WindDay", "WindNight", "WindGustDay", "WindGustNight"]:
return cast(StateType, self._sensor_data["Speed"]["Value"])
return cast(StateType, self._sensor_data)
@property
def icon(self):
def icon(self) -> str | None:
"""Return the icon."""
if self.forecast_day is not None:
return FORECAST_SENSOR_TYPES[self.kind][ATTR_ICON]
return SENSOR_TYPES[self.kind][ATTR_ICON]
return self._description["icon"]
@property
def device_class(self):
def device_class(self) -> str | None:
"""Return the device_class."""
if self.forecast_day is not None:
return FORECAST_SENSOR_TYPES[self.kind][ATTR_DEVICE_CLASS]
return SENSOR_TYPES[self.kind][ATTR_DEVICE_CLASS]
return self._description["device_class"]
@property
def unit_of_measurement(self):
def unit_of_measurement(self) -> str | None:
"""Return the unit the value is expressed in."""
if self.forecast_day is not None:
return FORECAST_SENSOR_TYPES[self.kind][self._unit_system]
return SENSOR_TYPES[self.kind][self._unit_system]
if self.coordinator.is_metric:
return self._description["unit_metric"]
return self._description["unit_imperial"]
@property
def extra_state_attributes(self):
def extra_state_attributes(self) -> dict[str, Any]:
"""Return the state attributes."""
if self.forecast_day is not None:
if self.kind in ["WindDay", "WindNight", "WindGustDay", "WindGustNight"]:
self._attrs["direction"] = self.coordinator.data[ATTR_FORECAST][
self.forecast_day
][self.kind]["Direction"]["English"]
self._attrs["direction"] = self._sensor_data["Direction"]["English"]
elif self.kind in ["Grass", "Mold", "Ragweed", "Tree", "UVIndex", "Ozone"]:
self._attrs["level"] = self.coordinator.data[ATTR_FORECAST][
self.forecast_day
][self.kind]["Category"]
self._attrs["level"] = self._sensor_data["Category"]
return self._attrs
if self.kind == "UVIndex":
self._attrs["level"] = self.coordinator.data["UVIndexText"]
@ -161,6 +167,6 @@ class AccuWeatherSensor(CoordinatorEntity, SensorEntity):
return self._attrs
@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 bool(self.kind not in OPTIONAL_SENSORS)
return self._description["enabled"]

View File

@ -1,4 +1,8 @@
"""Provide info to system health."""
from __future__ import annotations
from typing import Any
from accuweather.const import ENDPOINT
from homeassistant.components import system_health
@ -15,7 +19,7 @@ def async_register(
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."""
remaining_requests = list(hass.data[DOMAIN].values())[0][
COORDINATOR

View File

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

View File

@ -55,6 +55,17 @@ no_implicit_optional = true
warn_return_any = 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.*]
check_untyped_defs = true
disallow_incomplete_defs = true

View File

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

View File

@ -1,6 +1,6 @@
"""Define tests for the AccuWeather config flow."""
import json
from unittest.mock import patch
from unittest.mock import PropertyMock, patch
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):
"""Test that errors are shown when API key is invalid."""
with patch(
"accuweather.AccuWeather._async_get_data",
"homeassistant.components.accuweather.AccuWeather._async_get_data",
side_effect=InvalidApiKeyError("Invalid API key"),
):
@ -66,7 +66,7 @@ async def test_invalid_api_key(hass):
async def test_api_error(hass):
"""Test API error."""
with patch(
"accuweather.AccuWeather._async_get_data",
"homeassistant.components.accuweather.AccuWeather._async_get_data",
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):
"""Test requests exceeded error."""
with patch(
"accuweather.AccuWeather._async_get_data",
"homeassistant.components.accuweather.AccuWeather._async_get_data",
side_effect=RequestsExceededError(
"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):
"""Test we only allow a single config flow."""
with patch(
"accuweather.AccuWeather._async_get_data",
"homeassistant.components.accuweather.AccuWeather._async_get_data",
return_value=json.loads(load_fixture("accuweather/location_data.json")),
):
MockConfigEntry(
@ -122,7 +122,7 @@ async def test_integration_already_exists(hass):
async def test_create_entry(hass):
"""Test that the user step works."""
with patch(
"accuweather.AccuWeather._async_get_data",
"homeassistant.components.accuweather.AccuWeather._async_get_data",
return_value=json.loads(load_fixture("accuweather/location_data.json")),
), patch(
"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)
with patch(
"accuweather.AccuWeather._async_get_data",
"homeassistant.components.accuweather.AccuWeather._async_get_data",
return_value=json.loads(load_fixture("accuweather/location_data.json")),
), patch(
"accuweather.AccuWeather.async_get_current_conditions",
"homeassistant.components.accuweather.AccuWeather.async_get_current_conditions",
return_value=json.loads(
load_fixture("accuweather/current_conditions_data.json")
),
), 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)
await hass.async_block_till_done()

View File

@ -1,7 +1,7 @@
"""Test sensor of AccuWeather integration."""
from datetime import timedelta
import json
from unittest.mock import patch
from unittest.mock import PropertyMock, patch
from homeassistant.components.accuweather.const import ATTRIBUTION, DOMAIN
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
@ -13,6 +13,7 @@ from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT,
CONCENTRATION_PARTS_PER_CUBIC_METER,
DEVICE_CLASS_TEMPERATURE,
LENGTH_FEET,
LENGTH_METERS,
LENGTH_MILLIMETERS,
PERCENTAGE,
@ -25,6 +26,7 @@ from homeassistant.const import (
from homeassistant.helpers import entity_registry as er
from homeassistant.setup import async_setup_component
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.components.accuweather import init_integration
@ -616,6 +618,10 @@ async def test_availability(hass):
return_value=json.loads(
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)
await hass.async_block_till_done()
@ -641,7 +647,11 @@ async def test_manual_update_entity(hass):
) as mock_current, patch(
"homeassistant.components.accuweather.AccuWeather.async_get_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(
"homeassistant",
"update_entity",
@ -650,3 +660,16 @@ async def test_manual_update_entity(hass):
)
assert mock_current.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."""
from datetime import timedelta
import json
from unittest.mock import patch
from unittest.mock import PropertyMock, patch
from homeassistant.components.accuweather.const import ATTRIBUTION
from homeassistant.components.weather import (
@ -112,6 +112,10 @@ async def test_availability(hass):
return_value=json.loads(
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)
await hass.async_block_till_done()
@ -137,7 +141,11 @@ async def test_manual_update_entity(hass):
) as mock_current, patch(
"homeassistant.components.accuweather.AccuWeather.async_get_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(
"homeassistant",
"update_entity",