diff --git a/.strict-typing b/.strict-typing index 6d8b22493b6..1fbaaa39c30 100644 --- a/.strict-typing +++ b/.strict-typing @@ -4,6 +4,7 @@ homeassistant.components homeassistant.components.acer_projector.* +homeassistant.components.accuweather.* homeassistant.components.actiontec.* homeassistant.components.aftership.* homeassistant.components.air_quality.* diff --git a/homeassistant/components/accuweather/__init__.py b/homeassistant/components/accuweather/__init__.py index f6f124b2d4d..27dd4b9c196 100644 --- a/homeassistant/components/accuweather/__init__.py +++ b/homeassistant/components/accuweather/__init__.py @@ -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}} diff --git a/homeassistant/components/accuweather/config_flow.py b/homeassistant/components/accuweather/config_flow.py index 999a54b11a7..b9244a3645c 100644 --- a/homeassistant/components/accuweather/config_flow.py +++ b/homeassistant/components/accuweather/config_flow.py @@ -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) diff --git a/homeassistant/components/accuweather/const.py b/homeassistant/components/accuweather/const.py index 60fdd48c8f4..e4ec49ce2ac 100644 --- a/homeassistant/components/accuweather/const.py +++ b/homeassistant/components/accuweather/const.py @@ -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, }, } diff --git a/homeassistant/components/accuweather/model.py b/homeassistant/components/accuweather/model.py new file mode 100644 index 00000000000..cc51efbd0e2 --- /dev/null +++ b/homeassistant/components/accuweather/model.py @@ -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 diff --git a/homeassistant/components/accuweather/sensor.py b/homeassistant/components/accuweather/sensor.py index 722dd8869be..9f2d9ed78bd 100644 --- a/homeassistant/components/accuweather/sensor.py +++ b/homeassistant/components/accuweather/sensor.py @@ -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"] diff --git a/homeassistant/components/accuweather/system_health.py b/homeassistant/components/accuweather/system_health.py index 58c9ba35881..5feed5c1f34 100644 --- a/homeassistant/components/accuweather/system_health.py +++ b/homeassistant/components/accuweather/system_health.py @@ -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 diff --git a/homeassistant/components/accuweather/weather.py b/homeassistant/components/accuweather/weather.py index 3c0dcfedf43..e4745537c4f 100644 --- a/homeassistant/components/accuweather/weather.py +++ b/homeassistant/components/accuweather/weather.py @@ -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"] diff --git a/mypy.ini b/mypy.ini index 13f88680ae9..0ca5b618fc6 100644 --- a/mypy.ini +++ b/mypy.ini @@ -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 diff --git a/tests/components/accuweather/__init__.py b/tests/components/accuweather/__init__.py index d78eac4269b..3e0c6c2b875 100644 --- a/tests/components/accuweather/__init__.py +++ b/tests/components/accuweather/__init__.py @@ -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) diff --git a/tests/components/accuweather/test_config_flow.py b/tests/components/accuweather/test_config_flow.py index 1d9feecda3c..c8f2d3c8c89 100644 --- a/tests/components/accuweather/test_config_flow.py +++ b/tests/components/accuweather/test_config_flow.py @@ -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() diff --git a/tests/components/accuweather/test_sensor.py b/tests/components/accuweather/test_sensor.py index a4436445340..482fae696c0 100644 --- a/tests/components/accuweather/test_sensor.py +++ b/tests/components/accuweather/test_sensor.py @@ -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 diff --git a/tests/components/accuweather/test_weather.py b/tests/components/accuweather/test_weather.py index 8190d96e634..b1c87c7d404 100644 --- a/tests/components/accuweather/test_weather.py +++ b/tests/components/accuweather/test_weather.py @@ -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",