From cabfbc245d662aab6b5bbddd3ee9f5e5bebb9fc2 Mon Sep 17 00:00:00 2001 From: TJ Horner Date: Sun, 1 Oct 2023 14:20:09 -0700 Subject: [PATCH] Add weatherkit sensor platform (#101150) * Add weatherkit sensor platform and tests * Make unique ID assignment more explicit * Fix missing argument * Use const for top-level API response keys * Address code review feedback --- .../components/weatherkit/__init__.py | 2 +- homeassistant/components/weatherkit/const.py | 6 ++ homeassistant/components/weatherkit/entity.py | 33 +++++++++ homeassistant/components/weatherkit/sensor.py | 73 +++++++++++++++++++ .../components/weatherkit/strings.json | 12 +++ .../components/weatherkit/weather.py | 33 ++++----- .../weatherkit/fixtures/weather_response.json | 2 +- tests/components/weatherkit/test_sensor.py | 27 +++++++ 8 files changed, 167 insertions(+), 21 deletions(-) create mode 100644 homeassistant/components/weatherkit/entity.py create mode 100644 homeassistant/components/weatherkit/sensor.py create mode 100644 tests/components/weatherkit/test_sensor.py diff --git a/homeassistant/components/weatherkit/__init__.py b/homeassistant/components/weatherkit/__init__.py index fb41ffc1084..15ad5fa2ffb 100644 --- a/homeassistant/components/weatherkit/__init__.py +++ b/homeassistant/components/weatherkit/__init__.py @@ -23,7 +23,7 @@ from .const import ( ) from .coordinator import WeatherKitDataUpdateCoordinator -PLATFORMS: list[Platform] = [Platform.WEATHER] +PLATFORMS: list[Platform] = [Platform.WEATHER, Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/weatherkit/const.py b/homeassistant/components/weatherkit/const.py index 590ca65c9a9..e35dd33c561 100644 --- a/homeassistant/components/weatherkit/const.py +++ b/homeassistant/components/weatherkit/const.py @@ -10,7 +10,13 @@ ATTRIBUTION = ( "https://developer.apple.com/weatherkit/data-source-attribution/" ) +MANUFACTURER = "Apple Weather" + CONF_KEY_ID = "key_id" CONF_SERVICE_ID = "service_id" CONF_TEAM_ID = "team_id" CONF_KEY_PEM = "key_pem" + +ATTR_CURRENT_WEATHER = "currentWeather" +ATTR_FORECAST_HOURLY = "forecastHourly" +ATTR_FORECAST_DAILY = "forecastDaily" diff --git a/homeassistant/components/weatherkit/entity.py b/homeassistant/components/weatherkit/entity.py new file mode 100644 index 00000000000..a244c9c4525 --- /dev/null +++ b/homeassistant/components/weatherkit/entity.py @@ -0,0 +1,33 @@ +"""Base entity for weatherkit.""" + +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo +from homeassistant.helpers.entity import Entity + +from .const import DOMAIN, MANUFACTURER +from .coordinator import WeatherKitDataUpdateCoordinator + + +class WeatherKitEntity(Entity): + """Base entity for all WeatherKit platforms.""" + + _attr_has_entity_name = True + + def __init__( + self, coordinator: WeatherKitDataUpdateCoordinator, unique_id_suffix: str | None + ) -> None: + """Initialize the entity with device info and unique ID.""" + config_data = coordinator.config_entry.data + + config_entry_unique_id = ( + f"{config_data[CONF_LATITUDE]}-{config_data[CONF_LONGITUDE]}" + ) + self._attr_unique_id = config_entry_unique_id + if unique_id_suffix is not None: + self._attr_unique_id += f"_{unique_id_suffix}" + + self._attr_device_info = DeviceInfo( + entry_type=DeviceEntryType.SERVICE, + identifiers={(DOMAIN, config_entry_unique_id)}, + manufacturer=MANUFACTURER, + ) diff --git a/homeassistant/components/weatherkit/sensor.py b/homeassistant/components/weatherkit/sensor.py new file mode 100644 index 00000000000..38b4a60cba5 --- /dev/null +++ b/homeassistant/components/weatherkit/sensor.py @@ -0,0 +1,73 @@ +"""WeatherKit sensors.""" + + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import UnitOfVolumetricFlux +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import ATTR_CURRENT_WEATHER, DOMAIN +from .coordinator import WeatherKitDataUpdateCoordinator +from .entity import WeatherKitEntity + +SENSORS = ( + SensorEntityDescription( + key="precipitationIntensity", + device_class=SensorDeviceClass.PRECIPITATION_INTENSITY, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR, + ), + SensorEntityDescription( + key="pressureTrend", + device_class=SensorDeviceClass.ENUM, + icon="mdi:gauge", + options=["rising", "falling", "steady"], + translation_key="pressure_trend", + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Add sensor entities from a config_entry.""" + coordinator: WeatherKitDataUpdateCoordinator = hass.data[DOMAIN][ + config_entry.entry_id + ] + + async_add_entities( + WeatherKitSensor(coordinator, description) for description in SENSORS + ) + + +class WeatherKitSensor( + CoordinatorEntity[WeatherKitDataUpdateCoordinator], WeatherKitEntity, SensorEntity +): + """WeatherKit sensor entity.""" + + def __init__( + self, + coordinator: WeatherKitDataUpdateCoordinator, + entity_description: SensorEntityDescription, + ) -> None: + """Initialize the sensor.""" + super().__init__(coordinator) + WeatherKitEntity.__init__( + self, coordinator, unique_id_suffix=entity_description.key + ) + self.entity_description = entity_description + + @property + def native_value(self) -> StateType: + """Return native value from coordinator current weather.""" + return self.coordinator.data[ATTR_CURRENT_WEATHER][self.entity_description.key] diff --git a/homeassistant/components/weatherkit/strings.json b/homeassistant/components/weatherkit/strings.json index 4581028f209..a0b62a5e16f 100644 --- a/homeassistant/components/weatherkit/strings.json +++ b/homeassistant/components/weatherkit/strings.json @@ -21,5 +21,17 @@ "unknown": "[%key:common::config_flow::error::unknown%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" } + }, + "entity": { + "sensor": { + "pressure_trend": { + "name": "Pressure trend", + "state": { + "steady": "Steady", + "rising": "Rising", + "falling": "Falling" + } + } + } } } diff --git a/homeassistant/components/weatherkit/weather.py b/homeassistant/components/weatherkit/weather.py index ce997fa500f..98816d520ba 100644 --- a/homeassistant/components/weatherkit/weather.py +++ b/homeassistant/components/weatherkit/weather.py @@ -23,19 +23,23 @@ from homeassistant.components.weather import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - CONF_LATITUDE, - CONF_LONGITUDE, UnitOfLength, UnitOfPressure, UnitOfSpeed, UnitOfTemperature, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import ATTRIBUTION, DOMAIN +from .const import ( + ATTR_CURRENT_WEATHER, + ATTR_FORECAST_DAILY, + ATTR_FORECAST_HOURLY, + ATTRIBUTION, + DOMAIN, +) from .coordinator import WeatherKitDataUpdateCoordinator +from .entity import WeatherKitEntity async def async_setup_entry( @@ -121,13 +125,12 @@ def _map_hourly_forecast(forecast: dict[str, Any]) -> Forecast: class WeatherKitWeather( - SingleCoordinatorWeatherEntity[WeatherKitDataUpdateCoordinator] + SingleCoordinatorWeatherEntity[WeatherKitDataUpdateCoordinator], WeatherKitEntity ): """Weather entity for Apple WeatherKit integration.""" _attr_attribution = ATTRIBUTION - _attr_has_entity_name = True _attr_name = None _attr_native_temperature_unit = UnitOfTemperature.CELSIUS @@ -140,17 +143,9 @@ class WeatherKitWeather( self, coordinator: WeatherKitDataUpdateCoordinator, ) -> None: - """Initialise the platform with a data instance and site.""" + """Initialize the platform with a coordinator.""" super().__init__(coordinator) - config_data = coordinator.config_entry.data - self._attr_unique_id = ( - f"{config_data[CONF_LATITUDE]}-{config_data[CONF_LONGITUDE]}" - ) - self._attr_device_info = DeviceInfo( - entry_type=DeviceEntryType.SERVICE, - identifiers={(DOMAIN, self._attr_unique_id)}, - manufacturer="Apple Weather", - ) + WeatherKitEntity.__init__(self, coordinator, unique_id_suffix=None) @property def supported_features(self) -> WeatherEntityFeature: @@ -174,7 +169,7 @@ class WeatherKitWeather( @property def current_weather(self) -> dict[str, Any]: """Return current weather data.""" - return self.data["currentWeather"] + return self.data[ATTR_CURRENT_WEATHER] @property def condition(self) -> str | None: @@ -245,7 +240,7 @@ class WeatherKitWeather( @callback def _async_forecast_daily(self) -> list[Forecast] | None: """Return the daily forecast.""" - daily_forecast = self.data.get("forecastDaily") + daily_forecast = self.data.get(ATTR_FORECAST_DAILY) if not daily_forecast: return None @@ -255,7 +250,7 @@ class WeatherKitWeather( @callback def _async_forecast_hourly(self) -> list[Forecast] | None: """Return the hourly forecast.""" - hourly_forecast = self.data.get("forecastHourly") + hourly_forecast = self.data.get(ATTR_FORECAST_HOURLY) if not hourly_forecast: return None diff --git a/tests/components/weatherkit/fixtures/weather_response.json b/tests/components/weatherkit/fixtures/weather_response.json index c2d619d85d8..7a38347bdf5 100644 --- a/tests/components/weatherkit/fixtures/weather_response.json +++ b/tests/components/weatherkit/fixtures/weather_response.json @@ -19,7 +19,7 @@ "conditionCode": "PartlyCloudy", "daylight": true, "humidity": 0.91, - "precipitationIntensity": 0.0, + "precipitationIntensity": 0.7, "pressure": 1009.8, "pressureTrend": "rising", "temperature": 22.9, diff --git a/tests/components/weatherkit/test_sensor.py b/tests/components/weatherkit/test_sensor.py new file mode 100644 index 00000000000..6c6999c6bfd --- /dev/null +++ b/tests/components/weatherkit/test_sensor.py @@ -0,0 +1,27 @@ +"""Sensor entity tests for the WeatherKit integration.""" + +from typing import Any + +import pytest + +from homeassistant.core import HomeAssistant + +from . import init_integration + + +@pytest.mark.parametrize( + ("entity_name", "expected_value"), + [ + ("sensor.home_precipitation_intensity", 0.7), + ("sensor.home_pressure_trend", "rising"), + ], +) +async def test_sensor_values( + hass: HomeAssistant, entity_name: str, expected_value: Any +) -> None: + """Test that various sensor values match what we expect.""" + await init_integration(hass) + + state = hass.states.get(entity_name) + assert state + assert state.state == str(expected_value)