mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 05:07:41 +00:00
Add nws sensor platform (#45027)
* Resolve rebase conflict. Remove logging * lint: fix elif after return * fix attribution * add tests for None valuea * Remove Entity import Co-authored-by: Erik Montnemery <erik@montnemery.com> * Import SensorEntity Co-authored-by: Erik Montnemery <erik@montnemery.com> * Inherit SensorEntity Co-authored-by: Erik Montnemery <erik@montnemery.com> * remove unused logging * Use CoordinatorEntity * Use type instead of name. * add all entities * add nice rounding to temperature and humidity Co-authored-by: Erik Montnemery <erik@montnemery.com>
This commit is contained in:
parent
9f481e1642
commit
f8f0495319
@ -28,7 +28,7 @@ from .const import (
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
PLATFORMS = ["weather"]
|
PLATFORMS = ["sensor", "weather"]
|
||||||
|
|
||||||
DEFAULT_SCAN_INTERVAL = datetime.timedelta(minutes=10)
|
DEFAULT_SCAN_INTERVAL = datetime.timedelta(minutes=10)
|
||||||
FAILED_SCAN_INTERVAL = datetime.timedelta(minutes=1)
|
FAILED_SCAN_INTERVAL = datetime.timedelta(minutes=1)
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
"""Constants for National Weather Service Integration."""
|
"""Constants for National Weather Service Integration."""
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
from homeassistant.components.weather import (
|
from homeassistant.components.weather import (
|
||||||
ATTR_CONDITION_CLOUDY,
|
ATTR_CONDITION_CLOUDY,
|
||||||
ATTR_CONDITION_EXCEPTIONAL,
|
ATTR_CONDITION_EXCEPTIONAL,
|
||||||
@ -14,6 +16,21 @@ from homeassistant.components.weather import (
|
|||||||
ATTR_CONDITION_WINDY,
|
ATTR_CONDITION_WINDY,
|
||||||
ATTR_CONDITION_WINDY_VARIANT,
|
ATTR_CONDITION_WINDY_VARIANT,
|
||||||
)
|
)
|
||||||
|
from homeassistant.const import (
|
||||||
|
ATTR_DEVICE_CLASS,
|
||||||
|
DEGREE,
|
||||||
|
DEVICE_CLASS_HUMIDITY,
|
||||||
|
DEVICE_CLASS_PRESSURE,
|
||||||
|
DEVICE_CLASS_TEMPERATURE,
|
||||||
|
LENGTH_METERS,
|
||||||
|
LENGTH_MILES,
|
||||||
|
PERCENTAGE,
|
||||||
|
PRESSURE_INHG,
|
||||||
|
PRESSURE_PA,
|
||||||
|
SPEED_KILOMETERS_PER_HOUR,
|
||||||
|
SPEED_MILES_PER_HOUR,
|
||||||
|
TEMP_CELSIUS,
|
||||||
|
)
|
||||||
|
|
||||||
DOMAIN = "nws"
|
DOMAIN = "nws"
|
||||||
|
|
||||||
@ -23,6 +40,11 @@ ATTRIBUTION = "Data from National Weather Service/NOAA"
|
|||||||
|
|
||||||
ATTR_FORECAST_DETAILED_DESCRIPTION = "detailed_description"
|
ATTR_FORECAST_DETAILED_DESCRIPTION = "detailed_description"
|
||||||
ATTR_FORECAST_DAYTIME = "daytime"
|
ATTR_FORECAST_DAYTIME = "daytime"
|
||||||
|
ATTR_ICON = "icon"
|
||||||
|
ATTR_LABEL = "label"
|
||||||
|
ATTR_UNIT = "unit"
|
||||||
|
ATTR_UNIT_CONVERT = "unit_convert"
|
||||||
|
ATTR_UNIT_CONVERT_METHOD = "unit_convert_method"
|
||||||
|
|
||||||
CONDITION_CLASSES = {
|
CONDITION_CLASSES = {
|
||||||
ATTR_CONDITION_EXCEPTIONAL: [
|
ATTR_CONDITION_EXCEPTIONAL: [
|
||||||
@ -75,3 +97,86 @@ NWS_DATA = "nws data"
|
|||||||
COORDINATOR_OBSERVATION = "coordinator_observation"
|
COORDINATOR_OBSERVATION = "coordinator_observation"
|
||||||
COORDINATOR_FORECAST = "coordinator_forecast"
|
COORDINATOR_FORECAST = "coordinator_forecast"
|
||||||
COORDINATOR_FORECAST_HOURLY = "coordinator_forecast_hourly"
|
COORDINATOR_FORECAST_HOURLY = "coordinator_forecast_hourly"
|
||||||
|
|
||||||
|
OBSERVATION_VALID_TIME = timedelta(minutes=20)
|
||||||
|
FORECAST_VALID_TIME = timedelta(minutes=45)
|
||||||
|
|
||||||
|
SENSOR_TYPES = {
|
||||||
|
"dewpoint": {
|
||||||
|
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
|
||||||
|
ATTR_ICON: None,
|
||||||
|
ATTR_LABEL: "Dew Point",
|
||||||
|
ATTR_UNIT: TEMP_CELSIUS,
|
||||||
|
ATTR_UNIT_CONVERT: TEMP_CELSIUS,
|
||||||
|
},
|
||||||
|
"temperature": {
|
||||||
|
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
|
||||||
|
ATTR_ICON: None,
|
||||||
|
ATTR_LABEL: "Temperature",
|
||||||
|
ATTR_UNIT: TEMP_CELSIUS,
|
||||||
|
ATTR_UNIT_CONVERT: TEMP_CELSIUS,
|
||||||
|
},
|
||||||
|
"windChill": {
|
||||||
|
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
|
||||||
|
ATTR_ICON: None,
|
||||||
|
ATTR_LABEL: "Wind Chill",
|
||||||
|
ATTR_UNIT: TEMP_CELSIUS,
|
||||||
|
ATTR_UNIT_CONVERT: TEMP_CELSIUS,
|
||||||
|
},
|
||||||
|
"heatIndex": {
|
||||||
|
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
|
||||||
|
ATTR_ICON: None,
|
||||||
|
ATTR_LABEL: "Heat Index",
|
||||||
|
ATTR_UNIT: TEMP_CELSIUS,
|
||||||
|
ATTR_UNIT_CONVERT: TEMP_CELSIUS,
|
||||||
|
},
|
||||||
|
"relativeHumidity": {
|
||||||
|
ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY,
|
||||||
|
ATTR_ICON: None,
|
||||||
|
ATTR_LABEL: "Relative Humidity",
|
||||||
|
ATTR_UNIT: PERCENTAGE,
|
||||||
|
ATTR_UNIT_CONVERT: PERCENTAGE,
|
||||||
|
},
|
||||||
|
"windSpeed": {
|
||||||
|
ATTR_DEVICE_CLASS: None,
|
||||||
|
ATTR_ICON: "mdi:weather-windy",
|
||||||
|
ATTR_LABEL: "Wind Speed",
|
||||||
|
ATTR_UNIT: SPEED_KILOMETERS_PER_HOUR,
|
||||||
|
ATTR_UNIT_CONVERT: SPEED_MILES_PER_HOUR,
|
||||||
|
},
|
||||||
|
"windGust": {
|
||||||
|
ATTR_DEVICE_CLASS: None,
|
||||||
|
ATTR_ICON: "mdi:weather-windy",
|
||||||
|
ATTR_LABEL: "Wind Gust",
|
||||||
|
ATTR_UNIT: SPEED_KILOMETERS_PER_HOUR,
|
||||||
|
ATTR_UNIT_CONVERT: SPEED_MILES_PER_HOUR,
|
||||||
|
},
|
||||||
|
"windDirection": {
|
||||||
|
ATTR_DEVICE_CLASS: None,
|
||||||
|
ATTR_ICON: "mdi:compass-rose",
|
||||||
|
ATTR_LABEL: "Wind Direction",
|
||||||
|
ATTR_UNIT: DEGREE,
|
||||||
|
ATTR_UNIT_CONVERT: DEGREE,
|
||||||
|
},
|
||||||
|
"barometricPressure": {
|
||||||
|
ATTR_DEVICE_CLASS: DEVICE_CLASS_PRESSURE,
|
||||||
|
ATTR_ICON: None,
|
||||||
|
ATTR_LABEL: "Barometric Pressure",
|
||||||
|
ATTR_UNIT: PRESSURE_PA,
|
||||||
|
ATTR_UNIT_CONVERT: PRESSURE_INHG,
|
||||||
|
},
|
||||||
|
"seaLevelPressure": {
|
||||||
|
ATTR_DEVICE_CLASS: DEVICE_CLASS_PRESSURE,
|
||||||
|
ATTR_ICON: None,
|
||||||
|
ATTR_LABEL: "Sea Level Pressure",
|
||||||
|
ATTR_UNIT: PRESSURE_PA,
|
||||||
|
ATTR_UNIT_CONVERT: PRESSURE_INHG,
|
||||||
|
},
|
||||||
|
"visibility": {
|
||||||
|
ATTR_DEVICE_CLASS: None,
|
||||||
|
ATTR_ICON: "mdi:eye",
|
||||||
|
ATTR_LABEL: "Visibility",
|
||||||
|
ATTR_UNIT: LENGTH_METERS,
|
||||||
|
ATTR_UNIT_CONVERT: LENGTH_MILES,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
156
homeassistant/components/nws/sensor.py
Normal file
156
homeassistant/components/nws/sensor.py
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
"""Sensors for National Weather Service (NWS)."""
|
||||||
|
from homeassistant.components.sensor import SensorEntity
|
||||||
|
from homeassistant.const import (
|
||||||
|
ATTR_ATTRIBUTION,
|
||||||
|
ATTR_DEVICE_CLASS,
|
||||||
|
CONF_LATITUDE,
|
||||||
|
CONF_LONGITUDE,
|
||||||
|
LENGTH_KILOMETERS,
|
||||||
|
LENGTH_METERS,
|
||||||
|
LENGTH_MILES,
|
||||||
|
PERCENTAGE,
|
||||||
|
PRESSURE_INHG,
|
||||||
|
PRESSURE_PA,
|
||||||
|
SPEED_MILES_PER_HOUR,
|
||||||
|
TEMP_CELSIUS,
|
||||||
|
)
|
||||||
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
from homeassistant.util.distance import convert as convert_distance
|
||||||
|
from homeassistant.util.dt import utcnow
|
||||||
|
from homeassistant.util.pressure import convert as convert_pressure
|
||||||
|
|
||||||
|
from . import base_unique_id
|
||||||
|
from .const import (
|
||||||
|
ATTR_ICON,
|
||||||
|
ATTR_LABEL,
|
||||||
|
ATTR_UNIT,
|
||||||
|
ATTR_UNIT_CONVERT,
|
||||||
|
ATTRIBUTION,
|
||||||
|
CONF_STATION,
|
||||||
|
COORDINATOR_OBSERVATION,
|
||||||
|
DOMAIN,
|
||||||
|
NWS_DATA,
|
||||||
|
OBSERVATION_VALID_TIME,
|
||||||
|
SENSOR_TYPES,
|
||||||
|
)
|
||||||
|
|
||||||
|
PARALLEL_UPDATES = 0
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass, entry, async_add_entities):
|
||||||
|
"""Set up the NWS weather platform."""
|
||||||
|
hass_data = hass.data[DOMAIN][entry.entry_id]
|
||||||
|
station = entry.data[CONF_STATION]
|
||||||
|
|
||||||
|
entities = []
|
||||||
|
for sensor_type, sensor_data in SENSOR_TYPES.items():
|
||||||
|
if hass.config.units.is_metric:
|
||||||
|
unit = sensor_data[ATTR_UNIT]
|
||||||
|
else:
|
||||||
|
unit = sensor_data[ATTR_UNIT_CONVERT]
|
||||||
|
entities.append(
|
||||||
|
NWSSensor(
|
||||||
|
entry.data,
|
||||||
|
hass_data,
|
||||||
|
sensor_type,
|
||||||
|
station,
|
||||||
|
sensor_data[ATTR_LABEL],
|
||||||
|
sensor_data[ATTR_ICON],
|
||||||
|
sensor_data[ATTR_DEVICE_CLASS],
|
||||||
|
unit,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
async_add_entities(entities, False)
|
||||||
|
|
||||||
|
|
||||||
|
class NWSSensor(CoordinatorEntity, SensorEntity):
|
||||||
|
"""An NWS Sensor Entity."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
entry_data,
|
||||||
|
hass_data,
|
||||||
|
sensor_type,
|
||||||
|
station,
|
||||||
|
label,
|
||||||
|
icon,
|
||||||
|
device_class,
|
||||||
|
unit,
|
||||||
|
):
|
||||||
|
"""Initialise the platform with a data instance."""
|
||||||
|
super().__init__(hass_data[COORDINATOR_OBSERVATION])
|
||||||
|
self._nws = hass_data[NWS_DATA]
|
||||||
|
self._latitude = entry_data[CONF_LATITUDE]
|
||||||
|
self._longitude = entry_data[CONF_LONGITUDE]
|
||||||
|
self._type = sensor_type
|
||||||
|
self._station = station
|
||||||
|
self._label = label
|
||||||
|
self._icon = icon
|
||||||
|
self._device_class = device_class
|
||||||
|
self._unit = unit
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self):
|
||||||
|
"""Return the state."""
|
||||||
|
value = self._nws.observation.get(self._type)
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
if self._unit == SPEED_MILES_PER_HOUR:
|
||||||
|
return round(convert_distance(value, LENGTH_KILOMETERS, LENGTH_MILES))
|
||||||
|
if self._unit == LENGTH_MILES:
|
||||||
|
return round(convert_distance(value, LENGTH_METERS, LENGTH_MILES))
|
||||||
|
if self._unit == PRESSURE_INHG:
|
||||||
|
return round(convert_pressure(value, PRESSURE_PA, PRESSURE_INHG), 2)
|
||||||
|
if self._unit == TEMP_CELSIUS:
|
||||||
|
return round(value, 1)
|
||||||
|
if self._unit == PERCENTAGE:
|
||||||
|
return round(value)
|
||||||
|
return value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def icon(self):
|
||||||
|
"""Return the icon."""
|
||||||
|
return self._icon
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_class(self):
|
||||||
|
"""Return the device class."""
|
||||||
|
return self._device_class
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unit_of_measurement(self):
|
||||||
|
"""Return the unit the value is expressed in."""
|
||||||
|
return self._unit
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_state_attributes(self):
|
||||||
|
"""Return the attribution."""
|
||||||
|
return {ATTR_ATTRIBUTION: ATTRIBUTION}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
"""Return the name of the station."""
|
||||||
|
return f"{self._station} {self._label}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unique_id(self):
|
||||||
|
"""Return a unique_id for this entity."""
|
||||||
|
return f"{base_unique_id(self._latitude, self._longitude)}_{self._type}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available(self):
|
||||||
|
"""Return if state is available."""
|
||||||
|
if self.coordinator.last_update_success_time:
|
||||||
|
last_success_time = (
|
||||||
|
utcnow() - self.coordinator.last_update_success_time
|
||||||
|
< OBSERVATION_VALID_TIME
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
last_success_time = False
|
||||||
|
return self.coordinator.last_update_success or last_success_time
|
||||||
|
|
||||||
|
@property
|
||||||
|
def entity_registry_enabled_default(self) -> bool:
|
||||||
|
"""Return if the entity should be enabled when first added to the entity registry."""
|
||||||
|
return False
|
@ -1,6 +1,4 @@
|
|||||||
"""Support for NWS weather service."""
|
"""Support for NWS weather service."""
|
||||||
from datetime import timedelta
|
|
||||||
|
|
||||||
from homeassistant.components.weather import (
|
from homeassistant.components.weather import (
|
||||||
ATTR_CONDITION_CLEAR_NIGHT,
|
ATTR_CONDITION_CLEAR_NIGHT,
|
||||||
ATTR_CONDITION_SUNNY,
|
ATTR_CONDITION_SUNNY,
|
||||||
@ -42,15 +40,14 @@ from .const import (
|
|||||||
COORDINATOR_OBSERVATION,
|
COORDINATOR_OBSERVATION,
|
||||||
DAYNIGHT,
|
DAYNIGHT,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
|
FORECAST_VALID_TIME,
|
||||||
HOURLY,
|
HOURLY,
|
||||||
NWS_DATA,
|
NWS_DATA,
|
||||||
|
OBSERVATION_VALID_TIME,
|
||||||
)
|
)
|
||||||
|
|
||||||
PARALLEL_UPDATES = 0
|
PARALLEL_UPDATES = 0
|
||||||
|
|
||||||
OBSERVATION_VALID_TIME = timedelta(minutes=20)
|
|
||||||
FORECAST_VALID_TIME = timedelta(minutes=45)
|
|
||||||
|
|
||||||
|
|
||||||
def convert_condition(time, weather):
|
def convert_condition(time, weather):
|
||||||
"""
|
"""
|
||||||
|
@ -32,3 +32,21 @@ def mock_simple_nws_config():
|
|||||||
instance.station = "ABC"
|
instance.station = "ABC"
|
||||||
instance.stations = ["ABC"]
|
instance.stations = ["ABC"]
|
||||||
yield mock_nws
|
yield mock_nws
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def no_sensor():
|
||||||
|
"""Remove sensors."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.nws.sensor.async_setup_entry", return_value=True
|
||||||
|
) as mock_setup_entry:
|
||||||
|
yield mock_setup_entry
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def no_weather():
|
||||||
|
"""Remove weather."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.nws.weather.async_setup_entry", return_value=True
|
||||||
|
) as mock_setup_entry:
|
||||||
|
yield mock_setup_entry
|
||||||
|
@ -44,6 +44,7 @@ DEFAULT_STATIONS = ["ABC", "XYZ"]
|
|||||||
DEFAULT_OBSERVATION = {
|
DEFAULT_OBSERVATION = {
|
||||||
"temperature": 10,
|
"temperature": 10,
|
||||||
"seaLevelPressure": 100000,
|
"seaLevelPressure": 100000,
|
||||||
|
"barometricPressure": 100000,
|
||||||
"relativeHumidity": 10,
|
"relativeHumidity": 10,
|
||||||
"windSpeed": 10,
|
"windSpeed": 10,
|
||||||
"windDirection": 180,
|
"windDirection": 180,
|
||||||
@ -53,9 +54,45 @@ DEFAULT_OBSERVATION = {
|
|||||||
"timestamp": "2019-08-12T23:53:00+00:00",
|
"timestamp": "2019-08-12T23:53:00+00:00",
|
||||||
"iconTime": "day",
|
"iconTime": "day",
|
||||||
"iconWeather": (("Fair/clear", None),),
|
"iconWeather": (("Fair/clear", None),),
|
||||||
|
"dewpoint": 5,
|
||||||
|
"windChill": 5,
|
||||||
|
"heatIndex": 15,
|
||||||
|
"windGust": 20,
|
||||||
}
|
}
|
||||||
|
|
||||||
EXPECTED_OBSERVATION_IMPERIAL = {
|
SENSOR_EXPECTED_OBSERVATION_METRIC = {
|
||||||
|
"dewpoint": "5",
|
||||||
|
"temperature": "10",
|
||||||
|
"windChill": "5",
|
||||||
|
"heatIndex": "15",
|
||||||
|
"relativeHumidity": "10",
|
||||||
|
"windSpeed": "10",
|
||||||
|
"windGust": "20",
|
||||||
|
"windDirection": "180",
|
||||||
|
"barometricPressure": "100000",
|
||||||
|
"seaLevelPressure": "100000",
|
||||||
|
"visibility": "10000",
|
||||||
|
}
|
||||||
|
|
||||||
|
SENSOR_EXPECTED_OBSERVATION_IMPERIAL = {
|
||||||
|
"dewpoint": str(round(convert_temperature(5, TEMP_CELSIUS, TEMP_FAHRENHEIT))),
|
||||||
|
"temperature": str(round(convert_temperature(10, TEMP_CELSIUS, TEMP_FAHRENHEIT))),
|
||||||
|
"windChill": str(round(convert_temperature(5, TEMP_CELSIUS, TEMP_FAHRENHEIT))),
|
||||||
|
"heatIndex": str(round(convert_temperature(15, TEMP_CELSIUS, TEMP_FAHRENHEIT))),
|
||||||
|
"relativeHumidity": "10",
|
||||||
|
"windSpeed": str(round(convert_distance(10, LENGTH_KILOMETERS, LENGTH_MILES))),
|
||||||
|
"windGust": str(round(convert_distance(20, LENGTH_KILOMETERS, LENGTH_MILES))),
|
||||||
|
"windDirection": "180",
|
||||||
|
"barometricPressure": str(
|
||||||
|
round(convert_pressure(100000, PRESSURE_PA, PRESSURE_INHG), 2)
|
||||||
|
),
|
||||||
|
"seaLevelPressure": str(
|
||||||
|
round(convert_pressure(100000, PRESSURE_PA, PRESSURE_INHG), 2)
|
||||||
|
),
|
||||||
|
"visibility": str(round(convert_distance(10000, LENGTH_METERS, LENGTH_MILES))),
|
||||||
|
}
|
||||||
|
|
||||||
|
WEATHER_EXPECTED_OBSERVATION_IMPERIAL = {
|
||||||
ATTR_WEATHER_TEMPERATURE: round(
|
ATTR_WEATHER_TEMPERATURE: round(
|
||||||
convert_temperature(10, TEMP_CELSIUS, TEMP_FAHRENHEIT)
|
convert_temperature(10, TEMP_CELSIUS, TEMP_FAHRENHEIT)
|
||||||
),
|
),
|
||||||
@ -72,7 +109,7 @@ EXPECTED_OBSERVATION_IMPERIAL = {
|
|||||||
ATTR_WEATHER_HUMIDITY: 10,
|
ATTR_WEATHER_HUMIDITY: 10,
|
||||||
}
|
}
|
||||||
|
|
||||||
EXPECTED_OBSERVATION_METRIC = {
|
WEATHER_EXPECTED_OBSERVATION_METRIC = {
|
||||||
ATTR_WEATHER_TEMPERATURE: 10,
|
ATTR_WEATHER_TEMPERATURE: 10,
|
||||||
ATTR_WEATHER_WIND_BEARING: 180,
|
ATTR_WEATHER_WIND_BEARING: 180,
|
||||||
ATTR_WEATHER_WIND_SPEED: 10,
|
ATTR_WEATHER_WIND_SPEED: 10,
|
||||||
|
95
tests/components/nws/test_sensor.py
Normal file
95
tests/components/nws/test_sensor.py
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
"""Sensors for National Weather Service (NWS)."""
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.components.nws.const import (
|
||||||
|
ATTR_LABEL,
|
||||||
|
ATTRIBUTION,
|
||||||
|
DOMAIN,
|
||||||
|
SENSOR_TYPES,
|
||||||
|
)
|
||||||
|
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||||
|
from homeassistant.const import ATTR_ATTRIBUTION, STATE_UNKNOWN
|
||||||
|
from homeassistant.util import slugify
|
||||||
|
from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
from tests.components.nws.const import (
|
||||||
|
EXPECTED_FORECAST_IMPERIAL,
|
||||||
|
EXPECTED_FORECAST_METRIC,
|
||||||
|
NONE_OBSERVATION,
|
||||||
|
NWS_CONFIG,
|
||||||
|
SENSOR_EXPECTED_OBSERVATION_IMPERIAL,
|
||||||
|
SENSOR_EXPECTED_OBSERVATION_METRIC,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"units,result_observation,result_forecast",
|
||||||
|
[
|
||||||
|
(
|
||||||
|
IMPERIAL_SYSTEM,
|
||||||
|
SENSOR_EXPECTED_OBSERVATION_IMPERIAL,
|
||||||
|
EXPECTED_FORECAST_IMPERIAL,
|
||||||
|
),
|
||||||
|
(METRIC_SYSTEM, SENSOR_EXPECTED_OBSERVATION_METRIC, EXPECTED_FORECAST_METRIC),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_imperial_metric(
|
||||||
|
hass, units, result_observation, result_forecast, mock_simple_nws, no_weather
|
||||||
|
):
|
||||||
|
"""Test with imperial and metric units."""
|
||||||
|
registry = await hass.helpers.entity_registry.async_get_registry()
|
||||||
|
|
||||||
|
for sensor_name, sensor_data in SENSOR_TYPES.items():
|
||||||
|
registry.async_get_or_create(
|
||||||
|
SENSOR_DOMAIN,
|
||||||
|
DOMAIN,
|
||||||
|
f"35_-75_{sensor_name}",
|
||||||
|
suggested_object_id=f"abc_{sensor_data[ATTR_LABEL]}",
|
||||||
|
disabled_by=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
hass.config.units = units
|
||||||
|
entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
data=NWS_CONFIG,
|
||||||
|
)
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
await hass.config_entries.async_setup(entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
for sensor_name, sensor_data in SENSOR_TYPES.items():
|
||||||
|
state = hass.states.get(f"sensor.abc_{slugify(sensor_data[ATTR_LABEL])}")
|
||||||
|
assert state
|
||||||
|
assert state.state == result_observation[sensor_name]
|
||||||
|
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
|
||||||
|
|
||||||
|
|
||||||
|
async def test_none_values(hass, mock_simple_nws, no_weather):
|
||||||
|
"""Test with no values."""
|
||||||
|
instance = mock_simple_nws.return_value
|
||||||
|
instance.observation = NONE_OBSERVATION
|
||||||
|
|
||||||
|
registry = await hass.helpers.entity_registry.async_get_registry()
|
||||||
|
|
||||||
|
for sensor_name, sensor_data in SENSOR_TYPES.items():
|
||||||
|
registry.async_get_or_create(
|
||||||
|
SENSOR_DOMAIN,
|
||||||
|
DOMAIN,
|
||||||
|
f"35_-75_{sensor_name}",
|
||||||
|
suggested_object_id=f"abc_{sensor_data[ATTR_LABEL]}",
|
||||||
|
disabled_by=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
data=NWS_CONFIG,
|
||||||
|
)
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
await hass.config_entries.async_setup(entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
for sensor_name, sensor_data in SENSOR_TYPES.items():
|
||||||
|
state = hass.states.get(f"sensor.abc_{slugify(sensor_data[ATTR_LABEL])}")
|
||||||
|
assert state
|
||||||
|
assert state.state == STATE_UNKNOWN
|
@ -21,23 +21,27 @@ from tests.common import MockConfigEntry, async_fire_time_changed
|
|||||||
from tests.components.nws.const import (
|
from tests.components.nws.const import (
|
||||||
EXPECTED_FORECAST_IMPERIAL,
|
EXPECTED_FORECAST_IMPERIAL,
|
||||||
EXPECTED_FORECAST_METRIC,
|
EXPECTED_FORECAST_METRIC,
|
||||||
EXPECTED_OBSERVATION_IMPERIAL,
|
|
||||||
EXPECTED_OBSERVATION_METRIC,
|
|
||||||
NONE_FORECAST,
|
NONE_FORECAST,
|
||||||
NONE_OBSERVATION,
|
NONE_OBSERVATION,
|
||||||
NWS_CONFIG,
|
NWS_CONFIG,
|
||||||
|
WEATHER_EXPECTED_OBSERVATION_IMPERIAL,
|
||||||
|
WEATHER_EXPECTED_OBSERVATION_METRIC,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"units,result_observation,result_forecast",
|
"units,result_observation,result_forecast",
|
||||||
[
|
[
|
||||||
(IMPERIAL_SYSTEM, EXPECTED_OBSERVATION_IMPERIAL, EXPECTED_FORECAST_IMPERIAL),
|
(
|
||||||
(METRIC_SYSTEM, EXPECTED_OBSERVATION_METRIC, EXPECTED_FORECAST_METRIC),
|
IMPERIAL_SYSTEM,
|
||||||
|
WEATHER_EXPECTED_OBSERVATION_IMPERIAL,
|
||||||
|
EXPECTED_FORECAST_IMPERIAL,
|
||||||
|
),
|
||||||
|
(METRIC_SYSTEM, WEATHER_EXPECTED_OBSERVATION_METRIC, EXPECTED_FORECAST_METRIC),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def test_imperial_metric(
|
async def test_imperial_metric(
|
||||||
hass, units, result_observation, result_forecast, mock_simple_nws
|
hass, units, result_observation, result_forecast, mock_simple_nws, no_sensor
|
||||||
):
|
):
|
||||||
"""Test with imperial and metric units."""
|
"""Test with imperial and metric units."""
|
||||||
# enable the hourly entity
|
# enable the hourly entity
|
||||||
@ -86,7 +90,7 @@ async def test_imperial_metric(
|
|||||||
assert forecast[0].get(key) == value
|
assert forecast[0].get(key) == value
|
||||||
|
|
||||||
|
|
||||||
async def test_none_values(hass, mock_simple_nws):
|
async def test_none_values(hass, mock_simple_nws, no_sensor):
|
||||||
"""Test with none values in observation and forecast dicts."""
|
"""Test with none values in observation and forecast dicts."""
|
||||||
instance = mock_simple_nws.return_value
|
instance = mock_simple_nws.return_value
|
||||||
instance.observation = NONE_OBSERVATION
|
instance.observation = NONE_OBSERVATION
|
||||||
@ -103,7 +107,7 @@ async def test_none_values(hass, mock_simple_nws):
|
|||||||
state = hass.states.get("weather.abc_daynight")
|
state = hass.states.get("weather.abc_daynight")
|
||||||
assert state.state == STATE_UNKNOWN
|
assert state.state == STATE_UNKNOWN
|
||||||
data = state.attributes
|
data = state.attributes
|
||||||
for key in EXPECTED_OBSERVATION_IMPERIAL:
|
for key in WEATHER_EXPECTED_OBSERVATION_IMPERIAL:
|
||||||
assert data.get(key) is None
|
assert data.get(key) is None
|
||||||
|
|
||||||
forecast = data.get(ATTR_FORECAST)
|
forecast = data.get(ATTR_FORECAST)
|
||||||
@ -111,7 +115,7 @@ async def test_none_values(hass, mock_simple_nws):
|
|||||||
assert forecast[0].get(key) is None
|
assert forecast[0].get(key) is None
|
||||||
|
|
||||||
|
|
||||||
async def test_none(hass, mock_simple_nws):
|
async def test_none(hass, mock_simple_nws, no_sensor):
|
||||||
"""Test with None as observation and forecast."""
|
"""Test with None as observation and forecast."""
|
||||||
instance = mock_simple_nws.return_value
|
instance = mock_simple_nws.return_value
|
||||||
instance.observation = None
|
instance.observation = None
|
||||||
@ -130,14 +134,14 @@ async def test_none(hass, mock_simple_nws):
|
|||||||
assert state.state == STATE_UNKNOWN
|
assert state.state == STATE_UNKNOWN
|
||||||
|
|
||||||
data = state.attributes
|
data = state.attributes
|
||||||
for key in EXPECTED_OBSERVATION_IMPERIAL:
|
for key in WEATHER_EXPECTED_OBSERVATION_IMPERIAL:
|
||||||
assert data.get(key) is None
|
assert data.get(key) is None
|
||||||
|
|
||||||
forecast = data.get(ATTR_FORECAST)
|
forecast = data.get(ATTR_FORECAST)
|
||||||
assert forecast is None
|
assert forecast is None
|
||||||
|
|
||||||
|
|
||||||
async def test_error_station(hass, mock_simple_nws):
|
async def test_error_station(hass, mock_simple_nws, no_sensor):
|
||||||
"""Test error in setting station."""
|
"""Test error in setting station."""
|
||||||
|
|
||||||
instance = mock_simple_nws.return_value
|
instance = mock_simple_nws.return_value
|
||||||
@ -155,7 +159,7 @@ async def test_error_station(hass, mock_simple_nws):
|
|||||||
assert hass.states.get("weather.abc_daynight") is None
|
assert hass.states.get("weather.abc_daynight") is None
|
||||||
|
|
||||||
|
|
||||||
async def test_entity_refresh(hass, mock_simple_nws):
|
async def test_entity_refresh(hass, mock_simple_nws, no_sensor):
|
||||||
"""Test manual refresh."""
|
"""Test manual refresh."""
|
||||||
instance = mock_simple_nws.return_value
|
instance = mock_simple_nws.return_value
|
||||||
|
|
||||||
@ -184,7 +188,7 @@ async def test_entity_refresh(hass, mock_simple_nws):
|
|||||||
instance.update_forecast_hourly.assert_called_once()
|
instance.update_forecast_hourly.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
async def test_error_observation(hass, mock_simple_nws):
|
async def test_error_observation(hass, mock_simple_nws, no_sensor):
|
||||||
"""Test error during update observation."""
|
"""Test error during update observation."""
|
||||||
utc_time = dt_util.utcnow()
|
utc_time = dt_util.utcnow()
|
||||||
with patch("homeassistant.components.nws.utcnow") as mock_utc, patch(
|
with patch("homeassistant.components.nws.utcnow") as mock_utc, patch(
|
||||||
@ -248,7 +252,7 @@ async def test_error_observation(hass, mock_simple_nws):
|
|||||||
assert state.state == STATE_UNAVAILABLE
|
assert state.state == STATE_UNAVAILABLE
|
||||||
|
|
||||||
|
|
||||||
async def test_error_forecast(hass, mock_simple_nws):
|
async def test_error_forecast(hass, mock_simple_nws, no_sensor):
|
||||||
"""Test error during update forecast."""
|
"""Test error during update forecast."""
|
||||||
instance = mock_simple_nws.return_value
|
instance = mock_simple_nws.return_value
|
||||||
instance.update_forecast.side_effect = aiohttp.ClientError
|
instance.update_forecast.side_effect = aiohttp.ClientError
|
||||||
@ -279,7 +283,7 @@ async def test_error_forecast(hass, mock_simple_nws):
|
|||||||
assert state.state == ATTR_CONDITION_SUNNY
|
assert state.state == ATTR_CONDITION_SUNNY
|
||||||
|
|
||||||
|
|
||||||
async def test_error_forecast_hourly(hass, mock_simple_nws):
|
async def test_error_forecast_hourly(hass, mock_simple_nws, no_sensor):
|
||||||
"""Test error during update forecast hourly."""
|
"""Test error during update forecast hourly."""
|
||||||
instance = mock_simple_nws.return_value
|
instance = mock_simple_nws.return_value
|
||||||
instance.update_forecast_hourly.side_effect = aiohttp.ClientError
|
instance.update_forecast_hourly.side_effect = aiohttp.ClientError
|
||||||
@ -320,7 +324,7 @@ async def test_error_forecast_hourly(hass, mock_simple_nws):
|
|||||||
assert state.state == ATTR_CONDITION_SUNNY
|
assert state.state == ATTR_CONDITION_SUNNY
|
||||||
|
|
||||||
|
|
||||||
async def test_forecast_hourly_disable_enable(hass, mock_simple_nws):
|
async def test_forecast_hourly_disable_enable(hass, mock_simple_nws, no_sensor):
|
||||||
"""Test error during update forecast hourly."""
|
"""Test error during update forecast hourly."""
|
||||||
entry = MockConfigEntry(
|
entry = MockConfigEntry(
|
||||||
domain=nws.DOMAIN,
|
domain=nws.DOMAIN,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user