576 lines
22 KiB
Python
576 lines
22 KiB
Python
|
"""
|
||
|
Support for WUndergroundPWS weather service.
|
||
|
|
||
|
For more details about this platform, please refer to the documentation at
|
||
|
https://home-assistant.io/components/sensor.wundergroundpws/
|
||
|
"""
|
||
|
import asyncio
|
||
|
from datetime import timedelta
|
||
|
import logging
|
||
|
import re
|
||
|
|
||
|
import aiohttp
|
||
|
import async_timeout
|
||
|
import voluptuous as vol
|
||
|
|
||
|
from homeassistant.helpers.typing import HomeAssistantType, ConfigType
|
||
|
from homeassistant.components import sensor
|
||
|
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
||
|
from homeassistant.const import (
|
||
|
CONF_MONITORED_CONDITIONS, CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE,
|
||
|
TEMP_FAHRENHEIT, TEMP_CELSIUS, LENGTH_INCHES,
|
||
|
LENGTH_FEET, ATTR_ATTRIBUTION)
|
||
|
from homeassistant.exceptions import PlatformNotReady
|
||
|
from homeassistant.helpers.entity import Entity
|
||
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||
|
from homeassistant.util import Throttle
|
||
|
import homeassistant.helpers.config_validation as cv
|
||
|
import homeassistant.config as config
|
||
|
|
||
|
_RESOURCECURRENT = 'https://api.weather.com/v2/pws/observations/current?stationId={}&format=json&units={}&apiKey={}'
|
||
|
_RESOURCEFORECAST = 'https://api.weather.com/v3/wx/forecast/daily/5day?geocode={},{}&units={}&{}&format=json&apiKey={}'
|
||
|
_LOGGER = logging.getLogger(__name__)
|
||
|
|
||
|
CONF_ATTRIBUTION = "Data provided by the WUnderground weather service"
|
||
|
CONF_PWS_ID = 'pws_id'
|
||
|
CONF_LANG = 'lang'
|
||
|
LENGTH_MILLIMETERS = 'mm'
|
||
|
LENGTH_METERS = 'm'
|
||
|
|
||
|
DEFAULT_LANG = 'en-US'
|
||
|
|
||
|
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5)
|
||
|
|
||
|
conf_file = config.get_default_config_dir() + '/configuration.yaml'
|
||
|
load_config = config.load_yaml_config_file('/config/configuration.yaml')
|
||
|
|
||
|
try:
|
||
|
UNIT_SYSTEM = load_config['homeassistant']['unit_system']
|
||
|
except KeyError as err:
|
||
|
UNIT_SYSTEM = "metric"
|
||
|
|
||
|
if UNIT_SYSTEM == 'imperial':
|
||
|
TEMPUNIT = TEMP_FAHRENHEIT
|
||
|
LENGTHUNIT = LENGTH_INCHES
|
||
|
ALTITUDEUNIT = LENGTH_FEET
|
||
|
SPEEDUNIT = 'mph'
|
||
|
PRESSUREUNIT = 'inHg'
|
||
|
else:
|
||
|
TEMPUNIT = TEMP_CELSIUS
|
||
|
LENGTHUNIT = LENGTH_MILLIMETERS
|
||
|
ALTITUDEUNIT = LENGTH_METERS
|
||
|
SPEEDUNIT = 'kph'
|
||
|
PRESSUREUNIT = 'mBar'
|
||
|
|
||
|
|
||
|
# Helper classes for declaring sensor configurations
|
||
|
|
||
|
class WUSensorConfig:
|
||
|
"""WU Sensor Configuration.
|
||
|
|
||
|
defines basic HA properties of the weather sensor and
|
||
|
stores callbacks that can parse sensor values out of
|
||
|
the json data received by WU API.
|
||
|
"""
|
||
|
|
||
|
def __init__(self, friendly_name, feature, value,
|
||
|
unit_of_measurement=None, entity_picture=None,
|
||
|
icon="mdi:gauge", device_state_attributes=None,
|
||
|
device_class=None):
|
||
|
"""Constructor.
|
||
|
|
||
|
Args:
|
||
|
friendly_name (string|func): Friendly name
|
||
|
feature (string): WU feature. See:
|
||
|
https://docs.google.com/document/d/1eKCnKXI9xnoMGRRzOL1xPCBihNV2rOet08qpE_gArAY/edit
|
||
|
value (function(WUndergroundData)): callback that
|
||
|
extracts desired value from WUndergroundData object
|
||
|
unit_of_measurement (string): unit of measurement
|
||
|
entity_picture (string): value or callback returning
|
||
|
URL of entity picture
|
||
|
icon (string): icon name
|
||
|
device_state_attributes (dict): dictionary of attributes,
|
||
|
or callable that returns it
|
||
|
"""
|
||
|
self.friendly_name = friendly_name
|
||
|
self.unit_of_measurement = unit_of_measurement
|
||
|
self.feature = feature
|
||
|
self.value = value
|
||
|
self.entity_picture = entity_picture
|
||
|
self.icon = icon
|
||
|
self.device_state_attributes = device_state_attributes or {}
|
||
|
self.device_class = device_class
|
||
|
|
||
|
|
||
|
class WUCurrentConditionsSensorConfig(WUSensorConfig):
|
||
|
"""Helper for defining sensor configurations for current conditions."""
|
||
|
|
||
|
def __init__(self, friendly_name, unit_system, field, icon="mdi:gauge",
|
||
|
unit_of_measurement=None, device_class=None):
|
||
|
"""Constructor.
|
||
|
|
||
|
Args:
|
||
|
friendly_name (string|func): Friendly name of sensor
|
||
|
field (string): Field name in the "observations[0][unit_system]"
|
||
|
dictionary.
|
||
|
icon (string): icon name , if None sensor
|
||
|
will use current weather symbol
|
||
|
unit_of_measurement (string): unit of measurement
|
||
|
"""
|
||
|
super().__init__(
|
||
|
friendly_name,
|
||
|
"conditions",
|
||
|
value=lambda wu: wu.data['observations'][0][unit_system][field],
|
||
|
icon=icon,
|
||
|
unit_of_measurement=unit_of_measurement,
|
||
|
device_state_attributes={
|
||
|
'date': lambda wu: wu.data['observations'][0][
|
||
|
'obsTimeLocal']
|
||
|
},
|
||
|
device_class=device_class
|
||
|
)
|
||
|
|
||
|
|
||
|
class WUDailyTextForecastSensorConfig(WUSensorConfig):
|
||
|
"""Helper for defining sensor configurations for daily text forecasts."""
|
||
|
|
||
|
def __init__(self, period, field=None, unit_of_measurement=None):
|
||
|
"""Constructor.
|
||
|
|
||
|
Args:
|
||
|
period (int): forecast period number
|
||
|
field (string): field name to use as value
|
||
|
unit_of_measurement(string): unit of measurement
|
||
|
"""
|
||
|
super().__init__(
|
||
|
friendly_name=lambda wu: wu.data['daypart'][0]['daypartName'][period],
|
||
|
feature='forecast',
|
||
|
value=lambda wu: wu.data['daypart'][0]['narrative'][period],
|
||
|
entity_picture=lambda wu: '/local/wupws_icons/' + str(wu.data['daypart'][0]['iconCode'][period]) + '.png',
|
||
|
unit_of_measurement=unit_of_measurement,
|
||
|
device_state_attributes={
|
||
|
'date': lambda wu: wu.data['observations'][0]['obsTimeLocal']
|
||
|
}
|
||
|
)
|
||
|
|
||
|
|
||
|
class WUDailySimpleForecastSensorConfig(WUSensorConfig):
|
||
|
"""Helper for defining sensor configurations for daily simpleforecasts."""
|
||
|
|
||
|
def __init__(self, friendly_name, period, field,
|
||
|
ha_unit=None, icon=None, device_class=None):
|
||
|
"""Constructor.
|
||
|
|
||
|
Args:
|
||
|
period (int): forecast period number
|
||
|
field (string): field name to use as value
|
||
|
ha_unit (string): corresponding unit in home assistant
|
||
|
title (string): friendly_name of the sensor
|
||
|
"""
|
||
|
super().__init__(
|
||
|
friendly_name=friendly_name,
|
||
|
feature='forecast',
|
||
|
value=(lambda wu: wu.data['daypart'][0][field][period]),
|
||
|
unit_of_measurement=ha_unit,
|
||
|
entity_picture=lambda wu: wu.data['daypart'][0]['iconCode'][period] if not icon else None,
|
||
|
icon=icon,
|
||
|
device_state_attributes={
|
||
|
'date': lambda wu: wu.data['observations'][0]['obsTimeLocal']
|
||
|
},
|
||
|
device_class=device_class
|
||
|
)
|
||
|
|
||
|
|
||
|
# Declaration of supported WU sensors
|
||
|
# (see above helper classes for argument explanation)
|
||
|
|
||
|
SENSOR_TYPES = {
|
||
|
# current
|
||
|
'neighborhood': WUSensorConfig(
|
||
|
'Neighborhood',
|
||
|
'observations',
|
||
|
value=lambda wu: wu.data['observations'][0][
|
||
|
'neighborhood'],
|
||
|
icon="mdi:map-marker"),
|
||
|
'obsTimeLocal': WUSensorConfig(
|
||
|
'Local Observation Time',
|
||
|
'observations',
|
||
|
value=lambda wu: wu.data['observations'][0][
|
||
|
'obsTimeLocal'],
|
||
|
icon="mdi:clock"),
|
||
|
'humidity': WUSensorConfig(
|
||
|
'Relative Humidity',
|
||
|
'observations',
|
||
|
value=lambda wu: int(wu.data['observations'][0][
|
||
|
'humidity']),
|
||
|
unit_of_measurement='%',
|
||
|
icon="mdi:water-percent",
|
||
|
device_class="humidity"),
|
||
|
'stationID': WUSensorConfig(
|
||
|
'Station ID',
|
||
|
'observations',
|
||
|
value=lambda wu: wu.data['observations'][0][
|
||
|
'stationID'],
|
||
|
icon="mdi:home"),
|
||
|
'solarRadiation': WUSensorConfig(
|
||
|
'Solar Radiation',
|
||
|
'observations',
|
||
|
value=lambda wu: str(wu.data['observations'][0][
|
||
|
'solarRadiation']),
|
||
|
unit_of_measurement='w/m2',
|
||
|
icon="mdi:weather-sunny"),
|
||
|
'uv': WUSensorConfig(
|
||
|
'UV',
|
||
|
'observations',
|
||
|
value=lambda wu: str(wu.data['observations'][0][
|
||
|
'uv']),
|
||
|
unit_of_measurement='',
|
||
|
icon="mdi:sunglasses", ),
|
||
|
'winddir': WUSensorConfig(
|
||
|
'Wind Direction',
|
||
|
'observations',
|
||
|
value=lambda wu: int(wu.data['observations'][0][
|
||
|
'winddir']),
|
||
|
unit_of_measurement='\u00b0',
|
||
|
icon="mdi:weather-windy"),
|
||
|
'today_summary': WUSensorConfig(
|
||
|
'Today Summary',
|
||
|
'observations',
|
||
|
value=lambda wu: str(wu.data['narrative'][0]),
|
||
|
unit_of_measurement='\u00b0',
|
||
|
icon="mdi:gauge"),
|
||
|
'elev': WUCurrentConditionsSensorConfig(
|
||
|
'Elevation', UNIT_SYSTEM, 'elev', 'mdi:elevation-rise', ALTITUDEUNIT),
|
||
|
'dewpt': WUCurrentConditionsSensorConfig(
|
||
|
'Dewpoint', UNIT_SYSTEM, 'dewpt', 'mdi:water', TEMPUNIT),
|
||
|
'heatIndex': WUCurrentConditionsSensorConfig(
|
||
|
'Heat index', UNIT_SYSTEM, 'heatIndex', "mdi:thermometer", TEMPUNIT),
|
||
|
'windChill': WUCurrentConditionsSensorConfig(
|
||
|
'Wind chill', UNIT_SYSTEM, 'windChill', "mdi:thermometer", TEMPUNIT),
|
||
|
'precipRate': WUCurrentConditionsSensorConfig(
|
||
|
'Precipitation Rate', UNIT_SYSTEM, 'precipRate', "mdi:umbrella", LENGTHUNIT),
|
||
|
'precipTotal': WUCurrentConditionsSensorConfig(
|
||
|
'Precipitation Today', UNIT_SYSTEM, 'precipTotal', "mdi:umbrella", LENGTHUNIT),
|
||
|
'pressure': WUCurrentConditionsSensorConfig(
|
||
|
'Pressure', UNIT_SYSTEM, 'pressure', "mdi:gauge", PRESSUREUNIT,
|
||
|
device_class="pressure"),
|
||
|
'temp': WUCurrentConditionsSensorConfig(
|
||
|
'Temperature', UNIT_SYSTEM, 'temp', "mdi:thermometer", TEMPUNIT,
|
||
|
device_class="temperature"),
|
||
|
'windGust': WUCurrentConditionsSensorConfig(
|
||
|
'Wind Gust', UNIT_SYSTEM, 'windGust', "mdi:weather-windy", SPEEDUNIT),
|
||
|
'windSpeed': WUCurrentConditionsSensorConfig(
|
||
|
'Wind Speed', UNIT_SYSTEM, 'windSpeed', "mdi:weather-windy", SPEEDUNIT),
|
||
|
# forecast
|
||
|
'weather_1d': WUDailyTextForecastSensorConfig(0),
|
||
|
'weather_1n': WUDailyTextForecastSensorConfig(1),
|
||
|
'weather_2d': WUDailyTextForecastSensorConfig(2),
|
||
|
'weather_2n': WUDailyTextForecastSensorConfig(3),
|
||
|
'weather_3d': WUDailyTextForecastSensorConfig(4),
|
||
|
'weather_3n': WUDailyTextForecastSensorConfig(5),
|
||
|
'weather_4d': WUDailyTextForecastSensorConfig(6),
|
||
|
'weather_4n': WUDailyTextForecastSensorConfig(7),
|
||
|
'weather_5d': WUDailyTextForecastSensorConfig(8),
|
||
|
'weather_5n': WUDailyTextForecastSensorConfig(9),
|
||
|
'temp_high_1d': WUDailySimpleForecastSensorConfig(
|
||
|
"High Temperature Today", 0, "temperature", TEMPUNIT,
|
||
|
"mdi:thermometer", device_class="temperature"),
|
||
|
'temp_high_2d': WUDailySimpleForecastSensorConfig(
|
||
|
"High Temperature Tomorrow", 2, "temperature", TEMPUNIT,
|
||
|
"mdi:thermometer", device_class="temperature"),
|
||
|
'temp_high_3d': WUDailySimpleForecastSensorConfig(
|
||
|
"High Temperature in 3 Days", 4, "temperature", TEMPUNIT,
|
||
|
"mdi:thermometer", device_class="temperature"),
|
||
|
'temp_high_4d': WUDailySimpleForecastSensorConfig(
|
||
|
"High Temperature in 4 Days", 6, "temperature", TEMPUNIT,
|
||
|
"mdi:thermometer", device_class="temperature"),
|
||
|
'temp_high_5d': WUDailySimpleForecastSensorConfig(
|
||
|
"High Temperature in 5 Days", 8, "temperature", TEMPUNIT,
|
||
|
"mdi:thermometer", device_class="temperature"),
|
||
|
'temp_low_1d': WUDailySimpleForecastSensorConfig(
|
||
|
"Low Temperature Today", 1, "temperature", TEMPUNIT,
|
||
|
"mdi:thermometer", device_class="temperature"),
|
||
|
'temp_low_2d': WUDailySimpleForecastSensorConfig(
|
||
|
"Low Temperature Tomorrow", 3, "temperature", TEMPUNIT,
|
||
|
"mdi:thermometer", device_class="temperature"),
|
||
|
'temp_low_3d': WUDailySimpleForecastSensorConfig(
|
||
|
"Low Temperature in 3 Days", 5, "temperature", TEMPUNIT,
|
||
|
"mdi:thermometer", device_class="temperature"),
|
||
|
'temp_low_4d': WUDailySimpleForecastSensorConfig(
|
||
|
"Low Temperature in 4 Days", 7, "temperature", TEMPUNIT,
|
||
|
"mdi:thermometer", device_class="temperature"),
|
||
|
'temp_low_5d': WUDailySimpleForecastSensorConfig(
|
||
|
"Low Temperature in 5 Days", 9, "temperature", TEMPUNIT,
|
||
|
"mdi:thermometer", device_class="temperature"),
|
||
|
'wind_1d': WUDailySimpleForecastSensorConfig(
|
||
|
"Avg. Wind Today", 0, "windSpeed", SPEEDUNIT,
|
||
|
"mdi:weather-windy"),
|
||
|
'wind_2d': WUDailySimpleForecastSensorConfig(
|
||
|
"Avg. Wind Tomorrow", 2, "windSpeed", SPEEDUNIT,
|
||
|
"mdi:weather-windy"),
|
||
|
'wind_3d': WUDailySimpleForecastSensorConfig(
|
||
|
"Avg. Wind in 3 Days", 4, "windSpeed", SPEEDUNIT,
|
||
|
"mdi:weather-windy"),
|
||
|
'wind_4d': WUDailySimpleForecastSensorConfig(
|
||
|
"Avg. Wind in 4 Days", 6, "windSpeed", SPEEDUNIT,
|
||
|
"mdi:weather-windy"),
|
||
|
'wind_5d': WUDailySimpleForecastSensorConfig(
|
||
|
"Avg. Wind in 5 Days", 8, "windSpeed", SPEEDUNIT,
|
||
|
"mdi:weather-windy"),
|
||
|
'precip_1d': WUDailySimpleForecastSensorConfig(
|
||
|
"Precipitation Intensity Today", 0, 'qpf', LENGTHUNIT,
|
||
|
"mdi:umbrella"),
|
||
|
'precip_2d': WUDailySimpleForecastSensorConfig(
|
||
|
"Precipitation Intensity Tomorrow", 2, 'qpf', LENGTHUNIT,
|
||
|
"mdi:umbrella"),
|
||
|
'precip_3d': WUDailySimpleForecastSensorConfig(
|
||
|
"Precipitation Intensity in 3 Days", 4, 'qpf', LENGTHUNIT,
|
||
|
"mdi:umbrella"),
|
||
|
'precip_4d': WUDailySimpleForecastSensorConfig(
|
||
|
"Precipitation Intensity in 4 Days", 6, 'qpf', LENGTHUNIT,
|
||
|
"mdi:umbrella"),
|
||
|
'precip_5d': WUDailySimpleForecastSensorConfig(
|
||
|
"Precipitation Intensity in 5 Days", 8, 'qpf', LENGTHUNIT,
|
||
|
"mdi:umbrella"),
|
||
|
'precip_chance_1d': WUDailySimpleForecastSensorConfig(
|
||
|
"Precipitation Probability Today", 0, "precipChance", "%",
|
||
|
"mdi:umbrella"),
|
||
|
'precip_chance_2d': WUDailySimpleForecastSensorConfig(
|
||
|
"Precipitation Probability Tomorrow", 2, "precipChance", "%",
|
||
|
"mdi:umbrella"),
|
||
|
'precip_chance_3d': WUDailySimpleForecastSensorConfig(
|
||
|
"Precipitation Probability in 3 Days", 4, "precipChance", "%",
|
||
|
"mdi:umbrella"),
|
||
|
'precip_chance_4d': WUDailySimpleForecastSensorConfig(
|
||
|
"Precipitation Probability in 4 Days", 6, "precipChance", "%",
|
||
|
"mdi:umbrella"),
|
||
|
'precip_chance_5d': WUDailySimpleForecastSensorConfig(
|
||
|
"Precipitation Probability in 5 Days", 8, "precipChance", "%",
|
||
|
"mdi:umbrella"),
|
||
|
}
|
||
|
|
||
|
# Language Supported Codes
|
||
|
LANG_CODES = [
|
||
|
'ar-AE', 'az-AZ', 'bg-BG', 'bn-BD', 'bn-IN', 'bs-BA', 'ca-ES', 'cs-CZ', 'da-DK', 'de-DE', 'el-GR', 'en-GB', 'en-IN',
|
||
|
'en-US', 'es-AR', 'es-ES', 'es-LA', 'es-MX', 'es-UN', 'es-US', 'et-EE', 'fa-IR', 'fi-FI', 'fr-CA', 'fr-FR', 'gu-IN',
|
||
|
'he-IL', 'hi-IN', 'hr-HR', 'hu-HU', 'in-ID', 'is-IS', 'it-IT', 'iw-IL', 'ja-JP', 'jv-ID', 'ka-GE', 'kk-KZ', 'kn-IN',
|
||
|
'ko-KR', 'lt-LT', 'lv-LV', 'mk-MK', 'mn-MN', 'ms-MY', 'nl-NL', 'no-NO', 'pl-PL', 'pt-BR', 'pt-PT', 'ro-RO', 'ru-RU',
|
||
|
'si-LK', 'sk-SK', 'sl-SI', 'sq-AL', 'sr-BA', 'sr-ME', 'sr-RS', 'sv-SE', 'sw-KE', 'ta-IN', 'ta-LK', 'te-IN', 'tg-TJ',
|
||
|
'th-TH', 'tk-TM', 'tl-PH', 'tr-TR', 'uk-UA', 'ur-PK', 'uz-UZ', 'vi-VN', 'zh-CN', 'zh-HK', 'zh-TW'
|
||
|
]
|
||
|
|
||
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||
|
vol.Required(CONF_API_KEY): cv.string,
|
||
|
vol.Required(CONF_PWS_ID): cv.string,
|
||
|
vol.Optional(CONF_LANG, default=DEFAULT_LANG): vol.All(vol.In(LANG_CODES)),
|
||
|
vol.Inclusive(CONF_LATITUDE, 'coordinates',
|
||
|
'Latitude and longitude must exist together'): cv.latitude,
|
||
|
vol.Inclusive(CONF_LONGITUDE, 'coordinates',
|
||
|
'Latitude and longitude must exist together'): cv.longitude,
|
||
|
vol.Required(CONF_MONITORED_CONDITIONS):
|
||
|
vol.All(cv.ensure_list, vol.Length(min=1), [vol.In(SENSOR_TYPES)])
|
||
|
})
|
||
|
|
||
|
|
||
|
async def async_setup_platform(hass: HomeAssistantType, config: ConfigType,
|
||
|
async_add_entities, discovery_info=None):
|
||
|
"""Set up the WUnderground sensor."""
|
||
|
latitude = config.get(CONF_LATITUDE, hass.config.latitude)
|
||
|
longitude = config.get(CONF_LONGITUDE, hass.config.longitude)
|
||
|
pws_id = config.get(CONF_PWS_ID)
|
||
|
|
||
|
if hass.config.units.is_metric:
|
||
|
unit_system_api = 'm'
|
||
|
else:
|
||
|
unit_system_api = 'e'
|
||
|
|
||
|
rest = WUndergroundData(
|
||
|
hass, config.get(CONF_API_KEY), pws_id, unit_system_api,
|
||
|
config.get(CONF_LANG), latitude, longitude)
|
||
|
|
||
|
if pws_id is None:
|
||
|
unique_id_base = "@{:06f},{:06f}".format(longitude, latitude)
|
||
|
else:
|
||
|
# Manually specified weather station, use that for unique_id
|
||
|
unique_id_base = pws_id
|
||
|
sensors = []
|
||
|
for variable in config[CONF_MONITORED_CONDITIONS]:
|
||
|
sensors.append(WUndergroundSensor(hass, rest, variable,
|
||
|
unique_id_base))
|
||
|
|
||
|
await rest.async_update()
|
||
|
if not rest.data:
|
||
|
raise PlatformNotReady
|
||
|
|
||
|
async_add_entities(sensors, True)
|
||
|
|
||
|
|
||
|
class WUndergroundSensor(Entity):
|
||
|
"""Implementing the WUnderground sensor."""
|
||
|
|
||
|
def __init__(self, hass: HomeAssistantType, rest, condition,
|
||
|
unique_id_base: str):
|
||
|
"""Initialize the sensor."""
|
||
|
self.rest = rest
|
||
|
self._condition = condition
|
||
|
self._state = None
|
||
|
self._attributes = {
|
||
|
ATTR_ATTRIBUTION: CONF_ATTRIBUTION,
|
||
|
}
|
||
|
self._icon = None
|
||
|
self._entity_picture = None
|
||
|
self._unit_of_measurement = self._cfg_expand("unit_of_measurement")
|
||
|
self.rest.request_feature(SENSOR_TYPES[condition].feature)
|
||
|
# This is only the suggested entity id, it might get changed by
|
||
|
# the entity registry later.
|
||
|
self.entity_id = sensor.ENTITY_ID_FORMAT.format('wupws_' + condition)
|
||
|
self._unique_id = "{},{}".format(unique_id_base, condition)
|
||
|
self._device_class = self._cfg_expand("device_class")
|
||
|
|
||
|
def _cfg_expand(self, what, default=None):
|
||
|
"""Parse and return sensor data."""
|
||
|
cfg = SENSOR_TYPES[self._condition]
|
||
|
val = getattr(cfg, what)
|
||
|
if not callable(val):
|
||
|
return val
|
||
|
try:
|
||
|
val = val(self.rest)
|
||
|
except (KeyError, IndexError, TypeError, ValueError) as err:
|
||
|
_LOGGER.warning("Failed to expand cfg from WU API."
|
||
|
" Condition: %s Attr: %s Error: %s",
|
||
|
self._condition, what, repr(err))
|
||
|
val = default
|
||
|
|
||
|
return val
|
||
|
|
||
|
def _update_attrs(self):
|
||
|
"""Parse and update device state attributes."""
|
||
|
attrs = self._cfg_expand("device_state_attributes", {})
|
||
|
|
||
|
for (attr, callback) in attrs.items():
|
||
|
if callable(callback):
|
||
|
try:
|
||
|
self._attributes[attr] = callback(self.rest)
|
||
|
except (KeyError, IndexError, TypeError, ValueError) as err:
|
||
|
_LOGGER.warning("Failed to update attrs from WU API."
|
||
|
" Condition: %s Attr: %s Error: %s",
|
||
|
self._condition, attr, repr(err))
|
||
|
else:
|
||
|
self._attributes[attr] = callback
|
||
|
|
||
|
@property
|
||
|
def name(self):
|
||
|
"""Return the name of the sensor."""
|
||
|
return self._cfg_expand("friendly_name")
|
||
|
|
||
|
@property
|
||
|
def state(self):
|
||
|
"""Return the state of the sensor."""
|
||
|
return self._state
|
||
|
|
||
|
@property
|
||
|
def device_state_attributes(self):
|
||
|
"""Return the state attributes."""
|
||
|
return self._attributes
|
||
|
|
||
|
@property
|
||
|
def icon(self):
|
||
|
"""Return icon."""
|
||
|
return self._icon
|
||
|
|
||
|
@property
|
||
|
def entity_picture(self):
|
||
|
"""Return the entity picture."""
|
||
|
return self._entity_picture
|
||
|
|
||
|
@property
|
||
|
def unit_of_measurement(self):
|
||
|
"""Return the units of measurement."""
|
||
|
return self._unit_of_measurement
|
||
|
|
||
|
@property
|
||
|
def device_class(self):
|
||
|
"""Return the units of measurement."""
|
||
|
return self._device_class
|
||
|
|
||
|
async def async_update(self):
|
||
|
"""Update current conditions."""
|
||
|
await self.rest.async_update()
|
||
|
|
||
|
if not self.rest.data:
|
||
|
# no data, return
|
||
|
return
|
||
|
|
||
|
self._state = self._cfg_expand("value")
|
||
|
self._update_attrs()
|
||
|
self._icon = self._cfg_expand("icon", super().icon)
|
||
|
url = self._cfg_expand("entity_picture")
|
||
|
if isinstance(url, str):
|
||
|
self._entity_picture = re.sub(r'^http://', 'https://',
|
||
|
url, flags=re.IGNORECASE)
|
||
|
|
||
|
@property
|
||
|
def unique_id(self) -> str:
|
||
|
"""Return a unique ID."""
|
||
|
return self._unique_id
|
||
|
|
||
|
|
||
|
class WUndergroundData:
|
||
|
"""Get data from WUnderground."""
|
||
|
|
||
|
def __init__(self, hass, api_key, pws_id, unit_system_api, lang, latitude, longitude):
|
||
|
"""Initialize the data object."""
|
||
|
self._hass = hass
|
||
|
self._api_key = api_key
|
||
|
self._pws_id = pws_id
|
||
|
self._unit_system_api = unit_system_api
|
||
|
self._lang = 'language={}'.format(lang)
|
||
|
self._latitude = latitude
|
||
|
self._longitude = longitude
|
||
|
self._features = set()
|
||
|
self.data = None
|
||
|
self._session = async_get_clientsession(self._hass)
|
||
|
|
||
|
def request_feature(self, feature):
|
||
|
"""Register feature to be fetched from WU API."""
|
||
|
self._features.add(feature)
|
||
|
|
||
|
def _build_url(self, baseurl):
|
||
|
if baseurl is _RESOURCECURRENT:
|
||
|
url = baseurl.format(self._pws_id, self._unit_system_api, self._api_key)
|
||
|
else:
|
||
|
url = baseurl.format(self._latitude, self._longitude, self._unit_system_api, self._lang, self._api_key)
|
||
|
|
||
|
return url
|
||
|
|
||
|
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||
|
async def async_update(self):
|
||
|
"""Get the latest data from WUnderground."""
|
||
|
headers = {'Accept-Encoding': 'gzip'}
|
||
|
try:
|
||
|
with async_timeout.timeout(10, loop=self._hass.loop):
|
||
|
response = await self._session.get(self._build_url(_RESOURCECURRENT), headers=headers)
|
||
|
result_current = await response.json()
|
||
|
|
||
|
# need to check specific new api errors
|
||
|
# if "error" in result['response']:
|
||
|
# raise ValueError(result['response']["error"]["description"])
|
||
|
# _LOGGER.debug('result_current' + str(result_current))
|
||
|
|
||
|
if result_current is None:
|
||
|
raise ValueError('NO CURRENT RESULT')
|
||
|
with async_timeout.timeout(10, loop=self._hass.loop):
|
||
|
response = await self._session.get(self._build_url(_RESOURCEFORECAST), headers=headers)
|
||
|
result_forecast = await response.json()
|
||
|
|
||
|
if result_forecast is None:
|
||
|
raise ValueError('NO FORECAST RESULT')
|
||
|
|
||
|
result = {**result_current, **result_forecast}
|
||
|
|
||
|
self.data = result
|
||
|
except ValueError as err:
|
||
|
_LOGGER.error("Check WUnderground API %s", err.args)
|
||
|
except (asyncio.TimeoutError, aiohttp.ClientError) as err:
|
||
|
_LOGGER.error("Error fetching WUnderground data: %s", repr(err))
|