mirror of
https://github.com/home-assistant/core.git
synced 2025-07-17 02:07:09 +00:00
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
This commit is contained in:
parent
9261ad14e2
commit
cabfbc245d
@ -23,7 +23,7 @@ from .const import (
|
|||||||
)
|
)
|
||||||
from .coordinator import WeatherKitDataUpdateCoordinator
|
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:
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
|
@ -10,7 +10,13 @@ ATTRIBUTION = (
|
|||||||
"https://developer.apple.com/weatherkit/data-source-attribution/"
|
"https://developer.apple.com/weatherkit/data-source-attribution/"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
MANUFACTURER = "Apple Weather"
|
||||||
|
|
||||||
CONF_KEY_ID = "key_id"
|
CONF_KEY_ID = "key_id"
|
||||||
CONF_SERVICE_ID = "service_id"
|
CONF_SERVICE_ID = "service_id"
|
||||||
CONF_TEAM_ID = "team_id"
|
CONF_TEAM_ID = "team_id"
|
||||||
CONF_KEY_PEM = "key_pem"
|
CONF_KEY_PEM = "key_pem"
|
||||||
|
|
||||||
|
ATTR_CURRENT_WEATHER = "currentWeather"
|
||||||
|
ATTR_FORECAST_HOURLY = "forecastHourly"
|
||||||
|
ATTR_FORECAST_DAILY = "forecastDaily"
|
||||||
|
33
homeassistant/components/weatherkit/entity.py
Normal file
33
homeassistant/components/weatherkit/entity.py
Normal 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,
|
||||||
|
)
|
73
homeassistant/components/weatherkit/sensor.py
Normal file
73
homeassistant/components/weatherkit/sensor.py
Normal 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]
|
@ -21,5 +21,17 @@
|
|||||||
"unknown": "[%key:common::config_flow::error::unknown%]",
|
"unknown": "[%key:common::config_flow::error::unknown%]",
|
||||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
|
"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.config_entries import ConfigEntry
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_LATITUDE,
|
|
||||||
CONF_LONGITUDE,
|
|
||||||
UnitOfLength,
|
UnitOfLength,
|
||||||
UnitOfPressure,
|
UnitOfPressure,
|
||||||
UnitOfSpeed,
|
UnitOfSpeed,
|
||||||
UnitOfTemperature,
|
UnitOfTemperature,
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
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 .coordinator import WeatherKitDataUpdateCoordinator
|
||||||
|
from .entity import WeatherKitEntity
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
@ -121,13 +125,12 @@ def _map_hourly_forecast(forecast: dict[str, Any]) -> Forecast:
|
|||||||
|
|
||||||
|
|
||||||
class WeatherKitWeather(
|
class WeatherKitWeather(
|
||||||
SingleCoordinatorWeatherEntity[WeatherKitDataUpdateCoordinator]
|
SingleCoordinatorWeatherEntity[WeatherKitDataUpdateCoordinator], WeatherKitEntity
|
||||||
):
|
):
|
||||||
"""Weather entity for Apple WeatherKit integration."""
|
"""Weather entity for Apple WeatherKit integration."""
|
||||||
|
|
||||||
_attr_attribution = ATTRIBUTION
|
_attr_attribution = ATTRIBUTION
|
||||||
|
|
||||||
_attr_has_entity_name = True
|
|
||||||
_attr_name = None
|
_attr_name = None
|
||||||
|
|
||||||
_attr_native_temperature_unit = UnitOfTemperature.CELSIUS
|
_attr_native_temperature_unit = UnitOfTemperature.CELSIUS
|
||||||
@ -140,17 +143,9 @@ class WeatherKitWeather(
|
|||||||
self,
|
self,
|
||||||
coordinator: WeatherKitDataUpdateCoordinator,
|
coordinator: WeatherKitDataUpdateCoordinator,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialise the platform with a data instance and site."""
|
"""Initialize the platform with a coordinator."""
|
||||||
super().__init__(coordinator)
|
super().__init__(coordinator)
|
||||||
config_data = coordinator.config_entry.data
|
WeatherKitEntity.__init__(self, coordinator, unique_id_suffix=None)
|
||||||
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",
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def supported_features(self) -> WeatherEntityFeature:
|
def supported_features(self) -> WeatherEntityFeature:
|
||||||
@ -174,7 +169,7 @@ class WeatherKitWeather(
|
|||||||
@property
|
@property
|
||||||
def current_weather(self) -> dict[str, Any]:
|
def current_weather(self) -> dict[str, Any]:
|
||||||
"""Return current weather data."""
|
"""Return current weather data."""
|
||||||
return self.data["currentWeather"]
|
return self.data[ATTR_CURRENT_WEATHER]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def condition(self) -> str | None:
|
def condition(self) -> str | None:
|
||||||
@ -245,7 +240,7 @@ class WeatherKitWeather(
|
|||||||
@callback
|
@callback
|
||||||
def _async_forecast_daily(self) -> list[Forecast] | None:
|
def _async_forecast_daily(self) -> list[Forecast] | None:
|
||||||
"""Return the daily forecast."""
|
"""Return the daily forecast."""
|
||||||
daily_forecast = self.data.get("forecastDaily")
|
daily_forecast = self.data.get(ATTR_FORECAST_DAILY)
|
||||||
if not daily_forecast:
|
if not daily_forecast:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@ -255,7 +250,7 @@ class WeatherKitWeather(
|
|||||||
@callback
|
@callback
|
||||||
def _async_forecast_hourly(self) -> list[Forecast] | None:
|
def _async_forecast_hourly(self) -> list[Forecast] | None:
|
||||||
"""Return the hourly forecast."""
|
"""Return the hourly forecast."""
|
||||||
hourly_forecast = self.data.get("forecastHourly")
|
hourly_forecast = self.data.get(ATTR_FORECAST_HOURLY)
|
||||||
if not hourly_forecast:
|
if not hourly_forecast:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
"conditionCode": "PartlyCloudy",
|
"conditionCode": "PartlyCloudy",
|
||||||
"daylight": true,
|
"daylight": true,
|
||||||
"humidity": 0.91,
|
"humidity": 0.91,
|
||||||
"precipitationIntensity": 0.0,
|
"precipitationIntensity": 0.7,
|
||||||
"pressure": 1009.8,
|
"pressure": 1009.8,
|
||||||
"pressureTrend": "rising",
|
"pressureTrend": "rising",
|
||||||
"temperature": 22.9,
|
"temperature": 22.9,
|
||||||
|
27
tests/components/weatherkit/test_sensor.py
Normal file
27
tests/components/weatherkit/test_sensor.py
Normal 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)
|
Loading…
x
Reference in New Issue
Block a user