Add a new "Ambient Weather Network" integration (#105779)

* Adding a new "Ambient Weather Network" integration.

* Rebase and update code coverage.

* Addressed some reviewer comments.

* Remove mnemonics and replace with station names.

* Remove climate-utils

* Remove support for virtual stations.

* Rebase

* Address feedback

* Remove redundant errors

* Reviewer feedback

* Add icons.json

* More icons

* Reviewer feedback

* Fix test

* Make sensor tests more robust

* Make coordinator more robust

* Change update coordinator to raise UpdateFailed

* Recover from no station found error

* Dynamically set device name

* Address feedback

* Disable some sensors by default

* Reviewer feedback

* Change from hub to service

* Rebase

* Address reviewer feedback

* Reviewer feedback

* Manually rerun ruff on all files
This commit is contained in:
Thomas Kistler 2024-04-16 00:46:15 -07:00 committed by GitHub
parent f62fb76765
commit 0dd8ffd1f5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 2399 additions and 0 deletions

View File

@ -66,6 +66,7 @@ homeassistant.components.alpha_vantage.*
homeassistant.components.amazon_polly.*
homeassistant.components.amberelectric.*
homeassistant.components.ambiclimate.*
homeassistant.components.ambient_network.*
homeassistant.components.ambient_station.*
homeassistant.components.amcrest.*
homeassistant.components.ampio.*

View File

@ -90,6 +90,8 @@ build.json @home-assistant/supervisor
/tests/components/amberelectric/ @madpilot
/homeassistant/components/ambiclimate/ @danielhiversen
/tests/components/ambiclimate/ @danielhiversen
/homeassistant/components/ambient_network/ @thomaskistler
/tests/components/ambient_network/ @thomaskistler
/homeassistant/components/ambient_station/ @bachya
/tests/components/ambient_station/ @bachya
/homeassistant/components/amcrest/ @flacjacket

View File

@ -0,0 +1,35 @@
"""The Ambient Weather Network integration."""
from __future__ import annotations
from aioambient.open_api import OpenAPI
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from .const import DOMAIN
from .coordinator import AmbientNetworkDataUpdateCoordinator
PLATFORMS: list[Platform] = [Platform.SENSOR]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up the Ambient Weather Network from a config entry."""
api = OpenAPI()
coordinator = AmbientNetworkDataUpdateCoordinator(hass, api)
await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
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)
return unload_ok

View File

@ -0,0 +1,152 @@
"""Config flow for the Ambient Weather Network integration."""
from __future__ import annotations
from typing import Any
from aioambient import OpenAPI
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import (
CONF_LATITUDE,
CONF_LOCATION,
CONF_LONGITUDE,
CONF_MAC,
CONF_RADIUS,
UnitOfLength,
)
from homeassistant.helpers.selector import (
LocationSelector,
LocationSelectorConfig,
SelectOptionDict,
SelectSelector,
SelectSelectorConfig,
)
from homeassistant.util.unit_conversion import DistanceConverter
from .const import API_STATION_INDOOR, API_STATION_INFO, API_STATION_MAC_ADDRESS, DOMAIN
from .helper import get_station_name
CONF_USER = "user"
CONF_STATION = "station"
# One mile
CONF_RADIUS_DEFAULT = 1609.34
class AmbientNetworkConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle the config flow for the Ambient Weather Network integration."""
VERSION = 1
def __init__(self) -> None:
"""Construct the config flow."""
self._longitude = 0.0
self._latitude = 0.0
self._radius = 0.0
self._stations: dict[str, dict[str, Any]] = {}
async def async_step_user(
self,
user_input: dict[str, Any] | None = None,
) -> ConfigFlowResult:
"""Handle the initial step to select the location."""
errors: dict[str, str] | None = None
if user_input:
self._latitude = user_input[CONF_LOCATION][CONF_LATITUDE]
self._longitude = user_input[CONF_LOCATION][CONF_LONGITUDE]
self._radius = user_input[CONF_LOCATION][CONF_RADIUS]
client: OpenAPI = OpenAPI()
self._stations = {
x[API_STATION_MAC_ADDRESS]: x
for x in await client.get_devices_by_location(
self._latitude,
self._longitude,
radius=DistanceConverter.convert(
self._radius,
UnitOfLength.METERS,
UnitOfLength.MILES,
),
)
}
# Filter out indoor stations
self._stations = dict(
filter(
lambda item: not item[1]
.get(API_STATION_INFO, {})
.get(API_STATION_INDOOR, False),
self._stations.items(),
)
)
if self._stations:
return await self.async_step_station()
errors = {"base": "no_stations_found"}
schema: vol.Schema = self.add_suggested_values_to_schema(
vol.Schema(
{
vol.Required(
CONF_LOCATION,
): LocationSelector(LocationSelectorConfig(radius=True)),
}
),
{
CONF_LOCATION: {
CONF_LATITUDE: self.hass.config.latitude,
CONF_LONGITUDE: self.hass.config.longitude,
CONF_RADIUS: CONF_RADIUS_DEFAULT,
}
if not errors
else {
CONF_LATITUDE: self._latitude,
CONF_LONGITUDE: self._longitude,
CONF_RADIUS: self._radius,
}
},
)
return self.async_show_form(
step_id=CONF_USER, data_schema=schema, errors=errors if errors else {}
)
async def async_step_station(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the second step to select the station."""
if user_input:
mac_address = user_input[CONF_STATION]
await self.async_set_unique_id(mac_address)
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=get_station_name(self._stations[mac_address]),
data={CONF_MAC: mac_address},
)
options: list[SelectOptionDict] = [
SelectOptionDict(
label=get_station_name(station),
value=mac_address,
)
for mac_address, station in self._stations.items()
]
schema: vol.Schema = vol.Schema(
{
vol.Required(CONF_STATION): SelectSelector(
SelectSelectorConfig(options=options, multiple=False, sort=True),
)
}
)
return self.async_show_form(
step_id=CONF_STATION,
data_schema=schema,
)

View File

@ -0,0 +1,16 @@
"""Constants for the Ambient Weather Network integration."""
import logging
DOMAIN = "ambient_network"
API_LAST_DATA = "lastData"
API_STATION_COORDS = "coords"
API_STATION_INDOOR = "indoor"
API_STATION_INFO = "info"
API_STATION_LOCATION = "location"
API_STATION_NAME = "name"
API_STATION_MAC_ADDRESS = "macAddress"
API_STATION_TYPE = "stationtype"
LOGGER = logging.getLogger(__package__)

View File

@ -0,0 +1,65 @@
"""DataUpdateCoordinator for the Ambient Weather Network integration."""
from __future__ import annotations
from datetime import datetime, timedelta
from typing import Any, cast
from aioambient import OpenAPI
from aioambient.errors import RequestError
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_MAC
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import API_LAST_DATA, DOMAIN, LOGGER
from .helper import get_station_name
SCAN_INTERVAL = timedelta(minutes=5)
class AmbientNetworkDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
"""The Ambient Network Data Update Coordinator."""
config_entry: ConfigEntry
station_name: str
def __init__(self, hass: HomeAssistant, api: OpenAPI) -> None:
"""Initialize the coordinator."""
super().__init__(hass, LOGGER, name=DOMAIN, update_interval=SCAN_INTERVAL)
self.api = api
async def _async_update_data(self) -> dict[str, Any]:
"""Fetch the latest data from the Ambient Network."""
try:
response = await self.api.get_device_details(
self.config_entry.data[CONF_MAC]
)
except RequestError as ex:
raise UpdateFailed("Cannot connect to Ambient Network") from ex
self.station_name = get_station_name(response)
if (last_data := response.get(API_LAST_DATA)) is None:
raise UpdateFailed(
f"Station '{self.config_entry.title}' did not report any data"
)
# Eliminate data if the station hasn't been updated for a while.
if (created_at := last_data.get("created_at")) is None:
raise UpdateFailed(
f"Station '{self.config_entry.title}' did not report a time stamp"
)
# Eliminate data that has been generated more than an hour ago. The station is
# probably offline.
if int(created_at / 1000) < int(
(datetime.now() - timedelta(hours=1)).timestamp()
):
raise UpdateFailed(
f"Station '{self.config_entry.title}' reported stale data"
)
return cast(dict[str, Any], last_data)

View File

@ -0,0 +1,50 @@
"""Base entity class for the Ambient Weather Network integration."""
from __future__ import annotations
from abc import abstractmethod
from homeassistant.core import callback
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
from .coordinator import AmbientNetworkDataUpdateCoordinator
class AmbientNetworkEntity(CoordinatorEntity[AmbientNetworkDataUpdateCoordinator]):
"""Entity class for Ambient network devices."""
_attr_attribution = "Data provided by ambientnetwork.net"
_attr_has_entity_name = True
def __init__(
self,
coordinator: AmbientNetworkDataUpdateCoordinator,
description: EntityDescription,
mac_address: str,
) -> None:
"""Initialize the Ambient network entity."""
super().__init__(coordinator)
self.entity_description = description
self._attr_unique_id = f"{mac_address}_{description.key}"
self._attr_device_info = DeviceInfo(
entry_type=DeviceEntryType.SERVICE,
name=coordinator.station_name,
identifiers={(DOMAIN, mac_address)},
manufacturer="Ambient Weather",
)
self._update_attrs()
@abstractmethod
def _update_attrs(self) -> None:
"""Update state attributes."""
@callback
def _handle_coordinator_update(self) -> None:
"""Get the latest data and updates the state."""
self._update_attrs()
super()._handle_coordinator_update()

View File

@ -0,0 +1,31 @@
"""Helper class for the Ambient Weather Network integration."""
from __future__ import annotations
from typing import Any
from .const import (
API_LAST_DATA,
API_STATION_COORDS,
API_STATION_INFO,
API_STATION_LOCATION,
API_STATION_NAME,
API_STATION_TYPE,
)
def get_station_name(station: dict[str, Any]) -> str:
"""Pick a station name.
Station names can be empty, in which case we construct the name from
the location and device type.
"""
if name := station.get(API_STATION_INFO, {}).get(API_STATION_NAME):
return str(name)
location = (
station.get(API_STATION_INFO, {})
.get(API_STATION_COORDS, {})
.get(API_STATION_LOCATION)
)
station_type = station.get(API_LAST_DATA, {}).get(API_STATION_TYPE)
return f"{location}{'' if location is None or station_type is None else ' '}{station_type}"

View File

@ -0,0 +1,21 @@
{
"entity": {
"sensor": {
"last_rain": {
"default": "mdi:water"
},
"lightning_strikes_per_day": {
"default": "mdi:lightning-bolt"
},
"lightning_strikes_per_hour": {
"default": "mdi:lightning-bolt"
},
"lightning_distance": {
"default": "mdi:lightning-bolt"
},
"wind_direction": {
"default": "mdi:compass-outline"
}
}
}
}

View File

@ -0,0 +1,11 @@
{
"domain": "ambient_network",
"name": "Ambient Weather Network",
"codeowners": ["@thomaskistler"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/ambient_network",
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["aioambient"],
"requirements": ["aioambient==2024.01.0"]
}

View File

@ -0,0 +1,315 @@
"""Support for Ambient Weather Network sensors."""
from __future__ import annotations
from datetime import datetime
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_MILLION,
CONF_MAC,
DEGREE,
PERCENTAGE,
UnitOfIrradiance,
UnitOfLength,
UnitOfPrecipitationDepth,
UnitOfPressure,
UnitOfSpeed,
UnitOfTemperature,
UnitOfVolumetricFlux,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util import dt as dt_util
from .const import DOMAIN
from .coordinator import AmbientNetworkDataUpdateCoordinator
from .entity import AmbientNetworkEntity
TYPE_AQI_PM25 = "aqi_pm25"
TYPE_AQI_PM25_24H = "aqi_pm25_24h"
TYPE_BAROMABSIN = "baromabsin"
TYPE_BAROMRELIN = "baromrelin"
TYPE_CO2 = "co2"
TYPE_DAILYRAININ = "dailyrainin"
TYPE_DEWPOINT = "dewPoint"
TYPE_EVENTRAININ = "eventrainin"
TYPE_FEELSLIKE = "feelsLike"
TYPE_HOURLYRAININ = "hourlyrainin"
TYPE_HUMIDITY = "humidity"
TYPE_LASTRAIN = "lastRain"
TYPE_LIGHTNING_DISTANCE = "lightning_distance"
TYPE_LIGHTNING_PER_DAY = "lightning_day"
TYPE_LIGHTNING_PER_HOUR = "lightning_hour"
TYPE_MAXDAILYGUST = "maxdailygust"
TYPE_MONTHLYRAININ = "monthlyrainin"
TYPE_PM25 = "pm25"
TYPE_PM25_24H = "pm25_24h"
TYPE_SOLARRADIATION = "solarradiation"
TYPE_TEMPF = "tempf"
TYPE_UV = "uv"
TYPE_WEEKLYRAININ = "weeklyrainin"
TYPE_WINDDIR = "winddir"
TYPE_WINDGUSTMPH = "windgustmph"
TYPE_WINDSPEEDMPH = "windspeedmph"
TYPE_YEARLYRAININ = "yearlyrainin"
SENSOR_DESCRIPTIONS = (
SensorEntityDescription(
key=TYPE_AQI_PM25,
translation_key="pm25_aqi",
device_class=SensorDeviceClass.AQI,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
),
SensorEntityDescription(
key=TYPE_AQI_PM25_24H,
translation_key="pm25_aqi_24h_average",
device_class=SensorDeviceClass.AQI,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
entity_registry_enabled_default=False,
),
SensorEntityDescription(
key=TYPE_BAROMABSIN,
translation_key="absolute_pressure",
native_unit_of_measurement=UnitOfPressure.INHG,
device_class=SensorDeviceClass.PRESSURE,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=2,
entity_registry_enabled_default=False,
),
SensorEntityDescription(
key=TYPE_BAROMRELIN,
translation_key="relative_pressure",
native_unit_of_measurement=UnitOfPressure.INHG,
device_class=SensorDeviceClass.PRESSURE,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=2,
),
SensorEntityDescription(
key=TYPE_CO2,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
device_class=SensorDeviceClass.CO2,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=2,
entity_registry_enabled_default=False,
),
SensorEntityDescription(
key=TYPE_DAILYRAININ,
translation_key="daily_rain",
native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES,
device_class=SensorDeviceClass.PRECIPITATION,
state_class=SensorStateClass.TOTAL,
suggested_display_precision=2,
),
SensorEntityDescription(
key=TYPE_DEWPOINT,
translation_key="dew_point",
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
),
SensorEntityDescription(
key=TYPE_FEELSLIKE,
translation_key="feels_like",
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
),
SensorEntityDescription(
key=TYPE_HOURLYRAININ,
translation_key="hourly_rain",
native_unit_of_measurement=UnitOfVolumetricFlux.INCHES_PER_HOUR,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.PRECIPITATION_INTENSITY,
suggested_display_precision=2,
),
SensorEntityDescription(
key=TYPE_HUMIDITY,
native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
),
SensorEntityDescription(
key=TYPE_LASTRAIN,
translation_key="last_rain",
device_class=SensorDeviceClass.TIMESTAMP,
entity_registry_enabled_default=False,
),
SensorEntityDescription(
key=TYPE_LIGHTNING_PER_DAY,
translation_key="lightning_strikes_per_day",
native_unit_of_measurement="strikes",
state_class=SensorStateClass.TOTAL,
entity_registry_enabled_default=False,
),
SensorEntityDescription(
key=TYPE_LIGHTNING_PER_HOUR,
translation_key="lightning_strikes_per_hour",
native_unit_of_measurement="strikes/hour",
state_class=SensorStateClass.TOTAL,
entity_registry_enabled_default=False,
),
SensorEntityDescription(
key=TYPE_LIGHTNING_DISTANCE,
translation_key="lightning_distance",
native_unit_of_measurement=UnitOfLength.MILES,
device_class=SensorDeviceClass.DISTANCE,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=2,
entity_registry_enabled_default=False,
),
SensorEntityDescription(
key=TYPE_MAXDAILYGUST,
translation_key="max_daily_gust",
native_unit_of_measurement=UnitOfSpeed.MILES_PER_HOUR,
device_class=SensorDeviceClass.WIND_SPEED,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
),
SensorEntityDescription(
key=TYPE_MONTHLYRAININ,
translation_key="monthly_rain",
native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES,
device_class=SensorDeviceClass.PRECIPITATION,
state_class=SensorStateClass.TOTAL,
suggested_display_precision=2,
entity_registry_enabled_default=False,
),
SensorEntityDescription(
key=TYPE_PM25_24H,
translation_key="pm25_24h_average",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
device_class=SensorDeviceClass.PM25,
suggested_display_precision=1,
entity_registry_enabled_default=False,
),
SensorEntityDescription(
key=TYPE_PM25,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
device_class=SensorDeviceClass.PM25,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
entity_registry_enabled_default=False,
),
SensorEntityDescription(
key=TYPE_SOLARRADIATION,
native_unit_of_measurement=UnitOfIrradiance.WATTS_PER_SQUARE_METER,
device_class=SensorDeviceClass.IRRADIANCE,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
entity_registry_enabled_default=False,
),
SensorEntityDescription(
key=TYPE_TEMPF,
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
),
SensorEntityDescription(
key=TYPE_UV,
translation_key="uv_index",
native_unit_of_measurement="index",
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
),
SensorEntityDescription(
key=TYPE_WEEKLYRAININ,
translation_key="weekly_rain",
native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES,
device_class=SensorDeviceClass.PRECIPITATION,
state_class=SensorStateClass.TOTAL,
suggested_display_precision=2,
entity_registry_enabled_default=False,
),
SensorEntityDescription(
key=TYPE_WINDDIR,
translation_key="wind_direction",
native_unit_of_measurement=DEGREE,
suggested_display_precision=0,
entity_registry_enabled_default=False,
),
SensorEntityDescription(
key=TYPE_WINDGUSTMPH,
translation_key="wind_gust",
native_unit_of_measurement=UnitOfSpeed.MILES_PER_HOUR,
device_class=SensorDeviceClass.WIND_SPEED,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
),
SensorEntityDescription(
key=TYPE_WINDSPEEDMPH,
native_unit_of_measurement=UnitOfSpeed.MILES_PER_HOUR,
device_class=SensorDeviceClass.WIND_SPEED,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
),
SensorEntityDescription(
key=TYPE_YEARLYRAININ,
translation_key="yearly_rain",
native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES,
device_class=SensorDeviceClass.PRECIPITATION,
state_class=SensorStateClass.TOTAL,
suggested_display_precision=2,
entity_registry_enabled_default=False,
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the Ambient Network sensor entities."""
coordinator: AmbientNetworkDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
if coordinator.config_entry is not None:
async_add_entities(
AmbientNetworkSensor(
coordinator,
description,
coordinator.config_entry.data[CONF_MAC],
)
for description in SENSOR_DESCRIPTIONS
if coordinator.data.get(description.key) is not None
)
class AmbientNetworkSensor(AmbientNetworkEntity, SensorEntity):
"""A sensor implementation for an Ambient Weather Network sensor."""
def __init__(
self,
coordinator: AmbientNetworkDataUpdateCoordinator,
description: SensorEntityDescription,
mac_address: str,
) -> None:
"""Initialize a sensor object."""
super().__init__(coordinator, description, mac_address)
def _update_attrs(self) -> None:
"""Update sensor attributes."""
value = self.coordinator.data.get(self.entity_description.key)
# Treatments for special units.
if value is not None and self.device_class == SensorDeviceClass.TIMESTAMP:
value = datetime.fromtimestamp(value / 1000, tz=dt_util.DEFAULT_TIME_ZONE)
self._attr_available = value is not None
self._attr_native_value = value

View File

@ -0,0 +1,87 @@
{
"config": {
"step": {
"user": {
"title": "Select region",
"description": "Choose the region you want to survey in order to locate Ambient personal weather stations."
},
"station": {
"title": "Select station",
"description": "Select the weather station you want to add to Home Assistant.",
"data": {
"station": "Station"
}
}
},
"error": {
"no_stations_found": "Did not find any stations in the selected region."
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
},
"entity": {
"sensor": {
"pm25_24h_average": {
"name": "PM2.5 (24 hour average)"
},
"pm25_aqi": {
"name": "PM2.5 AQI"
},
"pm25_aqi_24h_average": {
"name": "PM2.5 AQI (24 hour average)"
},
"absolute_pressure": {
"name": "Absolute pressure"
},
"relative_pressure": {
"name": "Relative pressure"
},
"daily_rain": {
"name": "Daily rain"
},
"dew_point": {
"name": "Dew point"
},
"feels_like": {
"name": "Feels like"
},
"hourly_rain": {
"name": "Hourly rain"
},
"last_rain": {
"name": "Last rain"
},
"lightning_strikes_per_day": {
"name": "Lightning strikes per day"
},
"lightning_strikes_per_hour": {
"name": "Lightning strikes per hour"
},
"lightning_distance": {
"name": "Lightning distance"
},
"max_daily_gust": {
"name": "Max daily gust"
},
"monthly_rain": {
"name": "Monthly rain"
},
"uv_index": {
"name": "UV index"
},
"weekly_rain": {
"name": "Weekly rain"
},
"wind_direction": {
"name": "Wind direction"
},
"wind_gust": {
"name": "Wind gust"
},
"yearly_rain": {
"name": "Yearly rain"
}
}
}
}

View File

@ -42,6 +42,7 @@ FLOWS = {
"alarmdecoder",
"amberelectric",
"ambiclimate",
"ambient_network",
"ambient_station",
"analytics_insights",
"android_ip_webcam",

View File

@ -244,6 +244,12 @@
"config_flow": true,
"iot_class": "cloud_polling"
},
"ambient_network": {
"name": "Ambient Weather Network",
"integration_type": "service",
"config_flow": true,
"iot_class": "cloud_polling"
},
"ambient_station": {
"name": "Ambient Weather Station",
"integration_type": "hub",

View File

@ -421,6 +421,16 @@ disallow_untyped_defs = true
warn_return_any = true
warn_unreachable = true
[mypy-homeassistant.components.ambient_network.*]
check_untyped_defs = true
disallow_incomplete_defs = true
disallow_subclassing_any = true
disallow_untyped_calls = true
disallow_untyped_decorators = true
disallow_untyped_defs = true
warn_return_any = true
warn_unreachable = true
[mypy-homeassistant.components.ambient_station.*]
check_untyped_defs = true
disallow_incomplete_defs = true

View File

@ -190,6 +190,7 @@ aioairzone-cloud==0.5.1
# homeassistant.components.airzone
aioairzone==0.7.6
# homeassistant.components.ambient_network
# homeassistant.components.ambient_station
aioambient==2024.01.0

View File

@ -169,6 +169,7 @@ aioairzone-cloud==0.5.1
# homeassistant.components.airzone
aioairzone==0.7.6
# homeassistant.components.ambient_network
# homeassistant.components.ambient_station
aioambient==2024.01.0

View File

@ -0,0 +1 @@
"""Tests for the Ambient Weather Network integration."""

View File

@ -0,0 +1,91 @@
"""Common fixtures for the Ambient Weather Network integration tests."""
from collections.abc import Generator
from typing import Any
from unittest.mock import AsyncMock, Mock, patch
from aioambient import OpenAPI
import pytest
from homeassistant.components import ambient_network
from homeassistant.core import HomeAssistant
from tests.common import (
MockConfigEntry,
load_json_array_fixture,
load_json_object_fixture,
)
@pytest.fixture
def mock_setup_entry() -> Generator[AsyncMock, None, None]:
"""Override async_setup_entry."""
with patch(
"homeassistant.components.ambient_network.async_setup_entry", return_value=True
) as mock_setup_entry:
yield mock_setup_entry
@pytest.fixture(name="devices_by_location", scope="package")
def devices_by_location_fixture() -> list[dict[str, Any]]:
"""Return result of OpenAPI get_devices_by_location() call."""
return load_json_array_fixture(
"devices_by_location_response.json", "ambient_network"
)
def mock_device_details_callable(mac_address: str) -> dict[str, Any]:
"""Return result of OpenAPI get_device_details() call."""
return load_json_object_fixture(
f"device_details_response_{mac_address[0].lower()}.json", "ambient_network"
)
@pytest.fixture(name="open_api")
def mock_open_api() -> OpenAPI:
"""Mock OpenAPI object."""
return Mock(
get_device_details=AsyncMock(side_effect=mock_device_details_callable),
)
@pytest.fixture(name="aioambient")
async def mock_aioambient(open_api: OpenAPI):
"""Mock aioambient library."""
with (
patch(
"homeassistant.components.ambient_network.config_flow.OpenAPI",
return_value=open_api,
),
patch(
"homeassistant.components.ambient_network.OpenAPI",
return_value=open_api,
),
):
yield
@pytest.fixture(name="config_entry")
def config_entry_fixture(request) -> MockConfigEntry:
"""Mock config entry."""
return MockConfigEntry(
domain=ambient_network.DOMAIN,
title=f"Station {request.param[0]}",
data={"mac": request.param},
)
async def setup_platform(
expected_outcome: bool,
hass: HomeAssistant,
config_entry: MockConfigEntry,
):
"""Load the Ambient Network integration with the provided OpenAPI and config entry."""
config_entry.add_to_hass(hass)
assert (
await hass.config_entries.async_setup(config_entry.entry_id) == expected_outcome
)
await hass.async_block_till_done()
return

View File

@ -0,0 +1,34 @@
{
"_id": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"macAddress": "AA:AA:AA:AA:AA:AA",
"lastData": {
"stationtype": "AMBWeatherPro_V5.0.6",
"dateutc": 1699474320000,
"tempf": 82.9,
"dewPoint": 82.0,
"feelsLike": 85.0,
"humidity": 60,
"windspeedmph": 8.72,
"windgustmph": 9.17,
"maxdailygust": 22.82,
"winddir": 11,
"uv": 0,
"solarradiation": 37.64,
"hourlyrainin": 0,
"dailyrainin": 0,
"weeklyrainin": 0,
"monthlyrainin": 0,
"totalrainin": 26.402,
"baromrelin": 29.586,
"baromabsin": 28.869,
"batt_co2": 1,
"type": "weather-data",
"created_at": 1699474320914,
"dateutc5": 1699474200000,
"lastRain": 1698659100000,
"tz": "America/Chicago"
},
"info": {
"name": "Station A"
}
}

View File

@ -0,0 +1,7 @@
{
"_id": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"macAddress": "BB:BB:BB:BB:BB:BB",
"info": {
"name": "Station B"
}
}

View File

@ -0,0 +1,33 @@
{
"_id": "cccccccccccccccccccccccccccccccc",
"macAddress": "CC:CC:CC:CC:CC:CC",
"lastData": {
"stationtype": "AMBWeatherPro_V5.0.6",
"dateutc": 1699474320000,
"tempf": 82.9,
"dewPoint": 82.0,
"feelsLike": 85.0,
"humidity": 60,
"windspeedmph": 8.72,
"windgustmph": 9.17,
"maxdailygust": 22.82,
"winddir": 11,
"uv": 0,
"solarradiation": 37.64,
"hourlyrainin": 0,
"dailyrainin": 0,
"weeklyrainin": 0,
"monthlyrainin": 0,
"totalrainin": 26.402,
"baromrelin": 29.586,
"baromabsin": 28.869,
"batt_co2": 1,
"type": "weather-data",
"dateutc5": 1699474200000,
"lastRain": 1698659100000,
"tz": "America/Chicago"
},
"info": {
"name": "Station C"
}
}

View File

@ -0,0 +1,364 @@
[
{
"_id": "aaaaaaaaaaaaaaaaaaaaaaaa",
"macAddress": "AA:AA:AA:AA:AA:AA",
"lastData": {
"stationtype": "AMBWeatherPro_V5.0.6",
"dateutc": 1699474320000,
"tempf": 82.9,
"humidity": 60,
"windspeedmph": 8.72,
"windgustmph": 9.17,
"maxdailygust": 22.82,
"winddir": 11,
"uv": 0,
"solarradiation": 37.64,
"hourlyrainin": 0,
"dailyrainin": 0,
"weeklyrainin": 0,
"monthlyrainin": 0,
"totalrainin": 26.402,
"baromrelin": 29.586,
"baromabsin": 28.869,
"batt_co2": 1,
"type": "weather-data",
"created_at": 1699474320914,
"dateutc5": 1699474200000,
"lastRain": 1698659100000,
"tz": "America/Chicago"
},
"info": {
"name": "Station A",
"coords": {
"geo": {
"coordinates": [-97.0, 32.0],
"type": "Point"
},
"elevation": 237.0,
"location": "Location A",
"coords": {
"lon": -97.0,
"lat": 32.0
}
},
"indoor": false,
"slug": "aaaaaaaaaaaaaaaaaaaaaaaa"
},
"tz": {
"name": "America/Chicago"
}
},
{
"_id": "bbbbbbbbbbbbbbbbbbbbbbbb",
"macAddress": "BB:BB:BB:BB:BB:BB",
"lastData": {
"stationtype": "AMBWeatherV4.2.6",
"dateutc": 1700716980000,
"baromrelin": 29.342,
"baromabsin": 29.342,
"tempf": 35.8,
"humidity": 88,
"winddir": 237,
"winddir_avg10m": 221,
"windspeedmph": 0,
"windspdmph_avg10m": 0,
"windgustmph": 1.3,
"maxdailygust": 12.3,
"hourlyrainin": 0,
"eventrainin": 0,
"dailyrainin": 0,
"weeklyrainin": 0.024,
"monthlyrainin": 0.331,
"yearlyrainin": 12.382,
"solarradiation": 0,
"uv": 0,
"soilhum2": 0,
"type": "weather-data",
"created_at": 1700717004020,
"dateutc5": 1700716800000,
"lastRain": 1700445000000,
"discreets": {
"humidity1": [41, 42, 43]
},
"tz": "America/Chicago"
},
"info": {
"name": "Station B",
"coords": {
"coords": {
"lat": 32.0,
"lon": -97.0
},
"location": "Location B",
"elevation": 226.0,
"geo": {
"type": "Point",
"coordinates": [-97.0, 32.0]
}
},
"indoor": false,
"slug": "bbbbbbbbbbbbbbbbbbbbbbbb"
},
"tz": {
"name": "America/Chicago"
}
},
{
"_id": "cccccccccccccccccccccccc",
"macAddress": "CC:CC:CC:CC:CC:CC",
"lastData": {},
"info": {
"name": "Station C",
"coords": {
"geo": {
"coordinates": [-97.0, 32.0],
"type": "Point"
},
"elevation": 242.0,
"location": "Location C",
"coords": {
"lon": -97.0,
"lat": 32.0
}
},
"indoor": false,
"slug": "cccccccccccccccccccccccc"
},
"tz": {
"name": "America/Chicago"
}
},
{
"_id": "dddddddddddddddddddddddd",
"macAddress": "DD:DD:DD:DD:DD:DD",
"lastData": {
"stationtype": "AMBWeatherPro_V5.1.3",
"dateutc": 1700716920000,
"tempf": 38.1,
"humidity": 85,
"windspeedmph": 0,
"windgustmph": 0,
"maxdailygust": 0,
"winddir": 89,
"uv": 0,
"solarradiation": 0,
"hourlyrainin": 0,
"eventrainin": 0,
"dailyrainin": 0,
"weeklyrainin": 0.028,
"monthlyrainin": 0.327,
"yearlyrainin": 12.76,
"totalrainin": 12.76,
"baromrelin": 29.731,
"baromabsin": 29.338,
"type": "weather-data",
"created_at": 1700716969446,
"dateutc5": 1700716800000,
"lastRain": 1700449500000,
"tz": "America/Chicago"
},
"info": {
"name": "Station D",
"coords": {
"coords": {
"lat": 32.0,
"lon": -97.0
},
"address": "",
"location": "Location D",
"elevation": 221.0,
"address_components": [
{
"long_name": "1234",
"short_name": "1234",
"types": ["street_number"]
},
{
"long_name": "D Street",
"short_name": "D St.",
"types": ["route"]
},
{
"long_name": "D Town",
"short_name": "D Town",
"types": ["locality", "political"]
},
{
"long_name": "D County",
"short_name": "D County",
"types": ["administrative_area_level_2", "political"]
},
{
"long_name": "Delaware",
"short_name": "DE",
"types": ["administrative_area_level_1", "political"]
},
{
"long_name": "United States",
"short_name": "US",
"types": ["country", "political"]
},
{
"long_name": "12345",
"short_name": "12345",
"types": ["postal_code"]
}
],
"geo": {
"type": "Point",
"coordinates": [-97.0, 32.0]
}
},
"indoor": false,
"slug": "dddddddddddddddddddddddd"
},
"tz": {
"name": "America/Chicago"
}
},
{
"_id": "eeeeeeeeeeeeeeeeeeeeeeee",
"macAddress": "EE:EE:EE:EE:EE:EE",
"lastData": {
"stationtype": "AMBWeatherV4.3.4",
"dateutc": 1700716920000,
"baromrelin": 29.238,
"baromabsin": 29.238,
"tempf": 45,
"humidity": 55,
"winddir": 98,
"winddir_avg10m": 185,
"windspeedmph": 1.1,
"windspdmph_avg10m": 1.3,
"windgustmph": 3.4,
"maxdailygust": 12.5,
"hourlyrainin": 0,
"eventrainin": 0,
"dailyrainin": 0,
"weeklyrainin": 0.059,
"monthlyrainin": 0.39,
"yearlyrainin": 31.268,
"lightning_day": 1,
"lightning_time": 1700700515000,
"lightning_distance": 8.7,
"batt_lightning": 0,
"solarradiation": 0,
"uv": 0,
"batt_co2": 1,
"type": "weather-data",
"created_at": 1700716954726,
"dateutc5": 1700716800000,
"lastRain": 1700445300000,
"lightnings": [
[1700713320000, 0],
[1700713380000, 0],
[1700713440000, 0],
[1700713500000, 0],
[1700713560000, 0],
[1700713620000, 0],
[1700713680000, 0],
[1700713740000, 0],
[1700713800000, 0],
[1700713860000, 0],
[1700713920000, 0],
[1700713980000, 0],
[1700714040000, 0],
[1700714100000, 0],
[1700714160000, 0],
[1700714220000, 0],
[1700714280000, 0],
[1700714340000, 0],
[1700714400000, 0],
[1700714460000, 0],
[1700714520000, 0],
[1700714580000, 0],
[1700714640000, 0],
[1700714700000, 0],
[1700714760000, 0],
[1700714820000, 0],
[1700714880000, 0],
[1700714940000, 0],
[1700715000000, 0],
[1700715060000, 0],
[1700715120000, 0],
[1700715180000, 0],
[1700715240000, 0],
[1700715300000, 0],
[1700715360000, 0],
[1700715420000, 0],
[1700715480000, 0],
[1700715540000, 0],
[1700715600000, 0],
[1700715660000, 0],
[1700715720000, 0],
[1700715780000, 0],
[1700715840000, 0],
[1700715900000, 0],
[1700715960000, 0],
[1700716020000, 0],
[1700716080000, 0],
[1700716140000, 0],
[1700716200000, 0],
[1700716260000, 0],
[1700716320000, 0],
[1700716380000, 0],
[1700716440000, 0],
[1700716500000, 0],
[1700716560000, 0],
[1700716620000, 0],
[1700716680000, 0],
[1700716740000, 0],
[1700716800000, 0],
[1700716860000, 0],
[1700716920000, 0]
],
"lightning_hour": 0,
"tz": "America/Chicago"
},
"info": {
"name": "Station E",
"coords": {
"coords": {
"lat": 32.0,
"lon": -97.0
},
"location": "Location E",
"elevation": 236.0,
"geo": {
"type": "Point",
"coordinates": [-97.0, 32.0]
}
},
"indoor": false,
"slug": "eeeeeeeeeeeeeeeeeeeeeeee"
},
"tz": {
"name": "America/Chicago"
}
},
{
"_id": "ffffffffffffffffffffffff",
"macAddress": "FF:FF:FF:FF:FF:FF",
"lastData": {},
"info": {
"name": "",
"coords": {
"coords": {
"lat": 32.0,
"lon": -97.0
},
"location": "Location F",
"elevation": 242.0,
"geo": {
"type": "Point",
"coordinates": [-97.0, 32.0]
}
},
"indoor": false,
"slug": "ffffffffffffffffffffffff"
},
"tz": {
"name": "America/Chicago"
}
}
]

View File

@ -0,0 +1,856 @@
# serializer version: 1
# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_absolute_pressure-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': <RegistryEntryDisabler.INTEGRATION: 'integration'>,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.station_a_absolute_pressure',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfPressure.HPA: 'hPa'>,
}),
}),
'original_device_class': <SensorDeviceClass.PRESSURE: 'pressure'>,
'original_icon': None,
'original_name': 'Absolute pressure',
'platform': 'ambient_network',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'absolute_pressure',
'unique_id': 'AA:AA:AA:AA:AA:AA_baromabsin',
'unit_of_measurement': <UnitOfPressure.HPA: 'hPa'>,
})
# ---
# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_absolute_pressure-state]
None
# ---
# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_daily_rain-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL: 'total'>,
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.station_a_daily_rain',
'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': <UnitOfLength.MILLIMETERS: 'mm'>,
}),
}),
'original_device_class': <SensorDeviceClass.PRECIPITATION: 'precipitation'>,
'original_icon': None,
'original_name': 'Daily rain',
'platform': 'ambient_network',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'daily_rain',
'unique_id': 'AA:AA:AA:AA:AA:AA_dailyrainin',
'unit_of_measurement': <UnitOfLength.MILLIMETERS: 'mm'>,
})
# ---
# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_daily_rain-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by ambientnetwork.net',
'device_class': 'precipitation',
'friendly_name': 'Station A Daily rain',
'state_class': <SensorStateClass.TOTAL: 'total'>,
'unit_of_measurement': <UnitOfLength.MILLIMETERS: 'mm'>,
}),
'context': <ANY>,
'entity_id': 'sensor.station_a_daily_rain',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '0.0',
})
# ---
# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_dew_point-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.station_a_dew_point',
'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': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
'original_icon': None,
'original_name': 'Dew point',
'platform': 'ambient_network',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'dew_point',
'unique_id': 'AA:AA:AA:AA:AA:AA_dewPoint',
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
})
# ---
# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_dew_point-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by ambientnetwork.net',
'device_class': 'temperature',
'friendly_name': 'Station A Dew point',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
}),
'context': <ANY>,
'entity_id': 'sensor.station_a_dew_point',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '27.7777777777778',
})
# ---
# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_feels_like-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.station_a_feels_like',
'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': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
'original_icon': None,
'original_name': 'Feels like',
'platform': 'ambient_network',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'feels_like',
'unique_id': 'AA:AA:AA:AA:AA:AA_feelsLike',
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
})
# ---
# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_feels_like-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by ambientnetwork.net',
'device_class': 'temperature',
'friendly_name': 'Station A Feels like',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
}),
'context': <ANY>,
'entity_id': 'sensor.station_a_feels_like',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '29.4444444444444',
})
# ---
# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_hourly_rain-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.station_a_hourly_rain',
'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': <UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR: 'mm/h'>,
}),
}),
'original_device_class': <SensorDeviceClass.PRECIPITATION_INTENSITY: 'precipitation_intensity'>,
'original_icon': None,
'original_name': 'Hourly rain',
'platform': 'ambient_network',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'hourly_rain',
'unique_id': 'AA:AA:AA:AA:AA:AA_hourlyrainin',
'unit_of_measurement': <UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR: 'mm/h'>,
})
# ---
# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_hourly_rain-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by ambientnetwork.net',
'device_class': 'precipitation_intensity',
'friendly_name': 'Station A Hourly rain',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR: 'mm/h'>,
}),
'context': <ANY>,
'entity_id': 'sensor.station_a_hourly_rain',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '0.0',
})
# ---
# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_humidity-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.station_a_humidity',
'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': <SensorDeviceClass.HUMIDITY: 'humidity'>,
'original_icon': None,
'original_name': 'Humidity',
'platform': 'ambient_network',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'AA:AA:AA:AA:AA:AA_humidity',
'unit_of_measurement': '%',
})
# ---
# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_humidity-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by ambientnetwork.net',
'device_class': 'humidity',
'friendly_name': 'Station A Humidity',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': '%',
}),
'context': <ANY>,
'entity_id': 'sensor.station_a_humidity',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '60',
})
# ---
# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_irradiance-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': <RegistryEntryDisabler.INTEGRATION: 'integration'>,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.station_a_irradiance',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.IRRADIANCE: 'irradiance'>,
'original_icon': None,
'original_name': 'Irradiance',
'platform': 'ambient_network',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'AA:AA:AA:AA:AA:AA_solarradiation',
'unit_of_measurement': <UnitOfIrradiance.WATTS_PER_SQUARE_METER: 'W/m²'>,
})
# ---
# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_irradiance-state]
None
# ---
# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_last_rain-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': <RegistryEntryDisabler.INTEGRATION: 'integration'>,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.station_a_last_rain',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.TIMESTAMP: 'timestamp'>,
'original_icon': None,
'original_name': 'Last rain',
'platform': 'ambient_network',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'last_rain',
'unique_id': 'AA:AA:AA:AA:AA:AA_lastRain',
'unit_of_measurement': None,
})
# ---
# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_last_rain-state]
None
# ---
# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_max_daily_gust-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.station_a_max_daily_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': 'Max daily gust',
'platform': 'ambient_network',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'max_daily_gust',
'unique_id': 'AA:AA:AA:AA:AA:AA_maxdailygust',
'unit_of_measurement': <UnitOfSpeed.KILOMETERS_PER_HOUR: 'km/h'>,
})
# ---
# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_max_daily_gust-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by ambientnetwork.net',
'device_class': 'wind_speed',
'friendly_name': 'Station A Max daily gust',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfSpeed.KILOMETERS_PER_HOUR: 'km/h'>,
}),
'context': <ANY>,
'entity_id': 'sensor.station_a_max_daily_gust',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '36.72523008',
})
# ---
# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_monthly_rain-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL: 'total'>,
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': <RegistryEntryDisabler.INTEGRATION: 'integration'>,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.station_a_monthly_rain',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfLength.MILLIMETERS: 'mm'>,
}),
}),
'original_device_class': <SensorDeviceClass.PRECIPITATION: 'precipitation'>,
'original_icon': None,
'original_name': 'Monthly rain',
'platform': 'ambient_network',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'monthly_rain',
'unique_id': 'AA:AA:AA:AA:AA:AA_monthlyrainin',
'unit_of_measurement': <UnitOfLength.MILLIMETERS: 'mm'>,
})
# ---
# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_monthly_rain-state]
None
# ---
# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_relative_pressure-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.station_a_relative_pressure',
'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': <UnitOfPressure.HPA: 'hPa'>,
}),
}),
'original_device_class': <SensorDeviceClass.PRESSURE: 'pressure'>,
'original_icon': None,
'original_name': 'Relative pressure',
'platform': 'ambient_network',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'relative_pressure',
'unique_id': 'AA:AA:AA:AA:AA:AA_baromrelin',
'unit_of_measurement': <UnitOfPressure.HPA: 'hPa'>,
})
# ---
# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_relative_pressure-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by ambientnetwork.net',
'device_class': 'pressure',
'friendly_name': 'Station A Relative pressure',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfPressure.HPA: 'hPa'>,
}),
'context': <ANY>,
'entity_id': 'sensor.station_a_relative_pressure',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '1001.89694313129',
})
# ---
# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_temperature-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.station_a_temperature',
'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': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
'original_icon': None,
'original_name': 'Temperature',
'platform': 'ambient_network',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'AA:AA:AA:AA:AA:AA_tempf',
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
})
# ---
# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_temperature-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by ambientnetwork.net',
'device_class': 'temperature',
'friendly_name': 'Station A Temperature',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
}),
'context': <ANY>,
'entity_id': 'sensor.station_a_temperature',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '28.2777777777778',
})
# ---
# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_uv_index-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.station_a_uv_index',
'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': 'UV index',
'platform': 'ambient_network',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'uv_index',
'unique_id': 'AA:AA:AA:AA:AA:AA_uv',
'unit_of_measurement': 'index',
})
# ---
# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_uv_index-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by ambientnetwork.net',
'friendly_name': 'Station A UV index',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'index',
}),
'context': <ANY>,
'entity_id': 'sensor.station_a_uv_index',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '0',
})
# ---
# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_weekly_rain-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL: 'total'>,
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': <RegistryEntryDisabler.INTEGRATION: 'integration'>,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.station_a_weekly_rain',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfLength.MILLIMETERS: 'mm'>,
}),
}),
'original_device_class': <SensorDeviceClass.PRECIPITATION: 'precipitation'>,
'original_icon': None,
'original_name': 'Weekly rain',
'platform': 'ambient_network',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'weekly_rain',
'unique_id': 'AA:AA:AA:AA:AA:AA_weeklyrainin',
'unit_of_measurement': <UnitOfLength.MILLIMETERS: 'mm'>,
})
# ---
# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_weekly_rain-state]
None
# ---
# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_wind_direction-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': <RegistryEntryDisabler.INTEGRATION: 'integration'>,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.station_a_wind_direction',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Wind direction',
'platform': 'ambient_network',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'wind_direction',
'unique_id': 'AA:AA:AA:AA:AA:AA_winddir',
'unit_of_measurement': '°',
})
# ---
# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_wind_direction-state]
None
# ---
# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_wind_gust-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.station_a_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': 'ambient_network',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'wind_gust',
'unique_id': 'AA:AA:AA:AA:AA:AA_windgustmph',
'unit_of_measurement': <UnitOfSpeed.KILOMETERS_PER_HOUR: 'km/h'>,
})
# ---
# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_wind_gust-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by ambientnetwork.net',
'device_class': 'wind_speed',
'friendly_name': 'Station A Wind gust',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfSpeed.KILOMETERS_PER_HOUR: 'km/h'>,
}),
'context': <ANY>,
'entity_id': 'sensor.station_a_wind_gust',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '14.75768448',
})
# ---
# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_wind_speed-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.station_a_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': 'ambient_network',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'AA:AA:AA:AA:AA:AA_windspeedmph',
'unit_of_measurement': <UnitOfSpeed.KILOMETERS_PER_HOUR: 'km/h'>,
})
# ---
# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_wind_speed-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by ambientnetwork.net',
'device_class': 'wind_speed',
'friendly_name': 'Station A Wind speed',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfSpeed.KILOMETERS_PER_HOUR: 'km/h'>,
}),
'context': <ANY>,
'entity_id': 'sensor.station_a_wind_speed',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '14.03347968',
})
# ---

