"""Support for WUnderground weather service."""
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_KILOMETERS,
    LENGTH_MILES, 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

_RESOURCE = 'http://api.wunderground.com/api/{}/{}/{}/q/'
_LOGGER = logging.getLogger(__name__)

ATTRIBUTION = "Data provided by the WUnderground weather service"

CONF_PWS_ID = 'pws_id'
CONF_LANG = 'lang'

DEFAULT_LANG = 'EN'

MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5)


# 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://www.wunderground.com/weather/api/d/docs?d=data/index
            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 or URL
            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, 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 "current_observation"
                            dictionary.
            icon (string): icon name or URL, 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['current_observation'][field],
            icon=icon,
            unit_of_measurement=unit_of_measurement,
            entity_picture=lambda wu: wu.data['current_observation'][
                'icon_url'] if icon is None else None,
            device_state_attributes={
                'date': lambda wu: wu.data['current_observation'][
                    'observation_time']
            },
            device_class=device_class
        )


class WUDailyTextForecastSensorConfig(WUSensorConfig):
    """Helper for defining sensor configurations for daily text forecasts."""

    def __init__(self, period, field, 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['forecast']['txt_forecast'][
                'forecastday'][period]['title'],
            feature='forecast',
            value=lambda wu: wu.data['forecast']['txt_forecast'][
                'forecastday'][period][field],
            entity_picture=lambda wu: wu.data['forecast']['txt_forecast'][
                'forecastday'][period]['icon_url'],
            unit_of_measurement=unit_of_measurement,
            device_state_attributes={
                'date': lambda wu: wu.data['forecast']['txt_forecast']['date']
            }
        )


