mirror of
https://github.com/home-assistant/core.git
synced 2025-06-25 01:21:51 +02:00
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:
@ -4,6 +4,7 @@
|
||||
|
||||
homeassistant.components
|
||||
homeassistant.components.acer_projector.*
|
||||
homeassistant.components.accuweather.*
|
||||
homeassistant.components.actiontec.*
|
||||
homeassistant.components.aftership.*
|
||||
homeassistant.components.air_quality.*
|
||||
|
@ -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}}
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
15
homeassistant/components/accuweather/model.py
Normal file
15
homeassistant/components/accuweather/model.py
Normal 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
|
@ -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"]
|
||||
|
@ -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
|
||||
|
@ -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"]
|
||||
|
11
mypy.ini
11
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
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
|
Reference in New Issue
Block a user