mirror of
https://github.com/home-assistant/core.git
synced 2025-07-07 13:27:09 +00:00
Add Weatherflow Cloud wind support via websocket (#125611)
* rebase off of dev * update tests * update tests * addressing PR finally * API to back * adding a return type * need to test * removed teh extra check on available * some changes * ready for re-review * change assertions * remove icon function * update ambr * ruff * update snapshot and push * Update homeassistant/components/weatherflow_cloud/coordinator.py Co-authored-by: Erik Montnemery <erik@montnemery.com> * Update homeassistant/components/weatherflow_cloud/coordinator.py Co-authored-by: Erik Montnemery <erik@montnemery.com> * enhnaced tests * better coverage * Update homeassistant/components/weatherflow_cloud/coordinator.py Co-authored-by: Erik Montnemery <erik@montnemery.com> * Update homeassistant/components/weatherflow_cloud/coordinator.py Co-authored-by: Erik Montnemery <erik@montnemery.com> * Update homeassistant/components/weatherflow_cloud/coordinator.py Co-authored-by: Erik Montnemery <erik@montnemery.com> * Update homeassistant/components/weatherflow_cloud/coordinator.py Co-authored-by: Erik Montnemery <erik@montnemery.com> * Update homeassistant/components/weatherflow_cloud/coordinator.py Co-authored-by: Erik Montnemery <erik@montnemery.com> * remove comments --------- Co-authored-by: Erik Montnemery <erik@montnemery.com>
This commit is contained in:
parent
741a3d5009
commit
ea70229426
@ -2,30 +2,107 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
import asyncio
|
||||
from dataclasses import dataclass
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import WeatherFlowCloudDataUpdateCoordinator
|
||||
from weatherflow4py.api import WeatherFlowRestAPI
|
||||
from weatherflow4py.ws import WeatherFlowWebsocketAPI
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_API_TOKEN, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import DOMAIN, LOGGER
|
||||
from .coordinator import (
|
||||
WeatherFlowCloudUpdateCoordinatorREST,
|
||||
WeatherFlowObservationCoordinator,
|
||||
WeatherFlowWindCoordinator,
|
||||
)
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.SENSOR, Platform.WEATHER]
|
||||
|
||||
|
||||
@dataclass
|
||||
class WeatherFlowCoordinators:
|
||||
"""Data Class for Entry Data."""
|
||||
|
||||
rest: WeatherFlowCloudUpdateCoordinatorREST
|
||||
wind: WeatherFlowWindCoordinator
|
||||
observation: WeatherFlowObservationCoordinator
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up WeatherFlowCloud from a config entry."""
|
||||
|
||||
data_coordinator = WeatherFlowCloudDataUpdateCoordinator(hass, entry)
|
||||
await data_coordinator.async_config_entry_first_refresh()
|
||||
LOGGER.debug("Initializing WeatherFlowCloudDataUpdateCoordinatorREST coordinator")
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = data_coordinator
|
||||
rest_api = WeatherFlowRestAPI(
|
||||
api_token=entry.data[CONF_API_TOKEN], session=async_get_clientsession(hass)
|
||||
)
|
||||
|
||||
stations = await rest_api.async_get_stations()
|
||||
|
||||
# Define Rest Coordinator
|
||||
rest_data_coordinator = WeatherFlowCloudUpdateCoordinatorREST(
|
||||
hass=hass, config_entry=entry, rest_api=rest_api, stations=stations
|
||||
)
|
||||
|
||||
# Initialize the stations
|
||||
await rest_data_coordinator.async_config_entry_first_refresh()
|
||||
|
||||
# Construct Websocket Coordinators
|
||||
LOGGER.debug("Initializing websocket coordinators")
|
||||
websocket_device_ids = rest_data_coordinator.device_ids
|
||||
|
||||
# Build API once
|
||||
websocket_api = WeatherFlowWebsocketAPI(
|
||||
access_token=entry.data[CONF_API_TOKEN], device_ids=websocket_device_ids
|
||||
)
|
||||
|
||||
websocket_observation_coordinator = WeatherFlowObservationCoordinator(
|
||||
hass=hass,
|
||||
config_entry=entry,
|
||||
rest_api=rest_api,
|
||||
websocket_api=websocket_api,
|
||||
stations=stations,
|
||||
)
|
||||
|
||||
websocket_wind_coordinator = WeatherFlowWindCoordinator(
|
||||
hass=hass,
|
||||
config_entry=entry,
|
||||
rest_api=rest_api,
|
||||
websocket_api=websocket_api,
|
||||
stations=stations,
|
||||
)
|
||||
|
||||
# Run setup method
|
||||
await asyncio.gather(
|
||||
websocket_wind_coordinator.async_setup(),
|
||||
websocket_observation_coordinator.async_setup(),
|
||||
)
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = WeatherFlowCoordinators(
|
||||
rest_data_coordinator,
|
||||
websocket_wind_coordinator,
|
||||
websocket_observation_coordinator,
|
||||
)
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
# Websocket disconnect handler
|
||||
async def _async_disconnect_websocket() -> None:
|
||||
await websocket_api.stop_all_listeners()
|
||||
await websocket_api.close()
|
||||
|
||||
# Register a websocket shutdown handler
|
||||
entry.async_on_unload(_async_disconnect_websocket)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
|
@ -49,10 +49,11 @@ class WeatherFlowCloudConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
errors = await _validate_api_token(api_token)
|
||||
if not errors:
|
||||
# Update the existing entry and abort
|
||||
existing_entry = self._get_reauth_entry()
|
||||
return self.async_update_reload_and_abort(
|
||||
self._get_reauth_entry(),
|
||||
existing_entry,
|
||||
data={CONF_API_TOKEN: api_token},
|
||||
reload_even_if_entry_is_unchanged=False,
|
||||
reason="reauth_successful",
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
|
@ -5,7 +5,7 @@ import logging
|
||||
DOMAIN = "weatherflow_cloud"
|
||||
LOGGER = logging.getLogger(__package__)
|
||||
|
||||
ATTR_ATTRIBUTION = "Weather data delivered by WeatherFlow/Tempest REST Api"
|
||||
ATTR_ATTRIBUTION = "Weather data delivered by WeatherFlow/Tempest API"
|
||||
MANUFACTURER = "WeatherFlow"
|
||||
|
||||
STATE_MAP = {
|
||||
@ -29,3 +29,6 @@ STATE_MAP = {
|
||||
"thunderstorm": "lightning",
|
||||
"windy": "windy",
|
||||
}
|
||||
|
||||
WEBSOCKET_API = "Websocket API"
|
||||
REST_API = "REST API"
|
||||
|
@ -1,46 +1,207 @@
|
||||
"""Data coordinator for WeatherFlow Cloud Data."""
|
||||
"""Improved coordinator design with better type safety."""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from datetime import timedelta
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
from aiohttp import ClientResponseError
|
||||
from weatherflow4py.api import WeatherFlowRestAPI
|
||||
from weatherflow4py.models.rest.stations import StationsResponseREST
|
||||
from weatherflow4py.models.rest.unified import WeatherFlowDataREST
|
||||
from weatherflow4py.models.ws.obs import WebsocketObservation
|
||||
from weatherflow4py.models.ws.types import EventType
|
||||
from weatherflow4py.models.ws.websocket_request import (
|
||||
ListenStartMessage,
|
||||
RapidWindListenStartMessage,
|
||||
)
|
||||
from weatherflow4py.models.ws.websocket_response import (
|
||||
EventDataRapidWind,
|
||||
ObservationTempestWS,
|
||||
RapidWindWS,
|
||||
)
|
||||
from weatherflow4py.ws import WeatherFlowWebsocketAPI
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_API_TOKEN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
from homeassistant.util.ssl import client_context
|
||||
|
||||
from .const import DOMAIN, LOGGER
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
class WeatherFlowCloudDataUpdateCoordinator(
|
||||
DataUpdateCoordinator[dict[int, WeatherFlowDataREST]]
|
||||
):
|
||||
"""Class to manage fetching REST Based WeatherFlow Forecast data."""
|
||||
|
||||
config_entry: ConfigEntry
|
||||
class BaseWeatherFlowCoordinator(DataUpdateCoordinator[dict[int, T]], ABC, Generic[T]):
|
||||
"""Base class for WeatherFlow coordinators."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
rest_api: WeatherFlowRestAPI,
|
||||
stations: StationsResponseREST,
|
||||
update_interval: timedelta | None = None,
|
||||
always_update: bool = False,
|
||||
) -> None:
|
||||
"""Initialize Coordinator."""
|
||||
self._token = rest_api.api_token
|
||||
self._rest_api = rest_api
|
||||
self.stations = stations
|
||||
self.device_to_station_map = stations.device_station_map
|
||||
self.device_ids = list(stations.device_station_map.keys())
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None:
|
||||
"""Initialize global WeatherFlow forecast data updater."""
|
||||
self.weather_api = WeatherFlowRestAPI(
|
||||
api_token=config_entry.data[CONF_API_TOKEN]
|
||||
)
|
||||
super().__init__(
|
||||
hass,
|
||||
LOGGER,
|
||||
config_entry=config_entry,
|
||||
name=DOMAIN,
|
||||
always_update=always_update,
|
||||
update_interval=update_interval,
|
||||
)
|
||||
|
||||
@abstractmethod
|
||||
def get_station_name(self, station_id: int) -> str:
|
||||
"""Get station name for the given station ID."""
|
||||
|
||||
|
||||
class WeatherFlowCloudUpdateCoordinatorREST(
|
||||
BaseWeatherFlowCoordinator[WeatherFlowDataREST]
|
||||
):
|
||||
"""Class to manage fetching REST Based WeatherFlow Forecast data."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
rest_api: WeatherFlowRestAPI,
|
||||
stations: StationsResponseREST,
|
||||
) -> None:
|
||||
"""Initialize global WeatherFlow forecast data updater."""
|
||||
super().__init__(
|
||||
hass,
|
||||
config_entry,
|
||||
rest_api,
|
||||
stations,
|
||||
update_interval=timedelta(seconds=60),
|
||||
always_update=True,
|
||||
)
|
||||
|
||||
async def _async_update_data(self) -> dict[int, WeatherFlowDataREST]:
|
||||
"""Fetch data from WeatherFlow Forecast."""
|
||||
"""Update rest data."""
|
||||
try:
|
||||
async with self.weather_api:
|
||||
return await self.weather_api.get_all_data()
|
||||
async with self._rest_api:
|
||||
return await self._rest_api.get_all_data()
|
||||
except ClientResponseError as err:
|
||||
if err.status == 401:
|
||||
raise ConfigEntryAuthFailed(err) from err
|
||||
raise UpdateFailed(f"Update failed: {err}") from err
|
||||
|
||||
def get_station(self, station_id: int) -> WeatherFlowDataREST:
|
||||
"""Return station for id."""
|
||||
return self.data[station_id]
|
||||
|
||||
def get_station_name(self, station_id: int) -> str:
|
||||
"""Return station name for id."""
|
||||
return self.data[station_id].station.name
|
||||
|
||||
|
||||
class BaseWebsocketCoordinator(
|
||||
BaseWeatherFlowCoordinator[dict[int, T | None]], ABC, Generic[T]
|
||||
):
|
||||
"""Base class for websocket coordinators."""
|
||||
|
||||
_event_type: EventType
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
rest_api: WeatherFlowRestAPI,
|
||||
websocket_api: WeatherFlowWebsocketAPI,
|
||||
stations: StationsResponseREST,
|
||||
) -> None:
|
||||
"""Initialize Coordinator."""
|
||||
super().__init__(
|
||||
hass=hass, config_entry=config_entry, rest_api=rest_api, stations=stations
|
||||
)
|
||||
|
||||
self.websocket_api = websocket_api
|
||||
|
||||
# Configure the websocket data structure
|
||||
self._ws_data: dict[int, dict[int, T | None]] = {
|
||||
station: dict.fromkeys(devices)
|
||||
for station, devices in self.stations.station_device_map.items()
|
||||
}
|
||||
|
||||
async def async_setup(self) -> None:
|
||||
"""Set up the websocket connection."""
|
||||
await self.websocket_api.connect(client_context())
|
||||
self.websocket_api.register_callback(
|
||||
message_type=self._event_type,
|
||||
callback=self._handle_websocket_message,
|
||||
)
|
||||
|
||||
# Subscribe to messages for all devices
|
||||
for device_id in self.device_ids:
|
||||
message = self._create_listen_message(device_id)
|
||||
await self.websocket_api.send_message(message)
|
||||
|
||||
@abstractmethod
|
||||
def _create_listen_message(self, device_id: int):
|
||||
"""Create the appropriate listen message for this coordinator type."""
|
||||
|
||||
@abstractmethod
|
||||
async def _handle_websocket_message(self, data) -> None:
|
||||
"""Handle incoming websocket data."""
|
||||
|
||||
def get_station(self, station_id: int):
|
||||
"""Return station for id."""
|
||||
return self.stations.stations[station_id]
|
||||
|
||||
def get_station_name(self, station_id: int) -> str:
|
||||
"""Return station name for id."""
|
||||
return self.stations.station_map[station_id].name or ""
|
||||
|
||||
|
||||
class WeatherFlowWindCoordinator(BaseWebsocketCoordinator[EventDataRapidWind]):
|
||||
"""Coordinator specifically for rapid wind data."""
|
||||
|
||||
_event_type = EventType.RAPID_WIND
|
||||
|
||||
def _create_listen_message(self, device_id: int) -> RapidWindListenStartMessage:
|
||||
"""Create rapid wind listen message."""
|
||||
return RapidWindListenStartMessage(device_id=str(device_id))
|
||||
|
||||
async def _handle_websocket_message(self, data: RapidWindWS) -> None:
|
||||
"""Handle rapid wind websocket data."""
|
||||
device_id = data.device_id
|
||||
station_id = self.device_to_station_map[device_id]
|
||||
|
||||
# Extract the observation data from the RapidWindWS message
|
||||
self._ws_data[station_id][device_id] = data.ob
|
||||
self.async_set_updated_data(self._ws_data)
|
||||
|
||||
|
||||
class WeatherFlowObservationCoordinator(BaseWebsocketCoordinator[WebsocketObservation]):
|
||||
"""Coordinator specifically for observation data."""
|
||||
|
||||
_event_type = EventType.OBSERVATION
|
||||
|
||||
def _create_listen_message(self, device_id: int) -> ListenStartMessage:
|
||||
"""Create observation listen message."""
|
||||
return ListenStartMessage(device_id=str(device_id))
|
||||
|
||||
async def _handle_websocket_message(self, data: ObservationTempestWS) -> None:
|
||||
"""Handle observation websocket data."""
|
||||
device_id = data.device_id
|
||||
station_id = self.device_to_station_map[device_id]
|
||||
|
||||
# For observations, the data IS the observation
|
||||
self._ws_data[station_id][device_id] = data
|
||||
self.async_set_updated_data(self._ws_data)
|
||||
|
||||
|
||||
# Type aliases for better readability
|
||||
type WeatherFlowWindCallback = WeatherFlowWindCoordinator
|
||||
type WeatherFlowObservationCallback = WeatherFlowObservationCoordinator
|
||||
|
@ -1,23 +1,21 @@
|
||||
"""Base entity class for WeatherFlow Cloud integration."""
|
||||
|
||||
from weatherflow4py.models.rest.unified import WeatherFlowDataREST
|
||||
"""Entity definition."""
|
||||
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import ATTR_ATTRIBUTION, DOMAIN, MANUFACTURER
|
||||
from .coordinator import WeatherFlowCloudDataUpdateCoordinator
|
||||
from .coordinator import BaseWeatherFlowCoordinator
|
||||
|
||||
|
||||
class WeatherFlowCloudEntity(CoordinatorEntity[WeatherFlowCloudDataUpdateCoordinator]):
|
||||
"""Base entity class to use for everything."""
|
||||
class WeatherFlowCloudEntity[T](CoordinatorEntity[BaseWeatherFlowCoordinator[T]]):
|
||||
"""Base entity class for WeatherFlow Cloud integration."""
|
||||
|
||||
_attr_attribution = ATTR_ATTRIBUTION
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: WeatherFlowCloudDataUpdateCoordinator,
|
||||
coordinator: BaseWeatherFlowCoordinator[T],
|
||||
station_id: int,
|
||||
) -> None:
|
||||
"""Class initializer."""
|
||||
@ -25,14 +23,9 @@ class WeatherFlowCloudEntity(CoordinatorEntity[WeatherFlowCloudDataUpdateCoordin
|
||||
self.station_id = station_id
|
||||
|
||||
self._attr_device_info = DeviceInfo(
|
||||
name=self.station.station.name,
|
||||
name=coordinator.get_station_name(station_id),
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
identifiers={(DOMAIN, str(station_id))},
|
||||
manufacturer=MANUFACTURER,
|
||||
configuration_url=f"https://tempestwx.com/station/{station_id}/grid",
|
||||
)
|
||||
|
||||
@property
|
||||
def station(self) -> WeatherFlowDataREST:
|
||||
"""Individual Station data."""
|
||||
return self.coordinator.data[self.station_id]
|
||||
|
@ -1,11 +1,17 @@
|
||||
{
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"air_density": {
|
||||
"default": "mdi:format-line-weight"
|
||||
},
|
||||
"air_temperature": {
|
||||
"default": "mdi:thermometer"
|
||||
},
|
||||
"air_density": {
|
||||
"default": "mdi:format-line-weight"
|
||||
"barometric_pressure": {
|
||||
"default": "mdi:gauge"
|
||||
},
|
||||
"dew_point": {
|
||||
"default": "mdi:water-percent"
|
||||
},
|
||||
"feels_like": {
|
||||
"default": "mdi:thermometer"
|
||||
@ -13,12 +19,6 @@
|
||||
"heat_index": {
|
||||
"default": "mdi:sun-thermometer"
|
||||
},
|
||||
"wet_bulb_temperature": {
|
||||
"default": "mdi:thermometer-water"
|
||||
},
|
||||
"wet_bulb_globe_temperature": {
|
||||
"default": "mdi:thermometer-water"
|
||||
},
|
||||
"lightning_strike_count": {
|
||||
"default": "mdi:lightning-bolt"
|
||||
},
|
||||
@ -34,8 +34,43 @@
|
||||
"lightning_strike_last_epoch": {
|
||||
"default": "mdi:lightning-bolt"
|
||||
},
|
||||
"sea_level_pressure": {
|
||||
"default": "mdi:gauge"
|
||||
},
|
||||
"wet_bulb_globe_temperature": {
|
||||
"default": "mdi:thermometer-water"
|
||||
},
|
||||
"wet_bulb_temperature": {
|
||||
"default": "mdi:thermometer-water"
|
||||
},
|
||||
"wind_avg": {
|
||||
"default": "mdi:weather-windy"
|
||||
},
|
||||
"wind_chill": {
|
||||
"default": "mdi:snowflake-thermometer"
|
||||
},
|
||||
"wind_direction": {
|
||||
"default": "mdi:compass",
|
||||
"range": {
|
||||
"0": "mdi:arrow-up",
|
||||
"22.5": "mdi:arrow-top-right",
|
||||
"67.5": "mdi:arrow-right",
|
||||
"112.5": "mdi:arrow-bottom-right",
|
||||
"157.5": "mdi:arrow-down",
|
||||
"202.5": "mdi:arrow-bottom-left",
|
||||
"247.5": "mdi:arrow-left",
|
||||
"292.5": "mdi:arrow-top-left",
|
||||
"337.5": "mdi:arrow-up"
|
||||
}
|
||||
},
|
||||
"wind_gust": {
|
||||
"default": "mdi:weather-dust"
|
||||
},
|
||||
"wind_lull": {
|
||||
"default": "mdi:weather-windy-variant"
|
||||
},
|
||||
"wind_sample_interval": {
|
||||
"default": "mdi:timer-outline"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,11 +2,17 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from datetime import UTC, datetime
|
||||
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,
|
||||
@ -15,13 +21,22 @@ from homeassistant.components.sensor import (
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import UnitOfLength, UnitOfPressure, UnitOfTemperature
|
||||
from homeassistant.const import (
|
||||
EntityCategory,
|
||||
UnitOfLength,
|
||||
UnitOfPressure,
|
||||
UnitOfSpeed,
|
||||
UnitOfTemperature,
|
||||
UnitOfTime,
|
||||
)
|
||||
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 . import WeatherFlowCloudUpdateCoordinatorREST, WeatherFlowCoordinators
|
||||
from .const import DOMAIN
|
||||
from .coordinator import WeatherFlowCloudDataUpdateCoordinator
|
||||
from .coordinator import WeatherFlowObservationCoordinator, WeatherFlowWindCoordinator
|
||||
from .entity import WeatherFlowCloudEntity
|
||||
|
||||
|
||||
@ -34,6 +49,87 @@ class WeatherFlowCloudSensorEntityDescription(
|
||||
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(
|
||||
@ -176,35 +272,133 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up WeatherFlow sensors based on a config entry."""
|
||||
|
||||
coordinator: WeatherFlowCloudDataUpdateCoordinator = hass.data[DOMAIN][
|
||||
entry.entry_id
|
||||
coordinators: WeatherFlowCoordinators = hass.data[DOMAIN][entry.entry_id]
|
||||
rest_coordinator = coordinators.rest
|
||||
wind_coordinator = coordinators.wind # Now properly typed
|
||||
observation_coordinator = coordinators.observation # Now properly typed
|
||||
|
||||
entities: list[SensorEntity] = [
|
||||
WeatherFlowCloudSensorREST(rest_coordinator, sensor_description, station_id)
|
||||
for station_id in rest_coordinator.data
|
||||
for sensor_description in WF_SENSORS
|
||||
]
|
||||
|
||||
async_add_entities(
|
||||
WeatherFlowCloudSensor(coordinator, sensor_description, station_id)
|
||||
for station_id in 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 WeatherFlowCloudSensor(WeatherFlowCloudEntity, SensorEntity):
|
||||
"""Implementation of a WeatherFlow sensor."""
|
||||
|
||||
entity_description: WeatherFlowCloudSensorEntityDescription
|
||||
class WeatherFlowSensorBase(WeatherFlowCloudEntity, SensorEntity, ABC):
|
||||
"""Common base class."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: WeatherFlowCloudDataUpdateCoordinator,
|
||||
description: WeatherFlowCloudSensorEntityDescription,
|
||||
coordinator: (
|
||||
WeatherFlowCloudUpdateCoordinatorREST
|
||||
| WeatherFlowWindCoordinator
|
||||
| WeatherFlowObservationCoordinator
|
||||
),
|
||||
description: (
|
||||
WeatherFlowCloudSensorEntityDescription
|
||||
| WeatherFlowCloudSensorEntityDescriptionWebsocketWind
|
||||
| WeatherFlowCloudSensorEntityDescriptionWebsocketObservation
|
||||
),
|
||||
station_id: int,
|
||||
device_id: int | None = None,
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
# Initialize the Entity Class
|
||||
"""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 = f"{station_id}_{description.key}"
|
||||
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 state of the sensor."""
|
||||
return self.entity_description.value_fn(self.station.observation.obs[0])
|
||||
"""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 native_value(self) -> StateType | datetime:
|
||||
"""Return the native value."""
|
||||
return self.entity_description.value_fn(
|
||||
self.coordinator.data[self.station_id].observation.obs[0]
|
||||
)
|
||||
|
@ -32,13 +32,15 @@
|
||||
"barometric_pressure": {
|
||||
"name": "Pressure barometric"
|
||||
},
|
||||
"sea_level_pressure": {
|
||||
"name": "Pressure sea level"
|
||||
},
|
||||
|
||||
"dew_point": {
|
||||
"name": "Dew point"
|
||||
},
|
||||
"feels_like": {
|
||||
"name": "Feels like"
|
||||
},
|
||||
"heat_index": {
|
||||
"name": "Heat index"
|
||||
},
|
||||
"lightning_strike_count": {
|
||||
"name": "Lightning count"
|
||||
},
|
||||
@ -54,33 +56,32 @@
|
||||
"lightning_strike_last_epoch": {
|
||||
"name": "Lightning last strike"
|
||||
},
|
||||
|
||||
"sea_level_pressure": {
|
||||
"name": "Pressure sea level"
|
||||
},
|
||||
"wet_bulb_globe_temperature": {
|
||||
"name": "Wet bulb globe temperature"
|
||||
},
|
||||
"wet_bulb_temperature": {
|
||||
"name": "Wet bulb temperature"
|
||||
},
|
||||
"wind_avg": {
|
||||
"name": "Wind speed (avg)"
|
||||
},
|
||||
"wind_chill": {
|
||||
"name": "Wind chill"
|
||||
},
|
||||
"wind_direction": {
|
||||
"name": "Wind direction"
|
||||
},
|
||||
"wind_direction_cardinal": {
|
||||
"name": "Wind direction (cardinal)"
|
||||
},
|
||||
"wind_gust": {
|
||||
"name": "Wind gust"
|
||||
},
|
||||
"wind_lull": {
|
||||
"name": "Wind lull"
|
||||
},
|
||||
"feels_like": {
|
||||
"name": "Feels like"
|
||||
},
|
||||
"heat_index": {
|
||||
"name": "Heat index"
|
||||
},
|
||||
"wet_bulb_temperature": {
|
||||
"name": "Wet bulb temperature"
|
||||
},
|
||||
"wet_bulb_globe_temperature": {
|
||||
"name": "Wet bulb globe temperature"
|
||||
"wind_sample_interval": {
|
||||
"name": "Wind sample interval"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -19,8 +19,8 @@ from homeassistant.const import (
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import WeatherFlowCloudUpdateCoordinatorREST, WeatherFlowCoordinators
|
||||
from .const import DOMAIN, STATE_MAP
|
||||
from .coordinator import WeatherFlowCloudDataUpdateCoordinator
|
||||
from .entity import WeatherFlowCloudEntity
|
||||
|
||||
|
||||
@ -30,21 +30,19 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Add a weather entity from a config_entry."""
|
||||
coordinator: WeatherFlowCloudDataUpdateCoordinator = hass.data[DOMAIN][
|
||||
config_entry.entry_id
|
||||
]
|
||||
coordinators: WeatherFlowCoordinators = hass.data[DOMAIN][config_entry.entry_id]
|
||||
|
||||
async_add_entities(
|
||||
[
|
||||
WeatherFlowWeather(coordinator, station_id=station_id)
|
||||
for station_id, data in coordinator.data.items()
|
||||
WeatherFlowWeatherREST(coordinators.rest, station_id=station_id)
|
||||
for station_id, data in coordinators.rest.data.items()
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class WeatherFlowWeather(
|
||||
class WeatherFlowWeatherREST(
|
||||
WeatherFlowCloudEntity,
|
||||
SingleCoordinatorWeatherEntity[WeatherFlowCloudDataUpdateCoordinator],
|
||||
SingleCoordinatorWeatherEntity[WeatherFlowCloudUpdateCoordinatorREST],
|
||||
):
|
||||
"""Implementation of a WeatherFlow weather condition."""
|
||||
|
||||
@ -59,7 +57,7 @@ class WeatherFlowWeather(
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: WeatherFlowCloudDataUpdateCoordinator,
|
||||
coordinator: WeatherFlowCloudUpdateCoordinatorREST,
|
||||
station_id: int,
|
||||
) -> None:
|
||||
"""Initialise the platform with a data instance and station."""
|
||||
|
@ -1,14 +1,16 @@
|
||||
"""Common fixtures for the WeatherflowCloud tests."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
from unittest.mock import AsyncMock, MagicMock, Mock, patch
|
||||
|
||||
from aiohttp import ClientResponseError
|
||||
import pytest
|
||||
from weatherflow4py.api import WeatherFlowRestAPI
|
||||
from weatherflow4py.models.rest.forecast import WeatherDataForecastREST
|
||||
from weatherflow4py.models.rest.observation import ObservationStationREST
|
||||
from weatherflow4py.models.rest.stations import StationsResponseREST
|
||||
from weatherflow4py.models.rest.unified import WeatherFlowDataREST
|
||||
from weatherflow4py.ws import WeatherFlowWebsocketAPI
|
||||
|
||||
from homeassistant.components.weatherflow_cloud.const import DOMAIN
|
||||
from homeassistant.const import CONF_API_TOKEN
|
||||
@ -81,35 +83,88 @@ async def mock_config_entry() -> MockConfigEntry:
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_api():
|
||||
"""Fixture for Mock WeatherFlowRestAPI."""
|
||||
get_stations_response_data = StationsResponseREST.from_json(
|
||||
load_fixture("stations.json", DOMAIN)
|
||||
)
|
||||
get_forecast_response_data = WeatherDataForecastREST.from_json(
|
||||
load_fixture("forecast.json", DOMAIN)
|
||||
)
|
||||
get_observation_response_data = ObservationStationREST.from_json(
|
||||
load_fixture("station_observation.json", DOMAIN)
|
||||
)
|
||||
def mock_rest_api():
|
||||
"""Mock rest api."""
|
||||
fixtures = {
|
||||
"stations": StationsResponseREST.from_json(
|
||||
load_fixture("stations.json", DOMAIN)
|
||||
),
|
||||
"forecast": WeatherDataForecastREST.from_json(
|
||||
load_fixture("forecast.json", DOMAIN)
|
||||
),
|
||||
"observation": ObservationStationREST.from_json(
|
||||
load_fixture("station_observation.json", DOMAIN)
|
||||
),
|
||||
}
|
||||
|
||||
# Create device_station_map
|
||||
device_station_map = {
|
||||
device.device_id: station.station_id
|
||||
for station in fixtures["stations"].stations
|
||||
for device in station.devices
|
||||
}
|
||||
|
||||
# Prepare mock data
|
||||
data = {
|
||||
24432: WeatherFlowDataREST(
|
||||
weather=get_forecast_response_data,
|
||||
observation=get_observation_response_data,
|
||||
station=get_stations_response_data.stations[0],
|
||||
weather=fixtures["forecast"],
|
||||
observation=fixtures["observation"],
|
||||
station=fixtures["stations"].stations[0],
|
||||
device_observations=None,
|
||||
)
|
||||
}
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.weatherflow_cloud.coordinator.WeatherFlowRestAPI",
|
||||
autospec=True,
|
||||
) as mock_api_class:
|
||||
# Create an instance of AsyncMock for the API
|
||||
mock_api = AsyncMock()
|
||||
mock_api.get_all_data.return_value = data
|
||||
# Patch the class to return our mock_api instance
|
||||
mock_api_class.return_value = mock_api
|
||||
mock_api = AsyncMock(spec=WeatherFlowRestAPI)
|
||||
mock_api.get_all_data.return_value = data
|
||||
mock_api.async_get_stations.return_value = fixtures["stations"]
|
||||
mock_api.device_station_map = device_station_map
|
||||
mock_api.api_token = MOCK_API_TOKEN
|
||||
|
||||
# Apply patches
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.weatherflow_cloud.WeatherFlowRestAPI",
|
||||
return_value=mock_api,
|
||||
) as _,
|
||||
patch(
|
||||
"homeassistant.components.weatherflow_cloud.coordinator.WeatherFlowRestAPI",
|
||||
return_value=mock_api,
|
||||
) as _,
|
||||
):
|
||||
yield mock_api
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_stations_data(mock_rest_api):
|
||||
"""Mock stations data for coordinator tests."""
|
||||
return mock_rest_api.async_get_stations.return_value
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def mock_websocket_api():
|
||||
"""Mock WeatherFlowWebsocketAPI."""
|
||||
mock_websocket = AsyncMock()
|
||||
mock_websocket.send = AsyncMock()
|
||||
mock_websocket.recv = AsyncMock()
|
||||
|
||||
mock_ws_instance = AsyncMock(spec=WeatherFlowWebsocketAPI)
|
||||
mock_ws_instance.connect = AsyncMock()
|
||||
mock_ws_instance.send_message = AsyncMock()
|
||||
mock_ws_instance.register_callback = MagicMock()
|
||||
mock_ws_instance.websocket = mock_websocket
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.weatherflow_cloud.coordinator.WeatherFlowWebsocketAPI",
|
||||
return_value=mock_ws_instance,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.weatherflow_cloud.WeatherFlowWebsocketAPI",
|
||||
return_value=mock_ws_instance,
|
||||
),
|
||||
patch(
|
||||
"weatherflow4py.ws.WeatherFlowWebsocketAPI", return_value=mock_ws_instance
|
||||
),
|
||||
):
|
||||
# mock_connect.return_value = mock_websocket
|
||||
yield mock_ws_instance
|
||||
|
@ -42,7 +42,7 @@
|
||||
# name: test_all_entities[sensor.my_home_station_air_density-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'attribution': 'Weather data delivered by WeatherFlow/Tempest REST Api',
|
||||
'attribution': 'Weather data delivered by WeatherFlow/Tempest API',
|
||||
'friendly_name': 'My Home Station Air density',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': 'kg/m³',
|
||||
@ -98,7 +98,7 @@
|
||||
# name: test_all_entities[sensor.my_home_station_dew_point-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'attribution': 'Weather data delivered by WeatherFlow/Tempest REST Api',
|
||||
'attribution': 'Weather data delivered by WeatherFlow/Tempest API',
|
||||
'device_class': 'temperature',
|
||||
'friendly_name': 'My Home Station Dew point',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
@ -155,7 +155,7 @@
|
||||
# name: test_all_entities[sensor.my_home_station_feels_like-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'attribution': 'Weather data delivered by WeatherFlow/Tempest REST Api',
|
||||
'attribution': 'Weather data delivered by WeatherFlow/Tempest API',
|
||||
'device_class': 'temperature',
|
||||
'friendly_name': 'My Home Station Feels like',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
@ -212,7 +212,7 @@
|
||||
# name: test_all_entities[sensor.my_home_station_heat_index-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'attribution': 'Weather data delivered by WeatherFlow/Tempest REST Api',
|
||||
'attribution': 'Weather data delivered by WeatherFlow/Tempest API',
|
||||
'device_class': 'temperature',
|
||||
'friendly_name': 'My Home Station Heat index',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
@ -266,7 +266,7 @@
|
||||
# name: test_all_entities[sensor.my_home_station_lightning_count-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'attribution': 'Weather data delivered by WeatherFlow/Tempest REST Api',
|
||||
'attribution': 'Weather data delivered by WeatherFlow/Tempest API',
|
||||
'friendly_name': 'My Home Station Lightning count',
|
||||
'state_class': <SensorStateClass.TOTAL: 'total'>,
|
||||
}),
|
||||
@ -318,7 +318,7 @@
|
||||
# name: test_all_entities[sensor.my_home_station_lightning_count_last_1_hr-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'attribution': 'Weather data delivered by WeatherFlow/Tempest REST Api',
|
||||
'attribution': 'Weather data delivered by WeatherFlow/Tempest API',
|
||||
'friendly_name': 'My Home Station Lightning count last 1 hr',
|
||||
'state_class': <SensorStateClass.TOTAL: 'total'>,
|
||||
}),
|
||||
@ -370,7 +370,7 @@
|
||||
# name: test_all_entities[sensor.my_home_station_lightning_count_last_3_hr-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'attribution': 'Weather data delivered by WeatherFlow/Tempest REST Api',
|
||||
'attribution': 'Weather data delivered by WeatherFlow/Tempest API',
|
||||
'friendly_name': 'My Home Station Lightning count last 3 hr',
|
||||
'state_class': <SensorStateClass.TOTAL: 'total'>,
|
||||
}),
|
||||
@ -425,7 +425,7 @@
|
||||
# name: test_all_entities[sensor.my_home_station_lightning_last_distance-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'attribution': 'Weather data delivered by WeatherFlow/Tempest REST Api',
|
||||
'attribution': 'Weather data delivered by WeatherFlow/Tempest API',
|
||||
'device_class': 'distance',
|
||||
'friendly_name': 'My Home Station Lightning last distance',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
@ -477,7 +477,7 @@
|
||||
# name: test_all_entities[sensor.my_home_station_lightning_last_strike-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'attribution': 'Weather data delivered by WeatherFlow/Tempest REST Api',
|
||||
'attribution': 'Weather data delivered by WeatherFlow/Tempest API',
|
||||
'device_class': 'timestamp',
|
||||
'friendly_name': 'My Home Station Lightning last strike',
|
||||
}),
|
||||
@ -535,7 +535,7 @@
|
||||
# name: test_all_entities[sensor.my_home_station_pressure_barometric-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'attribution': 'Weather data delivered by WeatherFlow/Tempest REST Api',
|
||||
'attribution': 'Weather data delivered by WeatherFlow/Tempest API',
|
||||
'device_class': 'atmospheric_pressure',
|
||||
'friendly_name': 'My Home Station Pressure barometric',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
@ -595,7 +595,7 @@
|
||||
# name: test_all_entities[sensor.my_home_station_pressure_sea_level-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'attribution': 'Weather data delivered by WeatherFlow/Tempest REST Api',
|
||||
'attribution': 'Weather data delivered by WeatherFlow/Tempest API',
|
||||
'device_class': 'atmospheric_pressure',
|
||||
'friendly_name': 'My Home Station Pressure sea level',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
@ -652,7 +652,7 @@
|
||||
# name: test_all_entities[sensor.my_home_station_temperature-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'attribution': 'Weather data delivered by WeatherFlow/Tempest REST Api',
|
||||
'attribution': 'Weather data delivered by WeatherFlow/Tempest API',
|
||||
'device_class': 'temperature',
|
||||
'friendly_name': 'My Home Station Temperature',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
@ -709,7 +709,7 @@
|
||||
# name: test_all_entities[sensor.my_home_station_wet_bulb_globe_temperature-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'attribution': 'Weather data delivered by WeatherFlow/Tempest REST Api',
|
||||
'attribution': 'Weather data delivered by WeatherFlow/Tempest API',
|
||||
'device_class': 'temperature',
|
||||
'friendly_name': 'My Home Station Wet bulb globe temperature',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
@ -766,7 +766,7 @@
|
||||
# name: test_all_entities[sensor.my_home_station_wet_bulb_temperature-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'attribution': 'Weather data delivered by WeatherFlow/Tempest REST Api',
|
||||
'attribution': 'Weather data delivered by WeatherFlow/Tempest API',
|
||||
'device_class': 'temperature',
|
||||
'friendly_name': 'My Home Station Wet bulb temperature',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
@ -823,7 +823,7 @@
|
||||
# name: test_all_entities[sensor.my_home_station_wind_chill-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'attribution': 'Weather data delivered by WeatherFlow/Tempest REST Api',
|
||||
'attribution': 'Weather data delivered by WeatherFlow/Tempest API',
|
||||
'device_class': 'temperature',
|
||||
'friendly_name': 'My Home Station Wind chill',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
@ -837,3 +837,350 @@
|
||||
'state': '10.5',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.my_home_station_wind_direction-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.my_home_station_wind_direction',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.WIND_DIRECTION: 'wind_direction'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Wind direction',
|
||||
'platform': 'weatherflow_cloud',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'wind_direction',
|
||||
'unique_id': '24432_123456_wind_direction',
|
||||
'unit_of_measurement': '°',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.my_home_station_wind_direction-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'attribution': 'Weather data delivered by WeatherFlow/Tempest API',
|
||||
'device_class': 'wind_direction',
|
||||
'friendly_name': 'My Home Station Wind direction',
|
||||
'unit_of_measurement': '°',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.my_home_station_wind_direction',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unavailable',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.my_home_station_wind_gust-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.my_home_station_wind_gust',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 1,
|
||||
}),
|
||||
'sensor.private': dict({
|
||||
'suggested_unit_of_measurement': <UnitOfSpeed.KILOMETERS_PER_HOUR: 'km/h'>,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.WIND_SPEED: 'wind_speed'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Wind gust',
|
||||
'platform': 'weatherflow_cloud',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'wind_gust',
|
||||
'unique_id': '24432_123456_wind_gust',
|
||||
'unit_of_measurement': <UnitOfSpeed.KILOMETERS_PER_HOUR: 'km/h'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.my_home_station_wind_gust-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'attribution': 'Weather data delivered by WeatherFlow/Tempest API',
|
||||
'device_class': 'wind_speed',
|
||||
'friendly_name': 'My Home Station Wind gust',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfSpeed.KILOMETERS_PER_HOUR: 'km/h'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.my_home_station_wind_gust',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unavailable',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.my_home_station_wind_lull-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.my_home_station_wind_lull',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 1,
|
||||
}),
|
||||
'sensor.private': dict({
|
||||
'suggested_unit_of_measurement': <UnitOfSpeed.KILOMETERS_PER_HOUR: 'km/h'>,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.WIND_SPEED: 'wind_speed'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Wind lull',
|
||||
'platform': 'weatherflow_cloud',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'wind_lull',
|
||||
'unique_id': '24432_123456_wind_lull',
|
||||
'unit_of_measurement': <UnitOfSpeed.KILOMETERS_PER_HOUR: 'km/h'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.my_home_station_wind_lull-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'attribution': 'Weather data delivered by WeatherFlow/Tempest API',
|
||||
'device_class': 'wind_speed',
|
||||
'friendly_name': 'My Home Station Wind lull',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfSpeed.KILOMETERS_PER_HOUR: 'km/h'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.my_home_station_wind_lull',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unavailable',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.my_home_station_wind_sample_interval-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.my_home_station_wind_sample_interval',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 1,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Wind sample interval',
|
||||
'platform': 'weatherflow_cloud',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'wind_sample_interval',
|
||||
'unique_id': '24432_123456_wind_sample_interval',
|
||||
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.my_home_station_wind_sample_interval-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'attribution': 'Weather data delivered by WeatherFlow/Tempest API',
|
||||
'friendly_name': 'My Home Station Wind sample interval',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.my_home_station_wind_sample_interval',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unavailable',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.my_home_station_wind_speed-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.my_home_station_wind_speed',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 1,
|
||||
}),
|
||||
'sensor.private': dict({
|
||||
'suggested_unit_of_measurement': <UnitOfSpeed.KILOMETERS_PER_HOUR: 'km/h'>,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.WIND_SPEED: 'wind_speed'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Wind speed',
|
||||
'platform': 'weatherflow_cloud',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': '24432_123456_wind_speed',
|
||||
'unit_of_measurement': <UnitOfSpeed.KILOMETERS_PER_HOUR: 'km/h'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.my_home_station_wind_speed-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'attribution': 'Weather data delivered by WeatherFlow/Tempest API',
|
||||
'device_class': 'wind_speed',
|
||||
'friendly_name': 'My Home Station Wind speed',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfSpeed.KILOMETERS_PER_HOUR: 'km/h'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.my_home_station_wind_speed',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unavailable',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.my_home_station_wind_speed_avg-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.my_home_station_wind_speed_avg',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 1,
|
||||
}),
|
||||
'sensor.private': dict({
|
||||
'suggested_unit_of_measurement': <UnitOfSpeed.KILOMETERS_PER_HOUR: 'km/h'>,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.WIND_SPEED: 'wind_speed'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Wind speed (avg)',
|
||||
'platform': 'weatherflow_cloud',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'wind_avg',
|
||||
'unique_id': '24432_123456_wind_avg',
|
||||
'unit_of_measurement': <UnitOfSpeed.KILOMETERS_PER_HOUR: 'km/h'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.my_home_station_wind_speed_avg-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'attribution': 'Weather data delivered by WeatherFlow/Tempest API',
|
||||
'device_class': 'wind_speed',
|
||||
'friendly_name': 'My Home Station Wind speed (avg)',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfSpeed.KILOMETERS_PER_HOUR: 'km/h'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.my_home_station_wind_speed_avg',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unavailable',
|
||||
})
|
||||
# ---
|
||||
|
@ -37,7 +37,7 @@
|
||||
# name: test_weather[weather.my_home_station-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'attribution': 'Weather data delivered by WeatherFlow/Tempest REST Api',
|
||||
'attribution': 'Weather data delivered by WeatherFlow/Tempest API',
|
||||
'dew_point': -13.0,
|
||||
'friendly_name': 'My Home Station',
|
||||
'humidity': 27,
|
||||
|
223
tests/components/weatherflow_cloud/test_coordinators.py
Normal file
223
tests/components/weatherflow_cloud/test_coordinators.py
Normal file
@ -0,0 +1,223 @@
|
||||
"""Tests for the WeatherFlow Cloud coordinators."""
|
||||
|
||||
from unittest.mock import AsyncMock, Mock
|
||||
|
||||
from aiohttp import ClientResponseError
|
||||
import pytest
|
||||
from weatherflow4py.models.ws.types import EventType
|
||||
from weatherflow4py.models.ws.websocket_request import (
|
||||
ListenStartMessage,
|
||||
RapidWindListenStartMessage,
|
||||
)
|
||||
from weatherflow4py.models.ws.websocket_response import (
|
||||
EventDataRapidWind,
|
||||
ObservationTempestWS,
|
||||
RapidWindWS,
|
||||
)
|
||||
|
||||
from homeassistant.components.weatherflow_cloud.coordinator import (
|
||||
WeatherFlowCloudUpdateCoordinatorREST,
|
||||
WeatherFlowObservationCoordinator,
|
||||
WeatherFlowWindCoordinator,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntryAuthFailed
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import UpdateFailed
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_wind_coordinator_setup(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_rest_api: AsyncMock,
|
||||
mock_websocket_api: AsyncMock,
|
||||
mock_stations_data: Mock,
|
||||
) -> None:
|
||||
"""Test wind coordinator setup."""
|
||||
|
||||
coordinator = WeatherFlowWindCoordinator(
|
||||
hass=hass,
|
||||
config_entry=mock_config_entry,
|
||||
rest_api=mock_rest_api,
|
||||
websocket_api=mock_websocket_api,
|
||||
stations=mock_stations_data,
|
||||
)
|
||||
|
||||
await coordinator.async_setup()
|
||||
|
||||
# Verify websocket setup
|
||||
mock_websocket_api.connect.assert_called_once()
|
||||
mock_websocket_api.register_callback.assert_called_once_with(
|
||||
message_type=EventType.RAPID_WIND,
|
||||
callback=coordinator._handle_websocket_message,
|
||||
)
|
||||
# In the refactored code, send_message is called for each device ID
|
||||
assert mock_websocket_api.send_message.called
|
||||
|
||||
# Verify at least one message is of the correct type
|
||||
call_args_list = mock_websocket_api.send_message.call_args_list
|
||||
assert any(
|
||||
isinstance(call.args[0], RapidWindListenStartMessage) for call in call_args_list
|
||||
)
|
||||
|
||||
|
||||
async def test_observation_coordinator_setup(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_rest_api: AsyncMock,
|
||||
mock_websocket_api: AsyncMock,
|
||||
mock_stations_data: Mock,
|
||||
) -> None:
|
||||
"""Test observation coordinator setup."""
|
||||
|
||||
coordinator = WeatherFlowObservationCoordinator(
|
||||
hass=hass,
|
||||
config_entry=mock_config_entry,
|
||||
rest_api=mock_rest_api,
|
||||
websocket_api=mock_websocket_api,
|
||||
stations=mock_stations_data,
|
||||
)
|
||||
|
||||
await coordinator.async_setup()
|
||||
|
||||
# Verify websocket setup
|
||||
mock_websocket_api.connect.assert_called_once()
|
||||
mock_websocket_api.register_callback.assert_called_once_with(
|
||||
message_type=EventType.OBSERVATION,
|
||||
callback=coordinator._handle_websocket_message,
|
||||
)
|
||||
# In the refactored code, send_message is called for each device ID
|
||||
assert mock_websocket_api.send_message.called
|
||||
|
||||
# Verify at least one message is of the correct type
|
||||
call_args_list = mock_websocket_api.send_message.call_args_list
|
||||
assert any(isinstance(call.args[0], ListenStartMessage) for call in call_args_list)
|
||||
|
||||
|
||||
async def test_wind_coordinator_message_handling(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_rest_api: AsyncMock,
|
||||
mock_websocket_api: AsyncMock,
|
||||
mock_stations_data: Mock,
|
||||
) -> None:
|
||||
"""Test wind coordinator message handling."""
|
||||
|
||||
coordinator = WeatherFlowWindCoordinator(
|
||||
hass=hass,
|
||||
config_entry=mock_config_entry,
|
||||
rest_api=mock_rest_api,
|
||||
websocket_api=mock_websocket_api,
|
||||
stations=mock_stations_data,
|
||||
)
|
||||
|
||||
# Create mock wind data
|
||||
mock_wind_data = Mock(spec=EventDataRapidWind)
|
||||
mock_message = Mock(spec=RapidWindWS)
|
||||
|
||||
# Use a device ID from the actual mock data
|
||||
# The first device from the first station in the mock data
|
||||
device_id = mock_stations_data.stations[0].devices[0].device_id
|
||||
station_id = mock_stations_data.stations[0].station_id
|
||||
|
||||
mock_message.device_id = device_id
|
||||
mock_message.ob = mock_wind_data
|
||||
|
||||
# Handle the message
|
||||
await coordinator._handle_websocket_message(mock_message)
|
||||
|
||||
# Verify data was stored correctly
|
||||
assert coordinator._ws_data[station_id][device_id] == mock_wind_data
|
||||
|
||||
|
||||
async def test_observation_coordinator_message_handling(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_rest_api: AsyncMock,
|
||||
mock_websocket_api: AsyncMock,
|
||||
mock_stations_data: Mock,
|
||||
) -> None:
|
||||
"""Test observation coordinator message handling."""
|
||||
|
||||
coordinator = WeatherFlowObservationCoordinator(
|
||||
hass=hass,
|
||||
config_entry=mock_config_entry,
|
||||
rest_api=mock_rest_api,
|
||||
websocket_api=mock_websocket_api,
|
||||
stations=mock_stations_data,
|
||||
)
|
||||
|
||||
# Create mock observation data
|
||||
mock_message = Mock(spec=ObservationTempestWS)
|
||||
|
||||
# Use a device ID from the actual mock data
|
||||
# The first device from the first station in the mock data
|
||||
device_id = mock_stations_data.stations[0].devices[0].device_id
|
||||
station_id = mock_stations_data.stations[0].station_id
|
||||
|
||||
mock_message.device_id = device_id
|
||||
|
||||
# Handle the message
|
||||
await coordinator._handle_websocket_message(mock_message)
|
||||
|
||||
# Verify data was stored correctly (for observations, the message IS the data)
|
||||
assert coordinator._ws_data[station_id][device_id] == mock_message
|
||||
|
||||
|
||||
async def test_rest_coordinator_auth_error(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_rest_api: AsyncMock,
|
||||
mock_stations_data: Mock,
|
||||
) -> None:
|
||||
"""Test REST coordinator handling of 401 auth error."""
|
||||
# Create the coordinator
|
||||
coordinator = WeatherFlowCloudUpdateCoordinatorREST(
|
||||
hass=hass,
|
||||
config_entry=mock_config_entry,
|
||||
rest_api=mock_rest_api,
|
||||
stations=mock_stations_data,
|
||||
)
|
||||
|
||||
# Mock a 401 auth error
|
||||
mock_rest_api.get_all_data.side_effect = ClientResponseError(
|
||||
request_info=Mock(),
|
||||
history=Mock(),
|
||||
status=401,
|
||||
message="Unauthorized",
|
||||
)
|
||||
|
||||
# Verify the error is properly converted to ConfigEntryAuthFailed
|
||||
with pytest.raises(ConfigEntryAuthFailed):
|
||||
await coordinator._async_update_data()
|
||||
|
||||
|
||||
async def test_rest_coordinator_other_error(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_rest_api: AsyncMock,
|
||||
mock_stations_data: Mock,
|
||||
) -> None:
|
||||
"""Test REST coordinator handling of non-auth errors."""
|
||||
# Create the coordinator
|
||||
coordinator = WeatherFlowCloudUpdateCoordinatorREST(
|
||||
hass=hass,
|
||||
config_entry=mock_config_entry,
|
||||
rest_api=mock_rest_api,
|
||||
stations=mock_stations_data,
|
||||
)
|
||||
|
||||
# Mock a 500 server error
|
||||
mock_rest_api.get_all_data.side_effect = ClientResponseError(
|
||||
request_info=Mock(),
|
||||
history=Mock(),
|
||||
status=500,
|
||||
message="Internal Server Error",
|
||||
)
|
||||
|
||||
# Verify the error is properly converted to UpdateFailed
|
||||
with pytest.raises(
|
||||
UpdateFailed, match="Update failed: 500, message='Internal Server Error'"
|
||||
):
|
||||
await coordinator._async_update_data()
|
@ -1,13 +1,22 @@
|
||||
"""Tests for the WeatherFlow Cloud sensor platform."""
|
||||
|
||||
from datetime import timedelta
|
||||
from unittest.mock import AsyncMock, patch
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
from weatherflow4py.models.rest.observation import ObservationStationREST
|
||||
|
||||
from homeassistant.components.weatherflow_cloud import DOMAIN
|
||||
from homeassistant.components.weatherflow_cloud.coordinator import (
|
||||
WeatherFlowObservationCoordinator,
|
||||
WeatherFlowWindCoordinator,
|
||||
)
|
||||
from homeassistant.components.weatherflow_cloud.sensor import (
|
||||
WeatherFlowWebsocketSensorObservation,
|
||||
WeatherFlowWebsocketSensorWind,
|
||||
)
|
||||
from homeassistant.const import STATE_UNKNOWN, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
@ -17,17 +26,19 @@ from . import setup_integration
|
||||
from tests.common import (
|
||||
MockConfigEntry,
|
||||
async_fire_time_changed,
|
||||
async_load_fixture,
|
||||
load_fixture,
|
||||
snapshot_platform,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
async def test_all_entities(
|
||||
hass: HomeAssistant,
|
||||
snapshot: SnapshotAssertion,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mock_api: AsyncMock,
|
||||
mock_rest_api: AsyncMock,
|
||||
mock_websocket_api: AsyncMock,
|
||||
) -> None:
|
||||
"""Test all entities."""
|
||||
with patch(
|
||||
@ -38,17 +49,19 @@ async def test_all_entities(
|
||||
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
async def test_all_entities_with_lightning_error(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mock_api: AsyncMock,
|
||||
mock_rest_api: AsyncMock,
|
||||
mock_websocket_api: AsyncMock,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test all entities."""
|
||||
|
||||
get_observation_response_data = ObservationStationREST.from_json(
|
||||
await async_load_fixture(hass, "station_observation_error.json", DOMAIN)
|
||||
load_fixture("station_observation_error.json", DOMAIN)
|
||||
)
|
||||
|
||||
with patch(
|
||||
@ -62,9 +75,9 @@ async def test_all_entities_with_lightning_error(
|
||||
)
|
||||
|
||||
# Update the data in our API
|
||||
all_data = await mock_api.get_all_data()
|
||||
all_data = await mock_rest_api.get_all_data()
|
||||
all_data[24432].observation = get_observation_response_data
|
||||
mock_api.get_all_data.return_value = all_data
|
||||
mock_rest_api.get_all_data.return_value = all_data
|
||||
|
||||
# Move time forward
|
||||
freezer.tick(timedelta(minutes=5))
|
||||
@ -75,3 +88,92 @@ async def test_all_entities_with_lightning_error(
|
||||
hass.states.get("sensor.my_home_station_lightning_last_strike").state
|
||||
== STATE_UNKNOWN
|
||||
)
|
||||
|
||||
|
||||
async def test_websocket_sensor_observation(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_rest_api: AsyncMock,
|
||||
mock_websocket_api: AsyncMock,
|
||||
) -> None:
|
||||
"""Test the WebsocketSensorObservation class works."""
|
||||
# Set up the integration
|
||||
with patch(
|
||||
"homeassistant.components.weatherflow_cloud.PLATFORMS", [Platform.SENSOR]
|
||||
):
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
# Create a mock coordinator with test data
|
||||
coordinator = MagicMock(spec=WeatherFlowObservationCoordinator)
|
||||
|
||||
# Mock the coordinator data structure
|
||||
test_station_id = 24432
|
||||
test_device_id = 12345
|
||||
test_data = {
|
||||
"temperature": 22.5,
|
||||
"humidity": 45,
|
||||
"pressure": 1013.2,
|
||||
}
|
||||
|
||||
coordinator.data = {test_station_id: {test_device_id: test_data}}
|
||||
|
||||
# Create a sensor entity description
|
||||
entity_description = MagicMock()
|
||||
entity_description.value_fn = lambda data: data["temperature"]
|
||||
|
||||
# Create the sensor
|
||||
sensor = WeatherFlowWebsocketSensorObservation(
|
||||
coordinator=coordinator,
|
||||
description=entity_description,
|
||||
station_id=test_station_id,
|
||||
device_id=test_device_id,
|
||||
)
|
||||
|
||||
# Test that native_value returns the correct value
|
||||
assert sensor.native_value == 22.5
|
||||
|
||||
|
||||
async def test_websocket_sensor_wind(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_rest_api: AsyncMock,
|
||||
mock_websocket_api: AsyncMock,
|
||||
) -> None:
|
||||
"""Test the WebsocketSensorWind class works."""
|
||||
# Set up the integration
|
||||
with patch(
|
||||
"homeassistant.components.weatherflow_cloud.PLATFORMS", [Platform.SENSOR]
|
||||
):
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
# Create a mock coordinator with test data
|
||||
coordinator = MagicMock(spec=WeatherFlowWindCoordinator)
|
||||
|
||||
# Mock the coordinator data structure
|
||||
test_station_id = 24432
|
||||
test_device_id = 12345
|
||||
test_data = {
|
||||
"wind_speed": 5.2,
|
||||
"wind_direction": 180,
|
||||
}
|
||||
|
||||
coordinator.data = {test_station_id: {test_device_id: test_data}}
|
||||
|
||||
# Create a sensor entity description
|
||||
entity_description = MagicMock()
|
||||
entity_description.value_fn = lambda data: data["wind_speed"]
|
||||
|
||||
# Create the sensor
|
||||
sensor = WeatherFlowWebsocketSensorWind(
|
||||
coordinator=coordinator,
|
||||
description=entity_description,
|
||||
station_id=test_station_id,
|
||||
device_id=test_device_id,
|
||||
)
|
||||
|
||||
# Test that native_value returns the correct value
|
||||
assert sensor.native_value == 5.2
|
||||
|
||||
# Test with None data (startup condition)
|
||||
coordinator.data = None
|
||||
assert sensor.native_value is None
|
||||
|
@ -18,7 +18,9 @@ async def test_weather(
|
||||
snapshot: SnapshotAssertion,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mock_api: AsyncMock,
|
||||
mock_rest_api: AsyncMock,
|
||||
mock_get_stations: AsyncMock,
|
||||
mock_websocket_api: AsyncMock,
|
||||
) -> None:
|
||||
"""Test all entities."""
|
||||
with patch(
|
||||
|
Loading…
x
Reference in New Issue
Block a user