class WUDailySimpleForecastSensorConfig(WUSensorConfig):
    """Helper for defining sensor configurations for daily simpleforecasts."""

    def __init__(self, friendly_name, period, field, wu_unit=None,
                 ha_unit=None, icon=None, device_class=None):
        """Constructor.

        Args:
            period (int): forecast period number
            field (string): field name to use as value
            wu_unit (string): "fahrenheit", "celsius", "degrees" etc.
                 see the example json at:
        https://www.wunderground.com/weather/api/d/docs?d=data/forecast&MR=1
            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['forecast']['simpleforecast'][
                'forecastday'][period][field][wu_unit])
            if wu_unit else
            (lambda wu: wu.data['forecast']['simpleforecast'][
                'forecastday'][period][field]),
            unit_of_measurement=ha_unit,
            entity_picture=lambda wu: wu.data['forecast']['simpleforecast'][
                'forecastday'][period]['icon_url'] if not icon else None,
            icon=icon,
            device_state_attributes={
                'date': lambda wu: wu.data['forecast']['simpleforecast'][
                    'forecastday'][period]['date']['pretty']
            },
            device_class=device_class
        )


class WUHourlyForecastSensorConfig(WUSensorConfig):
    """Helper for defining sensor configurations for hourly text forecasts."""

    def __init__(self, period, field):
        """Constructor.

        Args:
            period (int): forecast period number
            field (int): field name to use as value
        """
        super().__init__(
            friendly_name=lambda wu: "{} {}".format(
                wu.data['hourly_forecast'][period]['FCTTIME'][
                    'weekday_name_abbrev'],
                wu.data['hourly_forecast'][period]['FCTTIME'][
                    'civil']),
            feature='hourly',
            value=lambda wu: wu.data['hourly_forecast'][period][
                field],
            entity_picture=lambda wu: wu.data['hourly_forecast'][
                period]["icon_url"],
            device_state_attributes={
                'temp_c': lambda wu: wu.data['hourly_forecast'][
                    period]['temp']['metric'],
                'temp_f': lambda wu: wu.data['hourly_forecast'][
                    period]['temp']['english'],
                'dewpoint_c': lambda wu: wu.data['hourly_forecast'][
                    period]['dewpoint']['metric'],
                'dewpoint_f': lambda wu: wu.data['hourly_forecast'][
                    period]['dewpoint']['english'],
                'precip_prop': lambda wu: wu.data['hourly_forecast'][
                    period]['pop'],
                'sky': lambda wu: wu.data['hourly_forecast'][
                    period]['sky'],
                'precip_mm': lambda wu: wu.data['hourly_forecast'][
                    period]['qpf']['metric'],
                'precip_in': lambda wu: wu.data['hourly_forecast'][
                    period]['qpf']['english'],
                'humidity': lambda wu: wu.data['hourly_forecast'][
                    period]['humidity'],
                'wind_kph': lambda wu: wu.data['hourly_forecast'][
                    period]['wspd']['metric'],
                'wind_mph': lambda wu: wu.data['hourly_forecast'][
                    period]['wspd']['english'],
                'pressure_mb': lambda wu: wu.data['hourly_forecast'][
                    period]['mslp']['metric'],
                'pressure_inHg': lambda wu: wu.data['hourly_forecast'][
                    period]['mslp']['english'],
                'date': lambda wu: wu.data['hourly_forecast'][
                    period]['FCTTIME']['pretty'],
            }
        )


class WUAlmanacSensorConfig(WUSensorConfig):
    """Helper for defining field configurations for almanac sensors."""

    def __init__(self, friendly_name, field, value_type, wu_unit,
                 unit_of_measurement, icon, device_class=None):
        """Constructor.

        Args:
            friendly_name (string|func): Friendly name
            field (string): value name returned in 'almanac' dict
                            as returned by the WU API
            value_type (string):  "record" or "normal"
            wu_unit (string): unit name in WU API
            icon (string): icon name or URL
            unit_of_measurement (string): unit of measurement
        """
        super().__init__(
            friendly_name=friendly_name,
            feature="almanac",
            value=lambda wu: wu.data['almanac'][field][value_type][wu_unit],
            unit_of_measurement=unit_of_measurement,
            icon=icon,
            device_class="temperature"
        )


class WUAlertsSensorConfig(WUSensorConfig):
    """Helper for defining field configuration for alerts."""

    def __init__(self, friendly_name):
        """Constructor.

        Args:
            friendly_name (string|func): Friendly name
        """
        super().__init__(
            friendly_name=friendly_name,
            feature="alerts",
            value=lambda wu: len(wu.data['alerts']),
            icon=lambda wu: "mdi:alert-circle-outline"
            if wu.data['alerts'] else "mdi:check-circle-outline",
            device_state_attributes=self._get_attributes
        )

    @staticmethod
    def _get_attributes(rest):

        attrs = {}

        if 'alerts' not in rest.data:
            return attrs

        alerts = rest.data['alerts']
        multiple_alerts = len(alerts) > 1
        for data in alerts:
            for alert in ALERTS_ATTRS:
                if data[alert]:
                    if multiple_alerts:
                        dkey = alert.capitalize() + '_' + data['type']
                    else:
                        dkey = alert.capitalize()
                    attrs[dkey] = data[alert]
        return attrs


# Declaration of supported WU sensors
# (see above helper classes for argument explanation)

SENSOR_TYPES = {
    'alerts': WUAlertsSensorConfig('Alerts'),
    'dewpoint_c': WUCurrentConditionsSensorConfig(
        'Dewpoint', 'dewpoint_c', 'mdi:water', TEMP_CELSIUS),
    'dewpoint_f': WUCurrentConditionsSensorConfig(
        'Dewpoint', 'dewpoint_f', 'mdi:water', TEMP_FAHRENHEIT),
    'dewpoint_string': WUCurrentConditionsSensorConfig(
        'Dewpoint Summary', 'dewpoint_string', 'mdi:water'),
    'feelslike_c': WUCurrentConditionsSensorConfig(
        'Feels Like', 'feelslike_c', 'mdi:thermometer', TEMP_CELSIUS),
    'feelslike_f': WUCurrentConditionsSensorConfig(
        'Feels Like', 'feelslike_f', 'mdi:thermometer', TEMP_FAHRENHEIT),
    'feelslike_string': WUCurrentConditionsSensorConfig(
        'Feels Like', 'feelslike_string', "mdi:thermometer"),
    'heat_index_c': WUCurrentConditionsSensorConfig(
        'Heat index', 'heat_index_c', "mdi:thermometer", TEMP_CELSIUS),
    'heat_index_f': WUCurrentConditionsSensorConfig(
        'Heat index', 'heat_index_f', "mdi:thermometer", TEMP_FAHRENHEIT),
    'heat_index_string': WUCurrentConditionsSensorConfig(
        'Heat Index Summary', 'heat_index_string', "mdi:thermometer"),
    'elevation': WUSensorConfig(
        'Elevation',
        'conditions',
        value=lambda wu: wu.data['current_observation'][
            'observation_location']['elevation'].split()[0],
        unit_of_measurement=LENGTH_FEET,
        icon="mdi:elevation-rise"),
    'location': WUSensorConfig(
        'Location',
        'conditions',
        value=lambda wu: wu.data['current_observation'][
            'display_location']['full'],
        icon="mdi:map-marker"),
    'observation_time': WUCurrentConditionsSensorConfig(
        'Observation Time', 'observation_time', "mdi:clock"),
    'precip_1hr_in': WUCurrentConditionsSensorConfig(
        'Precipitation 1hr', 'precip_1hr_in', "mdi:umbrella", LENGTH_INCHES),
    'precip_1hr_metric': WUCurrentConditionsSensorConfig(
        'Precipitation 1hr', 'precip_1hr_metric', "mdi:umbrella", 'mm'),
    'precip_1hr_string': WUCurrentConditionsSensorConfig(
        'Precipitation 1hr', 'precip_1hr_string', "mdi:umbrella"),
    'precip_today_in': WUCurrentConditionsSensorConfig(
        'Precipitation Today', 'precip_today_in', "mdi:umbrella",
        LENGTH_INCHES),
    'precip_today_metric': WUCurrentConditionsSensorConfig(
        'Precipitation Today', 'precip_today_metric', "mdi:umbrella", 'mm'),
    'precip_today_string': WUCurrentConditionsSensorConfig(
        'Precipitation Today', 'precip_today_string', "mdi:umbrella"),
    'pressure_in': WUCurrentConditionsSensorConfig(
        'Pressure', 'pressure_in', "mdi:gauge", 'inHg',
        device_class="pressure"),
    'pressure_mb': WUCurrentConditionsSensorConfig(
        'Pressure', 'pressure_mb', "mdi:gauge", 'mb',
        device_class="pressure"),
    'pressure_trend': WUCurrentConditionsSensorConfig(
        'Pressure Trend', 'pressure_trend', "mdi:gauge",
        device_class="pressure"),
    'relative_humidity': WUSensorConfig(
        'Relative Humidity',
        'conditions',
        value=lambda wu: int(wu.data['current_observation'][
            'relative_humidity'][:-1]),
        unit_of_measurement='%',
        icon="mdi:water-percent",
        device_class="humidity"),
    'station_id': WUCurrentConditionsSensorConfig(
        'Station ID', 'station_id', "mdi:home"),
    'solarradiation': WUCurrentConditionsSensorConfig(
        'Solar Radiation', 'solarradiation', "mdi:weather-sunny", "w/m2"),
    'temperature_string': WUCurrentConditionsSensorConfig(
        'Temperature Summary', 'temperature_string', "mdi:thermometer"),
    'temp_c': WUCurrentConditionsSensorConfig(
        'Temperature', 'temp_c', "mdi:thermometer", TEMP_CELSIUS,
        device_class="temperature"),
    'temp_f': WUCurrentConditionsSensorConfig(
        'Temperature', 'temp_f', "mdi:thermometer", TEMP_FAHRENHEIT,
        device_class="temperature"),
    'UV': WUCurrentConditionsSensorConfig(
        'UV', 'UV', "mdi:sunglasses"),
    'visibility_km': WUCurrentConditionsSensorConfig(
        'Visibility (km)', 'visibility_km', "mdi:eye", LENGTH_KILOMETERS),
    'visibility_mi': WUCurrentConditionsSensorConfig(
        'Visibility (miles)', 'visibility_mi', "mdi:eye", LENGTH_MILES),
    'weather': WUCurrentConditionsSensorConfig(
        'Weather Summary', 'weather', None),
    'wind_degrees': WUCurrentConditionsSensorConfig(
        'Wind Degrees', 'wind_degrees', "mdi:weather-windy", "°"),
    'wind_dir': WUCurrentConditionsSensorConfig(
        'Wind Direction', 'wind_dir', "mdi:weather-windy"),
    'wind_gust_kph': WUCurrentConditionsSensorConfig(
        'Wind Gust', 'wind_gust_kph', "mdi:weather-windy", 'kph'),
    'wind_gust_mph': WUCurrentConditionsSensorConfig(
        'Wind Gust', 'wind_gust_mph', "mdi:weather-windy", 'mph'),
    'wind_kph': WUCurrentConditionsSensorConfig(
        'Wind Speed', 'wind_kph', "mdi:weather-windy", 'kph'),
    'wind_mph': WUCurrentConditionsSensorConfig(
        'Wind Speed', 'wind_mph', "mdi:weather-windy", 'mph'),
    'wind_string': WUCurrentConditionsSensorConfig(
        'Wind Summary', 'wind_string', "mdi:weather-windy"),
    'temp_high_record_c': WUAlmanacSensorConfig(
        lambda wu: 'High Temperature Record ({})'.format(
            wu.data['almanac']['temp_high']['recordyear']),
        'temp_high', 'record', 'C', TEMP_CELSIUS, 'mdi:thermometer'),
    'temp_high_record_f': WUAlmanacSensorConfig(
        lambda wu: 'High Temperature Record ({})'.format(
            wu.data['almanac']['temp_high']['recordyear']),
        'temp_high', 'record', 'F', TEMP_FAHRENHEIT, 'mdi:thermometer'),
    'temp_low_record_c': WUAlmanacSensorConfig(
        lambda wu: 'Low Temperature Record ({})'.format(
            wu.data['almanac']['temp_low']['recordyear']),
        'temp_low', 'record', 'C', TEMP_CELSIUS, 'mdi:thermometer'),
    'temp_low_record_f': WUAlmanacSensorConfig(
        lambda wu: 'Low Temperature Record ({})'.format(
            wu.data['almanac']['temp_low']['recordyear']),
        'temp_low', 'record', 'F', TEMP_FAHRENHEIT, 'mdi:thermometer'),
    'temp_low_avg_c': WUAlmanacSensorConfig(
        'Historic Average of Low Temperatures for Today',
        'temp_low', 'normal', 'C', TEMP_CELSIUS, 'mdi:thermometer'),
    'temp_low_avg_f': WUAlmanacSensorConfig(
        'Historic Average of Low Temperatures for Today',
        'temp_low', 'normal', 'F', TEMP_FAHRENHEIT, 'mdi:thermometer'),
    'temp_high_avg_c': WUAlmanacSensorConfig(
        'Historic Average of High Temperatures for Today',
        'temp_high', 'normal', 'C', TEMP_CELSIUS, "mdi:thermometer"),
    'temp_high_avg_f': WUAlmanacSensorConfig(
        'Historic Average of High Temperatures for Today',
        'temp_high', 'normal', 'F', TEMP_FAHRENHEIT, "mdi:thermometer"),
    'weather_1d': WUDailyTextForecastSensorConfig(0, "fcttext"),
    'weather_1d_metric': WUDailyTextForecastSensorConfig(0, "fcttext_metric"),
    'weather_1n': WUDailyTextForecastSensorConfig(1, "fcttext"),
    'weather_1n_metric': WUDailyTextForecastSensorConfig(1, "fcttext_metric"),
    'weather_2d': WUDailyTextForecastSensorConfig(2, "fcttext"),
    'weather_2d_metric': WUDailyTextForecastSensorConfig(2, "fcttext_metric"),
    'weather_2n': WUDailyTextForecastSensorConfig(3, "fcttext"),
    'weather_2n_metric': WUDailyTextForecastSensorConfig(3, "fcttext_metric"),
    'weather_3d': WUDailyTextForecastSensorConfig(4, "fcttext"),
    'weather_3d_metric': WUDailyTextForecastSensorConfig(4, "fcttext_metric"),
    'weather_3n': WUDailyTextForecastSensorConfig(5, "fcttext"),
    'weather_3n_metric': WUDailyTextForecastSensorConfig(5, "fcttext_metric"),
    'weather_4d': WUDailyTextForecastSensorConfig(6, "fcttext"),
    'weather_4d_metric': WUDailyTextForecastSensorConfig(6, "fcttext_metric"),
    'weather_4n': WUDailyTextForecastSensorConfig(7, "fcttext"),
    'weather_4n_metric': WUDailyTextForecastSensorConfig(7, "fcttext_metric"),
    'weather_1h': WUHourlyForecastSensorConfig(0, "condition"),
    'weather_2h': WUHourlyForecastSensorConfig(1, "condition"),
    'weather_3h': WUHourlyForecastSensorConfig(2, "condition"),
    'weather_4h': WUHourlyForecastSensorConfig(3, "condition"),
    'weather_5h': WUHourlyForecastSensorConfig(4, "condition"),
    'weather_6h': WUHourlyForecastSensorConfig(5, "condition"),
    'weather_7h': WUHourlyForecastSensorConfig(6, "condition"),
    'weather_8h': WUHourlyForecastSensorConfig(7, "condition"),
    'weather_9h': WUHourlyForecastSensorConfig(8, "condition"),
    'weather_10h': WUHourlyForecastSensorConfig(9, "condition"),
    'weather_11h': WUHourlyForecastSensorConfig(10, "condition"),
    'weather_12h': WUHourlyForecastSensorConfig(11, "condition"),
    'weather_13h': WUHourlyForecastSensorConfig(12, "condition"),
    'weather_14h': WUHourlyForecastSensorConfig(13, "condition"),
    'weather_15h': WUHourlyForecastSensorConfig(14, "condition"),
    'weather_16h': WUHourlyForecastSensorConfig(15, "condition"),
    'weather_17h': WUHourlyForecastSensorConfig(16, "condition"),
    'weather_18h': WUHourlyForecastSensorConfig(17, "condition"),
    'weather_19h': WUHourlyForecastSensorConfig(18, "condition"),
    'weather_20h': WUHourlyForecastSensorConfig(19, "condition"),
    'weather_21h': WUHourlyForecastSensorConfig(20, "condition"),
    'weather_22h': WUHourlyForecastSensorConfig(21, "condition"),
    'weather_23h': WUHourlyForecastSensorConfig(22, "condition"),
    'weather_24h': WUHourlyForecastSensorConfig(23, "condition"),
    'weather_25h': WUHourlyForecastSensorConfig(24, "condition"),
    'weather_26h': WUHourlyForecastSensorConfig(25, "condition"),
    'weather_27h': WUHourlyForecastSensorConfig(26, "condition"),
    'weather_28h': WUHourlyForecastSensorConfig(27, "condition"),
    'weather_29h': WUHourlyForecastSensorConfig(28, "condition"),
    'weather_30h': WUHourlyForecastSensorConfig(29, "condition"),
    'weather_31h': WUHourlyForecastSensorConfig(30, "condition"),
    'weather_32h': WUHourlyForecastSensorConfig(31, "condition"),
    'weather_33h': WUHourlyForecastSensorConfig(32, "condition"),
    'weather_34h': WUHourlyForecastSensorConfig(33, "condition"),
    'weather_35h': WUHourlyForecastSensorConfig(34, "condition"),
    'weather_36h': WUHourlyForecastSensorConfig(35, "condition"),
    'temp_high_1d_c': WUDailySimpleForecastSensorConfig(
        "High Temperature Today", 0, "high", "celsius", TEMP_CELSIUS,
        "mdi:thermometer", device_class="temperature"),
    'temp_high_2d_c': WUDailySimpleForecastSensorConfig(
        "High Temperature Tomorrow", 1, "high", "celsius", TEMP_CELSIUS,
        "mdi:thermometer", device_class="temperature"),
    'temp_high_3d_c': WUDailySimpleForecastSensorConfig(
        "High Temperature in 3 Days", 2, "high", "celsius", TEMP_CELSIUS,
        "mdi:thermometer", device_class="temperature"),
    'temp_high_4d_c': WUDailySimpleForecastSensorConfig(
        "High Temperature in 4 Days", 3, "high", "celsius", TEMP_CELSIUS,
        "mdi:thermometer", device_class="temperature"),
    'temp_high_1d_f': WUDailySimpleForecastSensorConfig(
        "High Temperature Today", 0, "high", "fahrenheit", TEMP_FAHRENHEIT,
        "mdi:thermometer", device_class="temperature"),
    'temp_high_2d_f': WUDailySimpleForecastSensorConfig(
        "High Temperature Tomorrow", 1, "high", "fahrenheit", TEMP_FAHRENHEIT,
        "mdi:thermometer", device_class="temperature"),
    'temp_high_3d_f': WUDailySimpleForecastSensorConfig(
        "High Temperature in 3 Days", 2, "high", "fahrenheit", TEMP_FAHRENHEIT,
        "mdi:thermometer", device_class="temperature"),
    'temp_high_4d_f': WUDailySimpleForecastSensorConfig(
        "High Temperature in 4 Days", 3, "high", "fahrenheit", TEMP_FAHRENHEIT,
        "mdi:thermometer", device_class="temperature"),
    'temp_low_1d_c': WUDailySimpleForecastSensorConfig(
        "Low Temperature Today", 0, "low", "celsius", TEMP_CELSIUS,
        "mdi:thermometer", device_class="temperature"),
    'temp_low_2d_c': WUDailySimpleForecastSensorConfig(
        "Low Temperature Tomorrow", 1, "low", "celsius", TEMP_CELSIUS,
        "mdi:thermometer", device_class="temperature"),
    'temp_low_3d_c': WUDailySimpleForecastSensorConfig(
        "Low Temperature in 3 Days", 2, "low", "celsius", TEMP_CELSIUS,
        "mdi:thermometer", device_class="temperature"),
    'temp_low_4d_c': WUDailySimpleForecastSensorConfig(
        "Low Temperature in 4 Days", 3, "low", "celsius", TEMP_CELSIUS,
        "mdi:thermometer", device_class="temperature"),
    'temp_low_1d_f': WUDailySimpleForecastSensorConfig(
        "Low Temperature Today", 0, "low", "fahrenheit", TEMP_FAHRENHEIT,
        "mdi:thermometer", device_class="temperature"),
    'temp_low_2d_f': WUDailySimpleForecastSensorConfig(
        "Low Temperature Tomorrow", 1, "low", "fahrenheit", TEMP_FAHRENHEIT,
        "mdi:thermometer", device_class="temperature"),
    'temp_low_3d_f': WUDailySimpleForecastSensorConfig(
        "Low Temperature in 3 Days", 2, "low", "fahrenheit", TEMP_FAHRENHEIT,
        "mdi:thermometer", device_class="temperature"),
    'temp_low_4d_f': WUDailySimpleForecastSensorConfig(
        "Low Temperature in 4 Days", 3, "low", "fahrenheit", TEMP_FAHRENHEIT,
        "mdi:thermometer", device_class="temperature"),
    'wind_gust_1d_kph': WUDailySimpleForecastSensorConfig(
        "Max. Wind Today", 0, "maxwind", "kph", "kph", "mdi:weather-windy"),
    'wind_gust_2d_kph': WUDailySimpleForecastSensorConfig(
        "Max. Wind Tomorrow", 1, "maxwind", "kph", "kph", "mdi:weather-windy"),
    'wind_gust_3d_kph': WUDailySimpleForecastSensorConfig(
        "Max. Wind in 3 Days", 2, "maxwind", "kph", "kph",
        "mdi:weather-windy"),
    'wind_gust_4d_kph': WUDailySimpleForecastSensorConfig(
        "Max. Wind in 4 Days", 3, "maxwind", "kph", "kph",
        "mdi:weather-windy"),
    'wind_gust_1d_mph': WUDailySimpleForecastSensorConfig(
        "Max. Wind Today", 0, "maxwind", "mph", "mph",
        "mdi:weather-windy"),
    'wind_gust_2d_mph': WUDailySimpleForecastSensorConfig(
        "Max. Wind Tomorrow", 1, "maxwind", "mph", "mph",
        "mdi:weather-windy"),
    'wind_gust_3d_mph': WUDailySimpleForecastSensorConfig(
        "Max. Wind in 3 Days", 2, "maxwind", "mph", "mph",
        "mdi:weather-windy"),
    'wind_gust_4d_mph': WUDailySimpleForecastSensorConfig(
        "Max. Wind in 4 Days", 3, "maxwind", "mph", "mph",
        "mdi:weather-windy"),
    'wind_1d_kph': WUDailySimpleForecastSensorConfig(
        "Avg. Wind Today", 0, "avewind", "kph", "kph",
        "mdi:weather-windy"),
    'wind_2d_kph': WUDailySimpleForecastSensorConfig(
        "Avg. Wind Tomorrow", 1, "avewind", "kph", "kph",
        "mdi:weather-windy"),
    'wind_3d_kph': WUDailySimpleForecastSensorConfig(
        "Avg. Wind in 3 Days", 2, "avewind", "kph", "kph",
        "mdi:weather-windy"),
    'wind_4d_kph': WUDailySimpleForecastSensorConfig(
        "Avg. Wind in 4 Days", 3, "avewind", "kph", "kph",
        "mdi:weather-windy"),
    'wind_1d_mph': WUDailySimpleForecastSensorConfig(
        "Avg. Wind Today", 0, "avewind", "mph", "mph",
        "mdi:weather-windy"),
    'wind_2d_mph': WUDailySimpleForecastSensorConfig(
        "Avg. Wind Tomorrow", 1, "avewind", "mph", "mph",
        "mdi:weather-windy"),
    'wind_3d_mph': WUDailySimpleForecastSensorConfig(
        "Avg. Wind in 3 Days", 2, "avewind", "mph", "mph",
        "mdi:weather-windy"),
    'wind_4d_mph': WUDailySimpleForecastSensorConfig(
        "Avg. Wind in 4 Days", 3, "avewind", "mph", "mph",
        "mdi:weather-windy"),
    'precip_1d_mm': WUDailySimpleForecastSensorConfig(
        "Precipitation Intensity Today", 0, 'qpf_allday', 'mm', 'mm',
        "mdi:umbrella"),
    'precip_2d_mm': WUDailySimpleForecastSensorConfig(
        "Precipitation Intensity Tomorrow", 1, 'qpf_allday', 'mm', 'mm',
        "mdi:umbrella"),
    'precip_3d_mm': WUDailySimpleForecastSensorConfig(
        "Precipitation Intensity in 3 Days", 2, 'qpf_allday', 'mm', 'mm',
        "mdi:umbrella"),
    'precip_4d_mm': WUDailySimpleForecastSensorConfig(
        "Precipitation Intensity in 4 Days", 3, 'qpf_allday', 'mm', 'mm',
        "mdi:umbrella"),
    'precip_1d_in': WUDailySimpleForecastSensorConfig(
        "Precipitation Intensity Today", 0, 'qpf_allday', 'in',
        LENGTH_INCHES, "mdi:umbrella"),
    'precip_2d_in': WUDailySimpleForecastSensorConfig(
        "Precipitation Intensity Tomorrow", 1, 'qpf_allday', 'in',
        LENGTH_INCHES, "mdi:umbrella"),
    'precip_3d_in': WUDailySimpleForecastSensorConfig(
        "Precipitation Intensity in 3 Days", 2, 'qpf_allday', 'in',
        LENGTH_INCHES, "mdi:umbrella"),
    'precip_4d_in': WUDailySimpleForecastSensorConfig(
        "Precipitation Intensity in 4 Days", 3, 'qpf_allday', 'in',
        LENGTH_INCHES, "mdi:umbrella"),
    'precip_1d': WUDailySimpleForecastSensorConfig(
        "Precipitation Probability Today", 0, "pop", None, "%",
        "mdi:umbrella"),
    'precip_2d': WUDailySimpleForecastSensorConfig(
        "Precipitation Probability Tomorrow", 1, "pop", None, "%",
        "mdi:umbrella"),
    'precip_3d': WUDailySimpleForecastSensorConfig(
        "Precipitation Probability in 3 Days", 2, "pop", None, "%",
        "mdi:umbrella"),
    'precip_4d': WUDailySimpleForecastSensorConfig(
        "Precipitation Probability in 4 Days", 3, "pop", None, "%",
        "mdi:umbrella"),
}

# Alert Attributes
ALERTS_ATTRS = [
    'date',
    'description',
    'expires',
    'message',
]

# Language Supported Codes
LANG_CODES = [
    'AF', 'AL', 'AR', 'HY', 'AZ', 'EU',
    'BY', 'BU', 'LI', 'MY', 'CA', 'CN',
    'TW', 'CR', 'CZ', 'DK', 'DV', 'NL',
    'EN', 'EO', 'ET', 'FA', 'FI', 'FR',
    'FC', 'GZ', 'DL', 'KA', 'GR', 'GU',
    'HT', 'IL', 'HI', 'HU', 'IS', 'IO',
    'ID', 'IR', 'IT', 'JP', 'JW', 'KM',
    'KR', 'KU', 'LA', 'LV', 'LT', 'ND',
    'MK', 'MT', 'GM', 'MI', 'MR', 'MN',
    'NO', 'OC', 'PS', 'GN', 'PL', 'BR',
    'PA', 'RO', 'RU', 'SR', 'SK', 'SL',
    'SP', 'SI', 'SW', 'CH', 'TL', 'TT',
    'TH', 'TR', 'TK', 'UA', 'UZ', 'VU',
    'CY', 'SN', 'JI', 'YI',
]

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
    vol.Required(CONF_API_KEY): cv.string,
    vol.Optional(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)

    rest = WUndergroundData(
        hass, config.get(CONF_API_KEY), pws_id,
        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: 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('pws_' + 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, lang, latitude, longitude):
        """Initialize the data object."""
        self._hass = hass
        self._api_key = api_key
        self._pws_id = pws_id
        self._lang = 'lang:{}'.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=_RESOURCE):
        url = baseurl.format(
            self._api_key, '/'.join(sorted(self._features)), self._lang)
        if self._pws_id:
            url = url + 'pws:{}'.format(self._pws_id)
        else:
            url = url + '{},{}'.format(self._latitude, self._longitude)

        return url + '.json'

    @Throttle(MIN_TIME_BETWEEN_UPDATES)
    async def async_update(self):
        """Get the latest data from WUnderground."""
        try:
            with async_timeout.timeout(10):
                response = await self._session.get(self._build_url())
            result = await response.json()
            if "error" in result['response']:
                raise ValueError(result['response']["error"]["description"])
            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))
