Use NamedTuple - nws (#53293)

This commit is contained in:
Marc Mueller 2021-07-22 07:24:07 +02:00 committed by GitHub
parent e78a62c802
commit d98e580c3c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 122 additions and 146 deletions

View File

@ -1,5 +1,8 @@
"""Constants for National Weather Service Integration.""" """Constants for National Weather Service Integration."""
from __future__ import annotations
from datetime import timedelta from datetime import timedelta
from typing import NamedTuple
from homeassistant.components.weather import ( from homeassistant.components.weather import (
ATTR_CONDITION_CLOUDY, ATTR_CONDITION_CLOUDY,
@ -17,7 +20,6 @@ from homeassistant.components.weather import (
ATTR_CONDITION_WINDY_VARIANT, ATTR_CONDITION_WINDY_VARIANT,
) )
from homeassistant.const import ( from homeassistant.const import (
ATTR_DEVICE_CLASS,
DEGREE, DEGREE,
DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_PRESSURE, DEVICE_CLASS_PRESSURE,
@ -40,11 +42,6 @@ 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: [
@ -101,82 +98,93 @@ COORDINATOR_FORECAST_HOURLY = "coordinator_forecast_hourly"
OBSERVATION_VALID_TIME = timedelta(minutes=20) OBSERVATION_VALID_TIME = timedelta(minutes=20)
FORECAST_VALID_TIME = timedelta(minutes=45) FORECAST_VALID_TIME = timedelta(minutes=45)
SENSOR_TYPES = {
"dewpoint": { class NWSSensorMetadata(NamedTuple):
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, """Sensor metadata for an individual NWS sensor."""
ATTR_ICON: None,
ATTR_LABEL: "Dew Point", label: str
ATTR_UNIT: TEMP_CELSIUS, icon: str | None
ATTR_UNIT_CONVERT: TEMP_CELSIUS, device_class: str | None
}, unit: str
"temperature": { unit_convert: str
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
ATTR_ICON: None,
ATTR_LABEL: "Temperature", SENSOR_TYPES: dict[str, NWSSensorMetadata] = {
ATTR_UNIT: TEMP_CELSIUS, "dewpoint": NWSSensorMetadata(
ATTR_UNIT_CONVERT: TEMP_CELSIUS, "Dew Point",
}, icon=None,
"windChill": { device_class=DEVICE_CLASS_TEMPERATURE,
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, unit=TEMP_CELSIUS,
ATTR_ICON: None, unit_convert=TEMP_CELSIUS,
ATTR_LABEL: "Wind Chill", ),
ATTR_UNIT: TEMP_CELSIUS, "temperature": NWSSensorMetadata(
ATTR_UNIT_CONVERT: TEMP_CELSIUS, "Temperature",
}, icon=None,
"heatIndex": { device_class=DEVICE_CLASS_TEMPERATURE,
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, unit=TEMP_CELSIUS,
ATTR_ICON: None, unit_convert=TEMP_CELSIUS,
ATTR_LABEL: "Heat Index", ),
ATTR_UNIT: TEMP_CELSIUS, "windChill": NWSSensorMetadata(
ATTR_UNIT_CONVERT: TEMP_CELSIUS, "Wind Chill",
}, icon=None,
"relativeHumidity": { device_class=DEVICE_CLASS_TEMPERATURE,
ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, unit=TEMP_CELSIUS,
ATTR_ICON: None, unit_convert=TEMP_CELSIUS,
ATTR_LABEL: "Relative Humidity", ),
ATTR_UNIT: PERCENTAGE, "heatIndex": NWSSensorMetadata(
ATTR_UNIT_CONVERT: PERCENTAGE, "Heat Index",
}, icon=None,
"windSpeed": { device_class=DEVICE_CLASS_TEMPERATURE,
ATTR_DEVICE_CLASS: None, unit=TEMP_CELSIUS,
ATTR_ICON: "mdi:weather-windy", unit_convert=TEMP_CELSIUS,
ATTR_LABEL: "Wind Speed", ),
ATTR_UNIT: SPEED_KILOMETERS_PER_HOUR, "relativeHumidity": NWSSensorMetadata(
ATTR_UNIT_CONVERT: SPEED_MILES_PER_HOUR, "Relative Humidity",
}, icon=None,
"windGust": { device_class=DEVICE_CLASS_HUMIDITY,
ATTR_DEVICE_CLASS: None, unit=PERCENTAGE,
ATTR_ICON: "mdi:weather-windy", unit_convert=PERCENTAGE,
ATTR_LABEL: "Wind Gust", ),
ATTR_UNIT: SPEED_KILOMETERS_PER_HOUR, "windSpeed": NWSSensorMetadata(
ATTR_UNIT_CONVERT: SPEED_MILES_PER_HOUR, "Wind Speed",
}, icon="mdi:weather-windy",
"windDirection": { device_class=None,
ATTR_DEVICE_CLASS: None, unit=SPEED_KILOMETERS_PER_HOUR,
ATTR_ICON: "mdi:compass-rose", unit_convert=SPEED_MILES_PER_HOUR,
ATTR_LABEL: "Wind Direction", ),
ATTR_UNIT: DEGREE, "windGust": NWSSensorMetadata(
ATTR_UNIT_CONVERT: DEGREE, "Wind Gust",
}, icon="mdi:weather-windy",
"barometricPressure": { device_class=None,
ATTR_DEVICE_CLASS: DEVICE_CLASS_PRESSURE, unit=SPEED_KILOMETERS_PER_HOUR,
ATTR_ICON: None, unit_convert=SPEED_MILES_PER_HOUR,
ATTR_LABEL: "Barometric Pressure", ),
ATTR_UNIT: PRESSURE_PA, "windDirection": NWSSensorMetadata(
ATTR_UNIT_CONVERT: PRESSURE_INHG, "Wind Direction",
}, icon="mdi:compass-rose",
"seaLevelPressure": { device_class=None,
ATTR_DEVICE_CLASS: DEVICE_CLASS_PRESSURE, unit=DEGREE,
ATTR_ICON: None, unit_convert=DEGREE,
ATTR_LABEL: "Sea Level Pressure", ),
ATTR_UNIT: PRESSURE_PA, "barometricPressure": NWSSensorMetadata(
ATTR_UNIT_CONVERT: PRESSURE_INHG, "Barometric Pressure",
}, icon=None,
"visibility": { device_class=DEVICE_CLASS_PRESSURE,
ATTR_DEVICE_CLASS: None, unit=PRESSURE_PA,
ATTR_ICON: "mdi:eye", unit_convert=PRESSURE_INHG,
ATTR_LABEL: "Visibility", ),
ATTR_UNIT: LENGTH_METERS, "seaLevelPressure": NWSSensorMetadata(
ATTR_UNIT_CONVERT: LENGTH_MILES, "Sea Level Pressure",
}, icon=None,
device_class=DEVICE_CLASS_PRESSURE,
unit=PRESSURE_PA,
unit_convert=PRESSURE_INHG,
),
"visibility": NWSSensorMetadata(
"Visibility",
icon="mdi:eye",
device_class=None,
unit=LENGTH_METERS,
unit_convert=LENGTH_MILES,
),
} }

View File

@ -2,7 +2,6 @@
from homeassistant.components.sensor import SensorEntity from homeassistant.components.sensor import SensorEntity
from homeassistant.const import ( from homeassistant.const import (
ATTR_ATTRIBUTION, ATTR_ATTRIBUTION,
ATTR_DEVICE_CLASS,
CONF_LATITUDE, CONF_LATITUDE,
CONF_LONGITUDE, CONF_LONGITUDE,
LENGTH_KILOMETERS, LENGTH_KILOMETERS,
@ -14,6 +13,7 @@ from homeassistant.const import (
SPEED_MILES_PER_HOUR, SPEED_MILES_PER_HOUR,
TEMP_CELSIUS, TEMP_CELSIUS,
) )
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util.distance import convert as convert_distance from homeassistant.util.distance import convert as convert_distance
from homeassistant.util.dt import utcnow from homeassistant.util.dt import utcnow
@ -21,10 +21,6 @@ from homeassistant.util.pressure import convert as convert_pressure
from . import base_unique_id from . import base_unique_id
from .const import ( from .const import (
ATTR_ICON,
ATTR_LABEL,
ATTR_UNIT,
ATTR_UNIT_CONVERT,
ATTRIBUTION, ATTRIBUTION,
CONF_STATION, CONF_STATION,
COORDINATOR_OBSERVATION, COORDINATOR_OBSERVATION,
@ -32,6 +28,7 @@ from .const import (
NWS_DATA, NWS_DATA,
OBSERVATION_VALID_TIME, OBSERVATION_VALID_TIME,
SENSOR_TYPES, SENSOR_TYPES,
NWSSensorMetadata,
) )
PARALLEL_UPDATES = 0 PARALLEL_UPDATES = 0
@ -43,21 +40,15 @@ async def async_setup_entry(hass, entry, async_add_entities):
station = entry.data[CONF_STATION] station = entry.data[CONF_STATION]
entities = [] entities = []
for sensor_type, sensor_data in SENSOR_TYPES.items(): for sensor_type, metadata in SENSOR_TYPES.items():
if hass.config.units.is_metric:
unit = sensor_data[ATTR_UNIT]
else:
unit = sensor_data[ATTR_UNIT_CONVERT]
entities.append( entities.append(
NWSSensor( NWSSensor(
hass,
entry.data, entry.data,
hass_data, hass_data,
sensor_type, sensor_type,
metadata,
station, station,
sensor_data[ATTR_LABEL],
sensor_data[ATTR_ICON],
sensor_data[ATTR_DEVICE_CLASS],
unit,
), ),
) )
@ -69,14 +60,12 @@ class NWSSensor(CoordinatorEntity, SensorEntity):
def __init__( def __init__(
self, self,
hass: HomeAssistant,
entry_data, entry_data,
hass_data, hass_data,
sensor_type, sensor_type,
metadata: NWSSensorMetadata,
station, station,
label,
icon,
device_class,
unit,
): ):
"""Initialise the platform with a data instance.""" """Initialise the platform with a data instance."""
super().__init__(hass_data[COORDINATOR_OBSERVATION]) super().__init__(hass_data[COORDINATOR_OBSERVATION])
@ -84,11 +73,15 @@ class NWSSensor(CoordinatorEntity, SensorEntity):
self._latitude = entry_data[CONF_LATITUDE] self._latitude = entry_data[CONF_LATITUDE]
self._longitude = entry_data[CONF_LONGITUDE] self._longitude = entry_data[CONF_LONGITUDE]
self._type = sensor_type self._type = sensor_type
self._station = station self._metadata = metadata
self._label = label
self._icon = icon self._attr_name = f"{station} {metadata.label}"
self._device_class = device_class self._attr_icon = metadata.icon
self._unit = unit self._attr_device_class = metadata.device_class
if hass.config.units.is_metric:
self._attr_unit_of_measurement = metadata.unit
else:
self._attr_unit_of_measurement = metadata.unit_convert
@property @property
def state(self): def state(self):
@ -96,43 +89,23 @@ class NWSSensor(CoordinatorEntity, SensorEntity):
value = self._nws.observation.get(self._type) value = self._nws.observation.get(self._type)
if value is None: if value is None:
return None return None
if self._unit == SPEED_MILES_PER_HOUR: if self._attr_unit_of_measurement == SPEED_MILES_PER_HOUR:
return round(convert_distance(value, LENGTH_KILOMETERS, LENGTH_MILES)) return round(convert_distance(value, LENGTH_KILOMETERS, LENGTH_MILES))
if self._unit == LENGTH_MILES: if self._attr_unit_of_measurement == LENGTH_MILES:
return round(convert_distance(value, LENGTH_METERS, LENGTH_MILES)) return round(convert_distance(value, LENGTH_METERS, LENGTH_MILES))
if self._unit == PRESSURE_INHG: if self._attr_unit_of_measurement == PRESSURE_INHG:
return round(convert_pressure(value, PRESSURE_PA, PRESSURE_INHG), 2) return round(convert_pressure(value, PRESSURE_PA, PRESSURE_INHG), 2)
if self._unit == TEMP_CELSIUS: if self._attr_unit_of_measurement == TEMP_CELSIUS:
return round(value, 1) return round(value, 1)
if self._unit == PERCENTAGE: if self._attr_unit_of_measurement == PERCENTAGE:
return round(value) return round(value)
return 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 @property
def device_state_attributes(self): def device_state_attributes(self):
"""Return the attribution.""" """Return the attribution."""
return {ATTR_ATTRIBUTION: ATTRIBUTION} return {ATTR_ATTRIBUTION: ATTRIBUTION}
@property
def name(self):
"""Return the name of the station."""
return f"{self._station} {self._label}"
@property @property
def unique_id(self): def unique_id(self):
"""Return a unique_id for this entity.""" """Return a unique_id for this entity."""

View File

@ -1,12 +1,7 @@
"""Sensors for National Weather Service (NWS).""" """Sensors for National Weather Service (NWS)."""
import pytest import pytest
from homeassistant.components.nws.const import ( from homeassistant.components.nws.const import ATTRIBUTION, DOMAIN, SENSOR_TYPES
ATTR_LABEL,
ATTRIBUTION,
DOMAIN,
SENSOR_TYPES,
)
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.const import ATTR_ATTRIBUTION, STATE_UNKNOWN from homeassistant.const import ATTR_ATTRIBUTION, STATE_UNKNOWN
from homeassistant.util import slugify from homeassistant.util import slugify
@ -40,12 +35,12 @@ async def test_imperial_metric(
"""Test with imperial and metric units.""" """Test with imperial and metric units."""
registry = await hass.helpers.entity_registry.async_get_registry() registry = await hass.helpers.entity_registry.async_get_registry()
for sensor_name, sensor_data in SENSOR_TYPES.items(): for sensor_name, metadata in SENSOR_TYPES.items():
registry.async_get_or_create( registry.async_get_or_create(
SENSOR_DOMAIN, SENSOR_DOMAIN,
DOMAIN, DOMAIN,
f"35_-75_{sensor_name}", f"35_-75_{sensor_name}",
suggested_object_id=f"abc_{sensor_data[ATTR_LABEL]}", suggested_object_id=f"abc_{metadata.label}",
disabled_by=None, disabled_by=None,
) )
@ -58,8 +53,8 @@ async def test_imperial_metric(
await hass.config_entries.async_setup(entry.entry_id) await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
for sensor_name, sensor_data in SENSOR_TYPES.items(): for sensor_name, metadata in SENSOR_TYPES.items():
state = hass.states.get(f"sensor.abc_{slugify(sensor_data[ATTR_LABEL])}") state = hass.states.get(f"sensor.abc_{slugify(metadata.label)}")
assert state assert state
assert state.state == result_observation[sensor_name] assert state.state == result_observation[sensor_name]
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
@ -72,12 +67,12 @@ async def test_none_values(hass, mock_simple_nws, no_weather):
registry = await hass.helpers.entity_registry.async_get_registry() registry = await hass.helpers.entity_registry.async_get_registry()
for sensor_name, sensor_data in SENSOR_TYPES.items(): for sensor_name, metadata in SENSOR_TYPES.items():
registry.async_get_or_create( registry.async_get_or_create(
SENSOR_DOMAIN, SENSOR_DOMAIN,
DOMAIN, DOMAIN,
f"35_-75_{sensor_name}", f"35_-75_{sensor_name}",
suggested_object_id=f"abc_{sensor_data[ATTR_LABEL]}", suggested_object_id=f"abc_{metadata.label}",
disabled_by=None, disabled_by=None,
) )
@ -89,7 +84,7 @@ async def test_none_values(hass, mock_simple_nws, no_weather):
await hass.config_entries.async_setup(entry.entry_id) await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
for sensor_name, sensor_data in SENSOR_TYPES.items(): for sensor_name, metadata in SENSOR_TYPES.items():
state = hass.states.get(f"sensor.abc_{slugify(sensor_data[ATTR_LABEL])}") state = hass.states.get(f"sensor.abc_{slugify(metadata.label)}")
assert state assert state
assert state.state == STATE_UNKNOWN assert state.state == STATE_UNKNOWN