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
pull/101234/head^2
TJ Horner 2023-10-01 14:20:09 -07:00 committed by GitHub
parent 9261ad14e2
commit cabfbc245d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 167 additions and 21 deletions

View File

@ -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:

View File

@ -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"

View File

@ -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,
)

View File

@ -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]

View File

@ -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"
}
}
}
}
}

View File

@ -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

View File

@ -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,

View File

@ -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)