View File

@ -0,0 +1,85 @@
"""Test the Ambient Weather Network config flow."""
from typing import Any
from unittest.mock import AsyncMock, patch
from aioambient import OpenAPI
import pytest
from homeassistant.components.ambient_network.const import DOMAIN
from homeassistant.config_entries import SOURCE_USER, ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
pytestmark = pytest.mark.usefixtures("mock_setup_entry")
@pytest.mark.parametrize("config_entry", ["AA:AA:AA:AA:AA:AA"], indirect=True)
async def test_happy_path(
hass: HomeAssistant,
mock_setup_entry: AsyncMock,
open_api: OpenAPI,
aioambient: AsyncMock,
devices_by_location: list[dict[str, Any]],
config_entry: ConfigEntry,
) -> None:
"""Test the happy path."""
setup_result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert setup_result["type"] == FlowResultType.FORM
assert setup_result["step_id"] == "user"
with patch.object(
open_api,
"get_devices_by_location",
AsyncMock(return_value=devices_by_location),
):
user_result = await hass.config_entries.flow.async_configure(
setup_result["flow_id"],
{"location": {"latitude": 10.0, "longitude": 20.0, "radius": 1.0}},
)
assert user_result["type"] == FlowResultType.FORM
assert user_result["step_id"] == "station"
stations_result = await hass.config_entries.flow.async_configure(
user_result["flow_id"],
{
"station": "AA:AA:AA:AA:AA:AA",
},
)
assert stations_result["type"] == FlowResultType.CREATE_ENTRY
assert stations_result["title"] == config_entry.title
assert stations_result["data"] == config_entry.data
assert len(mock_setup_entry.mock_calls) == 1
async def test_no_station_found(
hass: HomeAssistant,
aioambient: AsyncMock,
open_api: OpenAPI,
) -> None:
"""Test that we abort when we cannot find a station in the area."""
setup_result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert setup_result["type"] == FlowResultType.FORM
assert setup_result["step_id"] == "user"
with patch.object(
open_api,
"get_devices_by_location",
AsyncMock(return_value=[]),
):
user_result = await hass.config_entries.flow.async_configure(
setup_result["flow_id"],
{"location": {"latitude": 10.0, "longitude": 20.0, "radius": 1.0}},
)
assert user_result["type"] == FlowResultType.FORM
assert user_result["step_id"] == "user"
assert user_result["errors"] == {"base": "no_stations_found"}

