diff --git a/homeassistant/components/accuweather/__init__.py b/homeassistant/components/accuweather/__init__.py index 9123648a38d..0484dd0c8e7 100644 --- a/homeassistant/components/accuweather/__init__.py +++ b/homeassistant/components/accuweather/__init__.py @@ -81,7 +81,6 @@ class AccuWeatherDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): """Initialize.""" self.location_key = location_key self.forecast = forecast - self.is_metric = hass.config.units.is_metric self.accuweather = AccuWeather(api_key, session, location_key=location_key) self.device_info = DeviceInfo( entry_type=DeviceEntryType.SERVICE, @@ -116,7 +115,9 @@ class AccuWeatherDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): async with timeout(10): current = await self.accuweather.async_get_current_conditions() forecast = ( - await self.accuweather.async_get_forecast(metric=self.is_metric) + await self.accuweather.async_get_forecast( + metric=self.hass.config.units.is_metric + ) if self.forecast else {} ) diff --git a/homeassistant/components/accuweather/const.py b/homeassistant/components/accuweather/const.py index c1b90de09e7..1336e31f415 100644 --- a/homeassistant/components/accuweather/const.py +++ b/homeassistant/components/accuweather/const.py @@ -23,7 +23,13 @@ from homeassistant.components.weather import ( API_IMPERIAL: Final = "Imperial" API_METRIC: Final = "Metric" ATTRIBUTION: Final = "Data provided by AccuWeather" +ATTR_CATEGORY: Final = "Category" +ATTR_DIRECTION: Final = "Direction" +ATTR_ENGLISH: Final = "English" +ATTR_LEVEL: Final = "level" ATTR_FORECAST: Final = "forecast" +ATTR_SPEED: Final = "Speed" +ATTR_VALUE: Final = "Value" CONF_FORECAST: Final = "forecast" DOMAIN: Final = "accuweather" MANUFACTURER: Final = "AccuWeather, Inc." diff --git a/homeassistant/components/accuweather/sensor.py b/homeassistant/components/accuweather/sensor.py index c13dedcdceb..f57af15714d 100644 --- a/homeassistant/components/accuweather/sensor.py +++ b/homeassistant/components/accuweather/sensor.py @@ -1,6 +1,7 @@ """Support for the AccuWeather service.""" from __future__ import annotations +from collections.abc import Callable from dataclasses import dataclass from typing import Any, cast @@ -34,7 +35,13 @@ from . import AccuWeatherDataUpdateCoordinator from .const import ( API_IMPERIAL, API_METRIC, + ATTR_CATEGORY, + ATTR_DIRECTION, + ATTR_ENGLISH, ATTR_FORECAST, + ATTR_LEVEL, + ATTR_SPEED, + ATTR_VALUE, ATTRIBUTION, DOMAIN, MAX_FORECAST_DAYS, @@ -44,11 +51,20 @@ PARALLEL_UPDATES = 1 @dataclass -class AccuWeatherSensorDescription(SensorEntityDescription): +class AccuWeatherSensorDescriptionMixin: + """Mixin for AccuWeather sensor.""" + + value_fn: Callable[[dict[str, Any], str], StateType] + + +@dataclass +class AccuWeatherSensorDescription( + SensorEntityDescription, AccuWeatherSensorDescriptionMixin +): """Class describing AccuWeather sensor entities.""" - unit_metric: str | None = None - unit_imperial: str | None = None + attr_fn: Callable[[dict[str, Any]], dict[str, StateType]] = lambda _: {} + unit_fn: Callable[[bool], str | None] = lambda _: None FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( @@ -56,145 +72,162 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( key="CloudCoverDay", icon="mdi:weather-cloudy", name="Cloud cover day", - unit_metric=PERCENTAGE, - unit_imperial=PERCENTAGE, entity_registry_enabled_default=False, + unit_fn=lambda _: PERCENTAGE, + value_fn=lambda data, _: cast(int, data), ), AccuWeatherSensorDescription( key="CloudCoverNight", icon="mdi:weather-cloudy", name="Cloud cover night", - unit_metric=PERCENTAGE, - unit_imperial=PERCENTAGE, entity_registry_enabled_default=False, + unit_fn=lambda _: PERCENTAGE, + value_fn=lambda data, _: cast(int, data), ), AccuWeatherSensorDescription( key="Grass", icon="mdi:grass", name="Grass pollen", - unit_metric=CONCENTRATION_PARTS_PER_CUBIC_METER, - unit_imperial=CONCENTRATION_PARTS_PER_CUBIC_METER, entity_registry_enabled_default=False, + unit_fn=lambda _: CONCENTRATION_PARTS_PER_CUBIC_METER, + value_fn=lambda data, _: cast(int, data[ATTR_VALUE]), + attr_fn=lambda data: {ATTR_LEVEL: data[ATTR_CATEGORY]}, ), AccuWeatherSensorDescription( key="HoursOfSun", icon="mdi:weather-partly-cloudy", name="Hours of sun", - unit_metric=TIME_HOURS, - unit_imperial=TIME_HOURS, + unit_fn=lambda _: TIME_HOURS, + value_fn=lambda data, _: cast(float, data), ), AccuWeatherSensorDescription( key="Mold", icon="mdi:blur", name="Mold pollen", - unit_metric=CONCENTRATION_PARTS_PER_CUBIC_METER, - unit_imperial=CONCENTRATION_PARTS_PER_CUBIC_METER, entity_registry_enabled_default=False, + unit_fn=lambda _: CONCENTRATION_PARTS_PER_CUBIC_METER, + value_fn=lambda data, _: cast(int, data[ATTR_VALUE]), + attr_fn=lambda data: {ATTR_LEVEL: data[ATTR_CATEGORY]}, ), AccuWeatherSensorDescription( key="Ozone", icon="mdi:vector-triangle", name="Ozone", - unit_metric=None, - unit_imperial=None, entity_registry_enabled_default=False, + value_fn=lambda data, _: cast(int, data[ATTR_VALUE]), + attr_fn=lambda data: {ATTR_LEVEL: data[ATTR_CATEGORY]}, ), AccuWeatherSensorDescription( key="Ragweed", icon="mdi:sprout", name="Ragweed pollen", - unit_metric=CONCENTRATION_PARTS_PER_CUBIC_METER, - unit_imperial=CONCENTRATION_PARTS_PER_CUBIC_METER, + unit_fn=lambda _: CONCENTRATION_PARTS_PER_CUBIC_METER, entity_registry_enabled_default=False, + value_fn=lambda data, _: cast(int, data[ATTR_VALUE]), + attr_fn=lambda data: {ATTR_LEVEL: data[ATTR_CATEGORY]}, ), AccuWeatherSensorDescription( key="RealFeelTemperatureMax", device_class=SensorDeviceClass.TEMPERATURE, name="RealFeel temperature max", - unit_metric=TEMP_CELSIUS, - unit_imperial=TEMP_FAHRENHEIT, + unit_fn=lambda metric: TEMP_CELSIUS if metric else TEMP_FAHRENHEIT, + value_fn=lambda data, _: cast(float, data[ATTR_VALUE]), ), AccuWeatherSensorDescription( key="RealFeelTemperatureMin", device_class=SensorDeviceClass.TEMPERATURE, name="RealFeel temperature min", - unit_metric=TEMP_CELSIUS, - unit_imperial=TEMP_FAHRENHEIT, + unit_fn=lambda metric: TEMP_CELSIUS if metric else TEMP_FAHRENHEIT, + value_fn=lambda data, _: cast(float, data[ATTR_VALUE]), ), AccuWeatherSensorDescription( key="RealFeelTemperatureShadeMax", device_class=SensorDeviceClass.TEMPERATURE, name="RealFeel temperature shade max", - unit_metric=TEMP_CELSIUS, - unit_imperial=TEMP_FAHRENHEIT, entity_registry_enabled_default=False, + unit_fn=lambda metric: TEMP_CELSIUS if metric else TEMP_FAHRENHEIT, + value_fn=lambda data, _: cast(float, data[ATTR_VALUE]), ), AccuWeatherSensorDescription( key="RealFeelTemperatureShadeMin", device_class=SensorDeviceClass.TEMPERATURE, name="RealFeel temperature shade min", - unit_metric=TEMP_CELSIUS, - unit_imperial=TEMP_FAHRENHEIT, entity_registry_enabled_default=False, + unit_fn=lambda metric: TEMP_CELSIUS if metric else TEMP_FAHRENHEIT, + value_fn=lambda data, _: cast(float, data[ATTR_VALUE]), ), AccuWeatherSensorDescription( key="ThunderstormProbabilityDay", icon="mdi:weather-lightning", name="Thunderstorm probability day", - unit_metric=PERCENTAGE, - unit_imperial=PERCENTAGE, + unit_fn=lambda _: PERCENTAGE, + value_fn=lambda data, _: cast(int, data), ), AccuWeatherSensorDescription( key="ThunderstormProbabilityNight", icon="mdi:weather-lightning", name="Thunderstorm probability night", - unit_metric=PERCENTAGE, - unit_imperial=PERCENTAGE, + unit_fn=lambda _: PERCENTAGE, + value_fn=lambda data, _: cast(int, data), ), AccuWeatherSensorDescription( key="Tree", icon="mdi:tree-outline", name="Tree pollen", - unit_metric=CONCENTRATION_PARTS_PER_CUBIC_METER, - unit_imperial=CONCENTRATION_PARTS_PER_CUBIC_METER, + unit_fn=lambda _: CONCENTRATION_PARTS_PER_CUBIC_METER, entity_registry_enabled_default=False, + value_fn=lambda data, _: cast(int, data[ATTR_VALUE]), + attr_fn=lambda data: {ATTR_LEVEL: data[ATTR_CATEGORY]}, ), AccuWeatherSensorDescription( key="UVIndex", icon="mdi:weather-sunny", name="UV index", - unit_metric=UV_INDEX, - unit_imperial=UV_INDEX, + unit_fn=lambda _: UV_INDEX, + value_fn=lambda data, _: cast(int, data[ATTR_VALUE]), + attr_fn=lambda data: {ATTR_LEVEL: data[ATTR_CATEGORY]}, ), AccuWeatherSensorDescription( key="WindGustDay", icon="mdi:weather-windy", name="Wind gust day", - unit_metric=SPEED_KILOMETERS_PER_HOUR, - unit_imperial=SPEED_MILES_PER_HOUR, entity_registry_enabled_default=False, + unit_fn=lambda metric: SPEED_KILOMETERS_PER_HOUR + if metric + else SPEED_MILES_PER_HOUR, + value_fn=lambda data, _: cast(float, data[ATTR_SPEED][ATTR_VALUE]), + attr_fn=lambda data: {"direction": data[ATTR_DIRECTION][ATTR_ENGLISH]}, ), AccuWeatherSensorDescription( key="WindGustNight", icon="mdi:weather-windy", name="Wind gust night", - unit_metric=SPEED_KILOMETERS_PER_HOUR, - unit_imperial=SPEED_MILES_PER_HOUR, entity_registry_enabled_default=False, + unit_fn=lambda metric: SPEED_KILOMETERS_PER_HOUR + if metric + else SPEED_MILES_PER_HOUR, + value_fn=lambda data, _: cast(float, data[ATTR_SPEED][ATTR_VALUE]), + attr_fn=lambda data: {"direction": data[ATTR_DIRECTION][ATTR_ENGLISH]}, ), AccuWeatherSensorDescription( key="WindDay", icon="mdi:weather-windy", name="Wind day", - unit_metric=SPEED_KILOMETERS_PER_HOUR, - unit_imperial=SPEED_MILES_PER_HOUR, + unit_fn=lambda metric: SPEED_KILOMETERS_PER_HOUR + if metric + else SPEED_MILES_PER_HOUR, + value_fn=lambda data, _: cast(float, data[ATTR_SPEED][ATTR_VALUE]), + attr_fn=lambda data: {"direction": data[ATTR_DIRECTION][ATTR_ENGLISH]}, ), AccuWeatherSensorDescription( key="WindNight", icon="mdi:weather-windy", name="Wind night", - unit_metric=SPEED_KILOMETERS_PER_HOUR, - unit_imperial=SPEED_MILES_PER_HOUR, + unit_fn=lambda metric: SPEED_KILOMETERS_PER_HOUR + if metric + else SPEED_MILES_PER_HOUR, + value_fn=lambda data, _: cast(float, data[ATTR_SPEED][ATTR_VALUE]), + attr_fn=lambda data: {"direction": data[ATTR_DIRECTION][ATTR_ENGLISH]}, ), ) @@ -203,112 +236,117 @@ SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( key="ApparentTemperature", device_class=SensorDeviceClass.TEMPERATURE, name="Apparent temperature", - unit_metric=TEMP_CELSIUS, - unit_imperial=TEMP_FAHRENHEIT, entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, + unit_fn=lambda metric: TEMP_CELSIUS if metric else TEMP_FAHRENHEIT, + value_fn=lambda data, unit: cast(float, data[unit][ATTR_VALUE]), ), AccuWeatherSensorDescription( key="Ceiling", icon="mdi:weather-fog", name="Cloud ceiling", - unit_metric=LENGTH_METERS, - unit_imperial=LENGTH_FEET, state_class=SensorStateClass.MEASUREMENT, + unit_fn=lambda metric: LENGTH_METERS if metric else LENGTH_FEET, + value_fn=lambda data, unit: round(data[unit][ATTR_VALUE]), ), AccuWeatherSensorDescription( key="CloudCover", icon="mdi:weather-cloudy", name="Cloud cover", - unit_metric=PERCENTAGE, - unit_imperial=PERCENTAGE, entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, + unit_fn=lambda _: PERCENTAGE, + value_fn=lambda data, _: cast(int, data), ), AccuWeatherSensorDescription( key="DewPoint", device_class=SensorDeviceClass.TEMPERATURE, name="Dew point", - unit_metric=TEMP_CELSIUS, - unit_imperial=TEMP_FAHRENHEIT, entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, + unit_fn=lambda metric: TEMP_CELSIUS if metric else TEMP_FAHRENHEIT, + value_fn=lambda data, unit: cast(float, data[unit][ATTR_VALUE]), ), AccuWeatherSensorDescription( key="RealFeelTemperature", device_class=SensorDeviceClass.TEMPERATURE, name="RealFeel temperature", - unit_metric=TEMP_CELSIUS, - unit_imperial=TEMP_FAHRENHEIT, state_class=SensorStateClass.MEASUREMENT, + unit_fn=lambda metric: TEMP_CELSIUS if metric else TEMP_FAHRENHEIT, + value_fn=lambda data, unit: cast(float, data[unit][ATTR_VALUE]), ), AccuWeatherSensorDescription( key="RealFeelTemperatureShade", device_class=SensorDeviceClass.TEMPERATURE, name="RealFeel temperature shade", - unit_metric=TEMP_CELSIUS, - unit_imperial=TEMP_FAHRENHEIT, entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, + unit_fn=lambda metric: TEMP_CELSIUS if metric else TEMP_FAHRENHEIT, + value_fn=lambda data, unit: cast(float, data[unit][ATTR_VALUE]), ), AccuWeatherSensorDescription( key="Precipitation", icon="mdi:weather-rainy", name="Precipitation", - unit_metric=LENGTH_MILLIMETERS, - unit_imperial=LENGTH_INCHES, state_class=SensorStateClass.MEASUREMENT, + unit_fn=lambda metric: LENGTH_MILLIMETERS if metric else LENGTH_INCHES, + value_fn=lambda data, unit: cast(float, data[unit][ATTR_VALUE]), + attr_fn=lambda data: {"type": data["PrecipitationType"]}, ), AccuWeatherSensorDescription( key="PressureTendency", device_class="accuweather__pressure_tendency", icon="mdi:gauge", name="Pressure tendency", - unit_metric=None, - unit_imperial=None, + value_fn=lambda data, _: cast(str, data["LocalizedText"]).lower(), ), AccuWeatherSensorDescription( key="UVIndex", icon="mdi:weather-sunny", name="UV index", - unit_metric=UV_INDEX, - unit_imperial=UV_INDEX, state_class=SensorStateClass.MEASUREMENT, + unit_fn=lambda _: UV_INDEX, + value_fn=lambda data, _: cast(int, data), + attr_fn=lambda data: {ATTR_LEVEL: data["UVIndexText"]}, ), AccuWeatherSensorDescription( key="WetBulbTemperature", device_class=SensorDeviceClass.TEMPERATURE, name="Wet bulb temperature", - unit_metric=TEMP_CELSIUS, - unit_imperial=TEMP_FAHRENHEIT, entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, + unit_fn=lambda metric: TEMP_CELSIUS if metric else TEMP_FAHRENHEIT, + value_fn=lambda data, unit: cast(float, data[unit][ATTR_VALUE]), ), AccuWeatherSensorDescription( key="WindChillTemperature", device_class=SensorDeviceClass.TEMPERATURE, name="Wind chill temperature", - unit_metric=TEMP_CELSIUS, - unit_imperial=TEMP_FAHRENHEIT, entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, + unit_fn=lambda metric: TEMP_CELSIUS if metric else TEMP_FAHRENHEIT, + value_fn=lambda data, unit: cast(float, data[unit][ATTR_VALUE]), ), AccuWeatherSensorDescription( key="Wind", icon="mdi:weather-windy", name="Wind", - unit_metric=SPEED_KILOMETERS_PER_HOUR, - unit_imperial=SPEED_MILES_PER_HOUR, state_class=SensorStateClass.MEASUREMENT, + unit_fn=lambda metric: SPEED_KILOMETERS_PER_HOUR + if metric + else SPEED_MILES_PER_HOUR, + value_fn=lambda data, unit: cast(float, data[ATTR_SPEED][unit][ATTR_VALUE]), ), AccuWeatherSensorDescription( key="WindGust", icon="mdi:weather-windy", name="Wind gust", - unit_metric=SPEED_KILOMETERS_PER_HOUR, - unit_imperial=SPEED_MILES_PER_HOUR, entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, + unit_fn=lambda metric: SPEED_KILOMETERS_PER_HOUR + if metric + else SPEED_MILES_PER_HOUR, + value_fn=lambda data, unit: cast(float, data[ATTR_SPEED][unit][ATTR_VALUE]), ), ) @@ -328,9 +366,9 @@ async def async_setup_entry( # Some air quality/allergy sensors are only available for certain # locations. sensors.extend( - AccuWeatherSensor(coordinator, description, forecast_day=day) - for description in FORECAST_SENSOR_TYPES + AccuWeatherForecastSensor(coordinator, description, forecast_day=day) for day in range(MAX_FORECAST_DAYS + 1) + for description in FORECAST_SENSOR_TYPES if description.key in coordinator.data[ATTR_FORECAST][0] ) @@ -356,9 +394,8 @@ class AccuWeatherSensor( super().__init__(coordinator) self.entity_description = description self._sensor_data = _get_sensor_data( - coordinator.data, forecast_day, description.key + coordinator.data, description.key, forecast_day ) - self._attrs: dict[str, Any] = {} if forecast_day is not None: self._attr_name = f"{description.name} {forecast_day}d" self._attr_unique_id = ( @@ -368,82 +405,40 @@ class AccuWeatherSensor( self._attr_unique_id = ( f"{coordinator.location_key}-{description.key}".lower() ) - if coordinator.is_metric: + if self.coordinator.hass.config.units.is_metric: self._unit_system = API_METRIC - self._attr_native_unit_of_measurement = description.unit_metric else: self._unit_system = API_IMPERIAL - self._attr_native_unit_of_measurement = description.unit_imperial + self._attr_native_unit_of_measurement = self.entity_description.unit_fn( + self.coordinator.hass.config.units.is_metric + ) self._attr_device_info = coordinator.device_info - self.forecast_day = forecast_day + if forecast_day is not None: + self.forecast_day = forecast_day @property def native_value(self) -> StateType: """Return the state.""" - if self.forecast_day is not None: - if self.entity_description.device_class == SensorDeviceClass.TEMPERATURE: - return cast(float, self._sensor_data["Value"]) - if self.entity_description.key == "UVIndex": - return cast(int, self._sensor_data["Value"]) - if self.entity_description.key in ("Grass", "Mold", "Ragweed", "Tree", "Ozone"): - return cast(int, self._sensor_data["Value"]) - if self.entity_description.key == "Ceiling": - return round(self._sensor_data[self._unit_system]["Value"]) - if self.entity_description.key == "PressureTendency": - return cast(str, self._sensor_data["LocalizedText"].lower()) - if self.entity_description.device_class == SensorDeviceClass.TEMPERATURE: - return cast(float, self._sensor_data[self._unit_system]["Value"]) - if self.entity_description.key == "Precipitation": - return cast(float, self._sensor_data[self._unit_system]["Value"]) - if self.entity_description.key in ("Wind", "WindGust"): - return cast(float, self._sensor_data["Speed"][self._unit_system]["Value"]) - if self.entity_description.key in ( - "WindDay", - "WindNight", - "WindGustDay", - "WindGustNight", - ): - return cast(StateType, self._sensor_data["Speed"]["Value"]) - return cast(StateType, self._sensor_data) + return self.entity_description.value_fn(self._sensor_data, self._unit_system) @property def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" - if self.forecast_day is not None: - if self.entity_description.key in ( - "WindDay", - "WindNight", - "WindGustDay", - "WindGustNight", - ): - self._attrs["direction"] = self._sensor_data["Direction"]["English"] - elif self.entity_description.key in ( - "Grass", - "Mold", - "Ozone", - "Ragweed", - "Tree", - "UVIndex", - ): - self._attrs["level"] = self._sensor_data["Category"] - return self._attrs - if self.entity_description.key == "UVIndex": - self._attrs["level"] = self.coordinator.data["UVIndexText"] - elif self.entity_description.key == "Precipitation": - self._attrs["type"] = self.coordinator.data["PrecipitationType"] - return self._attrs + return self.entity_description.attr_fn(self.coordinator.data) @callback def _handle_coordinator_update(self) -> None: """Handle data update.""" self._sensor_data = _get_sensor_data( - self.coordinator.data, self.forecast_day, self.entity_description.key + self.coordinator.data, self.entity_description.key ) self.async_write_ha_state() def _get_sensor_data( - sensors: dict[str, Any], forecast_day: int | None, kind: str + sensors: dict[str, Any], + kind: str, + forecast_day: int | None = None, ) -> Any: """Get sensor data.""" if forecast_day is not None: @@ -453,3 +448,20 @@ def _get_sensor_data( return sensors["PrecipitationSummary"][kind] return sensors[kind] + + +class AccuWeatherForecastSensor(AccuWeatherSensor): + """Define an AccuWeather forecast entity.""" + + @property + def extra_state_attributes(self) -> dict[str, Any]: + """Return the state attributes.""" + return self.entity_description.attr_fn(self._sensor_data) + + @callback + def _handle_coordinator_update(self) -> None: + """Handle data update.""" + self._sensor_data = _get_sensor_data( + self.coordinator.data, self.entity_description.key, self.forecast_day + ) + self.async_write_ha_state() diff --git a/homeassistant/components/accuweather/weather.py b/homeassistant/components/accuweather/weather.py index e8a5b0ab396..2bbbe7b9160 100644 --- a/homeassistant/components/accuweather/weather.py +++ b/homeassistant/components/accuweather/weather.py @@ -70,7 +70,7 @@ class AccuWeatherEntity( # Coordinator data is used also for sensors which don't have units automatically # converted, hence the weather entity's native units follow the configured unit # system - if coordinator.is_metric: + if coordinator.hass.config.units.is_metric: self._attr_native_precipitation_unit = LENGTH_MILLIMETERS self._attr_native_pressure_unit = PRESSURE_HPA self._attr_native_temperature_unit = TEMP_CELSIUS