1
0
mirror of https://github.com/home-assistant/core.git synced 2026-05-30 04:05:01 +01:00
Files
2026-05-13 21:19:21 +02:00

549 lines
20 KiB
Python

"""Sensors for cloud based weatherflow."""
from abc import ABC
from collections.abc import Callable
from dataclasses import dataclass
from datetime import date, datetime
from decimal import Decimal
from weatherflow4py.models.rest.observation import Observation
from weatherflow4py.models.ws.websocket_response import (
EventDataRapidWind,
WebsocketObservation,
)
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.const import (
LIGHT_LUX,
PERCENTAGE,
UV_INDEX,
EntityCategory,
UnitOfIrradiance,
UnitOfLength,
UnitOfMass,
UnitOfPrecipitationDepth,
UnitOfPressure,
UnitOfSpeed,
UnitOfTemperature,
UnitOfTime,
UnitOfVolume,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import StateType
from homeassistant.util.dt import UTC
from .coordinator import (
WeatherFlowCloudConfigEntry,
WeatherFlowCloudUpdateCoordinatorREST,
WeatherFlowObservationCoordinator,
WeatherFlowWindCoordinator,
)
from .entity import WeatherFlowCloudEntity
PRECIPITATION_TYPE = {
0: "none",
1: "rain",
2: "snow",
3: "sleet",
4: "storm",
}
@dataclass(frozen=True, kw_only=True)
class WeatherFlowCloudSensorEntityDescription(
SensorEntityDescription,
):
"""Describes a weatherflow sensor."""
value_fn: Callable[[Observation], StateType | datetime]
@dataclass(frozen=True, kw_only=True)
class WeatherFlowCloudSensorEntityDescriptionWebsocketWind(
SensorEntityDescription,
):
"""Describes a weatherflow sensor."""
value_fn: Callable[[EventDataRapidWind], StateType | datetime]
@dataclass(frozen=True, kw_only=True)
class WeatherFlowCloudSensorEntityDescriptionWebsocketObservation(
SensorEntityDescription,
):
"""Describes a weatherflow sensor."""
value_fn: Callable[[WebsocketObservation], StateType | datetime]
WEBSOCKET_WIND_SENSORS: tuple[
WeatherFlowCloudSensorEntityDescriptionWebsocketWind, ...
] = (
WeatherFlowCloudSensorEntityDescriptionWebsocketWind(
key="wind_speed",
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.WIND_SPEED,
suggested_display_precision=1,
value_fn=lambda data: data.wind_speed_meters_per_second,
native_unit_of_measurement=UnitOfSpeed.METERS_PER_SECOND,
),
WeatherFlowCloudSensorEntityDescriptionWebsocketWind(
key="wind_direction",
device_class=SensorDeviceClass.WIND_DIRECTION,
translation_key="wind_direction",
value_fn=lambda data: data.wind_direction_degrees,
native_unit_of_measurement="°",
),
)
WEBSOCKET_OBSERVATION_SENSORS: tuple[
WeatherFlowCloudSensorEntityDescriptionWebsocketObservation, ...
] = (
WeatherFlowCloudSensorEntityDescriptionWebsocketObservation(
key="wind_lull",
translation_key="wind_lull",
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.WIND_SPEED,
suggested_display_precision=1,
value_fn=lambda data: data.wind_lull,
native_unit_of_measurement=UnitOfSpeed.METERS_PER_SECOND,
),
WeatherFlowCloudSensorEntityDescriptionWebsocketObservation(
key="wind_gust",
translation_key="wind_gust",
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.WIND_SPEED,
suggested_display_precision=1,
value_fn=lambda data: data.wind_gust,
native_unit_of_measurement=UnitOfSpeed.METERS_PER_SECOND,
),
WeatherFlowCloudSensorEntityDescriptionWebsocketObservation(
key="wind_avg",
translation_key="wind_avg",
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.WIND_SPEED,
suggested_display_precision=1,
value_fn=lambda data: data.wind_avg,
native_unit_of_measurement=UnitOfSpeed.METERS_PER_SECOND,
),
WeatherFlowCloudSensorEntityDescriptionWebsocketObservation(
key="wind_sample_interval",
translation_key="wind_sample_interval",
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
native_unit_of_measurement=UnitOfTime.SECONDS,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
value_fn=lambda data: data.wind_sample_interval,
),
)
WF_SENSORS: tuple[WeatherFlowCloudSensorEntityDescription, ...] = (
# Air Sensors
WeatherFlowCloudSensorEntityDescription(
key="air_density",
translation_key="air_density",
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=5,
value_fn=lambda data: data.air_density,
native_unit_of_measurement=f"{UnitOfMass.KILOGRAMS}/{UnitOfVolume.CUBIC_METERS}",
),
WeatherFlowCloudSensorEntityDescription(
key="relative_humidity",
translation_key="relative_humidity",
device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
value_fn=lambda data: data.relative_humidity,
native_unit_of_measurement=PERCENTAGE,
),
# Light Sensors
WeatherFlowCloudSensorEntityDescription(
key="brightness",
translation_key="illuminance",
device_class=SensorDeviceClass.ILLUMINANCE,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
value_fn=lambda data: data.brightness,
native_unit_of_measurement=LIGHT_LUX,
),
WeatherFlowCloudSensorEntityDescription(
key="uv",
translation_key="uv_index",
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=2,
value_fn=lambda data: data.uv,
native_unit_of_measurement=UV_INDEX,
),
WeatherFlowCloudSensorEntityDescription(
key="solar_radiation",
translation_key="solar_radiation",
device_class=SensorDeviceClass.IRRADIANCE,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
value_fn=lambda data: data.solar_radiation,
native_unit_of_measurement=UnitOfIrradiance.WATTS_PER_SQUARE_METER,
),
# Temp Sensors
WeatherFlowCloudSensorEntityDescription(
key="air_temperature",
translation_key="air_temperature",
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
value_fn=lambda data: data.air_temperature,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
),
WeatherFlowCloudSensorEntityDescription(
key="dew_point",
translation_key="dew_point",
value_fn=lambda data: data.dew_point,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
),
WeatherFlowCloudSensorEntityDescription(
key="feels_like",
translation_key="feels_like",
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
value_fn=lambda data: data.feels_like,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
),
WeatherFlowCloudSensorEntityDescription(
key="heat_index",
translation_key="heat_index",
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
value_fn=lambda data: data.heat_index,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
),
WeatherFlowCloudSensorEntityDescription(
key="wind_chill",
translation_key="wind_chill",
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
value_fn=lambda data: data.wind_chill,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
),
WeatherFlowCloudSensorEntityDescription(
key="wet_bulb_temperature",
translation_key="wet_bulb_temperature",
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
value_fn=lambda data: data.wet_bulb_temperature,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
),
WeatherFlowCloudSensorEntityDescription(
key="wet_bulb_globe_temperature",
translation_key="wet_bulb_globe_temperature",
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
value_fn=lambda data: data.wet_bulb_globe_temperature,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
),
# Pressure Sensors
WeatherFlowCloudSensorEntityDescription(
key="barometric_pressure",
translation_key="barometric_pressure",
value_fn=lambda data: data.barometric_pressure,
native_unit_of_measurement=UnitOfPressure.MBAR,
device_class=SensorDeviceClass.ATMOSPHERIC_PRESSURE,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=3,
),
WeatherFlowCloudSensorEntityDescription(
key="sea_level_pressure",
translation_key="sea_level_pressure",
value_fn=lambda data: data.sea_level_pressure,
native_unit_of_measurement=UnitOfPressure.MBAR,
device_class=SensorDeviceClass.ATMOSPHERIC_PRESSURE,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=3,
),
# Rain Sensors
WeatherFlowCloudSensorEntityDescription(
key="precip_accum_last_1hr",
translation_key="precip_accum_last_1hr",
device_class=SensorDeviceClass.PRECIPITATION,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
value_fn=lambda data: data.precip_accum_last_1hr,
native_unit_of_measurement=UnitOfPrecipitationDepth.MILLIMETERS,
),
WeatherFlowCloudSensorEntityDescription(
key="precip_accum_local_day",
translation_key="precip_accum_local_day",
device_class=SensorDeviceClass.PRECIPITATION,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
value_fn=lambda data: data.precip_accum_local_day,
native_unit_of_measurement=UnitOfPrecipitationDepth.MILLIMETERS,
),
WeatherFlowCloudSensorEntityDescription(
key="precip_accum_local_day_final",
translation_key="precip_accum_local_day_final",
device_class=SensorDeviceClass.PRECIPITATION,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
value_fn=lambda data: data.precip_accum_local_day_final,
native_unit_of_measurement=UnitOfPrecipitationDepth.MILLIMETERS,
),
WeatherFlowCloudSensorEntityDescription(
key="precip_accum_local_yesterday",
translation_key="precip_accum_local_yesterday",
device_class=SensorDeviceClass.PRECIPITATION,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
value_fn=lambda data: data.precip_accum_local_yesterday,
native_unit_of_measurement=UnitOfPrecipitationDepth.MILLIMETERS,
),
WeatherFlowCloudSensorEntityDescription(
key="precip_accum_local_yesterday_final",
translation_key="precip_accum_local_yesterday_final",
device_class=SensorDeviceClass.PRECIPITATION,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
value_fn=lambda data: data.precip_accum_local_yesterday_final,
native_unit_of_measurement=UnitOfPrecipitationDepth.MILLIMETERS,
),
WeatherFlowCloudSensorEntityDescription(
key="precip_analysis_type_yesterday",
translation_key="precip_analysis_type_yesterday",
device_class=SensorDeviceClass.ENUM,
options=["none", "rain", "snow", "sleet", "storm"],
suggested_display_precision=1,
value_fn=lambda data: PRECIPITATION_TYPE.get(
data.precip_analysis_type_yesterday
),
),
WeatherFlowCloudSensorEntityDescription(
key="precip_minutes_local_day",
translation_key="precip_minutes_local_day",
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfTime.MINUTES,
suggested_display_precision=1,
value_fn=lambda data: data.precip_minutes_local_day,
),
WeatherFlowCloudSensorEntityDescription(
key="precip_minutes_local_yesterday",
translation_key="precip_minutes_local_yesterday",
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfTime.MINUTES,
suggested_display_precision=1,
value_fn=lambda data: data.precip_minutes_local_yesterday,
),
WeatherFlowCloudSensorEntityDescription(
key="precip_minutes_local_yesterday_final",
translation_key="precip_minutes_local_yesterday_final",
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfTime.MINUTES,
suggested_display_precision=1,
value_fn=lambda data: data.precip_minutes_local_yesterday_final,
),
# Lightning Sensors
WeatherFlowCloudSensorEntityDescription(
key="lightning_strike_count",
translation_key="lightning_strike_count",
state_class=SensorStateClass.TOTAL,
value_fn=lambda data: data.lightning_strike_count,
),
WeatherFlowCloudSensorEntityDescription(
key="lightning_strike_count_last_1hr",
translation_key="lightning_strike_count_last_1hr",
state_class=SensorStateClass.TOTAL,
value_fn=lambda data: data.lightning_strike_count_last_1hr,
),
WeatherFlowCloudSensorEntityDescription(
key="lightning_strike_count_last_3hr",
translation_key="lightning_strike_count_last_3hr",
state_class=SensorStateClass.TOTAL,
value_fn=lambda data: data.lightning_strike_count_last_3hr,
),
WeatherFlowCloudSensorEntityDescription(
key="lightning_strike_last_distance",
translation_key="lightning_strike_last_distance",
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.DISTANCE,
native_unit_of_measurement=UnitOfLength.KILOMETERS,
value_fn=lambda data: data.lightning_strike_last_distance,
),
WeatherFlowCloudSensorEntityDescription(
key="lightning_strike_last_epoch",
translation_key="lightning_strike_last_epoch",
device_class=SensorDeviceClass.TIMESTAMP,
value_fn=(
lambda data: (
datetime.fromtimestamp(data.lightning_strike_last_epoch, tz=UTC)
if data.lightning_strike_last_epoch is not None
else None
)
),
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: WeatherFlowCloudConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up WeatherFlow sensors based on a config entry."""
coordinators = entry.runtime_data
rest_coordinator = coordinators.rest
wind_coordinator = coordinators.wind
observation_coordinator = coordinators.observation
entities: list[SensorEntity] = [
WeatherFlowCloudSensorREST(rest_coordinator, sensor_description, station_id)
for station_id in rest_coordinator.data
for sensor_description in WF_SENSORS
]
entities.extend(
WeatherFlowWebsocketSensorWind(
coordinator=wind_coordinator,
description=sensor_description,
station_id=station_id,
device_id=device_id,
)
for station_id in wind_coordinator.stations.station_outdoor_device_map
for device_id in wind_coordinator.stations.station_outdoor_device_map[
station_id
]
for sensor_description in WEBSOCKET_WIND_SENSORS
)
entities.extend(
WeatherFlowWebsocketSensorObservation(
coordinator=observation_coordinator,
description=sensor_description,
station_id=station_id,
device_id=device_id,
)
for station_id in observation_coordinator.stations.station_outdoor_device_map
for device_id in observation_coordinator.stations.station_outdoor_device_map[
station_id
]
for sensor_description in WEBSOCKET_OBSERVATION_SENSORS
)
async_add_entities(entities)
class WeatherFlowSensorBase(WeatherFlowCloudEntity, SensorEntity, ABC):
"""Common base class."""
def __init__(
self,
coordinator: (
WeatherFlowCloudUpdateCoordinatorREST
| WeatherFlowWindCoordinator
| WeatherFlowObservationCoordinator
),
description: (
WeatherFlowCloudSensorEntityDescription
| WeatherFlowCloudSensorEntityDescriptionWebsocketWind
| WeatherFlowCloudSensorEntityDescriptionWebsocketObservation
),
station_id: int,
device_id: int | None = None,
) -> None:
"""Initialize a sensor."""
super().__init__(coordinator, station_id)
self.station_id = station_id
self.device_id = device_id
self.entity_description = description
self._attr_unique_id = self._generate_unique_id()
def _generate_unique_id(self) -> str:
"""Generate a unique ID for the sensor."""
if self.device_id is not None:
return f"{self.station_id}_{self.device_id}_{self.entity_description.key}"
return f"{self.station_id}_{self.entity_description.key}"
@property
def available(self) -> bool:
"""Get if available."""
if not super().available:
return False
if self.device_id is not None:
# Websocket sensors - have Device IDs
return bool(
self.coordinator.data
and self.coordinator.data[self.station_id][self.device_id] is not None
)
return True
class WeatherFlowWebsocketSensorObservation(WeatherFlowSensorBase):
"""Class for Websocket Observations."""
entity_description: WeatherFlowCloudSensorEntityDescriptionWebsocketObservation
@property
def native_value(self) -> StateType | date | datetime | Decimal:
"""Return the native value."""
data = self.coordinator.data[self.station_id][self.device_id]
return self.entity_description.value_fn(data)
class WeatherFlowWebsocketSensorWind(WeatherFlowSensorBase):
"""Class for wind over websockets."""
entity_description: WeatherFlowCloudSensorEntityDescriptionWebsocketWind
@property
def native_value(self) -> StateType | datetime:
"""Return the native value."""
# This data is often invalid at starutp.
if self.coordinator.data is not None:
data = self.coordinator.data[self.station_id][self.device_id]
return self.entity_description.value_fn(data)
return None
class WeatherFlowCloudSensorREST(WeatherFlowSensorBase):
"""Class for a REST based sensor."""
entity_description: WeatherFlowCloudSensorEntityDescription
coordinator: WeatherFlowCloudUpdateCoordinatorREST
@property
def _observation(self) -> Observation | None:
"""Return the current station observation."""
observations = self.coordinator.data[self.station_id].observation.obs
if not observations:
return None
return observations[0]
@property
def available(self) -> bool:
"""Get if available."""
return super().available and self._observation is not None
@property
def native_value(self) -> StateType | datetime:
"""Return the native value."""
if (observation := self._observation) is None:
return None
return self.entity_description.value_fn(observation)