View File

@ -0,0 +1,123 @@
"""Test Ambient Weather Network sensors."""
from datetime import datetime, timedelta
from unittest.mock import patch
from aioambient import OpenAPI
from aioambient.errors import RequestError
from freezegun import freeze_time
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from .conftest import setup_platform
from tests.common import async_fire_time_changed
@freeze_time("2023-11-08")
@pytest.mark.parametrize("config_entry", ["AA:AA:AA:AA:AA:AA"], indirect=True)
async def test_sensors(
hass: HomeAssistant,
open_api: OpenAPI,
aioambient,
config_entry,
entity_registry: er.EntityRegistry,
snapshot: SnapshotAssertion,
) -> None:
"""Test all sensors under normal operation."""
await setup_platform(True, hass, config_entry)
entity_entries = er.async_entries_for_config_entry(
entity_registry, config_entry.entry_id
)
assert entity_entries
for entity_entry in entity_entries:
assert hass.states.get(entity_entry.entity_id) == snapshot(
name=f"{entity_entry.entity_id}-state"
)
assert entity_entry == snapshot(name=f"{entity_entry.entity_id}-entry")
@freeze_time("2023-11-09")
@pytest.mark.parametrize("config_entry", ["AA:AA:AA:AA:AA:AA"], indirect=True)
async def test_sensors_with_stale_data(
hass: HomeAssistant, open_api: OpenAPI, aioambient, config_entry
) -> None:
"""Test that the sensors are not populated if the data is stale."""
await setup_platform(False, hass, config_entry)
sensor = hass.states.get("sensor.station_a_absolute_pressure")
assert sensor is None
@freeze_time("2023-11-08")
@pytest.mark.parametrize("config_entry", ["BB:BB:BB:BB:BB:BB"], indirect=True)
async def test_sensors_with_no_data(
hass: HomeAssistant, open_api: OpenAPI, aioambient, config_entry
) -> None:
"""Test that the sensors are not populated if the last data is absent."""
await setup_platform(False, hass, config_entry)
sensor = hass.states.get("sensor.station_b_absolute_pressure")
assert sensor is None
@freeze_time("2023-11-08")
@pytest.mark.parametrize("config_entry", ["CC:CC:CC:CC:CC:CC"], indirect=True)
async def test_sensors_with_no_update_time(
hass: HomeAssistant, open_api: OpenAPI, aioambient, config_entry
) -> None:
"""Test that the sensors are not populated if the update time is missing."""
await setup_platform(False, hass, config_entry)
sensor = hass.states.get("sensor.station_c_absolute_pressure")
assert sensor is None
@pytest.mark.parametrize("config_entry", ["AA:AA:AA:AA:AA:AA"], indirect=True)
async def test_sensors_disappearing(
hass: HomeAssistant,
open_api: OpenAPI,
aioambient,
config_entry,
caplog,
) -> None:
"""Test that we log errors properly."""
initial_datetime = datetime(year=2023, month=11, day=8)
with freeze_time(initial_datetime) as frozen_datetime:
# Normal state, sensor is available.
await setup_platform(True, hass, config_entry)
sensor = hass.states.get("sensor.station_a_relative_pressure")
assert sensor is not None
assert float(sensor.state) == pytest.approx(1001.89694313129)
# Sensor becomes unavailable if the network is unavailable. Log message
# should only show up once.
for _ in range(5):
with patch.object(
open_api, "get_device_details", side_effect=RequestError()
):
frozen_datetime.tick(timedelta(minutes=10))
async_fire_time_changed(hass)
await hass.async_block_till_done()
sensor = hass.states.get("sensor.station_a_relative_pressure")
assert sensor is not None
assert sensor.state == "unavailable"
assert caplog.text.count("Cannot connect to Ambient Network") == 1
# Network comes back. Sensor should start reporting again. Log message
# should only show up once.
for _ in range(5):
frozen_datetime.tick(timedelta(minutes=10))
async_fire_time_changed(hass)
await hass.async_block_till_done()
sensor = hass.states.get("sensor.station_a_relative_pressure")
assert sensor is not None
assert float(sensor.state) == pytest.approx(1001.89694313129)
assert caplog.text.count("Fetching ambient_network data recovered") == 1