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 feedbackpull/101234/head^2
parent
9261ad14e2
commit
cabfbc245d
|
@ -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:
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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,
|
||||
)
|
|
@ -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]
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
Loading…
Reference in New Issue