From c8c074963727f7cbad9174f71fcdc27e30aa0712 Mon Sep 17 00:00:00 2001 From: Kevin Eifinger Date: Mon, 11 Nov 2019 19:06:31 +0100 Subject: [PATCH 01/56] wip here_weather --- .../components/here_weather/__init__.py | 1 + .../components/here_weather/manifest.json | 12 + .../components/here_weather/sensor.py | 263 ++++++++++++++++ .../components/here_weather/weather.py | 298 ++++++++++++++++++ 4 files changed, 574 insertions(+) create mode 100644 homeassistant/components/here_weather/__init__.py create mode 100644 homeassistant/components/here_weather/manifest.json create mode 100644 homeassistant/components/here_weather/sensor.py create mode 100644 homeassistant/components/here_weather/weather.py diff --git a/homeassistant/components/here_weather/__init__.py b/homeassistant/components/here_weather/__init__.py new file mode 100644 index 00000000000000..877939475ddd98 --- /dev/null +++ b/homeassistant/components/here_weather/__init__.py @@ -0,0 +1 @@ +"""The here_weather component.""" diff --git a/homeassistant/components/here_weather/manifest.json b/homeassistant/components/here_weather/manifest.json new file mode 100644 index 00000000000000..77a330fb673a6b --- /dev/null +++ b/homeassistant/components/here_weather/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "here_weather", + "name": "HERE Destination Weather", + "documentation": "https://www.home-assistant.io/components/here_weather", + "requirements": [ + "herepy==0.6.3.1" + ], + "dependencies": [], + "codeowners": [ + "@eifinger" + ] + } diff --git a/homeassistant/components/here_weather/sensor.py b/homeassistant/components/here_weather/sensor.py new file mode 100644 index 00000000000000..081432c089d4c6 --- /dev/null +++ b/homeassistant/components/here_weather/sensor.py @@ -0,0 +1,263 @@ +"""Support for the HERE Destination Weather service.""" +from datetime import timedelta +import logging +from typing import Callable, Dict, Optional, Union + +import herepy +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import ( + ATTR_ATTRIBUTION, + CONF_API_KEY, + CONF_MONITORED_CONDITIONS, + CONF_MODE, + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_NAME, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, +) +from homeassistant.core import HomeAssistant, State +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle + +_LOGGER = logging.getLogger(__name__) + +CONF_APP_ID = "app_id" +CONF_APP_CODE = "app_code" +CONF_LOCATION_NAME = "location_name" +CONF_ZIP_CODE = "zip_code" +CONF_FORECAST = "forecast" +CONF_LANGUAGE = "language" + +DEFAULT_NAME = "here_weather" + +MODE_ASTRONOMY = "astronomy" +DEFAULT_MODE = MODE_ASTRONOMY +CONF_MODES = [MODE_ASTRONOMY] + +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=120) + +ASTRONOMY_ATTRIBUTES = { + "sunrise": {"name": "Sunrise", "unit_of_measurement": None}, + "sunset": {"name": "Sunset", "unit_of_measurement": None}, + "moonrise": {"name": "Moonrise", "unit_of_measurement": None}, + "moonset": {"name": "Moonset", "unit_of_measurement": None}, + "moonPhase": {"name": "Moon Phase", "unit_of_measurement": "%"}, + "moonPhaseDesc": {"name": "Moon Phase Description", "unit_of_measurement": None}, + "city": {"name": "City", "unit_of_measurement": None}, + "latitude": {"name": "Latitude", "unit_of_measurement": None}, + "longitude": {"name": "Longitude", "unit_of_measurement": None}, + "utcTime": {"name": "Sunrise", "unit_of_measurement": "timestamp"}, +} + +SENSOR_TYPES = { + MODE_ASTRONOMY: ASTRONOMY_ATTRIBUTES +} + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_APP_ID): cv.string, + vol.Required(CONF_APP_CODE): cv.string, + vol.Inclusive(CONF_LATITUDE, "coordinates"): cv.latitude, + vol.Inclusive(CONF_LONGITUDE, "coordinates"): cv.longitude, + vol.Exclusive(CONF_LATITUDE, "coords_or_name_or_zip_code"): cv.latitude, + vol.Exclusive(CONF_LOCATION_NAME, "coords_or_name_or_zip_code"): cv.string, + vol.Exclusive(CONF_ZIP_CODE, "coords_or_name_or_zip_code"): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_MODE, default=DEFAULT_MODE): vol.In(CONF_MODES), + vol.Optional(CONF_LATITUDE): cv.latitude, + vol.Optional(CONF_LONGITUDE): cv.longitude, + vol.Optional(CONF_LOCATION_NAME): cv.string, + vol.Optional(CONF_ZIP_CODE): cv.string, + } +) + +UNIT_OF_MEASUREMENT = "unit_of_measurement" + + +async def async_setup_platform( + hass: HomeAssistant, + config: Dict[str, Union[str, bool]], + async_add_entities: Callable, + discovery_info: None = None, +) -> None: + """Set up the HERE Destination Weather sensor.""" + + if config.get(CONF_LOCATION_NAME) is None: + if config.get(CONF_ZIP_CODE) is None: + if config.get(CONF_LONGITUDE) is None: + if None in (hass.config.latitude, hass.config.longitude): + _LOGGER.error("Latitude or longitude not set in Home Assistant config") + return + + app_id = config[CONF_APP_ID] + app_code = config[CONF_APP_CODE] + + here_client = herepy.DestinationWeatherApi(app_id, app_code) + + if not await hass.async_add_executor_job( + _are_valid_client_credentials, here_client + ): + _LOGGER.error( + "Invalid credentials. This error is returned if the specified token was invalid or no contract could be found for this token." + ) + return + + # SENSOR_TYPES["temperature"][1] = hass.config.units.temperature_unit + + name = config.get(CONF_NAME) + mode = config[CONF_MODE] + + data = WeatherData(here_client, mode, hass.config.latitude, hass.config.longitude) + dev = [] + for sensor_type in SENSOR_TYPES: + if sensor_type is mode: + for attribute in SENSOR_TYPES[sensor_type]: + unit_of_measurement = SENSOR_TYPES[sensor_type][attribute][UNIT_OF_MEASUREMENT] + dev.append( + HEREDestinationWeatherSensor(name, data, sensor_type, attribute, unit_of_measurement) + ) + + async_add_entities(dev, True) + + def _are_valid_client_credentials(here_client: herepy.DestinationWeatherApi) -> bool: + """Check if the provided credentials are correct using defaults.""" + try: + product = herepy.WeatherProductType.forecast_astronomy + known_good_zip_code = "10025" + here_client.weather_for_zip_code(known_good_zip_code, product) + except herepy.UnauthorizedError: + return False + return True + + +class HEREDestinationWeatherSensor(Entity): + """Implementation of an HERE Destination Weather sensor.""" + + def __init__(self, name, weather_data, sensor_type, attribute, unit_of_measurement): + """Initialize the sensor.""" + self._client_name = name + self._name = SENSOR_TYPES[sensor_type][attribute]["name"] + self._here_data = weather_data + self._temp_unit = temp_unit + self._type = sensor_type + self._state = None + self._unit_of_measurement = SENSOR_TYPES[sensor_type][UNIT_OF_MEASUREMENT] + + @property + def name(self): + """Return the name of the sensor.""" + return f"{self._client_name} {self._name}" + + @property + def state(self): + """Return the state of the device.""" + return self._state + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity, if any.""" + return self._unit_of_measurement + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return {ATTR_ATTRIBUTION: ATTRIBUTION} + + def update(self): + """Get the latest data from OWM and updates the states.""" + try: + self.owa_client.update() + except APICallError: + _LOGGER.error("Error when calling API to update data") + return + + data = self.owa_client.data + fc_data = self.owa_client.fc_data + + if data is None: + return + + try: + if self.type == "weather": + self._state = data.get_detailed_status() + elif self.type == "temperature": + if self.temp_unit == TEMP_CELSIUS: + self._state = round(data.get_temperature("celsius")["temp"], 1) + elif self.temp_unit == TEMP_FAHRENHEIT: + self._state = round(data.get_temperature("fahrenheit")["temp"], 1) + else: + self._state = round(data.get_temperature()["temp"], 1) + elif self.type == "wind_speed": + self._state = round(data.get_wind()["speed"], 1) + elif self.type == "wind_bearing": + self._state = round(data.get_wind()["deg"], 1) + elif self.type == "humidity": + self._state = round(data.get_humidity(), 1) + elif self.type == "pressure": + self._state = round(data.get_pressure()["press"], 0) + elif self.type == "clouds": + self._state = data.get_clouds() + elif self.type == "rain": + if data.get_rain(): + self._state = round(data.get_rain()["3h"], 0) + self._unit_of_measurement = "mm" + else: + self._state = "not raining" + self._unit_of_measurement = "" + elif self.type == "snow": + if data.get_snow(): + self._state = round(data.get_snow(), 0) + self._unit_of_measurement = "mm" + else: + self._state = "not snowing" + self._unit_of_measurement = "" + elif self.type == "forecast": + if fc_data is None: + return + self._state = fc_data.get_weathers()[0].get_detailed_status() + elif self.type == "weather_code": + self._state = data.get_weather_code() + except KeyError: + self._state = None + _LOGGER.warning("Condition is currently not available: %s", self.type) + + +class WeatherData: + """Get the latest data from OpenWeatherMap.""" + + def __init__(self, here_client, forecast, latitude, longitude): + """Initialize the data object.""" + self.here_client = here_client + self.forecast = forecast + self.latitude = latitude + self.longitude = longitude + self.data = None + self.fc_data = None + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """Get the latest data from OpenWeatherMap.""" + try: + obs = self.owm.weather_at_coords(self.latitude, self.longitude) + except (APICallError, TypeError): + _LOGGER.error("Error when calling API to get weather at coordinates") + obs = None + + if obs is None: + _LOGGER.warning("Failed to fetch data") + return + + self.data = obs.get_weather() + + if self.forecast == 1: + try: + obs = self.owm.three_hours_forecast_at_coords( + self.latitude, self.longitude + ) + self.fc_data = obs.get_forecast() + except (ConnectionResetError, TypeError): + _LOGGER.warning("Failed to fetch forecast") diff --git a/homeassistant/components/here_weather/weather.py b/homeassistant/components/here_weather/weather.py new file mode 100644 index 00000000000000..3b8c7dac3debec --- /dev/null +++ b/homeassistant/components/here_weather/weather.py @@ -0,0 +1,298 @@ +"""Support for the HERE Destination Weather API.""" +from datetime import timedelta +import logging +from typing import Callable, Dict, Optional, Union + +import voluptuous as vol + +import herepy + +from homeassistant.components.weather import ( + ATTR_FORECAST_CONDITION, + ATTR_FORECAST_PRECIPITATION, + ATTR_FORECAST_TEMP, + ATTR_FORECAST_TEMP_LOW, + ATTR_FORECAST_TIME, + ATTR_FORECAST_WIND_BEARING, + ATTR_FORECAST_WIND_SPEED, + PLATFORM_SCHEMA, + WeatherEntity, +) +from homeassistant.const import ( + CONF_API_KEY, + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_MODE, + CONF_NAME, + PRESSURE_HPA, + PRESSURE_INHG, + STATE_UNKNOWN, + TEMP_CELSIUS, +) +import homeassistant.helpers.config_validation as cv +from homeassistant.util.pressure import convert as convert_pressure + +_LOGGER = logging.getLogger(__name__) + +CONF_APP_ID = "app_id" +CONF_APP_CODE = "app_code" +CONF_LOCATION_NAME = "location_name" + +FORECAST_MODE = ["hourly", "daily"] + +DEFAULT_NAME = "HERE Destination Weather" + +MIN_TIME_BETWEEN_FORECAST_UPDATES = timedelta(minutes=30) +MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=10) + +CONDITION_CLASSES = { + "cloudy": [803, 804], + "fog": [701, 741], + "hail": [906], + "lightning": [210, 211, 212, 221], + "lightning-rainy": [200, 201, 202, 230, 231, 232], + "partlycloudy": [801, 802], + "pouring": [504, 314, 502, 503, 522], + "rainy": [300, 301, 302, 310, 311, 312, 313, 500, 501, 520, 521], + "snowy": [600, 601, 602, 611, 612, 620, 621, 622], + "snowy-rainy": [511, 615, 616], + "sunny": [800], + "windy": [905, 951, 952, 953, 954, 955, 956, 957], + "windy-variant": [958, 959, 960, 961], + "exceptional": [711, 721, 731, 751, 761, 762, 771, 900, 901, 962, 903, 904], +} + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_APP_ID): cv.string, + vol.Required(CONF_APP_CODE): cv.string, + vol.Inclusive(CONF_LATITUDE, "coordinates"): cv.latitude, + vol.Inclusive(CONF_LONGITUDE, "coordinates"): cv.longitude, + vol.Exclusive(CONF_LATITUDE, "coords_or_name"): cv.latitude, + vol.Exclusive(CONF_LOCATION_NAME, "coords_or_name"): cv.entity_id, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_LATITUDE): cv.latitude, + vol.Optional(CONF_LONGITUDE): cv.longitude, + vol.Optional(CONF_MODE, default="hourly"): vol.In(FORECAST_MODE), + } +) + + +async def async_setup_platform( + hass: HomeAssistant, + config: Dict[str, Union[str, bool]], + async_add_entities: Callable, + discovery_info: None = None, +) -> None: + """Set up the HERE Destination weather platform.""" + + longitude = config.get(CONF_LONGITUDE, hass.config.longitude) + latitude = config.get(CONF_LATITUDE) + name = config[CONF_NAME] + mode = config[CONF_MODE] + + try: + owm = pyowm.OWM(config.get(CONF_API_KEY)) + except pyowm.exceptions.api_call_error.APICallError: + _LOGGER.error("Error while connecting to OpenWeatherMap") + return False + + data = WeatherData(owm, latitude, longitude, mode) + + add_entities( + [OpenWeatherMapWeather(name, data, hass.config.units.temperature_unit, mode)], + True, + ) + + +class OpenWeatherMapWeather(WeatherEntity): + """Implementation of an OpenWeatherMap sensor.""" + + def __init__(self, name, owm, temperature_unit, mode): + """Initialize the sensor.""" + self._name = name + self._owm = owm + self._temperature_unit = temperature_unit + self._mode = mode + self.data = None + self.forecast_data = None + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def condition(self): + """Return the current condition.""" + try: + return [ + k + for k, v in CONDITION_CLASSES.items() + if self.data.get_weather_code() in v + ][0] + except IndexError: + return STATE_UNKNOWN + + @property + def temperature(self): + """Return the temperature.""" + return self.data.get_temperature("celsius").get("temp") + + @property + def temperature_unit(self): + """Return the unit of measurement.""" + return TEMP_CELSIUS + + @property + def pressure(self): + """Return the pressure.""" + pressure = self.data.get_pressure().get("press") + if self.hass.config.units.name == "imperial": + return round(convert_pressure(pressure, PRESSURE_HPA, PRESSURE_INHG), 2) + return pressure + + @property + def humidity(self): + """Return the humidity.""" + return self.data.get_humidity() + + @property + def wind_speed(self): + """Return the wind speed.""" + if self.hass.config.units.name == "imperial": + return round(self.data.get_wind().get("speed") * 2.24, 2) + + return round(self.data.get_wind().get("speed") * 3.6, 2) + + @property + def wind_bearing(self): + """Return the wind bearing.""" + return self.data.get_wind().get("deg") + + @property + def attribution(self): + """Return the attribution.""" + return ATTRIBUTION + + @property + def forecast(self): + """Return the forecast array.""" + data = [] + + def calc_precipitation(rain, snow): + """Calculate the precipitation.""" + rain_value = 0 if rain is None else rain + snow_value = 0 if snow is None else snow + if round(rain_value + snow_value, 1) == 0: + return None + return round(rain_value + snow_value, 1) + + if self._mode == "freedaily": + weather = self.forecast_data.get_weathers()[::8] + else: + weather = self.forecast_data.get_weathers() + + for entry in weather: + if self._mode == "daily": + data.append( + { + ATTR_FORECAST_TIME: entry.get_reference_time("unix") * 1000, + ATTR_FORECAST_TEMP: entry.get_temperature("celsius").get("day"), + ATTR_FORECAST_TEMP_LOW: entry.get_temperature("celsius").get( + "night" + ), + ATTR_FORECAST_PRECIPITATION: calc_precipitation( + entry.get_rain().get("all"), entry.get_snow().get("all") + ), + ATTR_FORECAST_WIND_SPEED: entry.get_wind().get("speed"), + ATTR_FORECAST_WIND_BEARING: entry.get_wind().get("deg"), + ATTR_FORECAST_CONDITION: [ + k + for k, v in CONDITION_CLASSES.items() + if entry.get_weather_code() in v + ][0], + } + ) + else: + data.append( + { + ATTR_FORECAST_TIME: entry.get_reference_time("unix") * 1000, + ATTR_FORECAST_TEMP: entry.get_temperature("celsius").get( + "temp" + ), + ATTR_FORECAST_PRECIPITATION: ( + round(entry.get_rain().get("3h"), 1) + if entry.get_rain().get("3h") is not None + and (round(entry.get_rain().get("3h"), 1) > 0) + else None + ), + ATTR_FORECAST_CONDITION: [ + k + for k, v in CONDITION_CLASSES.items() + if entry.get_weather_code() in v + ][0], + } + ) + return data + + def update(self): + """Get the latest data from OWM and updates the states.""" + from pyowm.exceptions.api_call_error import APICallError + + try: + self._owm.update() + self._owm.update_forecast() + except APICallError: + _LOGGER.error("Exception when calling OWM web API to update data") + return + + self.data = self._owm.data + self.forecast_data = self._owm.forecast_data + + +class WeatherData: + """Get the latest data from OpenWeatherMap.""" + + def __init__(self, owm, latitude, longitude, mode): + """Initialize the data object.""" + self._mode = mode + self.owm = owm + self.latitude = latitude + self.longitude = longitude + self.data = None + self.forecast_data = None + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """Get the latest data from OpenWeatherMap.""" + obs = self.owm.weather_at_coords(self.latitude, self.longitude) + if obs is None: + _LOGGER.warning("Failed to fetch data from OWM") + return + + self.data = obs.get_weather() + + @Throttle(MIN_TIME_BETWEEN_FORECAST_UPDATES) + def update_forecast(self): + """Get the latest forecast from OpenWeatherMap.""" + from pyowm.exceptions.api_call_error import APICallError + + try: + if self._mode == "daily": + fcd = self.owm.daily_forecast_at_coords( + self.latitude, self.longitude, 15 + ) + else: + fcd = self.owm.three_hours_forecast_at_coords( + self.latitude, self.longitude + ) + except APICallError: + _LOGGER.error("Exception when calling OWM web API " "to update forecast") + return + + if fcd is None: + _LOGGER.warning("Failed to fetch forecast data from OWM") + return + + self.forecast_data = fcd.get_forecast() From 31e7c2ecd5d6e32d7b8bc588eb038d42354e6529 Mon Sep 17 00:00:00 2001 From: Kevin Eifinger Date: Wed, 20 Nov 2019 21:30:57 +0100 Subject: [PATCH 02/56] first full version of here_weather --- .../components/here_weather/__init__.py | 119 + .../components/here_weather/const.py | 403 ++ .../components/here_weather/manifest.json | 2 +- .../components/here_weather/sensor.py | 260 +- .../components/here_weather/weather.py | 351 +- tests/components/here_weather/__init__.py | 56 + tests/components/here_weather/test_sensor.py | 315 + tests/components/here_weather/test_weather.py | 304 + ...ination_weather_error_invalid_request.json | 6 + ...estination_weather_error_unauthorized.json | 6 + .../destination_weather_forecasts.json | 761 +++ ...stination_weather_forecasts_astronomy.json | 118 + .../destination_weather_forecasts_hourly.json | 5057 +++++++++++++++++ .../destination_weather_forecasts_simple.json | 248 + ...tination_weather_observation_imperial.json | 61 + 15 files changed, 7701 insertions(+), 366 deletions(-) create mode 100644 homeassistant/components/here_weather/const.py create mode 100644 tests/components/here_weather/__init__.py create mode 100644 tests/components/here_weather/test_sensor.py create mode 100644 tests/components/here_weather/test_weather.py create mode 100644 tests/fixtures/here_weather/destination_weather_error_invalid_request.json create mode 100644 tests/fixtures/here_weather/destination_weather_error_unauthorized.json create mode 100644 tests/fixtures/here_weather/destination_weather_forecasts.json create mode 100644 tests/fixtures/here_weather/destination_weather_forecasts_astronomy.json create mode 100644 tests/fixtures/here_weather/destination_weather_forecasts_hourly.json create mode 100644 tests/fixtures/here_weather/destination_weather_forecasts_simple.json create mode 100644 tests/fixtures/here_weather/destination_weather_observation_imperial.json diff --git a/homeassistant/components/here_weather/__init__.py b/homeassistant/components/here_weather/__init__.py index 877939475ddd98..1c5d7eaf52ada5 100644 --- a/homeassistant/components/here_weather/__init__.py +++ b/homeassistant/components/here_weather/__init__.py @@ -1 +1,120 @@ """The here_weather component.""" +from datetime import timedelta +import logging + +import herepy + +from homeassistant.const import CONF_UNIT_SYSTEM_METRIC +from homeassistant.util import Throttle + +_LOGGER = logging.getLogger(__name__) + +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=120) + + +class HEREWeatherData: + """Get the latest data from HERE.""" + + def __init__( + self, + here_client: herepy.DestinationWeatherApi, + mode: str, + units: str, + latitude: str = None, + longitude: str = None, + location_name: str = None, + zip_code: str = None, + ) -> None: + """Initialize the data object.""" + self.here_client = here_client + self.latitude = latitude + self.longitude = longitude + self.location_name = location_name + self.zip_code = zip_code + self.weather_product_type = herepy.WeatherProductType[mode] + self.units = units + self.data = None + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self) -> None: + """Get the latest data from HERE.""" + is_metric = convert_units_to_boolean(self.units) + try: + if self.zip_code is not None: + data = self.here_client.weather_for_zip_code( + self.zip_code, self.weather_product_type, metric=is_metric + ) + elif self.location_name is not None: + data = self.here_client.weather_for_location_name( + self.location_name, self.weather_product_type, metric=is_metric + ) + else: + data = self.here_client.weather_for_coordinates( + self.latitude, + self.longitude, + self.weather_product_type, + metric=is_metric, + ) + self.data = extract_data_from_payload_for_product_type( + data, self.weather_product_type + ) + except herepy.InvalidRequestError as error: + _LOGGER.error("Error during sensor update: %s", error.message) + + +def get_attribute_from_here_data( + here_data: list, attribute_name: str, sensor_number: int = 0 +) -> str: + """Extract and convert data from HERE response or None if not found.""" + if here_data is None: + return None + try: + state = here_data[sensor_number][attribute_name] + state = convert_asterisk_to_none(state) + return state + except KeyError: + return None + + +def convert_asterisk_to_none(state: str) -> str: + """Convert HERE API representation of None.""" + if state == "*": + state = None + return state + + +def convert_units_to_boolean(units: str) -> bool: + """Convert metric/imperial to true/false.""" + return bool(units == CONF_UNIT_SYSTEM_METRIC) + + +def extract_data_from_payload_for_product_type( + data: herepy.DestinationWeatherResponse, product_type: herepy.WeatherProductType +) -> list: + """Extract the actual data from the HERE payload.""" + if product_type == herepy.WeatherProductType.forecast_astronomy: + return data.astronomy["astronomy"] + if product_type == herepy.WeatherProductType.observation: + return data.observations["location"][0]["observation"] + if product_type == herepy.WeatherProductType.forecast_7days: + return data.forecasts["forecastLocation"]["forecast"] + if product_type == herepy.WeatherProductType.forecast_7days_simple: + return data.dailyForecasts["forecastLocation"]["forecast"] + if product_type == herepy.WeatherProductType.forecast_hourly: + return data.hourlyForecasts["forecastLocation"]["forecast"] + + +def convert_unit_of_measurement_if_needed(unit_system, unit_of_measurement: str) -> str: + """Convert the unit of measurement to imperial if configured.""" + if unit_system != CONF_UNIT_SYSTEM_METRIC: + if unit_of_measurement == "°C": + unit_of_measurement = "°F" + elif unit_of_measurement == "cm": + unit_of_measurement = "in" + elif unit_of_measurement == "km/h": + unit_of_measurement = "mph" + elif unit_of_measurement == "mbar": + unit_of_measurement = "in" + elif unit_of_measurement == "km": + unit_of_measurement = "mi" + return unit_of_measurement diff --git a/homeassistant/components/here_weather/const.py b/homeassistant/components/here_weather/const.py new file mode 100644 index 00000000000000..8b8abf8bb13d26 --- /dev/null +++ b/homeassistant/components/here_weather/const.py @@ -0,0 +1,403 @@ +"""Constants for the HERE Destination Weather service.""" +CONF_APP_ID = "app_id" +CONF_APP_CODE = "app_code" +CONF_LOCATION_NAME = "location_name" +CONF_ZIP_CODE = "zip_code" +CONF_LANGUAGE = "language" +CONF_OFFSET = "offset" + +DEFAULT_NAME = "here_weather" + +MODE_ASTRONOMY = "forecast_astronomy" +MODE_HOURLY = "forecast_hourly" +MODE_DAILY = "forecast_7days" +MODE_DAILY_SIMPLE = "forecast_7days_simple" +MODE_OBSERVATION = "observation" +CONF_MODES = [ + MODE_ASTRONOMY, + MODE_HOURLY, + MODE_DAILY, + MODE_DAILY_SIMPLE, + MODE_OBSERVATION, +] +DEFAULT_MODE = MODE_DAILY_SIMPLE + +ASTRONOMY_ATTRIBUTES = { + "sunrise": {"name": "Sunrise", "unit_of_measurement": None}, + "sunset": {"name": "Sunset", "unit_of_measurement": None}, + "moonrise": {"name": "Moonrise", "unit_of_measurement": None}, + "moonset": {"name": "Moonset", "unit_of_measurement": None}, + "moonPhase": {"name": "Moon Phase", "unit_of_measurement": "%"}, + "moonPhaseDesc": {"name": "Moon Phase Description", "unit_of_measurement": None}, + "city": {"name": "City", "unit_of_measurement": None}, + "latitude": {"name": "Latitude", "unit_of_measurement": None}, + "longitude": {"name": "Longitude", "unit_of_measurement": None}, + "utcTime": {"name": "Sunrise", "unit_of_measurement": "timestamp"}, +} + +HOURLY_ATTRIBUTES = { + "daylight": {"name": "Daylight", "unit_of_measurement": None}, + "description": {"name": "Description", "unit_of_measurement": None}, + "skyInfo": {"name": "Sky Info", "unit_of_measurement": None}, + "skyDescription": {"name": "Sky Description", "unit_of_measurement": None}, + "temperature": {"name": "Temperature", "unit_of_measurement": "°C"}, + "temperatureDesc": {"name": "Temperature Description", "unit_of_measurement": None}, + "comfort": {"name": "Comfort", "unit_of_measurement": "°C"}, + "humidity": {"name": "Humidity", "unit_of_measurement": "%"}, + "dewPoint": {"name": "Dew Point", "unit_of_measurement": "°C"}, + "precipitationProbability": { + "name": "Precipitation Probability", + "unit_of_measurement": "%", + }, + "precipitationDesc": { + "name": "Precipitation Description", + "unit_of_measurement": None, + }, + "rainFall": {"name": "Rain Fall", "unit_of_measurement": "cm"}, + "snowFall": {"name": "Snow Fall", "unit_of_measurement": "cm"}, + "airInfo": {"name": "Air Info", "unit_of_measurement": None}, + "airDescription": {"name": "Air Description", "unit_of_measurement": None}, + "windSpeed": {"name": "Wind Speed", "unit_of_measurement": "km/h"}, + "windDirection": {"name": "Wind Direction", "unit_of_measurement": "°"}, + "windDesc": {"name": "Wind Description", "unit_of_measurement": "cm"}, + "windDescShort": {"name": "Wind Description Short", "unit_of_measurement": "cm"}, + "visibility": {"name": "Visibility", "unit_of_measurement": "km"}, + "icon": {"name": "Icon", "unit_of_measurement": None}, + "iconName": {"name": "Icon Name", "unit_of_measurement": None}, + "iconLink": {"name": "Icon Link", "unit_of_measurement": None}, + "dayOfWeek": {"name": "Day of Week", "unit_of_measurement": None}, + "weekday": {"name": "Week Day", "unit_of_measurement": None}, + "utcTime": {"name": "UTC Time", "unit_of_measurement": "timestamp"}, + "localTime": {"name": "Local Time", "unit_of_measurement": None}, + "localTimeFormat": {"name": "Local Time Format", "unit_of_measurement": None}, +} + +DAILY_SIMPLE_ATTRIBUTES = { + "daylight": {"name": "Daylight", "unit_of_measurement": None}, + "description": {"name": "Description", "unit_of_measurement": None}, + "skyInfo": {"name": "Sky Info", "unit_of_measurement": None}, + "skyDescription": {"name": "Sky Description", "unit_of_measurement": None}, + "temperatureDesc": {"name": "Temperature Description", "unit_of_measurement": None}, + "comfort": {"name": "Comfort", "unit_of_measurement": "°C"}, + "highTemperature": {"name": "High Temperature", "unit_of_measurement": "°C"}, + "lowTemperature": {"name": "Low Temperature", "unit_of_measurement": "°C"}, + "humidity": {"name": "Humidity", "unit_of_measurement": "%"}, + "dewPoint": {"name": "Dew Point", "unit_of_measurement": "°C"}, + "precipitationProbability": { + "name": "Precipitation Probability", + "unit_of_measurement": "%", + }, + "precipitationDesc": { + "name": "Precipitation Description", + "unit_of_measurement": None, + }, + "rainFall": {"name": "Rain Fall", "unit_of_measurement": "cm"}, + "snowFall": {"name": "Snow Fall", "unit_of_measurement": "cm"}, + "airInfo": {"name": "Air Info", "unit_of_measurement": None}, + "airDescription": {"name": "Air Description", "unit_of_measurement": None}, + "windSpeed": {"name": "Wind Speed", "unit_of_measurement": "km/h"}, + "windDirection": {"name": "Wind Direction", "unit_of_measurement": "°"}, + "windDesc": {"name": "Wind Description", "unit_of_measurement": "cm"}, + "windDescShort": {"name": "Wind Description Short", "unit_of_measurement": "cm"}, + "beaufortScale": {"name": "Beaufort Scale", "unit_of_measurement": None}, + "beaufortDescription": { + "name": "Beaufort Scale Description", + "unit_of_measurement": None, + }, + "uvIndex": {"name": "UV Index", "unit_of_measurement": None}, + "uvDesc": {"name": "UV Index Description", "unit_of_measurement": None}, + "barometerPressure": {"name": "Barometric Pressure", "unit_of_measurement": "mbar"}, + "icon": {"name": "Icon", "unit_of_measurement": None}, + "iconName": {"name": "Icon Name", "unit_of_measurement": None}, + "iconLink": {"name": "Icon Link", "unit_of_measurement": None}, + "dayOfWeek": {"name": "Day of Week", "unit_of_measurement": None}, + "weekday": {"name": "Week Day", "unit_of_measurement": None}, + "utcTime": {"name": "UTC Time", "unit_of_measurement": "timestamp"}, +} + +DAILY_ATTRIBUTES = { + "daylight": {"name": "Daylight", "unit_of_measurement": None}, + "daySegment": {"name": "Day Segment", "unit_of_measurement": None}, + "description": {"name": "Description", "unit_of_measurement": None}, + "skyInfo": {"name": "Sky Info", "unit_of_measurement": None}, + "skyDescription": {"name": "Sky Description", "unit_of_measurement": None}, + "temperature": {"name": "Temperature", "unit_of_measurement": "°C"}, + "temperatureDesc": {"name": "Temperature Description", "unit_of_measurement": None}, + "comfort": {"name": "Comfort", "unit_of_measurement": "°C"}, + "humidity": {"name": "Humidity", "unit_of_measurement": "%"}, + "dewPoint": {"name": "Dew Point", "unit_of_measurement": "°C"}, + "precipitationProbability": { + "name": "Precipitation Probability", + "unit_of_measurement": "%", + }, + "precipitationDesc": { + "name": "Precipitation Description", + "unit_of_measurement": None, + }, + "rainFall": {"name": "Rain Fall", "unit_of_measurement": "cm"}, + "snowFall": {"name": "Snow Fall", "unit_of_measurement": "cm"}, + "airInfo": {"name": "Air Info", "unit_of_measurement": None}, + "airDescription": {"name": "Air Description", "unit_of_measurement": None}, + "windSpeed": {"name": "Wind Speed", "unit_of_measurement": "km/h"}, + "windDirection": {"name": "Wind Direction", "unit_of_measurement": "°"}, + "windDesc": {"name": "Wind Description", "unit_of_measurement": "cm"}, + "windDescShort": {"name": "Wind Description Short", "unit_of_measurement": "cm"}, + "beaufortScale": {"name": "Beaufort Scale", "unit_of_measurement": None}, + "beaufortDescription": { + "name": "Beaufort Scale Description", + "unit_of_measurement": None, + }, + "visibility": {"name": "Visibility", "unit_of_measurement": "km"}, + "icon": {"name": "Icon", "unit_of_measurement": None}, + "iconName": {"name": "Icon Name", "unit_of_measurement": None}, + "iconLink": {"name": "Icon Link", "unit_of_measurement": None}, + "dayOfWeek": {"name": "Day of Week", "unit_of_measurement": None}, + "weekday": {"name": "Week Day", "unit_of_measurement": None}, + "utcTime": {"name": "UTC Time", "unit_of_measurement": "timestamp"}, +} + +OBSERVATION_ATTRIBUTES = { + "daylight": {"name": "Daylight", "unit_of_measurement": None}, + "description": {"name": "Description", "unit_of_measurement": None}, + "skyInfo": {"name": "Sky Info", "unit_of_measurement": None}, + "skyDescription": {"name": "Sky Description", "unit_of_measurement": None}, + "temperature": {"name": "Temperature", "unit_of_measurement": "°C"}, + "temperatureDesc": {"name": "Temperature Description", "unit_of_measurement": None}, + "comfort": {"name": "Comfort", "unit_of_measurement": "°C"}, + "highTemperature": {"name": "High Temperature", "unit_of_measurement": "°C"}, + "lowTemperature": {"name": "Low Temperature", "unit_of_measurement": "°C"}, + "humidity": {"name": "Humidity", "unit_of_measurement": "%"}, + "dewPoint": {"name": "Dew Point", "unit_of_measurement": "°C"}, + "precipitation1H": { + "name": "Precipitation Over 1 Hour", + "unit_of_measurement": "cm", + }, + "precipitation3H": { + "name": "Precipitation Over 3 Hours", + "unit_of_measurement": "cm", + }, + "precipitation6H": { + "name": "Precipitation Over 6 Hours", + "unit_of_measurement": "cm", + }, + "precipitation12H": { + "name": "Precipitation Over 12 Hours", + "unit_of_measurement": "cm", + }, + "precipitation24H": { + "name": "Precipitation Over 24 Hours", + "unit_of_measurement": "cm", + }, + "precipitationDesc": { + "name": "Precipitation Description", + "unit_of_measurement": None, + }, + "airInfo": {"name": "Air Info", "unit_of_measurement": None}, + "airDescription": {"name": "Air Description", "unit_of_measurement": None}, + "windSpeed": {"name": "Wind Speed", "unit_of_measurement": "km/h"}, + "windDirection": {"name": "Wind Direction", "unit_of_measurement": "°"}, + "windDesc": {"name": "Wind Description", "unit_of_measurement": "cm"}, + "windDescShort": {"name": "Wind Description Short", "unit_of_measurement": "cm"}, + "barometerPressure": {"name": "Barometric Pressure", "unit_of_measurement": "mbar"}, + "barometerTrend": { + "name": "Barometric Pressure Trend", + "unit_of_measurement": None, + }, + "visibility": {"name": "Visibility", "unit_of_measurement": "km"}, + "snowCover": {"name": "Snow Cover", "unit_of_measurement": "cm"}, + "icon": {"name": "Icon", "unit_of_measurement": None}, + "iconName": {"name": "Icon Name", "unit_of_measurement": None}, + "iconLink": {"name": "Icon Link", "unit_of_measurement": None}, + "ageMinutes": {"name": "Age In Minutes", "unit_of_measurement": "min"}, + "activeAlerts": {"name": "Active Alerts", "unit_of_measurement": None}, + "country": {"name": "Country", "unit_of_measurement": None}, + "state": {"name": "State", "unit_of_measurement": None}, + "city": {"name": "City", "unit_of_measurement": None}, + "latitude": {"name": "Latitude", "unit_of_measurement": None}, + "longitude": {"name": "Longitude", "unit_of_measurement": None}, + "distance": {"name": "Distance", "unit_of_measurement": "km"}, + "elevation": {"name": "Elevation", "unit_of_measurement": "km"}, + "utcTime": {"name": "UTC Time", "unit_of_measurement": "timestamp"}, +} + +SENSOR_TYPES = { + MODE_ASTRONOMY: ASTRONOMY_ATTRIBUTES, + MODE_HOURLY: HOURLY_ATTRIBUTES, + MODE_DAILY_SIMPLE: DAILY_SIMPLE_ATTRIBUTES, + MODE_DAILY: DAILY_ATTRIBUTES, + MODE_OBSERVATION: OBSERVATION_ATTRIBUTES, +} + +CONDITION_CLASSES = { + "clear-night": [ + "night_passing_clouds", + "night_mostly_clear", + "night_clearing_skies", + "night_clear", + ], + "cloudy": [ + "night_decreasing_cloudiness", + "night_mostly_cloudy", + "night_morning_clouds", + "night_afternoon_clouds", + "night_high_clouds", + "night_high_level_clouds", + "low_clouds", + "overcast", + "cloudy", + "afternoon_clouds", + "morning_clouds", + "high_level_clouds", + "high_clouds", + ], + "fog": [ + "night_low_level_haze", + "night_smoke", + "night_haze", + "dense_fog", + "fog", + "light_fog", + "early_fog", + "early_fog_followed_by_sunny_skies", + "low_level_haze", + "smoke", + "haze", + "ice_fog", + "hazy_sunshine", + ], + "hail": ["hail"], + "lightning": [ + "night_tstorms", + "night_scattered_tstorms", + "scattered_tstorms", + "night_a_few_tstorms", + "night_isolated_tstorms", + "night_widely_scattered_tstorms", + "tstorms_early", + "isolated_tstorms_late", + "scattered_tstorms_late", + "widely_scattered_tstorms", + "isolated_tstorms", + "a_few_tstorms", + ], + "lightning-rainy": [ + "strong_thunderstorms", + "severe_thunderstorms", + "thundershowers", + "thunderstorms", + "tstorms_late", + "tstorms", + ], + "partlycloudy": [ + "partly_sunny", + "mostly_cloudy", + "broken_clouds", + "more_clouds_than_sun", + "night_broken_clouds", + "increasing_cloudiness", + "night_partly_cloudy", + "night_scattered_clouds", + "passing_clounds", + "more_sun_than_clouds", + "scattered_clouds", + "partly_cloudy", + "a_mixture_of_sun_and_clouds", + "increasing_cloudiness", + "decreasing_cloudiness", + "clearing_skies", + "breaks_of_sun_late", + ], + "pouring": [ + "heavy_rain_late", + "heavy_rain_early", + "tons_of_rain", + "lots_of_rain", + "heavy_rain", + "heavy_rain_early", + "heavy_rain_early", + ], + "rainy": [ + "rain_late", + "showers_late", + "rain_early", + "showery", + "showers_early", + "numerous_showers", + "rain", + "light_rain_late", + "sprinkles_late", + "light_rain_early", + "sprinkles_early", + "light_rain", + "sprinkles", + "drizzle", + "night_showers", + "night_sprinkles", + "night_rain_showers", + "night_passing_showers", + "night_light_showers", + "night_a_few_showers", + "night_scattered_showers", + "rain_early", + "scattered_showers", + "a_few_showers", + "light_showers", + "passing_showers", + "rain_showers", + "showers", + ], + "snowy": [ + "heavy_snow_late", + "heavy_snow_early", + "heavy_snow", + "snow_late", + "snow_early", + "moderate_snow", + "snow", + "light_snow_late", + "snow_showers_late", + "flurries_late", + "light_snow_early", + "flurries_early", + "light_snow", + "snow_flurries", + "sleet", + "an_icy_mix_changing_to_snow", + "an_icy_mix_changing_to_rain", + "rain_changing_to_snow", + "icy_mix_early", + "light_icy_mix_late", + "icy_mix_late", + "scattered_flurries", + ], + "snowy-rainy": [ + "freezing_rain", + "light_freezing_rain", + "snow_showers_early", + "snow_showers", + "light_snow_showers", + "snow_rain_mix", + "light_icy_mix_early", + "rain_changing_to_an_icy_mix", + "snow_changing_to_an_icy_mix", + "snow_changing_to_rain", + "heavy_mixture_of_precip", + "light_mixture_of_precip", + "icy_mix", + "mixture_of_precip", + ], + "sunny": ["sunny", "clear", "mostly_sunny", "mostly_clear"], + "windy": [], + "windy-variant": [], + "exceptional": [ + "blizzard", + "snowstorm", + "duststorm", + "sandstorm", + "hurricane", + "tropical_storm", + "flood", + "flash_floods", + "tornado", + ], +} diff --git a/homeassistant/components/here_weather/manifest.json b/homeassistant/components/here_weather/manifest.json index 77a330fb673a6b..790882a37109ba 100644 --- a/homeassistant/components/here_weather/manifest.json +++ b/homeassistant/components/here_weather/manifest.json @@ -3,7 +3,7 @@ "name": "HERE Destination Weather", "documentation": "https://www.home-assistant.io/components/here_weather", "requirements": [ - "herepy==0.6.3.1" + "herepy==0.6.3.3" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/here_weather/sensor.py b/homeassistant/components/here_weather/sensor.py index 081432c089d4c6..58cafe87a76508 100644 --- a/homeassistant/components/here_weather/sensor.py +++ b/homeassistant/components/here_weather/sensor.py @@ -8,55 +8,41 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - ATTR_ATTRIBUTION, - CONF_API_KEY, - CONF_MONITORED_CONDITIONS, - CONF_MODE, CONF_LATITUDE, CONF_LONGITUDE, + CONF_MODE, CONF_NAME, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, + CONF_UNIT_SYSTEM, + CONF_UNIT_SYSTEM_IMPERIAL, + CONF_UNIT_SYSTEM_METRIC, ) -from homeassistant.core import HomeAssistant, State +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -from homeassistant.util import Throttle - -_LOGGER = logging.getLogger(__name__) -CONF_APP_ID = "app_id" -CONF_APP_CODE = "app_code" -CONF_LOCATION_NAME = "location_name" -CONF_ZIP_CODE = "zip_code" -CONF_FORECAST = "forecast" -CONF_LANGUAGE = "language" +from . import ( + HEREWeatherData, + convert_unit_of_measurement_if_needed, + get_attribute_from_here_data, +) +from .const import ( + CONF_APP_CODE, + CONF_APP_ID, + CONF_LOCATION_NAME, + CONF_MODES, + CONF_OFFSET, + CONF_ZIP_CODE, + DEFAULT_MODE, + DEFAULT_NAME, + SENSOR_TYPES, +) -DEFAULT_NAME = "here_weather" +UNITS = [CONF_UNIT_SYSTEM_METRIC, CONF_UNIT_SYSTEM_IMPERIAL] -MODE_ASTRONOMY = "astronomy" -DEFAULT_MODE = MODE_ASTRONOMY -CONF_MODES = [MODE_ASTRONOMY] +_LOGGER = logging.getLogger(__name__) MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=120) -ASTRONOMY_ATTRIBUTES = { - "sunrise": {"name": "Sunrise", "unit_of_measurement": None}, - "sunset": {"name": "Sunset", "unit_of_measurement": None}, - "moonrise": {"name": "Moonrise", "unit_of_measurement": None}, - "moonset": {"name": "Moonset", "unit_of_measurement": None}, - "moonPhase": {"name": "Moon Phase", "unit_of_measurement": "%"}, - "moonPhaseDesc": {"name": "Moon Phase Description", "unit_of_measurement": None}, - "city": {"name": "City", "unit_of_measurement": None}, - "latitude": {"name": "Latitude", "unit_of_measurement": None}, - "longitude": {"name": "Longitude", "unit_of_measurement": None}, - "utcTime": {"name": "Sunrise", "unit_of_measurement": "timestamp"}, -} - -SENSOR_TYPES = { - MODE_ASTRONOMY: ASTRONOMY_ATTRIBUTES -} - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_APP_ID): cv.string, @@ -66,17 +52,17 @@ vol.Exclusive(CONF_LATITUDE, "coords_or_name_or_zip_code"): cv.latitude, vol.Exclusive(CONF_LOCATION_NAME, "coords_or_name_or_zip_code"): cv.string, vol.Exclusive(CONF_ZIP_CODE, "coords_or_name_or_zip_code"): cv.string, + vol.Optional(CONF_OFFSET, default=0): cv.positive_int, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_MODE, default=DEFAULT_MODE): vol.In(CONF_MODES), vol.Optional(CONF_LATITUDE): cv.latitude, vol.Optional(CONF_LONGITUDE): cv.longitude, vol.Optional(CONF_LOCATION_NAME): cv.string, vol.Optional(CONF_ZIP_CODE): cv.string, + vol.Optional(CONF_UNIT_SYSTEM): vol.In(UNITS), } ) -UNIT_OF_MEASUREMENT = "unit_of_measurement" - async def async_setup_platform( hass: HomeAssistant, @@ -85,14 +71,6 @@ async def async_setup_platform( discovery_info: None = None, ) -> None: """Set up the HERE Destination Weather sensor.""" - - if config.get(CONF_LOCATION_NAME) is None: - if config.get(CONF_ZIP_CODE) is None: - if config.get(CONF_LONGITUDE) is None: - if None in (hass.config.latitude, hass.config.longitude): - _LOGGER.error("Latitude or longitude not set in Home Assistant config") - return - app_id = config[CONF_APP_ID] app_code = config[CONF_APP_CODE] @@ -106,158 +84,88 @@ async def async_setup_platform( ) return - # SENSOR_TYPES["temperature"][1] = hass.config.units.temperature_unit - name = config.get(CONF_NAME) mode = config[CONF_MODE] - - data = WeatherData(here_client, mode, hass.config.latitude, hass.config.longitude) - dev = [] + offset = config[CONF_OFFSET] + latitude = config.get(CONF_LATITUDE, hass.config.latitude) + longitude = config.get(CONF_LONGITUDE, hass.config.longitude) + location_name = config.get(CONF_LOCATION_NAME) + zip_code = config.get(CONF_ZIP_CODE) + units = config.get(CONF_UNIT_SYSTEM, hass.config.units.name) + + here_data = HEREWeatherData( + here_client, mode, units, latitude, longitude, location_name, zip_code + ) + sensors_to_add = [] for sensor_type in SENSOR_TYPES: - if sensor_type is mode: - for attribute in SENSOR_TYPES[sensor_type]: - unit_of_measurement = SENSOR_TYPES[sensor_type][attribute][UNIT_OF_MEASUREMENT] - dev.append( - HEREDestinationWeatherSensor(name, data, sensor_type, attribute, unit_of_measurement) + if sensor_type == mode: + for weather_attribute in SENSOR_TYPES[sensor_type]: + sensors_to_add.append( + HEREDestinationWeatherSensor( + name, here_data, sensor_type, offset, weather_attribute + ) ) + async_add_entities(sensors_to_add, True) - async_add_entities(dev, True) - def _are_valid_client_credentials(here_client: herepy.DestinationWeatherApi) -> bool: - """Check if the provided credentials are correct using defaults.""" - try: - product = herepy.WeatherProductType.forecast_astronomy - known_good_zip_code = "10025" - here_client.weather_for_zip_code(known_good_zip_code, product) - except herepy.UnauthorizedError: - return False - return True +def _are_valid_client_credentials(here_client: herepy.DestinationWeatherApi) -> bool: + """Check if the provided credentials are correct using defaults.""" + try: + product = herepy.WeatherProductType.forecast_astronomy + known_good_zip_code = "10025" + here_client.weather_for_zip_code(known_good_zip_code, product) + except herepy.UnauthorizedError: + return False + return True class HEREDestinationWeatherSensor(Entity): """Implementation of an HERE Destination Weather sensor.""" - def __init__(self, name, weather_data, sensor_type, attribute, unit_of_measurement): + def __init__( + self, + name: str, + here_data: "HEREWeatherData", + sensor_type: str, + sensor_number: int, + weather_attribute: str, + ) -> None: """Initialize the sensor.""" - self._client_name = name - self._name = SENSOR_TYPES[sensor_type][attribute]["name"] - self._here_data = weather_data - self._temp_unit = temp_unit - self._type = sensor_type - self._state = None - self._unit_of_measurement = SENSOR_TYPES[sensor_type][UNIT_OF_MEASUREMENT] + self._base_name = name + self._name_suffix = SENSOR_TYPES[sensor_type][weather_attribute]["name"] + self._here_data = here_data + self._sensor_type = sensor_type + self._sensor_number = sensor_number + self._weather_attribute = weather_attribute + self._unit_of_measurement = convert_unit_of_measurement_if_needed( + self._here_data.units, + SENSOR_TYPES[sensor_type][weather_attribute]["unit_of_measurement"], + ) @property - def name(self): + def name(self) -> str: """Return the name of the sensor.""" - return f"{self._client_name} {self._name}" + return f"{self._base_name} {self._name_suffix}" @property - def state(self): + def state(self) -> str: """Return the state of the device.""" - return self._state + return get_attribute_from_here_data( + self._here_data.data, self._weather_attribute, self._sensor_number + ) @property - def unit_of_measurement(self): + def unit_of_measurement(self) -> str: """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement @property - def device_state_attributes(self): + def device_state_attributes( + self, + ) -> Optional[Dict[str, Union[None, float, str, bool]]]: """Return the state attributes.""" - return {ATTR_ATTRIBUTION: ATTRIBUTION} - - def update(self): - """Get the latest data from OWM and updates the states.""" - try: - self.owa_client.update() - except APICallError: - _LOGGER.error("Error when calling API to update data") - return - - data = self.owa_client.data - fc_data = self.owa_client.fc_data - - if data is None: - return - - try: - if self.type == "weather": - self._state = data.get_detailed_status() - elif self.type == "temperature": - if self.temp_unit == TEMP_CELSIUS: - self._state = round(data.get_temperature("celsius")["temp"], 1) - elif self.temp_unit == TEMP_FAHRENHEIT: - self._state = round(data.get_temperature("fahrenheit")["temp"], 1) - else: - self._state = round(data.get_temperature()["temp"], 1) - elif self.type == "wind_speed": - self._state = round(data.get_wind()["speed"], 1) - elif self.type == "wind_bearing": - self._state = round(data.get_wind()["deg"], 1) - elif self.type == "humidity": - self._state = round(data.get_humidity(), 1) - elif self.type == "pressure": - self._state = round(data.get_pressure()["press"], 0) - elif self.type == "clouds": - self._state = data.get_clouds() - elif self.type == "rain": - if data.get_rain(): - self._state = round(data.get_rain()["3h"], 0) - self._unit_of_measurement = "mm" - else: - self._state = "not raining" - self._unit_of_measurement = "" - elif self.type == "snow": - if data.get_snow(): - self._state = round(data.get_snow(), 0) - self._unit_of_measurement = "mm" - else: - self._state = "not snowing" - self._unit_of_measurement = "" - elif self.type == "forecast": - if fc_data is None: - return - self._state = fc_data.get_weathers()[0].get_detailed_status() - elif self.type == "weather_code": - self._state = data.get_weather_code() - except KeyError: - self._state = None - _LOGGER.warning("Condition is currently not available: %s", self.type) - + return None -class WeatherData: - """Get the latest data from OpenWeatherMap.""" - - def __init__(self, here_client, forecast, latitude, longitude): - """Initialize the data object.""" - self.here_client = here_client - self.forecast = forecast - self.latitude = latitude - self.longitude = longitude - self.data = None - self.fc_data = None - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """Get the latest data from OpenWeatherMap.""" - try: - obs = self.owm.weather_at_coords(self.latitude, self.longitude) - except (APICallError, TypeError): - _LOGGER.error("Error when calling API to get weather at coordinates") - obs = None - - if obs is None: - _LOGGER.warning("Failed to fetch data") - return - - self.data = obs.get_weather() - - if self.forecast == 1: - try: - obs = self.owm.three_hours_forecast_at_coords( - self.latitude, self.longitude - ) - self.fc_data = obs.get_forecast() - except (ConnectionResetError, TypeError): - _LOGGER.warning("Failed to fetch forecast") + async def async_update(self) -> None: + """Get the latest data from HERE.""" + await self.hass.async_add_executor_job(self._here_data.update) diff --git a/homeassistant/components/here_weather/weather.py b/homeassistant/components/here_weather/weather.py index 3b8c7dac3debec..2b810b0d600215 100644 --- a/homeassistant/components/here_weather/weather.py +++ b/homeassistant/components/here_weather/weather.py @@ -1,11 +1,10 @@ """Support for the HERE Destination Weather API.""" from datetime import timedelta import logging -from typing import Callable, Dict, Optional, Union - -import voluptuous as vol +from typing import Callable, Dict, Union import herepy +import voluptuous as vol from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, @@ -19,61 +18,63 @@ WeatherEntity, ) from homeassistant.const import ( - CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_MODE, CONF_NAME, - PRESSURE_HPA, - PRESSURE_INHG, - STATE_UNKNOWN, + CONF_UNIT_SYSTEM, + CONF_UNIT_SYSTEM_IMPERIAL, + CONF_UNIT_SYSTEM_METRIC, TEMP_CELSIUS, ) +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv -from homeassistant.util.pressure import convert as convert_pressure -_LOGGER = logging.getLogger(__name__) +from . import ( + HEREWeatherData, + convert_unit_of_measurement_if_needed, + get_attribute_from_here_data, +) +from .const import ( + CONDITION_CLASSES, + CONF_APP_CODE, + CONF_APP_ID, + CONF_LOCATION_NAME, + CONF_ZIP_CODE, + DEFAULT_MODE, + MODE_DAILY, + MODE_DAILY_SIMPLE, + MODE_HOURLY, + MODE_OBSERVATION, +) -CONF_APP_ID = "app_id" -CONF_APP_CODE = "app_code" -CONF_LOCATION_NAME = "location_name" +CONF_MODES = [MODE_HOURLY, MODE_DAILY, MODE_DAILY_SIMPLE, MODE_OBSERVATION] -FORECAST_MODE = ["hourly", "daily"] +UNITS = [CONF_UNIT_SYSTEM_METRIC, CONF_UNIT_SYSTEM_IMPERIAL] + +_LOGGER = logging.getLogger(__name__) -DEFAULT_NAME = "HERE Destination Weather" +DEFAULT_NAME = "HERE" MIN_TIME_BETWEEN_FORECAST_UPDATES = timedelta(minutes=30) MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=10) -CONDITION_CLASSES = { - "cloudy": [803, 804], - "fog": [701, 741], - "hail": [906], - "lightning": [210, 211, 212, 221], - "lightning-rainy": [200, 201, 202, 230, 231, 232], - "partlycloudy": [801, 802], - "pouring": [504, 314, 502, 503, 522], - "rainy": [300, 301, 302, 310, 311, 312, 313, 500, 501, 520, 521], - "snowy": [600, 601, 602, 611, 612, 620, 621, 622], - "snowy-rainy": [511, 615, 616], - "sunny": [800], - "windy": [905, 951, 952, 953, 954, 955, 956, 957], - "windy-variant": [958, 959, 960, 961], - "exceptional": [711, 721, 731, 751, 761, 762, 771, 900, 901, 962, 903, 904], -} - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_APP_ID): cv.string, vol.Required(CONF_APP_CODE): cv.string, vol.Inclusive(CONF_LATITUDE, "coordinates"): cv.latitude, vol.Inclusive(CONF_LONGITUDE, "coordinates"): cv.longitude, - vol.Exclusive(CONF_LATITUDE, "coords_or_name"): cv.latitude, - vol.Exclusive(CONF_LOCATION_NAME, "coords_or_name"): cv.entity_id, + vol.Exclusive(CONF_LATITUDE, "coords_or_name_or_zip_code"): cv.latitude, + vol.Exclusive(CONF_LOCATION_NAME, "coords_or_name_or_zip_code"): cv.string, + vol.Exclusive(CONF_ZIP_CODE, "coords_or_name_or_zip_code"): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_MODE, default=DEFAULT_MODE): vol.In(CONF_MODES), vol.Optional(CONF_LATITUDE): cv.latitude, vol.Optional(CONF_LONGITUDE): cv.longitude, - vol.Optional(CONF_MODE, default="hourly"): vol.In(FORECAST_MODE), + vol.Optional(CONF_LOCATION_NAME): cv.string, + vol.Optional(CONF_ZIP_CODE): cv.string, + vol.Optional(CONF_UNIT_SYSTEM): vol.In(UNITS), } ) @@ -85,37 +86,53 @@ async def async_setup_platform( discovery_info: None = None, ) -> None: """Set up the HERE Destination weather platform.""" + app_id = config[CONF_APP_ID] + app_code = config[CONF_APP_CODE] - longitude = config.get(CONF_LONGITUDE, hass.config.longitude) - latitude = config.get(CONF_LATITUDE) - name = config[CONF_NAME] - mode = config[CONF_MODE] + here_client = herepy.DestinationWeatherApi(app_id, app_code) - try: - owm = pyowm.OWM(config.get(CONF_API_KEY)) - except pyowm.exceptions.api_call_error.APICallError: - _LOGGER.error("Error while connecting to OpenWeatherMap") - return False + if not await hass.async_add_executor_job( + _are_valid_client_credentials, here_client + ): + _LOGGER.error( + "Invalid credentials. This error is returned if the specified token was invalid or no contract could be found for this token." + ) + return - data = WeatherData(owm, latitude, longitude, mode) + name = config.get(CONF_NAME) + mode = config[CONF_MODE] + latitude = config.get(CONF_LATITUDE, hass.config.latitude) + longitude = config.get(CONF_LONGITUDE, hass.config.longitude) + location_name = config.get(CONF_LOCATION_NAME) + zip_code = config.get(CONF_ZIP_CODE) + units = config.get(CONF_UNIT_SYSTEM, hass.config.units.name) - add_entities( - [OpenWeatherMapWeather(name, data, hass.config.units.temperature_unit, mode)], - True, + here_data = HEREWeatherData( + here_client, mode, units, latitude, longitude, location_name, zip_code ) + async_add_entities([HEREDestinationWeather(name, here_data, mode)], True) + -class OpenWeatherMapWeather(WeatherEntity): - """Implementation of an OpenWeatherMap sensor.""" +def _are_valid_client_credentials(here_client: herepy.DestinationWeatherApi) -> bool: + """Check if the provided credentials are correct using defaults.""" + try: + product = herepy.WeatherProductType.forecast_astronomy + known_good_zip_code = "10025" + here_client.weather_for_zip_code(known_good_zip_code, product) + except herepy.UnauthorizedError: + return False + return True + + +class HEREDestinationWeather(WeatherEntity): + """Implementation of an HERE Destination Weather WeatherEntity.""" - def __init__(self, name, owm, temperature_unit, mode): + def __init__(self, name, here_data, mode): """Initialize the sensor.""" self._name = name - self._owm = owm - self._temperature_unit = temperature_unit + self._here_data = here_data self._mode = mode - self.data = None - self.forecast_data = None @property def name(self): @@ -125,174 +142,130 @@ def name(self): @property def condition(self): """Return the current condition.""" - try: - return [ - k - for k, v in CONDITION_CLASSES.items() - if self.data.get_weather_code() in v - ][0] - except IndexError: - return STATE_UNKNOWN + return get_condition_from_here_data(self._here_data.data) @property - def temperature(self): + def temperature(self) -> float: """Return the temperature.""" - return self.data.get_temperature("celsius").get("temp") + return get_temperature_from_here_data(self._here_data.data) @property def temperature_unit(self): """Return the unit of measurement.""" - return TEMP_CELSIUS + try: + return convert_unit_of_measurement_if_needed( + self._here_data.units, TEMP_CELSIUS + ) + except KeyError: + return None @property def pressure(self): """Return the pressure.""" - pressure = self.data.get_pressure().get("press") - if self.hass.config.units.name == "imperial": - return round(convert_pressure(pressure, PRESSURE_HPA, PRESSURE_INHG), 2) - return pressure + return None @property def humidity(self): """Return the humidity.""" - return self.data.get_humidity() + get_attribute_from_here_data(self._here_data.data, "humidity") @property def wind_speed(self): """Return the wind speed.""" - if self.hass.config.units.name == "imperial": - return round(self.data.get_wind().get("speed") * 2.24, 2) - - return round(self.data.get_wind().get("speed") * 3.6, 2) + get_attribute_from_here_data(self._here_data.data, "windSpeed") @property def wind_bearing(self): """Return the wind bearing.""" - return self.data.get_wind().get("deg") + get_attribute_from_here_data(self._here_data.data, "windDirection") @property def attribution(self): """Return the attribution.""" - return ATTRIBUTION + return None @property def forecast(self): """Return the forecast array.""" + if self._here_data.data is None: + return None data = [] - - def calc_precipitation(rain, snow): - """Calculate the precipitation.""" - rain_value = 0 if rain is None else rain - snow_value = 0 if snow is None else snow - if round(rain_value + snow_value, 1) == 0: - return None - return round(rain_value + snow_value, 1) - - if self._mode == "freedaily": - weather = self.forecast_data.get_weathers()[::8] - else: - weather = self.forecast_data.get_weathers() - - for entry in weather: - if self._mode == "daily": - data.append( - { - ATTR_FORECAST_TIME: entry.get_reference_time("unix") * 1000, - ATTR_FORECAST_TEMP: entry.get_temperature("celsius").get("day"), - ATTR_FORECAST_TEMP_LOW: entry.get_temperature("celsius").get( - "night" - ), - ATTR_FORECAST_PRECIPITATION: calc_precipitation( - entry.get_rain().get("all"), entry.get_snow().get("all") - ), - ATTR_FORECAST_WIND_SPEED: entry.get_wind().get("speed"), - ATTR_FORECAST_WIND_BEARING: entry.get_wind().get("deg"), - ATTR_FORECAST_CONDITION: [ - k - for k, v in CONDITION_CLASSES.items() - if entry.get_weather_code() in v - ][0], - } - ) - else: - data.append( - { - ATTR_FORECAST_TIME: entry.get_reference_time("unix") * 1000, - ATTR_FORECAST_TEMP: entry.get_temperature("celsius").get( - "temp" - ), - ATTR_FORECAST_PRECIPITATION: ( - round(entry.get_rain().get("3h"), 1) - if entry.get_rain().get("3h") is not None - and (round(entry.get_rain().get("3h"), 1) > 0) - else None - ), - ATTR_FORECAST_CONDITION: [ - k - for k, v in CONDITION_CLASSES.items() - if entry.get_weather_code() in v - ][0], - } - ) + for offset in range(len(self._here_data.data)): + data.append( + { + ATTR_FORECAST_TIME: get_attribute_from_here_data( + self._here_data.data, "utcTime", offset + ), + ATTR_FORECAST_TEMP: get_high_or_default_temperature_from_here_data( + self._here_data.data, offset + ), + ATTR_FORECAST_TEMP_LOW: get_low_or_default_temperature_from_here_data( + self._here_data.data, offset + ), + ATTR_FORECAST_PRECIPITATION: calc_precipitation( + self._here_data.data, offset + ), + ATTR_FORECAST_WIND_SPEED: get_attribute_from_here_data( + self._here_data.data, "windSpeed", offset + ), + ATTR_FORECAST_WIND_BEARING: get_attribute_from_here_data( + self._here_data.data, "windDirection", offset + ), + ATTR_FORECAST_CONDITION: get_condition_from_here_data( + self._here_data.data, offset + ), + } + ) return data - def update(self): - """Get the latest data from OWM and updates the states.""" - from pyowm.exceptions.api_call_error import APICallError + async def async_update(self) -> None: + """Get the latest data from HERE.""" + await self.hass.async_add_executor_job(self._here_data.update) - try: - self._owm.update() - self._owm.update_forecast() - except APICallError: - _LOGGER.error("Exception when calling OWM web API to update data") - return - - self.data = self._owm.data - self.forecast_data = self._owm.forecast_data - - -class WeatherData: - """Get the latest data from OpenWeatherMap.""" - def __init__(self, owm, latitude, longitude, mode): - """Initialize the data object.""" - self._mode = mode - self.owm = owm - self.latitude = latitude - self.longitude = longitude - self.data = None - self.forecast_data = None - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """Get the latest data from OpenWeatherMap.""" - obs = self.owm.weather_at_coords(self.latitude, self.longitude) - if obs is None: - _LOGGER.warning("Failed to fetch data from OWM") - return - - self.data = obs.get_weather() - - @Throttle(MIN_TIME_BETWEEN_FORECAST_UPDATES) - def update_forecast(self): - """Get the latest forecast from OpenWeatherMap.""" - from pyowm.exceptions.api_call_error import APICallError - - try: - if self._mode == "daily": - fcd = self.owm.daily_forecast_at_coords( - self.latitude, self.longitude, 15 - ) - else: - fcd = self.owm.three_hours_forecast_at_coords( - self.latitude, self.longitude - ) - except APICallError: - _LOGGER.error("Exception when calling OWM web API " "to update forecast") - return - - if fcd is None: - _LOGGER.warning("Failed to fetch forecast data from OWM") - return - - self.forecast_data = fcd.get_forecast() +def get_condition_from_here_data(here_data: list, offset: int = 0) -> str: + """Return the condition from here_data.""" + try: + return [ + k + for k, v in CONDITION_CLASSES.items() + if get_attribute_from_here_data(here_data, "iconName", offset) in v + ][0] + except IndexError: + return None + + +def get_high_or_default_temperature_from_here_data( + here_data: list, offset: int = 0 +) -> str: + """Return the temperature from here_data.""" + temperature = get_attribute_from_here_data(here_data, "highTemperature", offset) + if temperature is not None: + return float(temperature) + + return get_temperature_from_here_data(here_data, offset) + + +def get_low_or_default_temperature_from_here_data( + here_data: list, offset: int = 0 +) -> str: + """Return the temperature from here_data.""" + temperature = get_attribute_from_here_data(here_data, "lowTemperature", offset) + if temperature is not None: + return float(temperature) + return get_temperature_from_here_data(here_data, offset) + + +def get_temperature_from_here_data(here_data: list, offset: int = 0) -> str: + """Return the temperature from here_data.""" + temperature = get_attribute_from_here_data(here_data, "temperature", offset) + if temperature is not None: + return float(temperature) + + +def calc_precipitation(here_data: list, offset: int = 0) -> float: + """Calculate Precipitation.""" + rain_fall = get_attribute_from_here_data(here_data, "rainFall", offset) + snow_fall = get_attribute_from_here_data(here_data, "snowFall", offset) + if rain_fall is not None and snow_fall is not None: + return float(rain_fall) + float(snow_fall) diff --git a/tests/components/here_weather/__init__.py b/tests/components/here_weather/__init__.py new file mode 100644 index 00000000000000..159c72643fa50a --- /dev/null +++ b/tests/components/here_weather/__init__.py @@ -0,0 +1,56 @@ +"""Tests for here_weather component.""" +import urllib + +PLATFORM = "here_weather" + +APP_ID = "test" +APP_CODE = "test" + +ZIP_CODE = "10025" + +LOCATION_NAME = "New York" + +LATITUDE = "40.79962" +LONGITUDE = "-73.970314" + + +def build_base_mock_url(app_id, app_code, additional_params): + """Construct a url for HERE.""" + base_url = "https://weather.api.here.com/weather/1.0/report.json?" + parameters = { + "app_id": app_id, + "app_code": app_code, + "oneobservation": True, + "metric": True, + } + parameters.update(additional_params) + url = base_url + urllib.parse.urlencode(parameters) + return url + + +def build_zip_code_imperial_mock_url(app_id, app_code, zip_code, product): + """Construct a url for HERE.""" + parameters = {"product": product, "zipcode": zip_code, "metric": False} + url = build_base_mock_url(app_id, app_code, parameters) + return url + + +def build_coordinates_mock_url(app_id, app_code, latitude, longitude, product): + """Construct a url for HERE.""" + parameters = {"product": product, "latitude": latitude, "longitude": longitude} + url = build_base_mock_url(app_id, app_code, parameters) + return url + + +def build_location_name_mock_url(app_id, app_code, location_name, product): + """Construct a url for HERE.""" + parameters = {"product": product, "name": location_name} + url = build_base_mock_url(app_id, app_code, parameters) + return url + + +def build_zip_code_mock_url(app_id, app_code, zip_code, product): + """Construct a url for HERE.""" + parameters = {"product": product, "zipcode": zip_code} + url = build_base_mock_url(app_id, app_code, parameters) + return url diff --git a/tests/components/here_weather/test_sensor.py b/tests/components/here_weather/test_sensor.py new file mode 100644 index 00000000000000..af6b854ce639c3 --- /dev/null +++ b/tests/components/here_weather/test_sensor.py @@ -0,0 +1,315 @@ +"""The test for the here_weather sensor platform.""" +import logging + +import herepy +import pytest + +from homeassistant.components.here_weather.const import ( + ASTRONOMY_ATTRIBUTES, + DAILY_ATTRIBUTES, + DAILY_SIMPLE_ATTRIBUTES, + HOURLY_ATTRIBUTES, +) +from homeassistant.setup import async_setup_component + +from . import ( + APP_CODE, + APP_ID, + LATITUDE, + LOCATION_NAME, + LONGITUDE, + PLATFORM, + ZIP_CODE, + build_coordinates_mock_url, + build_location_name_mock_url, + build_zip_code_imperial_mock_url, + build_zip_code_mock_url, +) + +from tests.common import load_fixture + +DOMAIN = "sensor" + + +@pytest.fixture +def requests_mock_credentials_check(requests_mock): + """Add the url used in the api validation to all requests mock.""" + product = herepy.WeatherProductType.forecast_astronomy + response_url = build_zip_code_mock_url(APP_ID, APP_CODE, ZIP_CODE, product) + requests_mock.get( + response_url, + text=load_fixture("here_weather/destination_weather_forecasts_astronomy.json"), + ) + return requests_mock + + +@pytest.fixture +def requests_mock_invalid_credentials(requests_mock): + """Add the url for invalid credentials to requests mock.""" + product = herepy.WeatherProductType.forecast_astronomy + response_url = build_zip_code_mock_url(APP_ID, APP_CODE, ZIP_CODE, product) + requests_mock.get( + response_url, + text=load_fixture("here_weather/destination_weather_error_unauthorized.json"), + ) + return requests_mock + + +@pytest.fixture +def requests_mock_location_name(requests_mock_credentials_check): + """Add the url used in a request with location_name to requests mock.""" + product = herepy.WeatherProductType.forecast_astronomy + response_url = build_location_name_mock_url( + APP_ID, APP_CODE, LOCATION_NAME, product + ) + requests_mock_credentials_check.get( + response_url, + text=load_fixture("here_weather/destination_weather_forecasts_astronomy.json"), + ) + return requests_mock_credentials_check + + +@pytest.fixture +def requests_mock_coordinates(requests_mock_credentials_check): + """Add the url used in a request with coordinates to requests mock.""" + product = herepy.WeatherProductType.forecast_astronomy + response_url = build_coordinates_mock_url( + APP_ID, APP_CODE, LATITUDE, LONGITUDE, product + ) + requests_mock_credentials_check.get( + response_url, + text=load_fixture("here_weather/destination_weather_forecasts_astronomy.json"), + ) + return requests_mock_credentials_check + + +@pytest.fixture +def requests_mock_zip_code_imperial(requests_mock_credentials_check): + """Add the url used in a request with zip_code to requests mock.""" + product = herepy.WeatherProductType.observation + response_url = build_zip_code_imperial_mock_url(APP_ID, APP_CODE, ZIP_CODE, product) + requests_mock_credentials_check.get( + response_url, + text=load_fixture("here_weather/destination_weather_observation_imperial.json"), + ) + return requests_mock_credentials_check + + +@pytest.fixture +def requests_mock_forecast_7days(requests_mock_credentials_check): + """Add the url used in a request with forecast_7days to requests mock.""" + product = herepy.WeatherProductType.forecast_7days + response_url = build_location_name_mock_url( + APP_ID, APP_CODE, LOCATION_NAME, product + ) + requests_mock_credentials_check.get( + response_url, + text=load_fixture("here_weather/destination_weather_forecasts.json"), + ) + return requests_mock_credentials_check + + +@pytest.fixture +def requests_mock_forecast_hourly(requests_mock_credentials_check): + """Add the url used in a request with forecast_hourly to requests mock.""" + product = herepy.WeatherProductType.forecast_hourly + response_url = build_location_name_mock_url( + APP_ID, APP_CODE, LOCATION_NAME, product + ) + requests_mock_credentials_check.get( + response_url, + text=load_fixture("here_weather/destination_weather_forecasts_hourly.json"), + ) + return requests_mock_credentials_check + + +@pytest.fixture +def requests_mock_forecast_7days_simple(requests_mock_credentials_check): + """Add the url used in a request with forecast_7days_simple to requests mock.""" + product = herepy.WeatherProductType.forecast_7days_simple + response_url = build_location_name_mock_url( + APP_ID, APP_CODE, LOCATION_NAME, product + ) + requests_mock_credentials_check.get( + response_url, + text=load_fixture("here_weather/destination_weather_forecasts_simple.json"), + ) + return requests_mock_credentials_check + + +@pytest.fixture +def requests_mock_invalid_request(requests_mock_credentials_check): + """Add the url used in an invalid request to requests mock.""" + product = herepy.WeatherProductType.observation + response_url = build_zip_code_mock_url(APP_ID, APP_CODE, ZIP_CODE, product) + requests_mock_credentials_check.get( + response_url, + text=load_fixture( + "here_weather/destination_weather_error_invalid_request.json" + ), + ) + return requests_mock_credentials_check + + +async def test_invalid_credentials(hass, requests_mock_invalid_credentials, caplog): + """Test that invalid credentials error is correctly handled.""" + caplog.set_level(logging.ERROR) + config = { + DOMAIN: { + "platform": PLATFORM, + "name": "test", + "zip_code": ZIP_CODE, + "mode": "forecast_astronomy", + "app_id": APP_ID, + "app_code": APP_CODE, + } + } + assert await async_setup_component(hass, DOMAIN, config) + assert len(caplog.records) == 1 + assert "Invalid credentials" in caplog.text + + +async def test_forecast_astronomy(hass, requests_mock_credentials_check): + """Test that forecast_astronomy works.""" + config = { + DOMAIN: { + "platform": PLATFORM, + "name": "test", + "zip_code": ZIP_CODE, + "mode": "forecast_astronomy", + "app_id": APP_ID, + "app_code": APP_CODE, + } + } + + assert await async_setup_component(hass, DOMAIN, config) + assert len(hass.states.async_all()) == len(ASTRONOMY_ATTRIBUTES) + + +async def test_location_name(hass, requests_mock_location_name): + """Test that location_name option works.""" + config = { + DOMAIN: { + "platform": PLATFORM, + "name": "test", + "location_name": LOCATION_NAME, + "mode": "forecast_astronomy", + "app_id": APP_ID, + "app_code": APP_CODE, + } + } + + assert await async_setup_component(hass, DOMAIN, config) + assert len(hass.states.async_all()) == len(ASTRONOMY_ATTRIBUTES) + + +async def test_coordinates(hass, requests_mock_coordinates): + """Test that lat/lon option works.""" + config = { + DOMAIN: { + "platform": PLATFORM, + "name": "test", + "latitude": LATITUDE, + "longitude": LONGITUDE, + "mode": "forecast_astronomy", + "app_id": APP_ID, + "app_code": APP_CODE, + } + } + + assert await async_setup_component(hass, DOMAIN, config) + assert len(hass.states.async_all()) == len(ASTRONOMY_ATTRIBUTES) + + +async def test_imperial(hass, requests_mock_zip_code_imperial): + """Test that imperial mode works.""" + config = { + DOMAIN: { + "platform": PLATFORM, + "name": "test", + "zip_code": ZIP_CODE, + "mode": "observation", + "app_id": APP_ID, + "app_code": APP_CODE, + "unit_system": "imperial", + } + } + + assert await async_setup_component(hass, DOMAIN, config) + + sensor_name = f"{DOMAIN}.test_wind_speed" + sensor = hass.states.get(sensor_name) + assert sensor.attributes.get("unit_of_measurement") == "mph" + + +async def test_forecast_7days(hass, requests_mock_forecast_7days): + """Test that forecast_7days option works.""" + config = { + DOMAIN: { + "platform": PLATFORM, + "name": "test", + "location_name": LOCATION_NAME, + "mode": "forecast_7days", + "app_id": APP_ID, + "app_code": APP_CODE, + } + } + + assert await async_setup_component(hass, DOMAIN, config) + assert len(hass.states.async_all()) == len(DAILY_ATTRIBUTES) + + +async def test_forecast_7days_simple(hass, requests_mock_forecast_7days_simple): + """Test that forecast_7days_simple option works.""" + config = { + DOMAIN: { + "platform": PLATFORM, + "name": "test", + "location_name": LOCATION_NAME, + "mode": "forecast_7days_simple", + "app_id": APP_ID, + "app_code": APP_CODE, + } + } + + assert await async_setup_component(hass, DOMAIN, config) + assert len(hass.states.async_all()) == len(DAILY_SIMPLE_ATTRIBUTES) + + +async def test_forecast_forecast_hourly(hass, requests_mock_forecast_hourly): + """Test that forecast_hourly option works.""" + config = { + DOMAIN: { + "platform": PLATFORM, + "name": "test", + "location_name": LOCATION_NAME, + "mode": "forecast_hourly", + "app_id": APP_ID, + "app_code": APP_CODE, + } + } + + assert await async_setup_component(hass, DOMAIN, config) + assert len(hass.states.async_all()) == len(HOURLY_ATTRIBUTES) + + +async def test_invalid_request(hass, requests_mock_invalid_request, caplog): + """Test that invalid credentials error is correctly handled.""" + caplog.set_level(logging.ERROR) + config = { + DOMAIN: { + "platform": PLATFORM, + "name": "test", + "zip_code": ZIP_CODE, + "mode": "observation", + "app_id": APP_ID, + "app_code": APP_CODE, + } + } + assert await async_setup_component(hass, DOMAIN, config) + assert len(caplog.records) == 1 + assert "Error during sensor update" in caplog.text + + sensor_name = sensor_name = f"{DOMAIN}.test_wind_speed" + sensor = hass.states.get(sensor_name) + assert sensor.state == "unknown" diff --git a/tests/components/here_weather/test_weather.py b/tests/components/here_weather/test_weather.py new file mode 100644 index 00000000000000..1550f0ac55c4d4 --- /dev/null +++ b/tests/components/here_weather/test_weather.py @@ -0,0 +1,304 @@ +"""The test for the here_weather weather platform.""" +import logging + +import herepy +import pytest + +from homeassistant.setup import async_setup_component + +from . import ( + APP_CODE, + APP_ID, + LATITUDE, + LOCATION_NAME, + LONGITUDE, + PLATFORM, + ZIP_CODE, + build_coordinates_mock_url, + build_location_name_mock_url, + build_zip_code_imperial_mock_url, + build_zip_code_mock_url, +) + +from tests.common import load_fixture + +DOMAIN = "weather" + + +@pytest.fixture +def requests_mock_credentials_check(requests_mock): + """Add the url used in the api validation to all requests mock.""" + product = herepy.WeatherProductType.forecast_astronomy + response_url = build_zip_code_mock_url(APP_ID, APP_CODE, ZIP_CODE, product) + requests_mock.get( + response_url, + text=load_fixture("here_weather/destination_weather_forecasts_astronomy.json"), + ) + return requests_mock + + +@pytest.fixture +def requests_mock_invalid_credentials(requests_mock): + """Add the url for invalid credentials to requests mock.""" + product = herepy.WeatherProductType.forecast_astronomy + response_url = build_zip_code_mock_url(APP_ID, APP_CODE, ZIP_CODE, product) + requests_mock.get( + response_url, + text=load_fixture("here_weather/destination_weather_error_unauthorized.json"), + ) + return requests_mock + + +@pytest.fixture +def requests_mock_location_name(requests_mock_credentials_check): + """Add the url used in a request with location_name to requests mock.""" + product = herepy.WeatherProductType.forecast_astronomy + response_url = build_location_name_mock_url( + APP_ID, APP_CODE, LOCATION_NAME, product + ) + requests_mock_credentials_check.get( + response_url, + text=load_fixture("here_weather/destination_weather_forecasts_astronomy.json"), + ) + return requests_mock_credentials_check + + +@pytest.fixture +def requests_mock_coordinates(requests_mock_credentials_check): + """Add the url used in a request with coordinates to requests mock.""" + product = herepy.WeatherProductType.forecast_astronomy + response_url = build_coordinates_mock_url( + APP_ID, APP_CODE, LATITUDE, LONGITUDE, product + ) + requests_mock_credentials_check.get( + response_url, + text=load_fixture("here_weather/destination_weather_forecasts_astronomy.json"), + ) + return requests_mock_credentials_check + + +@pytest.fixture +def requests_mock_zip_code_imperial(requests_mock_credentials_check): + """Add the url used in a request with zip_code to requests mock.""" + product = herepy.WeatherProductType.observation + response_url = build_zip_code_imperial_mock_url(APP_ID, APP_CODE, ZIP_CODE, product) + requests_mock_credentials_check.get( + response_url, + text=load_fixture("here_weather/destination_weather_observation_imperial.json"), + ) + return requests_mock_credentials_check + + +@pytest.fixture +def requests_mock_forecast_7days(requests_mock_credentials_check): + """Add the url used in a request with forecast_7days to requests mock.""" + product = herepy.WeatherProductType.forecast_7days + response_url = build_location_name_mock_url( + APP_ID, APP_CODE, LOCATION_NAME, product + ) + requests_mock_credentials_check.get( + response_url, + text=load_fixture("here_weather/destination_weather_forecasts.json"), + ) + return requests_mock_credentials_check + + +@pytest.fixture +def requests_mock_forecast_hourly(requests_mock_credentials_check): + """Add the url used in a request with forecast_hourly to requests mock.""" + product = herepy.WeatherProductType.forecast_hourly + response_url = build_location_name_mock_url( + APP_ID, APP_CODE, LOCATION_NAME, product + ) + requests_mock_credentials_check.get( + response_url, + text=load_fixture("here_weather/destination_weather_forecasts_hourly.json"), + ) + return requests_mock_credentials_check + + +@pytest.fixture +def requests_mock_forecast_7days_simple(requests_mock_credentials_check): + """Add the url used in a request with forecast_7days_simple to requests mock.""" + product = herepy.WeatherProductType.forecast_7days_simple + response_url = build_location_name_mock_url( + APP_ID, APP_CODE, LOCATION_NAME, product + ) + requests_mock_credentials_check.get( + response_url, + text=load_fixture("here_weather/destination_weather_forecasts_simple.json"), + ) + return requests_mock_credentials_check + + +@pytest.fixture +def requests_mock_invalid_request(requests_mock_credentials_check): + """Add the url used in an invalid request to requests mock.""" + product = herepy.WeatherProductType.observation + response_url = build_zip_code_mock_url(APP_ID, APP_CODE, ZIP_CODE, product) + requests_mock_credentials_check.get( + response_url, + text=load_fixture( + "here_weather/destination_weather_error_invalid_request.json" + ), + ) + return requests_mock_credentials_check + + +async def test_invalid_credentials(hass, requests_mock_invalid_credentials, caplog): + """Test that invalid credentials error is correctly handled.""" + caplog.set_level(logging.ERROR) + config = { + DOMAIN: { + "platform": PLATFORM, + "name": "test", + "zip_code": ZIP_CODE, + "mode": "forecast_7days", + "app_id": APP_ID, + "app_code": APP_CODE, + } + } + assert await async_setup_component(hass, DOMAIN, config) + assert len(caplog.records) == 1 + assert "Invalid credentials" in caplog.text + + +async def test_location_name(hass, requests_mock_location_name): + """Test that location_name option works.""" + config = { + DOMAIN: { + "platform": PLATFORM, + "name": "test", + "location_name": LOCATION_NAME, + "mode": "forecast_7days", + "app_id": APP_ID, + "app_code": APP_CODE, + } + } + + assert await async_setup_component(hass, DOMAIN, config) + sensor_name = sensor_name = f"{DOMAIN}.test" + sensor = hass.states.get(sensor_name) + assert sensor.state == "cloudy" + + +async def test_coordinates(hass, requests_mock_coordinates): + """Test that lat/lon option works.""" + config = { + DOMAIN: { + "platform": PLATFORM, + "name": "test", + "latitude": LATITUDE, + "longitude": LONGITUDE, + "mode": "forecast_7days", + "app_id": APP_ID, + "app_code": APP_CODE, + } + } + + assert await async_setup_component(hass, DOMAIN, config) + sensor_name = sensor_name = f"{DOMAIN}.test" + sensor = hass.states.get(sensor_name) + assert sensor.state == "cloudy" + + +async def test_imperial(hass, requests_mock_zip_code_imperial): + """Test that imperial mode works.""" + config = { + DOMAIN: { + "platform": PLATFORM, + "name": "test", + "zip_code": ZIP_CODE, + "mode": "observation", + "app_id": APP_ID, + "app_code": APP_CODE, + "unit_system": "imperial", + } + } + + assert await async_setup_component(hass, DOMAIN, config) + + sensor_name = f"{DOMAIN}.test" + sensor = hass.states.get(sensor_name) + assert ( + sensor.attributes.get("temperature") == 8 + ) # Gets converted because standard hass config is metric + + +async def test_forecast_7days(hass, requests_mock_forecast_7days): + """Test that forecast_7days option works.""" + config = { + DOMAIN: { + "platform": PLATFORM, + "name": "test", + "location_name": LOCATION_NAME, + "mode": "forecast_7days", + "app_id": APP_ID, + "app_code": APP_CODE, + } + } + + assert await async_setup_component(hass, DOMAIN, config) + sensor_name = sensor_name = f"{DOMAIN}.test" + sensor = hass.states.get(sensor_name) + assert sensor.state == "cloudy" + + +async def test_forecast_7days_simple(hass, requests_mock_forecast_7days_simple): + """Test that forecast_7days_simple option works.""" + config = { + DOMAIN: { + "platform": PLATFORM, + "name": "test", + "location_name": LOCATION_NAME, + "mode": "forecast_7days_simple", + "app_id": APP_ID, + "app_code": APP_CODE, + } + } + + assert await async_setup_component(hass, DOMAIN, config) + sensor_name = sensor_name = f"{DOMAIN}.test" + sensor = hass.states.get(sensor_name) + assert sensor.state == "cloudy" + + +async def test_forecast_forecast_hourly(hass, requests_mock_forecast_hourly): + """Test that forecast_hourly option works.""" + config = { + DOMAIN: { + "platform": PLATFORM, + "name": "test", + "location_name": LOCATION_NAME, + "mode": "forecast_hourly", + "app_id": APP_ID, + "app_code": APP_CODE, + } + } + + assert await async_setup_component(hass, DOMAIN, config) + sensor_name = sensor_name = f"{DOMAIN}.test" + sensor = hass.states.get(sensor_name) + assert sensor.state == "cloudy" + + +async def test_invalid_request(hass, requests_mock_invalid_request, caplog): + """Test that invalid credentials error is correctly handled.""" + caplog.set_level(logging.ERROR) + config = { + DOMAIN: { + "platform": PLATFORM, + "name": "test", + "zip_code": ZIP_CODE, + "mode": "observation", + "app_id": APP_ID, + "app_code": APP_CODE, + } + } + assert await async_setup_component(hass, DOMAIN, config) + assert len(caplog.records) == 1 + assert "Error during sensor update" in caplog.text + + sensor_name = sensor_name = f"{DOMAIN}.test" + sensor = hass.states.get(sensor_name) + assert sensor.state == "unknown" diff --git a/tests/fixtures/here_weather/destination_weather_error_invalid_request.json b/tests/fixtures/here_weather/destination_weather_error_invalid_request.json new file mode 100644 index 00000000000000..e74086f5596b2b --- /dev/null +++ b/tests/fixtures/here_weather/destination_weather_error_invalid_request.json @@ -0,0 +1,6 @@ +{ + "Type": "Invalid Request", + "Message": [ + "Request parameter 'product' does not conform with expected data type. Please check the documentation." + ] +} \ No newline at end of file diff --git a/tests/fixtures/here_weather/destination_weather_error_unauthorized.json b/tests/fixtures/here_weather/destination_weather_error_unauthorized.json new file mode 100644 index 00000000000000..49077fe18bce6d --- /dev/null +++ b/tests/fixtures/here_weather/destination_weather_error_unauthorized.json @@ -0,0 +1,6 @@ +{ + "Type": "Unauthorized", + "Message": [ + "This is not a valid app_id and app_code pair. Please verify that the values are not swapped between the app_id and app_code and the values provisioned by HERE (either by your customer representative or via http://developer.here.com/myapps) were copied correctly into the request." + ] + } diff --git a/tests/fixtures/here_weather/destination_weather_forecasts.json b/tests/fixtures/here_weather/destination_weather_forecasts.json new file mode 100644 index 00000000000000..93db1a0d5d5a28 --- /dev/null +++ b/tests/fixtures/here_weather/destination_weather_forecasts.json @@ -0,0 +1,761 @@ +{ + "forecasts": { + "forecastLocation": { + "forecast": [ + { + "daylight": "D", + "daySegment": "Afternoon", + "description": "Overcast. Chilly.", + "skyInfo": "18", + "skyDescription": "Overcast", + "temperature": "4.00", + "temperatureDesc": "Chilly", + "comfort": "0.86", + "humidity": "73", + "dewPoint": "-0.40", + "precipitationProbability": "9", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "12.96", + "windDirection": "294", + "windDesc": "Northwest", + "windDescShort": "NW", + "beaufortScale": "3", + "beaufortDescription": "Gentle breeze", + "visibility": "11.38", + "icon": "7", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/17.png", + "dayOfWeek": "3", + "weekday": "Tuesday", + "utcTime": "2019-11-19T12:00:00.000-05:00" + }, + { + "daylight": "N", + "daySegment": "Evening", + "description": "Overcast. Cold.", + "skyInfo": "18", + "skyDescription": "Overcast", + "temperature": "-0.30", + "temperatureDesc": "Cold", + "comfort": "-3.06", + "humidity": "90", + "dewPoint": "-1.70", + "precipitationProbability": "10", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "7.92", + "windDirection": "290", + "windDesc": "West", + "windDescShort": "W", + "beaufortScale": "2", + "beaufortDescription": "Light breeze", + "visibility": "12.25", + "icon": "17", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", + "dayOfWeek": "3", + "weekday": "Tuesday", + "utcTime": "2019-11-19T18:00:00.000-05:00" + }, + { + "daylight": "N", + "daySegment": "Night", + "description": "Overcast. Cold.", + "skyInfo": "18", + "skyDescription": "Overcast", + "temperature": "-1.90", + "temperatureDesc": "Cold", + "comfort": "-4.94", + "humidity": "94", + "dewPoint": "-2.70", + "precipitationProbability": "7", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "7.92", + "windDirection": "301", + "windDesc": "Northwest", + "windDescShort": "NW", + "beaufortScale": "2", + "beaufortDescription": "Light breeze", + "visibility": "12.56", + "icon": "17", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", + "dayOfWeek": "3", + "weekday": "Tuesday", + "utcTime": "2019-11-20T00:00:00.000-05:00" + }, + { + "daylight": "D", + "daySegment": "Morning", + "description": "Light snow. Overcast. Cold.", + "skyInfo": "18", + "skyDescription": "Overcast", + "temperature": "-1.70", + "temperatureDesc": "Cold", + "comfort": "-5.47", + "humidity": "100", + "dewPoint": "-1.30", + "precipitationProbability": "42", + "precipitationDesc": "Light snow", + "rainFall": "*", + "snowFall": "0.53", + "airInfo": "*", + "airDescription": "", + "windSpeed": "10.44", + "windDirection": "308", + "windDesc": "Northwest", + "windDescShort": "NW", + "beaufortScale": "2", + "beaufortDescription": "Light breeze", + "visibility": "12.05", + "icon": "29", + "iconName": "light_snow", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/20.png", + "dayOfWeek": "4", + "weekday": "Wednesday", + "utcTime": "2019-11-20T06:00:00.000-05:00" + }, + { + "daylight": "D", + "daySegment": "Afternoon", + "description": "Light snow. Overcast. Chilly.", + "skyInfo": "18", + "skyDescription": "Overcast", + "temperature": "2.30", + "temperatureDesc": "Chilly", + "comfort": "-2.11", + "humidity": "83", + "dewPoint": "-0.30", + "precipitationProbability": "43", + "precipitationDesc": "Light snow", + "rainFall": "0.03", + "snowFall": "0.30", + "airInfo": "*", + "airDescription": "", + "windSpeed": "18.36", + "windDirection": "310", + "windDesc": "Northwest", + "windDescShort": "NW", + "beaufortScale": "4", + "beaufortDescription": "Moderate breeze", + "visibility": "12.41", + "icon": "29", + "iconName": "light_snow", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/20.png", + "dayOfWeek": "4", + "weekday": "Wednesday", + "utcTime": "2019-11-20T12:00:00.000-05:00" + }, + { + "daylight": "N", + "daySegment": "Evening", + "description": "Light snow. Overcast. Chilly.", + "skyInfo": "18", + "skyDescription": "Overcast", + "temperature": "0.70", + "temperatureDesc": "Chilly", + "comfort": "-2.93", + "humidity": "90", + "dewPoint": "-0.70", + "precipitationProbability": "28", + "precipitationDesc": "Light snow", + "rainFall": "*", + "snowFall": "0.10", + "airInfo": "32", + "airDescription": "Damp", + "windSpeed": "11.88", + "windDirection": "309", + "windDesc": "Northwest", + "windDescShort": "NW", + "beaufortScale": "3", + "beaufortDescription": "Gentle breeze", + "visibility": "11.72", + "icon": "29", + "iconName": "light_snow", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/20.png", + "dayOfWeek": "4", + "weekday": "Wednesday", + "utcTime": "2019-11-20T18:00:00.000-05:00" + }, + { + "daylight": "N", + "daySegment": "Night", + "description": "Overcast. Chilly.", + "skyInfo": "18", + "skyDescription": "Overcast", + "temperature": "0.60", + "temperatureDesc": "Chilly", + "comfort": "-2.88", + "humidity": "92", + "dewPoint": "-0.50", + "precipitationProbability": "8", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "11.16", + "windDirection": "305", + "windDesc": "Northwest", + "windDescShort": "NW", + "beaufortScale": "3", + "beaufortDescription": "Gentle breeze", + "visibility": "11.63", + "icon": "17", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", + "dayOfWeek": "4", + "weekday": "Wednesday", + "utcTime": "2019-11-21T00:00:00.000-05:00" + }, + { + "daylight": "D", + "daySegment": "Morning", + "description": "Cloudy. Cold.", + "skyInfo": "17", + "skyDescription": "Cloudy", + "temperature": "-0.10", + "temperatureDesc": "Cold", + "comfort": "-2.71", + "humidity": "99", + "dewPoint": "-0.20", + "precipitationProbability": "3", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "7.56", + "windDirection": "292", + "windDesc": "West", + "windDescShort": "W", + "beaufortScale": "2", + "beaufortDescription": "Light breeze", + "visibility": "12.53", + "icon": "7", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/17.png", + "dayOfWeek": "5", + "weekday": "Thursday", + "utcTime": "2019-11-21T06:00:00.000-05:00" + }, + { + "daylight": "D", + "daySegment": "Afternoon", + "description": "Partly sunny. Chilly.", + "skyInfo": "14", + "skyDescription": "Partly sunny", + "temperature": "3.50", + "temperatureDesc": "Chilly", + "comfort": "1.20", + "humidity": "79", + "dewPoint": "0.20", + "precipitationProbability": "5", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "8.64", + "windDirection": "180", + "windDesc": "South", + "windDescShort": "S", + "beaufortScale": "2", + "beaufortDescription": "Light breeze", + "visibility": "11.57", + "icon": "6", + "iconName": "mostly_cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/17.png", + "dayOfWeek": "5", + "weekday": "Thursday", + "utcTime": "2019-11-21T12:00:00.000-05:00" + }, + { + "daylight": "N", + "daySegment": "Evening", + "description": "Overcast. Chilly.", + "skyInfo": "18", + "skyDescription": "Overcast", + "temperature": "3.10", + "temperatureDesc": "Chilly", + "comfort": "-1.16", + "humidity": "84", + "dewPoint": "0.60", + "precipitationProbability": "9", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "18.72", + "windDirection": "170", + "windDesc": "South", + "windDescShort": "S", + "beaufortScale": "4", + "beaufortDescription": "Moderate breeze", + "visibility": "11.91", + "icon": "17", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", + "dayOfWeek": "5", + "weekday": "Thursday", + "utcTime": "2019-11-21T18:00:00.000-05:00" + }, + { + "daylight": "N", + "daySegment": "Night", + "description": "Sprinkles. Cloudy. Chilly.", + "skyInfo": "17", + "skyDescription": "Cloudy", + "temperature": "4.10", + "temperatureDesc": "Chilly", + "comfort": "-0.47", + "humidity": "91", + "dewPoint": "2.70", + "precipitationProbability": "33", + "precipitationDesc": "Sprinkles", + "rainFall": "0.06", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "23.40", + "windDirection": "205", + "windDesc": "Southwest", + "windDescShort": "SW", + "beaufortScale": "4", + "beaufortDescription": "Moderate breeze", + "visibility": "0.00", + "icon": "34", + "iconName": "night_sprinkles", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/22.png", + "dayOfWeek": "5", + "weekday": "Thursday", + "utcTime": "2019-11-22T00:00:00.000-05:00" + }, + { + "daylight": "D", + "daySegment": "Morning", + "description": "Sprinkles. Cloudy. Chilly.", + "skyInfo": "17", + "skyDescription": "Cloudy", + "temperature": "6.90", + "temperatureDesc": "Chilly", + "comfort": "3.53", + "humidity": "90", + "dewPoint": "5.40", + "precipitationProbability": "54", + "precipitationDesc": "Sprinkles", + "rainFall": "0.18", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "19.44", + "windDirection": "232", + "windDesc": "Southwest", + "windDescShort": "SW", + "beaufortScale": "4", + "beaufortDescription": "Moderate breeze", + "visibility": "0.00", + "icon": "18", + "iconName": "sprinkles", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/27.png", + "dayOfWeek": "6", + "weekday": "Friday", + "utcTime": "2019-11-22T06:00:00.000-05:00" + }, + { + "daylight": "D", + "daySegment": "Afternoon", + "description": "Overcast. Chilly.", + "skyInfo": "18", + "skyDescription": "Overcast", + "temperature": "2.60", + "temperatureDesc": "Chilly", + "comfort": "-3.49", + "humidity": "85", + "dewPoint": "0.30", + "precipitationProbability": "8", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "13", + "airDescription": "Breezy", + "windSpeed": "34.56", + "windDirection": "275", + "windDesc": "West", + "windDescShort": "W", + "beaufortScale": "5", + "beaufortDescription": "Fresh breeze", + "visibility": "0.00", + "icon": "7", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/17.png", + "dayOfWeek": "6", + "weekday": "Friday", + "utcTime": "2019-11-22T12:00:00.000-05:00" + }, + { + "daylight": "N", + "daySegment": "Evening", + "description": "Overcast. Cold.", + "skyInfo": "18", + "skyDescription": "Overcast", + "temperature": "-2.00", + "temperatureDesc": "Cold", + "comfort": "-9.70", + "humidity": "78", + "dewPoint": "-5.40", + "precipitationProbability": "7", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "13", + "airDescription": "Breezy", + "windSpeed": "36.00", + "windDirection": "294", + "windDesc": "Northwest", + "windDescShort": "NW", + "beaufortScale": "6", + "beaufortDescription": "Strong breeze", + "visibility": "0.00", + "icon": "17", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", + "dayOfWeek": "6", + "weekday": "Friday", + "utcTime": "2019-11-22T18:00:00.000-05:00" + }, + { + "daylight": "N", + "daySegment": "Night", + "description": "Partly cloudy. Cold.", + "skyInfo": "10", + "skyDescription": "Partly cloudy", + "temperature": "-5.20", + "temperatureDesc": "Cold", + "comfort": "-10.93", + "humidity": "81", + "dewPoint": "-6.50", + "precipitationProbability": "2", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "27.00", + "windDirection": "302", + "windDesc": "Northwest", + "windDescShort": "NW", + "beaufortScale": "4", + "beaufortDescription": "Moderate breeze", + "visibility": "0.00", + "icon": "15", + "iconName": "night_partly_cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", + "dayOfWeek": "6", + "weekday": "Friday", + "utcTime": "2019-11-23T00:00:00.000-05:00" + }, + { + "daylight": "D", + "daySegment": "Morning", + "description": "a mixture of sun and clouds. Cold.", + "skyInfo": "11", + "skyDescription": "a mixture of sun and clouds", + "temperature": "-3.30", + "temperatureDesc": "Cold", + "comfort": "-8.03", + "humidity": "80", + "dewPoint": "-6.30", + "precipitationProbability": "3", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "12.96", + "windDirection": "323", + "windDesc": "Northwest", + "windDescShort": "NW", + "beaufortScale": "3", + "beaufortDescription": "Gentle breeze", + "visibility": "0.00", + "icon": "4", + "iconName": "partly_cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/3.png", + "dayOfWeek": "7", + "weekday": "Saturday", + "utcTime": "2019-11-23T06:00:00.000-05:00" + }, + { + "daylight": "D", + "daySegment": "Afternoon", + "description": "a mixture of sun and clouds. Chilly.", + "skyInfo": "11", + "skyDescription": "a mixture of sun and clouds", + "temperature": "1.20", + "temperatureDesc": "Chilly", + "comfort": "-1.66", + "humidity": "71", + "dewPoint": "-3.90", + "precipitationProbability": "4", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "7.92", + "windDirection": "293", + "windDesc": "Northwest", + "windDescShort": "NW", + "beaufortScale": "2", + "beaufortDescription": "Light breeze", + "visibility": "0.00", + "icon": "4", + "iconName": "partly_cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/3.png", + "dayOfWeek": "7", + "weekday": "Saturday", + "utcTime": "2019-11-23T12:00:00.000-05:00" + }, + { + "daylight": "N", + "daySegment": "Evening", + "description": "Partly cloudy. Cold.", + "skyInfo": "10", + "skyDescription": "Partly cloudy", + "temperature": "-0.20", + "temperatureDesc": "Cold", + "comfort": "-3.28", + "humidity": "85", + "dewPoint": "-2.50", + "precipitationProbability": "3", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "9.00", + "windDirection": "155", + "windDesc": "Southeast", + "windDescShort": "SE", + "beaufortScale": "2", + "beaufortDescription": "Light breeze", + "visibility": "0.00", + "icon": "15", + "iconName": "night_partly_cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", + "dayOfWeek": "7", + "weekday": "Saturday", + "utcTime": "2019-11-23T18:00:00.000-05:00" + }, + { + "daylight": "N", + "daySegment": "Night", + "description": "Partly cloudy. Cold.", + "skyInfo": "10", + "skyDescription": "Partly cloudy", + "temperature": "-1.60", + "temperatureDesc": "Cold", + "comfort": "-3.99", + "humidity": "93", + "dewPoint": "-2.30", + "precipitationProbability": "8", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "7.20", + "windDirection": "77", + "windDesc": "East", + "windDescShort": "E", + "beaufortScale": "2", + "beaufortDescription": "Light breeze", + "visibility": "0.00", + "icon": "15", + "iconName": "night_partly_cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", + "dayOfWeek": "7", + "weekday": "Saturday", + "utcTime": "2019-11-24T00:00:00.000-05:00" + }, + { + "daylight": "D", + "daySegment": "Morning", + "description": "a mixture of sun and clouds. Cold.", + "skyInfo": "11", + "skyDescription": "a mixture of sun and clouds", + "temperature": "-0.80", + "temperatureDesc": "Cold", + "comfort": "-3.88", + "humidity": "92", + "dewPoint": "-1.90", + "precipitationProbability": "3", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "8.64", + "windDirection": "339", + "windDesc": "North", + "windDescShort": "N", + "beaufortScale": "2", + "beaufortDescription": "Light breeze", + "visibility": "0.00", + "icon": "4", + "iconName": "partly_cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/3.png", + "dayOfWeek": "1", + "weekday": "Sunday", + "utcTime": "2019-11-24T06:00:00.000-05:00" + }, + { + "daylight": "D", + "daySegment": "Afternoon", + "description": "a mixture of sun and clouds. Cold.", + "skyInfo": "11", + "skyDescription": "a mixture of sun and clouds", + "temperature": "0.60", + "temperatureDesc": "Cold", + "comfort": "-5.55", + "humidity": "86", + "dewPoint": "-2.20", + "precipitationProbability": "9", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "21.24", + "windDirection": "307", + "windDesc": "Northwest", + "windDescShort": "NW", + "beaufortScale": "4", + "beaufortDescription": "Moderate breeze", + "visibility": "0.00", + "icon": "4", + "iconName": "partly_cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/3.png", + "dayOfWeek": "1", + "weekday": "Sunday", + "utcTime": "2019-11-24T12:00:00.000-05:00" + }, + { + "daylight": "N", + "daySegment": "Evening", + "description": "Light snow. Cloudy. Chilly.", + "skyInfo": "17", + "skyDescription": "Cloudy", + "temperature": "0.30", + "temperatureDesc": "Chilly", + "comfort": "-4.32", + "humidity": "93", + "dewPoint": "-0.70", + "precipitationProbability": "25", + "precipitationDesc": "Light snow", + "rainFall": "*", + "snowFall": "0.60", + "airInfo": "*", + "airDescription": "", + "windSpeed": "16.56", + "windDirection": "296", + "windDesc": "Northwest", + "windDescShort": "NW", + "beaufortScale": "3", + "beaufortDescription": "Gentle breeze", + "visibility": "0.00", + "icon": "29", + "iconName": "light_snow", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/20.png", + "dayOfWeek": "1", + "weekday": "Sunday", + "utcTime": "2019-11-24T18:00:00.000-05:00" + }, + { + "daylight": "N", + "daySegment": "Night", + "description": "Cloudy. Cold.", + "skyInfo": "17", + "skyDescription": "Cloudy", + "temperature": "-1.00", + "temperatureDesc": "Cold", + "comfort": "-5.06", + "humidity": "99", + "dewPoint": "-0.70", + "precipitationProbability": "11", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "15.12", + "windDirection": "309", + "windDesc": "Northwest", + "windDescShort": "NW", + "beaufortScale": "3", + "beaufortDescription": "Gentle breeze", + "visibility": "0.00", + "icon": "17", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", + "dayOfWeek": "1", + "weekday": "Sunday", + "utcTime": "2019-11-25T00:00:00.000-05:00" + }, + { + "daylight": "D", + "daySegment": "Morning", + "description": "a mixture of sun and clouds. Chilly.", + "skyInfo": "11", + "skyDescription": "a mixture of sun and clouds", + "temperature": "0.30", + "temperatureDesc": "Chilly", + "comfort": "-4.26", + "humidity": "90", + "dewPoint": "-1.20", + "precipitationProbability": "12", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "16.20", + "windDirection": "309", + "windDesc": "Northwest", + "windDescShort": "NW", + "beaufortScale": "3", + "beaufortDescription": "Gentle breeze", + "visibility": "0.00", + "icon": "4", + "iconName": "partly_cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/3.png", + "dayOfWeek": "2", + "weekday": "Monday", + "utcTime": "2019-11-25T06:00:00.000-05:00" + } + ], + "country": "United States", + "state": "New York", + "city": "New York", + "latitude": 43.00035, + "longitude": -75.4999, + "distance": 0.00, + "timezone": -5 + } + }, + "feedCreation": "2019-11-19T22:49:09.348Z", + "metric": true +} \ No newline at end of file diff --git a/tests/fixtures/here_weather/destination_weather_forecasts_astronomy.json b/tests/fixtures/here_weather/destination_weather_forecasts_astronomy.json new file mode 100644 index 00000000000000..21285a2cef162e --- /dev/null +++ b/tests/fixtures/here_weather/destination_weather_forecasts_astronomy.json @@ -0,0 +1,118 @@ +{ + "astronomy": { + "astronomy": [ + { + "sunrise": "6:55AM", + "sunset": "6:33PM", + "moonrise": "1:27PM", + "moonset": "10:58PM", + "moonPhase": 0.328, + "moonPhaseDesc": "Waxing crescent", + "iconName": "cw_waxing_crescent", + "city": "Edgewater", + "latitude": 40.82, + "longitude": -73.97, + "utcTime": "2019-10-04T00:00:00.000-04:00" + }, + { + "sunrise": "6:56AM", + "sunset": "6:31PM", + "moonrise": "2:21PM", + "moonset": "11:50PM", + "moonPhase": 0.429, + "moonPhaseDesc": "First Quarter", + "iconName": "cw_first_qtr", + "city": "Edgewater", + "latitude": 40.82, + "longitude": -73.97, + "utcTime": "2019-10-05T00:00:00.000-04:00" + }, + { + "sunrise": "6:57AM", + "sunset": "6:30PM", + "moonrise": "3:10PM", + "moonset": "*", + "moonPhase": 0.530, + "moonPhaseDesc": "First Quarter", + "iconName": "cw_first_qtr", + "city": "Edgewater", + "latitude": 40.82, + "longitude": -73.97, + "utcTime": "2019-10-06T00:00:00.000-04:00" + }, + { + "sunrise": "6:58AM", + "sunset": "6:28PM", + "moonrise": "3:51PM", + "moonset": "12:45AM", + "moonPhase": 0.627, + "moonPhaseDesc": "Waxing gibbous", + "iconName": "cw_waxing_gibbous", + "city": "Edgewater", + "latitude": 40.82, + "longitude": -73.97, + "utcTime": "2019-10-07T00:00:00.000-04:00" + }, + { + "sunrise": "6:59AM", + "sunset": "6:26PM", + "moonrise": "4:26PM", + "moonset": "1:43AM", + "moonPhase": 0.717, + "moonPhaseDesc": "Waxing gibbous", + "iconName": "cw_waxing_gibbous", + "city": "Edgewater", + "latitude": 40.82, + "longitude": -73.97, + "utcTime": "2019-10-08T00:00:00.000-04:00" + }, + { + "sunrise": "7:00AM", + "sunset": "6:25PM", + "moonrise": "4:57PM", + "moonset": "2:41AM", + "moonPhase": 0.798, + "moonPhaseDesc": "Waxing gibbous", + "iconName": "cw_waxing_gibbous", + "city": "Edgewater", + "latitude": 40.82, + "longitude": -73.97, + "utcTime": "2019-10-09T00:00:00.000-04:00" + }, + { + "sunrise": "7:01AM", + "sunset": "6:23PM", + "moonrise": "5:25PM", + "moonset": "3:40AM", + "moonPhase": 0.868, + "moonPhaseDesc": "Waxing gibbous", + "iconName": "cw_waxing_gibbous", + "city": "Edgewater", + "latitude": 40.82, + "longitude": -73.97, + "utcTime": "2019-10-10T00:00:00.000-04:00" + }, + { + "sunrise": "7:02AM", + "sunset": "6:22PM", + "moonrise": "5:51PM", + "moonset": "4:38AM", + "moonPhase": 0.924, + "moonPhaseDesc": "Waxing gibbous", + "iconName": "cw_waxing_gibbous", + "city": "Edgewater", + "latitude": 40.82, + "longitude": -73.97, + "utcTime": "2019-10-11T00:00:00.000-04:00" + } + ], + "country": "United States", + "state": "New York", + "city": "New York", + "latitude": 40.79962, + "longitude": -73.970314, + "timezone": -5 + }, + "feedCreation": "2019-10-04T14:22:46.164Z", + "metric": true +} \ No newline at end of file diff --git a/tests/fixtures/here_weather/destination_weather_forecasts_hourly.json b/tests/fixtures/here_weather/destination_weather_forecasts_hourly.json new file mode 100644 index 00000000000000..be114fd2dc2cf1 --- /dev/null +++ b/tests/fixtures/here_weather/destination_weather_forecasts_hourly.json @@ -0,0 +1,5057 @@ +{ + "hourlyForecasts": { + "forecastLocation": { + "forecast": [ + { + "daylight": "D", + "description": "Overcast. Chilly.", + "skyInfo": "18", + "skyDescription": "Overcast", + "temperature": "4.00", + "temperatureDesc": "Chilly", + "comfort": "0.67", + "humidity": "74", + "dewPoint": "-0.30", + "precipitationProbability": "5", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "14.04", + "windDirection": "287", + "windDesc": "West", + "windDescShort": "W", + "visibility": "11.31", + "icon": "7", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/17.png", + "dayOfWeek": "3", + "weekday": "Tuesday", + "utcTime": "2019-11-19T13:00:00.000-05:00", + "localTime": "1311192019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "D", + "description": "Overcast. Chilly.", + "skyInfo": "18", + "skyDescription": "Overcast", + "temperature": "3.70", + "temperatureDesc": "Chilly", + "comfort": "0.37", + "humidity": "75", + "dewPoint": "-0.40", + "precipitationProbability": "6", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "13.68", + "windDirection": "290", + "windDesc": "West", + "windDescShort": "W", + "visibility": "12.27", + "icon": "7", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/17.png", + "dayOfWeek": "3", + "weekday": "Tuesday", + "utcTime": "2019-11-19T14:00:00.000-05:00", + "localTime": "1411192019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "D", + "description": "Overcast. Chilly.", + "skyInfo": "18", + "skyDescription": "Overcast", + "temperature": "2.90", + "temperatureDesc": "Chilly", + "comfort": "-0.48", + "humidity": "79", + "dewPoint": "-0.40", + "precipitationProbability": "6", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "12.96", + "windDirection": "294", + "windDesc": "Northwest", + "windDescShort": "NW", + "visibility": "11.59", + "icon": "7", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/17.png", + "dayOfWeek": "3", + "weekday": "Tuesday", + "utcTime": "2019-11-19T15:00:00.000-05:00", + "localTime": "1511192019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "D", + "description": "Overcast. Chilly.", + "skyInfo": "18", + "skyDescription": "Overcast", + "temperature": "2.00", + "temperatureDesc": "Chilly", + "comfort": "-1.35", + "humidity": "85", + "dewPoint": "-0.30", + "precipitationProbability": "7", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "11.88", + "windDirection": "298", + "windDesc": "Northwest", + "windDescShort": "NW", + "visibility": "11.99", + "icon": "7", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/17.png", + "dayOfWeek": "3", + "weekday": "Tuesday", + "utcTime": "2019-11-19T16:00:00.000-05:00", + "localTime": "1611192019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Overcast. Chilly.", + "skyInfo": "18", + "skyDescription": "Overcast", + "temperature": "1.70", + "temperatureDesc": "Chilly", + "comfort": "-1.30", + "humidity": "86", + "dewPoint": "-0.50", + "precipitationProbability": "8", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "32", + "airDescription": "Damp", + "windSpeed": "10.08", + "windDirection": "297", + "windDesc": "Northwest", + "windDescShort": "NW", + "visibility": "12.36", + "icon": "17", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", + "dayOfWeek": "3", + "weekday": "Tuesday", + "utcTime": "2019-11-19T17:00:00.000-05:00", + "localTime": "1711192019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Overcast. Chilly.", + "skyInfo": "18", + "skyDescription": "Overcast", + "temperature": "0.90", + "temperatureDesc": "Chilly", + "comfort": "-1.97", + "humidity": "89", + "dewPoint": "-0.80", + "precipitationProbability": "9", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "32", + "airDescription": "Damp", + "windSpeed": "9.00", + "windDirection": "291", + "windDesc": "West", + "windDescShort": "W", + "visibility": "11.77", + "icon": "17", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", + "dayOfWeek": "3", + "weekday": "Tuesday", + "utcTime": "2019-11-19T18:00:00.000-05:00", + "localTime": "1811192019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Overcast. Chilly.", + "skyInfo": "18", + "skyDescription": "Overcast", + "temperature": "0.00", + "temperatureDesc": "Chilly", + "comfort": "-2.83", + "humidity": "92", + "dewPoint": "-1.10", + "precipitationProbability": "10", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "8.28", + "windDirection": "287", + "windDesc": "West", + "windDescShort": "W", + "visibility": "12.18", + "icon": "17", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", + "dayOfWeek": "3", + "weekday": "Tuesday", + "utcTime": "2019-11-19T19:00:00.000-05:00", + "localTime": "1911192019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Overcast. Cold.", + "skyInfo": "18", + "skyDescription": "Overcast", + "temperature": "-0.30", + "temperatureDesc": "Cold", + "comfort": "-3.06", + "humidity": "92", + "dewPoint": "-1.40", + "precipitationProbability": "9", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "7.92", + "windDirection": "287", + "windDesc": "West", + "windDescShort": "W", + "visibility": "12.82", + "icon": "17", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", + "dayOfWeek": "3", + "weekday": "Tuesday", + "utcTime": "2019-11-19T20:00:00.000-05:00", + "localTime": "2011192019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Overcast. Cold.", + "skyInfo": "18", + "skyDescription": "Overcast", + "temperature": "-0.30", + "temperatureDesc": "Cold", + "comfort": "-3.06", + "humidity": "90", + "dewPoint": "-1.70", + "precipitationProbability": "9", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "7.92", + "windDirection": "290", + "windDesc": "West", + "windDescShort": "W", + "visibility": "12.48", + "icon": "17", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", + "dayOfWeek": "3", + "weekday": "Tuesday", + "utcTime": "2019-11-19T21:00:00.000-05:00", + "localTime": "2111192019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Overcast. Cold.", + "skyInfo": "18", + "skyDescription": "Overcast", + "temperature": "-0.70", + "temperatureDesc": "Cold", + "comfort": "-3.53", + "humidity": "92", + "dewPoint": "-1.90", + "precipitationProbability": "8", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "7.92", + "windDirection": "293", + "windDesc": "Northwest", + "windDescShort": "NW", + "visibility": "11.33", + "icon": "17", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", + "dayOfWeek": "3", + "weekday": "Tuesday", + "utcTime": "2019-11-19T22:00:00.000-05:00", + "localTime": "2211192019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Overcast. Cold.", + "skyInfo": "18", + "skyDescription": "Overcast", + "temperature": "-1.20", + "temperatureDesc": "Cold", + "comfort": "-4.12", + "humidity": "94", + "dewPoint": "-2.10", + "precipitationProbability": "8", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "7.92", + "windDirection": "296", + "windDesc": "Northwest", + "windDescShort": "NW", + "visibility": "12.01", + "icon": "17", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", + "dayOfWeek": "3", + "weekday": "Tuesday", + "utcTime": "2019-11-19T23:00:00.000-05:00", + "localTime": "2311192019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Overcast. Cold.", + "skyInfo": "18", + "skyDescription": "Overcast", + "temperature": "-1.20", + "temperatureDesc": "Cold", + "comfort": "-4.12", + "humidity": "94", + "dewPoint": "-2.10", + "precipitationProbability": "8", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "7.92", + "windDirection": "299", + "windDesc": "Northwest", + "windDescShort": "NW", + "visibility": "12.83", + "icon": "17", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", + "dayOfWeek": "4", + "weekday": "Wednesday", + "utcTime": "2019-11-20T00:00:00.000-05:00", + "localTime": "0011202019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Overcast. Cold.", + "skyInfo": "18", + "skyDescription": "Overcast", + "temperature": "-1.20", + "temperatureDesc": "Cold", + "comfort": "-4.12", + "humidity": "93", + "dewPoint": "-2.20", + "precipitationProbability": "7", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "7.92", + "windDirection": "301", + "windDesc": "Northwest", + "windDescShort": "NW", + "visibility": "11.30", + "icon": "17", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", + "dayOfWeek": "4", + "weekday": "Wednesday", + "utcTime": "2019-11-20T01:00:00.000-05:00", + "localTime": "0111202019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Overcast. Cold.", + "skyInfo": "18", + "skyDescription": "Overcast", + "temperature": "-1.50", + "temperatureDesc": "Cold", + "comfort": "-4.47", + "humidity": "94", + "dewPoint": "-2.40", + "precipitationProbability": "7", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "7.92", + "windDirection": "302", + "windDesc": "Northwest", + "windDescShort": "NW", + "visibility": "12.53", + "icon": "17", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", + "dayOfWeek": "4", + "weekday": "Wednesday", + "utcTime": "2019-11-20T02:00:00.000-05:00", + "localTime": "0211202019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Overcast. Cold.", + "skyInfo": "18", + "skyDescription": "Overcast", + "temperature": "-1.80", + "temperatureDesc": "Cold", + "comfort": "-4.83", + "humidity": "94", + "dewPoint": "-2.70", + "precipitationProbability": "6", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "7.92", + "windDirection": "301", + "windDesc": "Northwest", + "windDescShort": "NW", + "visibility": "12.02", + "icon": "17", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", + "dayOfWeek": "4", + "weekday": "Wednesday", + "utcTime": "2019-11-20T03:00:00.000-05:00", + "localTime": "0311202019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Overcast. Cold.", + "skyInfo": "18", + "skyDescription": "Overcast", + "temperature": "-1.90", + "temperatureDesc": "Cold", + "comfort": "-5.06", + "humidity": "94", + "dewPoint": "-2.80", + "precipitationProbability": "5", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "8.28", + "windDirection": "300", + "windDesc": "Northwest", + "windDescShort": "NW", + "visibility": "12.47", + "icon": "17", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", + "dayOfWeek": "4", + "weekday": "Wednesday", + "utcTime": "2019-11-20T04:00:00.000-05:00", + "localTime": "0411202019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Overcast. Cold.", + "skyInfo": "18", + "skyDescription": "Overcast", + "temperature": "-1.90", + "temperatureDesc": "Cold", + "comfort": "-5.06", + "humidity": "94", + "dewPoint": "-2.70", + "precipitationProbability": "7", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "8.28", + "windDirection": "297", + "windDesc": "Northwest", + "windDescShort": "NW", + "visibility": "12.62", + "icon": "17", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", + "dayOfWeek": "4", + "weekday": "Wednesday", + "utcTime": "2019-11-20T05:00:00.000-05:00", + "localTime": "0511202019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "D", + "description": "Overcast. Cold.", + "skyInfo": "18", + "skyDescription": "Overcast", + "temperature": "-1.70", + "temperatureDesc": "Cold", + "comfort": "-4.71", + "humidity": "93", + "dewPoint": "-2.70", + "precipitationProbability": "8", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "7.92", + "windDirection": "292", + "windDesc": "West", + "windDescShort": "W", + "visibility": "12.01", + "icon": "7", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/17.png", + "dayOfWeek": "4", + "weekday": "Wednesday", + "utcTime": "2019-11-20T06:00:00.000-05:00", + "localTime": "0611202019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "D", + "description": "Overcast. Cold.", + "skyInfo": "18", + "skyDescription": "Overcast", + "temperature": "-1.60", + "temperatureDesc": "Cold", + "comfort": "-4.59", + "humidity": "94", + "dewPoint": "-2.40", + "precipitationProbability": "9", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "7.92", + "windDirection": "290", + "windDesc": "West", + "windDescShort": "W", + "visibility": "12.46", + "icon": "7", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/17.png", + "dayOfWeek": "4", + "weekday": "Wednesday", + "utcTime": "2019-11-20T07:00:00.000-05:00", + "localTime": "0711202019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "D", + "description": "Light snow. Overcast. Cold.", + "skyInfo": "18", + "skyDescription": "Overcast", + "temperature": "-1.30", + "temperatureDesc": "Cold", + "comfort": "-4.58", + "humidity": "96", + "dewPoint": "-1.90", + "precipitationProbability": "26", + "precipitationDesc": "Light snow", + "rainFall": "*", + "snowFall": "0.13", + "airInfo": "*", + "airDescription": "", + "windSpeed": "9.00", + "windDirection": "298", + "windDesc": "Northwest", + "windDescShort": "NW", + "visibility": "12.12", + "icon": "29", + "iconName": "light_snow", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/20.png", + "dayOfWeek": "4", + "weekday": "Wednesday", + "utcTime": "2019-11-20T08:00:00.000-05:00", + "localTime": "0811202019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "D", + "description": "Light snow. Overcast. Cold.", + "skyInfo": "18", + "skyDescription": "Overcast", + "temperature": "-0.30", + "temperatureDesc": "Cold", + "comfort": "-3.79", + "humidity": "93", + "dewPoint": "-1.30", + "precipitationProbability": "33", + "precipitationDesc": "Light snow", + "rainFall": "*", + "snowFall": "0.10", + "airInfo": "*", + "airDescription": "", + "windSpeed": "10.44", + "windDirection": "308", + "windDesc": "Northwest", + "windDescShort": "NW", + "visibility": "11.52", + "icon": "29", + "iconName": "light_snow", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/20.png", + "dayOfWeek": "4", + "weekday": "Wednesday", + "utcTime": "2019-11-20T09:00:00.000-05:00", + "localTime": "0911202019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "D", + "description": "Light snow. Overcast. Chilly.", + "skyInfo": "18", + "skyDescription": "Overcast", + "temperature": "0.80", + "temperatureDesc": "Chilly", + "comfort": "-2.81", + "humidity": "90", + "dewPoint": "-0.70", + "precipitationProbability": "40", + "precipitationDesc": "Light snow", + "rainFall": "*", + "snowFall": "0.10", + "airInfo": "*", + "airDescription": "", + "windSpeed": "11.88", + "windDirection": "311", + "windDesc": "Northwest", + "windDescShort": "NW", + "visibility": "11.49", + "icon": "29", + "iconName": "light_snow", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/20.png", + "dayOfWeek": "4", + "weekday": "Wednesday", + "utcTime": "2019-11-20T10:00:00.000-05:00", + "localTime": "1011202019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "D", + "description": "Light snow. Overcast. Chilly.", + "skyInfo": "18", + "skyDescription": "Overcast", + "temperature": "1.60", + "temperatureDesc": "Chilly", + "comfort": "-2.27", + "humidity": "87", + "dewPoint": "-0.40", + "precipitationProbability": "41", + "precipitationDesc": "Light snow", + "rainFall": "*", + "snowFall": "0.10", + "airInfo": "32", + "airDescription": "Damp", + "windSpeed": "14.04", + "windDirection": "310", + "windDesc": "Northwest", + "windDescShort": "NW", + "visibility": "12.54", + "icon": "29", + "iconName": "light_snow", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/20.png", + "dayOfWeek": "4", + "weekday": "Wednesday", + "utcTime": "2019-11-20T11:00:00.000-05:00", + "localTime": "1111202019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "D", + "description": "Light snow. Overcast. Chilly.", + "skyInfo": "18", + "skyDescription": "Overcast", + "temperature": "2.00", + "temperatureDesc": "Chilly", + "comfort": "-2.09", + "humidity": "86", + "dewPoint": "-0.20", + "precipitationProbability": "42", + "precipitationDesc": "Light snow", + "rainFall": "*", + "snowFall": "0.10", + "airInfo": "*", + "airDescription": "", + "windSpeed": "15.84", + "windDirection": "308", + "windDesc": "Northwest", + "windDescShort": "NW", + "visibility": "12.86", + "icon": "29", + "iconName": "light_snow", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/20.png", + "dayOfWeek": "4", + "weekday": "Wednesday", + "utcTime": "2019-11-20T12:00:00.000-05:00", + "localTime": "1211202019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "D", + "description": "Sprinkles. Overcast. Chilly.", + "skyInfo": "18", + "skyDescription": "Overcast", + "temperature": "2.30", + "temperatureDesc": "Chilly", + "comfort": "-1.95", + "humidity": "85", + "dewPoint": "0.00", + "precipitationProbability": "43", + "precipitationDesc": "Sprinkles", + "rainFall": "0.01", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "17.28", + "windDirection": "308", + "windDesc": "Northwest", + "windDescShort": "NW", + "visibility": "12.36", + "icon": "18", + "iconName": "sprinkles", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/27.png", + "dayOfWeek": "4", + "weekday": "Wednesday", + "utcTime": "2019-11-20T13:00:00.000-05:00", + "localTime": "1311202019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "D", + "description": "Sprinkles. Overcast. Chilly.", + "skyInfo": "18", + "skyDescription": "Overcast", + "temperature": "2.30", + "temperatureDesc": "Chilly", + "comfort": "-2.11", + "humidity": "84", + "dewPoint": "-0.10", + "precipitationProbability": "42", + "precipitationDesc": "Sprinkles", + "rainFall": "0.01", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "18.36", + "windDirection": "308", + "windDesc": "Northwest", + "windDescShort": "NW", + "visibility": "12.31", + "icon": "18", + "iconName": "sprinkles", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/27.png", + "dayOfWeek": "4", + "weekday": "Wednesday", + "utcTime": "2019-11-20T14:00:00.000-05:00", + "localTime": "1411202019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "D", + "description": "Sprinkles. Overcast. Chilly.", + "skyInfo": "18", + "skyDescription": "Overcast", + "temperature": "2.00", + "temperatureDesc": "Chilly", + "comfort": "-2.49", + "humidity": "85", + "dewPoint": "-0.30", + "precipitationProbability": "41", + "precipitationDesc": "Sprinkles", + "rainFall": "0.01", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "18.36", + "windDirection": "310", + "windDesc": "Northwest", + "windDescShort": "NW", + "visibility": "11.66", + "icon": "18", + "iconName": "sprinkles", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/27.png", + "dayOfWeek": "4", + "weekday": "Wednesday", + "utcTime": "2019-11-20T15:00:00.000-05:00", + "localTime": "1511202019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "D", + "description": "Light snow. Overcast. Chilly.", + "skyInfo": "18", + "skyDescription": "Overcast", + "temperature": "1.70", + "temperatureDesc": "Chilly", + "comfort": "-2.75", + "humidity": "86", + "dewPoint": "-0.50", + "precipitationProbability": "40", + "precipitationDesc": "Light snow", + "rainFall": "*", + "snowFall": "0.10", + "airInfo": "*", + "airDescription": "", + "windSpeed": "17.64", + "windDirection": "311", + "windDesc": "Northwest", + "windDescShort": "NW", + "visibility": "11.31", + "icon": "29", + "iconName": "light_snow", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/20.png", + "dayOfWeek": "4", + "weekday": "Wednesday", + "utcTime": "2019-11-20T16:00:00.000-05:00", + "localTime": "1611202019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Light snow. Overcast. Chilly.", + "skyInfo": "18", + "skyDescription": "Overcast", + "temperature": "1.30", + "temperatureDesc": "Chilly", + "comfort": "-3.02", + "humidity": "87", + "dewPoint": "-0.70", + "precipitationProbability": "36", + "precipitationDesc": "Light snow", + "rainFall": "*", + "snowFall": "0.10", + "airInfo": "*", + "airDescription": "", + "windSpeed": "16.20", + "windDirection": "311", + "windDesc": "Northwest", + "windDescShort": "NW", + "visibility": "12.19", + "icon": "29", + "iconName": "light_snow", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/20.png", + "dayOfWeek": "4", + "weekday": "Wednesday", + "utcTime": "2019-11-20T17:00:00.000-05:00", + "localTime": "1711202019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Light snow. Overcast. Chilly.", + "skyInfo": "18", + "skyDescription": "Overcast", + "temperature": "0.90", + "temperatureDesc": "Chilly", + "comfort": "-3.20", + "humidity": "89", + "dewPoint": "-0.80", + "precipitationProbability": "32", + "precipitationDesc": "Light snow", + "rainFall": "*", + "snowFall": "0.10", + "airInfo": "*", + "airDescription": "", + "windSpeed": "14.40", + "windDirection": "311", + "windDesc": "Northwest", + "windDescShort": "NW", + "visibility": "12.21", + "icon": "29", + "iconName": "light_snow", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/20.png", + "dayOfWeek": "4", + "weekday": "Wednesday", + "utcTime": "2019-11-20T18:00:00.000-05:00", + "localTime": "1811202019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Light snow. Overcast. Chilly.", + "skyInfo": "18", + "skyDescription": "Overcast", + "temperature": "0.50", + "temperatureDesc": "Chilly", + "comfort": "-3.48", + "humidity": "90", + "dewPoint": "-1.00", + "precipitationProbability": "28", + "precipitationDesc": "Light snow", + "rainFall": "*", + "snowFall": "0.10", + "airInfo": "32", + "airDescription": "Damp", + "windSpeed": "13.32", + "windDirection": "311", + "windDesc": "Northwest", + "windDescShort": "NW", + "visibility": "11.59", + "icon": "29", + "iconName": "light_snow", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/20.png", + "dayOfWeek": "4", + "weekday": "Wednesday", + "utcTime": "2019-11-20T19:00:00.000-05:00", + "localTime": "1911202019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Overcast. Chilly.", + "skyInfo": "18", + "skyDescription": "Overcast", + "temperature": "0.60", + "temperatureDesc": "Chilly", + "comfort": "-3.20", + "humidity": "90", + "dewPoint": "-0.90", + "precipitationProbability": "11", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "32", + "airDescription": "Damp", + "windSpeed": "12.60", + "windDirection": "310", + "windDesc": "Northwest", + "windDescShort": "NW", + "visibility": "12.20", + "icon": "17", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", + "dayOfWeek": "4", + "weekday": "Wednesday", + "utcTime": "2019-11-20T20:00:00.000-05:00", + "localTime": "2011202019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Overcast. Chilly.", + "skyInfo": "18", + "skyDescription": "Overcast", + "temperature": "0.70", + "temperatureDesc": "Chilly", + "comfort": "-2.93", + "humidity": "90", + "dewPoint": "-0.70", + "precipitationProbability": "8", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "11.88", + "windDirection": "309", + "windDesc": "Northwest", + "windDescShort": "NW", + "visibility": "11.36", + "icon": "17", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", + "dayOfWeek": "4", + "weekday": "Wednesday", + "utcTime": "2019-11-20T21:00:00.000-05:00", + "localTime": "2111202019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Overcast. Chilly.", + "skyInfo": "18", + "skyDescription": "Overcast", + "temperature": "0.80", + "temperatureDesc": "Chilly", + "comfort": "-2.72", + "humidity": "91", + "dewPoint": "-0.50", + "precipitationProbability": "5", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "11.52", + "windDirection": "308", + "windDesc": "Northwest", + "windDescShort": "NW", + "visibility": "11.49", + "icon": "17", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", + "dayOfWeek": "4", + "weekday": "Wednesday", + "utcTime": "2019-11-20T22:00:00.000-05:00", + "localTime": "2211202019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Overcast. Chilly.", + "skyInfo": "18", + "skyDescription": "Overcast", + "temperature": "0.80", + "temperatureDesc": "Chilly", + "comfort": "-2.81", + "humidity": "92", + "dewPoint": "-0.40", + "precipitationProbability": "5", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "11.88", + "windDirection": "309", + "windDesc": "Northwest", + "windDescShort": "NW", + "visibility": "12.39", + "icon": "17", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", + "dayOfWeek": "4", + "weekday": "Wednesday", + "utcTime": "2019-11-20T23:00:00.000-05:00", + "localTime": "2311202019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Overcast. Chilly.", + "skyInfo": "18", + "skyDescription": "Overcast", + "temperature": "0.80", + "temperatureDesc": "Chilly", + "comfort": "-2.88", + "humidity": "92", + "dewPoint": "-0.30", + "precipitationProbability": "4", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "12.24", + "windDirection": "311", + "windDesc": "Northwest", + "windDescShort": "NW", + "visibility": "11.66", + "icon": "17", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", + "dayOfWeek": "5", + "weekday": "Thursday", + "utcTime": "2019-11-21T00:00:00.000-05:00", + "localTime": "0011212019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Overcast. Chilly.", + "skyInfo": "18", + "skyDescription": "Overcast", + "temperature": "0.80", + "temperatureDesc": "Chilly", + "comfort": "-2.88", + "humidity": "92", + "dewPoint": "-0.40", + "precipitationProbability": "3", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "12.24", + "windDirection": "312", + "windDesc": "Northwest", + "windDescShort": "NW", + "visibility": "11.30", + "icon": "17", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", + "dayOfWeek": "5", + "weekday": "Thursday", + "utcTime": "2019-11-21T01:00:00.000-05:00", + "localTime": "0111212019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Overcast. Chilly.", + "skyInfo": "18", + "skyDescription": "Overcast", + "temperature": "0.70", + "temperatureDesc": "Chilly", + "comfort": "-2.93", + "humidity": "92", + "dewPoint": "-0.40", + "precipitationProbability": "3", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "11.88", + "windDirection": "310", + "windDesc": "Northwest", + "windDescShort": "NW", + "visibility": "12.18", + "icon": "17", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", + "dayOfWeek": "5", + "weekday": "Thursday", + "utcTime": "2019-11-21T02:00:00.000-05:00", + "localTime": "0211212019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Overcast. Chilly.", + "skyInfo": "18", + "skyDescription": "Overcast", + "temperature": "0.60", + "temperatureDesc": "Chilly", + "comfort": "-2.88", + "humidity": "92", + "dewPoint": "-0.50", + "precipitationProbability": "3", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "11.16", + "windDirection": "305", + "windDesc": "Northwest", + "windDescShort": "NW", + "visibility": "12.73", + "icon": "17", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", + "dayOfWeek": "5", + "weekday": "Thursday", + "utcTime": "2019-11-21T03:00:00.000-05:00", + "localTime": "0311212019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Overcast. Chilly.", + "skyInfo": "18", + "skyDescription": "Overcast", + "temperature": "0.40", + "temperatureDesc": "Chilly", + "comfort": "-2.95", + "humidity": "92", + "dewPoint": "-0.70", + "precipitationProbability": "2", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "10.44", + "windDirection": "303", + "windDesc": "Northwest", + "windDescShort": "NW", + "visibility": "11.69", + "icon": "17", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", + "dayOfWeek": "5", + "weekday": "Thursday", + "utcTime": "2019-11-21T04:00:00.000-05:00", + "localTime": "0411212019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Overcast. Chilly.", + "skyInfo": "18", + "skyDescription": "Overcast", + "temperature": "0.30", + "temperatureDesc": "Chilly", + "comfort": "-2.98", + "humidity": "92", + "dewPoint": "-0.80", + "precipitationProbability": "2", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "10.08", + "windDirection": "305", + "windDesc": "Northwest", + "windDescShort": "NW", + "visibility": "12.07", + "icon": "17", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", + "dayOfWeek": "5", + "weekday": "Thursday", + "utcTime": "2019-11-21T05:00:00.000-05:00", + "localTime": "0511212019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "D", + "description": "Cloudy. Chilly.", + "skyInfo": "17", + "skyDescription": "Cloudy", + "temperature": "0.00", + "temperatureDesc": "Chilly", + "comfort": "-3.34", + "humidity": "94", + "dewPoint": "-0.90", + "precipitationProbability": "2", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "10.08", + "windDirection": "308", + "windDesc": "Northwest", + "windDescShort": "NW", + "visibility": "11.56", + "icon": "7", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/17.png", + "dayOfWeek": "5", + "weekday": "Thursday", + "utcTime": "2019-11-21T06:00:00.000-05:00", + "localTime": "0611212019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "D", + "description": "Cloudy. Cold.", + "skyInfo": "17", + "skyDescription": "Cloudy", + "temperature": "-0.10", + "temperatureDesc": "Cold", + "comfort": "-3.36", + "humidity": "94", + "dewPoint": "-1.00", + "precipitationProbability": "2", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "9.72", + "windDirection": "309", + "windDesc": "Northwest", + "windDescShort": "NW", + "visibility": "11.69", + "icon": "7", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/17.png", + "dayOfWeek": "5", + "weekday": "Thursday", + "utcTime": "2019-11-21T07:00:00.000-05:00", + "localTime": "0711212019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "D", + "description": "Cloudy. Chilly.", + "skyInfo": "17", + "skyDescription": "Cloudy", + "temperature": "0.20", + "temperatureDesc": "Chilly", + "comfort": "-2.70", + "humidity": "94", + "dewPoint": "-0.70", + "precipitationProbability": "2", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "8.64", + "windDirection": "304", + "windDesc": "Northwest", + "windDescShort": "NW", + "visibility": "11.31", + "icon": "7", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/17.png", + "dayOfWeek": "5", + "weekday": "Thursday", + "utcTime": "2019-11-21T08:00:00.000-05:00", + "localTime": "0811212019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "D", + "description": "Cloudy. Chilly.", + "skyInfo": "17", + "skyDescription": "Cloudy", + "temperature": "0.90", + "temperatureDesc": "Chilly", + "comfort": "-1.54", + "humidity": "92", + "dewPoint": "-0.20", + "precipitationProbability": "2", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "7.56", + "windDirection": "292", + "windDesc": "West", + "windDescShort": "W", + "visibility": "11.84", + "icon": "7", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/17.png", + "dayOfWeek": "5", + "weekday": "Thursday", + "utcTime": "2019-11-21T09:00:00.000-05:00", + "localTime": "0911212019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "D", + "description": "Cloudy. Chilly.", + "skyInfo": "17", + "skyDescription": "Cloudy", + "temperature": "1.70", + "temperatureDesc": "Chilly", + "comfort": "-0.25", + "humidity": "90", + "dewPoint": "0.20", + "precipitationProbability": "2", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "6.48", + "windDirection": "278", + "windDesc": "West", + "windDescShort": "W", + "visibility": "12.31", + "icon": "7", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/17.png", + "dayOfWeek": "5", + "weekday": "Thursday", + "utcTime": "2019-11-21T10:00:00.000-05:00", + "localTime": "1011212019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "D", + "description": "Cloudy. Chilly.", + "skyInfo": "17", + "skyDescription": "Cloudy", + "temperature": "2.30", + "temperatureDesc": "Chilly", + "comfort": "0.21", + "humidity": "87", + "dewPoint": "0.30", + "precipitationProbability": "3", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "7.20", + "windDirection": "267", + "windDesc": "West", + "windDescShort": "W", + "visibility": "11.88", + "icon": "7", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/17.png", + "dayOfWeek": "5", + "weekday": "Thursday", + "utcTime": "2019-11-21T11:00:00.000-05:00", + "localTime": "1111212019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "D", + "description": "Partly sunny. Chilly.", + "skyInfo": "14", + "skyDescription": "Partly sunny", + "temperature": "2.80", + "temperatureDesc": "Chilly", + "comfort": "0.57", + "humidity": "84", + "dewPoint": "0.40", + "precipitationProbability": "3", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "7.92", + "windDirection": "258", + "windDesc": "West", + "windDescShort": "W", + "visibility": "11.70", + "icon": "6", + "iconName": "mostly_cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/17.png", + "dayOfWeek": "5", + "weekday": "Thursday", + "utcTime": "2019-11-21T12:00:00.000-05:00", + "localTime": "1211212019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "D", + "description": "Partly sunny. Chilly.", + "skyInfo": "14", + "skyDescription": "Partly sunny", + "temperature": "3.20", + "temperatureDesc": "Chilly", + "comfort": "0.85", + "humidity": "82", + "dewPoint": "0.40", + "precipitationProbability": "4", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "8.64", + "windDirection": "248", + "windDesc": "West", + "windDescShort": "W", + "visibility": "14.31", + "icon": "6", + "iconName": "mostly_cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/17.png", + "dayOfWeek": "5", + "weekday": "Thursday", + "utcTime": "2019-11-21T13:00:00.000-05:00", + "localTime": "1311212019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "D", + "description": "Partly sunny. Chilly.", + "skyInfo": "14", + "skyDescription": "Partly sunny", + "temperature": "3.40", + "temperatureDesc": "Chilly", + "comfort": "1.08", + "humidity": "81", + "dewPoint": "0.40", + "precipitationProbability": "4", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "8.64", + "windDirection": "224", + "windDesc": "Southwest", + "windDescShort": "SW", + "visibility": "12.25", + "icon": "6", + "iconName": "mostly_cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/17.png", + "dayOfWeek": "5", + "weekday": "Thursday", + "utcTime": "2019-11-21T14:00:00.000-05:00", + "localTime": "1411212019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "D", + "description": "Partly sunny. Chilly.", + "skyInfo": "14", + "skyDescription": "Partly sunny", + "temperature": "3.50", + "temperatureDesc": "Chilly", + "comfort": "1.20", + "humidity": "79", + "dewPoint": "0.20", + "precipitationProbability": "5", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "8.64", + "windDirection": "180", + "windDesc": "South", + "windDescShort": "S", + "visibility": "11.57", + "icon": "6", + "iconName": "mostly_cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/17.png", + "dayOfWeek": "5", + "weekday": "Thursday", + "utcTime": "2019-11-21T15:00:00.000-05:00", + "localTime": "1511212019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "D", + "description": "Cloudy. Chilly.", + "skyInfo": "17", + "skyDescription": "Cloudy", + "temperature": "3.20", + "temperatureDesc": "Chilly", + "comfort": "0.75", + "humidity": "80", + "dewPoint": "0.10", + "precipitationProbability": "5", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "9.00", + "windDirection": "163", + "windDesc": "South", + "windDescShort": "S", + "visibility": "12.23", + "icon": "7", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/17.png", + "dayOfWeek": "5", + "weekday": "Thursday", + "utcTime": "2019-11-21T16:00:00.000-05:00", + "localTime": "1611212019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Overcast. Chilly.", + "skyInfo": "18", + "skyDescription": "Overcast", + "temperature": "3.00", + "temperatureDesc": "Chilly", + "comfort": "0.25", + "humidity": "81", + "dewPoint": "0.10", + "precipitationProbability": "9", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "10.08", + "windDirection": "167", + "windDesc": "South", + "windDescShort": "S", + "visibility": "12.66", + "icon": "17", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", + "dayOfWeek": "5", + "weekday": "Thursday", + "utcTime": "2019-11-21T17:00:00.000-05:00", + "localTime": "1711212019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Sprinkles. Overcast. Chilly.", + "skyInfo": "18", + "skyDescription": "Overcast", + "temperature": "2.70", + "temperatureDesc": "Chilly", + "comfort": "-0.51", + "humidity": "83", + "dewPoint": "0.00", + "precipitationProbability": "26", + "precipitationDesc": "Sprinkles", + "rainFall": "0.01", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "11.88", + "windDirection": "172", + "windDesc": "South", + "windDescShort": "S", + "visibility": "12.06", + "icon": "34", + "iconName": "night_sprinkles", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/22.png", + "dayOfWeek": "5", + "weekday": "Thursday", + "utcTime": "2019-11-21T18:00:00.000-05:00", + "localTime": "1811212019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Sprinkles. Overcast. Chilly.", + "skyInfo": "18", + "skyDescription": "Overcast", + "temperature": "2.60", + "temperatureDesc": "Chilly", + "comfort": "-0.98", + "humidity": "83", + "dewPoint": "0.00", + "precipitationProbability": "34", + "precipitationDesc": "Sprinkles", + "rainFall": "0.01", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "13.68", + "windDirection": "174", + "windDesc": "South", + "windDescShort": "S", + "visibility": "12.41", + "icon": "34", + "iconName": "night_sprinkles", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/22.png", + "dayOfWeek": "5", + "weekday": "Thursday", + "utcTime": "2019-11-21T19:00:00.000-05:00", + "localTime": "1911212019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Sprinkles. Overcast. Chilly.", + "skyInfo": "18", + "skyDescription": "Overcast", + "temperature": "2.80", + "temperatureDesc": "Chilly", + "comfort": "-1.16", + "humidity": "83", + "dewPoint": "0.10", + "precipitationProbability": "27", + "precipitationDesc": "Sprinkles", + "rainFall": "0.01", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "16.20", + "windDirection": "172", + "windDesc": "South", + "windDescShort": "S", + "visibility": "12.36", + "icon": "34", + "iconName": "night_sprinkles", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/22.png", + "dayOfWeek": "5", + "weekday": "Thursday", + "utcTime": "2019-11-21T20:00:00.000-05:00", + "localTime": "2011212019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Overcast. Chilly.", + "skyInfo": "18", + "skyDescription": "Overcast", + "temperature": "3.10", + "temperatureDesc": "Chilly", + "comfort": "-1.16", + "humidity": "84", + "dewPoint": "0.60", + "precipitationProbability": "9", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "18.72", + "windDirection": "170", + "windDesc": "South", + "windDescShort": "S", + "visibility": "11.91", + "icon": "17", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", + "dayOfWeek": "5", + "weekday": "Thursday", + "utcTime": "2019-11-21T21:00:00.000-05:00", + "localTime": "2111212019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Overcast. Chilly.", + "skyInfo": "18", + "skyDescription": "Overcast", + "temperature": "3.20", + "temperatureDesc": "Chilly", + "comfort": "-1.36", + "humidity": "86", + "dewPoint": "1.00", + "precipitationProbability": "6", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "21.24", + "windDirection": "170", + "windDesc": "South", + "windDescShort": "S", + "visibility": "12.85", + "icon": "17", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", + "dayOfWeek": "5", + "weekday": "Thursday", + "utcTime": "2019-11-21T22:00:00.000-05:00", + "localTime": "2211212019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Overcast. Chilly.", + "skyInfo": "18", + "skyDescription": "Overcast", + "temperature": "3.40", + "temperatureDesc": "Chilly", + "comfort": "-1.15", + "humidity": "86", + "dewPoint": "1.30", + "precipitationProbability": "12", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "21.60", + "windDirection": "175", + "windDesc": "South", + "windDescShort": "S", + "visibility": "12.35", + "icon": "17", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", + "dayOfWeek": "5", + "weekday": "Thursday", + "utcTime": "2019-11-21T23:00:00.000-05:00", + "localTime": "2311212019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Sprinkles. Overcast. Chilly.", + "skyInfo": "18", + "skyDescription": "Overcast", + "temperature": "3.60", + "temperatureDesc": "Chilly", + "comfort": "-0.81", + "humidity": "86", + "dewPoint": "1.50", + "precipitationProbability": "36", + "precipitationDesc": "Sprinkles", + "rainFall": "0.01", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "20.88", + "windDirection": "184", + "windDesc": "South", + "windDescShort": "S", + "visibility": "12.79", + "icon": "34", + "iconName": "night_sprinkles", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/22.png", + "dayOfWeek": "6", + "weekday": "Friday", + "utcTime": "2019-11-22T00:00:00.000-05:00", + "localTime": "0011222019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Sprinkles. Overcast. Chilly.", + "skyInfo": "18", + "skyDescription": "Overcast", + "temperature": "3.70", + "temperatureDesc": "Chilly", + "comfort": "-0.64", + "humidity": "88", + "dewPoint": "1.90", + "precipitationProbability": "47", + "precipitationDesc": "Sprinkles", + "rainFall": "0.01", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "20.52", + "windDirection": "192", + "windDesc": "South", + "windDescShort": "S", + "visibility": "12.01", + "icon": "34", + "iconName": "night_sprinkles", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/22.png", + "dayOfWeek": "6", + "weekday": "Friday", + "utcTime": "2019-11-22T01:00:00.000-05:00", + "localTime": "0111222019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Sprinkles. Cloudy. Chilly.", + "skyInfo": "17", + "skyDescription": "Cloudy", + "temperature": "4.00", + "temperatureDesc": "Chilly", + "comfort": "-0.39", + "humidity": "89", + "dewPoint": "2.30", + "precipitationProbability": "40", + "precipitationDesc": "Sprinkles", + "rainFall": "0.01", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "21.60", + "windDirection": "199", + "windDesc": "South", + "windDescShort": "S", + "visibility": "0.00", + "icon": "34", + "iconName": "night_sprinkles", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/22.png", + "dayOfWeek": "6", + "weekday": "Friday", + "utcTime": "2019-11-22T02:00:00.000-05:00", + "localTime": "0211222019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Sprinkles. Cloudy. Chilly.", + "skyInfo": "17", + "skyDescription": "Cloudy", + "temperature": "4.10", + "temperatureDesc": "Chilly", + "comfort": "-0.47", + "humidity": "91", + "dewPoint": "2.70", + "precipitationProbability": "33", + "precipitationDesc": "Sprinkles", + "rainFall": "0.01", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "23.40", + "windDirection": "205", + "windDesc": "Southwest", + "windDescShort": "SW", + "visibility": "0.00", + "icon": "34", + "iconName": "night_sprinkles", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/22.png", + "dayOfWeek": "6", + "weekday": "Friday", + "utcTime": "2019-11-22T03:00:00.000-05:00", + "localTime": "0311222019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Sprinkles. Cloudy. Chilly.", + "skyInfo": "17", + "skyDescription": "Cloudy", + "temperature": "4.20", + "temperatureDesc": "Chilly", + "comfort": "-0.38", + "humidity": "93", + "dewPoint": "3.10", + "precipitationProbability": "25", + "precipitationDesc": "Sprinkles", + "rainFall": "0.01", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "23.76", + "windDirection": "210", + "windDesc": "Southwest", + "windDescShort": "SW", + "visibility": "0.00", + "icon": "34", + "iconName": "night_sprinkles", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/22.png", + "dayOfWeek": "6", + "weekday": "Friday", + "utcTime": "2019-11-22T04:00:00.000-05:00", + "localTime": "0411222019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Sprinkles. Cloudy. Chilly.", + "skyInfo": "17", + "skyDescription": "Cloudy", + "temperature": "4.30", + "temperatureDesc": "Chilly", + "comfort": "-0.13", + "humidity": "95", + "dewPoint": "3.60", + "precipitationProbability": "34", + "precipitationDesc": "Sprinkles", + "rainFall": "0.01", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "22.68", + "windDirection": "211", + "windDesc": "Southwest", + "windDescShort": "SW", + "visibility": "0.00", + "icon": "34", + "iconName": "night_sprinkles", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/22.png", + "dayOfWeek": "6", + "weekday": "Friday", + "utcTime": "2019-11-22T05:00:00.000-05:00", + "localTime": "0511222019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "D", + "description": "Sprinkles. Cloudy. Chilly.", + "skyInfo": "17", + "skyDescription": "Cloudy", + "temperature": "4.40", + "temperatureDesc": "Chilly", + "comfort": "0.16", + "humidity": "98", + "dewPoint": "4.10", + "precipitationProbability": "43", + "precipitationDesc": "Sprinkles", + "rainFall": "0.01", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "21.24", + "windDirection": "211", + "windDesc": "Southwest", + "windDescShort": "SW", + "visibility": "0.00", + "icon": "18", + "iconName": "sprinkles", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/27.png", + "dayOfWeek": "6", + "weekday": "Friday", + "utcTime": "2019-11-22T06:00:00.000-05:00", + "localTime": "0611222019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "D", + "description": "Sprinkles. Cloudy. Chilly.", + "skyInfo": "17", + "skyDescription": "Cloudy", + "temperature": "4.50", + "temperatureDesc": "Chilly", + "comfort": "0.42", + "humidity": "99", + "dewPoint": "4.30", + "precipitationProbability": "52", + "precipitationDesc": "Sprinkles", + "rainFall": "0.01", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "20.16", + "windDirection": "213", + "windDesc": "Southwest", + "windDescShort": "SW", + "visibility": "0.00", + "icon": "18", + "iconName": "sprinkles", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/27.png", + "dayOfWeek": "6", + "weekday": "Friday", + "utcTime": "2019-11-22T07:00:00.000-05:00", + "localTime": "0711222019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "D", + "description": "Sprinkles. Cloudy. Chilly.", + "skyInfo": "17", + "skyDescription": "Cloudy", + "temperature": "5.80", + "temperatureDesc": "Chilly", + "comfort": "2.14", + "humidity": "95", + "dewPoint": "5.10", + "precipitationProbability": "53", + "precipitationDesc": "Sprinkles", + "rainFall": "0.03", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "19.44", + "windDirection": "221", + "windDesc": "Southwest", + "windDescShort": "SW", + "visibility": "0.00", + "icon": "18", + "iconName": "sprinkles", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/27.png", + "dayOfWeek": "6", + "weekday": "Friday", + "utcTime": "2019-11-22T08:00:00.000-05:00", + "localTime": "0811222019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "D", + "description": "Sprinkles. Cloudy. Chilly.", + "skyInfo": "17", + "skyDescription": "Cloudy", + "temperature": "6.90", + "temperatureDesc": "Chilly", + "comfort": "3.53", + "humidity": "90", + "dewPoint": "5.40", + "precipitationProbability": "54", + "precipitationDesc": "Sprinkles", + "rainFall": "0.03", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "19.44", + "windDirection": "232", + "windDesc": "Southwest", + "windDescShort": "SW", + "visibility": "0.00", + "icon": "18", + "iconName": "sprinkles", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/27.png", + "dayOfWeek": "6", + "weekday": "Friday", + "utcTime": "2019-11-22T09:00:00.000-05:00", + "localTime": "0911222019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "D", + "description": "Sprinkles. Cloudy. Chilly.", + "skyInfo": "17", + "skyDescription": "Cloudy", + "temperature": "6.90", + "temperatureDesc": "Chilly", + "comfort": "3.57", + "humidity": "90", + "dewPoint": "5.30", + "precipitationProbability": "55", + "precipitationDesc": "Sprinkles", + "rainFall": "0.03", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "19.08", + "windDirection": "244", + "windDesc": "Southwest", + "windDescShort": "SW", + "visibility": "0.00", + "icon": "18", + "iconName": "sprinkles", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/27.png", + "dayOfWeek": "6", + "weekday": "Friday", + "utcTime": "2019-11-22T10:00:00.000-05:00", + "localTime": "1011222019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "D", + "description": "Sprinkles. Overcast. Chilly.", + "skyInfo": "18", + "skyDescription": "Overcast", + "temperature": "6.30", + "temperatureDesc": "Chilly", + "comfort": "2.45", + "humidity": "89", + "dewPoint": "4.60", + "precipitationProbability": "44", + "precipitationDesc": "Sprinkles", + "rainFall": "0.01", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "22.32", + "windDirection": "255", + "windDesc": "West", + "windDescShort": "W", + "visibility": "0.00", + "icon": "18", + "iconName": "sprinkles", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/27.png", + "dayOfWeek": "6", + "weekday": "Friday", + "utcTime": "2019-11-22T11:00:00.000-05:00", + "localTime": "1111222019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "D", + "description": "Sprinkles. Overcast. Chilly.", + "skyInfo": "18", + "skyDescription": "Overcast", + "temperature": "5.50", + "temperatureDesc": "Chilly", + "comfort": "0.90", + "humidity": "87", + "dewPoint": "3.50", + "precipitationProbability": "33", + "precipitationDesc": "Sprinkles", + "rainFall": "0.01", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "27.72", + "windDirection": "264", + "windDesc": "West", + "windDescShort": "W", + "visibility": "0.00", + "icon": "18", + "iconName": "sprinkles", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/27.png", + "dayOfWeek": "6", + "weekday": "Friday", + "utcTime": "2019-11-22T12:00:00.000-05:00", + "localTime": "1211222019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "D", + "description": "Overcast. Chilly.", + "skyInfo": "18", + "skyDescription": "Overcast", + "temperature": "4.50", + "temperatureDesc": "Chilly", + "comfort": "-0.72", + "humidity": "86", + "dewPoint": "2.30", + "precipitationProbability": "10", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "13", + "airDescription": "Breezy", + "windSpeed": "31.32", + "windDirection": "269", + "windDesc": "West", + "windDescShort": "W", + "visibility": "0.00", + "icon": "7", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/17.png", + "dayOfWeek": "6", + "weekday": "Friday", + "utcTime": "2019-11-22T13:00:00.000-05:00", + "localTime": "1311222019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "D", + "description": "Overcast. Chilly.", + "skyInfo": "18", + "skyDescription": "Overcast", + "temperature": "3.80", + "temperatureDesc": "Chilly", + "comfort": "-1.79", + "humidity": "85", + "dewPoint": "1.40", + "precipitationProbability": "9", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "13", + "airDescription": "Breezy", + "windSpeed": "33.12", + "windDirection": "272", + "windDesc": "West", + "windDescShort": "W", + "visibility": "0.00", + "icon": "7", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/17.png", + "dayOfWeek": "6", + "weekday": "Friday", + "utcTime": "2019-11-22T14:00:00.000-05:00", + "localTime": "1411222019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "D", + "description": "Overcast. Chilly.", + "skyInfo": "18", + "skyDescription": "Overcast", + "temperature": "2.60", + "temperatureDesc": "Chilly", + "comfort": "-3.49", + "humidity": "85", + "dewPoint": "0.30", + "precipitationProbability": "8", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "13", + "airDescription": "Breezy", + "windSpeed": "34.56", + "windDirection": "275", + "windDesc": "West", + "windDescShort": "W", + "visibility": "0.00", + "icon": "7", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/17.png", + "dayOfWeek": "6", + "weekday": "Friday", + "utcTime": "2019-11-22T15:00:00.000-05:00", + "localTime": "1511222019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "D", + "description": "Overcast. Chilly.", + "skyInfo": "18", + "skyDescription": "Overcast", + "temperature": "1.20", + "temperatureDesc": "Chilly", + "comfort": "-5.46", + "humidity": "86", + "dewPoint": "-0.90", + "precipitationProbability": "7", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "13", + "airDescription": "Breezy", + "windSpeed": "36.00", + "windDirection": "278", + "windDesc": "West", + "windDescShort": "W", + "visibility": "0.00", + "icon": "7", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/17.png", + "dayOfWeek": "6", + "weekday": "Friday", + "utcTime": "2019-11-22T16:00:00.000-05:00", + "localTime": "1611222019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Overcast. Chilly.", + "skyInfo": "18", + "skyDescription": "Overcast", + "temperature": "0.30", + "temperatureDesc": "Chilly", + "comfort": "-6.72", + "humidity": "83", + "dewPoint": "-2.30", + "precipitationProbability": "7", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "13", + "airDescription": "Breezy", + "windSpeed": "36.72", + "windDirection": "282", + "windDesc": "West", + "windDescShort": "W", + "visibility": "0.00", + "icon": "17", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", + "dayOfWeek": "6", + "weekday": "Friday", + "utcTime": "2019-11-22T17:00:00.000-05:00", + "localTime": "1711222019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Overcast. Cold.", + "skyInfo": "18", + "skyDescription": "Overcast", + "temperature": "-1.00", + "temperatureDesc": "Cold", + "comfort": "-8.57", + "humidity": "82", + "dewPoint": "-3.80", + "precipitationProbability": "6", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "13", + "airDescription": "Breezy", + "windSpeed": "38.16", + "windDirection": "285", + "windDesc": "West", + "windDescShort": "W", + "visibility": "0.00", + "icon": "17", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", + "dayOfWeek": "6", + "weekday": "Friday", + "utcTime": "2019-11-22T18:00:00.000-05:00", + "localTime": "1811222019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Overcast. Cold.", + "skyInfo": "18", + "skyDescription": "Overcast", + "temperature": "-1.90", + "temperatureDesc": "Cold", + "comfort": "-9.74", + "humidity": "82", + "dewPoint": "-4.60", + "precipitationProbability": "6", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "13", + "airDescription": "Breezy", + "windSpeed": "37.80", + "windDirection": "288", + "windDesc": "West", + "windDescShort": "W", + "visibility": "0.00", + "icon": "17", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", + "dayOfWeek": "6", + "weekday": "Friday", + "utcTime": "2019-11-22T19:00:00.000-05:00", + "localTime": "1911222019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Overcast. Cold.", + "skyInfo": "18", + "skyDescription": "Overcast", + "temperature": "-1.20", + "temperatureDesc": "Cold", + "comfort": "-8.71", + "humidity": "76", + "dewPoint": "-5.00", + "precipitationProbability": "6", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "13", + "airDescription": "Breezy", + "windSpeed": "36.72", + "windDirection": "291", + "windDesc": "West", + "windDescShort": "W", + "visibility": "0.00", + "icon": "17", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", + "dayOfWeek": "6", + "weekday": "Friday", + "utcTime": "2019-11-22T20:00:00.000-05:00", + "localTime": "2011222019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Overcast. Cold.", + "skyInfo": "18", + "skyDescription": "Overcast", + "temperature": "-2.00", + "temperatureDesc": "Cold", + "comfort": "-9.70", + "humidity": "78", + "dewPoint": "-5.40", + "precipitationProbability": "7", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "13", + "airDescription": "Breezy", + "windSpeed": "36.00", + "windDirection": "294", + "windDesc": "Northwest", + "windDescShort": "NW", + "visibility": "0.00", + "icon": "17", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", + "dayOfWeek": "6", + "weekday": "Friday", + "utcTime": "2019-11-22T21:00:00.000-05:00", + "localTime": "2111222019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Overcast. Cold.", + "skyInfo": "18", + "skyDescription": "Overcast", + "temperature": "-2.40", + "temperatureDesc": "Cold", + "comfort": "-10.16", + "humidity": "79", + "dewPoint": "-5.60", + "precipitationProbability": "7", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "13", + "airDescription": "Breezy", + "windSpeed": "35.28", + "windDirection": "298", + "windDesc": "Northwest", + "windDescShort": "NW", + "visibility": "0.00", + "icon": "17", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", + "dayOfWeek": "6", + "weekday": "Friday", + "utcTime": "2019-11-22T22:00:00.000-05:00", + "localTime": "2211222019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Cloudy. Cold.", + "skyInfo": "17", + "skyDescription": "Cloudy", + "temperature": "-2.50", + "temperatureDesc": "Cold", + "comfort": "-10.22", + "humidity": "78", + "dewPoint": "-5.80", + "precipitationProbability": "6", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "13", + "airDescription": "Breezy", + "windSpeed": "34.56", + "windDirection": "300", + "windDesc": "Northwest", + "windDescShort": "NW", + "visibility": "0.00", + "icon": "17", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", + "dayOfWeek": "6", + "weekday": "Friday", + "utcTime": "2019-11-22T23:00:00.000-05:00", + "localTime": "2311222019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Cloudy. Cold.", + "skyInfo": "17", + "skyDescription": "Cloudy", + "temperature": "-3.10", + "temperatureDesc": "Cold", + "comfort": "-10.98", + "humidity": "81", + "dewPoint": "-6.00", + "precipitationProbability": "4", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "34.20", + "windDirection": "300", + "windDesc": "Northwest", + "windDescShort": "NW", + "visibility": "0.00", + "icon": "17", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", + "dayOfWeek": "7", + "weekday": "Saturday", + "utcTime": "2019-11-23T00:00:00.000-05:00", + "localTime": "0011232019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Cloudy. Cold.", + "skyInfo": "17", + "skyDescription": "Cloudy", + "temperature": "-3.30", + "temperatureDesc": "Cold", + "comfort": "-11.13", + "humidity": "81", + "dewPoint": "-6.10", + "precipitationProbability": "3", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "33.12", + "windDirection": "300", + "windDesc": "Northwest", + "windDescShort": "NW", + "visibility": "0.00", + "icon": "17", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", + "dayOfWeek": "7", + "weekday": "Saturday", + "utcTime": "2019-11-23T01:00:00.000-05:00", + "localTime": "0111232019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Partly cloudy. Cold.", + "skyInfo": "10", + "skyDescription": "Partly cloudy", + "temperature": "-3.30", + "temperatureDesc": "Cold", + "comfort": "-10.85", + "humidity": "80", + "dewPoint": "-6.30", + "precipitationProbability": "3", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "30.60", + "windDirection": "301", + "windDesc": "Northwest", + "windDescShort": "NW", + "visibility": "0.00", + "icon": "15", + "iconName": "night_partly_cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", + "dayOfWeek": "7", + "weekday": "Saturday", + "utcTime": "2019-11-23T02:00:00.000-05:00", + "localTime": "0211232019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Partly cloudy. Cold.", + "skyInfo": "10", + "skyDescription": "Partly cloudy", + "temperature": "-3.70", + "temperatureDesc": "Cold", + "comfort": "-10.93", + "humidity": "81", + "dewPoint": "-6.50", + "precipitationProbability": "2", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "27.00", + "windDirection": "302", + "windDesc": "Northwest", + "windDescShort": "NW", + "visibility": "0.00", + "icon": "15", + "iconName": "night_partly_cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", + "dayOfWeek": "7", + "weekday": "Saturday", + "utcTime": "2019-11-23T03:00:00.000-05:00", + "localTime": "0311232019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Partly cloudy. Cold.", + "skyInfo": "10", + "skyDescription": "Partly cloudy", + "temperature": "-4.30", + "temperatureDesc": "Cold", + "comfort": "-11.21", + "humidity": "84", + "dewPoint": "-6.60", + "precipitationProbability": "2", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "23.40", + "windDirection": "303", + "windDesc": "Northwest", + "windDescShort": "NW", + "visibility": "0.00", + "icon": "15", + "iconName": "night_partly_cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", + "dayOfWeek": "7", + "weekday": "Saturday", + "utcTime": "2019-11-23T04:00:00.000-05:00", + "localTime": "0411232019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Partly cloudy. Cold.", + "skyInfo": "10", + "skyDescription": "Partly cloudy", + "temperature": "-4.70", + "temperatureDesc": "Cold", + "comfort": "-11.14", + "humidity": "85", + "dewPoint": "-6.80", + "precipitationProbability": "2", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "19.80", + "windDirection": "304", + "windDesc": "Northwest", + "windDescShort": "NW", + "visibility": "0.00", + "icon": "15", + "iconName": "night_partly_cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", + "dayOfWeek": "7", + "weekday": "Saturday", + "utcTime": "2019-11-23T05:00:00.000-05:00", + "localTime": "0511232019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "D", + "description": "a mixture of sun and clouds. Cold.", + "skyInfo": "11", + "skyDescription": "a mixture of sun and clouds", + "temperature": "-4.80", + "temperatureDesc": "Cold", + "comfort": "-10.59", + "humidity": "85", + "dewPoint": "-7.00", + "precipitationProbability": "2", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "16.20", + "windDirection": "307", + "windDesc": "Northwest", + "windDescShort": "NW", + "visibility": "0.00", + "icon": "4", + "iconName": "partly_cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/3.png", + "dayOfWeek": "7", + "weekday": "Saturday", + "utcTime": "2019-11-23T06:00:00.000-05:00", + "localTime": "0611232019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "D", + "description": "a mixture of sun and clouds. Cold.", + "skyInfo": "11", + "skyDescription": "a mixture of sun and clouds", + "temperature": "-5.20", + "temperatureDesc": "Cold", + "comfort": "-10.35", + "humidity": "87", + "dewPoint": "-7.10", + "precipitationProbability": "2", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "12.96", + "windDirection": "310", + "windDesc": "Northwest", + "windDescShort": "NW", + "visibility": "0.00", + "icon": "4", + "iconName": "partly_cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/3.png", + "dayOfWeek": "7", + "weekday": "Saturday", + "utcTime": "2019-11-23T07:00:00.000-05:00", + "localTime": "0711232019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "D", + "description": "a mixture of sun and clouds. Cold.", + "skyInfo": "11", + "skyDescription": "a mixture of sun and clouds", + "temperature": "-5.00", + "temperatureDesc": "Cold", + "comfort": "-9.92", + "humidity": "87", + "dewPoint": "-6.90", + "precipitationProbability": "3", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "12.24", + "windDirection": "316", + "windDesc": "Northwest", + "windDescShort": "NW", + "visibility": "0.00", + "icon": "4", + "iconName": "partly_cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/3.png", + "dayOfWeek": "7", + "weekday": "Saturday", + "utcTime": "2019-11-23T08:00:00.000-05:00", + "localTime": "0811232019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "D", + "description": "a mixture of sun and clouds. Cold.", + "skyInfo": "11", + "skyDescription": "a mixture of sun and clouds", + "temperature": "-3.30", + "temperatureDesc": "Cold", + "comfort": "-8.03", + "humidity": "80", + "dewPoint": "-6.30", + "precipitationProbability": "3", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "12.96", + "windDirection": "323", + "windDesc": "Northwest", + "windDescShort": "NW", + "visibility": "0.00", + "icon": "4", + "iconName": "partly_cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/3.png", + "dayOfWeek": "7", + "weekday": "Saturday", + "utcTime": "2019-11-23T09:00:00.000-05:00", + "localTime": "0911232019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "D", + "description": "a mixture of sun and clouds. Cold.", + "skyInfo": "11", + "skyDescription": "a mixture of sun and clouds", + "temperature": "-1.10", + "temperatureDesc": "Cold", + "comfort": "-5.43", + "humidity": "72", + "dewPoint": "-5.60", + "precipitationProbability": "3", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "13.32", + "windDirection": "323", + "windDesc": "Northwest", + "windDescShort": "NW", + "visibility": "0.00", + "icon": "4", + "iconName": "partly_cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/3.png", + "dayOfWeek": "7", + "weekday": "Saturday", + "utcTime": "2019-11-23T10:00:00.000-05:00", + "localTime": "1011232019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "D", + "description": "a mixture of sun and clouds. Chilly.", + "skyInfo": "11", + "skyDescription": "a mixture of sun and clouds", + "temperature": "0.00", + "temperatureDesc": "Chilly", + "comfort": "-4.16", + "humidity": "67", + "dewPoint": "-5.40", + "precipitationProbability": "3", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "13.68", + "windDirection": "314", + "windDesc": "Northwest", + "windDescShort": "NW", + "visibility": "0.00", + "icon": "4", + "iconName": "partly_cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/3.png", + "dayOfWeek": "7", + "weekday": "Saturday", + "utcTime": "2019-11-23T11:00:00.000-05:00", + "localTime": "1111232019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "D", + "description": "a mixture of sun and clouds. Chilly.", + "skyInfo": "11", + "skyDescription": "a mixture of sun and clouds", + "temperature": "0.80", + "temperatureDesc": "Chilly", + "comfort": "-3.18", + "humidity": "64", + "dewPoint": "-5.30", + "precipitationProbability": "3", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "13.68", + "windDirection": "298", + "windDesc": "Northwest", + "windDescShort": "NW", + "visibility": "0.00", + "icon": "4", + "iconName": "partly_cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/3.png", + "dayOfWeek": "7", + "weekday": "Saturday", + "utcTime": "2019-11-23T12:00:00.000-05:00", + "localTime": "1211232019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "D", + "description": "a mixture of sun and clouds. Chilly.", + "skyInfo": "11", + "skyDescription": "a mixture of sun and clouds", + "temperature": "1.10", + "temperatureDesc": "Chilly", + "comfort": "-2.74", + "humidity": "64", + "dewPoint": "-5.00", + "precipitationProbability": "3", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "13.32", + "windDirection": "289", + "windDesc": "West", + "windDescShort": "W", + "visibility": "0.00", + "icon": "4", + "iconName": "partly_cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/3.png", + "dayOfWeek": "7", + "weekday": "Saturday", + "utcTime": "2019-11-23T13:00:00.000-05:00", + "localTime": "1311232019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "D", + "description": "a mixture of sun and clouds. Chilly.", + "skyInfo": "11", + "skyDescription": "a mixture of sun and clouds", + "temperature": "1.20", + "temperatureDesc": "Chilly", + "comfort": "-2.08", + "humidity": "66", + "dewPoint": "-4.50", + "precipitationProbability": "3", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "10.80", + "windDirection": "289", + "windDesc": "West", + "windDescShort": "W", + "visibility": "0.00", + "icon": "4", + "iconName": "partly_cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/3.png", + "dayOfWeek": "7", + "weekday": "Saturday", + "utcTime": "2019-11-23T14:00:00.000-05:00", + "localTime": "1411232019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "D", + "description": "a mixture of sun and clouds. Chilly.", + "skyInfo": "11", + "skyDescription": "a mixture of sun and clouds", + "temperature": "0.90", + "temperatureDesc": "Chilly", + "comfort": "-1.66", + "humidity": "71", + "dewPoint": "-3.90", + "precipitationProbability": "4", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "7.92", + "windDirection": "293", + "windDesc": "Northwest", + "windDescShort": "NW", + "visibility": "0.00", + "icon": "4", + "iconName": "partly_cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/3.png", + "dayOfWeek": "7", + "weekday": "Saturday", + "utcTime": "2019-11-23T15:00:00.000-05:00", + "localTime": "1511232019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "D", + "description": "a mixture of sun and clouds. Cold.", + "skyInfo": "11", + "skyDescription": "a mixture of sun and clouds", + "temperature": "-0.10", + "temperatureDesc": "Cold", + "comfort": "-2.46", + "humidity": "78", + "dewPoint": "-3.60", + "precipitationProbability": "4", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "6.84", + "windDirection": "297", + "windDesc": "Northwest", + "windDescShort": "NW", + "visibility": "0.00", + "icon": "4", + "iconName": "partly_cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/3.png", + "dayOfWeek": "7", + "weekday": "Saturday", + "utcTime": "2019-11-23T16:00:00.000-05:00", + "localTime": "1611232019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Partly cloudy. Cold.", + "skyInfo": "10", + "skyDescription": "Partly cloudy", + "temperature": "-0.50", + "temperatureDesc": "Cold", + "comfort": "-3.18", + "humidity": "81", + "dewPoint": "-3.40", + "precipitationProbability": "4", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "7.56", + "windDirection": "236", + "windDesc": "Southwest", + "windDescShort": "SW", + "visibility": "0.00", + "icon": "15", + "iconName": "night_partly_cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", + "dayOfWeek": "7", + "weekday": "Saturday", + "utcTime": "2019-11-23T17:00:00.000-05:00", + "localTime": "1711232019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Partly cloudy. Cold.", + "skyInfo": "10", + "skyDescription": "Partly cloudy", + "temperature": "-1.20", + "temperatureDesc": "Cold", + "comfort": "-4.12", + "humidity": "85", + "dewPoint": "-3.40", + "precipitationProbability": "4", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "7.92", + "windDirection": "144", + "windDesc": "Southeast", + "windDescShort": "SE", + "visibility": "0.00", + "icon": "15", + "iconName": "night_partly_cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", + "dayOfWeek": "7", + "weekday": "Saturday", + "utcTime": "2019-11-23T18:00:00.000-05:00", + "localTime": "1811232019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Partly cloudy. Cold.", + "skyInfo": "10", + "skyDescription": "Partly cloudy", + "temperature": "-1.40", + "temperatureDesc": "Cold", + "comfort": "-4.59", + "humidity": "87", + "dewPoint": "-3.30", + "precipitationProbability": "3", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "8.64", + "windDirection": "143", + "windDesc": "Southeast", + "windDescShort": "SE", + "visibility": "0.00", + "icon": "15", + "iconName": "night_partly_cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", + "dayOfWeek": "7", + "weekday": "Saturday", + "utcTime": "2019-11-23T19:00:00.000-05:00", + "localTime": "1911232019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Partly cloudy. Cold.", + "skyInfo": "10", + "skyDescription": "Partly cloudy", + "temperature": "-0.60", + "temperatureDesc": "Cold", + "comfort": "-3.75", + "humidity": "85", + "dewPoint": "-2.90", + "precipitationProbability": "3", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "9.00", + "windDirection": "149", + "windDesc": "Southeast", + "windDescShort": "SE", + "visibility": "0.00", + "icon": "15", + "iconName": "night_partly_cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", + "dayOfWeek": "7", + "weekday": "Saturday", + "utcTime": "2019-11-23T20:00:00.000-05:00", + "localTime": "2011232019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Partly cloudy. Cold.", + "skyInfo": "10", + "skyDescription": "Partly cloudy", + "temperature": "-0.20", + "temperatureDesc": "Cold", + "comfort": "-3.28", + "humidity": "85", + "dewPoint": "-2.50", + "precipitationProbability": "3", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "9.00", + "windDirection": "155", + "windDesc": "Southeast", + "windDescShort": "SE", + "visibility": "0.00", + "icon": "15", + "iconName": "night_partly_cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", + "dayOfWeek": "7", + "weekday": "Saturday", + "utcTime": "2019-11-23T21:00:00.000-05:00", + "localTime": "2111232019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Partly cloudy. Cold.", + "skyInfo": "10", + "skyDescription": "Partly cloudy", + "temperature": "-0.70", + "temperatureDesc": "Cold", + "comfort": "-3.87", + "humidity": "89", + "dewPoint": "-2.30", + "precipitationProbability": "3", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "9.00", + "windDirection": "153", + "windDesc": "Southeast", + "windDescShort": "SE", + "visibility": "0.00", + "icon": "15", + "iconName": "night_partly_cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", + "dayOfWeek": "7", + "weekday": "Saturday", + "utcTime": "2019-11-23T22:00:00.000-05:00", + "localTime": "2211232019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Partly cloudy. Cold.", + "skyInfo": "10", + "skyDescription": "Partly cloudy", + "temperature": "-0.80", + "temperatureDesc": "Cold", + "comfort": "-3.99", + "humidity": "90", + "dewPoint": "-2.20", + "precipitationProbability": "5", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "9.00", + "windDirection": "145", + "windDesc": "Southeast", + "windDescShort": "SE", + "visibility": "0.00", + "icon": "15", + "iconName": "night_partly_cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", + "dayOfWeek": "7", + "weekday": "Saturday", + "utcTime": "2019-11-23T23:00:00.000-05:00", + "localTime": "2311232019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Partly cloudy. Cold.", + "skyInfo": "10", + "skyDescription": "Partly cloudy", + "temperature": "-0.70", + "temperatureDesc": "Cold", + "comfort": "-3.53", + "humidity": "92", + "dewPoint": "-1.90", + "precipitationProbability": "7", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "7.92", + "windDirection": "132", + "windDesc": "Southeast", + "windDescShort": "SE", + "visibility": "0.00", + "icon": "15", + "iconName": "night_partly_cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", + "dayOfWeek": "1", + "weekday": "Sunday", + "utcTime": "2019-11-24T00:00:00.000-05:00", + "localTime": "0011242019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Partly cloudy. Cold.", + "skyInfo": "10", + "skyDescription": "Partly cloudy", + "temperature": "-0.90", + "temperatureDesc": "Cold", + "comfort": "-3.52", + "humidity": "92", + "dewPoint": "-2.00", + "precipitationProbability": "9", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "7.20", + "windDirection": "115", + "windDesc": "Southeast", + "windDescShort": "SE", + "visibility": "0.00", + "icon": "15", + "iconName": "night_partly_cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", + "dayOfWeek": "1", + "weekday": "Sunday", + "utcTime": "2019-11-24T01:00:00.000-05:00", + "localTime": "0111242019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Partly cloudy. Cold.", + "skyInfo": "10", + "skyDescription": "Partly cloudy", + "temperature": "-1.30", + "temperatureDesc": "Cold", + "comfort": "-3.99", + "humidity": "94", + "dewPoint": "-2.20", + "precipitationProbability": "8", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "7.20", + "windDirection": "96", + "windDesc": "East", + "windDescShort": "E", + "visibility": "0.00", + "icon": "15", + "iconName": "night_partly_cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", + "dayOfWeek": "1", + "weekday": "Sunday", + "utcTime": "2019-11-24T02:00:00.000-05:00", + "localTime": "0211242019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Partly cloudy. Cold.", + "skyInfo": "10", + "skyDescription": "Partly cloudy", + "temperature": "-1.30", + "temperatureDesc": "Cold", + "comfort": "-3.99", + "humidity": "93", + "dewPoint": "-2.30", + "precipitationProbability": "8", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "7.20", + "windDirection": "77", + "windDesc": "East", + "windDescShort": "E", + "visibility": "0.00", + "icon": "15", + "iconName": "night_partly_cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", + "dayOfWeek": "1", + "weekday": "Sunday", + "utcTime": "2019-11-24T03:00:00.000-05:00", + "localTime": "0311242019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Partly cloudy. Cold.", + "skyInfo": "10", + "skyDescription": "Partly cloudy", + "temperature": "-1.10", + "temperatureDesc": "Cold", + "comfort": "-3.88", + "humidity": "91", + "dewPoint": "-2.40", + "precipitationProbability": "7", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "7.56", + "windDirection": "60", + "windDesc": "Northeast", + "windDescShort": "NE", + "visibility": "0.00", + "icon": "15", + "iconName": "night_partly_cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", + "dayOfWeek": "1", + "weekday": "Sunday", + "utcTime": "2019-11-24T04:00:00.000-05:00", + "localTime": "0411242019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Partly cloudy. Cold.", + "skyInfo": "10", + "skyDescription": "Partly cloudy", + "temperature": "-1.20", + "temperatureDesc": "Cold", + "comfort": "-4.12", + "humidity": "92", + "dewPoint": "-2.40", + "precipitationProbability": "6", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "7.92", + "windDirection": "41", + "windDesc": "Northeast", + "windDescShort": "NE", + "visibility": "0.00", + "icon": "15", + "iconName": "night_partly_cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", + "dayOfWeek": "1", + "weekday": "Sunday", + "utcTime": "2019-11-24T05:00:00.000-05:00", + "localTime": "0511242019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "D", + "description": "a mixture of sun and clouds. Cold.", + "skyInfo": "11", + "skyDescription": "a mixture of sun and clouds", + "temperature": "-1.50", + "temperatureDesc": "Cold", + "comfort": "-4.59", + "humidity": "93", + "dewPoint": "-2.50", + "precipitationProbability": "5", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "8.28", + "windDirection": "23", + "windDesc": "Northeast", + "windDescShort": "NE", + "visibility": "0.00", + "icon": "4", + "iconName": "partly_cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/3.png", + "dayOfWeek": "1", + "weekday": "Sunday", + "utcTime": "2019-11-24T06:00:00.000-05:00", + "localTime": "0611242019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "D", + "description": "a mixture of sun and clouds. Cold.", + "skyInfo": "11", + "skyDescription": "a mixture of sun and clouds", + "temperature": "-1.60", + "temperatureDesc": "Cold", + "comfort": "-4.71", + "humidity": "94", + "dewPoint": "-2.50", + "precipitationProbability": "5", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "8.28", + "windDirection": "7", + "windDesc": "North", + "windDescShort": "N", + "visibility": "0.00", + "icon": "4", + "iconName": "partly_cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/3.png", + "dayOfWeek": "1", + "weekday": "Sunday", + "utcTime": "2019-11-24T07:00:00.000-05:00", + "localTime": "0711242019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "D", + "description": "a mixture of sun and clouds. Cold.", + "skyInfo": "11", + "skyDescription": "a mixture of sun and clouds", + "temperature": "-1.40", + "temperatureDesc": "Cold", + "comfort": "-4.59", + "humidity": "94", + "dewPoint": "-2.20", + "precipitationProbability": "4", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "8.64", + "windDirection": "354", + "windDesc": "North", + "windDescShort": "N", + "visibility": "0.00", + "icon": "4", + "iconName": "partly_cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/3.png", + "dayOfWeek": "1", + "weekday": "Sunday", + "utcTime": "2019-11-24T08:00:00.000-05:00", + "localTime": "0811242019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "D", + "description": "a mixture of sun and clouds. Cold.", + "skyInfo": "11", + "skyDescription": "a mixture of sun and clouds", + "temperature": "-0.80", + "temperatureDesc": "Cold", + "comfort": "-3.88", + "humidity": "92", + "dewPoint": "-1.90", + "precipitationProbability": "3", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "8.64", + "windDirection": "339", + "windDesc": "North", + "windDescShort": "N", + "visibility": "0.00", + "icon": "4", + "iconName": "partly_cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/3.png", + "dayOfWeek": "1", + "weekday": "Sunday", + "utcTime": "2019-11-24T09:00:00.000-05:00", + "localTime": "0911242019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "D", + "description": "a mixture of sun and clouds. Cold.", + "skyInfo": "11", + "skyDescription": "a mixture of sun and clouds", + "temperature": "-0.50", + "temperatureDesc": "Cold", + "comfort": "-3.53", + "humidity": "92", + "dewPoint": "-1.60", + "precipitationProbability": "2", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "8.64", + "windDirection": "326", + "windDesc": "Northwest", + "windDescShort": "NW", + "visibility": "0.00", + "icon": "4", + "iconName": "partly_cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/3.png", + "dayOfWeek": "1", + "weekday": "Sunday", + "utcTime": "2019-11-24T10:00:00.000-05:00", + "localTime": "1011242019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "D", + "description": "a mixture of sun and clouds. Cold.", + "skyInfo": "11", + "skyDescription": "a mixture of sun and clouds", + "temperature": "-0.30", + "temperatureDesc": "Cold", + "comfort": "-4.05", + "humidity": "91", + "dewPoint": "-1.60", + "precipitationProbability": "4", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "11.52", + "windDirection": "312", + "windDesc": "Northwest", + "windDescShort": "NW", + "visibility": "0.00", + "icon": "4", + "iconName": "partly_cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/3.png", + "dayOfWeek": "1", + "weekday": "Sunday", + "utcTime": "2019-11-24T11:00:00.000-05:00", + "localTime": "1111242019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "D", + "description": "a mixture of sun and clouds. Chilly.", + "skyInfo": "11", + "skyDescription": "a mixture of sun and clouds", + "temperature": "0.00", + "temperatureDesc": "Chilly", + "comfort": "-4.76", + "humidity": "89", + "dewPoint": "-1.60", + "precipitationProbability": "6", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "16.92", + "windDirection": "304", + "windDesc": "Northwest", + "windDescShort": "NW", + "visibility": "0.00", + "icon": "4", + "iconName": "partly_cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/3.png", + "dayOfWeek": "1", + "weekday": "Sunday", + "utcTime": "2019-11-24T12:00:00.000-05:00", + "localTime": "1211242019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "D", + "description": "a mixture of sun and clouds. Chilly.", + "skyInfo": "11", + "skyDescription": "a mixture of sun and clouds", + "temperature": "0.10", + "temperatureDesc": "Chilly", + "comfort": "-5.19", + "humidity": "88", + "dewPoint": "-1.70", + "precipitationProbability": "8", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "20.52", + "windDirection": "304", + "windDesc": "Northwest", + "windDescShort": "NW", + "visibility": "0.00", + "icon": "4", + "iconName": "partly_cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/3.png", + "dayOfWeek": "1", + "weekday": "Sunday", + "utcTime": "2019-11-24T13:00:00.000-05:00", + "localTime": "1311242019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "D", + "description": "a mixture of sun and clouds. Cold.", + "skyInfo": "11", + "skyDescription": "a mixture of sun and clouds", + "temperature": "-0.10", + "temperatureDesc": "Cold", + "comfort": "-5.55", + "humidity": "87", + "dewPoint": "-2.00", + "precipitationProbability": "9", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "21.24", + "windDirection": "305", + "windDesc": "Northwest", + "windDescShort": "NW", + "visibility": "0.00", + "icon": "4", + "iconName": "partly_cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/3.png", + "dayOfWeek": "1", + "weekday": "Sunday", + "utcTime": "2019-11-24T14:00:00.000-05:00", + "localTime": "1411242019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "D", + "description": "a mixture of sun and clouds. Cold.", + "skyInfo": "11", + "skyDescription": "a mixture of sun and clouds", + "temperature": "-0.10", + "temperatureDesc": "Cold", + "comfort": "-5.55", + "humidity": "86", + "dewPoint": "-2.20", + "precipitationProbability": "9", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "21.24", + "windDirection": "307", + "windDesc": "Northwest", + "windDescShort": "NW", + "visibility": "0.00", + "icon": "4", + "iconName": "partly_cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/3.png", + "dayOfWeek": "1", + "weekday": "Sunday", + "utcTime": "2019-11-24T15:00:00.000-05:00", + "localTime": "1511242019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "D", + "description": "a mixture of sun and clouds. Cold.", + "skyInfo": "11", + "skyDescription": "a mixture of sun and clouds", + "temperature": "-0.20", + "temperatureDesc": "Cold", + "comfort": "-5.57", + "humidity": "85", + "dewPoint": "-2.40", + "precipitationProbability": "10", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "20.52", + "windDirection": "307", + "windDesc": "Northwest", + "windDescShort": "NW", + "visibility": "0.00", + "icon": "4", + "iconName": "partly_cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/3.png", + "dayOfWeek": "1", + "weekday": "Sunday", + "utcTime": "2019-11-24T16:00:00.000-05:00", + "localTime": "1611242019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Cloudy. Cold.", + "skyInfo": "17", + "skyDescription": "Cloudy", + "temperature": "-0.10", + "temperatureDesc": "Cold", + "comfort": "-5.17", + "humidity": "87", + "dewPoint": "-2.10", + "precipitationProbability": "11", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "18.72", + "windDirection": "305", + "windDesc": "Northwest", + "windDescShort": "NW", + "visibility": "0.00", + "icon": "17", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", + "dayOfWeek": "1", + "weekday": "Sunday", + "utcTime": "2019-11-24T17:00:00.000-05:00", + "localTime": "1711242019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Cloudy. Chilly.", + "skyInfo": "17", + "skyDescription": "Cloudy", + "temperature": "0.60", + "temperatureDesc": "Chilly", + "comfort": "-3.89", + "humidity": "86", + "dewPoint": "-1.50", + "precipitationProbability": "6", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "16.20", + "windDirection": "300", + "windDesc": "Northwest", + "windDescShort": "NW", + "visibility": "0.00", + "icon": "17", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", + "dayOfWeek": "1", + "weekday": "Sunday", + "utcTime": "2019-11-24T18:00:00.000-05:00", + "localTime": "1811242019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Cloudy. Chilly.", + "skyInfo": "17", + "skyDescription": "Cloudy", + "temperature": "0.50", + "temperatureDesc": "Chilly", + "comfort": "-3.76", + "humidity": "87", + "dewPoint": "-1.40", + "precipitationProbability": "7", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "14.76", + "windDirection": "297", + "windDesc": "Northwest", + "windDescShort": "NW", + "visibility": "0.00", + "icon": "17", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", + "dayOfWeek": "1", + "weekday": "Sunday", + "utcTime": "2019-11-24T19:00:00.000-05:00", + "localTime": "1911242019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Light snow. Cloudy. Chilly.", + "skyInfo": "17", + "skyDescription": "Cloudy", + "temperature": "0.20", + "temperatureDesc": "Chilly", + "comfort": "-4.19", + "humidity": "90", + "dewPoint": "-1.20", + "precipitationProbability": "26", + "precipitationDesc": "Light snow", + "rainFall": "*", + "snowFall": "0.10", + "airInfo": "*", + "airDescription": "", + "windSpeed": "15.12", + "windDirection": "296", + "windDesc": "Northwest", + "windDescShort": "NW", + "visibility": "0.00", + "icon": "29", + "iconName": "light_snow", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/20.png", + "dayOfWeek": "1", + "weekday": "Sunday", + "utcTime": "2019-11-24T20:00:00.000-05:00", + "localTime": "2011242019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Light snow. Cloudy. Chilly.", + "skyInfo": "17", + "skyDescription": "Cloudy", + "temperature": "0.30", + "temperatureDesc": "Chilly", + "comfort": "-4.32", + "humidity": "93", + "dewPoint": "-0.70", + "precipitationProbability": "25", + "precipitationDesc": "Light snow", + "rainFall": "*", + "snowFall": "0.10", + "airInfo": "*", + "airDescription": "", + "windSpeed": "16.56", + "windDirection": "296", + "windDesc": "Northwest", + "windDescShort": "NW", + "visibility": "0.00", + "icon": "29", + "iconName": "light_snow", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/20.png", + "dayOfWeek": "1", + "weekday": "Sunday", + "utcTime": "2019-11-24T21:00:00.000-05:00", + "localTime": "2111242019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Cloudy. Chilly.", + "skyInfo": "17", + "skyDescription": "Cloudy", + "temperature": "0.10", + "temperatureDesc": "Chilly", + "comfort": "-4.63", + "humidity": "97", + "dewPoint": "-0.30", + "precipitationProbability": "11", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "16.92", + "windDirection": "295", + "windDesc": "Northwest", + "windDescShort": "NW", + "visibility": "0.00", + "icon": "17", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", + "dayOfWeek": "1", + "weekday": "Sunday", + "utcTime": "2019-11-24T22:00:00.000-05:00", + "localTime": "2211242019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Cloudy. Cold.", + "skyInfo": "17", + "skyDescription": "Cloudy", + "temperature": "-0.10", + "temperatureDesc": "Cold", + "comfort": "-4.49", + "humidity": "97", + "dewPoint": "-0.50", + "precipitationProbability": "6", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "14.76", + "windDirection": "296", + "windDesc": "Northwest", + "windDescShort": "NW", + "visibility": "0.00", + "icon": "17", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", + "dayOfWeek": "1", + "weekday": "Sunday", + "utcTime": "2019-11-24T23:00:00.000-05:00", + "localTime": "2311242019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Cloudy. Cold.", + "skyInfo": "17", + "skyDescription": "Cloudy", + "temperature": "-0.50", + "temperatureDesc": "Cold", + "comfort": "-4.38", + "humidity": "98", + "dewPoint": "-0.80", + "precipitationProbability": "6", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "11.88", + "windDirection": "299", + "windDesc": "Northwest", + "windDescShort": "NW", + "visibility": "0.00", + "icon": "17", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", + "dayOfWeek": "2", + "weekday": "Monday", + "utcTime": "2019-11-25T00:00:00.000-05:00", + "localTime": "0011252019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Cloudy. Cold.", + "skyInfo": "17", + "skyDescription": "Cloudy", + "temperature": "-0.80", + "temperatureDesc": "Cold", + "comfort": "-4.19", + "humidity": "99", + "dewPoint": "-1.00", + "precipitationProbability": "7", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "9.72", + "windDirection": "303", + "windDesc": "Northwest", + "windDescShort": "NW", + "visibility": "0.00", + "icon": "17", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", + "dayOfWeek": "2", + "weekday": "Monday", + "utcTime": "2019-11-25T01:00:00.000-05:00", + "localTime": "0111252019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Light snow. Cloudy. Cold.", + "skyInfo": "17", + "skyDescription": "Cloudy", + "temperature": "-0.70", + "temperatureDesc": "Cold", + "comfort": "-4.62", + "humidity": "99", + "dewPoint": "-0.90", + "precipitationProbability": "26", + "precipitationDesc": "Light snow", + "rainFall": "*", + "snowFall": "0.10", + "airInfo": "*", + "airDescription": "", + "windSpeed": "11.88", + "windDirection": "306", + "windDesc": "Northwest", + "windDescShort": "NW", + "visibility": "0.00", + "icon": "29", + "iconName": "light_snow", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/20.png", + "dayOfWeek": "2", + "weekday": "Monday", + "utcTime": "2019-11-25T02:00:00.000-05:00", + "localTime": "0211252019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Cloudy. Cold.", + "skyInfo": "17", + "skyDescription": "Cloudy", + "temperature": "-0.50", + "temperatureDesc": "Cold", + "comfort": "-5.06", + "humidity": "99", + "dewPoint": "-0.70", + "precipitationProbability": "11", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "15.12", + "windDirection": "309", + "windDesc": "Northwest", + "windDescShort": "NW", + "visibility": "0.00", + "icon": "17", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", + "dayOfWeek": "2", + "weekday": "Monday", + "utcTime": "2019-11-25T03:00:00.000-05:00", + "localTime": "0311252019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Cloudy. Cold.", + "skyInfo": "17", + "skyDescription": "Cloudy", + "temperature": "-0.30", + "temperatureDesc": "Cold", + "comfort": "-5.07", + "humidity": "99", + "dewPoint": "-0.50", + "precipitationProbability": "10", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "16.56", + "windDirection": "311", + "windDesc": "Northwest", + "windDescShort": "NW", + "visibility": "0.00", + "icon": "17", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", + "dayOfWeek": "2", + "weekday": "Monday", + "utcTime": "2019-11-25T04:00:00.000-05:00", + "localTime": "0411252019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Cloudy. Cold.", + "skyInfo": "17", + "skyDescription": "Cloudy", + "temperature": "-0.20", + "temperatureDesc": "Cold", + "comfort": "-4.69", + "humidity": "96", + "dewPoint": "-0.70", + "precipitationProbability": "11", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "15.12", + "windDirection": "311", + "windDesc": "Northwest", + "windDescShort": "NW", + "visibility": "0.00", + "icon": "17", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", + "dayOfWeek": "2", + "weekday": "Monday", + "utcTime": "2019-11-25T05:00:00.000-05:00", + "localTime": "0511252019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "D", + "description": "a mixture of sun and clouds. Cold.", + "skyInfo": "11", + "skyDescription": "a mixture of sun and clouds", + "temperature": "-0.80", + "temperatureDesc": "Cold", + "comfort": "-4.99", + "humidity": "99", + "dewPoint": "-1.00", + "precipitationProbability": "12", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "12.96", + "windDirection": "310", + "windDesc": "Northwest", + "windDescShort": "NW", + "visibility": "0.00", + "icon": "4", + "iconName": "partly_cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/3.png", + "dayOfWeek": "2", + "weekday": "Monday", + "utcTime": "2019-11-25T06:00:00.000-05:00", + "localTime": "0611252019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "D", + "description": "Light snow. a mixture of sun and clouds. Cold.", + "skyInfo": "11", + "skyDescription": "a mixture of sun and clouds", + "temperature": "-1.00", + "temperatureDesc": "Cold", + "comfort": "-4.98", + "humidity": "99", + "dewPoint": "-1.20", + "precipitationProbability": "25", + "precipitationDesc": "Light snow", + "rainFall": "*", + "snowFall": "0.16", + "airInfo": "*", + "airDescription": "", + "windSpeed": "11.88", + "windDirection": "308", + "windDesc": "Northwest", + "windDescShort": "NW", + "visibility": "0.00", + "icon": "29", + "iconName": "light_snow", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/20.png", + "dayOfWeek": "2", + "weekday": "Monday", + "utcTime": "2019-11-25T07:00:00.000-05:00", + "localTime": "0711252019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "D", + "description": "Light snow. a mixture of sun and clouds. Cold.", + "skyInfo": "11", + "skyDescription": "a mixture of sun and clouds", + "temperature": "-0.70", + "temperatureDesc": "Cold", + "comfort": "-4.94", + "humidity": "97", + "dewPoint": "-1.10", + "precipitationProbability": "25", + "precipitationDesc": "Light snow", + "rainFall": "*", + "snowFall": "0.10", + "airInfo": "*", + "airDescription": "", + "windSpeed": "13.32", + "windDirection": "309", + "windDesc": "Northwest", + "windDescShort": "NW", + "visibility": "0.00", + "icon": "29", + "iconName": "light_snow", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/20.png", + "dayOfWeek": "2", + "weekday": "Monday", + "utcTime": "2019-11-25T08:00:00.000-05:00", + "localTime": "0811252019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "D", + "description": "a mixture of sun and clouds. Chilly.", + "skyInfo": "11", + "skyDescription": "a mixture of sun and clouds", + "temperature": "0.30", + "temperatureDesc": "Chilly", + "comfort": "-4.26", + "humidity": "90", + "dewPoint": "-1.20", + "precipitationProbability": "12", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "16.20", + "windDirection": "309", + "windDesc": "Northwest", + "windDescShort": "NW", + "visibility": "0.00", + "icon": "4", + "iconName": "partly_cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/3.png", + "dayOfWeek": "2", + "weekday": "Monday", + "utcTime": "2019-11-25T09:00:00.000-05:00", + "localTime": "0911252019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "D", + "description": "a mixture of sun and clouds. Chilly.", + "skyInfo": "11", + "skyDescription": "a mixture of sun and clouds", + "temperature": "1.40", + "temperatureDesc": "Chilly", + "comfort": "-3.13", + "humidity": "84", + "dewPoint": "-1.10", + "precipitationProbability": "11", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "17.64", + "windDirection": "309", + "windDesc": "Northwest", + "windDescShort": "NW", + "visibility": "0.00", + "icon": "4", + "iconName": "partly_cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/3.png", + "dayOfWeek": "2", + "weekday": "Monday", + "utcTime": "2019-11-25T10:00:00.000-05:00", + "localTime": "1011252019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "D", + "description": "Sprinkles. a mixture of sun and clouds. Chilly.", + "skyInfo": "11", + "skyDescription": "a mixture of sun and clouds", + "temperature": "2.10", + "temperatureDesc": "Chilly", + "comfort": "-2.14", + "humidity": "81", + "dewPoint": "-0.80", + "precipitationProbability": "28", + "precipitationDesc": "Sprinkles", + "rainFall": "0.01", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "16.92", + "windDirection": "307", + "windDesc": "Northwest", + "windDescShort": "NW", + "visibility": "0.00", + "icon": "18", + "iconName": "sprinkles", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/27.png", + "dayOfWeek": "2", + "weekday": "Monday", + "utcTime": "2019-11-25T11:00:00.000-05:00", + "localTime": "1111252019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "D", + "description": "Sprinkles. a mixture of sun and clouds. Chilly.", + "skyInfo": "11", + "skyDescription": "a mixture of sun and clouds", + "temperature": "2.40", + "temperatureDesc": "Chilly", + "comfort": "-1.60", + "humidity": "83", + "dewPoint": "-0.30", + "precipitationProbability": "33", + "precipitationDesc": "Sprinkles", + "rainFall": "0.01", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "15.84", + "windDirection": "305", + "windDesc": "Northwest", + "windDescShort": "NW", + "visibility": "0.00", + "icon": "18", + "iconName": "sprinkles", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/27.png", + "dayOfWeek": "2", + "weekday": "Monday", + "utcTime": "2019-11-25T12:00:00.000-05:00", + "localTime": "1211252019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "D", + "description": "Sprinkles. a mixture of sun and clouds. Chilly.", + "skyInfo": "11", + "skyDescription": "a mixture of sun and clouds", + "temperature": "2.60", + "temperatureDesc": "Chilly", + "comfort": "-1.11", + "humidity": "84", + "dewPoint": "0.20", + "precipitationProbability": "37", + "precipitationDesc": "Sprinkles", + "rainFall": "0.01", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "14.40", + "windDirection": "303", + "windDesc": "Northwest", + "windDescShort": "NW", + "visibility": "0.00", + "icon": "18", + "iconName": "sprinkles", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/27.png", + "dayOfWeek": "2", + "weekday": "Monday", + "utcTime": "2019-11-25T13:00:00.000-05:00", + "localTime": "1311252019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "D", + "description": "a mixture of sun and clouds. Chilly.", + "skyInfo": "11", + "skyDescription": "a mixture of sun and clouds", + "temperature": "2.40", + "temperatureDesc": "Chilly", + "comfort": "-1.15", + "humidity": "87", + "dewPoint": "0.50", + "precipitationProbability": "10", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "13.32", + "windDirection": "299", + "windDesc": "Northwest", + "windDescShort": "NW", + "visibility": "0.00", + "icon": "4", + "iconName": "partly_cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/3.png", + "dayOfWeek": "2", + "weekday": "Monday", + "utcTime": "2019-11-25T14:00:00.000-05:00", + "localTime": "1411252019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "D", + "description": "a mixture of sun and clouds. Chilly.", + "skyInfo": "11", + "skyDescription": "a mixture of sun and clouds", + "temperature": "2.20", + "temperatureDesc": "Chilly", + "comfort": "-1.19", + "humidity": "89", + "dewPoint": "0.60", + "precipitationProbability": "10", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "12.24", + "windDirection": "295", + "windDesc": "Northwest", + "windDescShort": "NW", + "visibility": "0.00", + "icon": "4", + "iconName": "partly_cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/3.png", + "dayOfWeek": "2", + "weekday": "Monday", + "utcTime": "2019-11-25T15:00:00.000-05:00", + "localTime": "1511252019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "D", + "description": "a mixture of sun and clouds. Chilly.", + "skyInfo": "11", + "skyDescription": "a mixture of sun and clouds", + "temperature": "1.60", + "temperatureDesc": "Chilly", + "comfort": "-1.68", + "humidity": "93", + "dewPoint": "0.50", + "precipitationProbability": "11", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "11.16", + "windDirection": "290", + "windDesc": "West", + "windDescShort": "W", + "visibility": "0.00", + "icon": "4", + "iconName": "partly_cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/3.png", + "dayOfWeek": "2", + "weekday": "Monday", + "utcTime": "2019-11-25T16:00:00.000-05:00", + "localTime": "1611252019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Cloudy. Chilly.", + "skyInfo": "17", + "skyDescription": "Cloudy", + "temperature": "1.20", + "temperatureDesc": "Chilly", + "comfort": "-1.81", + "humidity": "92", + "dewPoint": "0.00", + "precipitationProbability": "10", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "9.72", + "windDirection": "277", + "windDesc": "West", + "windDescShort": "W", + "visibility": "0.00", + "icon": "17", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", + "dayOfWeek": "2", + "weekday": "Monday", + "utcTime": "2019-11-25T17:00:00.000-05:00", + "localTime": "1711252019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Cloudy. Chilly.", + "skyInfo": "17", + "skyDescription": "Cloudy", + "temperature": "0.30", + "temperatureDesc": "Chilly", + "comfort": "-2.79", + "humidity": "91", + "dewPoint": "-1.00", + "precipitationProbability": "9", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "9.36", + "windDirection": "255", + "windDesc": "West", + "windDescShort": "W", + "visibility": "0.00", + "icon": "17", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", + "dayOfWeek": "2", + "weekday": "Monday", + "utcTime": "2019-11-25T18:00:00.000-05:00", + "localTime": "1811252019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Cloudy. Cold.", + "skyInfo": "17", + "skyDescription": "Cloudy", + "temperature": "-0.80", + "temperatureDesc": "Cold", + "comfort": "-4.09", + "humidity": "94", + "dewPoint": "-1.70", + "precipitationProbability": "7", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "9.36", + "windDirection": "238", + "windDesc": "Southwest", + "windDescShort": "SW", + "visibility": "0.00", + "icon": "17", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", + "dayOfWeek": "2", + "weekday": "Monday", + "utcTime": "2019-11-25T19:00:00.000-05:00", + "localTime": "1911252019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Cloudy. Cold.", + "skyInfo": "17", + "skyDescription": "Cloudy", + "temperature": "-0.90", + "temperatureDesc": "Cold", + "comfort": "-4.21", + "humidity": "95", + "dewPoint": "-1.60", + "precipitationProbability": "6", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "9.36", + "windDirection": "239", + "windDesc": "Southwest", + "windDescShort": "SW", + "visibility": "0.00", + "icon": "17", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", + "dayOfWeek": "2", + "weekday": "Monday", + "utcTime": "2019-11-25T20:00:00.000-05:00", + "localTime": "2011252019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Cloudy. Cold.", + "skyInfo": "17", + "skyDescription": "Cloudy", + "temperature": "-0.60", + "temperatureDesc": "Cold", + "comfort": "-3.96", + "humidity": "94", + "dewPoint": "-1.40", + "precipitationProbability": "11", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "9.72", + "windDirection": "248", + "windDesc": "West", + "windDescShort": "W", + "visibility": "0.00", + "icon": "17", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", + "dayOfWeek": "2", + "weekday": "Monday", + "utcTime": "2019-11-25T21:00:00.000-05:00", + "localTime": "2111252019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Cloudy. Cold.", + "skyInfo": "17", + "skyDescription": "Cloudy", + "temperature": "-0.80", + "temperatureDesc": "Cold", + "comfort": "-4.19", + "humidity": "96", + "dewPoint": "-1.30", + "precipitationProbability": "9", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "9.72", + "windDirection": "253", + "windDesc": "West", + "windDescShort": "W", + "visibility": "0.00", + "icon": "17", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", + "dayOfWeek": "2", + "weekday": "Monday", + "utcTime": "2019-11-25T22:00:00.000-05:00", + "localTime": "2211252019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Cloudy. Cold.", + "skyInfo": "17", + "skyDescription": "Cloudy", + "temperature": "-0.80", + "temperatureDesc": "Cold", + "comfort": "-4.19", + "humidity": "96", + "dewPoint": "-1.30", + "precipitationProbability": "9", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "9.72", + "windDirection": "252", + "windDesc": "West", + "windDescShort": "W", + "visibility": "0.00", + "icon": "17", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", + "dayOfWeek": "2", + "weekday": "Monday", + "utcTime": "2019-11-25T23:00:00.000-05:00", + "localTime": "2311252019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Cloudy. Cold.", + "skyInfo": "17", + "skyDescription": "Cloudy", + "temperature": "-0.70", + "temperatureDesc": "Cold", + "comfort": "-4.07", + "humidity": "96", + "dewPoint": "-1.30", + "precipitationProbability": "9", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "9.72", + "windDirection": "251", + "windDesc": "West", + "windDescShort": "W", + "visibility": "0.00", + "icon": "17", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", + "dayOfWeek": "3", + "weekday": "Tuesday", + "utcTime": "2019-11-26T00:00:00.000-05:00", + "localTime": "0011262019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Cloudy. Cold.", + "skyInfo": "17", + "skyDescription": "Cloudy", + "temperature": "-0.80", + "temperatureDesc": "Cold", + "comfort": "-4.09", + "humidity": "96", + "dewPoint": "-1.30", + "precipitationProbability": "10", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "9.36", + "windDirection": "247", + "windDesc": "Southwest", + "windDescShort": "SW", + "visibility": "0.00", + "icon": "17", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", + "dayOfWeek": "3", + "weekday": "Tuesday", + "utcTime": "2019-11-26T01:00:00.000-05:00", + "localTime": "0111262019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Cloudy. Cold.", + "skyInfo": "17", + "skyDescription": "Cloudy", + "temperature": "-0.90", + "temperatureDesc": "Cold", + "comfort": "-4.11", + "humidity": "97", + "dewPoint": "-1.30", + "precipitationProbability": "9", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "9.00", + "windDirection": "241", + "windDesc": "Southwest", + "windDescShort": "SW", + "visibility": "0.00", + "icon": "17", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", + "dayOfWeek": "3", + "weekday": "Tuesday", + "utcTime": "2019-11-26T02:00:00.000-05:00", + "localTime": "0211262019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Cloudy. Cold.", + "skyInfo": "17", + "skyDescription": "Cloudy", + "temperature": "-0.90", + "temperatureDesc": "Cold", + "comfort": "-4.00", + "humidity": "96", + "dewPoint": "-1.40", + "precipitationProbability": "9", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "8.64", + "windDirection": "232", + "windDesc": "Southwest", + "windDescShort": "SW", + "visibility": "0.00", + "icon": "17", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", + "dayOfWeek": "3", + "weekday": "Tuesday", + "utcTime": "2019-11-26T03:00:00.000-05:00", + "localTime": "0311262019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Cloudy. Cold.", + "skyInfo": "17", + "skyDescription": "Cloudy", + "temperature": "-1.20", + "temperatureDesc": "Cold", + "comfort": "-4.24", + "humidity": "98", + "dewPoint": "-1.50", + "precipitationProbability": "9", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "8.28", + "windDirection": "224", + "windDesc": "Southwest", + "windDescShort": "SW", + "visibility": "0.00", + "icon": "17", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", + "dayOfWeek": "3", + "weekday": "Tuesday", + "utcTime": "2019-11-26T04:00:00.000-05:00", + "localTime": "0411262019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "N", + "description": "Cloudy. Cold.", + "skyInfo": "17", + "skyDescription": "Cloudy", + "temperature": "-1.40", + "temperatureDesc": "Cold", + "comfort": "-4.36", + "humidity": "99", + "dewPoint": "-1.60", + "precipitationProbability": "9", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "7.92", + "windDirection": "215", + "windDesc": "Southwest", + "windDescShort": "SW", + "visibility": "0.00", + "icon": "17", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", + "dayOfWeek": "3", + "weekday": "Tuesday", + "utcTime": "2019-11-26T05:00:00.000-05:00", + "localTime": "0511262019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "D", + "description": "a mixture of sun and clouds. Cold.", + "skyInfo": "11", + "skyDescription": "a mixture of sun and clouds", + "temperature": "-1.10", + "temperatureDesc": "Cold", + "comfort": "-3.88", + "humidity": "99", + "dewPoint": "-1.30", + "precipitationProbability": "9", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "7.56", + "windDirection": "207", + "windDesc": "Southwest", + "windDescShort": "SW", + "visibility": "0.00", + "icon": "4", + "iconName": "partly_cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/3.png", + "dayOfWeek": "3", + "weekday": "Tuesday", + "utcTime": "2019-11-26T06:00:00.000-05:00", + "localTime": "0611262019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "D", + "description": "a mixture of sun and clouds. Cold.", + "skyInfo": "11", + "skyDescription": "a mixture of sun and clouds", + "temperature": "-0.60", + "temperatureDesc": "Cold", + "comfort": "-3.30", + "humidity": "96", + "dewPoint": "-1.10", + "precipitationProbability": "9", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "7.56", + "windDirection": "199", + "windDesc": "South", + "windDescShort": "S", + "visibility": "0.00", + "icon": "4", + "iconName": "partly_cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/3.png", + "dayOfWeek": "3", + "weekday": "Tuesday", + "utcTime": "2019-11-26T07:00:00.000-05:00", + "localTime": "0711262019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "D", + "description": "a mixture of sun and clouds. Cold.", + "skyInfo": "11", + "skyDescription": "a mixture of sun and clouds", + "temperature": "-0.60", + "temperatureDesc": "Cold", + "comfort": "-3.17", + "humidity": "99", + "dewPoint": "-0.80", + "precipitationProbability": "9", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "7.20", + "windDirection": "199", + "windDesc": "South", + "windDescShort": "S", + "visibility": "0.00", + "icon": "4", + "iconName": "partly_cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/3.png", + "dayOfWeek": "3", + "weekday": "Tuesday", + "utcTime": "2019-11-26T08:00:00.000-05:00", + "localTime": "0811262019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "D", + "description": "a mixture of sun and clouds. Chilly.", + "skyInfo": "11", + "skyDescription": "a mixture of sun and clouds", + "temperature": "0.50", + "temperatureDesc": "Chilly", + "comfort": "-1.77", + "humidity": "97", + "dewPoint": "0.10", + "precipitationProbability": "9", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "6.84", + "windDirection": "209", + "windDesc": "Southwest", + "windDescShort": "SW", + "visibility": "0.00", + "icon": "4", + "iconName": "partly_cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/3.png", + "dayOfWeek": "3", + "weekday": "Tuesday", + "utcTime": "2019-11-26T09:00:00.000-05:00", + "localTime": "0911262019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "D", + "description": "a mixture of sun and clouds. Chilly.", + "skyInfo": "11", + "skyDescription": "a mixture of sun and clouds", + "temperature": "2.40", + "temperatureDesc": "Chilly", + "comfort": "0.56", + "humidity": "89", + "dewPoint": "0.80", + "precipitationProbability": "9", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "6.48", + "windDirection": "222", + "windDesc": "Southwest", + "windDescShort": "SW", + "visibility": "0.00", + "icon": "4", + "iconName": "partly_cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/3.png", + "dayOfWeek": "3", + "weekday": "Tuesday", + "utcTime": "2019-11-26T10:00:00.000-05:00", + "localTime": "1011262019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "D", + "description": "a mixture of sun and clouds. Chilly.", + "skyInfo": "11", + "skyDescription": "a mixture of sun and clouds", + "temperature": "3.90", + "temperatureDesc": "Chilly", + "comfort": "2.07", + "humidity": "83", + "dewPoint": "1.30", + "precipitationProbability": "9", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "7.20", + "windDirection": "235", + "windDesc": "Southwest", + "windDescShort": "SW", + "visibility": "0.00", + "icon": "4", + "iconName": "partly_cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/3.png", + "dayOfWeek": "3", + "weekday": "Tuesday", + "utcTime": "2019-11-26T11:00:00.000-05:00", + "localTime": "1111262019", + "localTimeFormat": "HHMMddyyyy" + }, + { + "daylight": "D", + "description": "a mixture of sun and clouds. Chilly.", + "skyInfo": "11", + "skyDescription": "a mixture of sun and clouds", + "temperature": "4.60", + "temperatureDesc": "Chilly", + "comfort": "2.78", + "humidity": "81", + "dewPoint": "1.60", + "precipitationProbability": "9", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "7.56", + "windDirection": "246", + "windDesc": "Southwest", + "windDescShort": "SW", + "visibility": "0.00", + "icon": "4", + "iconName": "partly_cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/3.png", + "dayOfWeek": "3", + "weekday": "Tuesday", + "utcTime": "2019-11-26T12:00:00.000-05:00", + "localTime": "1211262019", + "localTimeFormat": "HHMMddyyyy" + } + ], + "country": "United States", + "state": "New York", + "city": "New York", + "latitude": 43.00035, + "longitude": -75.4999, + "distance": 0.00, + "timezone": -5 + } + }, + "feedCreation": "2019-11-19T23:06:04.631Z", + "metric": true +} \ No newline at end of file diff --git a/tests/fixtures/here_weather/destination_weather_forecasts_simple.json b/tests/fixtures/here_weather/destination_weather_forecasts_simple.json new file mode 100644 index 00000000000000..dd04c974d1ca7a --- /dev/null +++ b/tests/fixtures/here_weather/destination_weather_forecasts_simple.json @@ -0,0 +1,248 @@ +{ + "dailyForecasts": { + "forecastLocation": { + "forecast": [ + { + "daylight": "D", + "description": "Overcast. Chilly.", + "skyInfo": "18", + "skyDescription": "Overcast", + "temperatureDesc": "Chilly", + "comfort": "-0.55", + "highTemperature": "4.00", + "lowTemperature": "-1.80", + "humidity": "80", + "dewPoint": "-0.40", + "precipitationProbability": "53", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "1.42", + "airInfo": "*", + "airDescription": "", + "windSpeed": "12.03", + "windDirection": "290", + "windDesc": "West", + "windDescShort": "W", + "beaufortScale": "3", + "beaufortDescription": "Gentle breeze", + "uvIndex": "0", + "uvDesc": "Minimal", + "barometerPressure": "1014.29", + "icon": "7", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/17.png", + "dayOfWeek": "3", + "weekday": "Tuesday", + "utcTime": "2019-11-19T00:00:00.000-05:00" + }, + { + "daylight": "D", + "description": "Light snow. Overcast. Chilly.", + "skyInfo": "18", + "skyDescription": "Overcast", + "temperatureDesc": "Chilly", + "comfort": "-2.53", + "highTemperature": "2.30", + "lowTemperature": "-1.90", + "humidity": "86", + "dewPoint": "-0.37", + "precipitationProbability": "53", + "precipitationDesc": "Light snow", + "rainFall": "0.05", + "snowFall": "0.73", + "airInfo": "*", + "airDescription": "", + "windSpeed": "16.87", + "windDirection": "308", + "windDesc": "Northwest", + "windDescShort": "NW", + "beaufortScale": "3", + "beaufortDescription": "Gentle breeze", + "uvIndex": "0", + "uvDesc": "Minimal", + "barometerPressure": "1022.34", + "icon": "29", + "iconName": "light_snow", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/20.png", + "dayOfWeek": "4", + "weekday": "Wednesday", + "utcTime": "2019-11-20T00:00:00.000-05:00" + }, + { + "daylight": "D", + "description": "Sprinkles late. a mixture of sun and clouds. Chilly.", + "skyInfo": "11", + "skyDescription": "a mixture of sun and clouds", + "temperatureDesc": "Chilly", + "comfort": "0.59", + "highTemperature": "3.50", + "lowTemperature": "-0.10", + "humidity": "82", + "dewPoint": "0.23", + "precipitationProbability": "49", + "precipitationDesc": "Sprinkles late", + "rainFall": "0.04", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "9.26", + "windDirection": "267", + "windDesc": "West", + "windDescShort": "W", + "beaufortScale": "2", + "beaufortDescription": "Light breeze", + "uvIndex": "0", + "uvDesc": "Minimal", + "barometerPressure": "1009.17", + "icon": "18", + "iconName": "sprinkles", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/27.png", + "dayOfWeek": "5", + "weekday": "Thursday", + "utcTime": "2019-11-21T00:00:00.000-05:00" + }, + { + "daylight": "D", + "description": "Sprinkles early. Overcast. Chilly.", + "skyInfo": "18", + "skyDescription": "Overcast", + "temperatureDesc": "Chilly", + "comfort": "-3.68", + "highTemperature": "6.90", + "lowTemperature": "-2.50", + "humidity": "85", + "dewPoint": "0.07", + "precipitationProbability": "62", + "precipitationDesc": "Sprinkles early", + "rainFall": "0.19", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "33.94", + "windDirection": "264", + "windDesc": "West", + "windDescShort": "W", + "beaufortScale": "5", + "beaufortDescription": "Fresh breeze", + "uvIndex": "0", + "uvDesc": "Minimal", + "barometerPressure": "1014.99", + "icon": "18", + "iconName": "sprinkles", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/27.png", + "dayOfWeek": "6", + "weekday": "Friday", + "utcTime": "2019-11-22T00:00:00.000-05:00" + }, + { + "daylight": "D", + "description": "a mixture of sun and clouds. Chilly.", + "skyInfo": "11", + "skyDescription": "a mixture of sun and clouds", + "temperatureDesc": "Chilly", + "comfort": "-2.87", + "highTemperature": "1.20", + "lowTemperature": "-5.20", + "humidity": "72", + "dewPoint": "-4.16", + "precipitationProbability": "4", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "9.72", + "windDirection": "300", + "windDesc": "Northwest", + "windDescShort": "NW", + "beaufortScale": "2", + "beaufortDescription": "Light breeze", + "uvIndex": "0", + "uvDesc": "Minimal", + "barometerPressure": "1008.99", + "icon": "4", + "iconName": "partly_cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/3.png", + "dayOfWeek": "7", + "weekday": "Saturday", + "utcTime": "2019-11-23T00:00:00.000-05:00" + }, + { + "daylight": "D", + "description": "Light snow late. Increasing cloudiness. Chilly.", + "skyInfo": "26", + "skyDescription": "Increasing cloudiness", + "temperatureDesc": "Chilly", + "comfort": "-5.11", + "highTemperature": "0.60", + "lowTemperature": "-1.60", + "humidity": "87", + "dewPoint": "-1.93", + "precipitationProbability": "40", + "precipitationDesc": "Light snow late", + "rainFall": "*", + "snowFall": "0.20", + "airInfo": "*", + "airDescription": "", + "windSpeed": "19.34", + "windDirection": "297", + "windDesc": "Northwest", + "windDescShort": "NW", + "beaufortScale": "4", + "beaufortDescription": "Moderate breeze", + "uvIndex": "0", + "uvDesc": "Minimal", + "barometerPressure": "1010.16", + "icon": "29", + "iconName": "light_snow", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/20.png", + "dayOfWeek": "1", + "weekday": "Sunday", + "utcTime": "2019-11-24T00:00:00.000-05:00" + }, + { + "daylight": "D", + "description": "Snow changing to rain. Cloudy. Chilly.", + "skyInfo": "17", + "skyDescription": "Cloudy", + "temperatureDesc": "Chilly", + "comfort": "-1.66", + "highTemperature": "2.60", + "lowTemperature": "-1.00", + "humidity": "88", + "dewPoint": "0.07", + "precipitationProbability": "51", + "precipitationDesc": "Snow changing to rain", + "rainFall": "0.03", + "snowFall": "0.36", + "airInfo": "*", + "airDescription": "", + "windSpeed": "12.29", + "windDirection": "303", + "windDesc": "Northwest", + "windDescShort": "NW", + "beaufortScale": "3", + "beaufortDescription": "Gentle breeze", + "uvIndex": "0", + "uvDesc": "Minimal", + "barometerPressure": "1016.65", + "icon": "28", + "iconName": "snow_rain_mix", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/19.png", + "dayOfWeek": "2", + "weekday": "Monday", + "utcTime": "2019-11-25T00:00:00.000-05:00" + } + ], + "country": "United States", + "state": "New York", + "city": "New York", + "latitude": 43.00035, + "longitude": -75.4999, + "distance": 0.00, + "timezone": -5 + } + }, + "feedCreation": "2019-11-19T23:01:26.632Z", + "metric": true +} \ No newline at end of file diff --git a/tests/fixtures/here_weather/destination_weather_observation_imperial.json b/tests/fixtures/here_weather/destination_weather_observation_imperial.json new file mode 100644 index 00000000000000..3932393c1b6efe --- /dev/null +++ b/tests/fixtures/here_weather/destination_weather_observation_imperial.json @@ -0,0 +1,61 @@ +{ + "observations": { + "location": [ + { + "observation": [ + { + "daylight": "D", + "description": "Overcast. Cool.", + "skyInfo": "18", + "skyDescription": "Overcast", + "temperature": "46.00", + "temperatureDesc": "Cool", + "comfort": "42.39", + "highTemperature": "50.72", + "lowTemperature": "39.74", + "humidity": "63", + "dewPoint": "34.00", + "precipitation1H": "*", + "precipitation3H": "*", + "precipitation6H": "*", + "precipitation12H": "*", + "precipitation24H": "*", + "precipitationDesc": "", + "airInfo": "*", + "airDescription": "", + "windSpeed": "6.89", + "windDirection": "0", + "windDesc": "North", + "windDescShort": "N", + "barometerPressure": "29.80", + "barometerTrend": "Rising", + "visibility": "10.00", + "snowCover": "*", + "icon": "7", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/17.png", + "ageMinutes": "46", + "activeAlerts": "6", + "country": "United States", + "state": "New York", + "city": "New York", + "latitude": 40.7996, + "longitude": -73.9703, + "distance": 1.43, + "elevation": 0.00, + "utcTime": "2019-11-19T15:51:00.000-05:00" + } + ], + "country": "United States", + "state": "New York", + "city": "New York", + "latitude": 40.79962, + "longitude": -73.970314, + "distance": 0.00, + "timezone": -5 + } + ] + }, + "feedCreation": "2019-11-19T21:37:57.279Z", + "metric": false +} \ No newline at end of file From bdad7e70f8a024ab8a4a41bbc76c4bb5ef38495b Mon Sep 17 00:00:00 2001 From: Kevin Eifinger Date: Thu, 21 Nov 2019 09:01:46 +0100 Subject: [PATCH 03/56] execute script.hassfest --- CODEOWNERS | 1 + 1 file changed, 1 insertion(+) diff --git a/CODEOWNERS b/CODEOWNERS index 0e92885d247a31..4563d8f132de5b 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -205,6 +205,7 @@ homeassistant/components/hassio/* @home-assistant/supervisor homeassistant/components/heatmiser/* @andylockran homeassistant/components/heos/* @andrewsayre homeassistant/components/here_travel_time/* @eifinger +homeassistant/components/here_weather/* @eifinger homeassistant/components/hikvision/* @mezz64 homeassistant/components/hikvisioncam/* @fbradyirl homeassistant/components/hisense_aehw4a1/* @bannhead From acd3e677d479696ffd29bbde0e3a45567d9e45a1 Mon Sep 17 00:00:00 2001 From: Kevin Eifinger Date: Thu, 21 Nov 2019 10:32:37 +0100 Subject: [PATCH 04/56] fix tests --- tests/components/here_weather/test_weather.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/components/here_weather/test_weather.py b/tests/components/here_weather/test_weather.py index 1550f0ac55c4d4..415978836ebf22 100644 --- a/tests/components/here_weather/test_weather.py +++ b/tests/components/here_weather/test_weather.py @@ -52,13 +52,13 @@ def requests_mock_invalid_credentials(requests_mock): @pytest.fixture def requests_mock_location_name(requests_mock_credentials_check): """Add the url used in a request with location_name to requests mock.""" - product = herepy.WeatherProductType.forecast_astronomy + product = herepy.WeatherProductType.forecast_7days response_url = build_location_name_mock_url( APP_ID, APP_CODE, LOCATION_NAME, product ) requests_mock_credentials_check.get( response_url, - text=load_fixture("here_weather/destination_weather_forecasts_astronomy.json"), + text=load_fixture("here_weather/destination_weather_forecasts.json"), ) return requests_mock_credentials_check @@ -66,13 +66,13 @@ def requests_mock_location_name(requests_mock_credentials_check): @pytest.fixture def requests_mock_coordinates(requests_mock_credentials_check): """Add the url used in a request with coordinates to requests mock.""" - product = herepy.WeatherProductType.forecast_astronomy + product = herepy.WeatherProductType.forecast_7days response_url = build_coordinates_mock_url( APP_ID, APP_CODE, LATITUDE, LONGITUDE, product ) requests_mock_credentials_check.get( response_url, - text=load_fixture("here_weather/destination_weather_forecasts_astronomy.json"), + text=load_fixture("here_weather/destination_weather_forecasts.json"), ) return requests_mock_credentials_check From e1b26d8b5513417b12d6c8dd7caaab4d5f63373f Mon Sep 17 00:00:00 2001 From: Kevin Eifinger Date: Sat, 23 Nov 2019 09:58:56 +0100 Subject: [PATCH 05/56] remove redundant update interval constants --- homeassistant/components/here_weather/sensor.py | 3 --- homeassistant/components/here_weather/weather.py | 4 ---- 2 files changed, 7 deletions(-) diff --git a/homeassistant/components/here_weather/sensor.py b/homeassistant/components/here_weather/sensor.py index 58cafe87a76508..57127c42fb54f3 100644 --- a/homeassistant/components/here_weather/sensor.py +++ b/homeassistant/components/here_weather/sensor.py @@ -1,5 +1,4 @@ """Support for the HERE Destination Weather service.""" -from datetime import timedelta import logging from typing import Callable, Dict, Optional, Union @@ -41,8 +40,6 @@ _LOGGER = logging.getLogger(__name__) -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=120) - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_APP_ID): cv.string, diff --git a/homeassistant/components/here_weather/weather.py b/homeassistant/components/here_weather/weather.py index 2b810b0d600215..1aad13e055eb31 100644 --- a/homeassistant/components/here_weather/weather.py +++ b/homeassistant/components/here_weather/weather.py @@ -1,5 +1,4 @@ """Support for the HERE Destination Weather API.""" -from datetime import timedelta import logging from typing import Callable, Dict, Union @@ -56,9 +55,6 @@ DEFAULT_NAME = "HERE" -MIN_TIME_BETWEEN_FORECAST_UPDATES = timedelta(minutes=30) -MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=10) - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_APP_ID): cv.string, From b639ca85fef7c0192b15227910c4e4ac4f7b2296 Mon Sep 17 00:00:00 2001 From: Kevin Eifinger Date: Mon, 16 Dec 2019 09:39:05 +0100 Subject: [PATCH 06/56] Migrate to api_key --- .../components/here_weather/const.py | 14 +---- .../components/here_weather/manifest.json | 2 +- .../components/here_weather/sensor.py | 12 ++-- .../components/here_weather/weather.py | 12 ++-- tests/components/here_weather/__init__.py | 24 ++++---- tests/components/here_weather/test_sensor.py | 58 ++++++------------- tests/components/here_weather/test_weather.py | 55 ++++++------------ 7 files changed, 62 insertions(+), 115 deletions(-) diff --git a/homeassistant/components/here_weather/const.py b/homeassistant/components/here_weather/const.py index 8b8abf8bb13d26..b824d58143e4ab 100644 --- a/homeassistant/components/here_weather/const.py +++ b/homeassistant/components/here_weather/const.py @@ -1,6 +1,5 @@ """Constants for the HERE Destination Weather service.""" -CONF_APP_ID = "app_id" -CONF_APP_CODE = "app_code" +CONF_API_KEY = "api_key" CONF_LOCATION_NAME = "location_name" CONF_ZIP_CODE = "zip_code" CONF_LANGUAGE = "language" @@ -280,14 +279,7 @@ "isolated_tstorms", "a_few_tstorms", ], - "lightning-rainy": [ - "strong_thunderstorms", - "severe_thunderstorms", - "thundershowers", - "thunderstorms", - "tstorms_late", - "tstorms", - ], + "lightning-rainy": ["thundershowers", "thunderstorms", "tstorms_late", "tstorms"], "partlycloudy": [ "partly_sunny", "mostly_cloudy", @@ -387,7 +379,7 @@ "mixture_of_precip", ], "sunny": ["sunny", "clear", "mostly_sunny", "mostly_clear"], - "windy": [], + "windy": ["strong_thunderstorms", "severe_thunderstorms"], "windy-variant": [], "exceptional": [ "blizzard", diff --git a/homeassistant/components/here_weather/manifest.json b/homeassistant/components/here_weather/manifest.json index 790882a37109ba..d8d584a1be474a 100644 --- a/homeassistant/components/here_weather/manifest.json +++ b/homeassistant/components/here_weather/manifest.json @@ -3,7 +3,7 @@ "name": "HERE Destination Weather", "documentation": "https://www.home-assistant.io/components/here_weather", "requirements": [ - "herepy==0.6.3.3" + "herepy==2.0.0" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/here_weather/sensor.py b/homeassistant/components/here_weather/sensor.py index 57127c42fb54f3..68c4dd64656876 100644 --- a/homeassistant/components/here_weather/sensor.py +++ b/homeassistant/components/here_weather/sensor.py @@ -25,8 +25,7 @@ get_attribute_from_here_data, ) from .const import ( - CONF_APP_CODE, - CONF_APP_ID, + CONF_API_KEY, CONF_LOCATION_NAME, CONF_MODES, CONF_OFFSET, @@ -42,8 +41,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { - vol.Required(CONF_APP_ID): cv.string, - vol.Required(CONF_APP_CODE): cv.string, + vol.Required(CONF_API_KEY): cv.string, vol.Inclusive(CONF_LATITUDE, "coordinates"): cv.latitude, vol.Inclusive(CONF_LONGITUDE, "coordinates"): cv.longitude, vol.Exclusive(CONF_LATITUDE, "coords_or_name_or_zip_code"): cv.latitude, @@ -68,10 +66,10 @@ async def async_setup_platform( discovery_info: None = None, ) -> None: """Set up the HERE Destination Weather sensor.""" - app_id = config[CONF_APP_ID] - app_code = config[CONF_APP_CODE] - here_client = herepy.DestinationWeatherApi(app_id, app_code) + api_key = config[CONF_API_KEY] + + here_client = herepy.DestinationWeatherApi(api_key) if not await hass.async_add_executor_job( _are_valid_client_credentials, here_client diff --git a/homeassistant/components/here_weather/weather.py b/homeassistant/components/here_weather/weather.py index 1aad13e055eb31..9b0db1c9da6076 100644 --- a/homeassistant/components/here_weather/weather.py +++ b/homeassistant/components/here_weather/weather.py @@ -36,8 +36,7 @@ ) from .const import ( CONDITION_CLASSES, - CONF_APP_CODE, - CONF_APP_ID, + CONF_API_KEY, CONF_LOCATION_NAME, CONF_ZIP_CODE, DEFAULT_MODE, @@ -57,8 +56,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { - vol.Required(CONF_APP_ID): cv.string, - vol.Required(CONF_APP_CODE): cv.string, + vol.Required(CONF_API_KEY): cv.string, vol.Inclusive(CONF_LATITUDE, "coordinates"): cv.latitude, vol.Inclusive(CONF_LONGITUDE, "coordinates"): cv.longitude, vol.Exclusive(CONF_LATITUDE, "coords_or_name_or_zip_code"): cv.latitude, @@ -82,10 +80,10 @@ async def async_setup_platform( discovery_info: None = None, ) -> None: """Set up the HERE Destination weather platform.""" - app_id = config[CONF_APP_ID] - app_code = config[CONF_APP_CODE] - here_client = herepy.DestinationWeatherApi(app_id, app_code) + api_key = config[CONF_API_KEY] + + here_client = herepy.DestinationWeatherApi(api_key) if not await hass.async_add_executor_job( _are_valid_client_credentials, here_client diff --git a/tests/components/here_weather/__init__.py b/tests/components/here_weather/__init__.py index 159c72643fa50a..f89215818044ce 100644 --- a/tests/components/here_weather/__init__.py +++ b/tests/components/here_weather/__init__.py @@ -3,8 +3,7 @@ PLATFORM = "here_weather" -APP_ID = "test" -APP_CODE = "test" +API_KEY = "test" ZIP_CODE = "10025" @@ -14,12 +13,11 @@ LONGITUDE = "-73.970314" -def build_base_mock_url(app_id, app_code, additional_params): +def build_base_mock_url(api_key, additional_params): """Construct a url for HERE.""" base_url = "https://weather.api.here.com/weather/1.0/report.json?" parameters = { - "app_id": app_id, - "app_code": app_code, + "apikey": api_key, "oneobservation": True, "metric": True, } @@ -28,29 +26,29 @@ def build_base_mock_url(app_id, app_code, additional_params): return url -def build_zip_code_imperial_mock_url(app_id, app_code, zip_code, product): +def build_zip_code_imperial_mock_url(api_key, zip_code, product): """Construct a url for HERE.""" parameters = {"product": product, "zipcode": zip_code, "metric": False} - url = build_base_mock_url(app_id, app_code, parameters) + url = build_base_mock_url(api_key, parameters) return url -def build_coordinates_mock_url(app_id, app_code, latitude, longitude, product): +def build_coordinates_mock_url(api_key, latitude, longitude, product): """Construct a url for HERE.""" parameters = {"product": product, "latitude": latitude, "longitude": longitude} - url = build_base_mock_url(app_id, app_code, parameters) + url = build_base_mock_url(api_key, parameters) return url -def build_location_name_mock_url(app_id, app_code, location_name, product): +def build_location_name_mock_url(api_key, location_name, product): """Construct a url for HERE.""" parameters = {"product": product, "name": location_name} - url = build_base_mock_url(app_id, app_code, parameters) + url = build_base_mock_url(api_key, parameters) return url -def build_zip_code_mock_url(app_id, app_code, zip_code, product): +def build_zip_code_mock_url(api_key, zip_code, product): """Construct a url for HERE.""" parameters = {"product": product, "zipcode": zip_code} - url = build_base_mock_url(app_id, app_code, parameters) + url = build_base_mock_url(api_key, parameters) return url diff --git a/tests/components/here_weather/test_sensor.py b/tests/components/here_weather/test_sensor.py index af6b854ce639c3..0ead97cc8ea857 100644 --- a/tests/components/here_weather/test_sensor.py +++ b/tests/components/here_weather/test_sensor.py @@ -13,8 +13,7 @@ from homeassistant.setup import async_setup_component from . import ( - APP_CODE, - APP_ID, + API_KEY, LATITUDE, LOCATION_NAME, LONGITUDE, @@ -35,7 +34,7 @@ def requests_mock_credentials_check(requests_mock): """Add the url used in the api validation to all requests mock.""" product = herepy.WeatherProductType.forecast_astronomy - response_url = build_zip_code_mock_url(APP_ID, APP_CODE, ZIP_CODE, product) + response_url = build_zip_code_mock_url(API_KEY, ZIP_CODE, product) requests_mock.get( response_url, text=load_fixture("here_weather/destination_weather_forecasts_astronomy.json"), @@ -47,7 +46,7 @@ def requests_mock_credentials_check(requests_mock): def requests_mock_invalid_credentials(requests_mock): """Add the url for invalid credentials to requests mock.""" product = herepy.WeatherProductType.forecast_astronomy - response_url = build_zip_code_mock_url(APP_ID, APP_CODE, ZIP_CODE, product) + response_url = build_zip_code_mock_url(API_KEY, ZIP_CODE, product) requests_mock.get( response_url, text=load_fixture("here_weather/destination_weather_error_unauthorized.json"), @@ -59,9 +58,7 @@ def requests_mock_invalid_credentials(requests_mock): def requests_mock_location_name(requests_mock_credentials_check): """Add the url used in a request with location_name to requests mock.""" product = herepy.WeatherProductType.forecast_astronomy - response_url = build_location_name_mock_url( - APP_ID, APP_CODE, LOCATION_NAME, product - ) + response_url = build_location_name_mock_url(API_KEY, LOCATION_NAME, product) requests_mock_credentials_check.get( response_url, text=load_fixture("here_weather/destination_weather_forecasts_astronomy.json"), @@ -73,9 +70,7 @@ def requests_mock_location_name(requests_mock_credentials_check): def requests_mock_coordinates(requests_mock_credentials_check): """Add the url used in a request with coordinates to requests mock.""" product = herepy.WeatherProductType.forecast_astronomy - response_url = build_coordinates_mock_url( - APP_ID, APP_CODE, LATITUDE, LONGITUDE, product - ) + response_url = build_coordinates_mock_url(API_KEY, LATITUDE, LONGITUDE, product) requests_mock_credentials_check.get( response_url, text=load_fixture("here_weather/destination_weather_forecasts_astronomy.json"), @@ -87,7 +82,7 @@ def requests_mock_coordinates(requests_mock_credentials_check): def requests_mock_zip_code_imperial(requests_mock_credentials_check): """Add the url used in a request with zip_code to requests mock.""" product = herepy.WeatherProductType.observation - response_url = build_zip_code_imperial_mock_url(APP_ID, APP_CODE, ZIP_CODE, product) + response_url = build_zip_code_imperial_mock_url(API_KEY, ZIP_CODE, product) requests_mock_credentials_check.get( response_url, text=load_fixture("here_weather/destination_weather_observation_imperial.json"), @@ -99,9 +94,7 @@ def requests_mock_zip_code_imperial(requests_mock_credentials_check): def requests_mock_forecast_7days(requests_mock_credentials_check): """Add the url used in a request with forecast_7days to requests mock.""" product = herepy.WeatherProductType.forecast_7days - response_url = build_location_name_mock_url( - APP_ID, APP_CODE, LOCATION_NAME, product - ) + response_url = build_location_name_mock_url(API_KEY, LOCATION_NAME, product) requests_mock_credentials_check.get( response_url, text=load_fixture("here_weather/destination_weather_forecasts.json"), @@ -113,9 +106,7 @@ def requests_mock_forecast_7days(requests_mock_credentials_check): def requests_mock_forecast_hourly(requests_mock_credentials_check): """Add the url used in a request with forecast_hourly to requests mock.""" product = herepy.WeatherProductType.forecast_hourly - response_url = build_location_name_mock_url( - APP_ID, APP_CODE, LOCATION_NAME, product - ) + response_url = build_location_name_mock_url(API_KEY, LOCATION_NAME, product) requests_mock_credentials_check.get( response_url, text=load_fixture("here_weather/destination_weather_forecasts_hourly.json"), @@ -127,9 +118,7 @@ def requests_mock_forecast_hourly(requests_mock_credentials_check): def requests_mock_forecast_7days_simple(requests_mock_credentials_check): """Add the url used in a request with forecast_7days_simple to requests mock.""" product = herepy.WeatherProductType.forecast_7days_simple - response_url = build_location_name_mock_url( - APP_ID, APP_CODE, LOCATION_NAME, product - ) + response_url = build_location_name_mock_url(API_KEY, LOCATION_NAME, product) requests_mock_credentials_check.get( response_url, text=load_fixture("here_weather/destination_weather_forecasts_simple.json"), @@ -141,7 +130,7 @@ def requests_mock_forecast_7days_simple(requests_mock_credentials_check): def requests_mock_invalid_request(requests_mock_credentials_check): """Add the url used in an invalid request to requests mock.""" product = herepy.WeatherProductType.observation - response_url = build_zip_code_mock_url(APP_ID, APP_CODE, ZIP_CODE, product) + response_url = build_zip_code_mock_url(API_KEY, ZIP_CODE, product) requests_mock_credentials_check.get( response_url, text=load_fixture( @@ -160,8 +149,7 @@ async def test_invalid_credentials(hass, requests_mock_invalid_credentials, capl "name": "test", "zip_code": ZIP_CODE, "mode": "forecast_astronomy", - "app_id": APP_ID, - "app_code": APP_CODE, + "api_key": API_KEY, } } assert await async_setup_component(hass, DOMAIN, config) @@ -177,8 +165,7 @@ async def test_forecast_astronomy(hass, requests_mock_credentials_check): "name": "test", "zip_code": ZIP_CODE, "mode": "forecast_astronomy", - "app_id": APP_ID, - "app_code": APP_CODE, + "api_key": API_KEY, } } @@ -194,8 +181,7 @@ async def test_location_name(hass, requests_mock_location_name): "name": "test", "location_name": LOCATION_NAME, "mode": "forecast_astronomy", - "app_id": APP_ID, - "app_code": APP_CODE, + "api_key": API_KEY, } } @@ -212,8 +198,7 @@ async def test_coordinates(hass, requests_mock_coordinates): "latitude": LATITUDE, "longitude": LONGITUDE, "mode": "forecast_astronomy", - "app_id": APP_ID, - "app_code": APP_CODE, + "api_key": API_KEY, } } @@ -229,8 +214,7 @@ async def test_imperial(hass, requests_mock_zip_code_imperial): "name": "test", "zip_code": ZIP_CODE, "mode": "observation", - "app_id": APP_ID, - "app_code": APP_CODE, + "api_key": API_KEY, "unit_system": "imperial", } } @@ -250,8 +234,7 @@ async def test_forecast_7days(hass, requests_mock_forecast_7days): "name": "test", "location_name": LOCATION_NAME, "mode": "forecast_7days", - "app_id": APP_ID, - "app_code": APP_CODE, + "api_key": API_KEY, } } @@ -267,8 +250,7 @@ async def test_forecast_7days_simple(hass, requests_mock_forecast_7days_simple): "name": "test", "location_name": LOCATION_NAME, "mode": "forecast_7days_simple", - "app_id": APP_ID, - "app_code": APP_CODE, + "api_key": API_KEY, } } @@ -284,8 +266,7 @@ async def test_forecast_forecast_hourly(hass, requests_mock_forecast_hourly): "name": "test", "location_name": LOCATION_NAME, "mode": "forecast_hourly", - "app_id": APP_ID, - "app_code": APP_CODE, + "api_key": API_KEY, } } @@ -302,8 +283,7 @@ async def test_invalid_request(hass, requests_mock_invalid_request, caplog): "name": "test", "zip_code": ZIP_CODE, "mode": "observation", - "app_id": APP_ID, - "app_code": APP_CODE, + "api_key": API_KEY, } } assert await async_setup_component(hass, DOMAIN, config) diff --git a/tests/components/here_weather/test_weather.py b/tests/components/here_weather/test_weather.py index 415978836ebf22..386a6394e5d550 100644 --- a/tests/components/here_weather/test_weather.py +++ b/tests/components/here_weather/test_weather.py @@ -7,8 +7,7 @@ from homeassistant.setup import async_setup_component from . import ( - APP_CODE, - APP_ID, + API_KEY, LATITUDE, LOCATION_NAME, LONGITUDE, @@ -29,7 +28,7 @@ def requests_mock_credentials_check(requests_mock): """Add the url used in the api validation to all requests mock.""" product = herepy.WeatherProductType.forecast_astronomy - response_url = build_zip_code_mock_url(APP_ID, APP_CODE, ZIP_CODE, product) + response_url = build_zip_code_mock_url(API_KEY, ZIP_CODE, product) requests_mock.get( response_url, text=load_fixture("here_weather/destination_weather_forecasts_astronomy.json"), @@ -41,7 +40,7 @@ def requests_mock_credentials_check(requests_mock): def requests_mock_invalid_credentials(requests_mock): """Add the url for invalid credentials to requests mock.""" product = herepy.WeatherProductType.forecast_astronomy - response_url = build_zip_code_mock_url(APP_ID, APP_CODE, ZIP_CODE, product) + response_url = build_zip_code_mock_url(API_KEY, ZIP_CODE, product) requests_mock.get( response_url, text=load_fixture("here_weather/destination_weather_error_unauthorized.json"), @@ -53,9 +52,7 @@ def requests_mock_invalid_credentials(requests_mock): def requests_mock_location_name(requests_mock_credentials_check): """Add the url used in a request with location_name to requests mock.""" product = herepy.WeatherProductType.forecast_7days - response_url = build_location_name_mock_url( - APP_ID, APP_CODE, LOCATION_NAME, product - ) + response_url = build_location_name_mock_url(API_KEY, LOCATION_NAME, product) requests_mock_credentials_check.get( response_url, text=load_fixture("here_weather/destination_weather_forecasts.json"), @@ -67,9 +64,7 @@ def requests_mock_location_name(requests_mock_credentials_check): def requests_mock_coordinates(requests_mock_credentials_check): """Add the url used in a request with coordinates to requests mock.""" product = herepy.WeatherProductType.forecast_7days - response_url = build_coordinates_mock_url( - APP_ID, APP_CODE, LATITUDE, LONGITUDE, product - ) + response_url = build_coordinates_mock_url(API_KEY, LATITUDE, LONGITUDE, product) requests_mock_credentials_check.get( response_url, text=load_fixture("here_weather/destination_weather_forecasts.json"), @@ -81,7 +76,7 @@ def requests_mock_coordinates(requests_mock_credentials_check): def requests_mock_zip_code_imperial(requests_mock_credentials_check): """Add the url used in a request with zip_code to requests mock.""" product = herepy.WeatherProductType.observation - response_url = build_zip_code_imperial_mock_url(APP_ID, APP_CODE, ZIP_CODE, product) + response_url = build_zip_code_imperial_mock_url(API_KEY, ZIP_CODE, product) requests_mock_credentials_check.get( response_url, text=load_fixture("here_weather/destination_weather_observation_imperial.json"), @@ -93,9 +88,7 @@ def requests_mock_zip_code_imperial(requests_mock_credentials_check): def requests_mock_forecast_7days(requests_mock_credentials_check): """Add the url used in a request with forecast_7days to requests mock.""" product = herepy.WeatherProductType.forecast_7days - response_url = build_location_name_mock_url( - APP_ID, APP_CODE, LOCATION_NAME, product - ) + response_url = build_location_name_mock_url(API_KEY, LOCATION_NAME, product) requests_mock_credentials_check.get( response_url, text=load_fixture("here_weather/destination_weather_forecasts.json"), @@ -107,9 +100,7 @@ def requests_mock_forecast_7days(requests_mock_credentials_check): def requests_mock_forecast_hourly(requests_mock_credentials_check): """Add the url used in a request with forecast_hourly to requests mock.""" product = herepy.WeatherProductType.forecast_hourly - response_url = build_location_name_mock_url( - APP_ID, APP_CODE, LOCATION_NAME, product - ) + response_url = build_location_name_mock_url(API_KEY, LOCATION_NAME, product) requests_mock_credentials_check.get( response_url, text=load_fixture("here_weather/destination_weather_forecasts_hourly.json"), @@ -121,9 +112,7 @@ def requests_mock_forecast_hourly(requests_mock_credentials_check): def requests_mock_forecast_7days_simple(requests_mock_credentials_check): """Add the url used in a request with forecast_7days_simple to requests mock.""" product = herepy.WeatherProductType.forecast_7days_simple - response_url = build_location_name_mock_url( - APP_ID, APP_CODE, LOCATION_NAME, product - ) + response_url = build_location_name_mock_url(API_KEY, LOCATION_NAME, product) requests_mock_credentials_check.get( response_url, text=load_fixture("here_weather/destination_weather_forecasts_simple.json"), @@ -135,7 +124,7 @@ def requests_mock_forecast_7days_simple(requests_mock_credentials_check): def requests_mock_invalid_request(requests_mock_credentials_check): """Add the url used in an invalid request to requests mock.""" product = herepy.WeatherProductType.observation - response_url = build_zip_code_mock_url(APP_ID, APP_CODE, ZIP_CODE, product) + response_url = build_zip_code_mock_url(API_KEY, ZIP_CODE, product) requests_mock_credentials_check.get( response_url, text=load_fixture( @@ -154,8 +143,7 @@ async def test_invalid_credentials(hass, requests_mock_invalid_credentials, capl "name": "test", "zip_code": ZIP_CODE, "mode": "forecast_7days", - "app_id": APP_ID, - "app_code": APP_CODE, + "api_key": API_KEY, } } assert await async_setup_component(hass, DOMAIN, config) @@ -171,8 +159,7 @@ async def test_location_name(hass, requests_mock_location_name): "name": "test", "location_name": LOCATION_NAME, "mode": "forecast_7days", - "app_id": APP_ID, - "app_code": APP_CODE, + "api_key": API_KEY, } } @@ -191,8 +178,7 @@ async def test_coordinates(hass, requests_mock_coordinates): "latitude": LATITUDE, "longitude": LONGITUDE, "mode": "forecast_7days", - "app_id": APP_ID, - "app_code": APP_CODE, + "api_key": API_KEY, } } @@ -210,8 +196,7 @@ async def test_imperial(hass, requests_mock_zip_code_imperial): "name": "test", "zip_code": ZIP_CODE, "mode": "observation", - "app_id": APP_ID, - "app_code": APP_CODE, + "api_key": API_KEY, "unit_system": "imperial", } } @@ -233,8 +218,7 @@ async def test_forecast_7days(hass, requests_mock_forecast_7days): "name": "test", "location_name": LOCATION_NAME, "mode": "forecast_7days", - "app_id": APP_ID, - "app_code": APP_CODE, + "api_key": API_KEY, } } @@ -252,8 +236,7 @@ async def test_forecast_7days_simple(hass, requests_mock_forecast_7days_simple): "name": "test", "location_name": LOCATION_NAME, "mode": "forecast_7days_simple", - "app_id": APP_ID, - "app_code": APP_CODE, + "api_key": API_KEY, } } @@ -271,8 +254,7 @@ async def test_forecast_forecast_hourly(hass, requests_mock_forecast_hourly): "name": "test", "location_name": LOCATION_NAME, "mode": "forecast_hourly", - "app_id": APP_ID, - "app_code": APP_CODE, + "api_key": API_KEY, } } @@ -291,8 +273,7 @@ async def test_invalid_request(hass, requests_mock_invalid_request, caplog): "name": "test", "zip_code": ZIP_CODE, "mode": "observation", - "app_id": APP_ID, - "app_code": APP_CODE, + "api_key": API_KEY, } } assert await async_setup_component(hass, DOMAIN, config) From 493ac351ba2b12b26a4e75c96518c3abcffe55e1 Mon Sep 17 00:00:00 2001 From: Kevin Eifinger Date: Mon, 16 Dec 2019 09:45:26 +0100 Subject: [PATCH 07/56] script.gen_requirements_all --- requirements_all.txt | 1 + requirements_test_all.txt | 1 + 2 files changed, 2 insertions(+) diff --git a/requirements_all.txt b/requirements_all.txt index a43f754b20221a..5ade639cec421e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -766,6 +766,7 @@ hdate==0.10.2 heatmiserV3==1.1.18 # homeassistant.components.here_travel_time +# homeassistant.components.here_weather herepy==2.0.0 # homeassistant.components.hikvisioncam diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5732833e079453..a2e0d2d235d891 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -442,6 +442,7 @@ hatasmota==0.2.20 hdate==0.10.2 # homeassistant.components.here_travel_time +# homeassistant.components.here_weather herepy==2.0.0 # homeassistant.components.hlk_sw16 From 5eff618bdfbe9c4d026888aa2d69658292543759 Mon Sep 17 00:00:00 2001 From: Kevin Eifinger Date: Mon, 16 Dec 2019 14:26:57 +0100 Subject: [PATCH 08/56] fix mock url --- tests/components/here_weather/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/here_weather/__init__.py b/tests/components/here_weather/__init__.py index f89215818044ce..afad5eeeaf72c6 100644 --- a/tests/components/here_weather/__init__.py +++ b/tests/components/here_weather/__init__.py @@ -15,7 +15,7 @@ def build_base_mock_url(api_key, additional_params): """Construct a url for HERE.""" - base_url = "https://weather.api.here.com/weather/1.0/report.json?" + base_url = "https://weather.ls.hereapi.com/weather/1.0/report.json?" parameters = { "apikey": api_key, "oneobservation": True, From 5e323916dcb58c7e0a65c276e2dd596bec23470d Mon Sep 17 00:00:00 2001 From: Kevin Eifinger Date: Sun, 12 Jul 2020 22:31:30 +0200 Subject: [PATCH 09/56] Implement Config Flow --- .../components/here_weather/__init__.py | 225 +- .../components/here_weather/config_flow.py | 240 + .../components/here_weather/const.py | 27 +- .../components/here_weather/manifest.json | 5 +- .../components/here_weather/sensor.py | 165 +- .../components/here_weather/strings.json | 65 + .../components/here_weather/utils.py | 40 + .../components/here_weather/weather.py | 207 +- homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 4 +- requirements_test_all.txt | 4 +- tests/components/here_weather/__init__.py | 53 - tests/components/here_weather/const.py | 89 + .../here_weather/test_config_flow.py | 217 + tests/components/here_weather/test_sensor.py | 384 +- tests/components/here_weather/test_weather.py | 336 +- ...ination_weather_error_invalid_request.json | 6 - ...estination_weather_error_unauthorized.json | 6 - .../destination_weather_forecasts.json | 761 --- ...stination_weather_forecasts_astronomy.json | 118 - .../destination_weather_forecasts_hourly.json | 5057 ----------------- .../destination_weather_forecasts_simple.json | 248 - ...tination_weather_observation_imperial.json | 61 - 23 files changed, 1131 insertions(+), 7188 deletions(-) create mode 100644 homeassistant/components/here_weather/config_flow.py create mode 100644 homeassistant/components/here_weather/strings.json create mode 100644 homeassistant/components/here_weather/utils.py create mode 100644 tests/components/here_weather/const.py create mode 100644 tests/components/here_weather/test_config_flow.py delete mode 100644 tests/fixtures/here_weather/destination_weather_error_invalid_request.json delete mode 100644 tests/fixtures/here_weather/destination_weather_error_unauthorized.json delete mode 100644 tests/fixtures/here_weather/destination_weather_forecasts.json delete mode 100644 tests/fixtures/here_weather/destination_weather_forecasts_astronomy.json delete mode 100644 tests/fixtures/here_weather/destination_weather_forecasts_hourly.json delete mode 100644 tests/fixtures/here_weather/destination_weather_forecasts_simple.json delete mode 100644 tests/fixtures/here_weather/destination_weather_observation_imperial.json diff --git a/homeassistant/components/here_weather/__init__.py b/homeassistant/components/here_weather/__init__.py index 1c5d7eaf52ada5..3a96926b1da991 100644 --- a/homeassistant/components/here_weather/__init__.py +++ b/homeassistant/components/here_weather/__init__.py @@ -1,86 +1,169 @@ """The here_weather component.""" +import asyncio from datetime import timedelta import logging +import async_timeout import herepy -from homeassistant.const import CONF_UNIT_SYSTEM_METRIC -from homeassistant.util import Throttle +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_MODE, + CONF_SCAN_INTERVAL, + CONF_UNIT_SYSTEM, + CONF_UNIT_SYSTEM_METRIC, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import ( + CONF_API_KEY, + CONF_LOCATION_NAME, + CONF_ZIP_CODE, + DEFAULT_SCAN_INTERVAL, + DOMAIN, + HERE_API_KEYS, +) _LOGGER = logging.getLogger(__name__) -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=120) +PLATFORMS = ["sensor", "weather"] + + +async def async_setup(hass: HomeAssistant, config: dict) -> bool: + """Set up the here_weather component.""" + return True + + +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Set up here_weather from a config entry.""" + here_weather_data = HEREWeatherData(hass, config_entry) + if not await here_weather_data.async_setup(): + return False + hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = here_weather_data + + known_api_keys = hass.data.setdefault(HERE_API_KEYS, []) + if config_entry.data[CONF_API_KEY] not in known_api_keys: + known_api_keys.append(config_entry.data[CONF_API_KEY]) + + for platform in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(config_entry, platform) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(config_entry, component) + for component in PLATFORMS + ] + ) + ) + if unload_ok: + hass.data[DOMAIN][config_entry.entry_id].unsub_handler() + hass.data[DOMAIN].pop(config_entry.entry_id) + + return unload_ok class HEREWeatherData: """Get the latest data from HERE.""" - def __init__( - self, - here_client: herepy.DestinationWeatherApi, - mode: str, - units: str, - latitude: str = None, - longitude: str = None, - location_name: str = None, - zip_code: str = None, - ) -> None: + def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None: """Initialize the data object.""" - self.here_client = here_client - self.latitude = latitude - self.longitude = longitude - self.location_name = location_name - self.zip_code = zip_code - self.weather_product_type = herepy.WeatherProductType[mode] - self.units = units - self.data = None - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self) -> None: - """Get the latest data from HERE.""" - is_metric = convert_units_to_boolean(self.units) - try: - if self.zip_code is not None: - data = self.here_client.weather_for_zip_code( - self.zip_code, self.weather_product_type, metric=is_metric - ) - elif self.location_name is not None: - data = self.here_client.weather_for_location_name( - self.location_name, self.weather_product_type, metric=is_metric - ) - else: - data = self.here_client.weather_for_coordinates( - self.latitude, - self.longitude, - self.weather_product_type, - metric=is_metric, - ) - self.data = extract_data_from_payload_for_product_type( - data, self.weather_product_type + self.hass = hass + self.config_entry = config_entry + self.here_client = herepy.DestinationWeatherApi(config_entry.data[CONF_API_KEY]) + self.latitude = config_entry.data.get(CONF_LATITUDE) + self.longitude = config_entry.data.get(CONF_LONGITUDE) + self.location_name = config_entry.data.get(CONF_LOCATION_NAME) + self.zip_code = config_entry.data.get(CONF_ZIP_CODE) + self.weather_product_type = herepy.WeatherProductType[ + config_entry.data[CONF_MODE] + ] + self.units = config_entry.data.get(CONF_UNIT_SYSTEM) + self.coordinator = None + self.unsub_handler = None + + async def async_setup(self): + """Set up the here_weather integration.""" + self.add_options() + self.unsub_handler = self.config_entry.add_update_listener( + self.async_options_updated + ) + await self._async_create_coordinator() + return True + + def add_options(self): + """Add options for here_weather integration.""" + if not self.config_entry.options: + options = {CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL} + self.hass.config_entries.async_update_entry( + self.config_entry, options=options ) - except herepy.InvalidRequestError as error: - _LOGGER.error("Error during sensor update: %s", error.message) + async def _async_create_coordinator(self): + """Create or recreate the DataUpdateCoordinator.""" + self.coordinator = DataUpdateCoordinator( + self.hass, + _LOGGER, + name=DOMAIN, + update_method=self.async_update, + update_interval=timedelta( + seconds=self.config_entry.options[CONF_SCAN_INTERVAL] + ), + ) + await self.coordinator.async_refresh() + + async def async_update(self) -> None: + """Handle data update with the DataUpdateCoordinator.""" + try: + async with async_timeout.timeout(10): + return await self.hass.async_add_executor_job(self._get_data) + except herepy.InvalidRequestError as error: + raise UpdateFailed(f"Unable to fetch data from HERE: {error.message}") -def get_attribute_from_here_data( - here_data: list, attribute_name: str, sensor_number: int = 0 -) -> str: - """Extract and convert data from HERE response or None if not found.""" - if here_data is None: - return None - try: - state = here_data[sensor_number][attribute_name] - state = convert_asterisk_to_none(state) - return state - except KeyError: - return None + def _get_data(self): + """Get the latest data from HERE.""" + is_metric = convert_units_to_boolean(self.units) + if self.zip_code is not None: + data = self.here_client.weather_for_zip_code( + self.zip_code, self.weather_product_type, metric=is_metric + ) + elif self.location_name is not None: + data = self.here_client.weather_for_location_name( + self.location_name, self.weather_product_type, metric=is_metric + ) + else: + data = self.here_client.weather_for_coordinates( + self.latitude, + self.longitude, + self.weather_product_type, + metric=is_metric, + ) + return extract_data_from_payload_for_product_type( + data, self.weather_product_type + ) + @staticmethod + async def async_options_updated( + hass: HomeAssistant, config_entry: ConfigEntry + ) -> None: + """Triggered by config entry options updates.""" + await hass.data[DOMAIN][config_entry.entry_id].async_set_scan_interval( + config_entry.options[CONF_SCAN_INTERVAL] + ) -def convert_asterisk_to_none(state: str) -> str: - """Convert HERE API representation of None.""" - if state == "*": - state = None - return state + async def async_set_scan_interval(self, scan_interval): + """Recreate the coordinator with the new scan interval.""" + await self._async_create_coordinator() def convert_units_to_boolean(units: str) -> bool: @@ -102,19 +185,3 @@ def extract_data_from_payload_for_product_type( return data.dailyForecasts["forecastLocation"]["forecast"] if product_type == herepy.WeatherProductType.forecast_hourly: return data.hourlyForecasts["forecastLocation"]["forecast"] - - -def convert_unit_of_measurement_if_needed(unit_system, unit_of_measurement: str) -> str: - """Convert the unit of measurement to imperial if configured.""" - if unit_system != CONF_UNIT_SYSTEM_METRIC: - if unit_of_measurement == "°C": - unit_of_measurement = "°F" - elif unit_of_measurement == "cm": - unit_of_measurement = "in" - elif unit_of_measurement == "km/h": - unit_of_measurement = "mph" - elif unit_of_measurement == "mbar": - unit_of_measurement = "in" - elif unit_of_measurement == "km": - unit_of_measurement = "mi" - return unit_of_measurement diff --git a/homeassistant/components/here_weather/config_flow.py b/homeassistant/components/here_weather/config_flow.py new file mode 100644 index 00000000000000..e76c77d4dde32c --- /dev/null +++ b/homeassistant/components/here_weather/config_flow.py @@ -0,0 +1,240 @@ +"""Config flow for here_weather integration.""" +import logging + +import herepy +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_MODE, + CONF_NAME, + CONF_SCAN_INTERVAL, + CONF_UNIT_SYSTEM, + CONF_UNIT_SYSTEM_IMPERIAL, + CONF_UNIT_SYSTEM_METRIC, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError +import homeassistant.helpers.config_validation as cv + +from .const import ( + CONF_API_KEY, + CONF_LOCATION_NAME, + CONF_MODES, + CONF_OPTION, + CONF_OPTION_COORDINATES, + CONF_OPTION_LOCATION_NAME, + CONF_OPTION_ZIP_CODE, + CONF_OPTIONS, + CONF_ZIP_CODE, + DEFAULT_MODE, + DEFAULT_SCAN_INTERVAL, + DOMAIN, + HERE_API_KEYS, +) + +_LOGGER = logging.getLogger(__name__) + + +def get_base_schema(hass: HomeAssistant) -> vol.Schema: + """Get the here_weather base schema.""" + known_api_key = None + if HERE_API_KEYS in hass.data: + known_api_key = hass.data[HERE_API_KEYS][0] + return vol.Schema( + { + vol.Required(CONF_API_KEY, default=known_api_key): str, + vol.Optional(CONF_NAME, default=DOMAIN): str, + vol.Optional(CONF_MODE, default=DEFAULT_MODE): vol.In(CONF_MODES), + vol.Optional(CONF_UNIT_SYSTEM, default=hass.config.units.name): vol.In( + [CONF_UNIT_SYSTEM_METRIC, CONF_UNIT_SYSTEM_IMPERIAL] + ), + } + ) + + +def get_coordinate_schema(hass: HomeAssistant) -> vol.Schema: + """Get the here_weather coordinate schema.""" + schema = get_base_schema(hass) + return schema.extend( + { + vol.Optional(CONF_LATITUDE, default=hass.config.latitude): cv.latitude, + vol.Optional(CONF_LONGITUDE, default=hass.config.longitude): cv.longitude, + } + ) + + +def get_zip_code_schema(hass: HomeAssistant) -> vol.Schema: + """Get the here_weather zip_code schema.""" + schema = get_base_schema(hass) + return schema.extend({vol.Required(CONF_ZIP_CODE): str}) + + +def get_location_name_schema(hass: HomeAssistant) -> vol.Schema: + """Get the here_weather location_name schema.""" + schema = get_base_schema(hass) + return schema.extend({vol.Required(CONF_LOCATION_NAME): str}) + + +async def async_validate_coordinate_input( + hass: HomeAssistant, user_input: dict +) -> None: + """Validate the user_input containing coordinates.""" + await async_validate_name(hass, user_input) + here_client = herepy.DestinationWeatherApi(user_input[CONF_API_KEY]) + await hass.async_add_executor_job( + here_client.weather_for_coordinates, + user_input[CONF_LATITUDE], + user_input[CONF_LONGITUDE], + herepy.WeatherProductType[user_input[CONF_MODE]], + ) + + +async def async_validate_zip_code_input(hass: HomeAssistant, user_input: dict) -> None: + """Validate the user_input containing a zip_code.""" + await async_validate_name(hass, user_input) + here_client = herepy.DestinationWeatherApi(user_input[CONF_API_KEY]) + await hass.async_add_executor_job( + here_client.weather_for_zip_code, + user_input[CONF_ZIP_CODE], + herepy.WeatherProductType[user_input[CONF_MODE]], + ) + + +async def async_validate_location_name_input( + hass: HomeAssistant, user_input: dict +) -> None: + """Validate the user_input containing a location_name.""" + await async_validate_name(hass, user_input) + here_client = herepy.DestinationWeatherApi(user_input[CONF_API_KEY]) + await hass.async_add_executor_job( + here_client.weather_for_location_name, + user_input[CONF_LOCATION_NAME], + herepy.WeatherProductType[user_input[CONF_MODE]], + ) + + +async def async_validate_name(hass: HomeAssistant, user_input: dict) -> None: + """Validate the user input.""" + for entry in hass.config_entries.async_entries(DOMAIN): + if entry.data[CONF_NAME] == user_input[CONF_NAME]: + raise AlreadyConfigured + + +class HereWeatherConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for here_weather.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + @staticmethod + @callback + def async_get_options_flow(config_entry: ConfigEntry): + """Get the options flow for this handler.""" + return HereWeatherOptionsFlowHandler(config_entry) + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + if user_input is not None: + if user_input[CONF_OPTION] == CONF_OPTION_COORDINATES: + return await self.async_step_coordinates() + if user_input[CONF_OPTION] == CONF_OPTION_ZIP_CODE: + return await self.async_step_zip_code() + if user_input[CONF_OPTION] == CONF_OPTION_LOCATION_NAME: + return await self.async_step_location_name() + return self.async_show_form( + step_id="user", + data_schema=vol.Schema({vol.Required(CONF_OPTION): vol.In(CONF_OPTIONS)}), + errors=errors, + ) + + async def async_step_coordinates(self, user_input=None): + """Handle set up by coordinates.""" + errors = {} + if user_input is not None: + try: + await async_validate_coordinate_input(self.hass, user_input) + return self.async_create_entry( + title=user_input[CONF_NAME], data=user_input + ) + except AlreadyConfigured: + return self.async_abort(reason="already_configured") + except herepy.InvalidRequestError: + errors["base"] = "invalid_request" + except herepy.UnauthorizedError: + errors["base"] = "unauthorized" + return self.async_show_form( + step_id="coordinates", + data_schema=get_coordinate_schema(self.hass), + errors=errors, + ) + + async def async_step_zip_code(self, user_input=None): + """Handle set up by zip_code.""" + errors = {} + if user_input is not None: + try: + await async_validate_zip_code_input(self.hass, user_input) + return self.async_create_entry( + title=user_input[CONF_NAME], data=user_input + ) + except AlreadyConfigured: + return self.async_abort(reason="already_configured") + except herepy.HEREError as error: + errors["base"] = error.message + return self.async_show_form( + step_id="zip_code", + data_schema=get_zip_code_schema(self.hass), + errors=errors, + ) + + async def async_step_location_name(self, user_input=None): + """Handle set up by location_name.""" + errors = {} + if user_input is not None: + try: + await async_validate_location_name_input(self.hass, user_input) + return self.async_create_entry( + title=user_input[CONF_NAME], data=user_input + ) + except AlreadyConfigured: + return self.async_abort(reason="already_configured") + except herepy.HEREError as error: + errors["base"] = error.message + return self.async_show_form( + step_id="location_name", + data_schema=get_location_name_schema(self.hass), + errors=errors, + ) + + +class HereWeatherOptionsFlowHandler(config_entries.OptionsFlow): + """Handle here_weather options.""" + + def __init__(self, config_entry): + """Initialize here_weather options flow.""" + self.config_entry = config_entry + + async def async_step_init(self, user_input=None): + """Manage the here_weather options.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + options = { + vol.Optional( + CONF_SCAN_INTERVAL, + default=self.config_entry.options.get( + CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL + ), + ): int + } + + return self.async_show_form(step_id="init", data_schema=vol.Schema(options)) + + +class AlreadyConfigured(HomeAssistantError): + """Error to indicate the asset pair is already configured.""" diff --git a/homeassistant/components/here_weather/const.py b/homeassistant/components/here_weather/const.py index b824d58143e4ab..b8d85e11b096d3 100644 --- a/homeassistant/components/here_weather/const.py +++ b/homeassistant/components/here_weather/const.py @@ -1,11 +1,24 @@ """Constants for the HERE Destination Weather service.""" +DOMAIN = "here_weather" + +HERE_API_KEYS = "here_api_keys" + +DEFAULT_SCAN_INTERVAL = 120 + CONF_API_KEY = "api_key" CONF_LOCATION_NAME = "location_name" CONF_ZIP_CODE = "zip_code" CONF_LANGUAGE = "language" CONF_OFFSET = "offset" - -DEFAULT_NAME = "here_weather" +CONF_OPTION = "option" +CONF_OPTION_COORDINATES = "Using GPS Coordinates" +CONF_OPTION_ZIP_CODE = "Using a US ZIP Code" +CONF_OPTION_LOCATION_NAME = "Using a Location Name or Address" +CONF_OPTIONS = [ + CONF_OPTION_COORDINATES, + CONF_OPTION_ZIP_CODE, + CONF_OPTION_LOCATION_NAME, +] MODE_ASTRONOMY = "forecast_astronomy" MODE_HOURLY = "forecast_hourly" @@ -31,7 +44,7 @@ "city": {"name": "City", "unit_of_measurement": None}, "latitude": {"name": "Latitude", "unit_of_measurement": None}, "longitude": {"name": "Longitude", "unit_of_measurement": None}, - "utcTime": {"name": "Sunrise", "unit_of_measurement": "timestamp"}, + "utcTime": {"name": "UTC Time", "unit_of_measurement": None}, } HOURLY_ATTRIBUTES = { @@ -66,7 +79,7 @@ "iconLink": {"name": "Icon Link", "unit_of_measurement": None}, "dayOfWeek": {"name": "Day of Week", "unit_of_measurement": None}, "weekday": {"name": "Week Day", "unit_of_measurement": None}, - "utcTime": {"name": "UTC Time", "unit_of_measurement": "timestamp"}, + "utcTime": {"name": "UTC Time", "unit_of_measurement": None}, "localTime": {"name": "Local Time", "unit_of_measurement": None}, "localTimeFormat": {"name": "Local Time Format", "unit_of_measurement": None}, } @@ -111,7 +124,7 @@ "iconLink": {"name": "Icon Link", "unit_of_measurement": None}, "dayOfWeek": {"name": "Day of Week", "unit_of_measurement": None}, "weekday": {"name": "Week Day", "unit_of_measurement": None}, - "utcTime": {"name": "UTC Time", "unit_of_measurement": "timestamp"}, + "utcTime": {"name": "UTC Time", "unit_of_measurement": None}, } DAILY_ATTRIBUTES = { @@ -152,7 +165,7 @@ "iconLink": {"name": "Icon Link", "unit_of_measurement": None}, "dayOfWeek": {"name": "Day of Week", "unit_of_measurement": None}, "weekday": {"name": "Week Day", "unit_of_measurement": None}, - "utcTime": {"name": "UTC Time", "unit_of_measurement": "timestamp"}, + "utcTime": {"name": "UTC Time", "unit_of_measurement": None}, } OBSERVATION_ATTRIBUTES = { @@ -216,7 +229,7 @@ "longitude": {"name": "Longitude", "unit_of_measurement": None}, "distance": {"name": "Distance", "unit_of_measurement": "km"}, "elevation": {"name": "Elevation", "unit_of_measurement": "km"}, - "utcTime": {"name": "UTC Time", "unit_of_measurement": "timestamp"}, + "utcTime": {"name": "UTC Time", "unit_of_measurement": None}, } SENSOR_TYPES = { diff --git a/homeassistant/components/here_weather/manifest.json b/homeassistant/components/here_weather/manifest.json index d8d584a1be474a..f8738488078f71 100644 --- a/homeassistant/components/here_weather/manifest.json +++ b/homeassistant/components/here_weather/manifest.json @@ -1,9 +1,10 @@ { "domain": "here_weather", "name": "HERE Destination Weather", - "documentation": "https://www.home-assistant.io/components/here_weather", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/here_weather", "requirements": [ - "herepy==2.0.0" + "herepy==3.0.1" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/here_weather/sensor.py b/homeassistant/components/here_weather/sensor.py index 68c4dd64656876..40b7a212d023de 100644 --- a/homeassistant/components/here_weather/sensor.py +++ b/homeassistant/components/here_weather/sensor.py @@ -1,129 +1,50 @@ """Support for the HERE Destination Weather service.""" import logging -from typing import Callable, Dict, Optional, Union - -import herepy -import voluptuous as vol - -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ( - CONF_LATITUDE, - CONF_LONGITUDE, - CONF_MODE, - CONF_NAME, - CONF_UNIT_SYSTEM, - CONF_UNIT_SYSTEM_IMPERIAL, - CONF_UNIT_SYSTEM_METRIC, -) +from typing import Dict, Optional, Union + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_MODE, CONF_NAME from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -from . import ( - HEREWeatherData, - convert_unit_of_measurement_if_needed, - get_attribute_from_here_data, -) -from .const import ( - CONF_API_KEY, - CONF_LOCATION_NAME, - CONF_MODES, - CONF_OFFSET, - CONF_ZIP_CODE, - DEFAULT_MODE, - DEFAULT_NAME, - SENSOR_TYPES, -) - -UNITS = [CONF_UNIT_SYSTEM_METRIC, CONF_UNIT_SYSTEM_IMPERIAL] +from . import HEREWeatherData +from .const import DOMAIN, SENSOR_TYPES +from .utils import convert_unit_of_measurement_if_needed, get_attribute_from_here_data _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_API_KEY): cv.string, - vol.Inclusive(CONF_LATITUDE, "coordinates"): cv.latitude, - vol.Inclusive(CONF_LONGITUDE, "coordinates"): cv.longitude, - vol.Exclusive(CONF_LATITUDE, "coords_or_name_or_zip_code"): cv.latitude, - vol.Exclusive(CONF_LOCATION_NAME, "coords_or_name_or_zip_code"): cv.string, - vol.Exclusive(CONF_ZIP_CODE, "coords_or_name_or_zip_code"): cv.string, - vol.Optional(CONF_OFFSET, default=0): cv.positive_int, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_MODE, default=DEFAULT_MODE): vol.In(CONF_MODES), - vol.Optional(CONF_LATITUDE): cv.latitude, - vol.Optional(CONF_LONGITUDE): cv.longitude, - vol.Optional(CONF_LOCATION_NAME): cv.string, - vol.Optional(CONF_ZIP_CODE): cv.string, - vol.Optional(CONF_UNIT_SYSTEM): vol.In(UNITS), - } -) - - -async def async_setup_platform( - hass: HomeAssistant, - config: Dict[str, Union[str, bool]], - async_add_entities: Callable, - discovery_info: None = None, -) -> None: - """Set up the HERE Destination Weather sensor.""" - - api_key = config[CONF_API_KEY] - - here_client = herepy.DestinationWeatherApi(api_key) - - if not await hass.async_add_executor_job( - _are_valid_client_credentials, here_client - ): - _LOGGER.error( - "Invalid credentials. This error is returned if the specified token was invalid or no contract could be found for this token." - ) - return - - name = config.get(CONF_NAME) - mode = config[CONF_MODE] - offset = config[CONF_OFFSET] - latitude = config.get(CONF_LATITUDE, hass.config.latitude) - longitude = config.get(CONF_LONGITUDE, hass.config.longitude) - location_name = config.get(CONF_LOCATION_NAME) - zip_code = config.get(CONF_ZIP_CODE) - units = config.get(CONF_UNIT_SYSTEM, hass.config.units.name) - - here_data = HEREWeatherData( - here_client, mode, units, latitude, longitude, location_name, zip_code - ) + +async def async_setup_entry( + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities +): + """Add here_weather entities from a config_entry.""" + here_weather_data = hass.data[DOMAIN][config_entry.entry_id] + sensors_to_add = [] for sensor_type in SENSOR_TYPES: - if sensor_type == mode: + if sensor_type == config_entry.data[CONF_MODE]: for weather_attribute in SENSOR_TYPES[sensor_type]: sensors_to_add.append( HEREDestinationWeatherSensor( - name, here_data, sensor_type, offset, weather_attribute + config_entry.data[CONF_NAME], + here_weather_data, + sensor_type, + weather_attribute, ) ) async_add_entities(sensors_to_add, True) -def _are_valid_client_credentials(here_client: herepy.DestinationWeatherApi) -> bool: - """Check if the provided credentials are correct using defaults.""" - try: - product = herepy.WeatherProductType.forecast_astronomy - known_good_zip_code = "10025" - here_client.weather_for_zip_code(known_good_zip_code, product) - except herepy.UnauthorizedError: - return False - return True - - class HEREDestinationWeatherSensor(Entity): """Implementation of an HERE Destination Weather sensor.""" def __init__( self, name: str, - here_data: "HEREWeatherData", + here_data: HEREWeatherData, sensor_type: str, - sensor_number: int, weather_attribute: str, + sensor_number: int = 0, # Additional supported offsets will be added in a separate PR ) -> None: """Initialize the sensor.""" self._base_name = name @@ -142,11 +63,18 @@ def name(self) -> str: """Return the name of the sensor.""" return f"{self._base_name} {self._name_suffix}" + @property + def unique_id(self): + """Set unique_id for sensor.""" + return self.name + @property def state(self) -> str: """Return the state of the device.""" return get_attribute_from_here_data( - self._here_data.data, self._weather_attribute, self._sensor_number + self._here_data.coordinator.data, + self._weather_attribute, + self._sensor_number, ) @property @@ -161,6 +89,37 @@ def device_state_attributes( """Return the state attributes.""" return None - async def async_update(self) -> None: + @property + def available(self): + """Could the api be accessed during the last update call.""" + return self._here_data.coordinator.last_update_success + + @property + def should_poll(self): + """Return the polling requirement for this sensor.""" + return False + + @property + def entity_registry_enabled_default(self): + """Return if the entity should be enabled when first added to the entity registry.""" + return True + + @property + def device_info(self) -> dict: + """Return a device description for device registry.""" + + return { + "identifiers": {(DOMAIN, self._base_name)}, + "name": self._base_name, + "manufacturer": "here.com", + } + + async def async_added_to_hass(self): + """When entity is added to hass.""" + self.async_on_remove( + self._here_data.coordinator.async_add_listener(self.async_write_ha_state) + ) + + async def async_update(self): """Get the latest data from HERE.""" - await self.hass.async_add_executor_job(self._here_data.update) + await self._here_data.coordinator.async_request_refresh() diff --git a/homeassistant/components/here_weather/strings.json b/homeassistant/components/here_weather/strings.json new file mode 100644 index 00000000000000..0f9747f202b268 --- /dev/null +++ b/homeassistant/components/here_weather/strings.json @@ -0,0 +1,65 @@ +{ + "title": "HERE Destination Weather", + "config": { + "step": { + "user": { + "title": "HERE Destination Weather", + "description": "Which configuration option do you want to use?", + "data": { + "option": "Configuration Option." + } + }, + "coordinates": { + "title": "HERE Destination Weather By Coordinates", + "description": "Provide the details for the coordinates.", + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]", + "name": "Name of this Integration Entry", + "mode": "The Weather Mode to use", + "unit_system": "The Unit System to use", + "latitude": "Latitude", + "longitude": "Longitude" + } + }, + "zip_code": { + "title": "HERE Destination Weather by US ZIP Code", + "description": "Provide the details for the US ZIP Code.", + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]", + "name": "Name of this Integration Entry", + "mode": "The Weather Mode to use", + "unit_system": "The Unit System to use", + "zip_code": "US ZIP Code" + } + }, + "location_name": { + "title": "HERE Destination Weather by Location Name or Address", + "description": "Provide the details for the Location Name or Address.", + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]", + "name": "Name of this Integration Entry", + "mode": "The Weather Mode to use", + "unit_system": "The Unit System to use", + "location_name": "Location Name or Address" + } + } + }, + "error": { + "invalid_request": "HERE reported an invalid request. This indicates the supplied location is not valid.", + "unauthorized": "Invalid credentials. This error is returned if the specified token was invalid or no contract could be found for this token." + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + }, + "options": { + "step": { + "init": { + "description": "Configure options for HERE Destination Weather", + "data": { + "scan_interval": "Update interval." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/here_weather/utils.py b/homeassistant/components/here_weather/utils.py new file mode 100644 index 00000000000000..8305ab48b19d04 --- /dev/null +++ b/homeassistant/components/here_weather/utils.py @@ -0,0 +1,40 @@ +"""Utility functions for here_weather.""" + +from homeassistant.const import CONF_UNIT_SYSTEM_METRIC + + +def convert_unit_of_measurement_if_needed(unit_system, unit_of_measurement: str) -> str: + """Convert the unit of measurement to imperial if configured.""" + if unit_system != CONF_UNIT_SYSTEM_METRIC: + if unit_of_measurement == "°C": + unit_of_measurement = "°F" + elif unit_of_measurement == "cm": + unit_of_measurement = "in" + elif unit_of_measurement == "km/h": + unit_of_measurement = "mph" + elif unit_of_measurement == "mbar": + unit_of_measurement = "in" + elif unit_of_measurement == "km": + unit_of_measurement = "mi" + return unit_of_measurement + + +def get_attribute_from_here_data( + here_data: list, attribute_name: str, sensor_number: int = 0 +) -> str: + """Extract and convert data from HERE response or None if not found.""" + if here_data is None: + return None + try: + state = here_data[sensor_number][attribute_name] + state = convert_asterisk_to_none(state) + return state + except KeyError: + return None + + +def convert_asterisk_to_none(state: str) -> str: + """Convert HERE API representation of None.""" + if state == "*": + state = None + return state diff --git a/homeassistant/components/here_weather/weather.py b/homeassistant/components/here_weather/weather.py index 9b0db1c9da6076..fd8d6c9716940b 100644 --- a/homeassistant/components/here_weather/weather.py +++ b/homeassistant/components/here_weather/weather.py @@ -1,9 +1,5 @@ """Support for the HERE Destination Weather API.""" import logging -from typing import Callable, Dict, Union - -import herepy -import voluptuous as vol from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, @@ -13,116 +9,42 @@ ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, ATTR_FORECAST_WIND_SPEED, - PLATFORM_SCHEMA, WeatherEntity, ) -from homeassistant.const import ( - CONF_LATITUDE, - CONF_LONGITUDE, - CONF_MODE, - CONF_NAME, - CONF_UNIT_SYSTEM, - CONF_UNIT_SYSTEM_IMPERIAL, - CONF_UNIT_SYSTEM_METRIC, - TEMP_CELSIUS, -) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_MODE, CONF_NAME, TEMP_CELSIUS from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv - -from . import ( - HEREWeatherData, - convert_unit_of_measurement_if_needed, - get_attribute_from_here_data, -) -from .const import ( - CONDITION_CLASSES, - CONF_API_KEY, - CONF_LOCATION_NAME, - CONF_ZIP_CODE, - DEFAULT_MODE, - MODE_DAILY, - MODE_DAILY_SIMPLE, - MODE_HOURLY, - MODE_OBSERVATION, -) - -CONF_MODES = [MODE_HOURLY, MODE_DAILY, MODE_DAILY_SIMPLE, MODE_OBSERVATION] -UNITS = [CONF_UNIT_SYSTEM_METRIC, CONF_UNIT_SYSTEM_IMPERIAL] +from . import HEREWeatherData +from .const import CONDITION_CLASSES, DOMAIN, MODE_ASTRONOMY, MODE_DAILY_SIMPLE +from .utils import convert_unit_of_measurement_if_needed, get_attribute_from_here_data _LOGGER = logging.getLogger(__name__) -DEFAULT_NAME = "HERE" - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_API_KEY): cv.string, - vol.Inclusive(CONF_LATITUDE, "coordinates"): cv.latitude, - vol.Inclusive(CONF_LONGITUDE, "coordinates"): cv.longitude, - vol.Exclusive(CONF_LATITUDE, "coords_or_name_or_zip_code"): cv.latitude, - vol.Exclusive(CONF_LOCATION_NAME, "coords_or_name_or_zip_code"): cv.string, - vol.Exclusive(CONF_ZIP_CODE, "coords_or_name_or_zip_code"): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_MODE, default=DEFAULT_MODE): vol.In(CONF_MODES), - vol.Optional(CONF_LATITUDE): cv.latitude, - vol.Optional(CONF_LONGITUDE): cv.longitude, - vol.Optional(CONF_LOCATION_NAME): cv.string, - vol.Optional(CONF_ZIP_CODE): cv.string, - vol.Optional(CONF_UNIT_SYSTEM): vol.In(UNITS), - } -) - -async def async_setup_platform( - hass: HomeAssistant, - config: Dict[str, Union[str, bool]], - async_add_entities: Callable, - discovery_info: None = None, -) -> None: - """Set up the HERE Destination weather platform.""" - - api_key = config[CONF_API_KEY] - - here_client = herepy.DestinationWeatherApi(api_key) - - if not await hass.async_add_executor_job( - _are_valid_client_credentials, here_client - ): - _LOGGER.error( - "Invalid credentials. This error is returned if the specified token was invalid or no contract could be found for this token." +async def async_setup_entry( + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities +): + """Add here_weather entities from a config_entry.""" + if config_entry.data[CONF_MODE] != MODE_ASTRONOMY: + here_weather_data = hass.data[DOMAIN][config_entry.entry_id] + + async_add_entities( + [ + HEREDestinationWeather( + config_entry.data[CONF_NAME], + here_weather_data, + config_entry.data[CONF_MODE], + ) + ], + True, ) - return - - name = config.get(CONF_NAME) - mode = config[CONF_MODE] - latitude = config.get(CONF_LATITUDE, hass.config.latitude) - longitude = config.get(CONF_LONGITUDE, hass.config.longitude) - location_name = config.get(CONF_LOCATION_NAME) - zip_code = config.get(CONF_ZIP_CODE) - units = config.get(CONF_UNIT_SYSTEM, hass.config.units.name) - - here_data = HEREWeatherData( - here_client, mode, units, latitude, longitude, location_name, zip_code - ) - - async_add_entities([HEREDestinationWeather(name, here_data, mode)], True) - - -def _are_valid_client_credentials(here_client: herepy.DestinationWeatherApi) -> bool: - """Check if the provided credentials are correct using defaults.""" - try: - product = herepy.WeatherProductType.forecast_astronomy - known_good_zip_code = "10025" - here_client.weather_for_zip_code(known_good_zip_code, product) - except herepy.UnauthorizedError: - return False - return True class HEREDestinationWeather(WeatherEntity): """Implementation of an HERE Destination Weather WeatherEntity.""" - def __init__(self, name, here_data, mode): + def __init__(self, name: str, here_data: HEREWeatherData, mode: str): """Initialize the sensor.""" self._name = name self._here_data = here_data @@ -133,15 +55,22 @@ def name(self): """Return the name of the sensor.""" return self._name + @property + def unique_id(self): + """Set unique_id for sensor.""" + return self._name + @property def condition(self): """Return the current condition.""" - return get_condition_from_here_data(self._here_data.data) + return get_condition_from_here_data(self._here_data.coordinator.data) @property def temperature(self) -> float: """Return the temperature.""" - return get_temperature_from_here_data(self._here_data.data) + return get_temperature_from_here_data( + self._here_data.coordinator.data, self._mode + ) @property def temperature_unit(self): @@ -161,17 +90,17 @@ def pressure(self): @property def humidity(self): """Return the humidity.""" - get_attribute_from_here_data(self._here_data.data, "humidity") + get_attribute_from_here_data(self._here_data.coordinator.data, "humidity") @property def wind_speed(self): """Return the wind speed.""" - get_attribute_from_here_data(self._here_data.data, "windSpeed") + get_attribute_from_here_data(self._here_data.coordinator.data, "windSpeed") @property def wind_bearing(self): """Return the wind bearing.""" - get_attribute_from_here_data(self._here_data.data, "windDirection") + get_attribute_from_here_data(self._here_data.coordinator.data, "windDirection") @property def attribution(self): @@ -181,40 +110,71 @@ def attribution(self): @property def forecast(self): """Return the forecast array.""" - if self._here_data.data is None: + if self._here_data.coordinator.data is None: return None data = [] - for offset in range(len(self._here_data.data)): + for offset in range(len(self._here_data.coordinator.data)): data.append( { ATTR_FORECAST_TIME: get_attribute_from_here_data( - self._here_data.data, "utcTime", offset + self._here_data.coordinator.data, "utcTime", offset ), ATTR_FORECAST_TEMP: get_high_or_default_temperature_from_here_data( - self._here_data.data, offset + self._here_data.coordinator.data, self._mode, offset ), ATTR_FORECAST_TEMP_LOW: get_low_or_default_temperature_from_here_data( - self._here_data.data, offset + self._here_data.coordinator.data, self._mode, offset ), ATTR_FORECAST_PRECIPITATION: calc_precipitation( - self._here_data.data, offset + self._here_data.coordinator.data, offset ), ATTR_FORECAST_WIND_SPEED: get_attribute_from_here_data( - self._here_data.data, "windSpeed", offset + self._here_data.coordinator.data, "windSpeed", offset ), ATTR_FORECAST_WIND_BEARING: get_attribute_from_here_data( - self._here_data.data, "windDirection", offset + self._here_data.coordinator.data, "windDirection", offset ), ATTR_FORECAST_CONDITION: get_condition_from_here_data( - self._here_data.data, offset + self._here_data.coordinator.data, offset ), } ) return data - async def async_update(self) -> None: + @property + def available(self): + """Could the api be accessed during the last update call.""" + return self._here_data.coordinator.last_update_success + + @property + def should_poll(self): + """Return the polling requirement for this sensor.""" + return False + + @property + def entity_registry_enabled_default(self): + """Return if the entity should be enabled when first added to the entity registry.""" + return True + + @property + def device_info(self) -> dict: + """Return a device description for device registry.""" + + return { + "identifiers": {(DOMAIN, self._name)}, + "name": self._name, + "manufacturer": "here.com", + } + + async def async_added_to_hass(self): + """When entity is added to hass.""" + self.async_on_remove( + self._here_data.coordinator.async_add_listener(self.async_write_ha_state) + ) + + async def async_update(self): """Get the latest data from HERE.""" - await self.hass.async_add_executor_job(self._here_data.update) + await self._here_data.coordinator.async_request_refresh() def get_condition_from_here_data(here_data: list, offset: int = 0) -> str: @@ -230,29 +190,32 @@ def get_condition_from_here_data(here_data: list, offset: int = 0) -> str: def get_high_or_default_temperature_from_here_data( - here_data: list, offset: int = 0 + here_data: list, mode: str, offset: int = 0 ) -> str: """Return the temperature from here_data.""" temperature = get_attribute_from_here_data(here_data, "highTemperature", offset) if temperature is not None: return float(temperature) - return get_temperature_from_here_data(here_data, offset) + return get_temperature_from_here_data(here_data, mode, offset) def get_low_or_default_temperature_from_here_data( - here_data: list, offset: int = 0 + here_data: list, mode: str, offset: int = 0 ) -> str: """Return the temperature from here_data.""" temperature = get_attribute_from_here_data(here_data, "lowTemperature", offset) if temperature is not None: return float(temperature) - return get_temperature_from_here_data(here_data, offset) + return get_temperature_from_here_data(here_data, mode, offset) -def get_temperature_from_here_data(here_data: list, offset: int = 0) -> str: +def get_temperature_from_here_data(here_data: list, mode: str, offset: int = 0) -> str: """Return the temperature from here_data.""" - temperature = get_attribute_from_here_data(here_data, "temperature", offset) + if mode == MODE_DAILY_SIMPLE: + temperature = get_attribute_from_here_data(here_data, "highTemperature", offset) + else: + temperature = get_attribute_from_here_data(here_data, "temperature", offset) if temperature is not None: return float(temperature) diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 0e7b6c52cc2465..dda95cdffcd645 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -109,6 +109,7 @@ "hangouts", "harmony", "heos", + "here_weather", "hisense_aehw4a1", "hive", "hlk_sw16", diff --git a/requirements_all.txt b/requirements_all.txt index 5ade639cec421e..cad70df2807b66 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -766,9 +766,11 @@ hdate==0.10.2 heatmiserV3==1.1.18 # homeassistant.components.here_travel_time -# homeassistant.components.here_weather herepy==2.0.0 +# homeassistant.components.here_weather +herepy==3.0.1 + # homeassistant.components.hikvisioncam hikvision==0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a2e0d2d235d891..0a9430526bd20c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -442,12 +442,14 @@ hatasmota==0.2.20 hdate==0.10.2 # homeassistant.components.here_travel_time -# homeassistant.components.here_weather herepy==2.0.0 # homeassistant.components.hlk_sw16 hlk-sw16==0.0.9 +# homeassistant.components.here_weather +herepy==3.0.1 + # homeassistant.components.pi_hole hole==0.5.1 diff --git a/tests/components/here_weather/__init__.py b/tests/components/here_weather/__init__.py index afad5eeeaf72c6..6810cc1f67bdff 100644 --- a/tests/components/here_weather/__init__.py +++ b/tests/components/here_weather/__init__.py @@ -1,54 +1 @@ """Tests for here_weather component.""" -import urllib - -PLATFORM = "here_weather" - -API_KEY = "test" - -ZIP_CODE = "10025" - -LOCATION_NAME = "New York" - -LATITUDE = "40.79962" -LONGITUDE = "-73.970314" - - -def build_base_mock_url(api_key, additional_params): - """Construct a url for HERE.""" - base_url = "https://weather.ls.hereapi.com/weather/1.0/report.json?" - parameters = { - "apikey": api_key, - "oneobservation": True, - "metric": True, - } - parameters.update(additional_params) - url = base_url + urllib.parse.urlencode(parameters) - return url - - -def build_zip_code_imperial_mock_url(api_key, zip_code, product): - """Construct a url for HERE.""" - parameters = {"product": product, "zipcode": zip_code, "metric": False} - url = build_base_mock_url(api_key, parameters) - return url - - -def build_coordinates_mock_url(api_key, latitude, longitude, product): - """Construct a url for HERE.""" - parameters = {"product": product, "latitude": latitude, "longitude": longitude} - url = build_base_mock_url(api_key, parameters) - return url - - -def build_location_name_mock_url(api_key, location_name, product): - """Construct a url for HERE.""" - parameters = {"product": product, "name": location_name} - url = build_base_mock_url(api_key, parameters) - return url - - -def build_zip_code_mock_url(api_key, zip_code, product): - """Construct a url for HERE.""" - parameters = {"product": product, "zipcode": zip_code} - url = build_base_mock_url(api_key, parameters) - return url diff --git a/tests/components/here_weather/const.py b/tests/components/here_weather/const.py new file mode 100644 index 00000000000000..83e35ed6c295e8 --- /dev/null +++ b/tests/components/here_weather/const.py @@ -0,0 +1,89 @@ +"""Constants for here_weather tests.""" +import herepy + +daily_forecasts_json = { + "dailyForecasts": { + "forecastLocation": { + "forecast": [ + { + "daylight": "D", + "description": "Overcast. Chilly.", + "skyInfo": "18", + "skyDescription": "Overcast", + "temperatureDesc": "Chilly", + "comfort": "-0.55", + "highTemperature": "4.00", + "lowTemperature": "-1.80", + "humidity": "80", + "dewPoint": "-0.40", + "precipitationProbability": "53", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "1.42", + "airInfo": "*", + "airDescription": "", + "windSpeed": "12.03", + "windDirection": "290", + "windDesc": "West", + "windDescShort": "W", + "beaufortScale": "3", + "beaufortDescription": "Gentle breeze", + "uvIndex": "0", + "uvDesc": "Minimal", + "barometerPressure": "1014.29", + "icon": "7", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/17.png", + "dayOfWeek": "3", + "weekday": "Tuesday", + "utcTime": "2019-11-19T00:00:00.000-05:00", + } + ], + "country": "United States", + "state": "New York", + "city": "New York", + "latitude": 43.00035, + "longitude": -75.4999, + "distance": 0.00, + "timezone": -5, + } + }, + "feedCreation": "2019-11-19T23:01:26.632Z", + "metric": True, +} + +daily_forecasts_response = herepy.DestinationWeatherResponse.new_from_jsondict( + daily_forecasts_json, param_defaults={"dailyForecasts": None} +) + +astronomy_json = { + "astronomy": { + "astronomy": [ + { + "sunrise": "6:55AM", + "sunset": "6:33PM", + "moonrise": "1:27PM", + "moonset": "10:58PM", + "moonPhase": 0.328, + "moonPhaseDesc": "Waxing crescent", + "iconName": "cw_waxing_crescent", + "city": "Edgewater", + "latitude": 40.82, + "longitude": -73.97, + "utcTime": "2019-10-04T00:00:00.000-04:00", + } + ], + "country": "United States", + "state": "New York", + "city": "New York", + "latitude": 40.79962, + "longitude": -73.970314, + "timezone": -5, + }, + "feedCreation": "2019-10-04T14:22:46.164Z", + "metric": True, +} + +astronomy_response = herepy.DestinationWeatherResponse.new_from_jsondict( + astronomy_json, param_defaults={"astronomy": None} +) diff --git a/tests/components/here_weather/test_config_flow.py b/tests/components/here_weather/test_config_flow.py new file mode 100644 index 00000000000000..270ddc687df3e2 --- /dev/null +++ b/tests/components/here_weather/test_config_flow.py @@ -0,0 +1,217 @@ +"""Tests for the here_weather config_flow.""" +import herepy +import pytest + +from homeassistant.components.here_weather.const import ( + CONF_API_KEY, + CONF_LOCATION_NAME, + CONF_OPTION, + CONF_OPTION_COORDINATES, + CONF_OPTION_LOCATION_NAME, + CONF_OPTION_ZIP_CODE, + CONF_ZIP_CODE, + DEFAULT_MODE, + DOMAIN, +) +from homeassistant.const import ( + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_MODE, + CONF_NAME, + CONF_SCAN_INTERVAL, + CONF_UNIT_SYSTEM, + CONF_UNIT_SYSTEM_METRIC, +) + +from .const import daily_forecasts_response + +from tests.async_mock import patch +from tests.common import MockConfigEntry + + +@pytest.mark.parametrize( + "conf_option, method_to_patch, conf_updates", + [ + ( + CONF_OPTION_COORDINATES, + "herepy.DestinationWeatherApi.weather_for_coordinates", + {CONF_LATITUDE: "40.79962", CONF_LONGITUDE: "-73.970314"}, + ), + ( + CONF_OPTION_ZIP_CODE, + "herepy.DestinationWeatherApi.weather_for_zip_code", + {CONF_ZIP_CODE: "test"}, + ), + ( + CONF_OPTION_LOCATION_NAME, + "herepy.DestinationWeatherApi.weather_for_location_name", + {CONF_LOCATION_NAME: "test"}, + ), + ], +) +async def test_config_flow(hass, conf_option, method_to_patch, conf_updates): + """Test we can finish a config flow.""" + with patch( + method_to_patch, return_value=daily_forecasts_response, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"} + ) + assert result["type"] == "form" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_OPTION: conf_option} + ) + assert result["type"] == "form" + config = { + CONF_API_KEY: "test", + CONF_NAME: DOMAIN, + CONF_MODE: DEFAULT_MODE, + CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC, + } + config.update(conf_updates) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], config + ) + assert result["type"] == "create_entry" + + await hass.async_block_till_done() + state = hass.states.get("sensor.here_weather_low_temperature") + assert state + + +async def test_unauthorized(hass): + """Test handling of an unauthorized api key.""" + with patch( + "herepy.DestinationWeatherApi.weather_for_coordinates", + side_effect=herepy.UnauthorizedError("Unauthorized"), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"} + ) + assert result["type"] == "form" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_OPTION: CONF_OPTION_COORDINATES} + ) + assert result["type"] == "form" + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_API_KEY: "test", + CONF_NAME: DOMAIN, + CONF_MODE: DEFAULT_MODE, + CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC, + CONF_LATITUDE: "40.79962", + CONF_LONGITUDE: "-73.970314", + }, + ) + assert result["type"] == "form" + assert result["errors"]["base"] == "unauthorized" + + +async def test_form_already_configured(hass): + """Test is already configured.""" + with patch( + "herepy.DestinationWeatherApi.weather_for_coordinates", + return_value=daily_forecasts_response, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"} + ) + assert result["type"] == "form" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_OPTION: CONF_OPTION_COORDINATES} + ) + assert result["type"] == "form" + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_API_KEY: "test", + CONF_NAME: DOMAIN, + CONF_MODE: DEFAULT_MODE, + CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC, + CONF_LATITUDE: "40.79962", + CONF_LONGITUDE: "-73.970314", + }, + ) + assert result["type"] == "create_entry" + + await hass.async_block_till_done() + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"} + ) + assert result["type"] == "form" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_OPTION: CONF_OPTION_COORDINATES} + ) + assert result["type"] == "form" + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_API_KEY: "test", + CONF_NAME: DOMAIN, + CONF_MODE: DEFAULT_MODE, + CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC, + CONF_LATITUDE: "40.79962", + CONF_LONGITUDE: "-73.970314", + }, + ) + + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + + +async def test_options(hass): + """Test options for Kraken.""" + with patch( + "herepy.DestinationWeatherApi.weather_for_coordinates", + return_value=daily_forecasts_response, + ): + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_API_KEY: "test", + CONF_NAME: DOMAIN, + CONF_MODE: DEFAULT_MODE, + CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC, + CONF_LATITUDE: "40.79962", + CONF_LONGITUDE: "-73.970314", + }, + options={CONF_SCAN_INTERVAL: 60}, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.options.async_init(entry.entry_id) + result = await hass.config_entries.options.async_configure( + result["flow_id"], {CONF_SCAN_INTERVAL: 60} + ) + assert result["type"] == "create_entry" + assert result["data"][CONF_SCAN_INTERVAL] == 60 + + +async def test_default_options(hass): + """Test default options for Kraken.""" + with patch( + "herepy.DestinationWeatherApi.weather_for_coordinates", + return_value=daily_forecasts_response, + ): + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_API_KEY: "test", + CONF_NAME: DOMAIN, + CONF_MODE: DEFAULT_MODE, + CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC, + CONF_LATITUDE: "40.79962", + CONF_LONGITUDE: "-73.970314", + }, + options={CONF_SCAN_INTERVAL: 60}, + ) + entry.add_to_hass(hass) + result = await hass.config_entries.options.async_init(entry.entry_id) + assert result["type"] == "form" + assert result["step_id"] == "init" diff --git a/tests/components/here_weather/test_sensor.py b/tests/components/here_weather/test_sensor.py index 0ead97cc8ea857..bc9b6ca1a55730 100644 --- a/tests/components/here_weather/test_sensor.py +++ b/tests/components/here_weather/test_sensor.py @@ -1,295 +1,121 @@ -"""The test for the here_weather sensor platform.""" -import logging - -import herepy -import pytest - +"""Tests for the here_weather sensor platform.""" from homeassistant.components.here_weather.const import ( ASTRONOMY_ATTRIBUTES, - DAILY_ATTRIBUTES, - DAILY_SIMPLE_ATTRIBUTES, - HOURLY_ATTRIBUTES, + CONF_API_KEY, + DEFAULT_MODE, + DOMAIN, + MODE_ASTRONOMY, ) -from homeassistant.setup import async_setup_component - -from . import ( - API_KEY, - LATITUDE, - LOCATION_NAME, - LONGITUDE, - PLATFORM, - ZIP_CODE, - build_coordinates_mock_url, - build_location_name_mock_url, - build_zip_code_imperial_mock_url, - build_zip_code_mock_url, +from homeassistant.const import ( + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_MODE, + CONF_NAME, + CONF_UNIT_SYSTEM, + CONF_UNIT_SYSTEM_IMPERIAL, + CONF_UNIT_SYSTEM_METRIC, + EVENT_HOMEASSISTANT_START, ) +import homeassistant.util.dt as dt_util -from tests.common import load_fixture - -DOMAIN = "sensor" - - -@pytest.fixture -def requests_mock_credentials_check(requests_mock): - """Add the url used in the api validation to all requests mock.""" - product = herepy.WeatherProductType.forecast_astronomy - response_url = build_zip_code_mock_url(API_KEY, ZIP_CODE, product) - requests_mock.get( - response_url, - text=load_fixture("here_weather/destination_weather_forecasts_astronomy.json"), - ) - return requests_mock - - -@pytest.fixture -def requests_mock_invalid_credentials(requests_mock): - """Add the url for invalid credentials to requests mock.""" - product = herepy.WeatherProductType.forecast_astronomy - response_url = build_zip_code_mock_url(API_KEY, ZIP_CODE, product) - requests_mock.get( - response_url, - text=load_fixture("here_weather/destination_weather_error_unauthorized.json"), - ) - return requests_mock - - -@pytest.fixture -def requests_mock_location_name(requests_mock_credentials_check): - """Add the url used in a request with location_name to requests mock.""" - product = herepy.WeatherProductType.forecast_astronomy - response_url = build_location_name_mock_url(API_KEY, LOCATION_NAME, product) - requests_mock_credentials_check.get( - response_url, - text=load_fixture("here_weather/destination_weather_forecasts_astronomy.json"), - ) - return requests_mock_credentials_check +from .const import astronomy_response, daily_forecasts_response +from tests.async_mock import patch +from tests.common import MockConfigEntry -@pytest.fixture -def requests_mock_coordinates(requests_mock_credentials_check): - """Add the url used in a request with coordinates to requests mock.""" - product = herepy.WeatherProductType.forecast_astronomy - response_url = build_coordinates_mock_url(API_KEY, LATITUDE, LONGITUDE, product) - requests_mock_credentials_check.get( - response_url, - text=load_fixture("here_weather/destination_weather_forecasts_astronomy.json"), - ) - return requests_mock_credentials_check +async def test_sensor(hass): + """Test that sensor has a value.""" + utcnow = dt_util.utcnow() + # Patching 'utcnow' to gain more control over the timed update. + with patch("homeassistant.util.dt.utcnow", return_value=utcnow), patch( + "herepy.DestinationWeatherApi.weather_for_coordinates", + return_value=daily_forecasts_response, + ): + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_API_KEY: "test", + CONF_NAME: DOMAIN, + CONF_MODE: DEFAULT_MODE, + CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC, + CONF_LATITUDE: "40.79962", + CONF_LONGITUDE: "-73.970314", + }, + ) + entry.add_to_hass(hass) -@pytest.fixture -def requests_mock_zip_code_imperial(requests_mock_credentials_check): - """Add the url used in a request with zip_code to requests mock.""" - product = herepy.WeatherProductType.observation - response_url = build_zip_code_imperial_mock_url(API_KEY, ZIP_CODE, product) - requests_mock_credentials_check.get( - response_url, - text=load_fixture("here_weather/destination_weather_observation_imperial.json"), - ) - return requests_mock_credentials_check + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() -@pytest.fixture -def requests_mock_forecast_7days(requests_mock_credentials_check): - """Add the url used in a request with forecast_7days to requests mock.""" - product = herepy.WeatherProductType.forecast_7days - response_url = build_location_name_mock_url(API_KEY, LOCATION_NAME, product) - requests_mock_credentials_check.get( - response_url, - text=load_fixture("here_weather/destination_weather_forecasts.json"), - ) - return requests_mock_credentials_check + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + sensor = hass.states.get("sensor.here_weather_low_temperature") + assert sensor.state == "-1.80" -@pytest.fixture -def requests_mock_forecast_hourly(requests_mock_credentials_check): - """Add the url used in a request with forecast_hourly to requests mock.""" - product = herepy.WeatherProductType.forecast_hourly - response_url = build_location_name_mock_url(API_KEY, LOCATION_NAME, product) - requests_mock_credentials_check.get( - response_url, - text=load_fixture("here_weather/destination_weather_forecasts_hourly.json"), - ) - return requests_mock_credentials_check - -@pytest.fixture -def requests_mock_forecast_7days_simple(requests_mock_credentials_check): - """Add the url used in a request with forecast_7days_simple to requests mock.""" - product = herepy.WeatherProductType.forecast_7days_simple - response_url = build_location_name_mock_url(API_KEY, LOCATION_NAME, product) - requests_mock_credentials_check.get( - response_url, - text=load_fixture("here_weather/destination_weather_forecasts_simple.json"), - ) - return requests_mock_credentials_check - - -@pytest.fixture -def requests_mock_invalid_request(requests_mock_credentials_check): - """Add the url used in an invalid request to requests mock.""" - product = herepy.WeatherProductType.observation - response_url = build_zip_code_mock_url(API_KEY, ZIP_CODE, product) - requests_mock_credentials_check.get( - response_url, - text=load_fixture( - "here_weather/destination_weather_error_invalid_request.json" - ), - ) - return requests_mock_credentials_check - - -async def test_invalid_credentials(hass, requests_mock_invalid_credentials, caplog): - """Test that invalid credentials error is correctly handled.""" - caplog.set_level(logging.ERROR) - config = { - DOMAIN: { - "platform": PLATFORM, - "name": "test", - "zip_code": ZIP_CODE, - "mode": "forecast_astronomy", - "api_key": API_KEY, - } - } - assert await async_setup_component(hass, DOMAIN, config) - assert len(caplog.records) == 1 - assert "Invalid credentials" in caplog.text - - -async def test_forecast_astronomy(hass, requests_mock_credentials_check): +async def test_forecast_astronomy(hass): """Test that forecast_astronomy works.""" - config = { - DOMAIN: { - "platform": PLATFORM, - "name": "test", - "zip_code": ZIP_CODE, - "mode": "forecast_astronomy", - "api_key": API_KEY, - } - } - - assert await async_setup_component(hass, DOMAIN, config) - assert len(hass.states.async_all()) == len(ASTRONOMY_ATTRIBUTES) - - -async def test_location_name(hass, requests_mock_location_name): - """Test that location_name option works.""" - config = { - DOMAIN: { - "platform": PLATFORM, - "name": "test", - "location_name": LOCATION_NAME, - "mode": "forecast_astronomy", - "api_key": API_KEY, - } - } - - assert await async_setup_component(hass, DOMAIN, config) - assert len(hass.states.async_all()) == len(ASTRONOMY_ATTRIBUTES) - - -async def test_coordinates(hass, requests_mock_coordinates): - """Test that lat/lon option works.""" - config = { - DOMAIN: { - "platform": PLATFORM, - "name": "test", - "latitude": LATITUDE, - "longitude": LONGITUDE, - "mode": "forecast_astronomy", - "api_key": API_KEY, - } - } - - assert await async_setup_component(hass, DOMAIN, config) - assert len(hass.states.async_all()) == len(ASTRONOMY_ATTRIBUTES) - - -async def test_imperial(hass, requests_mock_zip_code_imperial): + utcnow = dt_util.utcnow() + # Patching 'utcnow' to gain more control over the timed update. + with patch("homeassistant.util.dt.utcnow", return_value=utcnow), patch( + "herepy.DestinationWeatherApi.weather_for_coordinates", + return_value=astronomy_response, + ): + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_API_KEY: "test", + CONF_NAME: DOMAIN, + CONF_MODE: MODE_ASTRONOMY, + CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC, + CONF_LATITUDE: "40.79962", + CONF_LONGITUDE: "-73.970314", + }, + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + + sensor = hass.states.get("sensor.here_weather_sunrise") + assert sensor.state == "6:55AM" + assert len(hass.states.async_all()) == len(ASTRONOMY_ATTRIBUTES) + + +async def test_imperial(hass): """Test that imperial mode works.""" - config = { - DOMAIN: { - "platform": PLATFORM, - "name": "test", - "zip_code": ZIP_CODE, - "mode": "observation", - "api_key": API_KEY, - "unit_system": "imperial", - } - } - - assert await async_setup_component(hass, DOMAIN, config) - - sensor_name = f"{DOMAIN}.test_wind_speed" - sensor = hass.states.get(sensor_name) - assert sensor.attributes.get("unit_of_measurement") == "mph" - - -async def test_forecast_7days(hass, requests_mock_forecast_7days): - """Test that forecast_7days option works.""" - config = { - DOMAIN: { - "platform": PLATFORM, - "name": "test", - "location_name": LOCATION_NAME, - "mode": "forecast_7days", - "api_key": API_KEY, - } - } - - assert await async_setup_component(hass, DOMAIN, config) - assert len(hass.states.async_all()) == len(DAILY_ATTRIBUTES) - - -async def test_forecast_7days_simple(hass, requests_mock_forecast_7days_simple): - """Test that forecast_7days_simple option works.""" - config = { - DOMAIN: { - "platform": PLATFORM, - "name": "test", - "location_name": LOCATION_NAME, - "mode": "forecast_7days_simple", - "api_key": API_KEY, - } - } - - assert await async_setup_component(hass, DOMAIN, config) - assert len(hass.states.async_all()) == len(DAILY_SIMPLE_ATTRIBUTES) - - -async def test_forecast_forecast_hourly(hass, requests_mock_forecast_hourly): - """Test that forecast_hourly option works.""" - config = { - DOMAIN: { - "platform": PLATFORM, - "name": "test", - "location_name": LOCATION_NAME, - "mode": "forecast_hourly", - "api_key": API_KEY, - } - } - - assert await async_setup_component(hass, DOMAIN, config) - assert len(hass.states.async_all()) == len(HOURLY_ATTRIBUTES) - - -async def test_invalid_request(hass, requests_mock_invalid_request, caplog): - """Test that invalid credentials error is correctly handled.""" - caplog.set_level(logging.ERROR) - config = { - DOMAIN: { - "platform": PLATFORM, - "name": "test", - "zip_code": ZIP_CODE, - "mode": "observation", - "api_key": API_KEY, - } - } - assert await async_setup_component(hass, DOMAIN, config) - assert len(caplog.records) == 1 - assert "Error during sensor update" in caplog.text - - sensor_name = sensor_name = f"{DOMAIN}.test_wind_speed" - sensor = hass.states.get(sensor_name) - assert sensor.state == "unknown" + utcnow = dt_util.utcnow() + # Patching 'utcnow' to gain more control over the timed update. + with patch("homeassistant.util.dt.utcnow", return_value=utcnow), patch( + "herepy.DestinationWeatherApi.weather_for_coordinates", + return_value=daily_forecasts_response, + ): + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_API_KEY: "test", + CONF_NAME: DOMAIN, + CONF_MODE: DEFAULT_MODE, + CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_IMPERIAL, + CONF_LATITUDE: "40.79962", + CONF_LONGITUDE: "-73.970314", + }, + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + + sensor = hass.states.get("sensor.here_weather_wind_speed") + assert sensor.attributes.get("unit_of_measurement") == "mph" diff --git a/tests/components/here_weather/test_weather.py b/tests/components/here_weather/test_weather.py index 386a6394e5d550..eff72b8a9064b8 100644 --- a/tests/components/here_weather/test_weather.py +++ b/tests/components/here_weather/test_weather.py @@ -1,285 +1,53 @@ -"""The test for the here_weather weather platform.""" -import logging - -import herepy -import pytest - -from homeassistant.setup import async_setup_component - -from . import ( - API_KEY, - LATITUDE, - LOCATION_NAME, - LONGITUDE, - PLATFORM, - ZIP_CODE, - build_coordinates_mock_url, - build_location_name_mock_url, - build_zip_code_imperial_mock_url, - build_zip_code_mock_url, +"""Tests for the here_weather weather platform.""" +from homeassistant.components.here_weather.const import ( + CONF_API_KEY, + DEFAULT_MODE, + DOMAIN, ) - -from tests.common import load_fixture - -DOMAIN = "weather" - - -@pytest.fixture -def requests_mock_credentials_check(requests_mock): - """Add the url used in the api validation to all requests mock.""" - product = herepy.WeatherProductType.forecast_astronomy - response_url = build_zip_code_mock_url(API_KEY, ZIP_CODE, product) - requests_mock.get( - response_url, - text=load_fixture("here_weather/destination_weather_forecasts_astronomy.json"), - ) - return requests_mock - - -@pytest.fixture -def requests_mock_invalid_credentials(requests_mock): - """Add the url for invalid credentials to requests mock.""" - product = herepy.WeatherProductType.forecast_astronomy - response_url = build_zip_code_mock_url(API_KEY, ZIP_CODE, product) - requests_mock.get( - response_url, - text=load_fixture("here_weather/destination_weather_error_unauthorized.json"), - ) - return requests_mock - - -@pytest.fixture -def requests_mock_location_name(requests_mock_credentials_check): - """Add the url used in a request with location_name to requests mock.""" - product = herepy.WeatherProductType.forecast_7days - response_url = build_location_name_mock_url(API_KEY, LOCATION_NAME, product) - requests_mock_credentials_check.get( - response_url, - text=load_fixture("here_weather/destination_weather_forecasts.json"), - ) - return requests_mock_credentials_check - - -@pytest.fixture -def requests_mock_coordinates(requests_mock_credentials_check): - """Add the url used in a request with coordinates to requests mock.""" - product = herepy.WeatherProductType.forecast_7days - response_url = build_coordinates_mock_url(API_KEY, LATITUDE, LONGITUDE, product) - requests_mock_credentials_check.get( - response_url, - text=load_fixture("here_weather/destination_weather_forecasts.json"), - ) - return requests_mock_credentials_check - - -@pytest.fixture -def requests_mock_zip_code_imperial(requests_mock_credentials_check): - """Add the url used in a request with zip_code to requests mock.""" - product = herepy.WeatherProductType.observation - response_url = build_zip_code_imperial_mock_url(API_KEY, ZIP_CODE, product) - requests_mock_credentials_check.get( - response_url, - text=load_fixture("here_weather/destination_weather_observation_imperial.json"), - ) - return requests_mock_credentials_check - - -@pytest.fixture -def requests_mock_forecast_7days(requests_mock_credentials_check): - """Add the url used in a request with forecast_7days to requests mock.""" - product = herepy.WeatherProductType.forecast_7days - response_url = build_location_name_mock_url(API_KEY, LOCATION_NAME, product) - requests_mock_credentials_check.get( - response_url, - text=load_fixture("here_weather/destination_weather_forecasts.json"), - ) - return requests_mock_credentials_check - - -@pytest.fixture -def requests_mock_forecast_hourly(requests_mock_credentials_check): - """Add the url used in a request with forecast_hourly to requests mock.""" - product = herepy.WeatherProductType.forecast_hourly - response_url = build_location_name_mock_url(API_KEY, LOCATION_NAME, product) - requests_mock_credentials_check.get( - response_url, - text=load_fixture("here_weather/destination_weather_forecasts_hourly.json"), - ) - return requests_mock_credentials_check - - -@pytest.fixture -def requests_mock_forecast_7days_simple(requests_mock_credentials_check): - """Add the url used in a request with forecast_7days_simple to requests mock.""" - product = herepy.WeatherProductType.forecast_7days_simple - response_url = build_location_name_mock_url(API_KEY, LOCATION_NAME, product) - requests_mock_credentials_check.get( - response_url, - text=load_fixture("here_weather/destination_weather_forecasts_simple.json"), - ) - return requests_mock_credentials_check - - -@pytest.fixture -def requests_mock_invalid_request(requests_mock_credentials_check): - """Add the url used in an invalid request to requests mock.""" - product = herepy.WeatherProductType.observation - response_url = build_zip_code_mock_url(API_KEY, ZIP_CODE, product) - requests_mock_credentials_check.get( - response_url, - text=load_fixture( - "here_weather/destination_weather_error_invalid_request.json" - ), - ) - return requests_mock_credentials_check - - -async def test_invalid_credentials(hass, requests_mock_invalid_credentials, caplog): - """Test that invalid credentials error is correctly handled.""" - caplog.set_level(logging.ERROR) - config = { - DOMAIN: { - "platform": PLATFORM, - "name": "test", - "zip_code": ZIP_CODE, - "mode": "forecast_7days", - "api_key": API_KEY, - } - } - assert await async_setup_component(hass, DOMAIN, config) - assert len(caplog.records) == 1 - assert "Invalid credentials" in caplog.text - - -async def test_location_name(hass, requests_mock_location_name): - """Test that location_name option works.""" - config = { - DOMAIN: { - "platform": PLATFORM, - "name": "test", - "location_name": LOCATION_NAME, - "mode": "forecast_7days", - "api_key": API_KEY, - } - } - - assert await async_setup_component(hass, DOMAIN, config) - sensor_name = sensor_name = f"{DOMAIN}.test" - sensor = hass.states.get(sensor_name) - assert sensor.state == "cloudy" - - -async def test_coordinates(hass, requests_mock_coordinates): - """Test that lat/lon option works.""" - config = { - DOMAIN: { - "platform": PLATFORM, - "name": "test", - "latitude": LATITUDE, - "longitude": LONGITUDE, - "mode": "forecast_7days", - "api_key": API_KEY, - } - } - - assert await async_setup_component(hass, DOMAIN, config) - sensor_name = sensor_name = f"{DOMAIN}.test" - sensor = hass.states.get(sensor_name) - assert sensor.state == "cloudy" - - -async def test_imperial(hass, requests_mock_zip_code_imperial): - """Test that imperial mode works.""" - config = { - DOMAIN: { - "platform": PLATFORM, - "name": "test", - "zip_code": ZIP_CODE, - "mode": "observation", - "api_key": API_KEY, - "unit_system": "imperial", - } - } - - assert await async_setup_component(hass, DOMAIN, config) - - sensor_name = f"{DOMAIN}.test" - sensor = hass.states.get(sensor_name) - assert ( - sensor.attributes.get("temperature") == 8 - ) # Gets converted because standard hass config is metric - - -async def test_forecast_7days(hass, requests_mock_forecast_7days): - """Test that forecast_7days option works.""" - config = { - DOMAIN: { - "platform": PLATFORM, - "name": "test", - "location_name": LOCATION_NAME, - "mode": "forecast_7days", - "api_key": API_KEY, - } - } - - assert await async_setup_component(hass, DOMAIN, config) - sensor_name = sensor_name = f"{DOMAIN}.test" - sensor = hass.states.get(sensor_name) - assert sensor.state == "cloudy" - - -async def test_forecast_7days_simple(hass, requests_mock_forecast_7days_simple): - """Test that forecast_7days_simple option works.""" - config = { - DOMAIN: { - "platform": PLATFORM, - "name": "test", - "location_name": LOCATION_NAME, - "mode": "forecast_7days_simple", - "api_key": API_KEY, - } - } - - assert await async_setup_component(hass, DOMAIN, config) - sensor_name = sensor_name = f"{DOMAIN}.test" - sensor = hass.states.get(sensor_name) - assert sensor.state == "cloudy" - - -async def test_forecast_forecast_hourly(hass, requests_mock_forecast_hourly): - """Test that forecast_hourly option works.""" - config = { - DOMAIN: { - "platform": PLATFORM, - "name": "test", - "location_name": LOCATION_NAME, - "mode": "forecast_hourly", - "api_key": API_KEY, - } - } - - assert await async_setup_component(hass, DOMAIN, config) - sensor_name = sensor_name = f"{DOMAIN}.test" - sensor = hass.states.get(sensor_name) - assert sensor.state == "cloudy" - - -async def test_invalid_request(hass, requests_mock_invalid_request, caplog): - """Test that invalid credentials error is correctly handled.""" - caplog.set_level(logging.ERROR) - config = { - DOMAIN: { - "platform": PLATFORM, - "name": "test", - "zip_code": ZIP_CODE, - "mode": "observation", - "api_key": API_KEY, - } - } - assert await async_setup_component(hass, DOMAIN, config) - assert len(caplog.records) == 1 - assert "Error during sensor update" in caplog.text - - sensor_name = sensor_name = f"{DOMAIN}.test" - sensor = hass.states.get(sensor_name) - assert sensor.state == "unknown" +from homeassistant.const import ( + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_MODE, + CONF_NAME, + CONF_UNIT_SYSTEM, + CONF_UNIT_SYSTEM_METRIC, + EVENT_HOMEASSISTANT_START, +) +import homeassistant.util.dt as dt_util + +from .const import daily_forecasts_response + +from tests.async_mock import patch +from tests.common import MockConfigEntry + + +async def test_weather(hass): + """Test that sensor has a value.""" + utcnow = dt_util.utcnow() + # Patching 'utcnow' to gain more control over the timed update. + with patch("homeassistant.util.dt.utcnow", return_value=utcnow), patch( + "herepy.DestinationWeatherApi.weather_for_coordinates", + return_value=daily_forecasts_response, + ): + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_API_KEY: "test", + CONF_NAME: DOMAIN, + CONF_MODE: DEFAULT_MODE, + CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC, + CONF_LATITUDE: "40.79962", + CONF_LONGITUDE: "-73.970314", + }, + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + + sensor = hass.states.get("weather.here_weather") + assert sensor.state == "cloudy" diff --git a/tests/fixtures/here_weather/destination_weather_error_invalid_request.json b/tests/fixtures/here_weather/destination_weather_error_invalid_request.json deleted file mode 100644 index e74086f5596b2b..00000000000000 --- a/tests/fixtures/here_weather/destination_weather_error_invalid_request.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "Type": "Invalid Request", - "Message": [ - "Request parameter 'product' does not conform with expected data type. Please check the documentation." - ] -} \ No newline at end of file diff --git a/tests/fixtures/here_weather/destination_weather_error_unauthorized.json b/tests/fixtures/here_weather/destination_weather_error_unauthorized.json deleted file mode 100644 index 49077fe18bce6d..00000000000000 --- a/tests/fixtures/here_weather/destination_weather_error_unauthorized.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "Type": "Unauthorized", - "Message": [ - "This is not a valid app_id and app_code pair. Please verify that the values are not swapped between the app_id and app_code and the values provisioned by HERE (either by your customer representative or via http://developer.here.com/myapps) were copied correctly into the request." - ] - } diff --git a/tests/fixtures/here_weather/destination_weather_forecasts.json b/tests/fixtures/here_weather/destination_weather_forecasts.json deleted file mode 100644 index 93db1a0d5d5a28..00000000000000 --- a/tests/fixtures/here_weather/destination_weather_forecasts.json +++ /dev/null @@ -1,761 +0,0 @@ -{ - "forecasts": { - "forecastLocation": { - "forecast": [ - { - "daylight": "D", - "daySegment": "Afternoon", - "description": "Overcast. Chilly.", - "skyInfo": "18", - "skyDescription": "Overcast", - "temperature": "4.00", - "temperatureDesc": "Chilly", - "comfort": "0.86", - "humidity": "73", - "dewPoint": "-0.40", - "precipitationProbability": "9", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "12.96", - "windDirection": "294", - "windDesc": "Northwest", - "windDescShort": "NW", - "beaufortScale": "3", - "beaufortDescription": "Gentle breeze", - "visibility": "11.38", - "icon": "7", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/17.png", - "dayOfWeek": "3", - "weekday": "Tuesday", - "utcTime": "2019-11-19T12:00:00.000-05:00" - }, - { - "daylight": "N", - "daySegment": "Evening", - "description": "Overcast. Cold.", - "skyInfo": "18", - "skyDescription": "Overcast", - "temperature": "-0.30", - "temperatureDesc": "Cold", - "comfort": "-3.06", - "humidity": "90", - "dewPoint": "-1.70", - "precipitationProbability": "10", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "7.92", - "windDirection": "290", - "windDesc": "West", - "windDescShort": "W", - "beaufortScale": "2", - "beaufortDescription": "Light breeze", - "visibility": "12.25", - "icon": "17", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", - "dayOfWeek": "3", - "weekday": "Tuesday", - "utcTime": "2019-11-19T18:00:00.000-05:00" - }, - { - "daylight": "N", - "daySegment": "Night", - "description": "Overcast. Cold.", - "skyInfo": "18", - "skyDescription": "Overcast", - "temperature": "-1.90", - "temperatureDesc": "Cold", - "comfort": "-4.94", - "humidity": "94", - "dewPoint": "-2.70", - "precipitationProbability": "7", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "7.92", - "windDirection": "301", - "windDesc": "Northwest", - "windDescShort": "NW", - "beaufortScale": "2", - "beaufortDescription": "Light breeze", - "visibility": "12.56", - "icon": "17", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", - "dayOfWeek": "3", - "weekday": "Tuesday", - "utcTime": "2019-11-20T00:00:00.000-05:00" - }, - { - "daylight": "D", - "daySegment": "Morning", - "description": "Light snow. Overcast. Cold.", - "skyInfo": "18", - "skyDescription": "Overcast", - "temperature": "-1.70", - "temperatureDesc": "Cold", - "comfort": "-5.47", - "humidity": "100", - "dewPoint": "-1.30", - "precipitationProbability": "42", - "precipitationDesc": "Light snow", - "rainFall": "*", - "snowFall": "0.53", - "airInfo": "*", - "airDescription": "", - "windSpeed": "10.44", - "windDirection": "308", - "windDesc": "Northwest", - "windDescShort": "NW", - "beaufortScale": "2", - "beaufortDescription": "Light breeze", - "visibility": "12.05", - "icon": "29", - "iconName": "light_snow", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/20.png", - "dayOfWeek": "4", - "weekday": "Wednesday", - "utcTime": "2019-11-20T06:00:00.000-05:00" - }, - { - "daylight": "D", - "daySegment": "Afternoon", - "description": "Light snow. Overcast. Chilly.", - "skyInfo": "18", - "skyDescription": "Overcast", - "temperature": "2.30", - "temperatureDesc": "Chilly", - "comfort": "-2.11", - "humidity": "83", - "dewPoint": "-0.30", - "precipitationProbability": "43", - "precipitationDesc": "Light snow", - "rainFall": "0.03", - "snowFall": "0.30", - "airInfo": "*", - "airDescription": "", - "windSpeed": "18.36", - "windDirection": "310", - "windDesc": "Northwest", - "windDescShort": "NW", - "beaufortScale": "4", - "beaufortDescription": "Moderate breeze", - "visibility": "12.41", - "icon": "29", - "iconName": "light_snow", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/20.png", - "dayOfWeek": "4", - "weekday": "Wednesday", - "utcTime": "2019-11-20T12:00:00.000-05:00" - }, - { - "daylight": "N", - "daySegment": "Evening", - "description": "Light snow. Overcast. Chilly.", - "skyInfo": "18", - "skyDescription": "Overcast", - "temperature": "0.70", - "temperatureDesc": "Chilly", - "comfort": "-2.93", - "humidity": "90", - "dewPoint": "-0.70", - "precipitationProbability": "28", - "precipitationDesc": "Light snow", - "rainFall": "*", - "snowFall": "0.10", - "airInfo": "32", - "airDescription": "Damp", - "windSpeed": "11.88", - "windDirection": "309", - "windDesc": "Northwest", - "windDescShort": "NW", - "beaufortScale": "3", - "beaufortDescription": "Gentle breeze", - "visibility": "11.72", - "icon": "29", - "iconName": "light_snow", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/20.png", - "dayOfWeek": "4", - "weekday": "Wednesday", - "utcTime": "2019-11-20T18:00:00.000-05:00" - }, - { - "daylight": "N", - "daySegment": "Night", - "description": "Overcast. Chilly.", - "skyInfo": "18", - "skyDescription": "Overcast", - "temperature": "0.60", - "temperatureDesc": "Chilly", - "comfort": "-2.88", - "humidity": "92", - "dewPoint": "-0.50", - "precipitationProbability": "8", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "11.16", - "windDirection": "305", - "windDesc": "Northwest", - "windDescShort": "NW", - "beaufortScale": "3", - "beaufortDescription": "Gentle breeze", - "visibility": "11.63", - "icon": "17", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", - "dayOfWeek": "4", - "weekday": "Wednesday", - "utcTime": "2019-11-21T00:00:00.000-05:00" - }, - { - "daylight": "D", - "daySegment": "Morning", - "description": "Cloudy. Cold.", - "skyInfo": "17", - "skyDescription": "Cloudy", - "temperature": "-0.10", - "temperatureDesc": "Cold", - "comfort": "-2.71", - "humidity": "99", - "dewPoint": "-0.20", - "precipitationProbability": "3", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "7.56", - "windDirection": "292", - "windDesc": "West", - "windDescShort": "W", - "beaufortScale": "2", - "beaufortDescription": "Light breeze", - "visibility": "12.53", - "icon": "7", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/17.png", - "dayOfWeek": "5", - "weekday": "Thursday", - "utcTime": "2019-11-21T06:00:00.000-05:00" - }, - { - "daylight": "D", - "daySegment": "Afternoon", - "description": "Partly sunny. Chilly.", - "skyInfo": "14", - "skyDescription": "Partly sunny", - "temperature": "3.50", - "temperatureDesc": "Chilly", - "comfort": "1.20", - "humidity": "79", - "dewPoint": "0.20", - "precipitationProbability": "5", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "8.64", - "windDirection": "180", - "windDesc": "South", - "windDescShort": "S", - "beaufortScale": "2", - "beaufortDescription": "Light breeze", - "visibility": "11.57", - "icon": "6", - "iconName": "mostly_cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/17.png", - "dayOfWeek": "5", - "weekday": "Thursday", - "utcTime": "2019-11-21T12:00:00.000-05:00" - }, - { - "daylight": "N", - "daySegment": "Evening", - "description": "Overcast. Chilly.", - "skyInfo": "18", - "skyDescription": "Overcast", - "temperature": "3.10", - "temperatureDesc": "Chilly", - "comfort": "-1.16", - "humidity": "84", - "dewPoint": "0.60", - "precipitationProbability": "9", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "18.72", - "windDirection": "170", - "windDesc": "South", - "windDescShort": "S", - "beaufortScale": "4", - "beaufortDescription": "Moderate breeze", - "visibility": "11.91", - "icon": "17", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", - "dayOfWeek": "5", - "weekday": "Thursday", - "utcTime": "2019-11-21T18:00:00.000-05:00" - }, - { - "daylight": "N", - "daySegment": "Night", - "description": "Sprinkles. Cloudy. Chilly.", - "skyInfo": "17", - "skyDescription": "Cloudy", - "temperature": "4.10", - "temperatureDesc": "Chilly", - "comfort": "-0.47", - "humidity": "91", - "dewPoint": "2.70", - "precipitationProbability": "33", - "precipitationDesc": "Sprinkles", - "rainFall": "0.06", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "23.40", - "windDirection": "205", - "windDesc": "Southwest", - "windDescShort": "SW", - "beaufortScale": "4", - "beaufortDescription": "Moderate breeze", - "visibility": "0.00", - "icon": "34", - "iconName": "night_sprinkles", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/22.png", - "dayOfWeek": "5", - "weekday": "Thursday", - "utcTime": "2019-11-22T00:00:00.000-05:00" - }, - { - "daylight": "D", - "daySegment": "Morning", - "description": "Sprinkles. Cloudy. Chilly.", - "skyInfo": "17", - "skyDescription": "Cloudy", - "temperature": "6.90", - "temperatureDesc": "Chilly", - "comfort": "3.53", - "humidity": "90", - "dewPoint": "5.40", - "precipitationProbability": "54", - "precipitationDesc": "Sprinkles", - "rainFall": "0.18", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "19.44", - "windDirection": "232", - "windDesc": "Southwest", - "windDescShort": "SW", - "beaufortScale": "4", - "beaufortDescription": "Moderate breeze", - "visibility": "0.00", - "icon": "18", - "iconName": "sprinkles", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/27.png", - "dayOfWeek": "6", - "weekday": "Friday", - "utcTime": "2019-11-22T06:00:00.000-05:00" - }, - { - "daylight": "D", - "daySegment": "Afternoon", - "description": "Overcast. Chilly.", - "skyInfo": "18", - "skyDescription": "Overcast", - "temperature": "2.60", - "temperatureDesc": "Chilly", - "comfort": "-3.49", - "humidity": "85", - "dewPoint": "0.30", - "precipitationProbability": "8", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "13", - "airDescription": "Breezy", - "windSpeed": "34.56", - "windDirection": "275", - "windDesc": "West", - "windDescShort": "W", - "beaufortScale": "5", - "beaufortDescription": "Fresh breeze", - "visibility": "0.00", - "icon": "7", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/17.png", - "dayOfWeek": "6", - "weekday": "Friday", - "utcTime": "2019-11-22T12:00:00.000-05:00" - }, - { - "daylight": "N", - "daySegment": "Evening", - "description": "Overcast. Cold.", - "skyInfo": "18", - "skyDescription": "Overcast", - "temperature": "-2.00", - "temperatureDesc": "Cold", - "comfort": "-9.70", - "humidity": "78", - "dewPoint": "-5.40", - "precipitationProbability": "7", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "13", - "airDescription": "Breezy", - "windSpeed": "36.00", - "windDirection": "294", - "windDesc": "Northwest", - "windDescShort": "NW", - "beaufortScale": "6", - "beaufortDescription": "Strong breeze", - "visibility": "0.00", - "icon": "17", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", - "dayOfWeek": "6", - "weekday": "Friday", - "utcTime": "2019-11-22T18:00:00.000-05:00" - }, - { - "daylight": "N", - "daySegment": "Night", - "description": "Partly cloudy. Cold.", - "skyInfo": "10", - "skyDescription": "Partly cloudy", - "temperature": "-5.20", - "temperatureDesc": "Cold", - "comfort": "-10.93", - "humidity": "81", - "dewPoint": "-6.50", - "precipitationProbability": "2", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "27.00", - "windDirection": "302", - "windDesc": "Northwest", - "windDescShort": "NW", - "beaufortScale": "4", - "beaufortDescription": "Moderate breeze", - "visibility": "0.00", - "icon": "15", - "iconName": "night_partly_cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", - "dayOfWeek": "6", - "weekday": "Friday", - "utcTime": "2019-11-23T00:00:00.000-05:00" - }, - { - "daylight": "D", - "daySegment": "Morning", - "description": "a mixture of sun and clouds. Cold.", - "skyInfo": "11", - "skyDescription": "a mixture of sun and clouds", - "temperature": "-3.30", - "temperatureDesc": "Cold", - "comfort": "-8.03", - "humidity": "80", - "dewPoint": "-6.30", - "precipitationProbability": "3", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "12.96", - "windDirection": "323", - "windDesc": "Northwest", - "windDescShort": "NW", - "beaufortScale": "3", - "beaufortDescription": "Gentle breeze", - "visibility": "0.00", - "icon": "4", - "iconName": "partly_cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/3.png", - "dayOfWeek": "7", - "weekday": "Saturday", - "utcTime": "2019-11-23T06:00:00.000-05:00" - }, - { - "daylight": "D", - "daySegment": "Afternoon", - "description": "a mixture of sun and clouds. Chilly.", - "skyInfo": "11", - "skyDescription": "a mixture of sun and clouds", - "temperature": "1.20", - "temperatureDesc": "Chilly", - "comfort": "-1.66", - "humidity": "71", - "dewPoint": "-3.90", - "precipitationProbability": "4", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "7.92", - "windDirection": "293", - "windDesc": "Northwest", - "windDescShort": "NW", - "beaufortScale": "2", - "beaufortDescription": "Light breeze", - "visibility": "0.00", - "icon": "4", - "iconName": "partly_cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/3.png", - "dayOfWeek": "7", - "weekday": "Saturday", - "utcTime": "2019-11-23T12:00:00.000-05:00" - }, - { - "daylight": "N", - "daySegment": "Evening", - "description": "Partly cloudy. Cold.", - "skyInfo": "10", - "skyDescription": "Partly cloudy", - "temperature": "-0.20", - "temperatureDesc": "Cold", - "comfort": "-3.28", - "humidity": "85", - "dewPoint": "-2.50", - "precipitationProbability": "3", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "9.00", - "windDirection": "155", - "windDesc": "Southeast", - "windDescShort": "SE", - "beaufortScale": "2", - "beaufortDescription": "Light breeze", - "visibility": "0.00", - "icon": "15", - "iconName": "night_partly_cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", - "dayOfWeek": "7", - "weekday": "Saturday", - "utcTime": "2019-11-23T18:00:00.000-05:00" - }, - { - "daylight": "N", - "daySegment": "Night", - "description": "Partly cloudy. Cold.", - "skyInfo": "10", - "skyDescription": "Partly cloudy", - "temperature": "-1.60", - "temperatureDesc": "Cold", - "comfort": "-3.99", - "humidity": "93", - "dewPoint": "-2.30", - "precipitationProbability": "8", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "7.20", - "windDirection": "77", - "windDesc": "East", - "windDescShort": "E", - "beaufortScale": "2", - "beaufortDescription": "Light breeze", - "visibility": "0.00", - "icon": "15", - "iconName": "night_partly_cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", - "dayOfWeek": "7", - "weekday": "Saturday", - "utcTime": "2019-11-24T00:00:00.000-05:00" - }, - { - "daylight": "D", - "daySegment": "Morning", - "description": "a mixture of sun and clouds. Cold.", - "skyInfo": "11", - "skyDescription": "a mixture of sun and clouds", - "temperature": "-0.80", - "temperatureDesc": "Cold", - "comfort": "-3.88", - "humidity": "92", - "dewPoint": "-1.90", - "precipitationProbability": "3", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "8.64", - "windDirection": "339", - "windDesc": "North", - "windDescShort": "N", - "beaufortScale": "2", - "beaufortDescription": "Light breeze", - "visibility": "0.00", - "icon": "4", - "iconName": "partly_cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/3.png", - "dayOfWeek": "1", - "weekday": "Sunday", - "utcTime": "2019-11-24T06:00:00.000-05:00" - }, - { - "daylight": "D", - "daySegment": "Afternoon", - "description": "a mixture of sun and clouds. Cold.", - "skyInfo": "11", - "skyDescription": "a mixture of sun and clouds", - "temperature": "0.60", - "temperatureDesc": "Cold", - "comfort": "-5.55", - "humidity": "86", - "dewPoint": "-2.20", - "precipitationProbability": "9", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "21.24", - "windDirection": "307", - "windDesc": "Northwest", - "windDescShort": "NW", - "beaufortScale": "4", - "beaufortDescription": "Moderate breeze", - "visibility": "0.00", - "icon": "4", - "iconName": "partly_cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/3.png", - "dayOfWeek": "1", - "weekday": "Sunday", - "utcTime": "2019-11-24T12:00:00.000-05:00" - }, - { - "daylight": "N", - "daySegment": "Evening", - "description": "Light snow. Cloudy. Chilly.", - "skyInfo": "17", - "skyDescription": "Cloudy", - "temperature": "0.30", - "temperatureDesc": "Chilly", - "comfort": "-4.32", - "humidity": "93", - "dewPoint": "-0.70", - "precipitationProbability": "25", - "precipitationDesc": "Light snow", - "rainFall": "*", - "snowFall": "0.60", - "airInfo": "*", - "airDescription": "", - "windSpeed": "16.56", - "windDirection": "296", - "windDesc": "Northwest", - "windDescShort": "NW", - "beaufortScale": "3", - "beaufortDescription": "Gentle breeze", - "visibility": "0.00", - "icon": "29", - "iconName": "light_snow", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/20.png", - "dayOfWeek": "1", - "weekday": "Sunday", - "utcTime": "2019-11-24T18:00:00.000-05:00" - }, - { - "daylight": "N", - "daySegment": "Night", - "description": "Cloudy. Cold.", - "skyInfo": "17", - "skyDescription": "Cloudy", - "temperature": "-1.00", - "temperatureDesc": "Cold", - "comfort": "-5.06", - "humidity": "99", - "dewPoint": "-0.70", - "precipitationProbability": "11", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "15.12", - "windDirection": "309", - "windDesc": "Northwest", - "windDescShort": "NW", - "beaufortScale": "3", - "beaufortDescription": "Gentle breeze", - "visibility": "0.00", - "icon": "17", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", - "dayOfWeek": "1", - "weekday": "Sunday", - "utcTime": "2019-11-25T00:00:00.000-05:00" - }, - { - "daylight": "D", - "daySegment": "Morning", - "description": "a mixture of sun and clouds. Chilly.", - "skyInfo": "11", - "skyDescription": "a mixture of sun and clouds", - "temperature": "0.30", - "temperatureDesc": "Chilly", - "comfort": "-4.26", - "humidity": "90", - "dewPoint": "-1.20", - "precipitationProbability": "12", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "16.20", - "windDirection": "309", - "windDesc": "Northwest", - "windDescShort": "NW", - "beaufortScale": "3", - "beaufortDescription": "Gentle breeze", - "visibility": "0.00", - "icon": "4", - "iconName": "partly_cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/3.png", - "dayOfWeek": "2", - "weekday": "Monday", - "utcTime": "2019-11-25T06:00:00.000-05:00" - } - ], - "country": "United States", - "state": "New York", - "city": "New York", - "latitude": 43.00035, - "longitude": -75.4999, - "distance": 0.00, - "timezone": -5 - } - }, - "feedCreation": "2019-11-19T22:49:09.348Z", - "metric": true -} \ No newline at end of file diff --git a/tests/fixtures/here_weather/destination_weather_forecasts_astronomy.json b/tests/fixtures/here_weather/destination_weather_forecasts_astronomy.json deleted file mode 100644 index 21285a2cef162e..00000000000000 --- a/tests/fixtures/here_weather/destination_weather_forecasts_astronomy.json +++ /dev/null @@ -1,118 +0,0 @@ -{ - "astronomy": { - "astronomy": [ - { - "sunrise": "6:55AM", - "sunset": "6:33PM", - "moonrise": "1:27PM", - "moonset": "10:58PM", - "moonPhase": 0.328, - "moonPhaseDesc": "Waxing crescent", - "iconName": "cw_waxing_crescent", - "city": "Edgewater", - "latitude": 40.82, - "longitude": -73.97, - "utcTime": "2019-10-04T00:00:00.000-04:00" - }, - { - "sunrise": "6:56AM", - "sunset": "6:31PM", - "moonrise": "2:21PM", - "moonset": "11:50PM", - "moonPhase": 0.429, - "moonPhaseDesc": "First Quarter", - "iconName": "cw_first_qtr", - "city": "Edgewater", - "latitude": 40.82, - "longitude": -73.97, - "utcTime": "2019-10-05T00:00:00.000-04:00" - }, - { - "sunrise": "6:57AM", - "sunset": "6:30PM", - "moonrise": "3:10PM", - "moonset": "*", - "moonPhase": 0.530, - "moonPhaseDesc": "First Quarter", - "iconName": "cw_first_qtr", - "city": "Edgewater", - "latitude": 40.82, - "longitude": -73.97, - "utcTime": "2019-10-06T00:00:00.000-04:00" - }, - { - "sunrise": "6:58AM", - "sunset": "6:28PM", - "moonrise": "3:51PM", - "moonset": "12:45AM", - "moonPhase": 0.627, - "moonPhaseDesc": "Waxing gibbous", - "iconName": "cw_waxing_gibbous", - "city": "Edgewater", - "latitude": 40.82, - "longitude": -73.97, - "utcTime": "2019-10-07T00:00:00.000-04:00" - }, - { - "sunrise": "6:59AM", - "sunset": "6:26PM", - "moonrise": "4:26PM", - "moonset": "1:43AM", - "moonPhase": 0.717, - "moonPhaseDesc": "Waxing gibbous", - "iconName": "cw_waxing_gibbous", - "city": "Edgewater", - "latitude": 40.82, - "longitude": -73.97, - "utcTime": "2019-10-08T00:00:00.000-04:00" - }, - { - "sunrise": "7:00AM", - "sunset": "6:25PM", - "moonrise": "4:57PM", - "moonset": "2:41AM", - "moonPhase": 0.798, - "moonPhaseDesc": "Waxing gibbous", - "iconName": "cw_waxing_gibbous", - "city": "Edgewater", - "latitude": 40.82, - "longitude": -73.97, - "utcTime": "2019-10-09T00:00:00.000-04:00" - }, - { - "sunrise": "7:01AM", - "sunset": "6:23PM", - "moonrise": "5:25PM", - "moonset": "3:40AM", - "moonPhase": 0.868, - "moonPhaseDesc": "Waxing gibbous", - "iconName": "cw_waxing_gibbous", - "city": "Edgewater", - "latitude": 40.82, - "longitude": -73.97, - "utcTime": "2019-10-10T00:00:00.000-04:00" - }, - { - "sunrise": "7:02AM", - "sunset": "6:22PM", - "moonrise": "5:51PM", - "moonset": "4:38AM", - "moonPhase": 0.924, - "moonPhaseDesc": "Waxing gibbous", - "iconName": "cw_waxing_gibbous", - "city": "Edgewater", - "latitude": 40.82, - "longitude": -73.97, - "utcTime": "2019-10-11T00:00:00.000-04:00" - } - ], - "country": "United States", - "state": "New York", - "city": "New York", - "latitude": 40.79962, - "longitude": -73.970314, - "timezone": -5 - }, - "feedCreation": "2019-10-04T14:22:46.164Z", - "metric": true -} \ No newline at end of file diff --git a/tests/fixtures/here_weather/destination_weather_forecasts_hourly.json b/tests/fixtures/here_weather/destination_weather_forecasts_hourly.json deleted file mode 100644 index be114fd2dc2cf1..00000000000000 --- a/tests/fixtures/here_weather/destination_weather_forecasts_hourly.json +++ /dev/null @@ -1,5057 +0,0 @@ -{ - "hourlyForecasts": { - "forecastLocation": { - "forecast": [ - { - "daylight": "D", - "description": "Overcast. Chilly.", - "skyInfo": "18", - "skyDescription": "Overcast", - "temperature": "4.00", - "temperatureDesc": "Chilly", - "comfort": "0.67", - "humidity": "74", - "dewPoint": "-0.30", - "precipitationProbability": "5", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "14.04", - "windDirection": "287", - "windDesc": "West", - "windDescShort": "W", - "visibility": "11.31", - "icon": "7", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/17.png", - "dayOfWeek": "3", - "weekday": "Tuesday", - "utcTime": "2019-11-19T13:00:00.000-05:00", - "localTime": "1311192019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "D", - "description": "Overcast. Chilly.", - "skyInfo": "18", - "skyDescription": "Overcast", - "temperature": "3.70", - "temperatureDesc": "Chilly", - "comfort": "0.37", - "humidity": "75", - "dewPoint": "-0.40", - "precipitationProbability": "6", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "13.68", - "windDirection": "290", - "windDesc": "West", - "windDescShort": "W", - "visibility": "12.27", - "icon": "7", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/17.png", - "dayOfWeek": "3", - "weekday": "Tuesday", - "utcTime": "2019-11-19T14:00:00.000-05:00", - "localTime": "1411192019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "D", - "description": "Overcast. Chilly.", - "skyInfo": "18", - "skyDescription": "Overcast", - "temperature": "2.90", - "temperatureDesc": "Chilly", - "comfort": "-0.48", - "humidity": "79", - "dewPoint": "-0.40", - "precipitationProbability": "6", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "12.96", - "windDirection": "294", - "windDesc": "Northwest", - "windDescShort": "NW", - "visibility": "11.59", - "icon": "7", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/17.png", - "dayOfWeek": "3", - "weekday": "Tuesday", - "utcTime": "2019-11-19T15:00:00.000-05:00", - "localTime": "1511192019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "D", - "description": "Overcast. Chilly.", - "skyInfo": "18", - "skyDescription": "Overcast", - "temperature": "2.00", - "temperatureDesc": "Chilly", - "comfort": "-1.35", - "humidity": "85", - "dewPoint": "-0.30", - "precipitationProbability": "7", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "11.88", - "windDirection": "298", - "windDesc": "Northwest", - "windDescShort": "NW", - "visibility": "11.99", - "icon": "7", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/17.png", - "dayOfWeek": "3", - "weekday": "Tuesday", - "utcTime": "2019-11-19T16:00:00.000-05:00", - "localTime": "1611192019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Overcast. Chilly.", - "skyInfo": "18", - "skyDescription": "Overcast", - "temperature": "1.70", - "temperatureDesc": "Chilly", - "comfort": "-1.30", - "humidity": "86", - "dewPoint": "-0.50", - "precipitationProbability": "8", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "32", - "airDescription": "Damp", - "windSpeed": "10.08", - "windDirection": "297", - "windDesc": "Northwest", - "windDescShort": "NW", - "visibility": "12.36", - "icon": "17", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", - "dayOfWeek": "3", - "weekday": "Tuesday", - "utcTime": "2019-11-19T17:00:00.000-05:00", - "localTime": "1711192019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Overcast. Chilly.", - "skyInfo": "18", - "skyDescription": "Overcast", - "temperature": "0.90", - "temperatureDesc": "Chilly", - "comfort": "-1.97", - "humidity": "89", - "dewPoint": "-0.80", - "precipitationProbability": "9", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "32", - "airDescription": "Damp", - "windSpeed": "9.00", - "windDirection": "291", - "windDesc": "West", - "windDescShort": "W", - "visibility": "11.77", - "icon": "17", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", - "dayOfWeek": "3", - "weekday": "Tuesday", - "utcTime": "2019-11-19T18:00:00.000-05:00", - "localTime": "1811192019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Overcast. Chilly.", - "skyInfo": "18", - "skyDescription": "Overcast", - "temperature": "0.00", - "temperatureDesc": "Chilly", - "comfort": "-2.83", - "humidity": "92", - "dewPoint": "-1.10", - "precipitationProbability": "10", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "8.28", - "windDirection": "287", - "windDesc": "West", - "windDescShort": "W", - "visibility": "12.18", - "icon": "17", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", - "dayOfWeek": "3", - "weekday": "Tuesday", - "utcTime": "2019-11-19T19:00:00.000-05:00", - "localTime": "1911192019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Overcast. Cold.", - "skyInfo": "18", - "skyDescription": "Overcast", - "temperature": "-0.30", - "temperatureDesc": "Cold", - "comfort": "-3.06", - "humidity": "92", - "dewPoint": "-1.40", - "precipitationProbability": "9", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "7.92", - "windDirection": "287", - "windDesc": "West", - "windDescShort": "W", - "visibility": "12.82", - "icon": "17", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", - "dayOfWeek": "3", - "weekday": "Tuesday", - "utcTime": "2019-11-19T20:00:00.000-05:00", - "localTime": "2011192019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Overcast. Cold.", - "skyInfo": "18", - "skyDescription": "Overcast", - "temperature": "-0.30", - "temperatureDesc": "Cold", - "comfort": "-3.06", - "humidity": "90", - "dewPoint": "-1.70", - "precipitationProbability": "9", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "7.92", - "windDirection": "290", - "windDesc": "West", - "windDescShort": "W", - "visibility": "12.48", - "icon": "17", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", - "dayOfWeek": "3", - "weekday": "Tuesday", - "utcTime": "2019-11-19T21:00:00.000-05:00", - "localTime": "2111192019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Overcast. Cold.", - "skyInfo": "18", - "skyDescription": "Overcast", - "temperature": "-0.70", - "temperatureDesc": "Cold", - "comfort": "-3.53", - "humidity": "92", - "dewPoint": "-1.90", - "precipitationProbability": "8", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "7.92", - "windDirection": "293", - "windDesc": "Northwest", - "windDescShort": "NW", - "visibility": "11.33", - "icon": "17", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", - "dayOfWeek": "3", - "weekday": "Tuesday", - "utcTime": "2019-11-19T22:00:00.000-05:00", - "localTime": "2211192019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Overcast. Cold.", - "skyInfo": "18", - "skyDescription": "Overcast", - "temperature": "-1.20", - "temperatureDesc": "Cold", - "comfort": "-4.12", - "humidity": "94", - "dewPoint": "-2.10", - "precipitationProbability": "8", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "7.92", - "windDirection": "296", - "windDesc": "Northwest", - "windDescShort": "NW", - "visibility": "12.01", - "icon": "17", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", - "dayOfWeek": "3", - "weekday": "Tuesday", - "utcTime": "2019-11-19T23:00:00.000-05:00", - "localTime": "2311192019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Overcast. Cold.", - "skyInfo": "18", - "skyDescription": "Overcast", - "temperature": "-1.20", - "temperatureDesc": "Cold", - "comfort": "-4.12", - "humidity": "94", - "dewPoint": "-2.10", - "precipitationProbability": "8", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "7.92", - "windDirection": "299", - "windDesc": "Northwest", - "windDescShort": "NW", - "visibility": "12.83", - "icon": "17", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", - "dayOfWeek": "4", - "weekday": "Wednesday", - "utcTime": "2019-11-20T00:00:00.000-05:00", - "localTime": "0011202019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Overcast. Cold.", - "skyInfo": "18", - "skyDescription": "Overcast", - "temperature": "-1.20", - "temperatureDesc": "Cold", - "comfort": "-4.12", - "humidity": "93", - "dewPoint": "-2.20", - "precipitationProbability": "7", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "7.92", - "windDirection": "301", - "windDesc": "Northwest", - "windDescShort": "NW", - "visibility": "11.30", - "icon": "17", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", - "dayOfWeek": "4", - "weekday": "Wednesday", - "utcTime": "2019-11-20T01:00:00.000-05:00", - "localTime": "0111202019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Overcast. Cold.", - "skyInfo": "18", - "skyDescription": "Overcast", - "temperature": "-1.50", - "temperatureDesc": "Cold", - "comfort": "-4.47", - "humidity": "94", - "dewPoint": "-2.40", - "precipitationProbability": "7", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "7.92", - "windDirection": "302", - "windDesc": "Northwest", - "windDescShort": "NW", - "visibility": "12.53", - "icon": "17", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", - "dayOfWeek": "4", - "weekday": "Wednesday", - "utcTime": "2019-11-20T02:00:00.000-05:00", - "localTime": "0211202019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Overcast. Cold.", - "skyInfo": "18", - "skyDescription": "Overcast", - "temperature": "-1.80", - "temperatureDesc": "Cold", - "comfort": "-4.83", - "humidity": "94", - "dewPoint": "-2.70", - "precipitationProbability": "6", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "7.92", - "windDirection": "301", - "windDesc": "Northwest", - "windDescShort": "NW", - "visibility": "12.02", - "icon": "17", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", - "dayOfWeek": "4", - "weekday": "Wednesday", - "utcTime": "2019-11-20T03:00:00.000-05:00", - "localTime": "0311202019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Overcast. Cold.", - "skyInfo": "18", - "skyDescription": "Overcast", - "temperature": "-1.90", - "temperatureDesc": "Cold", - "comfort": "-5.06", - "humidity": "94", - "dewPoint": "-2.80", - "precipitationProbability": "5", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "8.28", - "windDirection": "300", - "windDesc": "Northwest", - "windDescShort": "NW", - "visibility": "12.47", - "icon": "17", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", - "dayOfWeek": "4", - "weekday": "Wednesday", - "utcTime": "2019-11-20T04:00:00.000-05:00", - "localTime": "0411202019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Overcast. Cold.", - "skyInfo": "18", - "skyDescription": "Overcast", - "temperature": "-1.90", - "temperatureDesc": "Cold", - "comfort": "-5.06", - "humidity": "94", - "dewPoint": "-2.70", - "precipitationProbability": "7", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "8.28", - "windDirection": "297", - "windDesc": "Northwest", - "windDescShort": "NW", - "visibility": "12.62", - "icon": "17", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", - "dayOfWeek": "4", - "weekday": "Wednesday", - "utcTime": "2019-11-20T05:00:00.000-05:00", - "localTime": "0511202019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "D", - "description": "Overcast. Cold.", - "skyInfo": "18", - "skyDescription": "Overcast", - "temperature": "-1.70", - "temperatureDesc": "Cold", - "comfort": "-4.71", - "humidity": "93", - "dewPoint": "-2.70", - "precipitationProbability": "8", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "7.92", - "windDirection": "292", - "windDesc": "West", - "windDescShort": "W", - "visibility": "12.01", - "icon": "7", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/17.png", - "dayOfWeek": "4", - "weekday": "Wednesday", - "utcTime": "2019-11-20T06:00:00.000-05:00", - "localTime": "0611202019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "D", - "description": "Overcast. Cold.", - "skyInfo": "18", - "skyDescription": "Overcast", - "temperature": "-1.60", - "temperatureDesc": "Cold", - "comfort": "-4.59", - "humidity": "94", - "dewPoint": "-2.40", - "precipitationProbability": "9", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "7.92", - "windDirection": "290", - "windDesc": "West", - "windDescShort": "W", - "visibility": "12.46", - "icon": "7", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/17.png", - "dayOfWeek": "4", - "weekday": "Wednesday", - "utcTime": "2019-11-20T07:00:00.000-05:00", - "localTime": "0711202019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "D", - "description": "Light snow. Overcast. Cold.", - "skyInfo": "18", - "skyDescription": "Overcast", - "temperature": "-1.30", - "temperatureDesc": "Cold", - "comfort": "-4.58", - "humidity": "96", - "dewPoint": "-1.90", - "precipitationProbability": "26", - "precipitationDesc": "Light snow", - "rainFall": "*", - "snowFall": "0.13", - "airInfo": "*", - "airDescription": "", - "windSpeed": "9.00", - "windDirection": "298", - "windDesc": "Northwest", - "windDescShort": "NW", - "visibility": "12.12", - "icon": "29", - "iconName": "light_snow", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/20.png", - "dayOfWeek": "4", - "weekday": "Wednesday", - "utcTime": "2019-11-20T08:00:00.000-05:00", - "localTime": "0811202019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "D", - "description": "Light snow. Overcast. Cold.", - "skyInfo": "18", - "skyDescription": "Overcast", - "temperature": "-0.30", - "temperatureDesc": "Cold", - "comfort": "-3.79", - "humidity": "93", - "dewPoint": "-1.30", - "precipitationProbability": "33", - "precipitationDesc": "Light snow", - "rainFall": "*", - "snowFall": "0.10", - "airInfo": "*", - "airDescription": "", - "windSpeed": "10.44", - "windDirection": "308", - "windDesc": "Northwest", - "windDescShort": "NW", - "visibility": "11.52", - "icon": "29", - "iconName": "light_snow", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/20.png", - "dayOfWeek": "4", - "weekday": "Wednesday", - "utcTime": "2019-11-20T09:00:00.000-05:00", - "localTime": "0911202019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "D", - "description": "Light snow. Overcast. Chilly.", - "skyInfo": "18", - "skyDescription": "Overcast", - "temperature": "0.80", - "temperatureDesc": "Chilly", - "comfort": "-2.81", - "humidity": "90", - "dewPoint": "-0.70", - "precipitationProbability": "40", - "precipitationDesc": "Light snow", - "rainFall": "*", - "snowFall": "0.10", - "airInfo": "*", - "airDescription": "", - "windSpeed": "11.88", - "windDirection": "311", - "windDesc": "Northwest", - "windDescShort": "NW", - "visibility": "11.49", - "icon": "29", - "iconName": "light_snow", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/20.png", - "dayOfWeek": "4", - "weekday": "Wednesday", - "utcTime": "2019-11-20T10:00:00.000-05:00", - "localTime": "1011202019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "D", - "description": "Light snow. Overcast. Chilly.", - "skyInfo": "18", - "skyDescription": "Overcast", - "temperature": "1.60", - "temperatureDesc": "Chilly", - "comfort": "-2.27", - "humidity": "87", - "dewPoint": "-0.40", - "precipitationProbability": "41", - "precipitationDesc": "Light snow", - "rainFall": "*", - "snowFall": "0.10", - "airInfo": "32", - "airDescription": "Damp", - "windSpeed": "14.04", - "windDirection": "310", - "windDesc": "Northwest", - "windDescShort": "NW", - "visibility": "12.54", - "icon": "29", - "iconName": "light_snow", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/20.png", - "dayOfWeek": "4", - "weekday": "Wednesday", - "utcTime": "2019-11-20T11:00:00.000-05:00", - "localTime": "1111202019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "D", - "description": "Light snow. Overcast. Chilly.", - "skyInfo": "18", - "skyDescription": "Overcast", - "temperature": "2.00", - "temperatureDesc": "Chilly", - "comfort": "-2.09", - "humidity": "86", - "dewPoint": "-0.20", - "precipitationProbability": "42", - "precipitationDesc": "Light snow", - "rainFall": "*", - "snowFall": "0.10", - "airInfo": "*", - "airDescription": "", - "windSpeed": "15.84", - "windDirection": "308", - "windDesc": "Northwest", - "windDescShort": "NW", - "visibility": "12.86", - "icon": "29", - "iconName": "light_snow", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/20.png", - "dayOfWeek": "4", - "weekday": "Wednesday", - "utcTime": "2019-11-20T12:00:00.000-05:00", - "localTime": "1211202019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "D", - "description": "Sprinkles. Overcast. Chilly.", - "skyInfo": "18", - "skyDescription": "Overcast", - "temperature": "2.30", - "temperatureDesc": "Chilly", - "comfort": "-1.95", - "humidity": "85", - "dewPoint": "0.00", - "precipitationProbability": "43", - "precipitationDesc": "Sprinkles", - "rainFall": "0.01", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "17.28", - "windDirection": "308", - "windDesc": "Northwest", - "windDescShort": "NW", - "visibility": "12.36", - "icon": "18", - "iconName": "sprinkles", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/27.png", - "dayOfWeek": "4", - "weekday": "Wednesday", - "utcTime": "2019-11-20T13:00:00.000-05:00", - "localTime": "1311202019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "D", - "description": "Sprinkles. Overcast. Chilly.", - "skyInfo": "18", - "skyDescription": "Overcast", - "temperature": "2.30", - "temperatureDesc": "Chilly", - "comfort": "-2.11", - "humidity": "84", - "dewPoint": "-0.10", - "precipitationProbability": "42", - "precipitationDesc": "Sprinkles", - "rainFall": "0.01", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "18.36", - "windDirection": "308", - "windDesc": "Northwest", - "windDescShort": "NW", - "visibility": "12.31", - "icon": "18", - "iconName": "sprinkles", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/27.png", - "dayOfWeek": "4", - "weekday": "Wednesday", - "utcTime": "2019-11-20T14:00:00.000-05:00", - "localTime": "1411202019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "D", - "description": "Sprinkles. Overcast. Chilly.", - "skyInfo": "18", - "skyDescription": "Overcast", - "temperature": "2.00", - "temperatureDesc": "Chilly", - "comfort": "-2.49", - "humidity": "85", - "dewPoint": "-0.30", - "precipitationProbability": "41", - "precipitationDesc": "Sprinkles", - "rainFall": "0.01", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "18.36", - "windDirection": "310", - "windDesc": "Northwest", - "windDescShort": "NW", - "visibility": "11.66", - "icon": "18", - "iconName": "sprinkles", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/27.png", - "dayOfWeek": "4", - "weekday": "Wednesday", - "utcTime": "2019-11-20T15:00:00.000-05:00", - "localTime": "1511202019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "D", - "description": "Light snow. Overcast. Chilly.", - "skyInfo": "18", - "skyDescription": "Overcast", - "temperature": "1.70", - "temperatureDesc": "Chilly", - "comfort": "-2.75", - "humidity": "86", - "dewPoint": "-0.50", - "precipitationProbability": "40", - "precipitationDesc": "Light snow", - "rainFall": "*", - "snowFall": "0.10", - "airInfo": "*", - "airDescription": "", - "windSpeed": "17.64", - "windDirection": "311", - "windDesc": "Northwest", - "windDescShort": "NW", - "visibility": "11.31", - "icon": "29", - "iconName": "light_snow", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/20.png", - "dayOfWeek": "4", - "weekday": "Wednesday", - "utcTime": "2019-11-20T16:00:00.000-05:00", - "localTime": "1611202019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Light snow. Overcast. Chilly.", - "skyInfo": "18", - "skyDescription": "Overcast", - "temperature": "1.30", - "temperatureDesc": "Chilly", - "comfort": "-3.02", - "humidity": "87", - "dewPoint": "-0.70", - "precipitationProbability": "36", - "precipitationDesc": "Light snow", - "rainFall": "*", - "snowFall": "0.10", - "airInfo": "*", - "airDescription": "", - "windSpeed": "16.20", - "windDirection": "311", - "windDesc": "Northwest", - "windDescShort": "NW", - "visibility": "12.19", - "icon": "29", - "iconName": "light_snow", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/20.png", - "dayOfWeek": "4", - "weekday": "Wednesday", - "utcTime": "2019-11-20T17:00:00.000-05:00", - "localTime": "1711202019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Light snow. Overcast. Chilly.", - "skyInfo": "18", - "skyDescription": "Overcast", - "temperature": "0.90", - "temperatureDesc": "Chilly", - "comfort": "-3.20", - "humidity": "89", - "dewPoint": "-0.80", - "precipitationProbability": "32", - "precipitationDesc": "Light snow", - "rainFall": "*", - "snowFall": "0.10", - "airInfo": "*", - "airDescription": "", - "windSpeed": "14.40", - "windDirection": "311", - "windDesc": "Northwest", - "windDescShort": "NW", - "visibility": "12.21", - "icon": "29", - "iconName": "light_snow", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/20.png", - "dayOfWeek": "4", - "weekday": "Wednesday", - "utcTime": "2019-11-20T18:00:00.000-05:00", - "localTime": "1811202019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Light snow. Overcast. Chilly.", - "skyInfo": "18", - "skyDescription": "Overcast", - "temperature": "0.50", - "temperatureDesc": "Chilly", - "comfort": "-3.48", - "humidity": "90", - "dewPoint": "-1.00", - "precipitationProbability": "28", - "precipitationDesc": "Light snow", - "rainFall": "*", - "snowFall": "0.10", - "airInfo": "32", - "airDescription": "Damp", - "windSpeed": "13.32", - "windDirection": "311", - "windDesc": "Northwest", - "windDescShort": "NW", - "visibility": "11.59", - "icon": "29", - "iconName": "light_snow", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/20.png", - "dayOfWeek": "4", - "weekday": "Wednesday", - "utcTime": "2019-11-20T19:00:00.000-05:00", - "localTime": "1911202019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Overcast. Chilly.", - "skyInfo": "18", - "skyDescription": "Overcast", - "temperature": "0.60", - "temperatureDesc": "Chilly", - "comfort": "-3.20", - "humidity": "90", - "dewPoint": "-0.90", - "precipitationProbability": "11", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "32", - "airDescription": "Damp", - "windSpeed": "12.60", - "windDirection": "310", - "windDesc": "Northwest", - "windDescShort": "NW", - "visibility": "12.20", - "icon": "17", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", - "dayOfWeek": "4", - "weekday": "Wednesday", - "utcTime": "2019-11-20T20:00:00.000-05:00", - "localTime": "2011202019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Overcast. Chilly.", - "skyInfo": "18", - "skyDescription": "Overcast", - "temperature": "0.70", - "temperatureDesc": "Chilly", - "comfort": "-2.93", - "humidity": "90", - "dewPoint": "-0.70", - "precipitationProbability": "8", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "11.88", - "windDirection": "309", - "windDesc": "Northwest", - "windDescShort": "NW", - "visibility": "11.36", - "icon": "17", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", - "dayOfWeek": "4", - "weekday": "Wednesday", - "utcTime": "2019-11-20T21:00:00.000-05:00", - "localTime": "2111202019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Overcast. Chilly.", - "skyInfo": "18", - "skyDescription": "Overcast", - "temperature": "0.80", - "temperatureDesc": "Chilly", - "comfort": "-2.72", - "humidity": "91", - "dewPoint": "-0.50", - "precipitationProbability": "5", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "11.52", - "windDirection": "308", - "windDesc": "Northwest", - "windDescShort": "NW", - "visibility": "11.49", - "icon": "17", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", - "dayOfWeek": "4", - "weekday": "Wednesday", - "utcTime": "2019-11-20T22:00:00.000-05:00", - "localTime": "2211202019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Overcast. Chilly.", - "skyInfo": "18", - "skyDescription": "Overcast", - "temperature": "0.80", - "temperatureDesc": "Chilly", - "comfort": "-2.81", - "humidity": "92", - "dewPoint": "-0.40", - "precipitationProbability": "5", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "11.88", - "windDirection": "309", - "windDesc": "Northwest", - "windDescShort": "NW", - "visibility": "12.39", - "icon": "17", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", - "dayOfWeek": "4", - "weekday": "Wednesday", - "utcTime": "2019-11-20T23:00:00.000-05:00", - "localTime": "2311202019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Overcast. Chilly.", - "skyInfo": "18", - "skyDescription": "Overcast", - "temperature": "0.80", - "temperatureDesc": "Chilly", - "comfort": "-2.88", - "humidity": "92", - "dewPoint": "-0.30", - "precipitationProbability": "4", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "12.24", - "windDirection": "311", - "windDesc": "Northwest", - "windDescShort": "NW", - "visibility": "11.66", - "icon": "17", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", - "dayOfWeek": "5", - "weekday": "Thursday", - "utcTime": "2019-11-21T00:00:00.000-05:00", - "localTime": "0011212019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Overcast. Chilly.", - "skyInfo": "18", - "skyDescription": "Overcast", - "temperature": "0.80", - "temperatureDesc": "Chilly", - "comfort": "-2.88", - "humidity": "92", - "dewPoint": "-0.40", - "precipitationProbability": "3", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "12.24", - "windDirection": "312", - "windDesc": "Northwest", - "windDescShort": "NW", - "visibility": "11.30", - "icon": "17", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", - "dayOfWeek": "5", - "weekday": "Thursday", - "utcTime": "2019-11-21T01:00:00.000-05:00", - "localTime": "0111212019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Overcast. Chilly.", - "skyInfo": "18", - "skyDescription": "Overcast", - "temperature": "0.70", - "temperatureDesc": "Chilly", - "comfort": "-2.93", - "humidity": "92", - "dewPoint": "-0.40", - "precipitationProbability": "3", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "11.88", - "windDirection": "310", - "windDesc": "Northwest", - "windDescShort": "NW", - "visibility": "12.18", - "icon": "17", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", - "dayOfWeek": "5", - "weekday": "Thursday", - "utcTime": "2019-11-21T02:00:00.000-05:00", - "localTime": "0211212019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Overcast. Chilly.", - "skyInfo": "18", - "skyDescription": "Overcast", - "temperature": "0.60", - "temperatureDesc": "Chilly", - "comfort": "-2.88", - "humidity": "92", - "dewPoint": "-0.50", - "precipitationProbability": "3", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "11.16", - "windDirection": "305", - "windDesc": "Northwest", - "windDescShort": "NW", - "visibility": "12.73", - "icon": "17", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", - "dayOfWeek": "5", - "weekday": "Thursday", - "utcTime": "2019-11-21T03:00:00.000-05:00", - "localTime": "0311212019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Overcast. Chilly.", - "skyInfo": "18", - "skyDescription": "Overcast", - "temperature": "0.40", - "temperatureDesc": "Chilly", - "comfort": "-2.95", - "humidity": "92", - "dewPoint": "-0.70", - "precipitationProbability": "2", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "10.44", - "windDirection": "303", - "windDesc": "Northwest", - "windDescShort": "NW", - "visibility": "11.69", - "icon": "17", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", - "dayOfWeek": "5", - "weekday": "Thursday", - "utcTime": "2019-11-21T04:00:00.000-05:00", - "localTime": "0411212019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Overcast. Chilly.", - "skyInfo": "18", - "skyDescription": "Overcast", - "temperature": "0.30", - "temperatureDesc": "Chilly", - "comfort": "-2.98", - "humidity": "92", - "dewPoint": "-0.80", - "precipitationProbability": "2", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "10.08", - "windDirection": "305", - "windDesc": "Northwest", - "windDescShort": "NW", - "visibility": "12.07", - "icon": "17", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", - "dayOfWeek": "5", - "weekday": "Thursday", - "utcTime": "2019-11-21T05:00:00.000-05:00", - "localTime": "0511212019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "D", - "description": "Cloudy. Chilly.", - "skyInfo": "17", - "skyDescription": "Cloudy", - "temperature": "0.00", - "temperatureDesc": "Chilly", - "comfort": "-3.34", - "humidity": "94", - "dewPoint": "-0.90", - "precipitationProbability": "2", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "10.08", - "windDirection": "308", - "windDesc": "Northwest", - "windDescShort": "NW", - "visibility": "11.56", - "icon": "7", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/17.png", - "dayOfWeek": "5", - "weekday": "Thursday", - "utcTime": "2019-11-21T06:00:00.000-05:00", - "localTime": "0611212019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "D", - "description": "Cloudy. Cold.", - "skyInfo": "17", - "skyDescription": "Cloudy", - "temperature": "-0.10", - "temperatureDesc": "Cold", - "comfort": "-3.36", - "humidity": "94", - "dewPoint": "-1.00", - "precipitationProbability": "2", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "9.72", - "windDirection": "309", - "windDesc": "Northwest", - "windDescShort": "NW", - "visibility": "11.69", - "icon": "7", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/17.png", - "dayOfWeek": "5", - "weekday": "Thursday", - "utcTime": "2019-11-21T07:00:00.000-05:00", - "localTime": "0711212019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "D", - "description": "Cloudy. Chilly.", - "skyInfo": "17", - "skyDescription": "Cloudy", - "temperature": "0.20", - "temperatureDesc": "Chilly", - "comfort": "-2.70", - "humidity": "94", - "dewPoint": "-0.70", - "precipitationProbability": "2", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "8.64", - "windDirection": "304", - "windDesc": "Northwest", - "windDescShort": "NW", - "visibility": "11.31", - "icon": "7", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/17.png", - "dayOfWeek": "5", - "weekday": "Thursday", - "utcTime": "2019-11-21T08:00:00.000-05:00", - "localTime": "0811212019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "D", - "description": "Cloudy. Chilly.", - "skyInfo": "17", - "skyDescription": "Cloudy", - "temperature": "0.90", - "temperatureDesc": "Chilly", - "comfort": "-1.54", - "humidity": "92", - "dewPoint": "-0.20", - "precipitationProbability": "2", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "7.56", - "windDirection": "292", - "windDesc": "West", - "windDescShort": "W", - "visibility": "11.84", - "icon": "7", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/17.png", - "dayOfWeek": "5", - "weekday": "Thursday", - "utcTime": "2019-11-21T09:00:00.000-05:00", - "localTime": "0911212019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "D", - "description": "Cloudy. Chilly.", - "skyInfo": "17", - "skyDescription": "Cloudy", - "temperature": "1.70", - "temperatureDesc": "Chilly", - "comfort": "-0.25", - "humidity": "90", - "dewPoint": "0.20", - "precipitationProbability": "2", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "6.48", - "windDirection": "278", - "windDesc": "West", - "windDescShort": "W", - "visibility": "12.31", - "icon": "7", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/17.png", - "dayOfWeek": "5", - "weekday": "Thursday", - "utcTime": "2019-11-21T10:00:00.000-05:00", - "localTime": "1011212019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "D", - "description": "Cloudy. Chilly.", - "skyInfo": "17", - "skyDescription": "Cloudy", - "temperature": "2.30", - "temperatureDesc": "Chilly", - "comfort": "0.21", - "humidity": "87", - "dewPoint": "0.30", - "precipitationProbability": "3", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "7.20", - "windDirection": "267", - "windDesc": "West", - "windDescShort": "W", - "visibility": "11.88", - "icon": "7", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/17.png", - "dayOfWeek": "5", - "weekday": "Thursday", - "utcTime": "2019-11-21T11:00:00.000-05:00", - "localTime": "1111212019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "D", - "description": "Partly sunny. Chilly.", - "skyInfo": "14", - "skyDescription": "Partly sunny", - "temperature": "2.80", - "temperatureDesc": "Chilly", - "comfort": "0.57", - "humidity": "84", - "dewPoint": "0.40", - "precipitationProbability": "3", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "7.92", - "windDirection": "258", - "windDesc": "West", - "windDescShort": "W", - "visibility": "11.70", - "icon": "6", - "iconName": "mostly_cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/17.png", - "dayOfWeek": "5", - "weekday": "Thursday", - "utcTime": "2019-11-21T12:00:00.000-05:00", - "localTime": "1211212019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "D", - "description": "Partly sunny. Chilly.", - "skyInfo": "14", - "skyDescription": "Partly sunny", - "temperature": "3.20", - "temperatureDesc": "Chilly", - "comfort": "0.85", - "humidity": "82", - "dewPoint": "0.40", - "precipitationProbability": "4", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "8.64", - "windDirection": "248", - "windDesc": "West", - "windDescShort": "W", - "visibility": "14.31", - "icon": "6", - "iconName": "mostly_cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/17.png", - "dayOfWeek": "5", - "weekday": "Thursday", - "utcTime": "2019-11-21T13:00:00.000-05:00", - "localTime": "1311212019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "D", - "description": "Partly sunny. Chilly.", - "skyInfo": "14", - "skyDescription": "Partly sunny", - "temperature": "3.40", - "temperatureDesc": "Chilly", - "comfort": "1.08", - "humidity": "81", - "dewPoint": "0.40", - "precipitationProbability": "4", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "8.64", - "windDirection": "224", - "windDesc": "Southwest", - "windDescShort": "SW", - "visibility": "12.25", - "icon": "6", - "iconName": "mostly_cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/17.png", - "dayOfWeek": "5", - "weekday": "Thursday", - "utcTime": "2019-11-21T14:00:00.000-05:00", - "localTime": "1411212019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "D", - "description": "Partly sunny. Chilly.", - "skyInfo": "14", - "skyDescription": "Partly sunny", - "temperature": "3.50", - "temperatureDesc": "Chilly", - "comfort": "1.20", - "humidity": "79", - "dewPoint": "0.20", - "precipitationProbability": "5", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "8.64", - "windDirection": "180", - "windDesc": "South", - "windDescShort": "S", - "visibility": "11.57", - "icon": "6", - "iconName": "mostly_cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/17.png", - "dayOfWeek": "5", - "weekday": "Thursday", - "utcTime": "2019-11-21T15:00:00.000-05:00", - "localTime": "1511212019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "D", - "description": "Cloudy. Chilly.", - "skyInfo": "17", - "skyDescription": "Cloudy", - "temperature": "3.20", - "temperatureDesc": "Chilly", - "comfort": "0.75", - "humidity": "80", - "dewPoint": "0.10", - "precipitationProbability": "5", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "9.00", - "windDirection": "163", - "windDesc": "South", - "windDescShort": "S", - "visibility": "12.23", - "icon": "7", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/17.png", - "dayOfWeek": "5", - "weekday": "Thursday", - "utcTime": "2019-11-21T16:00:00.000-05:00", - "localTime": "1611212019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Overcast. Chilly.", - "skyInfo": "18", - "skyDescription": "Overcast", - "temperature": "3.00", - "temperatureDesc": "Chilly", - "comfort": "0.25", - "humidity": "81", - "dewPoint": "0.10", - "precipitationProbability": "9", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "10.08", - "windDirection": "167", - "windDesc": "South", - "windDescShort": "S", - "visibility": "12.66", - "icon": "17", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", - "dayOfWeek": "5", - "weekday": "Thursday", - "utcTime": "2019-11-21T17:00:00.000-05:00", - "localTime": "1711212019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Sprinkles. Overcast. Chilly.", - "skyInfo": "18", - "skyDescription": "Overcast", - "temperature": "2.70", - "temperatureDesc": "Chilly", - "comfort": "-0.51", - "humidity": "83", - "dewPoint": "0.00", - "precipitationProbability": "26", - "precipitationDesc": "Sprinkles", - "rainFall": "0.01", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "11.88", - "windDirection": "172", - "windDesc": "South", - "windDescShort": "S", - "visibility": "12.06", - "icon": "34", - "iconName": "night_sprinkles", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/22.png", - "dayOfWeek": "5", - "weekday": "Thursday", - "utcTime": "2019-11-21T18:00:00.000-05:00", - "localTime": "1811212019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Sprinkles. Overcast. Chilly.", - "skyInfo": "18", - "skyDescription": "Overcast", - "temperature": "2.60", - "temperatureDesc": "Chilly", - "comfort": "-0.98", - "humidity": "83", - "dewPoint": "0.00", - "precipitationProbability": "34", - "precipitationDesc": "Sprinkles", - "rainFall": "0.01", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "13.68", - "windDirection": "174", - "windDesc": "South", - "windDescShort": "S", - "visibility": "12.41", - "icon": "34", - "iconName": "night_sprinkles", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/22.png", - "dayOfWeek": "5", - "weekday": "Thursday", - "utcTime": "2019-11-21T19:00:00.000-05:00", - "localTime": "1911212019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Sprinkles. Overcast. Chilly.", - "skyInfo": "18", - "skyDescription": "Overcast", - "temperature": "2.80", - "temperatureDesc": "Chilly", - "comfort": "-1.16", - "humidity": "83", - "dewPoint": "0.10", - "precipitationProbability": "27", - "precipitationDesc": "Sprinkles", - "rainFall": "0.01", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "16.20", - "windDirection": "172", - "windDesc": "South", - "windDescShort": "S", - "visibility": "12.36", - "icon": "34", - "iconName": "night_sprinkles", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/22.png", - "dayOfWeek": "5", - "weekday": "Thursday", - "utcTime": "2019-11-21T20:00:00.000-05:00", - "localTime": "2011212019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Overcast. Chilly.", - "skyInfo": "18", - "skyDescription": "Overcast", - "temperature": "3.10", - "temperatureDesc": "Chilly", - "comfort": "-1.16", - "humidity": "84", - "dewPoint": "0.60", - "precipitationProbability": "9", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "18.72", - "windDirection": "170", - "windDesc": "South", - "windDescShort": "S", - "visibility": "11.91", - "icon": "17", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", - "dayOfWeek": "5", - "weekday": "Thursday", - "utcTime": "2019-11-21T21:00:00.000-05:00", - "localTime": "2111212019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Overcast. Chilly.", - "skyInfo": "18", - "skyDescription": "Overcast", - "temperature": "3.20", - "temperatureDesc": "Chilly", - "comfort": "-1.36", - "humidity": "86", - "dewPoint": "1.00", - "precipitationProbability": "6", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "21.24", - "windDirection": "170", - "windDesc": "South", - "windDescShort": "S", - "visibility": "12.85", - "icon": "17", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", - "dayOfWeek": "5", - "weekday": "Thursday", - "utcTime": "2019-11-21T22:00:00.000-05:00", - "localTime": "2211212019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Overcast. Chilly.", - "skyInfo": "18", - "skyDescription": "Overcast", - "temperature": "3.40", - "temperatureDesc": "Chilly", - "comfort": "-1.15", - "humidity": "86", - "dewPoint": "1.30", - "precipitationProbability": "12", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "21.60", - "windDirection": "175", - "windDesc": "South", - "windDescShort": "S", - "visibility": "12.35", - "icon": "17", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", - "dayOfWeek": "5", - "weekday": "Thursday", - "utcTime": "2019-11-21T23:00:00.000-05:00", - "localTime": "2311212019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Sprinkles. Overcast. Chilly.", - "skyInfo": "18", - "skyDescription": "Overcast", - "temperature": "3.60", - "temperatureDesc": "Chilly", - "comfort": "-0.81", - "humidity": "86", - "dewPoint": "1.50", - "precipitationProbability": "36", - "precipitationDesc": "Sprinkles", - "rainFall": "0.01", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "20.88", - "windDirection": "184", - "windDesc": "South", - "windDescShort": "S", - "visibility": "12.79", - "icon": "34", - "iconName": "night_sprinkles", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/22.png", - "dayOfWeek": "6", - "weekday": "Friday", - "utcTime": "2019-11-22T00:00:00.000-05:00", - "localTime": "0011222019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Sprinkles. Overcast. Chilly.", - "skyInfo": "18", - "skyDescription": "Overcast", - "temperature": "3.70", - "temperatureDesc": "Chilly", - "comfort": "-0.64", - "humidity": "88", - "dewPoint": "1.90", - "precipitationProbability": "47", - "precipitationDesc": "Sprinkles", - "rainFall": "0.01", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "20.52", - "windDirection": "192", - "windDesc": "South", - "windDescShort": "S", - "visibility": "12.01", - "icon": "34", - "iconName": "night_sprinkles", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/22.png", - "dayOfWeek": "6", - "weekday": "Friday", - "utcTime": "2019-11-22T01:00:00.000-05:00", - "localTime": "0111222019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Sprinkles. Cloudy. Chilly.", - "skyInfo": "17", - "skyDescription": "Cloudy", - "temperature": "4.00", - "temperatureDesc": "Chilly", - "comfort": "-0.39", - "humidity": "89", - "dewPoint": "2.30", - "precipitationProbability": "40", - "precipitationDesc": "Sprinkles", - "rainFall": "0.01", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "21.60", - "windDirection": "199", - "windDesc": "South", - "windDescShort": "S", - "visibility": "0.00", - "icon": "34", - "iconName": "night_sprinkles", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/22.png", - "dayOfWeek": "6", - "weekday": "Friday", - "utcTime": "2019-11-22T02:00:00.000-05:00", - "localTime": "0211222019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Sprinkles. Cloudy. Chilly.", - "skyInfo": "17", - "skyDescription": "Cloudy", - "temperature": "4.10", - "temperatureDesc": "Chilly", - "comfort": "-0.47", - "humidity": "91", - "dewPoint": "2.70", - "precipitationProbability": "33", - "precipitationDesc": "Sprinkles", - "rainFall": "0.01", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "23.40", - "windDirection": "205", - "windDesc": "Southwest", - "windDescShort": "SW", - "visibility": "0.00", - "icon": "34", - "iconName": "night_sprinkles", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/22.png", - "dayOfWeek": "6", - "weekday": "Friday", - "utcTime": "2019-11-22T03:00:00.000-05:00", - "localTime": "0311222019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Sprinkles. Cloudy. Chilly.", - "skyInfo": "17", - "skyDescription": "Cloudy", - "temperature": "4.20", - "temperatureDesc": "Chilly", - "comfort": "-0.38", - "humidity": "93", - "dewPoint": "3.10", - "precipitationProbability": "25", - "precipitationDesc": "Sprinkles", - "rainFall": "0.01", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "23.76", - "windDirection": "210", - "windDesc": "Southwest", - "windDescShort": "SW", - "visibility": "0.00", - "icon": "34", - "iconName": "night_sprinkles", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/22.png", - "dayOfWeek": "6", - "weekday": "Friday", - "utcTime": "2019-11-22T04:00:00.000-05:00", - "localTime": "0411222019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Sprinkles. Cloudy. Chilly.", - "skyInfo": "17", - "skyDescription": "Cloudy", - "temperature": "4.30", - "temperatureDesc": "Chilly", - "comfort": "-0.13", - "humidity": "95", - "dewPoint": "3.60", - "precipitationProbability": "34", - "precipitationDesc": "Sprinkles", - "rainFall": "0.01", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "22.68", - "windDirection": "211", - "windDesc": "Southwest", - "windDescShort": "SW", - "visibility": "0.00", - "icon": "34", - "iconName": "night_sprinkles", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/22.png", - "dayOfWeek": "6", - "weekday": "Friday", - "utcTime": "2019-11-22T05:00:00.000-05:00", - "localTime": "0511222019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "D", - "description": "Sprinkles. Cloudy. Chilly.", - "skyInfo": "17", - "skyDescription": "Cloudy", - "temperature": "4.40", - "temperatureDesc": "Chilly", - "comfort": "0.16", - "humidity": "98", - "dewPoint": "4.10", - "precipitationProbability": "43", - "precipitationDesc": "Sprinkles", - "rainFall": "0.01", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "21.24", - "windDirection": "211", - "windDesc": "Southwest", - "windDescShort": "SW", - "visibility": "0.00", - "icon": "18", - "iconName": "sprinkles", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/27.png", - "dayOfWeek": "6", - "weekday": "Friday", - "utcTime": "2019-11-22T06:00:00.000-05:00", - "localTime": "0611222019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "D", - "description": "Sprinkles. Cloudy. Chilly.", - "skyInfo": "17", - "skyDescription": "Cloudy", - "temperature": "4.50", - "temperatureDesc": "Chilly", - "comfort": "0.42", - "humidity": "99", - "dewPoint": "4.30", - "precipitationProbability": "52", - "precipitationDesc": "Sprinkles", - "rainFall": "0.01", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "20.16", - "windDirection": "213", - "windDesc": "Southwest", - "windDescShort": "SW", - "visibility": "0.00", - "icon": "18", - "iconName": "sprinkles", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/27.png", - "dayOfWeek": "6", - "weekday": "Friday", - "utcTime": "2019-11-22T07:00:00.000-05:00", - "localTime": "0711222019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "D", - "description": "Sprinkles. Cloudy. Chilly.", - "skyInfo": "17", - "skyDescription": "Cloudy", - "temperature": "5.80", - "temperatureDesc": "Chilly", - "comfort": "2.14", - "humidity": "95", - "dewPoint": "5.10", - "precipitationProbability": "53", - "precipitationDesc": "Sprinkles", - "rainFall": "0.03", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "19.44", - "windDirection": "221", - "windDesc": "Southwest", - "windDescShort": "SW", - "visibility": "0.00", - "icon": "18", - "iconName": "sprinkles", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/27.png", - "dayOfWeek": "6", - "weekday": "Friday", - "utcTime": "2019-11-22T08:00:00.000-05:00", - "localTime": "0811222019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "D", - "description": "Sprinkles. Cloudy. Chilly.", - "skyInfo": "17", - "skyDescription": "Cloudy", - "temperature": "6.90", - "temperatureDesc": "Chilly", - "comfort": "3.53", - "humidity": "90", - "dewPoint": "5.40", - "precipitationProbability": "54", - "precipitationDesc": "Sprinkles", - "rainFall": "0.03", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "19.44", - "windDirection": "232", - "windDesc": "Southwest", - "windDescShort": "SW", - "visibility": "0.00", - "icon": "18", - "iconName": "sprinkles", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/27.png", - "dayOfWeek": "6", - "weekday": "Friday", - "utcTime": "2019-11-22T09:00:00.000-05:00", - "localTime": "0911222019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "D", - "description": "Sprinkles. Cloudy. Chilly.", - "skyInfo": "17", - "skyDescription": "Cloudy", - "temperature": "6.90", - "temperatureDesc": "Chilly", - "comfort": "3.57", - "humidity": "90", - "dewPoint": "5.30", - "precipitationProbability": "55", - "precipitationDesc": "Sprinkles", - "rainFall": "0.03", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "19.08", - "windDirection": "244", - "windDesc": "Southwest", - "windDescShort": "SW", - "visibility": "0.00", - "icon": "18", - "iconName": "sprinkles", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/27.png", - "dayOfWeek": "6", - "weekday": "Friday", - "utcTime": "2019-11-22T10:00:00.000-05:00", - "localTime": "1011222019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "D", - "description": "Sprinkles. Overcast. Chilly.", - "skyInfo": "18", - "skyDescription": "Overcast", - "temperature": "6.30", - "temperatureDesc": "Chilly", - "comfort": "2.45", - "humidity": "89", - "dewPoint": "4.60", - "precipitationProbability": "44", - "precipitationDesc": "Sprinkles", - "rainFall": "0.01", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "22.32", - "windDirection": "255", - "windDesc": "West", - "windDescShort": "W", - "visibility": "0.00", - "icon": "18", - "iconName": "sprinkles", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/27.png", - "dayOfWeek": "6", - "weekday": "Friday", - "utcTime": "2019-11-22T11:00:00.000-05:00", - "localTime": "1111222019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "D", - "description": "Sprinkles. Overcast. Chilly.", - "skyInfo": "18", - "skyDescription": "Overcast", - "temperature": "5.50", - "temperatureDesc": "Chilly", - "comfort": "0.90", - "humidity": "87", - "dewPoint": "3.50", - "precipitationProbability": "33", - "precipitationDesc": "Sprinkles", - "rainFall": "0.01", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "27.72", - "windDirection": "264", - "windDesc": "West", - "windDescShort": "W", - "visibility": "0.00", - "icon": "18", - "iconName": "sprinkles", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/27.png", - "dayOfWeek": "6", - "weekday": "Friday", - "utcTime": "2019-11-22T12:00:00.000-05:00", - "localTime": "1211222019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "D", - "description": "Overcast. Chilly.", - "skyInfo": "18", - "skyDescription": "Overcast", - "temperature": "4.50", - "temperatureDesc": "Chilly", - "comfort": "-0.72", - "humidity": "86", - "dewPoint": "2.30", - "precipitationProbability": "10", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "13", - "airDescription": "Breezy", - "windSpeed": "31.32", - "windDirection": "269", - "windDesc": "West", - "windDescShort": "W", - "visibility": "0.00", - "icon": "7", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/17.png", - "dayOfWeek": "6", - "weekday": "Friday", - "utcTime": "2019-11-22T13:00:00.000-05:00", - "localTime": "1311222019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "D", - "description": "Overcast. Chilly.", - "skyInfo": "18", - "skyDescription": "Overcast", - "temperature": "3.80", - "temperatureDesc": "Chilly", - "comfort": "-1.79", - "humidity": "85", - "dewPoint": "1.40", - "precipitationProbability": "9", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "13", - "airDescription": "Breezy", - "windSpeed": "33.12", - "windDirection": "272", - "windDesc": "West", - "windDescShort": "W", - "visibility": "0.00", - "icon": "7", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/17.png", - "dayOfWeek": "6", - "weekday": "Friday", - "utcTime": "2019-11-22T14:00:00.000-05:00", - "localTime": "1411222019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "D", - "description": "Overcast. Chilly.", - "skyInfo": "18", - "skyDescription": "Overcast", - "temperature": "2.60", - "temperatureDesc": "Chilly", - "comfort": "-3.49", - "humidity": "85", - "dewPoint": "0.30", - "precipitationProbability": "8", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "13", - "airDescription": "Breezy", - "windSpeed": "34.56", - "windDirection": "275", - "windDesc": "West", - "windDescShort": "W", - "visibility": "0.00", - "icon": "7", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/17.png", - "dayOfWeek": "6", - "weekday": "Friday", - "utcTime": "2019-11-22T15:00:00.000-05:00", - "localTime": "1511222019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "D", - "description": "Overcast. Chilly.", - "skyInfo": "18", - "skyDescription": "Overcast", - "temperature": "1.20", - "temperatureDesc": "Chilly", - "comfort": "-5.46", - "humidity": "86", - "dewPoint": "-0.90", - "precipitationProbability": "7", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "13", - "airDescription": "Breezy", - "windSpeed": "36.00", - "windDirection": "278", - "windDesc": "West", - "windDescShort": "W", - "visibility": "0.00", - "icon": "7", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/17.png", - "dayOfWeek": "6", - "weekday": "Friday", - "utcTime": "2019-11-22T16:00:00.000-05:00", - "localTime": "1611222019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Overcast. Chilly.", - "skyInfo": "18", - "skyDescription": "Overcast", - "temperature": "0.30", - "temperatureDesc": "Chilly", - "comfort": "-6.72", - "humidity": "83", - "dewPoint": "-2.30", - "precipitationProbability": "7", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "13", - "airDescription": "Breezy", - "windSpeed": "36.72", - "windDirection": "282", - "windDesc": "West", - "windDescShort": "W", - "visibility": "0.00", - "icon": "17", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", - "dayOfWeek": "6", - "weekday": "Friday", - "utcTime": "2019-11-22T17:00:00.000-05:00", - "localTime": "1711222019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Overcast. Cold.", - "skyInfo": "18", - "skyDescription": "Overcast", - "temperature": "-1.00", - "temperatureDesc": "Cold", - "comfort": "-8.57", - "humidity": "82", - "dewPoint": "-3.80", - "precipitationProbability": "6", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "13", - "airDescription": "Breezy", - "windSpeed": "38.16", - "windDirection": "285", - "windDesc": "West", - "windDescShort": "W", - "visibility": "0.00", - "icon": "17", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", - "dayOfWeek": "6", - "weekday": "Friday", - "utcTime": "2019-11-22T18:00:00.000-05:00", - "localTime": "1811222019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Overcast. Cold.", - "skyInfo": "18", - "skyDescription": "Overcast", - "temperature": "-1.90", - "temperatureDesc": "Cold", - "comfort": "-9.74", - "humidity": "82", - "dewPoint": "-4.60", - "precipitationProbability": "6", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "13", - "airDescription": "Breezy", - "windSpeed": "37.80", - "windDirection": "288", - "windDesc": "West", - "windDescShort": "W", - "visibility": "0.00", - "icon": "17", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", - "dayOfWeek": "6", - "weekday": "Friday", - "utcTime": "2019-11-22T19:00:00.000-05:00", - "localTime": "1911222019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Overcast. Cold.", - "skyInfo": "18", - "skyDescription": "Overcast", - "temperature": "-1.20", - "temperatureDesc": "Cold", - "comfort": "-8.71", - "humidity": "76", - "dewPoint": "-5.00", - "precipitationProbability": "6", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "13", - "airDescription": "Breezy", - "windSpeed": "36.72", - "windDirection": "291", - "windDesc": "West", - "windDescShort": "W", - "visibility": "0.00", - "icon": "17", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", - "dayOfWeek": "6", - "weekday": "Friday", - "utcTime": "2019-11-22T20:00:00.000-05:00", - "localTime": "2011222019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Overcast. Cold.", - "skyInfo": "18", - "skyDescription": "Overcast", - "temperature": "-2.00", - "temperatureDesc": "Cold", - "comfort": "-9.70", - "humidity": "78", - "dewPoint": "-5.40", - "precipitationProbability": "7", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "13", - "airDescription": "Breezy", - "windSpeed": "36.00", - "windDirection": "294", - "windDesc": "Northwest", - "windDescShort": "NW", - "visibility": "0.00", - "icon": "17", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", - "dayOfWeek": "6", - "weekday": "Friday", - "utcTime": "2019-11-22T21:00:00.000-05:00", - "localTime": "2111222019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Overcast. Cold.", - "skyInfo": "18", - "skyDescription": "Overcast", - "temperature": "-2.40", - "temperatureDesc": "Cold", - "comfort": "-10.16", - "humidity": "79", - "dewPoint": "-5.60", - "precipitationProbability": "7", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "13", - "airDescription": "Breezy", - "windSpeed": "35.28", - "windDirection": "298", - "windDesc": "Northwest", - "windDescShort": "NW", - "visibility": "0.00", - "icon": "17", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", - "dayOfWeek": "6", - "weekday": "Friday", - "utcTime": "2019-11-22T22:00:00.000-05:00", - "localTime": "2211222019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Cloudy. Cold.", - "skyInfo": "17", - "skyDescription": "Cloudy", - "temperature": "-2.50", - "temperatureDesc": "Cold", - "comfort": "-10.22", - "humidity": "78", - "dewPoint": "-5.80", - "precipitationProbability": "6", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "13", - "airDescription": "Breezy", - "windSpeed": "34.56", - "windDirection": "300", - "windDesc": "Northwest", - "windDescShort": "NW", - "visibility": "0.00", - "icon": "17", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", - "dayOfWeek": "6", - "weekday": "Friday", - "utcTime": "2019-11-22T23:00:00.000-05:00", - "localTime": "2311222019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Cloudy. Cold.", - "skyInfo": "17", - "skyDescription": "Cloudy", - "temperature": "-3.10", - "temperatureDesc": "Cold", - "comfort": "-10.98", - "humidity": "81", - "dewPoint": "-6.00", - "precipitationProbability": "4", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "34.20", - "windDirection": "300", - "windDesc": "Northwest", - "windDescShort": "NW", - "visibility": "0.00", - "icon": "17", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", - "dayOfWeek": "7", - "weekday": "Saturday", - "utcTime": "2019-11-23T00:00:00.000-05:00", - "localTime": "0011232019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Cloudy. Cold.", - "skyInfo": "17", - "skyDescription": "Cloudy", - "temperature": "-3.30", - "temperatureDesc": "Cold", - "comfort": "-11.13", - "humidity": "81", - "dewPoint": "-6.10", - "precipitationProbability": "3", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "33.12", - "windDirection": "300", - "windDesc": "Northwest", - "windDescShort": "NW", - "visibility": "0.00", - "icon": "17", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", - "dayOfWeek": "7", - "weekday": "Saturday", - "utcTime": "2019-11-23T01:00:00.000-05:00", - "localTime": "0111232019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Partly cloudy. Cold.", - "skyInfo": "10", - "skyDescription": "Partly cloudy", - "temperature": "-3.30", - "temperatureDesc": "Cold", - "comfort": "-10.85", - "humidity": "80", - "dewPoint": "-6.30", - "precipitationProbability": "3", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "30.60", - "windDirection": "301", - "windDesc": "Northwest", - "windDescShort": "NW", - "visibility": "0.00", - "icon": "15", - "iconName": "night_partly_cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", - "dayOfWeek": "7", - "weekday": "Saturday", - "utcTime": "2019-11-23T02:00:00.000-05:00", - "localTime": "0211232019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Partly cloudy. Cold.", - "skyInfo": "10", - "skyDescription": "Partly cloudy", - "temperature": "-3.70", - "temperatureDesc": "Cold", - "comfort": "-10.93", - "humidity": "81", - "dewPoint": "-6.50", - "precipitationProbability": "2", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "27.00", - "windDirection": "302", - "windDesc": "Northwest", - "windDescShort": "NW", - "visibility": "0.00", - "icon": "15", - "iconName": "night_partly_cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", - "dayOfWeek": "7", - "weekday": "Saturday", - "utcTime": "2019-11-23T03:00:00.000-05:00", - "localTime": "0311232019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Partly cloudy. Cold.", - "skyInfo": "10", - "skyDescription": "Partly cloudy", - "temperature": "-4.30", - "temperatureDesc": "Cold", - "comfort": "-11.21", - "humidity": "84", - "dewPoint": "-6.60", - "precipitationProbability": "2", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "23.40", - "windDirection": "303", - "windDesc": "Northwest", - "windDescShort": "NW", - "visibility": "0.00", - "icon": "15", - "iconName": "night_partly_cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", - "dayOfWeek": "7", - "weekday": "Saturday", - "utcTime": "2019-11-23T04:00:00.000-05:00", - "localTime": "0411232019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Partly cloudy. Cold.", - "skyInfo": "10", - "skyDescription": "Partly cloudy", - "temperature": "-4.70", - "temperatureDesc": "Cold", - "comfort": "-11.14", - "humidity": "85", - "dewPoint": "-6.80", - "precipitationProbability": "2", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "19.80", - "windDirection": "304", - "windDesc": "Northwest", - "windDescShort": "NW", - "visibility": "0.00", - "icon": "15", - "iconName": "night_partly_cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", - "dayOfWeek": "7", - "weekday": "Saturday", - "utcTime": "2019-11-23T05:00:00.000-05:00", - "localTime": "0511232019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "D", - "description": "a mixture of sun and clouds. Cold.", - "skyInfo": "11", - "skyDescription": "a mixture of sun and clouds", - "temperature": "-4.80", - "temperatureDesc": "Cold", - "comfort": "-10.59", - "humidity": "85", - "dewPoint": "-7.00", - "precipitationProbability": "2", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "16.20", - "windDirection": "307", - "windDesc": "Northwest", - "windDescShort": "NW", - "visibility": "0.00", - "icon": "4", - "iconName": "partly_cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/3.png", - "dayOfWeek": "7", - "weekday": "Saturday", - "utcTime": "2019-11-23T06:00:00.000-05:00", - "localTime": "0611232019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "D", - "description": "a mixture of sun and clouds. Cold.", - "skyInfo": "11", - "skyDescription": "a mixture of sun and clouds", - "temperature": "-5.20", - "temperatureDesc": "Cold", - "comfort": "-10.35", - "humidity": "87", - "dewPoint": "-7.10", - "precipitationProbability": "2", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "12.96", - "windDirection": "310", - "windDesc": "Northwest", - "windDescShort": "NW", - "visibility": "0.00", - "icon": "4", - "iconName": "partly_cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/3.png", - "dayOfWeek": "7", - "weekday": "Saturday", - "utcTime": "2019-11-23T07:00:00.000-05:00", - "localTime": "0711232019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "D", - "description": "a mixture of sun and clouds. Cold.", - "skyInfo": "11", - "skyDescription": "a mixture of sun and clouds", - "temperature": "-5.00", - "temperatureDesc": "Cold", - "comfort": "-9.92", - "humidity": "87", - "dewPoint": "-6.90", - "precipitationProbability": "3", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "12.24", - "windDirection": "316", - "windDesc": "Northwest", - "windDescShort": "NW", - "visibility": "0.00", - "icon": "4", - "iconName": "partly_cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/3.png", - "dayOfWeek": "7", - "weekday": "Saturday", - "utcTime": "2019-11-23T08:00:00.000-05:00", - "localTime": "0811232019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "D", - "description": "a mixture of sun and clouds. Cold.", - "skyInfo": "11", - "skyDescription": "a mixture of sun and clouds", - "temperature": "-3.30", - "temperatureDesc": "Cold", - "comfort": "-8.03", - "humidity": "80", - "dewPoint": "-6.30", - "precipitationProbability": "3", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "12.96", - "windDirection": "323", - "windDesc": "Northwest", - "windDescShort": "NW", - "visibility": "0.00", - "icon": "4", - "iconName": "partly_cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/3.png", - "dayOfWeek": "7", - "weekday": "Saturday", - "utcTime": "2019-11-23T09:00:00.000-05:00", - "localTime": "0911232019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "D", - "description": "a mixture of sun and clouds. Cold.", - "skyInfo": "11", - "skyDescription": "a mixture of sun and clouds", - "temperature": "-1.10", - "temperatureDesc": "Cold", - "comfort": "-5.43", - "humidity": "72", - "dewPoint": "-5.60", - "precipitationProbability": "3", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "13.32", - "windDirection": "323", - "windDesc": "Northwest", - "windDescShort": "NW", - "visibility": "0.00", - "icon": "4", - "iconName": "partly_cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/3.png", - "dayOfWeek": "7", - "weekday": "Saturday", - "utcTime": "2019-11-23T10:00:00.000-05:00", - "localTime": "1011232019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "D", - "description": "a mixture of sun and clouds. Chilly.", - "skyInfo": "11", - "skyDescription": "a mixture of sun and clouds", - "temperature": "0.00", - "temperatureDesc": "Chilly", - "comfort": "-4.16", - "humidity": "67", - "dewPoint": "-5.40", - "precipitationProbability": "3", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "13.68", - "windDirection": "314", - "windDesc": "Northwest", - "windDescShort": "NW", - "visibility": "0.00", - "icon": "4", - "iconName": "partly_cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/3.png", - "dayOfWeek": "7", - "weekday": "Saturday", - "utcTime": "2019-11-23T11:00:00.000-05:00", - "localTime": "1111232019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "D", - "description": "a mixture of sun and clouds. Chilly.", - "skyInfo": "11", - "skyDescription": "a mixture of sun and clouds", - "temperature": "0.80", - "temperatureDesc": "Chilly", - "comfort": "-3.18", - "humidity": "64", - "dewPoint": "-5.30", - "precipitationProbability": "3", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "13.68", - "windDirection": "298", - "windDesc": "Northwest", - "windDescShort": "NW", - "visibility": "0.00", - "icon": "4", - "iconName": "partly_cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/3.png", - "dayOfWeek": "7", - "weekday": "Saturday", - "utcTime": "2019-11-23T12:00:00.000-05:00", - "localTime": "1211232019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "D", - "description": "a mixture of sun and clouds. Chilly.", - "skyInfo": "11", - "skyDescription": "a mixture of sun and clouds", - "temperature": "1.10", - "temperatureDesc": "Chilly", - "comfort": "-2.74", - "humidity": "64", - "dewPoint": "-5.00", - "precipitationProbability": "3", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "13.32", - "windDirection": "289", - "windDesc": "West", - "windDescShort": "W", - "visibility": "0.00", - "icon": "4", - "iconName": "partly_cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/3.png", - "dayOfWeek": "7", - "weekday": "Saturday", - "utcTime": "2019-11-23T13:00:00.000-05:00", - "localTime": "1311232019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "D", - "description": "a mixture of sun and clouds. Chilly.", - "skyInfo": "11", - "skyDescription": "a mixture of sun and clouds", - "temperature": "1.20", - "temperatureDesc": "Chilly", - "comfort": "-2.08", - "humidity": "66", - "dewPoint": "-4.50", - "precipitationProbability": "3", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "10.80", - "windDirection": "289", - "windDesc": "West", - "windDescShort": "W", - "visibility": "0.00", - "icon": "4", - "iconName": "partly_cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/3.png", - "dayOfWeek": "7", - "weekday": "Saturday", - "utcTime": "2019-11-23T14:00:00.000-05:00", - "localTime": "1411232019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "D", - "description": "a mixture of sun and clouds. Chilly.", - "skyInfo": "11", - "skyDescription": "a mixture of sun and clouds", - "temperature": "0.90", - "temperatureDesc": "Chilly", - "comfort": "-1.66", - "humidity": "71", - "dewPoint": "-3.90", - "precipitationProbability": "4", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "7.92", - "windDirection": "293", - "windDesc": "Northwest", - "windDescShort": "NW", - "visibility": "0.00", - "icon": "4", - "iconName": "partly_cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/3.png", - "dayOfWeek": "7", - "weekday": "Saturday", - "utcTime": "2019-11-23T15:00:00.000-05:00", - "localTime": "1511232019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "D", - "description": "a mixture of sun and clouds. Cold.", - "skyInfo": "11", - "skyDescription": "a mixture of sun and clouds", - "temperature": "-0.10", - "temperatureDesc": "Cold", - "comfort": "-2.46", - "humidity": "78", - "dewPoint": "-3.60", - "precipitationProbability": "4", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "6.84", - "windDirection": "297", - "windDesc": "Northwest", - "windDescShort": "NW", - "visibility": "0.00", - "icon": "4", - "iconName": "partly_cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/3.png", - "dayOfWeek": "7", - "weekday": "Saturday", - "utcTime": "2019-11-23T16:00:00.000-05:00", - "localTime": "1611232019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Partly cloudy. Cold.", - "skyInfo": "10", - "skyDescription": "Partly cloudy", - "temperature": "-0.50", - "temperatureDesc": "Cold", - "comfort": "-3.18", - "humidity": "81", - "dewPoint": "-3.40", - "precipitationProbability": "4", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "7.56", - "windDirection": "236", - "windDesc": "Southwest", - "windDescShort": "SW", - "visibility": "0.00", - "icon": "15", - "iconName": "night_partly_cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", - "dayOfWeek": "7", - "weekday": "Saturday", - "utcTime": "2019-11-23T17:00:00.000-05:00", - "localTime": "1711232019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Partly cloudy. Cold.", - "skyInfo": "10", - "skyDescription": "Partly cloudy", - "temperature": "-1.20", - "temperatureDesc": "Cold", - "comfort": "-4.12", - "humidity": "85", - "dewPoint": "-3.40", - "precipitationProbability": "4", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "7.92", - "windDirection": "144", - "windDesc": "Southeast", - "windDescShort": "SE", - "visibility": "0.00", - "icon": "15", - "iconName": "night_partly_cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", - "dayOfWeek": "7", - "weekday": "Saturday", - "utcTime": "2019-11-23T18:00:00.000-05:00", - "localTime": "1811232019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Partly cloudy. Cold.", - "skyInfo": "10", - "skyDescription": "Partly cloudy", - "temperature": "-1.40", - "temperatureDesc": "Cold", - "comfort": "-4.59", - "humidity": "87", - "dewPoint": "-3.30", - "precipitationProbability": "3", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "8.64", - "windDirection": "143", - "windDesc": "Southeast", - "windDescShort": "SE", - "visibility": "0.00", - "icon": "15", - "iconName": "night_partly_cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", - "dayOfWeek": "7", - "weekday": "Saturday", - "utcTime": "2019-11-23T19:00:00.000-05:00", - "localTime": "1911232019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Partly cloudy. Cold.", - "skyInfo": "10", - "skyDescription": "Partly cloudy", - "temperature": "-0.60", - "temperatureDesc": "Cold", - "comfort": "-3.75", - "humidity": "85", - "dewPoint": "-2.90", - "precipitationProbability": "3", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "9.00", - "windDirection": "149", - "windDesc": "Southeast", - "windDescShort": "SE", - "visibility": "0.00", - "icon": "15", - "iconName": "night_partly_cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", - "dayOfWeek": "7", - "weekday": "Saturday", - "utcTime": "2019-11-23T20:00:00.000-05:00", - "localTime": "2011232019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Partly cloudy. Cold.", - "skyInfo": "10", - "skyDescription": "Partly cloudy", - "temperature": "-0.20", - "temperatureDesc": "Cold", - "comfort": "-3.28", - "humidity": "85", - "dewPoint": "-2.50", - "precipitationProbability": "3", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "9.00", - "windDirection": "155", - "windDesc": "Southeast", - "windDescShort": "SE", - "visibility": "0.00", - "icon": "15", - "iconName": "night_partly_cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", - "dayOfWeek": "7", - "weekday": "Saturday", - "utcTime": "2019-11-23T21:00:00.000-05:00", - "localTime": "2111232019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Partly cloudy. Cold.", - "skyInfo": "10", - "skyDescription": "Partly cloudy", - "temperature": "-0.70", - "temperatureDesc": "Cold", - "comfort": "-3.87", - "humidity": "89", - "dewPoint": "-2.30", - "precipitationProbability": "3", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "9.00", - "windDirection": "153", - "windDesc": "Southeast", - "windDescShort": "SE", - "visibility": "0.00", - "icon": "15", - "iconName": "night_partly_cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", - "dayOfWeek": "7", - "weekday": "Saturday", - "utcTime": "2019-11-23T22:00:00.000-05:00", - "localTime": "2211232019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Partly cloudy. Cold.", - "skyInfo": "10", - "skyDescription": "Partly cloudy", - "temperature": "-0.80", - "temperatureDesc": "Cold", - "comfort": "-3.99", - "humidity": "90", - "dewPoint": "-2.20", - "precipitationProbability": "5", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "9.00", - "windDirection": "145", - "windDesc": "Southeast", - "windDescShort": "SE", - "visibility": "0.00", - "icon": "15", - "iconName": "night_partly_cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", - "dayOfWeek": "7", - "weekday": "Saturday", - "utcTime": "2019-11-23T23:00:00.000-05:00", - "localTime": "2311232019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Partly cloudy. Cold.", - "skyInfo": "10", - "skyDescription": "Partly cloudy", - "temperature": "-0.70", - "temperatureDesc": "Cold", - "comfort": "-3.53", - "humidity": "92", - "dewPoint": "-1.90", - "precipitationProbability": "7", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "7.92", - "windDirection": "132", - "windDesc": "Southeast", - "windDescShort": "SE", - "visibility": "0.00", - "icon": "15", - "iconName": "night_partly_cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", - "dayOfWeek": "1", - "weekday": "Sunday", - "utcTime": "2019-11-24T00:00:00.000-05:00", - "localTime": "0011242019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Partly cloudy. Cold.", - "skyInfo": "10", - "skyDescription": "Partly cloudy", - "temperature": "-0.90", - "temperatureDesc": "Cold", - "comfort": "-3.52", - "humidity": "92", - "dewPoint": "-2.00", - "precipitationProbability": "9", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "7.20", - "windDirection": "115", - "windDesc": "Southeast", - "windDescShort": "SE", - "visibility": "0.00", - "icon": "15", - "iconName": "night_partly_cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", - "dayOfWeek": "1", - "weekday": "Sunday", - "utcTime": "2019-11-24T01:00:00.000-05:00", - "localTime": "0111242019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Partly cloudy. Cold.", - "skyInfo": "10", - "skyDescription": "Partly cloudy", - "temperature": "-1.30", - "temperatureDesc": "Cold", - "comfort": "-3.99", - "humidity": "94", - "dewPoint": "-2.20", - "precipitationProbability": "8", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "7.20", - "windDirection": "96", - "windDesc": "East", - "windDescShort": "E", - "visibility": "0.00", - "icon": "15", - "iconName": "night_partly_cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", - "dayOfWeek": "1", - "weekday": "Sunday", - "utcTime": "2019-11-24T02:00:00.000-05:00", - "localTime": "0211242019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Partly cloudy. Cold.", - "skyInfo": "10", - "skyDescription": "Partly cloudy", - "temperature": "-1.30", - "temperatureDesc": "Cold", - "comfort": "-3.99", - "humidity": "93", - "dewPoint": "-2.30", - "precipitationProbability": "8", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "7.20", - "windDirection": "77", - "windDesc": "East", - "windDescShort": "E", - "visibility": "0.00", - "icon": "15", - "iconName": "night_partly_cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", - "dayOfWeek": "1", - "weekday": "Sunday", - "utcTime": "2019-11-24T03:00:00.000-05:00", - "localTime": "0311242019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Partly cloudy. Cold.", - "skyInfo": "10", - "skyDescription": "Partly cloudy", - "temperature": "-1.10", - "temperatureDesc": "Cold", - "comfort": "-3.88", - "humidity": "91", - "dewPoint": "-2.40", - "precipitationProbability": "7", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "7.56", - "windDirection": "60", - "windDesc": "Northeast", - "windDescShort": "NE", - "visibility": "0.00", - "icon": "15", - "iconName": "night_partly_cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", - "dayOfWeek": "1", - "weekday": "Sunday", - "utcTime": "2019-11-24T04:00:00.000-05:00", - "localTime": "0411242019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Partly cloudy. Cold.", - "skyInfo": "10", - "skyDescription": "Partly cloudy", - "temperature": "-1.20", - "temperatureDesc": "Cold", - "comfort": "-4.12", - "humidity": "92", - "dewPoint": "-2.40", - "precipitationProbability": "6", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "7.92", - "windDirection": "41", - "windDesc": "Northeast", - "windDescShort": "NE", - "visibility": "0.00", - "icon": "15", - "iconName": "night_partly_cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", - "dayOfWeek": "1", - "weekday": "Sunday", - "utcTime": "2019-11-24T05:00:00.000-05:00", - "localTime": "0511242019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "D", - "description": "a mixture of sun and clouds. Cold.", - "skyInfo": "11", - "skyDescription": "a mixture of sun and clouds", - "temperature": "-1.50", - "temperatureDesc": "Cold", - "comfort": "-4.59", - "humidity": "93", - "dewPoint": "-2.50", - "precipitationProbability": "5", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "8.28", - "windDirection": "23", - "windDesc": "Northeast", - "windDescShort": "NE", - "visibility": "0.00", - "icon": "4", - "iconName": "partly_cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/3.png", - "dayOfWeek": "1", - "weekday": "Sunday", - "utcTime": "2019-11-24T06:00:00.000-05:00", - "localTime": "0611242019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "D", - "description": "a mixture of sun and clouds. Cold.", - "skyInfo": "11", - "skyDescription": "a mixture of sun and clouds", - "temperature": "-1.60", - "temperatureDesc": "Cold", - "comfort": "-4.71", - "humidity": "94", - "dewPoint": "-2.50", - "precipitationProbability": "5", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "8.28", - "windDirection": "7", - "windDesc": "North", - "windDescShort": "N", - "visibility": "0.00", - "icon": "4", - "iconName": "partly_cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/3.png", - "dayOfWeek": "1", - "weekday": "Sunday", - "utcTime": "2019-11-24T07:00:00.000-05:00", - "localTime": "0711242019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "D", - "description": "a mixture of sun and clouds. Cold.", - "skyInfo": "11", - "skyDescription": "a mixture of sun and clouds", - "temperature": "-1.40", - "temperatureDesc": "Cold", - "comfort": "-4.59", - "humidity": "94", - "dewPoint": "-2.20", - "precipitationProbability": "4", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "8.64", - "windDirection": "354", - "windDesc": "North", - "windDescShort": "N", - "visibility": "0.00", - "icon": "4", - "iconName": "partly_cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/3.png", - "dayOfWeek": "1", - "weekday": "Sunday", - "utcTime": "2019-11-24T08:00:00.000-05:00", - "localTime": "0811242019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "D", - "description": "a mixture of sun and clouds. Cold.", - "skyInfo": "11", - "skyDescription": "a mixture of sun and clouds", - "temperature": "-0.80", - "temperatureDesc": "Cold", - "comfort": "-3.88", - "humidity": "92", - "dewPoint": "-1.90", - "precipitationProbability": "3", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "8.64", - "windDirection": "339", - "windDesc": "North", - "windDescShort": "N", - "visibility": "0.00", - "icon": "4", - "iconName": "partly_cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/3.png", - "dayOfWeek": "1", - "weekday": "Sunday", - "utcTime": "2019-11-24T09:00:00.000-05:00", - "localTime": "0911242019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "D", - "description": "a mixture of sun and clouds. Cold.", - "skyInfo": "11", - "skyDescription": "a mixture of sun and clouds", - "temperature": "-0.50", - "temperatureDesc": "Cold", - "comfort": "-3.53", - "humidity": "92", - "dewPoint": "-1.60", - "precipitationProbability": "2", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "8.64", - "windDirection": "326", - "windDesc": "Northwest", - "windDescShort": "NW", - "visibility": "0.00", - "icon": "4", - "iconName": "partly_cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/3.png", - "dayOfWeek": "1", - "weekday": "Sunday", - "utcTime": "2019-11-24T10:00:00.000-05:00", - "localTime": "1011242019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "D", - "description": "a mixture of sun and clouds. Cold.", - "skyInfo": "11", - "skyDescription": "a mixture of sun and clouds", - "temperature": "-0.30", - "temperatureDesc": "Cold", - "comfort": "-4.05", - "humidity": "91", - "dewPoint": "-1.60", - "precipitationProbability": "4", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "11.52", - "windDirection": "312", - "windDesc": "Northwest", - "windDescShort": "NW", - "visibility": "0.00", - "icon": "4", - "iconName": "partly_cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/3.png", - "dayOfWeek": "1", - "weekday": "Sunday", - "utcTime": "2019-11-24T11:00:00.000-05:00", - "localTime": "1111242019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "D", - "description": "a mixture of sun and clouds. Chilly.", - "skyInfo": "11", - "skyDescription": "a mixture of sun and clouds", - "temperature": "0.00", - "temperatureDesc": "Chilly", - "comfort": "-4.76", - "humidity": "89", - "dewPoint": "-1.60", - "precipitationProbability": "6", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "16.92", - "windDirection": "304", - "windDesc": "Northwest", - "windDescShort": "NW", - "visibility": "0.00", - "icon": "4", - "iconName": "partly_cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/3.png", - "dayOfWeek": "1", - "weekday": "Sunday", - "utcTime": "2019-11-24T12:00:00.000-05:00", - "localTime": "1211242019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "D", - "description": "a mixture of sun and clouds. Chilly.", - "skyInfo": "11", - "skyDescription": "a mixture of sun and clouds", - "temperature": "0.10", - "temperatureDesc": "Chilly", - "comfort": "-5.19", - "humidity": "88", - "dewPoint": "-1.70", - "precipitationProbability": "8", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "20.52", - "windDirection": "304", - "windDesc": "Northwest", - "windDescShort": "NW", - "visibility": "0.00", - "icon": "4", - "iconName": "partly_cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/3.png", - "dayOfWeek": "1", - "weekday": "Sunday", - "utcTime": "2019-11-24T13:00:00.000-05:00", - "localTime": "1311242019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "D", - "description": "a mixture of sun and clouds. Cold.", - "skyInfo": "11", - "skyDescription": "a mixture of sun and clouds", - "temperature": "-0.10", - "temperatureDesc": "Cold", - "comfort": "-5.55", - "humidity": "87", - "dewPoint": "-2.00", - "precipitationProbability": "9", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "21.24", - "windDirection": "305", - "windDesc": "Northwest", - "windDescShort": "NW", - "visibility": "0.00", - "icon": "4", - "iconName": "partly_cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/3.png", - "dayOfWeek": "1", - "weekday": "Sunday", - "utcTime": "2019-11-24T14:00:00.000-05:00", - "localTime": "1411242019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "D", - "description": "a mixture of sun and clouds. Cold.", - "skyInfo": "11", - "skyDescription": "a mixture of sun and clouds", - "temperature": "-0.10", - "temperatureDesc": "Cold", - "comfort": "-5.55", - "humidity": "86", - "dewPoint": "-2.20", - "precipitationProbability": "9", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "21.24", - "windDirection": "307", - "windDesc": "Northwest", - "windDescShort": "NW", - "visibility": "0.00", - "icon": "4", - "iconName": "partly_cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/3.png", - "dayOfWeek": "1", - "weekday": "Sunday", - "utcTime": "2019-11-24T15:00:00.000-05:00", - "localTime": "1511242019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "D", - "description": "a mixture of sun and clouds. Cold.", - "skyInfo": "11", - "skyDescription": "a mixture of sun and clouds", - "temperature": "-0.20", - "temperatureDesc": "Cold", - "comfort": "-5.57", - "humidity": "85", - "dewPoint": "-2.40", - "precipitationProbability": "10", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "20.52", - "windDirection": "307", - "windDesc": "Northwest", - "windDescShort": "NW", - "visibility": "0.00", - "icon": "4", - "iconName": "partly_cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/3.png", - "dayOfWeek": "1", - "weekday": "Sunday", - "utcTime": "2019-11-24T16:00:00.000-05:00", - "localTime": "1611242019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Cloudy. Cold.", - "skyInfo": "17", - "skyDescription": "Cloudy", - "temperature": "-0.10", - "temperatureDesc": "Cold", - "comfort": "-5.17", - "humidity": "87", - "dewPoint": "-2.10", - "precipitationProbability": "11", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "18.72", - "windDirection": "305", - "windDesc": "Northwest", - "windDescShort": "NW", - "visibility": "0.00", - "icon": "17", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", - "dayOfWeek": "1", - "weekday": "Sunday", - "utcTime": "2019-11-24T17:00:00.000-05:00", - "localTime": "1711242019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Cloudy. Chilly.", - "skyInfo": "17", - "skyDescription": "Cloudy", - "temperature": "0.60", - "temperatureDesc": "Chilly", - "comfort": "-3.89", - "humidity": "86", - "dewPoint": "-1.50", - "precipitationProbability": "6", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "16.20", - "windDirection": "300", - "windDesc": "Northwest", - "windDescShort": "NW", - "visibility": "0.00", - "icon": "17", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", - "dayOfWeek": "1", - "weekday": "Sunday", - "utcTime": "2019-11-24T18:00:00.000-05:00", - "localTime": "1811242019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Cloudy. Chilly.", - "skyInfo": "17", - "skyDescription": "Cloudy", - "temperature": "0.50", - "temperatureDesc": "Chilly", - "comfort": "-3.76", - "humidity": "87", - "dewPoint": "-1.40", - "precipitationProbability": "7", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "14.76", - "windDirection": "297", - "windDesc": "Northwest", - "windDescShort": "NW", - "visibility": "0.00", - "icon": "17", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", - "dayOfWeek": "1", - "weekday": "Sunday", - "utcTime": "2019-11-24T19:00:00.000-05:00", - "localTime": "1911242019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Light snow. Cloudy. Chilly.", - "skyInfo": "17", - "skyDescription": "Cloudy", - "temperature": "0.20", - "temperatureDesc": "Chilly", - "comfort": "-4.19", - "humidity": "90", - "dewPoint": "-1.20", - "precipitationProbability": "26", - "precipitationDesc": "Light snow", - "rainFall": "*", - "snowFall": "0.10", - "airInfo": "*", - "airDescription": "", - "windSpeed": "15.12", - "windDirection": "296", - "windDesc": "Northwest", - "windDescShort": "NW", - "visibility": "0.00", - "icon": "29", - "iconName": "light_snow", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/20.png", - "dayOfWeek": "1", - "weekday": "Sunday", - "utcTime": "2019-11-24T20:00:00.000-05:00", - "localTime": "2011242019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Light snow. Cloudy. Chilly.", - "skyInfo": "17", - "skyDescription": "Cloudy", - "temperature": "0.30", - "temperatureDesc": "Chilly", - "comfort": "-4.32", - "humidity": "93", - "dewPoint": "-0.70", - "precipitationProbability": "25", - "precipitationDesc": "Light snow", - "rainFall": "*", - "snowFall": "0.10", - "airInfo": "*", - "airDescription": "", - "windSpeed": "16.56", - "windDirection": "296", - "windDesc": "Northwest", - "windDescShort": "NW", - "visibility": "0.00", - "icon": "29", - "iconName": "light_snow", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/20.png", - "dayOfWeek": "1", - "weekday": "Sunday", - "utcTime": "2019-11-24T21:00:00.000-05:00", - "localTime": "2111242019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Cloudy. Chilly.", - "skyInfo": "17", - "skyDescription": "Cloudy", - "temperature": "0.10", - "temperatureDesc": "Chilly", - "comfort": "-4.63", - "humidity": "97", - "dewPoint": "-0.30", - "precipitationProbability": "11", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "16.92", - "windDirection": "295", - "windDesc": "Northwest", - "windDescShort": "NW", - "visibility": "0.00", - "icon": "17", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", - "dayOfWeek": "1", - "weekday": "Sunday", - "utcTime": "2019-11-24T22:00:00.000-05:00", - "localTime": "2211242019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Cloudy. Cold.", - "skyInfo": "17", - "skyDescription": "Cloudy", - "temperature": "-0.10", - "temperatureDesc": "Cold", - "comfort": "-4.49", - "humidity": "97", - "dewPoint": "-0.50", - "precipitationProbability": "6", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "14.76", - "windDirection": "296", - "windDesc": "Northwest", - "windDescShort": "NW", - "visibility": "0.00", - "icon": "17", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", - "dayOfWeek": "1", - "weekday": "Sunday", - "utcTime": "2019-11-24T23:00:00.000-05:00", - "localTime": "2311242019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Cloudy. Cold.", - "skyInfo": "17", - "skyDescription": "Cloudy", - "temperature": "-0.50", - "temperatureDesc": "Cold", - "comfort": "-4.38", - "humidity": "98", - "dewPoint": "-0.80", - "precipitationProbability": "6", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "11.88", - "windDirection": "299", - "windDesc": "Northwest", - "windDescShort": "NW", - "visibility": "0.00", - "icon": "17", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", - "dayOfWeek": "2", - "weekday": "Monday", - "utcTime": "2019-11-25T00:00:00.000-05:00", - "localTime": "0011252019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Cloudy. Cold.", - "skyInfo": "17", - "skyDescription": "Cloudy", - "temperature": "-0.80", - "temperatureDesc": "Cold", - "comfort": "-4.19", - "humidity": "99", - "dewPoint": "-1.00", - "precipitationProbability": "7", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "9.72", - "windDirection": "303", - "windDesc": "Northwest", - "windDescShort": "NW", - "visibility": "0.00", - "icon": "17", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", - "dayOfWeek": "2", - "weekday": "Monday", - "utcTime": "2019-11-25T01:00:00.000-05:00", - "localTime": "0111252019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Light snow. Cloudy. Cold.", - "skyInfo": "17", - "skyDescription": "Cloudy", - "temperature": "-0.70", - "temperatureDesc": "Cold", - "comfort": "-4.62", - "humidity": "99", - "dewPoint": "-0.90", - "precipitationProbability": "26", - "precipitationDesc": "Light snow", - "rainFall": "*", - "snowFall": "0.10", - "airInfo": "*", - "airDescription": "", - "windSpeed": "11.88", - "windDirection": "306", - "windDesc": "Northwest", - "windDescShort": "NW", - "visibility": "0.00", - "icon": "29", - "iconName": "light_snow", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/20.png", - "dayOfWeek": "2", - "weekday": "Monday", - "utcTime": "2019-11-25T02:00:00.000-05:00", - "localTime": "0211252019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Cloudy. Cold.", - "skyInfo": "17", - "skyDescription": "Cloudy", - "temperature": "-0.50", - "temperatureDesc": "Cold", - "comfort": "-5.06", - "humidity": "99", - "dewPoint": "-0.70", - "precipitationProbability": "11", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "15.12", - "windDirection": "309", - "windDesc": "Northwest", - "windDescShort": "NW", - "visibility": "0.00", - "icon": "17", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", - "dayOfWeek": "2", - "weekday": "Monday", - "utcTime": "2019-11-25T03:00:00.000-05:00", - "localTime": "0311252019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Cloudy. Cold.", - "skyInfo": "17", - "skyDescription": "Cloudy", - "temperature": "-0.30", - "temperatureDesc": "Cold", - "comfort": "-5.07", - "humidity": "99", - "dewPoint": "-0.50", - "precipitationProbability": "10", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "16.56", - "windDirection": "311", - "windDesc": "Northwest", - "windDescShort": "NW", - "visibility": "0.00", - "icon": "17", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", - "dayOfWeek": "2", - "weekday": "Monday", - "utcTime": "2019-11-25T04:00:00.000-05:00", - "localTime": "0411252019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Cloudy. Cold.", - "skyInfo": "17", - "skyDescription": "Cloudy", - "temperature": "-0.20", - "temperatureDesc": "Cold", - "comfort": "-4.69", - "humidity": "96", - "dewPoint": "-0.70", - "precipitationProbability": "11", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "15.12", - "windDirection": "311", - "windDesc": "Northwest", - "windDescShort": "NW", - "visibility": "0.00", - "icon": "17", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", - "dayOfWeek": "2", - "weekday": "Monday", - "utcTime": "2019-11-25T05:00:00.000-05:00", - "localTime": "0511252019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "D", - "description": "a mixture of sun and clouds. Cold.", - "skyInfo": "11", - "skyDescription": "a mixture of sun and clouds", - "temperature": "-0.80", - "temperatureDesc": "Cold", - "comfort": "-4.99", - "humidity": "99", - "dewPoint": "-1.00", - "precipitationProbability": "12", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "12.96", - "windDirection": "310", - "windDesc": "Northwest", - "windDescShort": "NW", - "visibility": "0.00", - "icon": "4", - "iconName": "partly_cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/3.png", - "dayOfWeek": "2", - "weekday": "Monday", - "utcTime": "2019-11-25T06:00:00.000-05:00", - "localTime": "0611252019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "D", - "description": "Light snow. a mixture of sun and clouds. Cold.", - "skyInfo": "11", - "skyDescription": "a mixture of sun and clouds", - "temperature": "-1.00", - "temperatureDesc": "Cold", - "comfort": "-4.98", - "humidity": "99", - "dewPoint": "-1.20", - "precipitationProbability": "25", - "precipitationDesc": "Light snow", - "rainFall": "*", - "snowFall": "0.16", - "airInfo": "*", - "airDescription": "", - "windSpeed": "11.88", - "windDirection": "308", - "windDesc": "Northwest", - "windDescShort": "NW", - "visibility": "0.00", - "icon": "29", - "iconName": "light_snow", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/20.png", - "dayOfWeek": "2", - "weekday": "Monday", - "utcTime": "2019-11-25T07:00:00.000-05:00", - "localTime": "0711252019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "D", - "description": "Light snow. a mixture of sun and clouds. Cold.", - "skyInfo": "11", - "skyDescription": "a mixture of sun and clouds", - "temperature": "-0.70", - "temperatureDesc": "Cold", - "comfort": "-4.94", - "humidity": "97", - "dewPoint": "-1.10", - "precipitationProbability": "25", - "precipitationDesc": "Light snow", - "rainFall": "*", - "snowFall": "0.10", - "airInfo": "*", - "airDescription": "", - "windSpeed": "13.32", - "windDirection": "309", - "windDesc": "Northwest", - "windDescShort": "NW", - "visibility": "0.00", - "icon": "29", - "iconName": "light_snow", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/20.png", - "dayOfWeek": "2", - "weekday": "Monday", - "utcTime": "2019-11-25T08:00:00.000-05:00", - "localTime": "0811252019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "D", - "description": "a mixture of sun and clouds. Chilly.", - "skyInfo": "11", - "skyDescription": "a mixture of sun and clouds", - "temperature": "0.30", - "temperatureDesc": "Chilly", - "comfort": "-4.26", - "humidity": "90", - "dewPoint": "-1.20", - "precipitationProbability": "12", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "16.20", - "windDirection": "309", - "windDesc": "Northwest", - "windDescShort": "NW", - "visibility": "0.00", - "icon": "4", - "iconName": "partly_cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/3.png", - "dayOfWeek": "2", - "weekday": "Monday", - "utcTime": "2019-11-25T09:00:00.000-05:00", - "localTime": "0911252019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "D", - "description": "a mixture of sun and clouds. Chilly.", - "skyInfo": "11", - "skyDescription": "a mixture of sun and clouds", - "temperature": "1.40", - "temperatureDesc": "Chilly", - "comfort": "-3.13", - "humidity": "84", - "dewPoint": "-1.10", - "precipitationProbability": "11", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "17.64", - "windDirection": "309", - "windDesc": "Northwest", - "windDescShort": "NW", - "visibility": "0.00", - "icon": "4", - "iconName": "partly_cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/3.png", - "dayOfWeek": "2", - "weekday": "Monday", - "utcTime": "2019-11-25T10:00:00.000-05:00", - "localTime": "1011252019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "D", - "description": "Sprinkles. a mixture of sun and clouds. Chilly.", - "skyInfo": "11", - "skyDescription": "a mixture of sun and clouds", - "temperature": "2.10", - "temperatureDesc": "Chilly", - "comfort": "-2.14", - "humidity": "81", - "dewPoint": "-0.80", - "precipitationProbability": "28", - "precipitationDesc": "Sprinkles", - "rainFall": "0.01", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "16.92", - "windDirection": "307", - "windDesc": "Northwest", - "windDescShort": "NW", - "visibility": "0.00", - "icon": "18", - "iconName": "sprinkles", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/27.png", - "dayOfWeek": "2", - "weekday": "Monday", - "utcTime": "2019-11-25T11:00:00.000-05:00", - "localTime": "1111252019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "D", - "description": "Sprinkles. a mixture of sun and clouds. Chilly.", - "skyInfo": "11", - "skyDescription": "a mixture of sun and clouds", - "temperature": "2.40", - "temperatureDesc": "Chilly", - "comfort": "-1.60", - "humidity": "83", - "dewPoint": "-0.30", - "precipitationProbability": "33", - "precipitationDesc": "Sprinkles", - "rainFall": "0.01", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "15.84", - "windDirection": "305", - "windDesc": "Northwest", - "windDescShort": "NW", - "visibility": "0.00", - "icon": "18", - "iconName": "sprinkles", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/27.png", - "dayOfWeek": "2", - "weekday": "Monday", - "utcTime": "2019-11-25T12:00:00.000-05:00", - "localTime": "1211252019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "D", - "description": "Sprinkles. a mixture of sun and clouds. Chilly.", - "skyInfo": "11", - "skyDescription": "a mixture of sun and clouds", - "temperature": "2.60", - "temperatureDesc": "Chilly", - "comfort": "-1.11", - "humidity": "84", - "dewPoint": "0.20", - "precipitationProbability": "37", - "precipitationDesc": "Sprinkles", - "rainFall": "0.01", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "14.40", - "windDirection": "303", - "windDesc": "Northwest", - "windDescShort": "NW", - "visibility": "0.00", - "icon": "18", - "iconName": "sprinkles", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/27.png", - "dayOfWeek": "2", - "weekday": "Monday", - "utcTime": "2019-11-25T13:00:00.000-05:00", - "localTime": "1311252019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "D", - "description": "a mixture of sun and clouds. Chilly.", - "skyInfo": "11", - "skyDescription": "a mixture of sun and clouds", - "temperature": "2.40", - "temperatureDesc": "Chilly", - "comfort": "-1.15", - "humidity": "87", - "dewPoint": "0.50", - "precipitationProbability": "10", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "13.32", - "windDirection": "299", - "windDesc": "Northwest", - "windDescShort": "NW", - "visibility": "0.00", - "icon": "4", - "iconName": "partly_cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/3.png", - "dayOfWeek": "2", - "weekday": "Monday", - "utcTime": "2019-11-25T14:00:00.000-05:00", - "localTime": "1411252019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "D", - "description": "a mixture of sun and clouds. Chilly.", - "skyInfo": "11", - "skyDescription": "a mixture of sun and clouds", - "temperature": "2.20", - "temperatureDesc": "Chilly", - "comfort": "-1.19", - "humidity": "89", - "dewPoint": "0.60", - "precipitationProbability": "10", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "12.24", - "windDirection": "295", - "windDesc": "Northwest", - "windDescShort": "NW", - "visibility": "0.00", - "icon": "4", - "iconName": "partly_cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/3.png", - "dayOfWeek": "2", - "weekday": "Monday", - "utcTime": "2019-11-25T15:00:00.000-05:00", - "localTime": "1511252019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "D", - "description": "a mixture of sun and clouds. Chilly.", - "skyInfo": "11", - "skyDescription": "a mixture of sun and clouds", - "temperature": "1.60", - "temperatureDesc": "Chilly", - "comfort": "-1.68", - "humidity": "93", - "dewPoint": "0.50", - "precipitationProbability": "11", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "11.16", - "windDirection": "290", - "windDesc": "West", - "windDescShort": "W", - "visibility": "0.00", - "icon": "4", - "iconName": "partly_cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/3.png", - "dayOfWeek": "2", - "weekday": "Monday", - "utcTime": "2019-11-25T16:00:00.000-05:00", - "localTime": "1611252019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Cloudy. Chilly.", - "skyInfo": "17", - "skyDescription": "Cloudy", - "temperature": "1.20", - "temperatureDesc": "Chilly", - "comfort": "-1.81", - "humidity": "92", - "dewPoint": "0.00", - "precipitationProbability": "10", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "9.72", - "windDirection": "277", - "windDesc": "West", - "windDescShort": "W", - "visibility": "0.00", - "icon": "17", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", - "dayOfWeek": "2", - "weekday": "Monday", - "utcTime": "2019-11-25T17:00:00.000-05:00", - "localTime": "1711252019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Cloudy. Chilly.", - "skyInfo": "17", - "skyDescription": "Cloudy", - "temperature": "0.30", - "temperatureDesc": "Chilly", - "comfort": "-2.79", - "humidity": "91", - "dewPoint": "-1.00", - "precipitationProbability": "9", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "9.36", - "windDirection": "255", - "windDesc": "West", - "windDescShort": "W", - "visibility": "0.00", - "icon": "17", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", - "dayOfWeek": "2", - "weekday": "Monday", - "utcTime": "2019-11-25T18:00:00.000-05:00", - "localTime": "1811252019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Cloudy. Cold.", - "skyInfo": "17", - "skyDescription": "Cloudy", - "temperature": "-0.80", - "temperatureDesc": "Cold", - "comfort": "-4.09", - "humidity": "94", - "dewPoint": "-1.70", - "precipitationProbability": "7", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "9.36", - "windDirection": "238", - "windDesc": "Southwest", - "windDescShort": "SW", - "visibility": "0.00", - "icon": "17", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", - "dayOfWeek": "2", - "weekday": "Monday", - "utcTime": "2019-11-25T19:00:00.000-05:00", - "localTime": "1911252019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Cloudy. Cold.", - "skyInfo": "17", - "skyDescription": "Cloudy", - "temperature": "-0.90", - "temperatureDesc": "Cold", - "comfort": "-4.21", - "humidity": "95", - "dewPoint": "-1.60", - "precipitationProbability": "6", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "9.36", - "windDirection": "239", - "windDesc": "Southwest", - "windDescShort": "SW", - "visibility": "0.00", - "icon": "17", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", - "dayOfWeek": "2", - "weekday": "Monday", - "utcTime": "2019-11-25T20:00:00.000-05:00", - "localTime": "2011252019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Cloudy. Cold.", - "skyInfo": "17", - "skyDescription": "Cloudy", - "temperature": "-0.60", - "temperatureDesc": "Cold", - "comfort": "-3.96", - "humidity": "94", - "dewPoint": "-1.40", - "precipitationProbability": "11", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "9.72", - "windDirection": "248", - "windDesc": "West", - "windDescShort": "W", - "visibility": "0.00", - "icon": "17", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", - "dayOfWeek": "2", - "weekday": "Monday", - "utcTime": "2019-11-25T21:00:00.000-05:00", - "localTime": "2111252019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Cloudy. Cold.", - "skyInfo": "17", - "skyDescription": "Cloudy", - "temperature": "-0.80", - "temperatureDesc": "Cold", - "comfort": "-4.19", - "humidity": "96", - "dewPoint": "-1.30", - "precipitationProbability": "9", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "9.72", - "windDirection": "253", - "windDesc": "West", - "windDescShort": "W", - "visibility": "0.00", - "icon": "17", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", - "dayOfWeek": "2", - "weekday": "Monday", - "utcTime": "2019-11-25T22:00:00.000-05:00", - "localTime": "2211252019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Cloudy. Cold.", - "skyInfo": "17", - "skyDescription": "Cloudy", - "temperature": "-0.80", - "temperatureDesc": "Cold", - "comfort": "-4.19", - "humidity": "96", - "dewPoint": "-1.30", - "precipitationProbability": "9", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "9.72", - "windDirection": "252", - "windDesc": "West", - "windDescShort": "W", - "visibility": "0.00", - "icon": "17", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", - "dayOfWeek": "2", - "weekday": "Monday", - "utcTime": "2019-11-25T23:00:00.000-05:00", - "localTime": "2311252019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Cloudy. Cold.", - "skyInfo": "17", - "skyDescription": "Cloudy", - "temperature": "-0.70", - "temperatureDesc": "Cold", - "comfort": "-4.07", - "humidity": "96", - "dewPoint": "-1.30", - "precipitationProbability": "9", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "9.72", - "windDirection": "251", - "windDesc": "West", - "windDescShort": "W", - "visibility": "0.00", - "icon": "17", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", - "dayOfWeek": "3", - "weekday": "Tuesday", - "utcTime": "2019-11-26T00:00:00.000-05:00", - "localTime": "0011262019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Cloudy. Cold.", - "skyInfo": "17", - "skyDescription": "Cloudy", - "temperature": "-0.80", - "temperatureDesc": "Cold", - "comfort": "-4.09", - "humidity": "96", - "dewPoint": "-1.30", - "precipitationProbability": "10", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "9.36", - "windDirection": "247", - "windDesc": "Southwest", - "windDescShort": "SW", - "visibility": "0.00", - "icon": "17", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", - "dayOfWeek": "3", - "weekday": "Tuesday", - "utcTime": "2019-11-26T01:00:00.000-05:00", - "localTime": "0111262019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Cloudy. Cold.", - "skyInfo": "17", - "skyDescription": "Cloudy", - "temperature": "-0.90", - "temperatureDesc": "Cold", - "comfort": "-4.11", - "humidity": "97", - "dewPoint": "-1.30", - "precipitationProbability": "9", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "9.00", - "windDirection": "241", - "windDesc": "Southwest", - "windDescShort": "SW", - "visibility": "0.00", - "icon": "17", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", - "dayOfWeek": "3", - "weekday": "Tuesday", - "utcTime": "2019-11-26T02:00:00.000-05:00", - "localTime": "0211262019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Cloudy. Cold.", - "skyInfo": "17", - "skyDescription": "Cloudy", - "temperature": "-0.90", - "temperatureDesc": "Cold", - "comfort": "-4.00", - "humidity": "96", - "dewPoint": "-1.40", - "precipitationProbability": "9", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "8.64", - "windDirection": "232", - "windDesc": "Southwest", - "windDescShort": "SW", - "visibility": "0.00", - "icon": "17", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", - "dayOfWeek": "3", - "weekday": "Tuesday", - "utcTime": "2019-11-26T03:00:00.000-05:00", - "localTime": "0311262019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Cloudy. Cold.", - "skyInfo": "17", - "skyDescription": "Cloudy", - "temperature": "-1.20", - "temperatureDesc": "Cold", - "comfort": "-4.24", - "humidity": "98", - "dewPoint": "-1.50", - "precipitationProbability": "9", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "8.28", - "windDirection": "224", - "windDesc": "Southwest", - "windDescShort": "SW", - "visibility": "0.00", - "icon": "17", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", - "dayOfWeek": "3", - "weekday": "Tuesday", - "utcTime": "2019-11-26T04:00:00.000-05:00", - "localTime": "0411262019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "N", - "description": "Cloudy. Cold.", - "skyInfo": "17", - "skyDescription": "Cloudy", - "temperature": "-1.40", - "temperatureDesc": "Cold", - "comfort": "-4.36", - "humidity": "99", - "dewPoint": "-1.60", - "precipitationProbability": "9", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "7.92", - "windDirection": "215", - "windDesc": "Southwest", - "windDescShort": "SW", - "visibility": "0.00", - "icon": "17", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/24.png", - "dayOfWeek": "3", - "weekday": "Tuesday", - "utcTime": "2019-11-26T05:00:00.000-05:00", - "localTime": "0511262019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "D", - "description": "a mixture of sun and clouds. Cold.", - "skyInfo": "11", - "skyDescription": "a mixture of sun and clouds", - "temperature": "-1.10", - "temperatureDesc": "Cold", - "comfort": "-3.88", - "humidity": "99", - "dewPoint": "-1.30", - "precipitationProbability": "9", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "7.56", - "windDirection": "207", - "windDesc": "Southwest", - "windDescShort": "SW", - "visibility": "0.00", - "icon": "4", - "iconName": "partly_cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/3.png", - "dayOfWeek": "3", - "weekday": "Tuesday", - "utcTime": "2019-11-26T06:00:00.000-05:00", - "localTime": "0611262019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "D", - "description": "a mixture of sun and clouds. Cold.", - "skyInfo": "11", - "skyDescription": "a mixture of sun and clouds", - "temperature": "-0.60", - "temperatureDesc": "Cold", - "comfort": "-3.30", - "humidity": "96", - "dewPoint": "-1.10", - "precipitationProbability": "9", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "7.56", - "windDirection": "199", - "windDesc": "South", - "windDescShort": "S", - "visibility": "0.00", - "icon": "4", - "iconName": "partly_cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/3.png", - "dayOfWeek": "3", - "weekday": "Tuesday", - "utcTime": "2019-11-26T07:00:00.000-05:00", - "localTime": "0711262019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "D", - "description": "a mixture of sun and clouds. Cold.", - "skyInfo": "11", - "skyDescription": "a mixture of sun and clouds", - "temperature": "-0.60", - "temperatureDesc": "Cold", - "comfort": "-3.17", - "humidity": "99", - "dewPoint": "-0.80", - "precipitationProbability": "9", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "7.20", - "windDirection": "199", - "windDesc": "South", - "windDescShort": "S", - "visibility": "0.00", - "icon": "4", - "iconName": "partly_cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/3.png", - "dayOfWeek": "3", - "weekday": "Tuesday", - "utcTime": "2019-11-26T08:00:00.000-05:00", - "localTime": "0811262019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "D", - "description": "a mixture of sun and clouds. Chilly.", - "skyInfo": "11", - "skyDescription": "a mixture of sun and clouds", - "temperature": "0.50", - "temperatureDesc": "Chilly", - "comfort": "-1.77", - "humidity": "97", - "dewPoint": "0.10", - "precipitationProbability": "9", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "6.84", - "windDirection": "209", - "windDesc": "Southwest", - "windDescShort": "SW", - "visibility": "0.00", - "icon": "4", - "iconName": "partly_cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/3.png", - "dayOfWeek": "3", - "weekday": "Tuesday", - "utcTime": "2019-11-26T09:00:00.000-05:00", - "localTime": "0911262019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "D", - "description": "a mixture of sun and clouds. Chilly.", - "skyInfo": "11", - "skyDescription": "a mixture of sun and clouds", - "temperature": "2.40", - "temperatureDesc": "Chilly", - "comfort": "0.56", - "humidity": "89", - "dewPoint": "0.80", - "precipitationProbability": "9", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "6.48", - "windDirection": "222", - "windDesc": "Southwest", - "windDescShort": "SW", - "visibility": "0.00", - "icon": "4", - "iconName": "partly_cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/3.png", - "dayOfWeek": "3", - "weekday": "Tuesday", - "utcTime": "2019-11-26T10:00:00.000-05:00", - "localTime": "1011262019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "D", - "description": "a mixture of sun and clouds. Chilly.", - "skyInfo": "11", - "skyDescription": "a mixture of sun and clouds", - "temperature": "3.90", - "temperatureDesc": "Chilly", - "comfort": "2.07", - "humidity": "83", - "dewPoint": "1.30", - "precipitationProbability": "9", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "7.20", - "windDirection": "235", - "windDesc": "Southwest", - "windDescShort": "SW", - "visibility": "0.00", - "icon": "4", - "iconName": "partly_cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/3.png", - "dayOfWeek": "3", - "weekday": "Tuesday", - "utcTime": "2019-11-26T11:00:00.000-05:00", - "localTime": "1111262019", - "localTimeFormat": "HHMMddyyyy" - }, - { - "daylight": "D", - "description": "a mixture of sun and clouds. Chilly.", - "skyInfo": "11", - "skyDescription": "a mixture of sun and clouds", - "temperature": "4.60", - "temperatureDesc": "Chilly", - "comfort": "2.78", - "humidity": "81", - "dewPoint": "1.60", - "precipitationProbability": "9", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "7.56", - "windDirection": "246", - "windDesc": "Southwest", - "windDescShort": "SW", - "visibility": "0.00", - "icon": "4", - "iconName": "partly_cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/3.png", - "dayOfWeek": "3", - "weekday": "Tuesday", - "utcTime": "2019-11-26T12:00:00.000-05:00", - "localTime": "1211262019", - "localTimeFormat": "HHMMddyyyy" - } - ], - "country": "United States", - "state": "New York", - "city": "New York", - "latitude": 43.00035, - "longitude": -75.4999, - "distance": 0.00, - "timezone": -5 - } - }, - "feedCreation": "2019-11-19T23:06:04.631Z", - "metric": true -} \ No newline at end of file diff --git a/tests/fixtures/here_weather/destination_weather_forecasts_simple.json b/tests/fixtures/here_weather/destination_weather_forecasts_simple.json deleted file mode 100644 index dd04c974d1ca7a..00000000000000 --- a/tests/fixtures/here_weather/destination_weather_forecasts_simple.json +++ /dev/null @@ -1,248 +0,0 @@ -{ - "dailyForecasts": { - "forecastLocation": { - "forecast": [ - { - "daylight": "D", - "description": "Overcast. Chilly.", - "skyInfo": "18", - "skyDescription": "Overcast", - "temperatureDesc": "Chilly", - "comfort": "-0.55", - "highTemperature": "4.00", - "lowTemperature": "-1.80", - "humidity": "80", - "dewPoint": "-0.40", - "precipitationProbability": "53", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "1.42", - "airInfo": "*", - "airDescription": "", - "windSpeed": "12.03", - "windDirection": "290", - "windDesc": "West", - "windDescShort": "W", - "beaufortScale": "3", - "beaufortDescription": "Gentle breeze", - "uvIndex": "0", - "uvDesc": "Minimal", - "barometerPressure": "1014.29", - "icon": "7", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/17.png", - "dayOfWeek": "3", - "weekday": "Tuesday", - "utcTime": "2019-11-19T00:00:00.000-05:00" - }, - { - "daylight": "D", - "description": "Light snow. Overcast. Chilly.", - "skyInfo": "18", - "skyDescription": "Overcast", - "temperatureDesc": "Chilly", - "comfort": "-2.53", - "highTemperature": "2.30", - "lowTemperature": "-1.90", - "humidity": "86", - "dewPoint": "-0.37", - "precipitationProbability": "53", - "precipitationDesc": "Light snow", - "rainFall": "0.05", - "snowFall": "0.73", - "airInfo": "*", - "airDescription": "", - "windSpeed": "16.87", - "windDirection": "308", - "windDesc": "Northwest", - "windDescShort": "NW", - "beaufortScale": "3", - "beaufortDescription": "Gentle breeze", - "uvIndex": "0", - "uvDesc": "Minimal", - "barometerPressure": "1022.34", - "icon": "29", - "iconName": "light_snow", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/20.png", - "dayOfWeek": "4", - "weekday": "Wednesday", - "utcTime": "2019-11-20T00:00:00.000-05:00" - }, - { - "daylight": "D", - "description": "Sprinkles late. a mixture of sun and clouds. Chilly.", - "skyInfo": "11", - "skyDescription": "a mixture of sun and clouds", - "temperatureDesc": "Chilly", - "comfort": "0.59", - "highTemperature": "3.50", - "lowTemperature": "-0.10", - "humidity": "82", - "dewPoint": "0.23", - "precipitationProbability": "49", - "precipitationDesc": "Sprinkles late", - "rainFall": "0.04", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "9.26", - "windDirection": "267", - "windDesc": "West", - "windDescShort": "W", - "beaufortScale": "2", - "beaufortDescription": "Light breeze", - "uvIndex": "0", - "uvDesc": "Minimal", - "barometerPressure": "1009.17", - "icon": "18", - "iconName": "sprinkles", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/27.png", - "dayOfWeek": "5", - "weekday": "Thursday", - "utcTime": "2019-11-21T00:00:00.000-05:00" - }, - { - "daylight": "D", - "description": "Sprinkles early. Overcast. Chilly.", - "skyInfo": "18", - "skyDescription": "Overcast", - "temperatureDesc": "Chilly", - "comfort": "-3.68", - "highTemperature": "6.90", - "lowTemperature": "-2.50", - "humidity": "85", - "dewPoint": "0.07", - "precipitationProbability": "62", - "precipitationDesc": "Sprinkles early", - "rainFall": "0.19", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "33.94", - "windDirection": "264", - "windDesc": "West", - "windDescShort": "W", - "beaufortScale": "5", - "beaufortDescription": "Fresh breeze", - "uvIndex": "0", - "uvDesc": "Minimal", - "barometerPressure": "1014.99", - "icon": "18", - "iconName": "sprinkles", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/27.png", - "dayOfWeek": "6", - "weekday": "Friday", - "utcTime": "2019-11-22T00:00:00.000-05:00" - }, - { - "daylight": "D", - "description": "a mixture of sun and clouds. Chilly.", - "skyInfo": "11", - "skyDescription": "a mixture of sun and clouds", - "temperatureDesc": "Chilly", - "comfort": "-2.87", - "highTemperature": "1.20", - "lowTemperature": "-5.20", - "humidity": "72", - "dewPoint": "-4.16", - "precipitationProbability": "4", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "9.72", - "windDirection": "300", - "windDesc": "Northwest", - "windDescShort": "NW", - "beaufortScale": "2", - "beaufortDescription": "Light breeze", - "uvIndex": "0", - "uvDesc": "Minimal", - "barometerPressure": "1008.99", - "icon": "4", - "iconName": "partly_cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/3.png", - "dayOfWeek": "7", - "weekday": "Saturday", - "utcTime": "2019-11-23T00:00:00.000-05:00" - }, - { - "daylight": "D", - "description": "Light snow late. Increasing cloudiness. Chilly.", - "skyInfo": "26", - "skyDescription": "Increasing cloudiness", - "temperatureDesc": "Chilly", - "comfort": "-5.11", - "highTemperature": "0.60", - "lowTemperature": "-1.60", - "humidity": "87", - "dewPoint": "-1.93", - "precipitationProbability": "40", - "precipitationDesc": "Light snow late", - "rainFall": "*", - "snowFall": "0.20", - "airInfo": "*", - "airDescription": "", - "windSpeed": "19.34", - "windDirection": "297", - "windDesc": "Northwest", - "windDescShort": "NW", - "beaufortScale": "4", - "beaufortDescription": "Moderate breeze", - "uvIndex": "0", - "uvDesc": "Minimal", - "barometerPressure": "1010.16", - "icon": "29", - "iconName": "light_snow", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/20.png", - "dayOfWeek": "1", - "weekday": "Sunday", - "utcTime": "2019-11-24T00:00:00.000-05:00" - }, - { - "daylight": "D", - "description": "Snow changing to rain. Cloudy. Chilly.", - "skyInfo": "17", - "skyDescription": "Cloudy", - "temperatureDesc": "Chilly", - "comfort": "-1.66", - "highTemperature": "2.60", - "lowTemperature": "-1.00", - "humidity": "88", - "dewPoint": "0.07", - "precipitationProbability": "51", - "precipitationDesc": "Snow changing to rain", - "rainFall": "0.03", - "snowFall": "0.36", - "airInfo": "*", - "airDescription": "", - "windSpeed": "12.29", - "windDirection": "303", - "windDesc": "Northwest", - "windDescShort": "NW", - "beaufortScale": "3", - "beaufortDescription": "Gentle breeze", - "uvIndex": "0", - "uvDesc": "Minimal", - "barometerPressure": "1016.65", - "icon": "28", - "iconName": "snow_rain_mix", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/19.png", - "dayOfWeek": "2", - "weekday": "Monday", - "utcTime": "2019-11-25T00:00:00.000-05:00" - } - ], - "country": "United States", - "state": "New York", - "city": "New York", - "latitude": 43.00035, - "longitude": -75.4999, - "distance": 0.00, - "timezone": -5 - } - }, - "feedCreation": "2019-11-19T23:01:26.632Z", - "metric": true -} \ No newline at end of file diff --git a/tests/fixtures/here_weather/destination_weather_observation_imperial.json b/tests/fixtures/here_weather/destination_weather_observation_imperial.json deleted file mode 100644 index 3932393c1b6efe..00000000000000 --- a/tests/fixtures/here_weather/destination_weather_observation_imperial.json +++ /dev/null @@ -1,61 +0,0 @@ -{ - "observations": { - "location": [ - { - "observation": [ - { - "daylight": "D", - "description": "Overcast. Cool.", - "skyInfo": "18", - "skyDescription": "Overcast", - "temperature": "46.00", - "temperatureDesc": "Cool", - "comfort": "42.39", - "highTemperature": "50.72", - "lowTemperature": "39.74", - "humidity": "63", - "dewPoint": "34.00", - "precipitation1H": "*", - "precipitation3H": "*", - "precipitation6H": "*", - "precipitation12H": "*", - "precipitation24H": "*", - "precipitationDesc": "", - "airInfo": "*", - "airDescription": "", - "windSpeed": "6.89", - "windDirection": "0", - "windDesc": "North", - "windDescShort": "N", - "barometerPressure": "29.80", - "barometerTrend": "Rising", - "visibility": "10.00", - "snowCover": "*", - "icon": "7", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/17.png", - "ageMinutes": "46", - "activeAlerts": "6", - "country": "United States", - "state": "New York", - "city": "New York", - "latitude": 40.7996, - "longitude": -73.9703, - "distance": 1.43, - "elevation": 0.00, - "utcTime": "2019-11-19T15:51:00.000-05:00" - } - ], - "country": "United States", - "state": "New York", - "city": "New York", - "latitude": 40.79962, - "longitude": -73.970314, - "distance": 0.00, - "timezone": -5 - } - ] - }, - "feedCreation": "2019-11-19T21:37:57.279Z", - "metric": false -} \ No newline at end of file From 75bf364b64267607f97ce2d8830682d3fce78054 Mon Sep 17 00:00:00 2001 From: Kevin Eifinger Date: Sun, 12 Jul 2020 22:40:37 +0200 Subject: [PATCH 10/56] bump herepy to 3.0.1 --- homeassistant/components/here_travel_time/manifest.json | 2 +- requirements_all.txt | 2 -- requirements_test_all.txt | 6 ++---- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/here_travel_time/manifest.json b/homeassistant/components/here_travel_time/manifest.json index 9a3e8bd482717b..015d8bed592506 100644 --- a/homeassistant/components/here_travel_time/manifest.json +++ b/homeassistant/components/here_travel_time/manifest.json @@ -2,7 +2,7 @@ "domain": "here_travel_time", "name": "HERE Travel Time", "documentation": "https://www.home-assistant.io/integrations/here_travel_time", - "requirements": ["herepy==2.0.0"], + "requirements": ["herepy==3.0.1"], "codeowners": ["@eifinger"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index cad70df2807b66..a80b962158afe9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -766,8 +766,6 @@ hdate==0.10.2 heatmiserV3==1.1.18 # homeassistant.components.here_travel_time -herepy==2.0.0 - # homeassistant.components.here_weather herepy==3.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0a9430526bd20c..dcc98317ee6275 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -442,14 +442,12 @@ hatasmota==0.2.20 hdate==0.10.2 # homeassistant.components.here_travel_time -herepy==2.0.0 +# homeassistant.components.here_weather +herepy==3.0.1 # homeassistant.components.hlk_sw16 hlk-sw16==0.0.9 -# homeassistant.components.here_weather -herepy==3.0.1 - # homeassistant.components.pi_hole hole==0.5.1 From 0d9e9999483379206a49639fad26e28d2776edd1 Mon Sep 17 00:00:00 2001 From: Kevin Eifinger Date: Mon, 13 Jul 2020 10:20:03 +0200 Subject: [PATCH 11/56] Increase CodeCov --- .../components/here_weather/config_flow.py | 12 +- tests/components/here_weather/const.py | 177 +++++++++++++++- .../here_weather/test_config_flow.py | 198 ++++++++++++++---- tests/components/here_weather/test_sensor.py | 83 ++++---- tests/components/here_weather/test_weather.py | 40 ++-- 5 files changed, 412 insertions(+), 98 deletions(-) diff --git a/homeassistant/components/here_weather/config_flow.py b/homeassistant/components/here_weather/config_flow.py index e76c77d4dde32c..bd45b97870280b 100644 --- a/homeassistant/components/here_weather/config_flow.py +++ b/homeassistant/components/here_weather/config_flow.py @@ -184,8 +184,10 @@ async def async_step_zip_code(self, user_input=None): ) except AlreadyConfigured: return self.async_abort(reason="already_configured") - except herepy.HEREError as error: - errors["base"] = error.message + except herepy.InvalidRequestError: + errors["base"] = "invalid_request" + except herepy.UnauthorizedError: + errors["base"] = "unauthorized" return self.async_show_form( step_id="zip_code", data_schema=get_zip_code_schema(self.hass), @@ -203,8 +205,10 @@ async def async_step_location_name(self, user_input=None): ) except AlreadyConfigured: return self.async_abort(reason="already_configured") - except herepy.HEREError as error: - errors["base"] = error.message + except herepy.InvalidRequestError: + errors["base"] = "invalid_request" + except herepy.UnauthorizedError: + errors["base"] = "unauthorized" return self.async_show_form( step_id="location_name", data_schema=get_location_name_schema(self.hass), diff --git a/tests/components/here_weather/const.py b/tests/components/here_weather/const.py index 83e35ed6c295e8..f185c7498eca1d 100644 --- a/tests/components/here_weather/const.py +++ b/tests/components/here_weather/const.py @@ -1,7 +1,7 @@ """Constants for here_weather tests.""" import herepy -daily_forecasts_json = { +daily_simple_forecasts_json = { "dailyForecasts": { "forecastLocation": { "forecast": [ @@ -52,8 +52,8 @@ "metric": True, } -daily_forecasts_response = herepy.DestinationWeatherResponse.new_from_jsondict( - daily_forecasts_json, param_defaults={"dailyForecasts": None} +daily_simple_forecasts_response = herepy.DestinationWeatherResponse.new_from_jsondict( + daily_simple_forecasts_json, param_defaults={"dailyForecasts": None} ) astronomy_json = { @@ -87,3 +87,174 @@ astronomy_response = herepy.DestinationWeatherResponse.new_from_jsondict( astronomy_json, param_defaults={"astronomy": None} ) + +hourly_json = { + "hourlyForecasts": { + "forecastLocation": { + "forecast": [ + { + "daylight": "D", + "description": "Overcast. Chilly.", + "skyInfo": "18", + "skyDescription": "Overcast", + "temperature": "4.00", + "temperatureDesc": "Chilly", + "comfort": "0.67", + "humidity": "74", + "dewPoint": "-0.30", + "precipitationProbability": "5", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "14.04", + "windDirection": "287", + "windDesc": "West", + "windDescShort": "W", + "visibility": "11.31", + "icon": "7", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/17.png", + "dayOfWeek": "3", + "weekday": "Tuesday", + "utcTime": "2019-11-19T13:00:00.000-05:00", + "localTime": "1311192019", + "localTimeFormat": "HHMMddyyyy", + } + ], + "country": "United States", + "state": "New York", + "city": "New York", + "latitude": 43.00035, + "longitude": -75.4999, + "distance": 0.00, + "timezone": -5, + } + }, + "feedCreation": "2019-11-19T23:06:04.631Z", + "metric": True, +} + +hourly_response = herepy.DestinationWeatherResponse.new_from_jsondict( + hourly_json, param_defaults={"hourlyForecasts": None} +) + +observation_json = { + "observations": { + "location": [ + { + "observation": [ + { + "daylight": "D", + "description": "Overcast. Cool.", + "skyInfo": "18", + "skyDescription": "Overcast", + "temperature": "46.00", + "temperatureDesc": "Cool", + "comfort": "42.39", + "highTemperature": "50.72", + "lowTemperature": "39.74", + "humidity": "63", + "dewPoint": "34.00", + "precipitation1H": "*", + "precipitation3H": "*", + "precipitation6H": "*", + "precipitation12H": "*", + "precipitation24H": "*", + "precipitationDesc": "", + "airInfo": "*", + "airDescription": "", + "windSpeed": "6.89", + "windDirection": "0", + "windDesc": "North", + "windDescShort": "N", + "barometerPressure": "29.80", + "barometerTrend": "Rising", + "visibility": "10.00", + "snowCover": "*", + "icon": "7", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/17.png", + "ageMinutes": "46", + "activeAlerts": "6", + "country": "United States", + "state": "New York", + "city": "New York", + "latitude": 40.7996, + "longitude": -73.9703, + "distance": 1.43, + "elevation": 0.00, + "utcTime": "2019-11-19T15:51:00.000-05:00", + } + ], + "country": "United States", + "state": "New York", + "city": "New York", + "latitude": 40.79962, + "longitude": -73.970314, + "distance": 0.00, + "timezone": -5, + } + ] + }, + "feedCreation": "2019-11-19T21:37:57.279Z", + "metric": False, +} + +observation_response = herepy.DestinationWeatherResponse.new_from_jsondict( + observation_json, param_defaults={"observations": None} +) + +daily_json = { + "forecasts": { + "forecastLocation": { + "forecast": [ + { + "daylight": "D", + "daySegment": "Afternoon", + "description": "Overcast. Chilly.", + "skyInfo": "18", + "skyDescription": "Overcast", + "temperature": "4.00", + "temperatureDesc": "Chilly", + "comfort": "0.86", + "humidity": "73", + "dewPoint": "-0.40", + "precipitationProbability": "9", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "12.96", + "windDirection": "294", + "windDesc": "Northwest", + "windDescShort": "NW", + "beaufortScale": "3", + "beaufortDescription": "Gentle breeze", + "visibility": "11.38", + "icon": "7", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/17.png", + "dayOfWeek": "3", + "weekday": "Tuesday", + "utcTime": "2019-11-19T12:00:00.000-05:00", + } + ], + "country": "United States", + "state": "New York", + "city": "New York", + "latitude": 43.00035, + "longitude": -75.4999, + "distance": 0.00, + "timezone": -5, + } + }, + "feedCreation": "2019-11-19T22:49:09.348Z", + "metric": True, +} + +daily_response = herepy.DestinationWeatherResponse.new_from_jsondict( + daily_json, param_defaults={"forecasts": None} +) diff --git a/tests/components/here_weather/test_config_flow.py b/tests/components/here_weather/test_config_flow.py index 270ddc687df3e2..3031e322d278f5 100644 --- a/tests/components/here_weather/test_config_flow.py +++ b/tests/components/here_weather/test_config_flow.py @@ -10,6 +10,7 @@ CONF_OPTION_LOCATION_NAME, CONF_OPTION_ZIP_CODE, CONF_ZIP_CODE, + DAILY_SIMPLE_ATTRIBUTES, DEFAULT_MODE, DOMAIN, ) @@ -21,9 +22,10 @@ CONF_SCAN_INTERVAL, CONF_UNIT_SYSTEM, CONF_UNIT_SYSTEM_METRIC, + EVENT_HOMEASSISTANT_START, ) -from .const import daily_forecasts_response +from .const import daily_simple_forecasts_response from tests.async_mock import patch from tests.common import MockConfigEntry @@ -52,7 +54,7 @@ async def test_config_flow(hass, conf_option, method_to_patch, conf_updates): """Test we can finish a config flow.""" with patch( - method_to_patch, return_value=daily_forecasts_response, + method_to_patch, return_value=daily_simple_forecasts_response, ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "user"} @@ -80,11 +82,30 @@ async def test_config_flow(hass, conf_option, method_to_patch, conf_updates): assert state -async def test_unauthorized(hass): +@pytest.mark.parametrize( + "conf_option, method_to_patch, conf_updates", + [ + ( + CONF_OPTION_COORDINATES, + "herepy.DestinationWeatherApi.weather_for_coordinates", + {CONF_LATITUDE: "40.79962", CONF_LONGITUDE: "-73.970314"}, + ), + ( + CONF_OPTION_ZIP_CODE, + "herepy.DestinationWeatherApi.weather_for_zip_code", + {CONF_ZIP_CODE: "test"}, + ), + ( + CONF_OPTION_LOCATION_NAME, + "herepy.DestinationWeatherApi.weather_for_location_name", + {CONF_LOCATION_NAME: "test"}, + ), + ], +) +async def test_unauthorized(hass, conf_option, method_to_patch, conf_updates): """Test handling of an unauthorized api key.""" with patch( - "herepy.DestinationWeatherApi.weather_for_coordinates", - side_effect=herepy.UnauthorizedError("Unauthorized"), + method_to_patch, side_effect=herepy.UnauthorizedError("Unauthorized"), ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "user"} @@ -92,29 +113,97 @@ async def test_unauthorized(hass): assert result["type"] == "form" result = await hass.config_entries.flow.async_configure( - result["flow_id"], {CONF_OPTION: CONF_OPTION_COORDINATES} + result["flow_id"], {CONF_OPTION: conf_option} ) assert result["type"] == "form" + config = { + CONF_API_KEY: "test", + CONF_NAME: DOMAIN, + CONF_MODE: DEFAULT_MODE, + CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC, + } + config.update(conf_updates) result = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_API_KEY: "test", - CONF_NAME: DOMAIN, - CONF_MODE: DEFAULT_MODE, - CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC, - CONF_LATITUDE: "40.79962", - CONF_LONGITUDE: "-73.970314", - }, + result["flow_id"], config ) assert result["type"] == "form" assert result["errors"]["base"] == "unauthorized" -async def test_form_already_configured(hass): +@pytest.mark.parametrize( + "conf_option, method_to_patch, conf_updates", + [ + ( + CONF_OPTION_COORDINATES, + "herepy.DestinationWeatherApi.weather_for_coordinates", + {CONF_LATITUDE: "40.79962", CONF_LONGITUDE: "-73.970314"}, + ), + ( + CONF_OPTION_ZIP_CODE, + "herepy.DestinationWeatherApi.weather_for_zip_code", + {CONF_ZIP_CODE: "test"}, + ), + ( + CONF_OPTION_LOCATION_NAME, + "herepy.DestinationWeatherApi.weather_for_location_name", + {CONF_LOCATION_NAME: "test"}, + ), + ], +) +async def test_invalid_request(hass, conf_option, method_to_patch, conf_updates): + """Test handling of an invalid request.""" + with patch( + method_to_patch, side_effect=herepy.InvalidRequestError("Invalid"), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"} + ) + assert result["type"] == "form" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_OPTION: conf_option} + ) + assert result["type"] == "form" + config = { + CONF_API_KEY: "test", + CONF_NAME: DOMAIN, + CONF_MODE: DEFAULT_MODE, + CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC, + } + config.update(conf_updates) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], config + ) + assert result["type"] == "form" + assert result["errors"]["base"] == "invalid_request" + + +@pytest.mark.parametrize( + "conf_option, method_to_patch, conf_updates", + [ + ( + CONF_OPTION_COORDINATES, + "herepy.DestinationWeatherApi.weather_for_coordinates", + {CONF_LATITUDE: "40.79962", CONF_LONGITUDE: "-73.970314"}, + ), + ( + CONF_OPTION_ZIP_CODE, + "herepy.DestinationWeatherApi.weather_for_zip_code", + {CONF_ZIP_CODE: "test"}, + ), + ( + CONF_OPTION_LOCATION_NAME, + "herepy.DestinationWeatherApi.weather_for_location_name", + {CONF_LOCATION_NAME: "test"}, + ), + ], +) +async def test_form_already_configured( + hass, conf_option, method_to_patch, conf_updates +): """Test is already configured.""" with patch( - "herepy.DestinationWeatherApi.weather_for_coordinates", - return_value=daily_forecasts_response, + method_to_patch, return_value=daily_simple_forecasts_response, ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "user"} @@ -122,19 +211,18 @@ async def test_form_already_configured(hass): assert result["type"] == "form" result = await hass.config_entries.flow.async_configure( - result["flow_id"], {CONF_OPTION: CONF_OPTION_COORDINATES} + result["flow_id"], {CONF_OPTION: conf_option} ) assert result["type"] == "form" + config = { + CONF_API_KEY: "test", + CONF_NAME: DOMAIN, + CONF_MODE: DEFAULT_MODE, + CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC, + } + config.update(conf_updates) result = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_API_KEY: "test", - CONF_NAME: DOMAIN, - CONF_MODE: DEFAULT_MODE, - CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC, - CONF_LATITUDE: "40.79962", - CONF_LONGITUDE: "-73.970314", - }, + result["flow_id"], config ) assert result["type"] == "create_entry" @@ -146,19 +234,11 @@ async def test_form_already_configured(hass): assert result["type"] == "form" result = await hass.config_entries.flow.async_configure( - result["flow_id"], {CONF_OPTION: CONF_OPTION_COORDINATES} + result["flow_id"], {CONF_OPTION: conf_option} ) assert result["type"] == "form" result = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_API_KEY: "test", - CONF_NAME: DOMAIN, - CONF_MODE: DEFAULT_MODE, - CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC, - CONF_LATITUDE: "40.79962", - CONF_LONGITUDE: "-73.970314", - }, + result["flow_id"], config ) assert result["type"] == "abort" @@ -169,7 +249,7 @@ async def test_options(hass): """Test options for Kraken.""" with patch( "herepy.DestinationWeatherApi.weather_for_coordinates", - return_value=daily_forecasts_response, + return_value=daily_simple_forecasts_response, ): entry = MockConfigEntry( domain=DOMAIN, @@ -184,20 +264,23 @@ async def test_options(hass): options={CONF_SCAN_INTERVAL: 60}, ) entry.add_to_hass(hass) - + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) result = await hass.config_entries.options.async_init(entry.entry_id) result = await hass.config_entries.options.async_configure( - result["flow_id"], {CONF_SCAN_INTERVAL: 60} + result["flow_id"], {CONF_SCAN_INTERVAL: 10} ) + await hass.async_block_till_done() assert result["type"] == "create_entry" - assert result["data"][CONF_SCAN_INTERVAL] == 60 + assert result["data"][CONF_SCAN_INTERVAL] == 10 async def test_default_options(hass): """Test default options for Kraken.""" with patch( "herepy.DestinationWeatherApi.weather_for_coordinates", - return_value=daily_forecasts_response, + return_value=daily_simple_forecasts_response, ): entry = MockConfigEntry( domain=DOMAIN, @@ -215,3 +298,32 @@ async def test_default_options(hass): result = await hass.config_entries.options.async_init(entry.entry_id) assert result["type"] == "form" assert result["step_id"] == "init" + + +async def test_unload_entry(hass): + """Test unloading a config entry removes all entities.""" + with patch( + "herepy.DestinationWeatherApi.weather_for_coordinates", + return_value=daily_simple_forecasts_response, + ): + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_API_KEY: "test", + CONF_NAME: DOMAIN, + CONF_MODE: DEFAULT_MODE, + CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC, + CONF_LATITUDE: "40.79962", + CONF_LONGITUDE: "-73.970314", + }, + options={CONF_SCAN_INTERVAL: 60}, + ) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == (len(DAILY_SIMPLE_ATTRIBUTES) + 1) + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 0 diff --git a/tests/components/here_weather/test_sensor.py b/tests/components/here_weather/test_sensor.py index bc9b6ca1a55730..393dd833eb334e 100644 --- a/tests/components/here_weather/test_sensor.py +++ b/tests/components/here_weather/test_sensor.py @@ -1,8 +1,13 @@ """Tests for the here_weather sensor platform.""" +from datetime import timedelta + +import herepy + from homeassistant.components.here_weather.const import ( ASTRONOMY_ATTRIBUTES, CONF_API_KEY, DEFAULT_MODE, + DEFAULT_SCAN_INTERVAL, DOMAIN, MODE_ASTRONOMY, ) @@ -18,49 +23,57 @@ ) import homeassistant.util.dt as dt_util -from .const import astronomy_response, daily_forecasts_response +from .const import astronomy_response, daily_simple_forecasts_response from tests.async_mock import patch -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed -async def test_sensor(hass): - """Test that sensor has a value.""" +async def test_sensor_invalid_request(hass): + """Test that sensor value is unavailable after an invalid request.""" utcnow = dt_util.utcnow() # Patching 'utcnow' to gain more control over the timed update. - with patch("homeassistant.util.dt.utcnow", return_value=utcnow), patch( - "herepy.DestinationWeatherApi.weather_for_coordinates", - return_value=daily_forecasts_response, - ): - entry = MockConfigEntry( - domain=DOMAIN, - data={ - CONF_API_KEY: "test", - CONF_NAME: DOMAIN, - CONF_MODE: DEFAULT_MODE, - CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC, - CONF_LATITUDE: "40.79962", - CONF_LONGITUDE: "-73.970314", - }, - ) - entry.add_to_hass(hass) - - await hass.config_entries.async_setup(entry.entry_id) - - await hass.async_block_till_done() - - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) - await hass.async_block_till_done() - - sensor = hass.states.get("sensor.here_weather_low_temperature") - assert sensor.state == "-1.80" + with patch("homeassistant.util.dt.utcnow", return_value=utcnow): + with patch( + "herepy.DestinationWeatherApi.weather_for_coordinates", + return_value=daily_simple_forecasts_response, + ): + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_API_KEY: "test", + CONF_NAME: DOMAIN, + CONF_MODE: DEFAULT_MODE, + CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC, + CONF_LATITUDE: "40.79962", + CONF_LONGITUDE: "-73.970314", + }, + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + + sensor = hass.states.get("sensor.here_weather_low_temperature") + assert sensor.state == "-1.80" + with patch( + "herepy.DestinationWeatherApi.weather_for_coordinates", + side_effect=herepy.InvalidRequestError("Invalid"), + ): + async_fire_time_changed(hass, utcnow + timedelta(DEFAULT_SCAN_INTERVAL * 2)) + await hass.async_block_till_done() + sensor = hass.states.get("sensor.here_weather_low_temperature") + assert sensor.state == "unavailable" async def test_forecast_astronomy(hass): """Test that forecast_astronomy works.""" - utcnow = dt_util.utcnow() # Patching 'utcnow' to gain more control over the timed update. - with patch("homeassistant.util.dt.utcnow", return_value=utcnow), patch( + with patch( "herepy.DestinationWeatherApi.weather_for_coordinates", return_value=astronomy_response, ): @@ -91,11 +104,9 @@ async def test_forecast_astronomy(hass): async def test_imperial(hass): """Test that imperial mode works.""" - utcnow = dt_util.utcnow() - # Patching 'utcnow' to gain more control over the timed update. - with patch("homeassistant.util.dt.utcnow", return_value=utcnow), patch( + with patch( "herepy.DestinationWeatherApi.weather_for_coordinates", - return_value=daily_forecasts_response, + return_value=daily_simple_forecasts_response, ): entry = MockConfigEntry( domain=DOMAIN, diff --git a/tests/components/here_weather/test_weather.py b/tests/components/here_weather/test_weather.py index eff72b8a9064b8..ccec0f9df131ca 100644 --- a/tests/components/here_weather/test_weather.py +++ b/tests/components/here_weather/test_weather.py @@ -1,8 +1,13 @@ """Tests for the here_weather weather platform.""" +import pytest + from homeassistant.components.here_weather.const import ( CONF_API_KEY, - DEFAULT_MODE, DOMAIN, + MODE_DAILY, + MODE_DAILY_SIMPLE, + MODE_HOURLY, + MODE_OBSERVATION, ) from homeassistant.const import ( CONF_LATITUDE, @@ -10,32 +15,43 @@ CONF_MODE, CONF_NAME, CONF_UNIT_SYSTEM, - CONF_UNIT_SYSTEM_METRIC, + CONF_UNIT_SYSTEM_IMPERIAL, EVENT_HOMEASSISTANT_START, ) -import homeassistant.util.dt as dt_util -from .const import daily_forecasts_response +from .const import ( + daily_response, + daily_simple_forecasts_response, + hourly_response, + observation_response, +) from tests.async_mock import patch from tests.common import MockConfigEntry -async def test_weather(hass): - """Test that sensor has a value.""" - utcnow = dt_util.utcnow() - # Patching 'utcnow' to gain more control over the timed update. - with patch("homeassistant.util.dt.utcnow", return_value=utcnow), patch( +@pytest.mark.parametrize( + "conf_mode, return_value", + [ + (MODE_DAILY, daily_response), + (MODE_DAILY_SIMPLE, daily_simple_forecasts_response), + (MODE_OBSERVATION, observation_response), + (MODE_HOURLY, hourly_response), + ], +) +async def test_weather_imperial(hass, conf_mode, return_value): + """Test that weather has a value.""" + with patch( "herepy.DestinationWeatherApi.weather_for_coordinates", - return_value=daily_forecasts_response, + return_value=return_value, ): entry = MockConfigEntry( domain=DOMAIN, data={ CONF_API_KEY: "test", CONF_NAME: DOMAIN, - CONF_MODE: DEFAULT_MODE, - CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC, + CONF_MODE: conf_mode, + CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_IMPERIAL, CONF_LATITUDE: "40.79962", CONF_LONGITUDE: "-73.970314", }, From f868a90ea1bd4ad3a07457151bedb6ce0d80a116 Mon Sep 17 00:00:00 2001 From: Kevin Eifinger Date: Thu, 17 Sep 2020 14:01:39 +0200 Subject: [PATCH 12/56] Remove weather platform Fix review points --- .../components/here_weather/__init__.py | 90 +++---- .../components/here_weather/config_flow.py | 175 +++--------- .../components/here_weather/const.py | 13 +- .../components/here_weather/manifest.json | 1 - .../components/here_weather/sensor.py | 38 +-- .../components/here_weather/strings.json | 33 +-- .../components/here_weather/utils.py | 38 ++- .../components/here_weather/weather.py | 228 ---------------- tests/components/here_weather/const.py | 255 +----------------- .../here_weather/test_config_flow.py | 194 +++---------- tests/components/here_weather/test_sensor.py | 10 +- tests/components/here_weather/test_weather.py | 69 ----- tests/fixtures/here_weather/astronomy.json | 27 ++ tests/fixtures/here_weather/daily.json | 48 ++++ .../here_weather/daily_simple_forecasts.json | 50 ++++ tests/fixtures/here_weather/hourly.json | 47 ++++ tests/fixtures/here_weather/observation.json | 61 +++++ 17 files changed, 394 insertions(+), 983 deletions(-) delete mode 100644 homeassistant/components/here_weather/weather.py delete mode 100644 tests/components/here_weather/test_weather.py create mode 100644 tests/fixtures/here_weather/astronomy.json create mode 100644 tests/fixtures/here_weather/daily.json create mode 100644 tests/fixtures/here_weather/daily_simple_forecasts.json create mode 100644 tests/fixtures/here_weather/hourly.json create mode 100644 tests/fixtures/here_weather/observation.json diff --git a/homeassistant/components/here_weather/__init__.py b/homeassistant/components/here_weather/__init__.py index 3a96926b1da991..142d9b9a14175e 100644 --- a/homeassistant/components/here_weather/__init__.py +++ b/homeassistant/components/here_weather/__init__.py @@ -8,6 +8,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( + CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_MODE, @@ -18,18 +19,11 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import ( - CONF_API_KEY, - CONF_LOCATION_NAME, - CONF_ZIP_CODE, - DEFAULT_SCAN_INTERVAL, - DOMAIN, - HERE_API_KEYS, -) +from .const import DEFAULT_SCAN_INTERVAL, DOMAIN, HERE_API_KEYS _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["sensor", "weather"] +PLATFORMS = ["sensor"] async def async_setup(hass: HomeAssistant, config: dict) -> bool: @@ -40,8 +34,7 @@ async def async_setup(hass: HomeAssistant, config: dict) -> bool: async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Set up here_weather from a config entry.""" here_weather_data = HEREWeatherData(hass, config_entry) - if not await here_weather_data.async_setup(): - return False + await here_weather_data.async_setup() hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = here_weather_data known_api_keys = hass.data.setdefault(HERE_API_KEYS, []) @@ -81,36 +74,22 @@ def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None: self.hass = hass self.config_entry = config_entry self.here_client = herepy.DestinationWeatherApi(config_entry.data[CONF_API_KEY]) - self.latitude = config_entry.data.get(CONF_LATITUDE) - self.longitude = config_entry.data.get(CONF_LONGITUDE) - self.location_name = config_entry.data.get(CONF_LOCATION_NAME) - self.zip_code = config_entry.data.get(CONF_ZIP_CODE) + self.latitude = config_entry.data[CONF_LATITUDE] + self.longitude = config_entry.data[CONF_LONGITUDE] self.weather_product_type = herepy.WeatherProductType[ config_entry.data[CONF_MODE] ] - self.units = config_entry.data.get(CONF_UNIT_SYSTEM) + self.units = None self.coordinator = None self.unsub_handler = None - async def async_setup(self): + async def async_setup(self) -> list: """Set up the here_weather integration.""" self.add_options() + self.units = self.config_entry.options[CONF_UNIT_SYSTEM] self.unsub_handler = self.config_entry.add_update_listener( self.async_options_updated ) - await self._async_create_coordinator() - return True - - def add_options(self): - """Add options for here_weather integration.""" - if not self.config_entry.options: - options = {CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL} - self.hass.config_entries.async_update_entry( - self.config_entry, options=options - ) - - async def _async_create_coordinator(self): - """Create or recreate the DataUpdateCoordinator.""" self.coordinator = DataUpdateCoordinator( self.hass, _LOGGER, @@ -122,32 +101,36 @@ async def _async_create_coordinator(self): ) await self.coordinator.async_refresh() + def add_options(self) -> None: + """Add options for here_weather integration.""" + if not self.config_entry.options: + options = { + CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL, + CONF_UNIT_SYSTEM: self.hass.config.units.name, + } + self.hass.config_entries.async_update_entry( + self.config_entry, options=options + ) + async def async_update(self) -> None: """Handle data update with the DataUpdateCoordinator.""" try: async with async_timeout.timeout(10): return await self.hass.async_add_executor_job(self._get_data) except herepy.InvalidRequestError as error: - raise UpdateFailed(f"Unable to fetch data from HERE: {error.message}") + raise UpdateFailed( + f"Unable to fetch data from HERE: {error.message}" + ) from error def _get_data(self): """Get the latest data from HERE.""" - is_metric = convert_units_to_boolean(self.units) - if self.zip_code is not None: - data = self.here_client.weather_for_zip_code( - self.zip_code, self.weather_product_type, metric=is_metric - ) - elif self.location_name is not None: - data = self.here_client.weather_for_location_name( - self.location_name, self.weather_product_type, metric=is_metric - ) - else: - data = self.here_client.weather_for_coordinates( - self.latitude, - self.longitude, - self.weather_product_type, - metric=is_metric, - ) + is_metric = self.units == CONF_UNIT_SYSTEM_METRIC + data = self.here_client.weather_for_coordinates( + self.latitude, + self.longitude, + self.weather_product_type, + metric=is_metric, + ) return extract_data_from_payload_for_product_type( data, self.weather_product_type ) @@ -157,18 +140,13 @@ async def async_options_updated( hass: HomeAssistant, config_entry: ConfigEntry ) -> None: """Triggered by config entry options updates.""" - await hass.data[DOMAIN][config_entry.entry_id].async_set_scan_interval( + hass.data[DOMAIN][config_entry.entry_id].set_update_interval( config_entry.options[CONF_SCAN_INTERVAL] ) - async def async_set_scan_interval(self, scan_interval): - """Recreate the coordinator with the new scan interval.""" - await self._async_create_coordinator() - - -def convert_units_to_boolean(units: str) -> bool: - """Convert metric/imperial to true/false.""" - return bool(units == CONF_UNIT_SYSTEM_METRIC) + def set_update_interval(self, update_interval: int) -> None: + """Set the coordinator update_interval to the supplied update_interval.""" + self.coordinator.update_interval = timedelta(seconds=update_interval) def extract_data_from_payload_for_product_type( diff --git a/homeassistant/components/here_weather/config_flow.py b/homeassistant/components/here_weather/config_flow.py index bd45b97870280b..37d167fca8c9ff 100644 --- a/homeassistant/components/here_weather/config_flow.py +++ b/homeassistant/components/here_weather/config_flow.py @@ -1,5 +1,6 @@ """Config flow for here_weather integration.""" import logging +from typing import Optional import herepy import voluptuous as vol @@ -7,6 +8,7 @@ from homeassistant import config_entries from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( + CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_MODE, @@ -21,15 +23,7 @@ import homeassistant.helpers.config_validation as cv from .const import ( - CONF_API_KEY, - CONF_LOCATION_NAME, CONF_MODES, - CONF_OPTION, - CONF_OPTION_COORDINATES, - CONF_OPTION_LOCATION_NAME, - CONF_OPTION_ZIP_CODE, - CONF_OPTIONS, - CONF_ZIP_CODE, DEFAULT_MODE, DEFAULT_SCAN_INTERVAL, DOMAIN, @@ -39,49 +33,7 @@ _LOGGER = logging.getLogger(__name__) -def get_base_schema(hass: HomeAssistant) -> vol.Schema: - """Get the here_weather base schema.""" - known_api_key = None - if HERE_API_KEYS in hass.data: - known_api_key = hass.data[HERE_API_KEYS][0] - return vol.Schema( - { - vol.Required(CONF_API_KEY, default=known_api_key): str, - vol.Optional(CONF_NAME, default=DOMAIN): str, - vol.Optional(CONF_MODE, default=DEFAULT_MODE): vol.In(CONF_MODES), - vol.Optional(CONF_UNIT_SYSTEM, default=hass.config.units.name): vol.In( - [CONF_UNIT_SYSTEM_METRIC, CONF_UNIT_SYSTEM_IMPERIAL] - ), - } - ) - - -def get_coordinate_schema(hass: HomeAssistant) -> vol.Schema: - """Get the here_weather coordinate schema.""" - schema = get_base_schema(hass) - return schema.extend( - { - vol.Optional(CONF_LATITUDE, default=hass.config.latitude): cv.latitude, - vol.Optional(CONF_LONGITUDE, default=hass.config.longitude): cv.longitude, - } - ) - - -def get_zip_code_schema(hass: HomeAssistant) -> vol.Schema: - """Get the here_weather zip_code schema.""" - schema = get_base_schema(hass) - return schema.extend({vol.Required(CONF_ZIP_CODE): str}) - - -def get_location_name_schema(hass: HomeAssistant) -> vol.Schema: - """Get the here_weather location_name schema.""" - schema = get_base_schema(hass) - return schema.extend({vol.Required(CONF_LOCATION_NAME): str}) - - -async def async_validate_coordinate_input( - hass: HomeAssistant, user_input: dict -) -> None: +async def async_validate_user_input(hass: HomeAssistant, user_input: dict) -> None: """Validate the user_input containing coordinates.""" await async_validate_name(hass, user_input) here_client = herepy.DestinationWeatherApi(user_input[CONF_API_KEY]) @@ -93,30 +45,6 @@ async def async_validate_coordinate_input( ) -async def async_validate_zip_code_input(hass: HomeAssistant, user_input: dict) -> None: - """Validate the user_input containing a zip_code.""" - await async_validate_name(hass, user_input) - here_client = herepy.DestinationWeatherApi(user_input[CONF_API_KEY]) - await hass.async_add_executor_job( - here_client.weather_for_zip_code, - user_input[CONF_ZIP_CODE], - herepy.WeatherProductType[user_input[CONF_MODE]], - ) - - -async def async_validate_location_name_input( - hass: HomeAssistant, user_input: dict -) -> None: - """Validate the user_input containing a location_name.""" - await async_validate_name(hass, user_input) - here_client = herepy.DestinationWeatherApi(user_input[CONF_API_KEY]) - await hass.async_add_executor_job( - here_client.weather_for_location_name, - user_input[CONF_LOCATION_NAME], - herepy.WeatherProductType[user_input[CONF_MODE]], - ) - - async def async_validate_name(hass: HomeAssistant, user_input: dict) -> None: """Validate the user input.""" for entry in hass.config_entries.async_entries(DOMAIN): @@ -139,46 +67,9 @@ def async_get_options_flow(config_entry: ConfigEntry): async def async_step_user(self, user_input=None): """Handle the initial step.""" errors = {} - if user_input is not None: - if user_input[CONF_OPTION] == CONF_OPTION_COORDINATES: - return await self.async_step_coordinates() - if user_input[CONF_OPTION] == CONF_OPTION_ZIP_CODE: - return await self.async_step_zip_code() - if user_input[CONF_OPTION] == CONF_OPTION_LOCATION_NAME: - return await self.async_step_location_name() - return self.async_show_form( - step_id="user", - data_schema=vol.Schema({vol.Required(CONF_OPTION): vol.In(CONF_OPTIONS)}), - errors=errors, - ) - - async def async_step_coordinates(self, user_input=None): - """Handle set up by coordinates.""" - errors = {} - if user_input is not None: - try: - await async_validate_coordinate_input(self.hass, user_input) - return self.async_create_entry( - title=user_input[CONF_NAME], data=user_input - ) - except AlreadyConfigured: - return self.async_abort(reason="already_configured") - except herepy.InvalidRequestError: - errors["base"] = "invalid_request" - except herepy.UnauthorizedError: - errors["base"] = "unauthorized" - return self.async_show_form( - step_id="coordinates", - data_schema=get_coordinate_schema(self.hass), - errors=errors, - ) - - async def async_step_zip_code(self, user_input=None): - """Handle set up by zip_code.""" - errors = {} if user_input is not None: try: - await async_validate_zip_code_input(self.hass, user_input) + await async_validate_user_input(self.hass, user_input) return self.async_create_entry( title=user_input[CONF_NAME], data=user_input ) @@ -189,30 +80,43 @@ async def async_step_zip_code(self, user_input=None): except herepy.UnauthorizedError: errors["base"] = "unauthorized" return self.async_show_form( - step_id="zip_code", - data_schema=get_zip_code_schema(self.hass), + step_id="user", + data_schema=self._get_schema(user_input), errors=errors, ) - async def async_step_location_name(self, user_input=None): - """Handle set up by location_name.""" - errors = {} + def _get_schema(self, user_input: Optional[dict]) -> vol.Schema: + known_api_key = None + if HERE_API_KEYS in self.hass.data: + known_api_key = self.hass.data[HERE_API_KEYS][0] if user_input is not None: - try: - await async_validate_location_name_input(self.hass, user_input) - return self.async_create_entry( - title=user_input[CONF_NAME], data=user_input - ) - except AlreadyConfigured: - return self.async_abort(reason="already_configured") - except herepy.InvalidRequestError: - errors["base"] = "invalid_request" - except herepy.UnauthorizedError: - errors["base"] = "unauthorized" - return self.async_show_form( - step_id="location_name", - data_schema=get_location_name_schema(self.hass), - errors=errors, + return vol.Schema( + { + vol.Required(CONF_API_KEY, default=user_input[CONF_API_KEY]): str, + vol.Required(CONF_NAME, default=user_input[CONF_NAME]): str, + vol.Required(CONF_MODE, default=user_input[CONF_MODE]): vol.In( + CONF_MODES + ), + vol.Required( + CONF_LATITUDE, default=user_input[CONF_LATITUDE] + ): cv.latitude, + vol.Required( + CONF_LONGITUDE, default=user_input[CONF_LONGITUDE] + ): cv.longitude, + } + ) + return vol.Schema( + { + vol.Required(CONF_API_KEY, default=known_api_key): str, + vol.Required(CONF_NAME, default=DOMAIN): str, + vol.Required(CONF_MODE, default=DEFAULT_MODE): vol.In(CONF_MODES), + vol.Required( + CONF_LATITUDE, default=self.hass.config.latitude + ): cv.latitude, + vol.Required( + CONF_LONGITUDE, default=self.hass.config.longitude + ): cv.longitude, + } ) @@ -234,7 +138,10 @@ async def async_step_init(self, user_input=None): default=self.config_entry.options.get( CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL ), - ): int + ): int, + vol.Optional(CONF_UNIT_SYSTEM, default=self.hass.config.units.name): vol.In( + [CONF_UNIT_SYSTEM_METRIC, CONF_UNIT_SYSTEM_IMPERIAL] + ), } return self.async_show_form(step_id="init", data_schema=vol.Schema(options)) diff --git a/homeassistant/components/here_weather/const.py b/homeassistant/components/here_weather/const.py index b8d85e11b096d3..5b84d76942980f 100644 --- a/homeassistant/components/here_weather/const.py +++ b/homeassistant/components/here_weather/const.py @@ -5,20 +5,9 @@ DEFAULT_SCAN_INTERVAL = 120 -CONF_API_KEY = "api_key" -CONF_LOCATION_NAME = "location_name" -CONF_ZIP_CODE = "zip_code" CONF_LANGUAGE = "language" CONF_OFFSET = "offset" CONF_OPTION = "option" -CONF_OPTION_COORDINATES = "Using GPS Coordinates" -CONF_OPTION_ZIP_CODE = "Using a US ZIP Code" -CONF_OPTION_LOCATION_NAME = "Using a Location Name or Address" -CONF_OPTIONS = [ - CONF_OPTION_COORDINATES, - CONF_OPTION_ZIP_CODE, - CONF_OPTION_LOCATION_NAME, -] MODE_ASTRONOMY = "forecast_astronomy" MODE_HOURLY = "forecast_hourly" @@ -209,7 +198,7 @@ "windSpeed": {"name": "Wind Speed", "unit_of_measurement": "km/h"}, "windDirection": {"name": "Wind Direction", "unit_of_measurement": "°"}, "windDesc": {"name": "Wind Description", "unit_of_measurement": "cm"}, - "windDescShort": {"name": "Wind Description Short", "unit_of_measurement": "cm"}, + "windDescShort": {"name": "Wind Description Short", "unit_of_measurement": None}, "barometerPressure": {"name": "Barometric Pressure", "unit_of_measurement": "mbar"}, "barometerTrend": { "name": "Barometric Pressure Trend", diff --git a/homeassistant/components/here_weather/manifest.json b/homeassistant/components/here_weather/manifest.json index f8738488078f71..621d29f2924766 100644 --- a/homeassistant/components/here_weather/manifest.json +++ b/homeassistant/components/here_weather/manifest.json @@ -6,7 +6,6 @@ "requirements": [ "herepy==3.0.1" ], - "dependencies": [], "codeowners": [ "@eifinger" ] diff --git a/homeassistant/components/here_weather/sensor.py b/homeassistant/components/here_weather/sensor.py index 40b7a212d023de..34190844c43bf2 100644 --- a/homeassistant/components/here_weather/sensor.py +++ b/homeassistant/components/here_weather/sensor.py @@ -1,11 +1,10 @@ """Support for the HERE Destination Weather service.""" import logging -from typing import Dict, Optional, Union from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_MODE, CONF_NAME from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import HEREWeatherData from .const import DOMAIN, SENSOR_TYPES @@ -35,7 +34,7 @@ async def async_setup_entry( async_add_entities(sensors_to_add, True) -class HEREDestinationWeatherSensor(Entity): +class HEREDestinationWeatherSensor(CoordinatorEntity): """Implementation of an HERE Destination Weather sensor.""" def __init__( @@ -47,6 +46,7 @@ def __init__( sensor_number: int = 0, # Additional supported offsets will be added in a separate PR ) -> None: """Initialize the sensor.""" + super().__init__(here_data.coordinator) self._base_name = name self._name_suffix = SENSOR_TYPES[sensor_type][weather_attribute]["name"] self._here_data = here_data @@ -64,9 +64,9 @@ def name(self) -> str: return f"{self._base_name} {self._name_suffix}" @property - def unique_id(self): + def unique_id(self) -> str: """Set unique_id for sensor.""" - return self.name + return f"{self._here_data.latitude}_{self._here_data.latitude}_{self._sensor_type}_{self._weather_attribute}_{self._sensor_number}" @property def state(self) -> str: @@ -82,28 +82,11 @@ def unit_of_measurement(self) -> str: """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement - @property - def device_state_attributes( - self, - ) -> Optional[Dict[str, Union[None, float, str, bool]]]: - """Return the state attributes.""" - return None - @property def available(self): """Could the api be accessed during the last update call.""" return self._here_data.coordinator.last_update_success - @property - def should_poll(self): - """Return the polling requirement for this sensor.""" - return False - - @property - def entity_registry_enabled_default(self): - """Return if the entity should be enabled when first added to the entity registry.""" - return True - @property def device_info(self) -> dict: """Return a device description for device registry.""" @@ -112,14 +95,5 @@ def device_info(self) -> dict: "identifiers": {(DOMAIN, self._base_name)}, "name": self._base_name, "manufacturer": "here.com", + "entry_type": "service", } - - async def async_added_to_hass(self): - """When entity is added to hass.""" - self.async_on_remove( - self._here_data.coordinator.async_add_listener(self.async_write_ha_state) - ) - - async def async_update(self): - """Get the latest data from HERE.""" - await self._here_data.coordinator.async_request_refresh() diff --git a/homeassistant/components/here_weather/strings.json b/homeassistant/components/here_weather/strings.json index 0f9747f202b268..b51149e86c6033 100644 --- a/homeassistant/components/here_weather/strings.json +++ b/homeassistant/components/here_weather/strings.json @@ -4,44 +4,14 @@ "step": { "user": { "title": "HERE Destination Weather", - "description": "Which configuration option do you want to use?", - "data": { - "option": "Configuration Option." - } - }, - "coordinates": { - "title": "HERE Destination Weather By Coordinates", - "description": "Provide the details for the coordinates.", + "description": "This will configure the HERE Destination Weather Integration.", "data": { "api_key": "[%key:common::config_flow::data::api_key%]", "name": "Name of this Integration Entry", "mode": "The Weather Mode to use", - "unit_system": "The Unit System to use", "latitude": "Latitude", "longitude": "Longitude" } - }, - "zip_code": { - "title": "HERE Destination Weather by US ZIP Code", - "description": "Provide the details for the US ZIP Code.", - "data": { - "api_key": "[%key:common::config_flow::data::api_key%]", - "name": "Name of this Integration Entry", - "mode": "The Weather Mode to use", - "unit_system": "The Unit System to use", - "zip_code": "US ZIP Code" - } - }, - "location_name": { - "title": "HERE Destination Weather by Location Name or Address", - "description": "Provide the details for the Location Name or Address.", - "data": { - "api_key": "[%key:common::config_flow::data::api_key%]", - "name": "Name of this Integration Entry", - "mode": "The Weather Mode to use", - "unit_system": "The Unit System to use", - "location_name": "Location Name or Address" - } } }, "error": { @@ -57,6 +27,7 @@ "init": { "description": "Configure options for HERE Destination Weather", "data": { + "unit_system": "The Unit System to use", "scan_interval": "Update interval." } } diff --git a/homeassistant/components/here_weather/utils.py b/homeassistant/components/here_weather/utils.py index 8305ab48b19d04..bbc530b564e889 100644 --- a/homeassistant/components/here_weather/utils.py +++ b/homeassistant/components/here_weather/utils.py @@ -1,27 +1,41 @@ """Utility functions for here_weather.""" -from homeassistant.const import CONF_UNIT_SYSTEM_METRIC +from typing import Optional + +from homeassistant.const import ( + CONF_UNIT_SYSTEM_METRIC, + LENGTH_CENTIMETERS, + LENGTH_INCHES, + LENGTH_KILOMETERS, + LENGTH_MILES, + PRESSURE_INHG, + PRESSURE_MBAR, + SPEED_KILOMETERS_PER_HOUR, + SPEED_MILES_PER_HOUR, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, +) def convert_unit_of_measurement_if_needed(unit_system, unit_of_measurement: str) -> str: """Convert the unit of measurement to imperial if configured.""" if unit_system != CONF_UNIT_SYSTEM_METRIC: - if unit_of_measurement == "°C": - unit_of_measurement = "°F" - elif unit_of_measurement == "cm": - unit_of_measurement = "in" - elif unit_of_measurement == "km/h": - unit_of_measurement = "mph" - elif unit_of_measurement == "mbar": - unit_of_measurement = "in" - elif unit_of_measurement == "km": - unit_of_measurement = "mi" + if unit_of_measurement == TEMP_CELSIUS: + unit_of_measurement = TEMP_FAHRENHEIT + elif unit_of_measurement == LENGTH_CENTIMETERS: + unit_of_measurement = LENGTH_INCHES + elif unit_of_measurement == SPEED_KILOMETERS_PER_HOUR: + unit_of_measurement = SPEED_MILES_PER_HOUR + elif unit_of_measurement == PRESSURE_MBAR: + unit_of_measurement = PRESSURE_INHG + elif unit_of_measurement == LENGTH_KILOMETERS: + unit_of_measurement = LENGTH_MILES return unit_of_measurement def get_attribute_from_here_data( here_data: list, attribute_name: str, sensor_number: int = 0 -) -> str: +) -> Optional[str]: """Extract and convert data from HERE response or None if not found.""" if here_data is None: return None diff --git a/homeassistant/components/here_weather/weather.py b/homeassistant/components/here_weather/weather.py deleted file mode 100644 index fd8d6c9716940b..00000000000000 --- a/homeassistant/components/here_weather/weather.py +++ /dev/null @@ -1,228 +0,0 @@ -"""Support for the HERE Destination Weather API.""" -import logging - -from homeassistant.components.weather import ( - ATTR_FORECAST_CONDITION, - ATTR_FORECAST_PRECIPITATION, - ATTR_FORECAST_TEMP, - ATTR_FORECAST_TEMP_LOW, - ATTR_FORECAST_TIME, - ATTR_FORECAST_WIND_BEARING, - ATTR_FORECAST_WIND_SPEED, - WeatherEntity, -) -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_MODE, CONF_NAME, TEMP_CELSIUS -from homeassistant.core import HomeAssistant - -from . import HEREWeatherData -from .const import CONDITION_CLASSES, DOMAIN, MODE_ASTRONOMY, MODE_DAILY_SIMPLE -from .utils import convert_unit_of_measurement_if_needed, get_attribute_from_here_data - -_LOGGER = logging.getLogger(__name__) - - -async def async_setup_entry( - hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities -): - """Add here_weather entities from a config_entry.""" - if config_entry.data[CONF_MODE] != MODE_ASTRONOMY: - here_weather_data = hass.data[DOMAIN][config_entry.entry_id] - - async_add_entities( - [ - HEREDestinationWeather( - config_entry.data[CONF_NAME], - here_weather_data, - config_entry.data[CONF_MODE], - ) - ], - True, - ) - - -class HEREDestinationWeather(WeatherEntity): - """Implementation of an HERE Destination Weather WeatherEntity.""" - - def __init__(self, name: str, here_data: HEREWeatherData, mode: str): - """Initialize the sensor.""" - self._name = name - self._here_data = here_data - self._mode = mode - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def unique_id(self): - """Set unique_id for sensor.""" - return self._name - - @property - def condition(self): - """Return the current condition.""" - return get_condition_from_here_data(self._here_data.coordinator.data) - - @property - def temperature(self) -> float: - """Return the temperature.""" - return get_temperature_from_here_data( - self._here_data.coordinator.data, self._mode - ) - - @property - def temperature_unit(self): - """Return the unit of measurement.""" - try: - return convert_unit_of_measurement_if_needed( - self._here_data.units, TEMP_CELSIUS - ) - except KeyError: - return None - - @property - def pressure(self): - """Return the pressure.""" - return None - - @property - def humidity(self): - """Return the humidity.""" - get_attribute_from_here_data(self._here_data.coordinator.data, "humidity") - - @property - def wind_speed(self): - """Return the wind speed.""" - get_attribute_from_here_data(self._here_data.coordinator.data, "windSpeed") - - @property - def wind_bearing(self): - """Return the wind bearing.""" - get_attribute_from_here_data(self._here_data.coordinator.data, "windDirection") - - @property - def attribution(self): - """Return the attribution.""" - return None - - @property - def forecast(self): - """Return the forecast array.""" - if self._here_data.coordinator.data is None: - return None - data = [] - for offset in range(len(self._here_data.coordinator.data)): - data.append( - { - ATTR_FORECAST_TIME: get_attribute_from_here_data( - self._here_data.coordinator.data, "utcTime", offset - ), - ATTR_FORECAST_TEMP: get_high_or_default_temperature_from_here_data( - self._here_data.coordinator.data, self._mode, offset - ), - ATTR_FORECAST_TEMP_LOW: get_low_or_default_temperature_from_here_data( - self._here_data.coordinator.data, self._mode, offset - ), - ATTR_FORECAST_PRECIPITATION: calc_precipitation( - self._here_data.coordinator.data, offset - ), - ATTR_FORECAST_WIND_SPEED: get_attribute_from_here_data( - self._here_data.coordinator.data, "windSpeed", offset - ), - ATTR_FORECAST_WIND_BEARING: get_attribute_from_here_data( - self._here_data.coordinator.data, "windDirection", offset - ), - ATTR_FORECAST_CONDITION: get_condition_from_here_data( - self._here_data.coordinator.data, offset - ), - } - ) - return data - - @property - def available(self): - """Could the api be accessed during the last update call.""" - return self._here_data.coordinator.last_update_success - - @property - def should_poll(self): - """Return the polling requirement for this sensor.""" - return False - - @property - def entity_registry_enabled_default(self): - """Return if the entity should be enabled when first added to the entity registry.""" - return True - - @property - def device_info(self) -> dict: - """Return a device description for device registry.""" - - return { - "identifiers": {(DOMAIN, self._name)}, - "name": self._name, - "manufacturer": "here.com", - } - - async def async_added_to_hass(self): - """When entity is added to hass.""" - self.async_on_remove( - self._here_data.coordinator.async_add_listener(self.async_write_ha_state) - ) - - async def async_update(self): - """Get the latest data from HERE.""" - await self._here_data.coordinator.async_request_refresh() - - -def get_condition_from_here_data(here_data: list, offset: int = 0) -> str: - """Return the condition from here_data.""" - try: - return [ - k - for k, v in CONDITION_CLASSES.items() - if get_attribute_from_here_data(here_data, "iconName", offset) in v - ][0] - except IndexError: - return None - - -def get_high_or_default_temperature_from_here_data( - here_data: list, mode: str, offset: int = 0 -) -> str: - """Return the temperature from here_data.""" - temperature = get_attribute_from_here_data(here_data, "highTemperature", offset) - if temperature is not None: - return float(temperature) - - return get_temperature_from_here_data(here_data, mode, offset) - - -def get_low_or_default_temperature_from_here_data( - here_data: list, mode: str, offset: int = 0 -) -> str: - """Return the temperature from here_data.""" - temperature = get_attribute_from_here_data(here_data, "lowTemperature", offset) - if temperature is not None: - return float(temperature) - return get_temperature_from_here_data(here_data, mode, offset) - - -def get_temperature_from_here_data(here_data: list, mode: str, offset: int = 0) -> str: - """Return the temperature from here_data.""" - if mode == MODE_DAILY_SIMPLE: - temperature = get_attribute_from_here_data(here_data, "highTemperature", offset) - else: - temperature = get_attribute_from_here_data(here_data, "temperature", offset) - if temperature is not None: - return float(temperature) - - -def calc_precipitation(here_data: list, offset: int = 0) -> float: - """Calculate Precipitation.""" - rain_fall = get_attribute_from_here_data(here_data, "rainFall", offset) - snow_fall = get_attribute_from_here_data(here_data, "snowFall", offset) - if rain_fall is not None and snow_fall is not None: - return float(rain_fall) + float(snow_fall) diff --git a/tests/components/here_weather/const.py b/tests/components/here_weather/const.py index f185c7498eca1d..1360b3818e1711 100644 --- a/tests/components/here_weather/const.py +++ b/tests/components/here_weather/const.py @@ -1,260 +1,31 @@ """Constants for here_weather tests.""" +import json + import herepy -daily_simple_forecasts_json = { - "dailyForecasts": { - "forecastLocation": { - "forecast": [ - { - "daylight": "D", - "description": "Overcast. Chilly.", - "skyInfo": "18", - "skyDescription": "Overcast", - "temperatureDesc": "Chilly", - "comfort": "-0.55", - "highTemperature": "4.00", - "lowTemperature": "-1.80", - "humidity": "80", - "dewPoint": "-0.40", - "precipitationProbability": "53", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "1.42", - "airInfo": "*", - "airDescription": "", - "windSpeed": "12.03", - "windDirection": "290", - "windDesc": "West", - "windDescShort": "W", - "beaufortScale": "3", - "beaufortDescription": "Gentle breeze", - "uvIndex": "0", - "uvDesc": "Minimal", - "barometerPressure": "1014.29", - "icon": "7", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/17.png", - "dayOfWeek": "3", - "weekday": "Tuesday", - "utcTime": "2019-11-19T00:00:00.000-05:00", - } - ], - "country": "United States", - "state": "New York", - "city": "New York", - "latitude": 43.00035, - "longitude": -75.4999, - "distance": 0.00, - "timezone": -5, - } - }, - "feedCreation": "2019-11-19T23:01:26.632Z", - "metric": True, -} +from tests.common import load_fixture daily_simple_forecasts_response = herepy.DestinationWeatherResponse.new_from_jsondict( - daily_simple_forecasts_json, param_defaults={"dailyForecasts": None} + json.loads(load_fixture("here_weather/daily_simple_forecasts.json")), + param_defaults={"dailyForecasts": None}, ) -astronomy_json = { - "astronomy": { - "astronomy": [ - { - "sunrise": "6:55AM", - "sunset": "6:33PM", - "moonrise": "1:27PM", - "moonset": "10:58PM", - "moonPhase": 0.328, - "moonPhaseDesc": "Waxing crescent", - "iconName": "cw_waxing_crescent", - "city": "Edgewater", - "latitude": 40.82, - "longitude": -73.97, - "utcTime": "2019-10-04T00:00:00.000-04:00", - } - ], - "country": "United States", - "state": "New York", - "city": "New York", - "latitude": 40.79962, - "longitude": -73.970314, - "timezone": -5, - }, - "feedCreation": "2019-10-04T14:22:46.164Z", - "metric": True, -} - astronomy_response = herepy.DestinationWeatherResponse.new_from_jsondict( - astronomy_json, param_defaults={"astronomy": None} + json.loads(load_fixture("here_weather/astronomy.json")), + param_defaults={"astronomy": None}, ) -hourly_json = { - "hourlyForecasts": { - "forecastLocation": { - "forecast": [ - { - "daylight": "D", - "description": "Overcast. Chilly.", - "skyInfo": "18", - "skyDescription": "Overcast", - "temperature": "4.00", - "temperatureDesc": "Chilly", - "comfort": "0.67", - "humidity": "74", - "dewPoint": "-0.30", - "precipitationProbability": "5", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "14.04", - "windDirection": "287", - "windDesc": "West", - "windDescShort": "W", - "visibility": "11.31", - "icon": "7", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/17.png", - "dayOfWeek": "3", - "weekday": "Tuesday", - "utcTime": "2019-11-19T13:00:00.000-05:00", - "localTime": "1311192019", - "localTimeFormat": "HHMMddyyyy", - } - ], - "country": "United States", - "state": "New York", - "city": "New York", - "latitude": 43.00035, - "longitude": -75.4999, - "distance": 0.00, - "timezone": -5, - } - }, - "feedCreation": "2019-11-19T23:06:04.631Z", - "metric": True, -} - hourly_response = herepy.DestinationWeatherResponse.new_from_jsondict( - hourly_json, param_defaults={"hourlyForecasts": None} + json.loads(load_fixture("here_weather/hourly.json")), + param_defaults={"hourlyForecasts": None}, ) -observation_json = { - "observations": { - "location": [ - { - "observation": [ - { - "daylight": "D", - "description": "Overcast. Cool.", - "skyInfo": "18", - "skyDescription": "Overcast", - "temperature": "46.00", - "temperatureDesc": "Cool", - "comfort": "42.39", - "highTemperature": "50.72", - "lowTemperature": "39.74", - "humidity": "63", - "dewPoint": "34.00", - "precipitation1H": "*", - "precipitation3H": "*", - "precipitation6H": "*", - "precipitation12H": "*", - "precipitation24H": "*", - "precipitationDesc": "", - "airInfo": "*", - "airDescription": "", - "windSpeed": "6.89", - "windDirection": "0", - "windDesc": "North", - "windDescShort": "N", - "barometerPressure": "29.80", - "barometerTrend": "Rising", - "visibility": "10.00", - "snowCover": "*", - "icon": "7", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/17.png", - "ageMinutes": "46", - "activeAlerts": "6", - "country": "United States", - "state": "New York", - "city": "New York", - "latitude": 40.7996, - "longitude": -73.9703, - "distance": 1.43, - "elevation": 0.00, - "utcTime": "2019-11-19T15:51:00.000-05:00", - } - ], - "country": "United States", - "state": "New York", - "city": "New York", - "latitude": 40.79962, - "longitude": -73.970314, - "distance": 0.00, - "timezone": -5, - } - ] - }, - "feedCreation": "2019-11-19T21:37:57.279Z", - "metric": False, -} - observation_response = herepy.DestinationWeatherResponse.new_from_jsondict( - observation_json, param_defaults={"observations": None} + json.loads(load_fixture("here_weather/observation.json")), + param_defaults={"observations": None}, ) -daily_json = { - "forecasts": { - "forecastLocation": { - "forecast": [ - { - "daylight": "D", - "daySegment": "Afternoon", - "description": "Overcast. Chilly.", - "skyInfo": "18", - "skyDescription": "Overcast", - "temperature": "4.00", - "temperatureDesc": "Chilly", - "comfort": "0.86", - "humidity": "73", - "dewPoint": "-0.40", - "precipitationProbability": "9", - "precipitationDesc": "", - "rainFall": "*", - "snowFall": "*", - "airInfo": "*", - "airDescription": "", - "windSpeed": "12.96", - "windDirection": "294", - "windDesc": "Northwest", - "windDescShort": "NW", - "beaufortScale": "3", - "beaufortDescription": "Gentle breeze", - "visibility": "11.38", - "icon": "7", - "iconName": "cloudy", - "iconLink": "https://weather.cit.api.here.com/static/weather/icon/17.png", - "dayOfWeek": "3", - "weekday": "Tuesday", - "utcTime": "2019-11-19T12:00:00.000-05:00", - } - ], - "country": "United States", - "state": "New York", - "city": "New York", - "latitude": 43.00035, - "longitude": -75.4999, - "distance": 0.00, - "timezone": -5, - } - }, - "feedCreation": "2019-11-19T22:49:09.348Z", - "metric": True, -} - daily_response = herepy.DestinationWeatherResponse.new_from_jsondict( - daily_json, param_defaults={"forecasts": None} + json.loads(load_fixture("here_weather/daily.json")), + param_defaults={"forecasts": None}, ) diff --git a/tests/components/here_weather/test_config_flow.py b/tests/components/here_weather/test_config_flow.py index 3031e322d278f5..dbdd6fd4086a3e 100644 --- a/tests/components/here_weather/test_config_flow.py +++ b/tests/components/here_weather/test_config_flow.py @@ -1,20 +1,13 @@ """Tests for the here_weather config_flow.""" import herepy -import pytest from homeassistant.components.here_weather.const import ( - CONF_API_KEY, - CONF_LOCATION_NAME, - CONF_OPTION, - CONF_OPTION_COORDINATES, - CONF_OPTION_LOCATION_NAME, - CONF_OPTION_ZIP_CODE, - CONF_ZIP_CODE, DAILY_SIMPLE_ATTRIBUTES, DEFAULT_MODE, DOMAIN, ) from homeassistant.const import ( + CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_MODE, @@ -31,47 +24,23 @@ from tests.common import MockConfigEntry -@pytest.mark.parametrize( - "conf_option, method_to_patch, conf_updates", - [ - ( - CONF_OPTION_COORDINATES, - "herepy.DestinationWeatherApi.weather_for_coordinates", - {CONF_LATITUDE: "40.79962", CONF_LONGITUDE: "-73.970314"}, - ), - ( - CONF_OPTION_ZIP_CODE, - "herepy.DestinationWeatherApi.weather_for_zip_code", - {CONF_ZIP_CODE: "test"}, - ), - ( - CONF_OPTION_LOCATION_NAME, - "herepy.DestinationWeatherApi.weather_for_location_name", - {CONF_LOCATION_NAME: "test"}, - ), - ], -) -async def test_config_flow(hass, conf_option, method_to_patch, conf_updates): +async def test_config_flow(hass): """Test we can finish a config flow.""" with patch( - method_to_patch, return_value=daily_simple_forecasts_response, + "herepy.DestinationWeatherApi.weather_for_coordinates", + return_value=daily_simple_forecasts_response, ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "user"} ) assert result["type"] == "form" - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], {CONF_OPTION: conf_option} - ) - assert result["type"] == "form" config = { CONF_API_KEY: "test", CONF_NAME: DOMAIN, CONF_MODE: DEFAULT_MODE, - CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC, + CONF_LATITUDE: "40.79962", + CONF_LONGITUDE: "-73.970314", } - config.update(conf_updates) result = await hass.config_entries.flow.async_configure( result["flow_id"], config ) @@ -82,47 +51,23 @@ async def test_config_flow(hass, conf_option, method_to_patch, conf_updates): assert state -@pytest.mark.parametrize( - "conf_option, method_to_patch, conf_updates", - [ - ( - CONF_OPTION_COORDINATES, - "herepy.DestinationWeatherApi.weather_for_coordinates", - {CONF_LATITUDE: "40.79962", CONF_LONGITUDE: "-73.970314"}, - ), - ( - CONF_OPTION_ZIP_CODE, - "herepy.DestinationWeatherApi.weather_for_zip_code", - {CONF_ZIP_CODE: "test"}, - ), - ( - CONF_OPTION_LOCATION_NAME, - "herepy.DestinationWeatherApi.weather_for_location_name", - {CONF_LOCATION_NAME: "test"}, - ), - ], -) -async def test_unauthorized(hass, conf_option, method_to_patch, conf_updates): +async def test_unauthorized(hass): """Test handling of an unauthorized api key.""" with patch( - method_to_patch, side_effect=herepy.UnauthorizedError("Unauthorized"), + "herepy.DestinationWeatherApi.weather_for_coordinates", + side_effect=herepy.UnauthorizedError("Unauthorized"), ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "user"} ) assert result["type"] == "form" - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], {CONF_OPTION: conf_option} - ) - assert result["type"] == "form" config = { CONF_API_KEY: "test", CONF_NAME: DOMAIN, CONF_MODE: DEFAULT_MODE, - CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC, + CONF_LATITUDE: "40.79962", + CONF_LONGITUDE: "-73.970314", } - config.update(conf_updates) result = await hass.config_entries.flow.async_configure( result["flow_id"], config ) @@ -130,47 +75,23 @@ async def test_unauthorized(hass, conf_option, method_to_patch, conf_updates): assert result["errors"]["base"] == "unauthorized" -@pytest.mark.parametrize( - "conf_option, method_to_patch, conf_updates", - [ - ( - CONF_OPTION_COORDINATES, - "herepy.DestinationWeatherApi.weather_for_coordinates", - {CONF_LATITUDE: "40.79962", CONF_LONGITUDE: "-73.970314"}, - ), - ( - CONF_OPTION_ZIP_CODE, - "herepy.DestinationWeatherApi.weather_for_zip_code", - {CONF_ZIP_CODE: "test"}, - ), - ( - CONF_OPTION_LOCATION_NAME, - "herepy.DestinationWeatherApi.weather_for_location_name", - {CONF_LOCATION_NAME: "test"}, - ), - ], -) -async def test_invalid_request(hass, conf_option, method_to_patch, conf_updates): +async def test_invalid_request(hass): """Test handling of an invalid request.""" with patch( - method_to_patch, side_effect=herepy.InvalidRequestError("Invalid"), + "herepy.DestinationWeatherApi.weather_for_coordinates", + side_effect=herepy.InvalidRequestError("Invalid"), ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "user"} ) assert result["type"] == "form" - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], {CONF_OPTION: conf_option} - ) - assert result["type"] == "form" config = { CONF_API_KEY: "test", CONF_NAME: DOMAIN, CONF_MODE: DEFAULT_MODE, - CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC, + CONF_LATITUDE: "40.79962", + CONF_LONGITUDE: "-73.970314", } - config.update(conf_updates) result = await hass.config_entries.flow.async_configure( result["flow_id"], config ) @@ -178,49 +99,23 @@ async def test_invalid_request(hass, conf_option, method_to_patch, conf_updates) assert result["errors"]["base"] == "invalid_request" -@pytest.mark.parametrize( - "conf_option, method_to_patch, conf_updates", - [ - ( - CONF_OPTION_COORDINATES, - "herepy.DestinationWeatherApi.weather_for_coordinates", - {CONF_LATITUDE: "40.79962", CONF_LONGITUDE: "-73.970314"}, - ), - ( - CONF_OPTION_ZIP_CODE, - "herepy.DestinationWeatherApi.weather_for_zip_code", - {CONF_ZIP_CODE: "test"}, - ), - ( - CONF_OPTION_LOCATION_NAME, - "herepy.DestinationWeatherApi.weather_for_location_name", - {CONF_LOCATION_NAME: "test"}, - ), - ], -) -async def test_form_already_configured( - hass, conf_option, method_to_patch, conf_updates -): +async def test_form_already_configured(hass): """Test is already configured.""" with patch( - method_to_patch, return_value=daily_simple_forecasts_response, + "herepy.DestinationWeatherApi.weather_for_coordinates", + return_value=daily_simple_forecasts_response, ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "user"} ) assert result["type"] == "form" - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], {CONF_OPTION: conf_option} - ) - assert result["type"] == "form" config = { CONF_API_KEY: "test", CONF_NAME: DOMAIN, CONF_MODE: DEFAULT_MODE, - CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC, + CONF_LATITUDE: "40.79962", + CONF_LONGITUDE: "-73.970314", } - config.update(conf_updates) result = await hass.config_entries.flow.async_configure( result["flow_id"], config ) @@ -232,11 +127,13 @@ async def test_form_already_configured( DOMAIN, context={"source": "user"} ) assert result["type"] == "form" - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], {CONF_OPTION: conf_option} - ) - assert result["type"] == "form" + config = { + CONF_API_KEY: "test", + CONF_NAME: DOMAIN, + CONF_MODE: DEFAULT_MODE, + CONF_LATITUDE: "40.79962", + CONF_LONGITUDE: "-73.970314", + } result = await hass.config_entries.flow.async_configure( result["flow_id"], config ) @@ -246,7 +143,7 @@ async def test_form_already_configured( async def test_options(hass): - """Test options for Kraken.""" + """Test the options flow.""" with patch( "herepy.DestinationWeatherApi.weather_for_coordinates", return_value=daily_simple_forecasts_response, @@ -257,11 +154,9 @@ async def test_options(hass): CONF_API_KEY: "test", CONF_NAME: DOMAIN, CONF_MODE: DEFAULT_MODE, - CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC, CONF_LATITUDE: "40.79962", CONF_LONGITUDE: "-73.970314", }, - options={CONF_SCAN_INTERVAL: 60}, ) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) @@ -269,37 +164,14 @@ async def test_options(hass): hass.bus.async_fire(EVENT_HOMEASSISTANT_START) result = await hass.config_entries.options.async_init(entry.entry_id) result = await hass.config_entries.options.async_configure( - result["flow_id"], {CONF_SCAN_INTERVAL: 10} + result["flow_id"], + {CONF_SCAN_INTERVAL: 10, CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC}, ) await hass.async_block_till_done() assert result["type"] == "create_entry" assert result["data"][CONF_SCAN_INTERVAL] == 10 -async def test_default_options(hass): - """Test default options for Kraken.""" - with patch( - "herepy.DestinationWeatherApi.weather_for_coordinates", - return_value=daily_simple_forecasts_response, - ): - entry = MockConfigEntry( - domain=DOMAIN, - data={ - CONF_API_KEY: "test", - CONF_NAME: DOMAIN, - CONF_MODE: DEFAULT_MODE, - CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC, - CONF_LATITUDE: "40.79962", - CONF_LONGITUDE: "-73.970314", - }, - options={CONF_SCAN_INTERVAL: 60}, - ) - entry.add_to_hass(hass) - result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == "form" - assert result["step_id"] == "init" - - async def test_unload_entry(hass): """Test unloading a config entry removes all entities.""" with patch( @@ -312,18 +184,16 @@ async def test_unload_entry(hass): CONF_API_KEY: "test", CONF_NAME: DOMAIN, CONF_MODE: DEFAULT_MODE, - CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC, CONF_LATITUDE: "40.79962", CONF_LONGITUDE: "-73.970314", }, - options={CONF_SCAN_INTERVAL: 60}, ) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() - assert len(hass.states.async_all()) == (len(DAILY_SIMPLE_ATTRIBUTES) + 1) + assert len(hass.states.async_all()) == (len(DAILY_SIMPLE_ATTRIBUTES)) assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 diff --git a/tests/components/here_weather/test_sensor.py b/tests/components/here_weather/test_sensor.py index 393dd833eb334e..dc7af597efe7ce 100644 --- a/tests/components/here_weather/test_sensor.py +++ b/tests/components/here_weather/test_sensor.py @@ -5,20 +5,20 @@ from homeassistant.components.here_weather.const import ( ASTRONOMY_ATTRIBUTES, - CONF_API_KEY, DEFAULT_MODE, DEFAULT_SCAN_INTERVAL, DOMAIN, MODE_ASTRONOMY, ) from homeassistant.const import ( + CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_MODE, CONF_NAME, + CONF_SCAN_INTERVAL, CONF_UNIT_SYSTEM, CONF_UNIT_SYSTEM_IMPERIAL, - CONF_UNIT_SYSTEM_METRIC, EVENT_HOMEASSISTANT_START, ) import homeassistant.util.dt as dt_util @@ -44,7 +44,6 @@ async def test_sensor_invalid_request(hass): CONF_API_KEY: "test", CONF_NAME: DOMAIN, CONF_MODE: DEFAULT_MODE, - CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC, CONF_LATITUDE: "40.79962", CONF_LONGITUDE: "-73.970314", }, @@ -83,7 +82,6 @@ async def test_forecast_astronomy(hass): CONF_API_KEY: "test", CONF_NAME: DOMAIN, CONF_MODE: MODE_ASTRONOMY, - CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC, CONF_LATITUDE: "40.79962", CONF_LONGITUDE: "-73.970314", }, @@ -118,6 +116,10 @@ async def test_imperial(hass): CONF_LATITUDE: "40.79962", CONF_LONGITUDE: "-73.970314", }, + options={ + CONF_SCAN_INTERVAL: 60, + CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_IMPERIAL, + }, ) entry.add_to_hass(hass) diff --git a/tests/components/here_weather/test_weather.py b/tests/components/here_weather/test_weather.py deleted file mode 100644 index ccec0f9df131ca..00000000000000 --- a/tests/components/here_weather/test_weather.py +++ /dev/null @@ -1,69 +0,0 @@ -"""Tests for the here_weather weather platform.""" -import pytest - -from homeassistant.components.here_weather.const import ( - CONF_API_KEY, - DOMAIN, - MODE_DAILY, - MODE_DAILY_SIMPLE, - MODE_HOURLY, - MODE_OBSERVATION, -) -from homeassistant.const import ( - CONF_LATITUDE, - CONF_LONGITUDE, - CONF_MODE, - CONF_NAME, - CONF_UNIT_SYSTEM, - CONF_UNIT_SYSTEM_IMPERIAL, - EVENT_HOMEASSISTANT_START, -) - -from .const import ( - daily_response, - daily_simple_forecasts_response, - hourly_response, - observation_response, -) - -from tests.async_mock import patch -from tests.common import MockConfigEntry - - -@pytest.mark.parametrize( - "conf_mode, return_value", - [ - (MODE_DAILY, daily_response), - (MODE_DAILY_SIMPLE, daily_simple_forecasts_response), - (MODE_OBSERVATION, observation_response), - (MODE_HOURLY, hourly_response), - ], -) -async def test_weather_imperial(hass, conf_mode, return_value): - """Test that weather has a value.""" - with patch( - "herepy.DestinationWeatherApi.weather_for_coordinates", - return_value=return_value, - ): - entry = MockConfigEntry( - domain=DOMAIN, - data={ - CONF_API_KEY: "test", - CONF_NAME: DOMAIN, - CONF_MODE: conf_mode, - CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_IMPERIAL, - CONF_LATITUDE: "40.79962", - CONF_LONGITUDE: "-73.970314", - }, - ) - entry.add_to_hass(hass) - - await hass.config_entries.async_setup(entry.entry_id) - - await hass.async_block_till_done() - - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) - await hass.async_block_till_done() - - sensor = hass.states.get("weather.here_weather") - assert sensor.state == "cloudy" diff --git a/tests/fixtures/here_weather/astronomy.json b/tests/fixtures/here_weather/astronomy.json new file mode 100644 index 00000000000000..aaf0b30a71a469 --- /dev/null +++ b/tests/fixtures/here_weather/astronomy.json @@ -0,0 +1,27 @@ +{ + "astronomy": { + "astronomy": [ + { + "sunrise": "6:55AM", + "sunset": "6:33PM", + "moonrise": "1:27PM", + "moonset": "10:58PM", + "moonPhase": 0.328, + "moonPhaseDesc": "Waxing crescent", + "iconName": "cw_waxing_crescent", + "city": "Edgewater", + "latitude": 40.82, + "longitude": -73.97, + "utcTime": "2019-10-04T00:00:00.000-04:00" + } + ], + "country": "United States", + "state": "New York", + "city": "New York", + "latitude": 40.79962, + "longitude": -73.970314, + "timezone": -5 + }, + "feedCreation": "2019-10-04T14:22:46.164Z", + "metric": true +} \ No newline at end of file diff --git a/tests/fixtures/here_weather/daily.json b/tests/fixtures/here_weather/daily.json new file mode 100644 index 00000000000000..5ec55882fd3426 --- /dev/null +++ b/tests/fixtures/here_weather/daily.json @@ -0,0 +1,48 @@ +{ + "forecasts": { + "forecastLocation": { + "forecast": [ + { + "daylight": "D", + "daySegment": "Afternoon", + "description": "Overcast. Chilly.", + "skyInfo": "18", + "skyDescription": "Overcast", + "temperature": "4.00", + "temperatureDesc": "Chilly", + "comfort": "0.86", + "humidity": "73", + "dewPoint": "-0.40", + "precipitationProbability": "9", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "12.96", + "windDirection": "294", + "windDesc": "Northwest", + "windDescShort": "NW", + "beaufortScale": "3", + "beaufortDescription": "Gentle breeze", + "visibility": "11.38", + "icon": "7", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/17.png", + "dayOfWeek": "3", + "weekday": "Tuesday", + "utcTime": "2019-11-19T12:00:00.000-05:00" + } + ], + "country": "United States", + "state": "New York", + "city": "New York", + "latitude": 43.00035, + "longitude": -75.4999, + "distance": 0.00, + "timezone": -5 + } + }, + "feedCreation": "2019-11-19T22:49:09.348Z", + "metric": true +} \ No newline at end of file diff --git a/tests/fixtures/here_weather/daily_simple_forecasts.json b/tests/fixtures/here_weather/daily_simple_forecasts.json new file mode 100644 index 00000000000000..6c3d7037e9b038 --- /dev/null +++ b/tests/fixtures/here_weather/daily_simple_forecasts.json @@ -0,0 +1,50 @@ +{ + "dailyForecasts": { + "forecastLocation": { + "forecast": [ + { + "daylight": "D", + "description": "Overcast. Chilly.", + "skyInfo": "18", + "skyDescription": "Overcast", + "temperatureDesc": "Chilly", + "comfort": "-0.55", + "highTemperature": "4.00", + "lowTemperature": "-1.80", + "humidity": "80", + "dewPoint": "-0.40", + "precipitationProbability": "53", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "1.42", + "airInfo": "*", + "airDescription": "", + "windSpeed": "12.03", + "windDirection": "290", + "windDesc": "West", + "windDescShort": "W", + "beaufortScale": "3", + "beaufortDescription": "Gentle breeze", + "uvIndex": "0", + "uvDesc": "Minimal", + "barometerPressure": "1014.29", + "icon": "7", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/17.png", + "dayOfWeek": "3", + "weekday": "Tuesday", + "utcTime": "2019-11-19T00:00:00.000-05:00" + } + ], + "country": "United States", + "state": "New York", + "city": "New York", + "latitude": 43.00035, + "longitude": -75.4999, + "distance": 0.00, + "timezone": -5 + } + }, + "feedCreation": "2019-11-19T23:01:26.632Z", + "metric": true +} \ No newline at end of file diff --git a/tests/fixtures/here_weather/hourly.json b/tests/fixtures/here_weather/hourly.json new file mode 100644 index 00000000000000..6d35c76524828e --- /dev/null +++ b/tests/fixtures/here_weather/hourly.json @@ -0,0 +1,47 @@ +{ + "hourlyForecasts": { + "forecastLocation": { + "forecast": [ + { + "daylight": "D", + "description": "Overcast. Chilly.", + "skyInfo": "18", + "skyDescription": "Overcast", + "temperature": "4.00", + "temperatureDesc": "Chilly", + "comfort": "0.67", + "humidity": "74", + "dewPoint": "-0.30", + "precipitationProbability": "5", + "precipitationDesc": "", + "rainFall": "*", + "snowFall": "*", + "airInfo": "*", + "airDescription": "", + "windSpeed": "14.04", + "windDirection": "287", + "windDesc": "West", + "windDescShort": "W", + "visibility": "11.31", + "icon": "7", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/17.png", + "dayOfWeek": "3", + "weekday": "Tuesday", + "utcTime": "2019-11-19T13:00:00.000-05:00", + "localTime": "1311192019", + "localTimeFormat": "HHMMddyyyy" + } + ], + "country": "United States", + "state": "New York", + "city": "New York", + "latitude": 43.00035, + "longitude": -75.4999, + "distance": 0.00, + "timezone": -5 + } + }, + "feedCreation": "2019-11-19T23:06:04.631Z", + "metric": true +} \ No newline at end of file diff --git a/tests/fixtures/here_weather/observation.json b/tests/fixtures/here_weather/observation.json new file mode 100644 index 00000000000000..3932393c1b6efe --- /dev/null +++ b/tests/fixtures/here_weather/observation.json @@ -0,0 +1,61 @@ +{ + "observations": { + "location": [ + { + "observation": [ + { + "daylight": "D", + "description": "Overcast. Cool.", + "skyInfo": "18", + "skyDescription": "Overcast", + "temperature": "46.00", + "temperatureDesc": "Cool", + "comfort": "42.39", + "highTemperature": "50.72", + "lowTemperature": "39.74", + "humidity": "63", + "dewPoint": "34.00", + "precipitation1H": "*", + "precipitation3H": "*", + "precipitation6H": "*", + "precipitation12H": "*", + "precipitation24H": "*", + "precipitationDesc": "", + "airInfo": "*", + "airDescription": "", + "windSpeed": "6.89", + "windDirection": "0", + "windDesc": "North", + "windDescShort": "N", + "barometerPressure": "29.80", + "barometerTrend": "Rising", + "visibility": "10.00", + "snowCover": "*", + "icon": "7", + "iconName": "cloudy", + "iconLink": "https://weather.cit.api.here.com/static/weather/icon/17.png", + "ageMinutes": "46", + "activeAlerts": "6", + "country": "United States", + "state": "New York", + "city": "New York", + "latitude": 40.7996, + "longitude": -73.9703, + "distance": 1.43, + "elevation": 0.00, + "utcTime": "2019-11-19T15:51:00.000-05:00" + } + ], + "country": "United States", + "state": "New York", + "city": "New York", + "latitude": 40.79962, + "longitude": -73.970314, + "distance": 0.00, + "timezone": -5 + } + ] + }, + "feedCreation": "2019-11-19T21:37:57.279Z", + "metric": false +} \ No newline at end of file From cae2ca3478f50f0f71bdb9807045445754617a4e Mon Sep 17 00:00:00 2001 From: Kevin Eifinger Date: Fri, 18 Sep 2020 16:13:47 +0200 Subject: [PATCH 13/56] bump herepy to 3.0.2 --- homeassistant/components/here_travel_time/manifest.json | 2 +- homeassistant/components/here_weather/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/here_travel_time/manifest.json b/homeassistant/components/here_travel_time/manifest.json index 015d8bed592506..b5505f6d27bada 100644 --- a/homeassistant/components/here_travel_time/manifest.json +++ b/homeassistant/components/here_travel_time/manifest.json @@ -2,7 +2,7 @@ "domain": "here_travel_time", "name": "HERE Travel Time", "documentation": "https://www.home-assistant.io/integrations/here_travel_time", - "requirements": ["herepy==3.0.1"], + "requirements": ["herepy==3.0.2"], "codeowners": ["@eifinger"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/here_weather/manifest.json b/homeassistant/components/here_weather/manifest.json index 621d29f2924766..1fb1326f05937c 100644 --- a/homeassistant/components/here_weather/manifest.json +++ b/homeassistant/components/here_weather/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/here_weather", "requirements": [ - "herepy==3.0.1" + "herepy==3.0.2" ], "codeowners": [ "@eifinger" diff --git a/requirements_all.txt b/requirements_all.txt index a80b962158afe9..8da52882ab92c8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -767,7 +767,7 @@ heatmiserV3==1.1.18 # homeassistant.components.here_travel_time # homeassistant.components.here_weather -herepy==3.0.1 +herepy==3.0.2 # homeassistant.components.hikvisioncam hikvision==0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dcc98317ee6275..05b88a7842d71f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -443,7 +443,7 @@ hdate==0.10.2 # homeassistant.components.here_travel_time # homeassistant.components.here_weather -herepy==3.0.1 +herepy==3.0.2 # homeassistant.components.hlk_sw16 hlk-sw16==0.0.9 From 37b3e6dcb003fa2bbaa50d794a65aaf305770da6 Mon Sep 17 00:00:00 2001 From: Kevin Eifinger Date: Fri, 18 Sep 2020 16:49:11 +0200 Subject: [PATCH 14/56] update fixture for here_travel_time for 3.0.2 --- .../routing_error_invalid_credentials.json | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/tests/fixtures/here_travel_time/routing_error_invalid_credentials.json b/tests/fixtures/here_travel_time/routing_error_invalid_credentials.json index 81fb246178c938..e1be0ed42123e3 100644 --- a/tests/fixtures/here_travel_time/routing_error_invalid_credentials.json +++ b/tests/fixtures/here_travel_time/routing_error_invalid_credentials.json @@ -1,15 +1,4 @@ { - "_type": "ns2:RoutingServiceErrorType", - "type": "PermissionError", - "subtype": "InvalidCredentials", - "details": "This is not a valid app_id and app_code pair. Please verify that the values are not swapped between the app_id and app_code and the values provisioned by HERE (either by your customer representative or via http://developer.here.com/myapps) were copied correctly into the request.", - "metaInfo": { - "timestamp": "2019-07-10T09:43:14Z", - "mapVersion": "8.30.98.152", - "moduleVersion": "7.2.201927-4307", - "interfaceVersion": "2.6.64", - "availableMapVersion": [ - "8.30.98.152" - ] - } -} \ No newline at end of file + "error": "Unauthorized", + "error_description": "No credentials found" + } \ No newline at end of file From 18b7dbbc2710bb131b5d0ec4e673f33fc615c0f9 Mon Sep 17 00:00:00 2001 From: Kevin Eifinger Date: Sun, 4 Oct 2020 13:09:11 +0200 Subject: [PATCH 15/56] Use more common strings --- homeassistant/components/here_weather/strings.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/here_weather/strings.json b/homeassistant/components/here_weather/strings.json index b51149e86c6033..d79ded0be8754a 100644 --- a/homeassistant/components/here_weather/strings.json +++ b/homeassistant/components/here_weather/strings.json @@ -4,13 +4,13 @@ "step": { "user": { "title": "HERE Destination Weather", - "description": "This will configure the HERE Destination Weather Integration.", + "description": "[%key:common::config_flow::description::confirm_setup%]", "data": { "api_key": "[%key:common::config_flow::data::api_key%]", - "name": "Name of this Integration Entry", + "name": "[%key:common::config_flow::data::name%]", "mode": "The Weather Mode to use", - "latitude": "Latitude", - "longitude": "Longitude" + "latitude": "[%key:common::config_flow::data::latitude%]", + "longitude": "[%key:common::config_flow::data::longitude%]" } } }, From 0f460ed39e3186bbd4d840b6cfb2e3c593fa2e59 Mon Sep 17 00:00:00 2001 From: Kevin Eifinger Date: Wed, 13 Jan 2021 21:58:52 +0100 Subject: [PATCH 16/56] Apply suggestions from code review --- .../components/here_weather/__init__.py | 6 +-- .../components/here_weather/config_flow.py | 21 --------- .../components/here_weather/const.py | 6 --- .../components/here_weather/sensor.py | 2 +- .../components/here_weather/strings.json | 3 -- .../here_weather/test_config_flow.py | 47 +------------------ tests/components/here_weather/test_sensor.py | 6 +-- 7 files changed, 5 insertions(+), 86 deletions(-) diff --git a/homeassistant/components/here_weather/__init__.py b/homeassistant/components/here_weather/__init__.py index 142d9b9a14175e..baf7efe0fac7f5 100644 --- a/homeassistant/components/here_weather/__init__.py +++ b/homeassistant/components/here_weather/__init__.py @@ -13,7 +13,6 @@ CONF_LONGITUDE, CONF_MODE, CONF_SCAN_INTERVAL, - CONF_UNIT_SYSTEM, CONF_UNIT_SYSTEM_METRIC, ) from homeassistant.core import HomeAssistant @@ -79,14 +78,12 @@ def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None: self.weather_product_type = herepy.WeatherProductType[ config_entry.data[CONF_MODE] ] - self.units = None self.coordinator = None self.unsub_handler = None async def async_setup(self) -> list: """Set up the here_weather integration.""" self.add_options() - self.units = self.config_entry.options[CONF_UNIT_SYSTEM] self.unsub_handler = self.config_entry.add_update_listener( self.async_options_updated ) @@ -106,7 +103,6 @@ def add_options(self) -> None: if not self.config_entry.options: options = { CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL, - CONF_UNIT_SYSTEM: self.hass.config.units.name, } self.hass.config_entries.async_update_entry( self.config_entry, options=options @@ -124,7 +120,7 @@ async def async_update(self) -> None: def _get_data(self): """Get the latest data from HERE.""" - is_metric = self.units == CONF_UNIT_SYSTEM_METRIC + is_metric = self.hass.config.units.name == CONF_UNIT_SYSTEM_METRIC data = self.here_client.weather_for_coordinates( self.latitude, self.longitude, diff --git a/homeassistant/components/here_weather/config_flow.py b/homeassistant/components/here_weather/config_flow.py index 37d167fca8c9ff..1ff5488a692eb8 100644 --- a/homeassistant/components/here_weather/config_flow.py +++ b/homeassistant/components/here_weather/config_flow.py @@ -14,12 +14,8 @@ CONF_MODE, CONF_NAME, CONF_SCAN_INTERVAL, - CONF_UNIT_SYSTEM, - CONF_UNIT_SYSTEM_IMPERIAL, - CONF_UNIT_SYSTEM_METRIC, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv from .const import ( @@ -35,7 +31,6 @@ async def async_validate_user_input(hass: HomeAssistant, user_input: dict) -> None: """Validate the user_input containing coordinates.""" - await async_validate_name(hass, user_input) here_client = herepy.DestinationWeatherApi(user_input[CONF_API_KEY]) await hass.async_add_executor_job( here_client.weather_for_coordinates, @@ -45,13 +40,6 @@ async def async_validate_user_input(hass: HomeAssistant, user_input: dict) -> No ) -async def async_validate_name(hass: HomeAssistant, user_input: dict) -> None: - """Validate the user input.""" - for entry in hass.config_entries.async_entries(DOMAIN): - if entry.data[CONF_NAME] == user_input[CONF_NAME]: - raise AlreadyConfigured - - class HereWeatherConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for here_weather.""" @@ -73,8 +61,6 @@ async def async_step_user(self, user_input=None): return self.async_create_entry( title=user_input[CONF_NAME], data=user_input ) - except AlreadyConfigured: - return self.async_abort(reason="already_configured") except herepy.InvalidRequestError: errors["base"] = "invalid_request" except herepy.UnauthorizedError: @@ -139,13 +125,6 @@ async def async_step_init(self, user_input=None): CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL ), ): int, - vol.Optional(CONF_UNIT_SYSTEM, default=self.hass.config.units.name): vol.In( - [CONF_UNIT_SYSTEM_METRIC, CONF_UNIT_SYSTEM_IMPERIAL] - ), } return self.async_show_form(step_id="init", data_schema=vol.Schema(options)) - - -class AlreadyConfigured(HomeAssistantError): - """Error to indicate the asset pair is already configured.""" diff --git a/homeassistant/components/here_weather/const.py b/homeassistant/components/here_weather/const.py index 5b84d76942980f..560e0b63dc11a2 100644 --- a/homeassistant/components/here_weather/const.py +++ b/homeassistant/components/here_weather/const.py @@ -209,16 +209,10 @@ "icon": {"name": "Icon", "unit_of_measurement": None}, "iconName": {"name": "Icon Name", "unit_of_measurement": None}, "iconLink": {"name": "Icon Link", "unit_of_measurement": None}, - "ageMinutes": {"name": "Age In Minutes", "unit_of_measurement": "min"}, "activeAlerts": {"name": "Active Alerts", "unit_of_measurement": None}, "country": {"name": "Country", "unit_of_measurement": None}, "state": {"name": "State", "unit_of_measurement": None}, "city": {"name": "City", "unit_of_measurement": None}, - "latitude": {"name": "Latitude", "unit_of_measurement": None}, - "longitude": {"name": "Longitude", "unit_of_measurement": None}, - "distance": {"name": "Distance", "unit_of_measurement": "km"}, - "elevation": {"name": "Elevation", "unit_of_measurement": "km"}, - "utcTime": {"name": "UTC Time", "unit_of_measurement": None}, } SENSOR_TYPES = { diff --git a/homeassistant/components/here_weather/sensor.py b/homeassistant/components/here_weather/sensor.py index 34190844c43bf2..6d7ee871744d25 100644 --- a/homeassistant/components/here_weather/sensor.py +++ b/homeassistant/components/here_weather/sensor.py @@ -54,7 +54,7 @@ def __init__( self._sensor_number = sensor_number self._weather_attribute = weather_attribute self._unit_of_measurement = convert_unit_of_measurement_if_needed( - self._here_data.units, + self._here_data.coordinator.hass.config.units.name, SENSOR_TYPES[sensor_type][weather_attribute]["unit_of_measurement"], ) diff --git a/homeassistant/components/here_weather/strings.json b/homeassistant/components/here_weather/strings.json index d79ded0be8754a..e3be2ff41d4e42 100644 --- a/homeassistant/components/here_weather/strings.json +++ b/homeassistant/components/here_weather/strings.json @@ -17,9 +17,6 @@ "error": { "invalid_request": "HERE reported an invalid request. This indicates the supplied location is not valid.", "unauthorized": "Invalid credentials. This error is returned if the specified token was invalid or no contract could be found for this token." - }, - "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } }, "options": { diff --git a/tests/components/here_weather/test_config_flow.py b/tests/components/here_weather/test_config_flow.py index dbdd6fd4086a3e..de6cbf79ecc6e8 100644 --- a/tests/components/here_weather/test_config_flow.py +++ b/tests/components/here_weather/test_config_flow.py @@ -13,8 +13,6 @@ CONF_MODE, CONF_NAME, CONF_SCAN_INTERVAL, - CONF_UNIT_SYSTEM, - CONF_UNIT_SYSTEM_METRIC, EVENT_HOMEASSISTANT_START, ) @@ -99,49 +97,6 @@ async def test_invalid_request(hass): assert result["errors"]["base"] == "invalid_request" -async def test_form_already_configured(hass): - """Test is already configured.""" - with patch( - "herepy.DestinationWeatherApi.weather_for_coordinates", - return_value=daily_simple_forecasts_response, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"} - ) - assert result["type"] == "form" - config = { - CONF_API_KEY: "test", - CONF_NAME: DOMAIN, - CONF_MODE: DEFAULT_MODE, - CONF_LATITUDE: "40.79962", - CONF_LONGITUDE: "-73.970314", - } - result = await hass.config_entries.flow.async_configure( - result["flow_id"], config - ) - assert result["type"] == "create_entry" - - await hass.async_block_till_done() - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"} - ) - assert result["type"] == "form" - config = { - CONF_API_KEY: "test", - CONF_NAME: DOMAIN, - CONF_MODE: DEFAULT_MODE, - CONF_LATITUDE: "40.79962", - CONF_LONGITUDE: "-73.970314", - } - result = await hass.config_entries.flow.async_configure( - result["flow_id"], config - ) - - assert result["type"] == "abort" - assert result["reason"] == "already_configured" - - async def test_options(hass): """Test the options flow.""" with patch( @@ -165,7 +120,7 @@ async def test_options(hass): result = await hass.config_entries.options.async_init(entry.entry_id) result = await hass.config_entries.options.async_configure( result["flow_id"], - {CONF_SCAN_INTERVAL: 10, CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC}, + {CONF_SCAN_INTERVAL: 10}, ) await hass.async_block_till_done() assert result["type"] == "create_entry" diff --git a/tests/components/here_weather/test_sensor.py b/tests/components/here_weather/test_sensor.py index dc7af597efe7ce..f188fb06e9326a 100644 --- a/tests/components/here_weather/test_sensor.py +++ b/tests/components/here_weather/test_sensor.py @@ -17,11 +17,10 @@ CONF_MODE, CONF_NAME, CONF_SCAN_INTERVAL, - CONF_UNIT_SYSTEM, - CONF_UNIT_SYSTEM_IMPERIAL, EVENT_HOMEASSISTANT_START, ) import homeassistant.util.dt as dt_util +from homeassistant.util.unit_system import IMPERIAL_SYSTEM from .const import astronomy_response, daily_simple_forecasts_response @@ -106,19 +105,18 @@ async def test_imperial(hass): "herepy.DestinationWeatherApi.weather_for_coordinates", return_value=daily_simple_forecasts_response, ): + hass.config.units = IMPERIAL_SYSTEM entry = MockConfigEntry( domain=DOMAIN, data={ CONF_API_KEY: "test", CONF_NAME: DOMAIN, CONF_MODE: DEFAULT_MODE, - CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_IMPERIAL, CONF_LATITUDE: "40.79962", CONF_LONGITUDE: "-73.970314", }, options={ CONF_SCAN_INTERVAL: 60, - CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_IMPERIAL, }, ) entry.add_to_hass(hass) From 3c26fc3a312bb918d0061ff0150ec2ab0d541047 Mon Sep 17 00:00:00 2001 From: Kevin Eifinger Date: Thu, 14 Jan 2021 13:39:42 +0100 Subject: [PATCH 17/56] fix import patch --- tests/components/here_weather/test_config_flow.py | 3 ++- tests/components/here_weather/test_sensor.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/components/here_weather/test_config_flow.py b/tests/components/here_weather/test_config_flow.py index de6cbf79ecc6e8..ca266a528c59b5 100644 --- a/tests/components/here_weather/test_config_flow.py +++ b/tests/components/here_weather/test_config_flow.py @@ -1,4 +1,6 @@ """Tests for the here_weather config_flow.""" +from unittest.mock import patch + import herepy from homeassistant.components.here_weather.const import ( @@ -18,7 +20,6 @@ from .const import daily_simple_forecasts_response -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/here_weather/test_sensor.py b/tests/components/here_weather/test_sensor.py index f188fb06e9326a..8138244a6e2566 100644 --- a/tests/components/here_weather/test_sensor.py +++ b/tests/components/here_weather/test_sensor.py @@ -1,5 +1,6 @@ """Tests for the here_weather sensor platform.""" from datetime import timedelta +from unittest.mock import patch import herepy @@ -24,7 +25,6 @@ from .const import astronomy_response, daily_simple_forecasts_response -from tests.async_mock import patch from tests.common import MockConfigEntry, async_fire_time_changed From 1f481c8d1ff4f2c3a93f0f13368c0d5f76dc745e Mon Sep 17 00:00:00 2001 From: Kevin Eifinger Date: Wed, 20 Jan 2021 20:25:32 +0100 Subject: [PATCH 18/56] Apply suggestions Co-authored-by: Paulus Schoutsen --- homeassistant/components/here_weather/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/here_weather/__init__.py b/homeassistant/components/here_weather/__init__.py index baf7efe0fac7f5..f64dc31c938085 100644 --- a/homeassistant/components/here_weather/__init__.py +++ b/homeassistant/components/here_weather/__init__.py @@ -108,7 +108,7 @@ def add_options(self) -> None: self.config_entry, options=options ) - async def async_update(self) -> None: + async def async_update(self): """Handle data update with the DataUpdateCoordinator.""" try: async with async_timeout.timeout(10): From a667352a18603a4bfdb0860fa245267aa7b077c0 Mon Sep 17 00:00:00 2001 From: Kevin Eifinger Date: Sun, 14 Feb 2021 13:06:32 +0100 Subject: [PATCH 19/56] Add weather platform and multiple coordinators --- .../components/here_weather/__init__.py | 32 +-- .../components/here_weather/sensor.py | 61 ++--- .../components/here_weather/weather.py | 215 ++++++++++++++++++ tests/components/here_weather/__init__.py | 31 +++ .../here_weather/test_config_flow.py | 45 +++- tests/components/here_weather/test_sensor.py | 70 ++++-- tests/components/here_weather/test_weather.py | 116 ++++++++++ .../here_weather/daily_simple_forecasts.json | 2 +- 8 files changed, 497 insertions(+), 75 deletions(-) create mode 100644 homeassistant/components/here_weather/weather.py create mode 100644 tests/components/here_weather/test_weather.py diff --git a/homeassistant/components/here_weather/__init__.py b/homeassistant/components/here_weather/__init__.py index f64dc31c938085..25bca3c17d1a9e 100644 --- a/homeassistant/components/here_weather/__init__.py +++ b/homeassistant/components/here_weather/__init__.py @@ -11,18 +11,17 @@ CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, - CONF_MODE, CONF_SCAN_INTERVAL, CONF_UNIT_SYSTEM_METRIC, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import DEFAULT_SCAN_INTERVAL, DOMAIN, HERE_API_KEYS +from .const import CONF_MODES, DEFAULT_SCAN_INTERVAL, DOMAIN, HERE_API_KEYS _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["sensor"] +PLATFORMS = ["sensor", "weather"] async def async_setup(hass: HomeAssistant, config: dict) -> bool: @@ -32,9 +31,12 @@ async def async_setup(hass: HomeAssistant, config: dict) -> bool: async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Set up here_weather from a config entry.""" - here_weather_data = HEREWeatherData(hass, config_entry) - await here_weather_data.async_setup() - hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = here_weather_data + here_weather_data_dict = {} + for mode in CONF_MODES: + here_weather_data = HEREWeatherData(hass, config_entry, mode) + await here_weather_data.async_setup() + here_weather_data_dict[mode] = here_weather_data + hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = here_weather_data_dict known_api_keys = hass.data.setdefault(HERE_API_KEYS, []) if config_entry.data[CONF_API_KEY] not in known_api_keys: @@ -59,7 +61,8 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> ) ) if unload_ok: - hass.data[DOMAIN][config_entry.entry_id].unsub_handler() + for mode in CONF_MODES: + hass.data[DOMAIN][config_entry.entry_id][mode].unsub_handler() hass.data[DOMAIN].pop(config_entry.entry_id) return unload_ok @@ -68,16 +71,16 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> class HEREWeatherData: """Get the latest data from HERE.""" - def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None: + def __init__( + self, hass: HomeAssistant, config_entry: ConfigEntry, mode: str + ) -> None: """Initialize the data object.""" self.hass = hass self.config_entry = config_entry self.here_client = herepy.DestinationWeatherApi(config_entry.data[CONF_API_KEY]) self.latitude = config_entry.data[CONF_LATITUDE] self.longitude = config_entry.data[CONF_LONGITUDE] - self.weather_product_type = herepy.WeatherProductType[ - config_entry.data[CONF_MODE] - ] + self.weather_product_type = herepy.WeatherProductType[mode] self.coordinator = None self.unsub_handler = None @@ -136,9 +139,10 @@ async def async_options_updated( hass: HomeAssistant, config_entry: ConfigEntry ) -> None: """Triggered by config entry options updates.""" - hass.data[DOMAIN][config_entry.entry_id].set_update_interval( - config_entry.options[CONF_SCAN_INTERVAL] - ) + for mode in CONF_MODES: + hass.data[DOMAIN][config_entry.entry_id][mode].set_update_interval( + config_entry.options[CONF_SCAN_INTERVAL] + ) def set_update_interval(self, update_interval: int) -> None: """Set the coordinator update_interval to the supplied update_interval.""" diff --git a/homeassistant/components/here_weather/sensor.py b/homeassistant/components/here_weather/sensor.py index 6d7ee871744d25..0f3d06f26313e5 100644 --- a/homeassistant/components/here_weather/sensor.py +++ b/homeassistant/components/here_weather/sensor.py @@ -1,12 +1,14 @@ -"""Support for the HERE Destination Weather service.""" +"""Sensor platform for the HERE Destination Weather service.""" import logging from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_MODE, CONF_NAME +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.core import HomeAssistant -from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) -from . import HEREWeatherData from .const import DOMAIN, SENSOR_TYPES from .utils import convert_unit_of_measurement_if_needed, get_attribute_from_here_data @@ -17,20 +19,19 @@ async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities ): """Add here_weather entities from a config_entry.""" - here_weather_data = hass.data[DOMAIN][config_entry.entry_id] + here_weather_data_dict = hass.data[DOMAIN][config_entry.entry_id] sensors_to_add = [] for sensor_type in SENSOR_TYPES: - if sensor_type == config_entry.data[CONF_MODE]: - for weather_attribute in SENSOR_TYPES[sensor_type]: - sensors_to_add.append( - HEREDestinationWeatherSensor( - config_entry.data[CONF_NAME], - here_weather_data, - sensor_type, - weather_attribute, - ) + for weather_attribute in SENSOR_TYPES[sensor_type]: + sensors_to_add.append( + HEREDestinationWeatherSensor( + config_entry, + here_weather_data_dict[sensor_type].coordinator, + sensor_type, + weather_attribute, ) + ) async_add_entities(sensors_to_add, True) @@ -39,40 +40,49 @@ class HEREDestinationWeatherSensor(CoordinatorEntity): def __init__( self, - name: str, - here_data: HEREWeatherData, + config_entry: ConfigEntry, + coordinator: DataUpdateCoordinator, sensor_type: str, weather_attribute: str, sensor_number: int = 0, # Additional supported offsets will be added in a separate PR ) -> None: """Initialize the sensor.""" - super().__init__(here_data.coordinator) - self._base_name = name + super().__init__(coordinator) + self._base_name = config_entry.data[CONF_NAME] self._name_suffix = SENSOR_TYPES[sensor_type][weather_attribute]["name"] - self._here_data = here_data + self._latitude = config_entry.data[CONF_LATITUDE] + self._longitude = config_entry.data[CONF_LONGITUDE] self._sensor_type = sensor_type self._sensor_number = sensor_number self._weather_attribute = weather_attribute self._unit_of_measurement = convert_unit_of_measurement_if_needed( - self._here_data.coordinator.hass.config.units.name, + self.coordinator.hass.config.units.name, SENSOR_TYPES[sensor_type][weather_attribute]["unit_of_measurement"], ) + self._unique_id = "".join( + f"{self._base_name}_{self._sensor_type}_{self._name_suffix}_{self._sensor_number}".lower().split() + ) + + @property + def entity_registry_enabled_default(self): + """Return if the entity should be enabled when first added to the entity registry.""" + return False @property def name(self) -> str: """Return the name of the sensor.""" - return f"{self._base_name} {self._name_suffix}" + return f"{self._base_name} {self._sensor_type} {self._name_suffix} {self._sensor_number}" @property def unique_id(self) -> str: """Set unique_id for sensor.""" - return f"{self._here_data.latitude}_{self._here_data.latitude}_{self._sensor_type}_{self._weather_attribute}_{self._sensor_number}" + return self._unique_id @property def state(self) -> str: """Return the state of the device.""" return get_attribute_from_here_data( - self._here_data.coordinator.data, + self.coordinator.data, self._weather_attribute, self._sensor_number, ) @@ -82,11 +92,6 @@ def unit_of_measurement(self) -> str: """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement - @property - def available(self): - """Could the api be accessed during the last update call.""" - return self._here_data.coordinator.last_update_success - @property def device_info(self) -> dict: """Return a device description for device registry.""" diff --git a/homeassistant/components/here_weather/weather.py b/homeassistant/components/here_weather/weather.py new file mode 100644 index 00000000000000..c642944ec5a00f --- /dev/null +++ b/homeassistant/components/here_weather/weather.py @@ -0,0 +1,215 @@ +"""Weather platform for the HERE Destination Weather service.""" +import logging + +from homeassistant.components.weather import ( + ATTR_FORECAST_CONDITION, + ATTR_FORECAST_PRECIPITATION, + ATTR_FORECAST_TEMP, + ATTR_FORECAST_TEMP_LOW, + ATTR_FORECAST_TIME, + ATTR_FORECAST_WIND_BEARING, + ATTR_FORECAST_WIND_SPEED, + WeatherEntity, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_NAME, TEMP_CELSIUS +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) + +from .const import ( + CONDITION_CLASSES, + DEFAULT_MODE, + DOMAIN, + MODE_ASTRONOMY, + MODE_DAILY_SIMPLE, + SENSOR_TYPES, +) +from .utils import convert_unit_of_measurement_if_needed, get_attribute_from_here_data + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities +): + """Add here_weather entities from a config_entry.""" + here_weather_data_dict = hass.data[DOMAIN][config_entry.entry_id] + for sensor_type in SENSOR_TYPES: + if sensor_type != MODE_ASTRONOMY: + async_add_entities( + [ + HEREDestinationWeather( + config_entry, + here_weather_data_dict[sensor_type].coordinator, + sensor_type, + ) + ], + True, + ) + + +class HEREDestinationWeather(CoordinatorEntity, WeatherEntity): + """Implementation of an HERE Destination Weather WeatherEntity.""" + + def __init__( + self, config_entry: ConfigEntry, coordinator: DataUpdateCoordinator, mode: str + ): + """Initialize the sensor.""" + super().__init__(coordinator) + self._name = config_entry.data[CONF_NAME] + self._mode = mode + + @property + def name(self): + """Return the name of the sensor.""" + return f"{self._name} {self._mode}" + + @property + def unique_id(self): + """Set unique_id for sensor.""" + return f"{self._name}_{self._mode}" + + @property + def condition(self): + """Return the current condition.""" + return get_condition_from_here_data(self.coordinator.data) + + @property + def temperature(self) -> float: + """Return the temperature.""" + return get_temperature_from_here_data(self.coordinator.data, self._mode) + + @property + def temperature_unit(self): + """Return the unit of measurement.""" + return convert_unit_of_measurement_if_needed( + self.coordinator.hass.config.units.name, TEMP_CELSIUS + ) + + @property + def pressure(self): + """Return the pressure.""" + return None + + @property + def humidity(self): + """Return the humidity.""" + get_attribute_from_here_data(self.coordinator.data, "humidity") + + @property + def wind_speed(self): + """Return the wind speed.""" + get_attribute_from_here_data(self.coordinator.data, "windSpeed") + + @property + def wind_bearing(self): + """Return the wind bearing.""" + get_attribute_from_here_data(self.coordinator.data, "windDirection") + + @property + def attribution(self): + """Return the attribution.""" + return None + + @property + def forecast(self): + """Return the forecast array.""" + if self.coordinator.data is None: + return None + data = [] + for offset in range(len(self.coordinator.data)): + data.append( + { + ATTR_FORECAST_TIME: get_attribute_from_here_data( + self.coordinator.data, "utcTime", offset + ), + ATTR_FORECAST_TEMP: get_high_or_default_temperature_from_here_data( + self.coordinator.data, self._mode, offset + ), + ATTR_FORECAST_TEMP_LOW: get_low_or_default_temperature_from_here_data( + self.coordinator.data, self._mode, offset + ), + ATTR_FORECAST_PRECIPITATION: calc_precipitation( + self.coordinator.data, offset + ), + ATTR_FORECAST_WIND_SPEED: get_attribute_from_here_data( + self.coordinator.data, "windSpeed", offset + ), + ATTR_FORECAST_WIND_BEARING: get_attribute_from_here_data( + self.coordinator.data, "windDirection", offset + ), + ATTR_FORECAST_CONDITION: get_condition_from_here_data( + self.coordinator.data, offset + ), + } + ) + return data + + @property + def entity_registry_enabled_default(self): + """Return if the entity should be enabled when first added to the entity registry.""" + return self._mode == DEFAULT_MODE + + @property + def device_info(self) -> dict: + """Return a device description for device registry.""" + + return { + "identifiers": {(DOMAIN, self._name)}, + "name": self._name, + "manufacturer": "here.com", + } + + +def get_condition_from_here_data(here_data: list, offset: int = 0) -> str: + """Return the condition from here_data.""" + try: + return [ + k + for k, v in CONDITION_CLASSES.items() + if get_attribute_from_here_data(here_data, "iconName", offset) in v + ][0] + except IndexError: + return None # Fallback if the API introduces new values + + +def get_high_or_default_temperature_from_here_data( + here_data: list, mode: str, offset: int = 0 +) -> str: + """Return the temperature from here_data.""" + temperature = get_attribute_from_here_data(here_data, "highTemperature", offset) + if temperature is not None: + return float(temperature) + + return get_temperature_from_here_data(here_data, mode, offset) + + +def get_low_or_default_temperature_from_here_data( + here_data: list, mode: str, offset: int = 0 +) -> str: + """Return the temperature from here_data.""" + temperature = get_attribute_from_here_data(here_data, "lowTemperature", offset) + if temperature is not None: + return float(temperature) + return get_temperature_from_here_data(here_data, mode, offset) + + +def get_temperature_from_here_data(here_data: list, mode: str, offset: int = 0) -> str: + """Return the temperature from here_data.""" + if mode == MODE_DAILY_SIMPLE: + temperature = get_attribute_from_here_data(here_data, "highTemperature", offset) + else: + temperature = get_attribute_from_here_data(here_data, "temperature", offset) + if temperature is not None: + return float(temperature) + + +def calc_precipitation(here_data: list, offset: int = 0) -> float: + """Calculate Precipitation.""" + rain_fall = get_attribute_from_here_data(here_data, "rainFall", offset) + snow_fall = get_attribute_from_here_data(here_data, "snowFall", offset) + if rain_fall is not None and snow_fall is not None: + return float(rain_fall) + float(snow_fall) diff --git a/tests/components/here_weather/__init__.py b/tests/components/here_weather/__init__.py index 6810cc1f67bdff..64cc1960b11e97 100644 --- a/tests/components/here_weather/__init__.py +++ b/tests/components/here_weather/__init__.py @@ -1 +1,32 @@ """Tests for here_weather component.""" +import herepy + +from homeassistant.components.here_weather.const import ( + MODE_ASTRONOMY, + MODE_DAILY, + MODE_DAILY_SIMPLE, + MODE_HOURLY, + MODE_OBSERVATION, +) + +from .const import ( + astronomy_response, + daily_response, + daily_simple_forecasts_response, + hourly_response, + observation_response, +) + + +def mock_weather_for_coordinates(*args, **kwargs): + """Return mock data for request weather product type.""" + if args[2] == herepy.WeatherProductType[MODE_ASTRONOMY]: + return astronomy_response + elif args[2] == herepy.WeatherProductType[MODE_HOURLY]: + return hourly_response + elif args[2] == herepy.WeatherProductType[MODE_DAILY]: + return daily_response + elif args[2] == herepy.WeatherProductType[MODE_DAILY_SIMPLE]: + return daily_simple_forecasts_response + elif args[2] == herepy.WeatherProductType[MODE_OBSERVATION]: + return observation_response diff --git a/tests/components/here_weather/test_config_flow.py b/tests/components/here_weather/test_config_flow.py index ca266a528c59b5..9e5afafc47cfe2 100644 --- a/tests/components/here_weather/test_config_flow.py +++ b/tests/components/here_weather/test_config_flow.py @@ -4,9 +4,9 @@ import herepy from homeassistant.components.here_weather.const import ( - DAILY_SIMPLE_ATTRIBUTES, DEFAULT_MODE, DOMAIN, + HERE_API_KEYS, ) from homeassistant.const import ( CONF_API_KEY, @@ -18,7 +18,7 @@ EVENT_HOMEASSISTANT_START, ) -from .const import daily_simple_forecasts_response +from . import mock_weather_for_coordinates from tests.common import MockConfigEntry @@ -27,7 +27,7 @@ async def test_config_flow(hass): """Test we can finish a config flow.""" with patch( "herepy.DestinationWeatherApi.weather_for_coordinates", - return_value=daily_simple_forecasts_response, + side_effect=mock_weather_for_coordinates, ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "user"} @@ -36,7 +36,6 @@ async def test_config_flow(hass): config = { CONF_API_KEY: "test", CONF_NAME: DOMAIN, - CONF_MODE: DEFAULT_MODE, CONF_LATITUDE: "40.79962", CONF_LONGITUDE: "-73.970314", } @@ -46,7 +45,34 @@ async def test_config_flow(hass): assert result["type"] == "create_entry" await hass.async_block_till_done() - state = hass.states.get("sensor.here_weather_low_temperature") + state = hass.states.get("weather.here_weather_forecast_7days_simple") + assert state + + +async def test_config_flow_known_api_key(hass): + """Test we can finish a config flow.""" + with patch( + "herepy.DestinationWeatherApi.weather_for_coordinates", + side_effect=mock_weather_for_coordinates, + ): + hass.data.setdefault(HERE_API_KEYS, []).append("test") + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"} + ) + assert result["type"] == "form" + config = { + CONF_API_KEY: "test", + CONF_NAME: DOMAIN, + CONF_LATITUDE: "40.79962", + CONF_LONGITUDE: "-73.970314", + } + result = await hass.config_entries.flow.async_configure( + result["flow_id"], config + ) + assert result["type"] == "create_entry" + + await hass.async_block_till_done() + state = hass.states.get("weather.here_weather_forecast_7days_simple") assert state @@ -87,7 +113,6 @@ async def test_invalid_request(hass): config = { CONF_API_KEY: "test", CONF_NAME: DOMAIN, - CONF_MODE: DEFAULT_MODE, CONF_LATITUDE: "40.79962", CONF_LONGITUDE: "-73.970314", } @@ -102,14 +127,13 @@ async def test_options(hass): """Test the options flow.""" with patch( "herepy.DestinationWeatherApi.weather_for_coordinates", - return_value=daily_simple_forecasts_response, + side_effect=mock_weather_for_coordinates, ): entry = MockConfigEntry( domain=DOMAIN, data={ CONF_API_KEY: "test", CONF_NAME: DOMAIN, - CONF_MODE: DEFAULT_MODE, CONF_LATITUDE: "40.79962", CONF_LONGITUDE: "-73.970314", }, @@ -132,14 +156,13 @@ async def test_unload_entry(hass): """Test unloading a config entry removes all entities.""" with patch( "herepy.DestinationWeatherApi.weather_for_coordinates", - return_value=daily_simple_forecasts_response, + side_effect=mock_weather_for_coordinates, ): entry = MockConfigEntry( domain=DOMAIN, data={ CONF_API_KEY: "test", CONF_NAME: DOMAIN, - CONF_MODE: DEFAULT_MODE, CONF_LATITUDE: "40.79962", CONF_LONGITUDE: "-73.970314", }, @@ -149,7 +172,7 @@ async def test_unload_entry(hass): await hass.async_block_till_done() hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() - assert len(hass.states.async_all()) == (len(DAILY_SIMPLE_ATTRIBUTES)) + assert len(hass.states.async_all()) == 1 assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 diff --git a/tests/components/here_weather/test_sensor.py b/tests/components/here_weather/test_sensor.py index 8138244a6e2566..4932aec4121a8b 100644 --- a/tests/components/here_weather/test_sensor.py +++ b/tests/components/here_weather/test_sensor.py @@ -4,18 +4,11 @@ import herepy -from homeassistant.components.here_weather.const import ( - ASTRONOMY_ATTRIBUTES, - DEFAULT_MODE, - DEFAULT_SCAN_INTERVAL, - DOMAIN, - MODE_ASTRONOMY, -) +from homeassistant.components.here_weather.const import DEFAULT_SCAN_INTERVAL, DOMAIN from homeassistant.const import ( CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, - CONF_MODE, CONF_NAME, CONF_SCAN_INTERVAL, EVENT_HOMEASSISTANT_START, @@ -23,7 +16,7 @@ import homeassistant.util.dt as dt_util from homeassistant.util.unit_system import IMPERIAL_SYSTEM -from .const import astronomy_response, daily_simple_forecasts_response +from . import mock_weather_for_coordinates from tests.common import MockConfigEntry, async_fire_time_changed @@ -35,20 +28,30 @@ async def test_sensor_invalid_request(hass): with patch("homeassistant.util.dt.utcnow", return_value=utcnow): with patch( "herepy.DestinationWeatherApi.weather_for_coordinates", - return_value=daily_simple_forecasts_response, + side_effect=mock_weather_for_coordinates, ): entry = MockConfigEntry( domain=DOMAIN, data={ CONF_API_KEY: "test", CONF_NAME: DOMAIN, - CONF_MODE: DEFAULT_MODE, CONF_LATITUDE: "40.79962", CONF_LONGITUDE: "-73.970314", }, ) entry.add_to_hass(hass) + registry = await hass.helpers.entity_registry.async_get_registry() + + # Pre-create registry entries for disabled by default sensors + registry.async_get_or_create( + "sensor", + DOMAIN, + "here_weather_forecast_7days_simple_windspeed_0", + suggested_object_id="here_weather_forecast_7days_simple_windspeed_0", + disabled_by=None, + ) + await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -56,15 +59,19 @@ async def test_sensor_invalid_request(hass): hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() - sensor = hass.states.get("sensor.here_weather_low_temperature") - assert sensor.state == "-1.80" + sensor = hass.states.get( + "sensor.here_weather_forecast_7days_simple_windspeed_0" + ) + assert sensor.state == "12.03" with patch( "herepy.DestinationWeatherApi.weather_for_coordinates", side_effect=herepy.InvalidRequestError("Invalid"), ): async_fire_time_changed(hass, utcnow + timedelta(DEFAULT_SCAN_INTERVAL * 2)) await hass.async_block_till_done() - sensor = hass.states.get("sensor.here_weather_low_temperature") + sensor = hass.states.get( + "sensor.here_weather_forecast_7days_simple_windspeed_0" + ) assert sensor.state == "unavailable" @@ -73,20 +80,30 @@ async def test_forecast_astronomy(hass): # Patching 'utcnow' to gain more control over the timed update. with patch( "herepy.DestinationWeatherApi.weather_for_coordinates", - return_value=astronomy_response, + side_effect=mock_weather_for_coordinates, ): entry = MockConfigEntry( domain=DOMAIN, data={ CONF_API_KEY: "test", CONF_NAME: DOMAIN, - CONF_MODE: MODE_ASTRONOMY, CONF_LATITUDE: "40.79962", CONF_LONGITUDE: "-73.970314", }, ) entry.add_to_hass(hass) + registry = await hass.helpers.entity_registry.async_get_registry() + + # Pre-create registry entries for disabled by default sensors + registry.async_get_or_create( + "sensor", + DOMAIN, + "here_weather_forecast_astronomy_sunrise_0", + suggested_object_id="here_weather_forecast_astronomy_sunrise_0", + disabled_by=None, + ) + await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -94,16 +111,15 @@ async def test_forecast_astronomy(hass): hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() - sensor = hass.states.get("sensor.here_weather_sunrise") + sensor = hass.states.get("sensor.here_weather_forecast_astronomy_sunrise_0") assert sensor.state == "6:55AM" - assert len(hass.states.async_all()) == len(ASTRONOMY_ATTRIBUTES) async def test_imperial(hass): """Test that imperial mode works.""" with patch( "herepy.DestinationWeatherApi.weather_for_coordinates", - return_value=daily_simple_forecasts_response, + side_effect=mock_weather_for_coordinates, ): hass.config.units = IMPERIAL_SYSTEM entry = MockConfigEntry( @@ -111,7 +127,6 @@ async def test_imperial(hass): data={ CONF_API_KEY: "test", CONF_NAME: DOMAIN, - CONF_MODE: DEFAULT_MODE, CONF_LATITUDE: "40.79962", CONF_LONGITUDE: "-73.970314", }, @@ -121,6 +136,17 @@ async def test_imperial(hass): ) entry.add_to_hass(hass) + registry = await hass.helpers.entity_registry.async_get_registry() + + # Pre-create registry entries for disabled by default sensors + registry.async_get_or_create( + "sensor", + DOMAIN, + "here_weather_forecast_7days_simple_windspeed_0", + suggested_object_id="here_weather_forecast_7days_simple_windspeed_0", + disabled_by=None, + ) + await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -128,5 +154,7 @@ async def test_imperial(hass): hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() - sensor = hass.states.get("sensor.here_weather_wind_speed") + sensor = hass.states.get( + "sensor.here_weather_forecast_7days_simple_windspeed_0" + ) assert sensor.attributes.get("unit_of_measurement") == "mph" diff --git a/tests/components/here_weather/test_weather.py b/tests/components/here_weather/test_weather.py new file mode 100644 index 00000000000000..b340af3156ee9b --- /dev/null +++ b/tests/components/here_weather/test_weather.py @@ -0,0 +1,116 @@ +"""Tests for the here_weather weather platform.""" +from unittest.mock import patch + +import herepy + +from homeassistant.components.here_weather.const import DOMAIN +from homeassistant.const import ( + CONF_API_KEY, + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_NAME, + EVENT_HOMEASSISTANT_START, +) +from homeassistant.util.unit_system import IMPERIAL_SYSTEM + +from . import mock_weather_for_coordinates + +from tests.common import MockConfigEntry + + +async def test_weather(hass): + """Test that weather has a value.""" + with patch( + "herepy.DestinationWeatherApi.weather_for_coordinates", + side_effect=mock_weather_for_coordinates, + ): + hass.config.units = IMPERIAL_SYSTEM + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_API_KEY: "test", + CONF_NAME: DOMAIN, + CONF_LATITUDE: "40.79962", + CONF_LONGITUDE: "-73.970314", + }, + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + + sensor = hass.states.get("weather.here_weather_forecast_7days_simple") + assert sensor.state == "cloudy" + + +async def test_weather_no_response(hass): + """Test that weather has a value.""" + with patch( + "herepy.DestinationWeatherApi.weather_for_coordinates", + side_effect=herepy.InvalidRequestError("Invalid"), + ): + hass.config.units = IMPERIAL_SYSTEM + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_API_KEY: "test", + CONF_NAME: DOMAIN, + CONF_LATITUDE: "40.79962", + CONF_LONGITUDE: "-73.970314", + }, + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + + sensor = hass.states.get("weather.here_weather_forecast_7days_simple") + assert sensor.state == "unavailable" + + +async def test_weather_daily(hass): + """Test that weather has a value.""" + with patch( + "herepy.DestinationWeatherApi.weather_for_coordinates", + side_effect=mock_weather_for_coordinates, + ): + hass.config.units = IMPERIAL_SYSTEM + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_API_KEY: "test", + CONF_NAME: DOMAIN, + CONF_LATITUDE: "40.79962", + CONF_LONGITUDE: "-73.970314", + }, + ) + entry.add_to_hass(hass) + + registry = await hass.helpers.entity_registry.async_get_registry() + + # Pre-create registry entries for disabled by default sensors + registry.async_get_or_create( + "weather", + DOMAIN, + "here_weather_forecast_7days", + suggested_object_id="here_weather_forecast_7days", + disabled_by=None, + ) + + await hass.config_entries.async_setup(entry.entry_id) + + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + + sensor = hass.states.get("weather.here_weather_forecast_7days") + assert sensor.state == "cloudy" diff --git a/tests/fixtures/here_weather/daily_simple_forecasts.json b/tests/fixtures/here_weather/daily_simple_forecasts.json index 6c3d7037e9b038..ede035fe05a088 100644 --- a/tests/fixtures/here_weather/daily_simple_forecasts.json +++ b/tests/fixtures/here_weather/daily_simple_forecasts.json @@ -15,7 +15,7 @@ "dewPoint": "-0.40", "precipitationProbability": "53", "precipitationDesc": "", - "rainFall": "*", + "rainFall": "1.42", "snowFall": "1.42", "airInfo": "*", "airDescription": "", From e044a4bccfe406f1898c9e0546f39d63e675ccb1 Mon Sep 17 00:00:00 2001 From: Kevin Eifinger Date: Sun, 14 Feb 2021 13:13:41 +0100 Subject: [PATCH 20/56] Clean up unused entries in config_flow --- .../components/here_weather/config_flow.py | 15 ++------------- .../components/here_weather/strings.json | 2 -- 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/here_weather/config_flow.py b/homeassistant/components/here_weather/config_flow.py index 1ff5488a692eb8..6d437a4cb30016 100644 --- a/homeassistant/components/here_weather/config_flow.py +++ b/homeassistant/components/here_weather/config_flow.py @@ -11,20 +11,13 @@ CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, - CONF_MODE, CONF_NAME, CONF_SCAN_INTERVAL, ) from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv -from .const import ( - CONF_MODES, - DEFAULT_MODE, - DEFAULT_SCAN_INTERVAL, - DOMAIN, - HERE_API_KEYS, -) +from .const import DEFAULT_MODE, DEFAULT_SCAN_INTERVAL, DOMAIN, HERE_API_KEYS _LOGGER = logging.getLogger(__name__) @@ -36,7 +29,7 @@ async def async_validate_user_input(hass: HomeAssistant, user_input: dict) -> No here_client.weather_for_coordinates, user_input[CONF_LATITUDE], user_input[CONF_LONGITUDE], - herepy.WeatherProductType[user_input[CONF_MODE]], + herepy.WeatherProductType[DEFAULT_MODE], ) @@ -80,9 +73,6 @@ def _get_schema(self, user_input: Optional[dict]) -> vol.Schema: { vol.Required(CONF_API_KEY, default=user_input[CONF_API_KEY]): str, vol.Required(CONF_NAME, default=user_input[CONF_NAME]): str, - vol.Required(CONF_MODE, default=user_input[CONF_MODE]): vol.In( - CONF_MODES - ), vol.Required( CONF_LATITUDE, default=user_input[CONF_LATITUDE] ): cv.latitude, @@ -95,7 +85,6 @@ def _get_schema(self, user_input: Optional[dict]) -> vol.Schema: { vol.Required(CONF_API_KEY, default=known_api_key): str, vol.Required(CONF_NAME, default=DOMAIN): str, - vol.Required(CONF_MODE, default=DEFAULT_MODE): vol.In(CONF_MODES), vol.Required( CONF_LATITUDE, default=self.hass.config.latitude ): cv.latitude, diff --git a/homeassistant/components/here_weather/strings.json b/homeassistant/components/here_weather/strings.json index e3be2ff41d4e42..ef9388d8dada09 100644 --- a/homeassistant/components/here_weather/strings.json +++ b/homeassistant/components/here_weather/strings.json @@ -8,7 +8,6 @@ "data": { "api_key": "[%key:common::config_flow::data::api_key%]", "name": "[%key:common::config_flow::data::name%]", - "mode": "The Weather Mode to use", "latitude": "[%key:common::config_flow::data::latitude%]", "longitude": "[%key:common::config_flow::data::longitude%]" } @@ -24,7 +23,6 @@ "init": { "description": "Configure options for HERE Destination Weather", "data": { - "unit_system": "The Unit System to use", "scan_interval": "Update interval." } } From 0efd5526a4773f3884ebaf4364cf74f159639bf9 Mon Sep 17 00:00:00 2001 From: Kevin Eifinger Date: Sun, 14 Feb 2021 13:58:54 +0100 Subject: [PATCH 21/56] Remove mode in tests --- tests/components/here_weather/test_config_flow.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/tests/components/here_weather/test_config_flow.py b/tests/components/here_weather/test_config_flow.py index 9e5afafc47cfe2..eee299467f049b 100644 --- a/tests/components/here_weather/test_config_flow.py +++ b/tests/components/here_weather/test_config_flow.py @@ -3,16 +3,11 @@ import herepy -from homeassistant.components.here_weather.const import ( - DEFAULT_MODE, - DOMAIN, - HERE_API_KEYS, -) +from homeassistant.components.here_weather.const import DOMAIN, HERE_API_KEYS from homeassistant.const import ( CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, - CONF_MODE, CONF_NAME, CONF_SCAN_INTERVAL, EVENT_HOMEASSISTANT_START, @@ -89,7 +84,6 @@ async def test_unauthorized(hass): config = { CONF_API_KEY: "test", CONF_NAME: DOMAIN, - CONF_MODE: DEFAULT_MODE, CONF_LATITUDE: "40.79962", CONF_LONGITUDE: "-73.970314", } From 622a3b6d21952895adf360a16a6d243257cbf538 Mon Sep 17 00:00:00 2001 From: Kevin Eifinger Date: Sun, 14 Feb 2021 14:09:02 +0100 Subject: [PATCH 22/56] Test unload correctly --- tests/components/here_weather/test_config_flow.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/components/here_weather/test_config_flow.py b/tests/components/here_weather/test_config_flow.py index eee299467f049b..148073ad843c11 100644 --- a/tests/components/here_weather/test_config_flow.py +++ b/tests/components/here_weather/test_config_flow.py @@ -166,7 +166,7 @@ async def test_unload_entry(hass): await hass.async_block_till_done() hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 1 + assert hass.data[DOMAIN] assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 0 + assert not hass.data[DOMAIN] From a8b9396618c60205d0488bd410f1c26b1818a707 Mon Sep 17 00:00:00 2001 From: Kevin Eifinger Date: Sat, 26 Jun 2021 00:14:46 +0200 Subject: [PATCH 23/56] Add iot_class to manifest --- .../components/here_weather/config_flow.py | 5 ++-- .../components/here_weather/const.py | 4 ++- .../components/here_weather/manifest.json | 3 +- .../components/here_weather/utils.py | 9 +++--- .../components/here_weather/weather.py | 29 ++++++++++++------- 5 files changed, 30 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/here_weather/config_flow.py b/homeassistant/components/here_weather/config_flow.py index 6d437a4cb30016..29e03359b973a7 100644 --- a/homeassistant/components/here_weather/config_flow.py +++ b/homeassistant/components/here_weather/config_flow.py @@ -1,6 +1,7 @@ """Config flow for here_weather integration.""" +from __future__ import annotations + import logging -from typing import Optional import herepy import voluptuous as vol @@ -64,7 +65,7 @@ async def async_step_user(self, user_input=None): errors=errors, ) - def _get_schema(self, user_input: Optional[dict]) -> vol.Schema: + def _get_schema(self, user_input: dict | None) -> vol.Schema: known_api_key = None if HERE_API_KEYS in self.hass.data: known_api_key = self.hass.data[HERE_API_KEYS][0] diff --git a/homeassistant/components/here_weather/const.py b/homeassistant/components/here_weather/const.py index 560e0b63dc11a2..dcc73a265c805c 100644 --- a/homeassistant/components/here_weather/const.py +++ b/homeassistant/components/here_weather/const.py @@ -1,4 +1,6 @@ """Constants for the HERE Destination Weather service.""" +from __future__ import annotations + DOMAIN = "here_weather" HERE_API_KEYS = "here_api_keys" @@ -223,7 +225,7 @@ MODE_OBSERVATION: OBSERVATION_ATTRIBUTES, } -CONDITION_CLASSES = { +CONDITION_CLASSES: dict[str, list[str]] = { "clear-night": [ "night_passing_clouds", "night_mostly_clear", diff --git a/homeassistant/components/here_weather/manifest.json b/homeassistant/components/here_weather/manifest.json index 1fb1326f05937c..5569254e4e75d1 100644 --- a/homeassistant/components/here_weather/manifest.json +++ b/homeassistant/components/here_weather/manifest.json @@ -8,5 +8,6 @@ ], "codeowners": [ "@eifinger" - ] + ], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/here_weather/utils.py b/homeassistant/components/here_weather/utils.py index bbc530b564e889..90b2ac135259e9 100644 --- a/homeassistant/components/here_weather/utils.py +++ b/homeassistant/components/here_weather/utils.py @@ -1,6 +1,5 @@ """Utility functions for here_weather.""" - -from typing import Optional +from __future__ import annotations from homeassistant.const import ( CONF_UNIT_SYSTEM_METRIC, @@ -35,7 +34,7 @@ def convert_unit_of_measurement_if_needed(unit_system, unit_of_measurement: str) def get_attribute_from_here_data( here_data: list, attribute_name: str, sensor_number: int = 0 -) -> Optional[str]: +) -> str | None: """Extract and convert data from HERE response or None if not found.""" if here_data is None: return None @@ -47,8 +46,8 @@ def get_attribute_from_here_data( return None -def convert_asterisk_to_none(state: str) -> str: +def convert_asterisk_to_none(state: str) -> str | None: """Convert HERE API representation of None.""" if state == "*": - state = None + return None return state diff --git a/homeassistant/components/here_weather/weather.py b/homeassistant/components/here_weather/weather.py index c642944ec5a00f..86b9d7563a51b2 100644 --- a/homeassistant/components/here_weather/weather.py +++ b/homeassistant/components/here_weather/weather.py @@ -1,4 +1,6 @@ """Weather platform for the HERE Destination Weather service.""" +from __future__ import annotations + import logging from homeassistant.components.weather import ( @@ -14,6 +16,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME, TEMP_CELSIUS from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -56,7 +59,7 @@ class HEREDestinationWeather(CoordinatorEntity, WeatherEntity): def __init__( self, config_entry: ConfigEntry, coordinator: DataUpdateCoordinator, mode: str - ): + ) -> None: """Initialize the sensor.""" super().__init__(coordinator) self._name = config_entry.data[CONF_NAME] @@ -78,7 +81,7 @@ def condition(self): return get_condition_from_here_data(self.coordinator.data) @property - def temperature(self) -> float: + def temperature(self): """Return the temperature.""" return get_temperature_from_here_data(self.coordinator.data, self._mode) @@ -154,7 +157,7 @@ def entity_registry_enabled_default(self): return self._mode == DEFAULT_MODE @property - def device_info(self) -> dict: + def device_info(self) -> DeviceInfo: """Return a device description for device registry.""" return { @@ -164,7 +167,7 @@ def device_info(self) -> dict: } -def get_condition_from_here_data(here_data: list, offset: int = 0) -> str: +def get_condition_from_here_data(here_data: list, offset: int = 0) -> str | None: """Return the condition from here_data.""" try: return [ @@ -178,38 +181,42 @@ def get_condition_from_here_data(here_data: list, offset: int = 0) -> str: def get_high_or_default_temperature_from_here_data( here_data: list, mode: str, offset: int = 0 -) -> str: +) -> str | None: """Return the temperature from here_data.""" temperature = get_attribute_from_here_data(here_data, "highTemperature", offset) if temperature is not None: - return float(temperature) + return str(temperature) return get_temperature_from_here_data(here_data, mode, offset) def get_low_or_default_temperature_from_here_data( here_data: list, mode: str, offset: int = 0 -) -> str: +) -> str | None: """Return the temperature from here_data.""" temperature = get_attribute_from_here_data(here_data, "lowTemperature", offset) if temperature is not None: - return float(temperature) + return str(temperature) return get_temperature_from_here_data(here_data, mode, offset) -def get_temperature_from_here_data(here_data: list, mode: str, offset: int = 0) -> str: +def get_temperature_from_here_data( + here_data: list, mode: str, offset: int = 0 +) -> str | None: """Return the temperature from here_data.""" if mode == MODE_DAILY_SIMPLE: temperature = get_attribute_from_here_data(here_data, "highTemperature", offset) else: temperature = get_attribute_from_here_data(here_data, "temperature", offset) if temperature is not None: - return float(temperature) + return str(temperature) + return None -def calc_precipitation(here_data: list, offset: int = 0) -> float: +def calc_precipitation(here_data: list, offset: int = 0) -> float | None: """Calculate Precipitation.""" rain_fall = get_attribute_from_here_data(here_data, "rainFall", offset) snow_fall = get_attribute_from_here_data(here_data, "snowFall", offset) if rain_fall is not None and snow_fall is not None: return float(rain_fall) + float(snow_fall) + return None From 59af2aff91b99a456197947f4116c8ae9aec2bcd Mon Sep 17 00:00:00 2001 From: Kevin Eifinger Date: Sat, 26 Jun 2021 00:56:31 +0200 Subject: [PATCH 24/56] Fix mypy --- .../components/here_weather/__init__.py | 18 +++++++++++------- homeassistant/components/here_weather/const.py | 12 ++++++------ .../components/here_weather/sensor.py | 10 +++++++--- homeassistant/components/here_weather/utils.py | 4 +++- 4 files changed, 27 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/here_weather/__init__.py b/homeassistant/components/here_weather/__init__.py index 25bca3c17d1a9e..75e87776ee8563 100644 --- a/homeassistant/components/here_weather/__init__.py +++ b/homeassistant/components/here_weather/__init__.py @@ -1,4 +1,6 @@ """The here_weather component.""" +from __future__ import annotations + import asyncio from datetime import timedelta import logging @@ -81,14 +83,13 @@ def __init__( self.latitude = config_entry.data[CONF_LATITUDE] self.longitude = config_entry.data[CONF_LONGITUDE] self.weather_product_type = herepy.WeatherProductType[mode] - self.coordinator = None - self.unsub_handler = None + self.coordinator: DataUpdateCoordinator | None = None - async def async_setup(self) -> list: + async def async_setup(self) -> None: """Set up the here_weather integration.""" self.add_options() - self.unsub_handler = self.config_entry.add_update_listener( - self.async_options_updated + self.config_entry.async_on_unload( + self.config_entry.add_update_listener(self.async_options_updated) ) self.coordinator = DataUpdateCoordinator( self.hass, @@ -99,7 +100,7 @@ async def async_setup(self) -> list: seconds=self.config_entry.options[CONF_SCAN_INTERVAL] ), ) - await self.coordinator.async_refresh() + await self.coordinator.async_config_entry_first_refresh() def add_options(self) -> None: """Add options for here_weather integration.""" @@ -146,7 +147,8 @@ async def async_options_updated( def set_update_interval(self, update_interval: int) -> None: """Set the coordinator update_interval to the supplied update_interval.""" - self.coordinator.update_interval = timedelta(seconds=update_interval) + if self.coordinator is not None: + self.coordinator.update_interval = timedelta(seconds=update_interval) def extract_data_from_payload_for_product_type( @@ -163,3 +165,5 @@ def extract_data_from_payload_for_product_type( return data.dailyForecasts["forecastLocation"]["forecast"] if product_type == herepy.WeatherProductType.forecast_hourly: return data.hourlyForecasts["forecastLocation"]["forecast"] + _LOGGER.debug("Payload malformed: %s", data) + raise UpdateFailed("Payload malformed") diff --git a/homeassistant/components/here_weather/const.py b/homeassistant/components/here_weather/const.py index dcc73a265c805c..6e93a99a7af1f7 100644 --- a/homeassistant/components/here_weather/const.py +++ b/homeassistant/components/here_weather/const.py @@ -25,7 +25,7 @@ ] DEFAULT_MODE = MODE_DAILY_SIMPLE -ASTRONOMY_ATTRIBUTES = { +ASTRONOMY_ATTRIBUTES: dict[str, dict[str, str | None]] = { "sunrise": {"name": "Sunrise", "unit_of_measurement": None}, "sunset": {"name": "Sunset", "unit_of_measurement": None}, "moonrise": {"name": "Moonrise", "unit_of_measurement": None}, @@ -38,7 +38,7 @@ "utcTime": {"name": "UTC Time", "unit_of_measurement": None}, } -HOURLY_ATTRIBUTES = { +HOURLY_ATTRIBUTES: dict[str, dict[str, str | None]] = { "daylight": {"name": "Daylight", "unit_of_measurement": None}, "description": {"name": "Description", "unit_of_measurement": None}, "skyInfo": {"name": "Sky Info", "unit_of_measurement": None}, @@ -75,7 +75,7 @@ "localTimeFormat": {"name": "Local Time Format", "unit_of_measurement": None}, } -DAILY_SIMPLE_ATTRIBUTES = { +DAILY_SIMPLE_ATTRIBUTES: dict[str, dict[str, str | None]] = { "daylight": {"name": "Daylight", "unit_of_measurement": None}, "description": {"name": "Description", "unit_of_measurement": None}, "skyInfo": {"name": "Sky Info", "unit_of_measurement": None}, @@ -118,7 +118,7 @@ "utcTime": {"name": "UTC Time", "unit_of_measurement": None}, } -DAILY_ATTRIBUTES = { +DAILY_ATTRIBUTES: dict[str, dict[str, str | None]] = { "daylight": {"name": "Daylight", "unit_of_measurement": None}, "daySegment": {"name": "Day Segment", "unit_of_measurement": None}, "description": {"name": "Description", "unit_of_measurement": None}, @@ -159,7 +159,7 @@ "utcTime": {"name": "UTC Time", "unit_of_measurement": None}, } -OBSERVATION_ATTRIBUTES = { +OBSERVATION_ATTRIBUTES: dict[str, dict[str, str | None]] = { "daylight": {"name": "Daylight", "unit_of_measurement": None}, "description": {"name": "Description", "unit_of_measurement": None}, "skyInfo": {"name": "Sky Info", "unit_of_measurement": None}, @@ -217,7 +217,7 @@ "city": {"name": "City", "unit_of_measurement": None}, } -SENSOR_TYPES = { +SENSOR_TYPES: dict[str, dict[str, dict[str, str | None]]] = { MODE_ASTRONOMY: ASTRONOMY_ATTRIBUTES, MODE_HOURLY: HOURLY_ATTRIBUTES, MODE_DAILY_SIMPLE: DAILY_SIMPLE_ATTRIBUTES, diff --git a/homeassistant/components/here_weather/sensor.py b/homeassistant/components/here_weather/sensor.py index 0f3d06f26313e5..c0e4000f8059ac 100644 --- a/homeassistant/components/here_weather/sensor.py +++ b/homeassistant/components/here_weather/sensor.py @@ -1,9 +1,13 @@ """Sensor platform for the HERE Destination Weather service.""" +from __future__ import annotations + import logging from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -79,7 +83,7 @@ def unique_id(self) -> str: return self._unique_id @property - def state(self) -> str: + def state(self) -> StateType: """Return the state of the device.""" return get_attribute_from_here_data( self.coordinator.data, @@ -88,12 +92,12 @@ def state(self) -> str: ) @property - def unit_of_measurement(self) -> str: + def unit_of_measurement(self) -> str | None: """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement @property - def device_info(self) -> dict: + def device_info(self) -> DeviceInfo: """Return a device description for device registry.""" return { diff --git a/homeassistant/components/here_weather/utils.py b/homeassistant/components/here_weather/utils.py index 90b2ac135259e9..d017c415640c3f 100644 --- a/homeassistant/components/here_weather/utils.py +++ b/homeassistant/components/here_weather/utils.py @@ -16,7 +16,9 @@ ) -def convert_unit_of_measurement_if_needed(unit_system, unit_of_measurement: str) -> str: +def convert_unit_of_measurement_if_needed( + unit_system, unit_of_measurement: str | None +) -> str | None: """Convert the unit of measurement to imperial if configured.""" if unit_system != CONF_UNIT_SYSTEM_METRIC: if unit_of_measurement == TEMP_CELSIUS: From 131f9956fa8d2d526dce82db747252004db0ae3f Mon Sep 17 00:00:00 2001 From: Kevin Eifinger Date: Sat, 26 Jun 2021 02:19:57 +0200 Subject: [PATCH 25/56] Fix tests --- .../components/here_weather/__init__.py | 2 -- .../components/here_weather/weather.py | 12 +++---- tests/components/here_weather/test_weather.py | 31 ------------------- 3 files changed, 6 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/here_weather/__init__.py b/homeassistant/components/here_weather/__init__.py index 75e87776ee8563..c5d630ae36a453 100644 --- a/homeassistant/components/here_weather/__init__.py +++ b/homeassistant/components/here_weather/__init__.py @@ -63,8 +63,6 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> ) ) if unload_ok: - for mode in CONF_MODES: - hass.data[DOMAIN][config_entry.entry_id][mode].unsub_handler() hass.data[DOMAIN].pop(config_entry.entry_id) return unload_ok diff --git a/homeassistant/components/here_weather/weather.py b/homeassistant/components/here_weather/weather.py index 86b9d7563a51b2..c6ec664c63c021 100644 --- a/homeassistant/components/here_weather/weather.py +++ b/homeassistant/components/here_weather/weather.py @@ -181,35 +181,35 @@ def get_condition_from_here_data(here_data: list, offset: int = 0) -> str | None def get_high_or_default_temperature_from_here_data( here_data: list, mode: str, offset: int = 0 -) -> str | None: +) -> float | None: """Return the temperature from here_data.""" temperature = get_attribute_from_here_data(here_data, "highTemperature", offset) if temperature is not None: - return str(temperature) + return float(temperature) return get_temperature_from_here_data(here_data, mode, offset) def get_low_or_default_temperature_from_here_data( here_data: list, mode: str, offset: int = 0 -) -> str | None: +) -> float | None: """Return the temperature from here_data.""" temperature = get_attribute_from_here_data(here_data, "lowTemperature", offset) if temperature is not None: - return str(temperature) + return float(temperature) return get_temperature_from_here_data(here_data, mode, offset) def get_temperature_from_here_data( here_data: list, mode: str, offset: int = 0 -) -> str | None: +) -> float | None: """Return the temperature from here_data.""" if mode == MODE_DAILY_SIMPLE: temperature = get_attribute_from_here_data(here_data, "highTemperature", offset) else: temperature = get_attribute_from_here_data(here_data, "temperature", offset) if temperature is not None: - return str(temperature) + return float(temperature) return None diff --git a/tests/components/here_weather/test_weather.py b/tests/components/here_weather/test_weather.py index b340af3156ee9b..8fef60417fc4fc 100644 --- a/tests/components/here_weather/test_weather.py +++ b/tests/components/here_weather/test_weather.py @@ -1,8 +1,6 @@ """Tests for the here_weather weather platform.""" from unittest.mock import patch -import herepy - from homeassistant.components.here_weather.const import DOMAIN from homeassistant.const import ( CONF_API_KEY, @@ -47,35 +45,6 @@ async def test_weather(hass): assert sensor.state == "cloudy" -async def test_weather_no_response(hass): - """Test that weather has a value.""" - with patch( - "herepy.DestinationWeatherApi.weather_for_coordinates", - side_effect=herepy.InvalidRequestError("Invalid"), - ): - hass.config.units = IMPERIAL_SYSTEM - entry = MockConfigEntry( - domain=DOMAIN, - data={ - CONF_API_KEY: "test", - CONF_NAME: DOMAIN, - CONF_LATITUDE: "40.79962", - CONF_LONGITUDE: "-73.970314", - }, - ) - entry.add_to_hass(hass) - - await hass.config_entries.async_setup(entry.entry_id) - - await hass.async_block_till_done() - - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) - await hass.async_block_till_done() - - sensor = hass.states.get("weather.here_weather_forecast_7days_simple") - assert sensor.state == "unavailable" - - async def test_weather_daily(hass): """Test that weather has a value.""" with patch( From de80121deeb844f41fadabd719c68b1d4782b1c0 Mon Sep 17 00:00:00 2001 From: Kevin Eifinger Date: Sat, 3 Jul 2021 13:33:06 +0200 Subject: [PATCH 26/56] Apply suggestions from code review Co-authored-by: Franck Nijhof --- .../components/here_weather/__init__.py | 19 ++----------------- .../components/here_weather/sensor.py | 2 +- 2 files changed, 3 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/here_weather/__init__.py b/homeassistant/components/here_weather/__init__.py index c5d630ae36a453..719dc71a9add89 100644 --- a/homeassistant/components/here_weather/__init__.py +++ b/homeassistant/components/here_weather/__init__.py @@ -26,11 +26,6 @@ PLATFORMS = ["sensor", "weather"] -async def async_setup(hass: HomeAssistant, config: dict) -> bool: - """Set up the here_weather component.""" - return True - - async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Set up here_weather from a config entry.""" here_weather_data_dict = {} @@ -44,24 +39,14 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b if config_entry.data[CONF_API_KEY] not in known_api_keys: known_api_keys.append(config_entry.data[CONF_API_KEY]) - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(config_entry, component) - for component in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(config_entry.entry_id) diff --git a/homeassistant/components/here_weather/sensor.py b/homeassistant/components/here_weather/sensor.py index c0e4000f8059ac..77e797142c9983 100644 --- a/homeassistant/components/here_weather/sensor.py +++ b/homeassistant/components/here_weather/sensor.py @@ -36,7 +36,7 @@ async def async_setup_entry( weather_attribute, ) ) - async_add_entities(sensors_to_add, True) + async_add_entities(sensors_to_add) class HEREDestinationWeatherSensor(CoordinatorEntity): From 5b9f991be405d408203685a6ae8ac36560b4d8b3 Mon Sep 17 00:00:00 2001 From: Kevin Eifinger Date: Sat, 3 Jul 2021 14:20:38 +0200 Subject: [PATCH 27/56] style: rename config_entry to entry --- .../components/here_weather/__init__.py | 49 ++++++++----------- 1 file changed, 20 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/here_weather/__init__.py b/homeassistant/components/here_weather/__init__.py index 719dc71a9add89..c9bff85c4db67d 100644 --- a/homeassistant/components/here_weather/__init__.py +++ b/homeassistant/components/here_weather/__init__.py @@ -1,7 +1,6 @@ """The here_weather component.""" from __future__ import annotations -import asyncio from datetime import timedelta import logging @@ -26,29 +25,29 @@ PLATFORMS = ["sensor", "weather"] -async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up here_weather from a config entry.""" here_weather_data_dict = {} for mode in CONF_MODES: - here_weather_data = HEREWeatherData(hass, config_entry, mode) + here_weather_data = HEREWeatherData(hass, entry, mode) await here_weather_data.async_setup() here_weather_data_dict[mode] = here_weather_data - hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = here_weather_data_dict + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = here_weather_data_dict known_api_keys = hass.data.setdefault(HERE_API_KEYS, []) - if config_entry.data[CONF_API_KEY] not in known_api_keys: - known_api_keys.append(config_entry.data[CONF_API_KEY]) + if entry.data[CONF_API_KEY] not in known_api_keys: + known_api_keys.append(entry.data[CONF_API_KEY]) hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True -async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: - hass.data[DOMAIN].pop(config_entry.entry_id) + hass.data[DOMAIN].pop(entry.entry_id) return unload_ok @@ -56,44 +55,38 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> class HEREWeatherData: """Get the latest data from HERE.""" - def __init__( - self, hass: HomeAssistant, config_entry: ConfigEntry, mode: str - ) -> None: + def __init__(self, hass: HomeAssistant, entry: ConfigEntry, mode: str) -> None: """Initialize the data object.""" self.hass = hass - self.config_entry = config_entry - self.here_client = herepy.DestinationWeatherApi(config_entry.data[CONF_API_KEY]) - self.latitude = config_entry.data[CONF_LATITUDE] - self.longitude = config_entry.data[CONF_LONGITUDE] + self.entry = entry + self.here_client = herepy.DestinationWeatherApi(entry.data[CONF_API_KEY]) + self.latitude = entry.data[CONF_LATITUDE] + self.longitude = entry.data[CONF_LONGITUDE] self.weather_product_type = herepy.WeatherProductType[mode] self.coordinator: DataUpdateCoordinator | None = None async def async_setup(self) -> None: """Set up the here_weather integration.""" self.add_options() - self.config_entry.async_on_unload( - self.config_entry.add_update_listener(self.async_options_updated) + self.entry.async_on_unload( + self.entry.add_update_listener(self.async_options_updated) ) self.coordinator = DataUpdateCoordinator( self.hass, _LOGGER, name=DOMAIN, update_method=self.async_update, - update_interval=timedelta( - seconds=self.config_entry.options[CONF_SCAN_INTERVAL] - ), + update_interval=timedelta(seconds=self.entry.options[CONF_SCAN_INTERVAL]), ) await self.coordinator.async_config_entry_first_refresh() def add_options(self) -> None: """Add options for here_weather integration.""" - if not self.config_entry.options: + if not self.entry.options: options = { CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL, } - self.hass.config_entries.async_update_entry( - self.config_entry, options=options - ) + self.hass.config_entries.async_update_entry(self.entry, options=options) async def async_update(self): """Handle data update with the DataUpdateCoordinator.""" @@ -119,13 +112,11 @@ def _get_data(self): ) @staticmethod - async def async_options_updated( - hass: HomeAssistant, config_entry: ConfigEntry - ) -> None: + async def async_options_updated(hass: HomeAssistant, entry: ConfigEntry) -> None: """Triggered by config entry options updates.""" for mode in CONF_MODES: - hass.data[DOMAIN][config_entry.entry_id][mode].set_update_interval( - config_entry.options[CONF_SCAN_INTERVAL] + hass.data[DOMAIN][entry.entry_id][mode].set_update_interval( + entry.options[CONF_SCAN_INTERVAL] ) def set_update_interval(self, update_interval: int) -> None: From b617a61ef095ef05aeddf789f0e43abd4f3ea6cc Mon Sep 17 00:00:00 2001 From: Kevin Eifinger Date: Thu, 22 Jul 2021 12:37:04 +0200 Subject: [PATCH 28/56] remove None check in get_attribute_from_here_data --- homeassistant/components/here_weather/sensor.py | 12 +++++++----- homeassistant/components/here_weather/utils.py | 2 -- homeassistant/components/here_weather/weather.py | 15 ++++++++++----- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/here_weather/sensor.py b/homeassistant/components/here_weather/sensor.py index 77e797142c9983..017f08313fdfc4 100644 --- a/homeassistant/components/here_weather/sensor.py +++ b/homeassistant/components/here_weather/sensor.py @@ -85,11 +85,13 @@ def unique_id(self) -> str: @property def state(self) -> StateType: """Return the state of the device.""" - return get_attribute_from_here_data( - self.coordinator.data, - self._weather_attribute, - self._sensor_number, - ) + if self.coordinator.data is not None: + return get_attribute_from_here_data( + self.coordinator.data, + self._weather_attribute, + self._sensor_number, + ) + return None @property def unit_of_measurement(self) -> str | None: diff --git a/homeassistant/components/here_weather/utils.py b/homeassistant/components/here_weather/utils.py index d017c415640c3f..611b64d3740ad4 100644 --- a/homeassistant/components/here_weather/utils.py +++ b/homeassistant/components/here_weather/utils.py @@ -38,8 +38,6 @@ def get_attribute_from_here_data( here_data: list, attribute_name: str, sensor_number: int = 0 ) -> str | None: """Extract and convert data from HERE response or None if not found.""" - if here_data is None: - return None try: state = here_data[sensor_number][attribute_name] state = convert_asterisk_to_none(state) diff --git a/homeassistant/components/here_weather/weather.py b/homeassistant/components/here_weather/weather.py index c6ec664c63c021..b23c1a9df30019 100644 --- a/homeassistant/components/here_weather/weather.py +++ b/homeassistant/components/here_weather/weather.py @@ -78,12 +78,14 @@ def unique_id(self): @property def condition(self): """Return the current condition.""" - return get_condition_from_here_data(self.coordinator.data) + if self.coordinator.data is not None: + return get_condition_from_here_data(self.coordinator.data) @property def temperature(self): """Return the temperature.""" - return get_temperature_from_here_data(self.coordinator.data, self._mode) + if self.coordinator.data is not None: + return get_temperature_from_here_data(self.coordinator.data, self._mode) @property def temperature_unit(self): @@ -100,17 +102,20 @@ def pressure(self): @property def humidity(self): """Return the humidity.""" - get_attribute_from_here_data(self.coordinator.data, "humidity") + if self.coordinator.data is not None: + return get_attribute_from_here_data(self.coordinator.data, "humidity") @property def wind_speed(self): """Return the wind speed.""" - get_attribute_from_here_data(self.coordinator.data, "windSpeed") + if self.coordinator.data is not None: + return get_attribute_from_here_data(self.coordinator.data, "windSpeed") @property def wind_bearing(self): """Return the wind bearing.""" - get_attribute_from_here_data(self.coordinator.data, "windDirection") + if self.coordinator.data is not None: + return get_attribute_from_here_data(self.coordinator.data, "windDirection") @property def attribution(self): From 1d828e711351cc5b3165732592087d7b4852d4bf Mon Sep 17 00:00:00 2001 From: Kevin Eifinger Date: Thu, 22 Jul 2021 12:39:43 +0200 Subject: [PATCH 29/56] simplify except KeyError --- homeassistant/components/here_weather/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/here_weather/utils.py b/homeassistant/components/here_weather/utils.py index 611b64d3740ad4..e4768c63eb541b 100644 --- a/homeassistant/components/here_weather/utils.py +++ b/homeassistant/components/here_weather/utils.py @@ -40,10 +40,10 @@ def get_attribute_from_here_data( """Extract and convert data from HERE response or None if not found.""" try: state = here_data[sensor_number][attribute_name] - state = convert_asterisk_to_none(state) - return state except KeyError: return None + state = convert_asterisk_to_none(state) + return state def convert_asterisk_to_none(state: str) -> str | None: From 8ed8ce3ee9a90654699bfc6c483f501686b5bdae Mon Sep 17 00:00:00 2001 From: Kevin Eifinger Date: Thu, 22 Jul 2021 14:03:45 +0200 Subject: [PATCH 30/56] Return correct types for weather attributes --- .../components/here_weather/utils.py | 11 +- .../components/here_weather/weather.py | 141 ++++++++++++++---- 2 files changed, 125 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/here_weather/utils.py b/homeassistant/components/here_weather/utils.py index e4768c63eb541b..13dc85ebb73934 100644 --- a/homeassistant/components/here_weather/utils.py +++ b/homeassistant/components/here_weather/utils.py @@ -16,8 +16,17 @@ ) +def convert_temperature_unit_of_measurement_if_needed( + unit_system: str, unit_of_measurement: str +) -> str: + """Convert the temperature unit of measurement to imperial if configured.""" + if unit_system != CONF_UNIT_SYSTEM_METRIC: + unit_of_measurement = TEMP_FAHRENHEIT + return unit_of_measurement + + def convert_unit_of_measurement_if_needed( - unit_system, unit_of_measurement: str | None + unit_system: str, unit_of_measurement: str | None ) -> str | None: """Convert the unit of measurement to imperial if configured.""" if unit_system != CONF_UNIT_SYSTEM_METRIC: diff --git a/homeassistant/components/here_weather/weather.py b/homeassistant/components/here_weather/weather.py index b23c1a9df30019..01f32bb6c510bd 100644 --- a/homeassistant/components/here_weather/weather.py +++ b/homeassistant/components/here_weather/weather.py @@ -6,11 +6,14 @@ from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, ATTR_FORECAST_PRECIPITATION, + ATTR_FORECAST_PRECIPITATION_PROBABILITY, + ATTR_FORECAST_PRESSURE, ATTR_FORECAST_TEMP, ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, ATTR_FORECAST_WIND_SPEED, + Forecast, WeatherEntity, ) from homeassistant.config_entries import ConfigEntry @@ -30,7 +33,10 @@ MODE_DAILY_SIMPLE, SENSOR_TYPES, ) -from .utils import convert_unit_of_measurement_if_needed, get_attribute_from_here_data +from .utils import ( + convert_temperature_unit_of_measurement_if_needed, + get_attribute_from_here_data, +) _LOGGER = logging.getLogger(__name__) @@ -76,80 +82,113 @@ def unique_id(self): return f"{self._name}_{self._mode}" @property - def condition(self): + def condition(self) -> str | None: """Return the current condition.""" if self.coordinator.data is not None: return get_condition_from_here_data(self.coordinator.data) + return None @property - def temperature(self): + def temperature(self) -> float | None: """Return the temperature.""" if self.coordinator.data is not None: return get_temperature_from_here_data(self.coordinator.data, self._mode) + return None @property - def temperature_unit(self): + def temperature_unit(self) -> str: """Return the unit of measurement.""" - return convert_unit_of_measurement_if_needed( + return convert_temperature_unit_of_measurement_if_needed( self.coordinator.hass.config.units.name, TEMP_CELSIUS ) @property - def pressure(self): + def pressure(self) -> float | None: """Return the pressure.""" + if self.coordinator.data is not None: + return get_pressure_from_here_data(self.coordinator.data, self._mode) return None @property - def humidity(self): + def humidity(self) -> float | None: """Return the humidity.""" if self.coordinator.data is not None: - return get_attribute_from_here_data(self.coordinator.data, "humidity") + if ( + humidity := get_attribute_from_here_data( + self.coordinator.data, "humidity" + ) + is not None + ): + return float(humidity) + return None @property - def wind_speed(self): + def wind_speed(self) -> float | None: """Return the wind speed.""" if self.coordinator.data is not None: - return get_attribute_from_here_data(self.coordinator.data, "windSpeed") + return get_wind_speed_from_here_data(self.coordinator.data) + return None @property - def wind_bearing(self): + def wind_bearing(self) -> float | str | None: """Return the wind bearing.""" if self.coordinator.data is not None: - return get_attribute_from_here_data(self.coordinator.data, "windDirection") + return get_wind_bearing_from_here_data(self.coordinator.data) + return None @property - def attribution(self): + def attribution(self) -> str | None: """Return the attribution.""" return None @property - def forecast(self): + def visibility(self) -> float | None: + """Return the visibility.""" + if "visibility" in SENSOR_TYPES[self._mode]: + if self.coordinator.data is not None: + if ( + visibility := get_attribute_from_here_data( + self.coordinator.data, "visibility" + ) + is not None + ): + return float(visibility) + return None + + @property + def forecast(self) -> list[Forecast] | None: """Return the forecast array.""" if self.coordinator.data is None: return None - data = [] + data: list[Forecast] = [] for offset in range(len(self.coordinator.data)): data.append( { - ATTR_FORECAST_TIME: get_attribute_from_here_data( - self.coordinator.data, "utcTime", offset + ATTR_FORECAST_CONDITION: get_condition_from_here_data( + self.coordinator.data, offset ), - ATTR_FORECAST_TEMP: get_high_or_default_temperature_from_here_data( - self.coordinator.data, self._mode, offset + ATTR_FORECAST_TIME: get_time_from_here_data( + self.coordinator.data, offset ), - ATTR_FORECAST_TEMP_LOW: get_low_or_default_temperature_from_here_data( + ATTR_FORECAST_PRECIPITATION_PROBABILITY: get_precipitation_probability( self.coordinator.data, self._mode, offset ), ATTR_FORECAST_PRECIPITATION: calc_precipitation( self.coordinator.data, offset ), - ATTR_FORECAST_WIND_SPEED: get_attribute_from_here_data( - self.coordinator.data, "windSpeed", offset + ATTR_FORECAST_PRESSURE: get_pressure_from_here_data( + self.coordinator.data, self._mode, offset ), - ATTR_FORECAST_WIND_BEARING: get_attribute_from_here_data( - self.coordinator.data, "windDirection", offset + ATTR_FORECAST_TEMP: get_high_or_default_temperature_from_here_data( + self.coordinator.data, self._mode, offset ), - ATTR_FORECAST_CONDITION: get_condition_from_here_data( + ATTR_FORECAST_TEMP_LOW: get_low_or_default_temperature_from_here_data( + self.coordinator.data, self._mode, offset + ), + ATTR_FORECAST_WIND_BEARING: get_wind_bearing_from_here_data( + self.coordinator.data, offset + ), + ATTR_FORECAST_WIND_SPEED: get_wind_speed_from_here_data( self.coordinator.data, offset ), } @@ -164,7 +203,6 @@ def entity_registry_enabled_default(self): @property def device_info(self) -> DeviceInfo: """Return a device description for device registry.""" - return { "identifiers": {(DOMAIN, self._name)}, "name": self._name, @@ -172,6 +210,57 @@ def device_info(self) -> DeviceInfo: } +def get_wind_speed_from_here_data(here_data: list, offset: int = 0) -> float: + """Return the wind speed from here_data.""" + wind_speed = get_attribute_from_here_data(here_data, "windSpeed", offset) + assert wind_speed is not None + return float(wind_speed) + + +def get_wind_bearing_from_here_data(here_data: list, offset: int = 0) -> int: + """Return the wind bearing from here_data.""" + wind_bearing = get_attribute_from_here_data(here_data, "windDirection", offset) + assert wind_bearing is not None + return int(wind_bearing) + + +def get_time_from_here_data(here_data: list, offset: int = 0) -> str: + """Return the time from here_data.""" + time = get_attribute_from_here_data(here_data, "utcTime", offset) + assert time is not None + return time + + +def get_pressure_from_here_data( + here_data: list, mode: str, offset: int = 0 +) -> float | None: + """Return the pressure from here_data.""" + if "barometerPressure" in SENSOR_TYPES[mode]: + if ( + pressure := get_attribute_from_here_data( + here_data, "barometerPressure", offset + ) + is not None + ): + return float(pressure) + return None + + +def get_precipitation_probability( + here_data: list, mode: str, offset: int = 0 +) -> int | None: + """Return the precipitation probability from here_data.""" + if "precipitationProbability" in SENSOR_TYPES[mode]: + if ( + precipitation_probability := get_attribute_from_here_data( + here_data, "precipitationProbability", offset + ) + is not None + ): + return int(precipitation_probability) + return None + + def get_condition_from_here_data(here_data: list, offset: int = 0) -> str | None: """Return the condition from here_data.""" try: From b7a2ed75bfbed1e13d27c4c72d7d4412caf298e1 Mon Sep 17 00:00:00 2001 From: Kevin Eifinger Date: Thu, 22 Jul 2021 14:10:51 +0200 Subject: [PATCH 31/56] Only call async_add_entities once --- .../components/here_weather/weather.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/here_weather/weather.py b/homeassistant/components/here_weather/weather.py index 01f32bb6c510bd..1ea40f704ebd6d 100644 --- a/homeassistant/components/here_weather/weather.py +++ b/homeassistant/components/here_weather/weather.py @@ -46,18 +46,18 @@ async def async_setup_entry( ): """Add here_weather entities from a config_entry.""" here_weather_data_dict = hass.data[DOMAIN][config_entry.entry_id] + + entities_to_add = [] for sensor_type in SENSOR_TYPES: if sensor_type != MODE_ASTRONOMY: - async_add_entities( - [ - HEREDestinationWeather( - config_entry, - here_weather_data_dict[sensor_type].coordinator, - sensor_type, - ) - ], - True, + entities_to_add.append( + HEREDestinationWeather( + config_entry, + here_weather_data_dict[sensor_type].coordinator, + sensor_type, + ) ) + async_add_entities(entities_to_add) class HEREDestinationWeather(CoordinatorEntity, WeatherEntity): From 6703563ac92371ceabac09bcfb3f13aa0b107360 Mon Sep 17 00:00:00 2001 From: Kevin Eifinger Date: Thu, 22 Jul 2021 15:33:03 +0200 Subject: [PATCH 32/56] Rename config_entry to entry --- .../components/here_weather/sensor.py | 20 ++++++++----------- .../components/here_weather/weather.py | 16 ++++++--------- 2 files changed, 14 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/here_weather/sensor.py b/homeassistant/components/here_weather/sensor.py index 017f08313fdfc4..407ac39eb8865c 100644 --- a/homeassistant/components/here_weather/sensor.py +++ b/homeassistant/components/here_weather/sensor.py @@ -1,8 +1,6 @@ """Sensor platform for the HERE Destination Weather service.""" from __future__ import annotations -import logging - from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.core import HomeAssistant @@ -16,21 +14,19 @@ from .const import DOMAIN, SENSOR_TYPES from .utils import convert_unit_of_measurement_if_needed, get_attribute_from_here_data -_LOGGER = logging.getLogger(__name__) - async def async_setup_entry( - hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ): - """Add here_weather entities from a config_entry.""" - here_weather_data_dict = hass.data[DOMAIN][config_entry.entry_id] + """Add here_weather entities from a ConfigEntry.""" + here_weather_data_dict = hass.data[DOMAIN][entry.entry_id] sensors_to_add = [] for sensor_type in SENSOR_TYPES: for weather_attribute in SENSOR_TYPES[sensor_type]: sensors_to_add.append( HEREDestinationWeatherSensor( - config_entry, + entry, here_weather_data_dict[sensor_type].coordinator, sensor_type, weather_attribute, @@ -44,7 +40,7 @@ class HEREDestinationWeatherSensor(CoordinatorEntity): def __init__( self, - config_entry: ConfigEntry, + entry: ConfigEntry, coordinator: DataUpdateCoordinator, sensor_type: str, weather_attribute: str, @@ -52,10 +48,10 @@ def __init__( ) -> None: """Initialize the sensor.""" super().__init__(coordinator) - self._base_name = config_entry.data[CONF_NAME] + self._base_name = entry.data[CONF_NAME] self._name_suffix = SENSOR_TYPES[sensor_type][weather_attribute]["name"] - self._latitude = config_entry.data[CONF_LATITUDE] - self._longitude = config_entry.data[CONF_LONGITUDE] + self._latitude = entry.data[CONF_LATITUDE] + self._longitude = entry.data[CONF_LONGITUDE] self._sensor_type = sensor_type self._sensor_number = sensor_number self._weather_attribute = weather_attribute diff --git a/homeassistant/components/here_weather/weather.py b/homeassistant/components/here_weather/weather.py index 1ea40f704ebd6d..3101f41e9a73b1 100644 --- a/homeassistant/components/here_weather/weather.py +++ b/homeassistant/components/here_weather/weather.py @@ -1,8 +1,6 @@ """Weather platform for the HERE Destination Weather service.""" from __future__ import annotations -import logging - from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, ATTR_FORECAST_PRECIPITATION, @@ -38,21 +36,19 @@ get_attribute_from_here_data, ) -_LOGGER = logging.getLogger(__name__) - async def async_setup_entry( - hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ): - """Add here_weather entities from a config_entry.""" - here_weather_data_dict = hass.data[DOMAIN][config_entry.entry_id] + """Add here_weather entities from a ConfigEntry.""" + here_weather_data_dict = hass.data[DOMAIN][entry.entry_id] entities_to_add = [] for sensor_type in SENSOR_TYPES: if sensor_type != MODE_ASTRONOMY: entities_to_add.append( HEREDestinationWeather( - config_entry, + entry, here_weather_data_dict[sensor_type].coordinator, sensor_type, ) @@ -64,11 +60,11 @@ class HEREDestinationWeather(CoordinatorEntity, WeatherEntity): """Implementation of an HERE Destination Weather WeatherEntity.""" def __init__( - self, config_entry: ConfigEntry, coordinator: DataUpdateCoordinator, mode: str + self, entry: ConfigEntry, coordinator: DataUpdateCoordinator, mode: str ) -> None: """Initialize the sensor.""" super().__init__(coordinator) - self._name = config_entry.data[CONF_NAME] + self._name = entry.data[CONF_NAME] self._mode = mode @property From 8fadeb3bc0c8044d5e139078cebf2a783c74aedd Mon Sep 17 00:00:00 2001 From: Kevin Eifinger Date: Thu, 22 Jul 2021 15:41:31 +0200 Subject: [PATCH 33/56] Use .items() to iterate --- homeassistant/components/here_weather/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/here_weather/sensor.py b/homeassistant/components/here_weather/sensor.py index 407ac39eb8865c..24f6890240b878 100644 --- a/homeassistant/components/here_weather/sensor.py +++ b/homeassistant/components/here_weather/sensor.py @@ -22,8 +22,8 @@ async def async_setup_entry( here_weather_data_dict = hass.data[DOMAIN][entry.entry_id] sensors_to_add = [] - for sensor_type in SENSOR_TYPES: - for weather_attribute in SENSOR_TYPES[sensor_type]: + for sensor_type, weather_attributes in SENSOR_TYPES.items(): + for weather_attribute in weather_attributes: sensors_to_add.append( HEREDestinationWeatherSensor( entry, From 8c3d47d60a0677c49cc0bc32941588f6a735f496 Mon Sep 17 00:00:00 2001 From: Kevin Eifinger Date: Thu, 22 Jul 2021 15:50:03 +0200 Subject: [PATCH 34/56] Use generator expression --- homeassistant/components/here_weather/weather.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/here_weather/weather.py b/homeassistant/components/here_weather/weather.py index 3101f41e9a73b1..cb796c9dfcb322 100644 --- a/homeassistant/components/here_weather/weather.py +++ b/homeassistant/components/here_weather/weather.py @@ -259,14 +259,14 @@ def get_precipitation_probability( def get_condition_from_here_data(here_data: list, offset: int = 0) -> str | None: """Return the condition from here_data.""" - try: - return [ + return next( + ( k for k, v in CONDITION_CLASSES.items() if get_attribute_from_here_data(here_data, "iconName", offset) in v - ][0] - except IndexError: - return None # Fallback if the API introduces new values + ), + None, + ) def get_high_or_default_temperature_from_here_data( From 5e0c87814c3bbe1f73fbe715684dba2d86e1d089 Mon Sep 17 00:00:00 2001 From: Kevin Eifinger Date: Thu, 22 Jul 2021 16:06:12 +0200 Subject: [PATCH 35/56] remove title from strings.json --- homeassistant/components/here_weather/strings.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/here_weather/strings.json b/homeassistant/components/here_weather/strings.json index ef9388d8dada09..1765f2984bf019 100644 --- a/homeassistant/components/here_weather/strings.json +++ b/homeassistant/components/here_weather/strings.json @@ -1,9 +1,7 @@ { - "title": "HERE Destination Weather", "config": { "step": { "user": { - "title": "HERE Destination Weather", "description": "[%key:common::config_flow::description::confirm_setup%]", "data": { "api_key": "[%key:common::config_flow::data::api_key%]", From bf7f9adf72fba862ce9335013e48e3bef1b74b1e Mon Sep 17 00:00:00 2001 From: Kevin Eifinger Date: Thu, 22 Jul 2021 16:07:45 +0200 Subject: [PATCH 36/56] remove fire start event --- .../here_weather/test_config_flow.py | 55 ++++++++++++------- 1 file changed, 34 insertions(+), 21 deletions(-) diff --git a/tests/components/here_weather/test_config_flow.py b/tests/components/here_weather/test_config_flow.py index 148073ad843c11..11808efe9d96aa 100644 --- a/tests/components/here_weather/test_config_flow.py +++ b/tests/components/here_weather/test_config_flow.py @@ -3,6 +3,7 @@ import herepy +from homeassistant import config_entries, setup from homeassistant.components.here_weather.const import DOMAIN, HERE_API_KEYS from homeassistant.const import ( CONF_API_KEY, @@ -12,36 +13,49 @@ CONF_SCAN_INTERVAL, EVENT_HOMEASSISTANT_START, ) +from homeassistant.core import HomeAssistant from . import mock_weather_for_coordinates from tests.common import MockConfigEntry -async def test_config_flow(hass): - """Test we can finish a config flow.""" +async def test_form(hass: HomeAssistant) -> None: + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + with patch( "herepy.DestinationWeatherApi.weather_for_coordinates", - side_effect=mock_weather_for_coordinates, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"} - ) - assert result["type"] == "form" - config = { - CONF_API_KEY: "test", - CONF_NAME: DOMAIN, - CONF_LATITUDE: "40.79962", - CONF_LONGITUDE: "-73.970314", - } - result = await hass.config_entries.flow.async_configure( - result["flow_id"], config + return_value=None, + ), patch( + "homeassistant.components.here_weather.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_API_KEY: "test", + CONF_NAME: DOMAIN, + CONF_LATITUDE: "40.79962", + CONF_LONGITUDE: "-73.970314", + }, ) - assert result["type"] == "create_entry" - await hass.async_block_till_done() - state = hass.states.get("weather.here_weather_forecast_7days_simple") - assert state + + assert result2["type"] == "create_entry" + assert result2["title"] == DOMAIN + assert result2["data"] == { + CONF_API_KEY: "test", + CONF_NAME: DOMAIN, + CONF_LATITUDE: 40.79962, + CONF_LONGITUDE: -73.970314, + } + assert len(mock_setup_entry.mock_calls) == 1 async def test_config_flow_known_api_key(hass): @@ -135,7 +149,6 @@ async def test_options(hass): entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) result = await hass.config_entries.options.async_init(entry.entry_id) result = await hass.config_entries.options.async_configure( result["flow_id"], From b8b8e744cd9b5379f57cfedb51987ac9c18ba530 Mon Sep 17 00:00:00 2001 From: Kevin Eifinger Date: Thu, 22 Jul 2021 16:15:45 +0200 Subject: [PATCH 37/56] Move test_unload_entry --- .../here_weather/test_config_flow.py | 27 --------------- tests/components/here_weather/test_init.py | 34 +++++++++++++++++++ 2 files changed, 34 insertions(+), 27 deletions(-) create mode 100644 tests/components/here_weather/test_init.py diff --git a/tests/components/here_weather/test_config_flow.py b/tests/components/here_weather/test_config_flow.py index 11808efe9d96aa..7f182282034aad 100644 --- a/tests/components/here_weather/test_config_flow.py +++ b/tests/components/here_weather/test_config_flow.py @@ -11,7 +11,6 @@ CONF_LONGITUDE, CONF_NAME, CONF_SCAN_INTERVAL, - EVENT_HOMEASSISTANT_START, ) from homeassistant.core import HomeAssistant @@ -157,29 +156,3 @@ async def test_options(hass): await hass.async_block_till_done() assert result["type"] == "create_entry" assert result["data"][CONF_SCAN_INTERVAL] == 10 - - -async def test_unload_entry(hass): - """Test unloading a config entry removes all entities.""" - with patch( - "herepy.DestinationWeatherApi.weather_for_coordinates", - side_effect=mock_weather_for_coordinates, - ): - entry = MockConfigEntry( - domain=DOMAIN, - data={ - CONF_API_KEY: "test", - CONF_NAME: DOMAIN, - CONF_LATITUDE: "40.79962", - CONF_LONGITUDE: "-73.970314", - }, - ) - entry.add_to_hass(hass) - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) - await hass.async_block_till_done() - assert hass.data[DOMAIN] - assert await hass.config_entries.async_unload(entry.entry_id) - await hass.async_block_till_done() - assert not hass.data[DOMAIN] diff --git a/tests/components/here_weather/test_init.py b/tests/components/here_weather/test_init.py new file mode 100644 index 00000000000000..2fd27a6c778342 --- /dev/null +++ b/tests/components/here_weather/test_init.py @@ -0,0 +1,34 @@ +"""Tests for the here_weather integration.""" + +from unittest.mock import patch + +from homeassistant.components.here_weather.const import DOMAIN +from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME + +from . import mock_weather_for_coordinates + +from tests.common import MockConfigEntry + + +async def test_unload_entry(hass): + """Test unloading a config entry removes all entities.""" + with patch( + "herepy.DestinationWeatherApi.weather_for_coordinates", + side_effect=mock_weather_for_coordinates, + ): + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_API_KEY: "test", + CONF_NAME: DOMAIN, + CONF_LATITUDE: "40.79962", + CONF_LONGITUDE: "-73.970314", + }, + ) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert hass.data[DOMAIN] + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + assert not hass.data[DOMAIN] From ff12f84403ca50ac9efb85106a340bed3cbe9e6a Mon Sep 17 00:00:00 2001 From: Kevin Eifinger Date: Thu, 22 Jul 2021 16:17:42 +0200 Subject: [PATCH 38/56] Inherit from SensorEntity --- homeassistant/components/here_weather/sensor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/here_weather/sensor.py b/homeassistant/components/here_weather/sensor.py index 24f6890240b878..db3322d99c2e6a 100644 --- a/homeassistant/components/here_weather/sensor.py +++ b/homeassistant/components/here_weather/sensor.py @@ -1,6 +1,7 @@ """Sensor platform for the HERE Destination Weather service.""" from __future__ import annotations +from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.core import HomeAssistant @@ -35,7 +36,7 @@ async def async_setup_entry( async_add_entities(sensors_to_add) -class HEREDestinationWeatherSensor(CoordinatorEntity): +class HEREDestinationWeatherSensor(CoordinatorEntity, SensorEntity): """Implementation of an HERE Destination Weather sensor.""" def __init__( From 34104ed8240aef0e98e90517905aadf757958d97 Mon Sep 17 00:00:00 2001 From: Kevin Eifinger Date: Thu, 22 Jul 2021 16:29:29 +0200 Subject: [PATCH 39/56] Use known api keys from entries --- .../components/here_weather/__init__.py | 6 +-- .../components/here_weather/config_flow.py | 14 +++---- .../components/here_weather/const.py | 2 - .../here_weather/test_config_flow.py | 41 ++++++------------- 4 files changed, 18 insertions(+), 45 deletions(-) diff --git a/homeassistant/components/here_weather/__init__.py b/homeassistant/components/here_weather/__init__.py index c9bff85c4db67d..f06273655873e2 100644 --- a/homeassistant/components/here_weather/__init__.py +++ b/homeassistant/components/here_weather/__init__.py @@ -18,7 +18,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import CONF_MODES, DEFAULT_SCAN_INTERVAL, DOMAIN, HERE_API_KEYS +from .const import CONF_MODES, DEFAULT_SCAN_INTERVAL, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -34,10 +34,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: here_weather_data_dict[mode] = here_weather_data hass.data.setdefault(DOMAIN, {})[entry.entry_id] = here_weather_data_dict - known_api_keys = hass.data.setdefault(HERE_API_KEYS, []) - if entry.data[CONF_API_KEY] not in known_api_keys: - known_api_keys.append(entry.data[CONF_API_KEY]) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True diff --git a/homeassistant/components/here_weather/config_flow.py b/homeassistant/components/here_weather/config_flow.py index 29e03359b973a7..73741bcb4f53cf 100644 --- a/homeassistant/components/here_weather/config_flow.py +++ b/homeassistant/components/here_weather/config_flow.py @@ -1,8 +1,6 @@ """Config flow for here_weather integration.""" from __future__ import annotations -import logging - import herepy import voluptuous as vol @@ -18,9 +16,7 @@ from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv -from .const import DEFAULT_MODE, DEFAULT_SCAN_INTERVAL, DOMAIN, HERE_API_KEYS - -_LOGGER = logging.getLogger(__name__) +from .const import DEFAULT_MODE, DEFAULT_SCAN_INTERVAL, DOMAIN async def async_validate_user_input(hass: HomeAssistant, user_input: dict) -> None: @@ -66,9 +62,9 @@ async def async_step_user(self, user_input=None): ) def _get_schema(self, user_input: dict | None) -> vol.Schema: - known_api_key = None - if HERE_API_KEYS in self.hass.data: - known_api_key = self.hass.data[HERE_API_KEYS][0] + known_api_keys = [ + entry.data[CONF_API_KEY] for entry in self._async_current_entries() + ] if user_input is not None: return vol.Schema( { @@ -84,7 +80,7 @@ def _get_schema(self, user_input: dict | None) -> vol.Schema: ) return vol.Schema( { - vol.Required(CONF_API_KEY, default=known_api_key): str, + vol.Required(CONF_API_KEY, default=known_api_keys): str, vol.Required(CONF_NAME, default=DOMAIN): str, vol.Required( CONF_LATITUDE, default=self.hass.config.latitude diff --git a/homeassistant/components/here_weather/const.py b/homeassistant/components/here_weather/const.py index 6e93a99a7af1f7..4fefba58a3a8c0 100644 --- a/homeassistant/components/here_weather/const.py +++ b/homeassistant/components/here_weather/const.py @@ -3,8 +3,6 @@ DOMAIN = "here_weather" -HERE_API_KEYS = "here_api_keys" - DEFAULT_SCAN_INTERVAL = 120 CONF_LANGUAGE = "language" diff --git a/tests/components/here_weather/test_config_flow.py b/tests/components/here_weather/test_config_flow.py index 7f182282034aad..007f47217e0170 100644 --- a/tests/components/here_weather/test_config_flow.py +++ b/tests/components/here_weather/test_config_flow.py @@ -4,7 +4,7 @@ import herepy from homeassistant import config_entries, setup -from homeassistant.components.here_weather.const import DOMAIN, HERE_API_KEYS +from homeassistant.components.here_weather.const import DOMAIN from homeassistant.const import ( CONF_API_KEY, CONF_LATITUDE, @@ -35,6 +35,16 @@ async def test_form(hass: HomeAssistant) -> None: "homeassistant.components.here_weather.async_setup_entry", return_value=True, ) as mock_setup_entry: + existing_entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_API_KEY: "test", + CONF_NAME: DOMAIN, + CONF_LATITUDE: "40.79962", + CONF_LONGITUDE: "-73.970314", + }, + ) + existing_entry.add_to_hass(hass) result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -54,34 +64,7 @@ async def test_form(hass: HomeAssistant) -> None: CONF_LATITUDE: 40.79962, CONF_LONGITUDE: -73.970314, } - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_config_flow_known_api_key(hass): - """Test we can finish a config flow.""" - with patch( - "herepy.DestinationWeatherApi.weather_for_coordinates", - side_effect=mock_weather_for_coordinates, - ): - hass.data.setdefault(HERE_API_KEYS, []).append("test") - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"} - ) - assert result["type"] == "form" - config = { - CONF_API_KEY: "test", - CONF_NAME: DOMAIN, - CONF_LATITUDE: "40.79962", - CONF_LONGITUDE: "-73.970314", - } - result = await hass.config_entries.flow.async_configure( - result["flow_id"], config - ) - assert result["type"] == "create_entry" - - await hass.async_block_till_done() - state = hass.states.get("weather.here_weather_forecast_7days_simple") - assert state + assert len(mock_setup_entry.mock_calls) == 2 async def test_unauthorized(hass): From 1315639d8245ec8a73728d78e32d3111d7a5ed7a Mon Sep 17 00:00:00 2001 From: Kevin Eifinger Date: Thu, 22 Jul 2021 16:55:41 +0200 Subject: [PATCH 40/56] Set unique id --- .../components/here_weather/config_flow.py | 6 ++++++ homeassistant/components/here_weather/sensor.py | 4 ++-- homeassistant/components/here_weather/weather.py | 9 ++++++--- tests/components/here_weather/test_sensor.py | 16 +++------------- tests/components/here_weather/test_weather.py | 14 +------------- 5 files changed, 18 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/here_weather/config_flow.py b/homeassistant/components/here_weather/config_flow.py index 73741bcb4f53cf..9436710ff34c9f 100644 --- a/homeassistant/components/here_weather/config_flow.py +++ b/homeassistant/components/here_weather/config_flow.py @@ -47,6 +47,8 @@ async def async_step_user(self, user_input=None): errors = {} if user_input is not None: try: + await self.async_set_unique_id(_unique_id(user_input)) + self._abort_if_unique_id_configured() await async_validate_user_input(self.hass, user_input) return self.async_create_entry( title=user_input[CONF_NAME], data=user_input @@ -92,6 +94,10 @@ def _get_schema(self, user_input: dict | None) -> vol.Schema: ) +def _unique_id(user_input: dict) -> str: + return f"{user_input[CONF_LATITUDE]}_{user_input[CONF_LONGITUDE]}" + + class HereWeatherOptionsFlowHandler(config_entries.OptionsFlow): """Handle here_weather options.""" diff --git a/homeassistant/components/here_weather/sensor.py b/homeassistant/components/here_weather/sensor.py index db3322d99c2e6a..e5f17a3ed176fe 100644 --- a/homeassistant/components/here_weather/sensor.py +++ b/homeassistant/components/here_weather/sensor.py @@ -61,7 +61,7 @@ def __init__( SENSOR_TYPES[sensor_type][weather_attribute]["unit_of_measurement"], ) self._unique_id = "".join( - f"{self._base_name}_{self._sensor_type}_{self._name_suffix}_{self._sensor_number}".lower().split() + f"{self._latitude}_{self._longitude}_{self._sensor_type}_{self._name_suffix}_{self._sensor_number}".lower().split() ) @property @@ -100,7 +100,7 @@ def device_info(self) -> DeviceInfo: """Return a device description for device registry.""" return { - "identifiers": {(DOMAIN, self._base_name)}, + "identifiers": {(DOMAIN, self._unique_id)}, "name": self._base_name, "manufacturer": "here.com", "entry_type": "service", diff --git a/homeassistant/components/here_weather/weather.py b/homeassistant/components/here_weather/weather.py index cb796c9dfcb322..a1cc812852451b 100644 --- a/homeassistant/components/here_weather/weather.py +++ b/homeassistant/components/here_weather/weather.py @@ -15,7 +15,7 @@ WeatherEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_NAME, TEMP_CELSIUS +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, TEMP_CELSIUS from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import ( @@ -66,6 +66,9 @@ def __init__( super().__init__(coordinator) self._name = entry.data[CONF_NAME] self._mode = mode + self._unique_id = "".join( + f"{entry.data[CONF_LATITUDE]}_{entry.data[CONF_LONGITUDE]}_{self._mode}".lower().split() + ) @property def name(self): @@ -75,7 +78,7 @@ def name(self): @property def unique_id(self): """Set unique_id for sensor.""" - return f"{self._name}_{self._mode}" + return self._unique_id @property def condition(self) -> str | None: @@ -200,7 +203,7 @@ def entity_registry_enabled_default(self): def device_info(self) -> DeviceInfo: """Return a device description for device registry.""" return { - "identifiers": {(DOMAIN, self._name)}, + "identifiers": {(DOMAIN, self._unique_id)}, "name": self._name, "manufacturer": "here.com", } diff --git a/tests/components/here_weather/test_sensor.py b/tests/components/here_weather/test_sensor.py index 4932aec4121a8b..762866c261b172 100644 --- a/tests/components/here_weather/test_sensor.py +++ b/tests/components/here_weather/test_sensor.py @@ -11,7 +11,6 @@ CONF_LONGITUDE, CONF_NAME, CONF_SCAN_INTERVAL, - EVENT_HOMEASSISTANT_START, ) import homeassistant.util.dt as dt_util from homeassistant.util.unit_system import IMPERIAL_SYSTEM @@ -47,7 +46,7 @@ async def test_sensor_invalid_request(hass): registry.async_get_or_create( "sensor", DOMAIN, - "here_weather_forecast_7days_simple_windspeed_0", + "40.79962_-73.970314_forecast_7days_simple_windspeed_0", suggested_object_id="here_weather_forecast_7days_simple_windspeed_0", disabled_by=None, ) @@ -56,9 +55,6 @@ async def test_sensor_invalid_request(hass): await hass.async_block_till_done() - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) - await hass.async_block_till_done() - sensor = hass.states.get( "sensor.here_weather_forecast_7days_simple_windspeed_0" ) @@ -99,7 +95,7 @@ async def test_forecast_astronomy(hass): registry.async_get_or_create( "sensor", DOMAIN, - "here_weather_forecast_astronomy_sunrise_0", + "40.79962_-73.970314_forecast_astronomy_sunrise_0", suggested_object_id="here_weather_forecast_astronomy_sunrise_0", disabled_by=None, ) @@ -108,9 +104,6 @@ async def test_forecast_astronomy(hass): await hass.async_block_till_done() - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) - await hass.async_block_till_done() - sensor = hass.states.get("sensor.here_weather_forecast_astronomy_sunrise_0") assert sensor.state == "6:55AM" @@ -142,7 +135,7 @@ async def test_imperial(hass): registry.async_get_or_create( "sensor", DOMAIN, - "here_weather_forecast_7days_simple_windspeed_0", + "40.79962_-73.970314_forecast_7days_simple_windspeed_0", suggested_object_id="here_weather_forecast_7days_simple_windspeed_0", disabled_by=None, ) @@ -151,9 +144,6 @@ async def test_imperial(hass): await hass.async_block_till_done() - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) - await hass.async_block_till_done() - sensor = hass.states.get( "sensor.here_weather_forecast_7days_simple_windspeed_0" ) diff --git a/tests/components/here_weather/test_weather.py b/tests/components/here_weather/test_weather.py index 8fef60417fc4fc..5ff700587d9c7a 100644 --- a/tests/components/here_weather/test_weather.py +++ b/tests/components/here_weather/test_weather.py @@ -2,13 +2,7 @@ from unittest.mock import patch from homeassistant.components.here_weather.const import DOMAIN -from homeassistant.const import ( - CONF_API_KEY, - CONF_LATITUDE, - CONF_LONGITUDE, - CONF_NAME, - EVENT_HOMEASSISTANT_START, -) +from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.util.unit_system import IMPERIAL_SYSTEM from . import mock_weather_for_coordinates @@ -38,9 +32,6 @@ async def test_weather(hass): await hass.async_block_till_done() - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) - await hass.async_block_till_done() - sensor = hass.states.get("weather.here_weather_forecast_7days_simple") assert sensor.state == "cloudy" @@ -78,8 +69,5 @@ async def test_weather_daily(hass): await hass.async_block_till_done() - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) - await hass.async_block_till_done() - sensor = hass.states.get("weather.here_weather_forecast_7days") assert sensor.state == "cloudy" From 8a0dd2325b18979a6f8ed0d77837555e52cfd384 Mon Sep 17 00:00:00 2001 From: Kevin Eifinger Date: Thu, 22 Jul 2021 16:56:33 +0200 Subject: [PATCH 41/56] remove CONNECTION_CLASS --- homeassistant/components/here_weather/config_flow.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/here_weather/config_flow.py b/homeassistant/components/here_weather/config_flow.py index 9436710ff34c9f..5e94dadb343443 100644 --- a/homeassistant/components/here_weather/config_flow.py +++ b/homeassistant/components/here_weather/config_flow.py @@ -34,7 +34,6 @@ class HereWeatherConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for here_weather.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL @staticmethod @callback From 76f1ce5ef37aa3d1703900111f297761ef188cc1 Mon Sep 17 00:00:00 2001 From: Kevin Eifinger Date: Thu, 22 Jul 2021 16:58:42 +0200 Subject: [PATCH 42/56] remove localTime --- homeassistant/components/here_weather/const.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/here_weather/const.py b/homeassistant/components/here_weather/const.py index 4fefba58a3a8c0..77f48de746f643 100644 --- a/homeassistant/components/here_weather/const.py +++ b/homeassistant/components/here_weather/const.py @@ -69,8 +69,6 @@ "dayOfWeek": {"name": "Day of Week", "unit_of_measurement": None}, "weekday": {"name": "Week Day", "unit_of_measurement": None}, "utcTime": {"name": "UTC Time", "unit_of_measurement": None}, - "localTime": {"name": "Local Time", "unit_of_measurement": None}, - "localTimeFormat": {"name": "Local Time Format", "unit_of_measurement": None}, } DAILY_SIMPLE_ATTRIBUTES: dict[str, dict[str, str | None]] = { From 40949577d7e7c78c0ec625ff004b5e0d14db179f Mon Sep 17 00:00:00 2001 From: Kevin Eifinger Date: Thu, 22 Jul 2021 21:26:58 +0200 Subject: [PATCH 43/56] add device_class --- .../components/here_weather/const.py | 383 ++++++++++++------ .../components/here_weather/sensor.py | 20 +- .../components/here_weather/weather.py | 50 +-- tests/components/here_weather/test_weather.py | 2 +- 4 files changed, 280 insertions(+), 175 deletions(-) diff --git a/homeassistant/components/here_weather/const.py b/homeassistant/components/here_weather/const.py index 77f48de746f643..decae962b9b1a7 100644 --- a/homeassistant/components/here_weather/const.py +++ b/homeassistant/components/here_weather/const.py @@ -1,6 +1,20 @@ """Constants for the HERE Destination Weather service.""" from __future__ import annotations +from homeassistant.const import ( + DEGREE, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_TIMESTAMP, + LENGTH_CENTIMETERS, + LENGTH_KILOMETERS, + PERCENTAGE, + PRESSURE_MBAR, + SPEED_KILOMETERS_PER_HOUR, + TEMP_CELSIUS, +) + DOMAIN = "here_weather" DEFAULT_SCAN_INTERVAL = 120 @@ -24,193 +38,294 @@ DEFAULT_MODE = MODE_DAILY_SIMPLE ASTRONOMY_ATTRIBUTES: dict[str, dict[str, str | None]] = { - "sunrise": {"name": "Sunrise", "unit_of_measurement": None}, - "sunset": {"name": "Sunset", "unit_of_measurement": None}, - "moonrise": {"name": "Moonrise", "unit_of_measurement": None}, - "moonset": {"name": "Moonset", "unit_of_measurement": None}, - "moonPhase": {"name": "Moon Phase", "unit_of_measurement": "%"}, - "moonPhaseDesc": {"name": "Moon Phase Description", "unit_of_measurement": None}, - "city": {"name": "City", "unit_of_measurement": None}, - "latitude": {"name": "Latitude", "unit_of_measurement": None}, - "longitude": {"name": "Longitude", "unit_of_measurement": None}, - "utcTime": {"name": "UTC Time", "unit_of_measurement": None}, + "sunrise": {"name": "Sunrise", "unit_of_measurement": None, "device_class": None}, + "sunset": {"name": "Sunset", "unit_of_measurement": None, "device_class": None}, + "moonrise": {"name": "Moonrise", "unit_of_measurement": None, "device_class": None}, + "moonset": {"name": "Moonset", "unit_of_measurement": None, "device_class": None}, + "moonPhase": { + "name": "Moon Phase", + "unit_of_measurement": "%", + "device_class": None, + }, + "moonPhaseDesc": { + "name": "Moon Phase Description", + "unit_of_measurement": None, + "device_class": None, + }, + "city": {"name": "City", "unit_of_measurement": None, "device_class": None}, + "latitude": {"name": "Latitude", "unit_of_measurement": None, "device_class": None}, + "longitude": { + "name": "Longitude", + "unit_of_measurement": None, + "device_class": None, + }, + "utcTime": { + "name": "UTC Time", + "unit_of_measurement": None, + "device_class": DEVICE_CLASS_TIMESTAMP, + }, } -HOURLY_ATTRIBUTES: dict[str, dict[str, str | None]] = { - "daylight": {"name": "Daylight", "unit_of_measurement": None}, - "description": {"name": "Description", "unit_of_measurement": None}, - "skyInfo": {"name": "Sky Info", "unit_of_measurement": None}, - "skyDescription": {"name": "Sky Description", "unit_of_measurement": None}, - "temperature": {"name": "Temperature", "unit_of_measurement": "°C"}, - "temperatureDesc": {"name": "Temperature Description", "unit_of_measurement": None}, - "comfort": {"name": "Comfort", "unit_of_measurement": "°C"}, - "humidity": {"name": "Humidity", "unit_of_measurement": "%"}, - "dewPoint": {"name": "Dew Point", "unit_of_measurement": "°C"}, +COMMON_ATTRIBUTES: dict[str, dict[str, str | None]] = { + "daylight": {"name": "Daylight", "unit_of_measurement": None, "device_class": None}, + "description": { + "name": "Description", + "unit_of_measurement": None, + "device_class": None, + }, + "skyInfo": {"name": "Sky Info", "unit_of_measurement": None, "device_class": None}, + "skyDescription": { + "name": "Sky Description", + "unit_of_measurement": None, + "device_class": None, + }, + "temperatureDesc": { + "name": "Temperature Description", + "unit_of_measurement": None, + "device_class": None, + }, + "comfort": { + "name": "Comfort", + "unit_of_measurement": TEMP_CELSIUS, + "device_class": DEVICE_CLASS_TEMPERATURE, + }, + "humidity": { + "name": "Humidity", + "unit_of_measurement": PERCENTAGE, + "device_class": DEVICE_CLASS_HUMIDITY, + }, + "dewPoint": { + "name": "Dew Point", + "unit_of_measurement": TEMP_CELSIUS, + "device_class": DEVICE_CLASS_TEMPERATURE, + }, "precipitationProbability": { "name": "Precipitation Probability", - "unit_of_measurement": "%", + "unit_of_measurement": PERCENTAGE, + "device_class": None, }, "precipitationDesc": { "name": "Precipitation Description", "unit_of_measurement": None, + "device_class": None, + }, + "airDescription": { + "name": "Air Description", + "unit_of_measurement": None, + "device_class": None, + }, + "windSpeed": { + "name": "Wind Speed", + "unit_of_measurement": SPEED_KILOMETERS_PER_HOUR, + "device_class": None, + }, + "windDirection": { + "name": "Wind Direction", + "unit_of_measurement": DEGREE, + "device_class": None, + }, + "windDesc": { + "name": "Wind Description", + "unit_of_measurement": LENGTH_CENTIMETERS, + "device_class": None, + }, + "windDescShort": { + "name": "Wind Description Short", + "unit_of_measurement": LENGTH_CENTIMETERS, + "device_class": None, + }, + "icon": {"name": "Icon", "unit_of_measurement": None, "device_class": None}, + "iconName": { + "name": "Icon Name", + "unit_of_measurement": None, + "device_class": None, + }, + "iconLink": { + "name": "Icon Link", + "unit_of_measurement": None, + "device_class": None, }, - "rainFall": {"name": "Rain Fall", "unit_of_measurement": "cm"}, - "snowFall": {"name": "Snow Fall", "unit_of_measurement": "cm"}, - "airInfo": {"name": "Air Info", "unit_of_measurement": None}, - "airDescription": {"name": "Air Description", "unit_of_measurement": None}, - "windSpeed": {"name": "Wind Speed", "unit_of_measurement": "km/h"}, - "windDirection": {"name": "Wind Direction", "unit_of_measurement": "°"}, - "windDesc": {"name": "Wind Description", "unit_of_measurement": "cm"}, - "windDescShort": {"name": "Wind Description Short", "unit_of_measurement": "cm"}, - "visibility": {"name": "Visibility", "unit_of_measurement": "km"}, - "icon": {"name": "Icon", "unit_of_measurement": None}, - "iconName": {"name": "Icon Name", "unit_of_measurement": None}, - "iconLink": {"name": "Icon Link", "unit_of_measurement": None}, - "dayOfWeek": {"name": "Day of Week", "unit_of_measurement": None}, - "weekday": {"name": "Week Day", "unit_of_measurement": None}, - "utcTime": {"name": "UTC Time", "unit_of_measurement": None}, } -DAILY_SIMPLE_ATTRIBUTES: dict[str, dict[str, str | None]] = { - "daylight": {"name": "Daylight", "unit_of_measurement": None}, - "description": {"name": "Description", "unit_of_measurement": None}, - "skyInfo": {"name": "Sky Info", "unit_of_measurement": None}, - "skyDescription": {"name": "Sky Description", "unit_of_measurement": None}, - "temperatureDesc": {"name": "Temperature Description", "unit_of_measurement": None}, - "comfort": {"name": "Comfort", "unit_of_measurement": "°C"}, - "highTemperature": {"name": "High Temperature", "unit_of_measurement": "°C"}, - "lowTemperature": {"name": "Low Temperature", "unit_of_measurement": "°C"}, - "humidity": {"name": "Humidity", "unit_of_measurement": "%"}, - "dewPoint": {"name": "Dew Point", "unit_of_measurement": "°C"}, +NON_OBERSATION_ATTRIBUTES: dict[str, dict[str, str | None]] = { + "rainFall": { + "name": "Rain Fall", + "unit_of_measurement": LENGTH_CENTIMETERS, + "device_class": None, + }, + "snowFall": { + "name": "Snow Fall", + "unit_of_measurement": LENGTH_CENTIMETERS, + "device_class": None, + }, "precipitationProbability": { "name": "Precipitation Probability", - "unit_of_measurement": "%", + "unit_of_measurement": PERCENTAGE, + "device_class": None, }, - "precipitationDesc": { - "name": "Precipitation Description", + "dayOfWeek": { + "name": "Day of Week", "unit_of_measurement": None, + "device_class": None, + }, + "weekday": {"name": "Week Day", "unit_of_measurement": None, "device_class": None}, + "utcTime": { + "name": "UTC Time", + "unit_of_measurement": None, + "device_class": DEVICE_CLASS_TIMESTAMP, + }, + **COMMON_ATTRIBUTES, +} + +HOURLY_ATTRIBUTES: dict[str, dict[str, str | None]] = { + "temperature": { + "name": "Temperature", + "unit_of_measurement": TEMP_CELSIUS, + "device_class": DEVICE_CLASS_TEMPERATURE, + }, + "airInfo": {"name": "Air Info", "unit_of_measurement": None, "device_class": None}, + "visibility": { + "name": "Visibility", + "unit_of_measurement": LENGTH_KILOMETERS, + "device_class": None, + }, + **NON_OBERSATION_ATTRIBUTES, +} + +DAILY_SIMPLE_ATTRIBUTES: dict[str, dict[str, str | None]] = { + "highTemperature": { + "name": "High Temperature", + "unit_of_measurement": TEMP_CELSIUS, + "device_class": DEVICE_CLASS_TEMPERATURE, + }, + "lowTemperature": { + "name": "Low Temperature", + "unit_of_measurement": TEMP_CELSIUS, + "device_class": DEVICE_CLASS_TEMPERATURE, + }, + "beaufortScale": { + "name": "Beaufort Scale", + "unit_of_measurement": None, + "device_class": None, }, - "rainFall": {"name": "Rain Fall", "unit_of_measurement": "cm"}, - "snowFall": {"name": "Snow Fall", "unit_of_measurement": "cm"}, - "airInfo": {"name": "Air Info", "unit_of_measurement": None}, - "airDescription": {"name": "Air Description", "unit_of_measurement": None}, - "windSpeed": {"name": "Wind Speed", "unit_of_measurement": "km/h"}, - "windDirection": {"name": "Wind Direction", "unit_of_measurement": "°"}, - "windDesc": {"name": "Wind Description", "unit_of_measurement": "cm"}, - "windDescShort": {"name": "Wind Description Short", "unit_of_measurement": "cm"}, - "beaufortScale": {"name": "Beaufort Scale", "unit_of_measurement": None}, "beaufortDescription": { "name": "Beaufort Scale Description", "unit_of_measurement": None, + "device_class": None, }, - "uvIndex": {"name": "UV Index", "unit_of_measurement": None}, - "uvDesc": {"name": "UV Index Description", "unit_of_measurement": None}, - "barometerPressure": {"name": "Barometric Pressure", "unit_of_measurement": "mbar"}, - "icon": {"name": "Icon", "unit_of_measurement": None}, - "iconName": {"name": "Icon Name", "unit_of_measurement": None}, - "iconLink": {"name": "Icon Link", "unit_of_measurement": None}, - "dayOfWeek": {"name": "Day of Week", "unit_of_measurement": None}, - "weekday": {"name": "Week Day", "unit_of_measurement": None}, - "utcTime": {"name": "UTC Time", "unit_of_measurement": None}, + "uvIndex": {"name": "UV Index", "unit_of_measurement": None, "device_class": None}, + "uvDesc": { + "name": "UV Index Description", + "unit_of_measurement": None, + "device_class": None, + }, + "barometerPressure": { + "name": "Barometric Pressure", + "unit_of_measurement": PRESSURE_MBAR, + "device_class": DEVICE_CLASS_PRESSURE, + }, + **NON_OBERSATION_ATTRIBUTES, } DAILY_ATTRIBUTES: dict[str, dict[str, str | None]] = { - "daylight": {"name": "Daylight", "unit_of_measurement": None}, - "daySegment": {"name": "Day Segment", "unit_of_measurement": None}, - "description": {"name": "Description", "unit_of_measurement": None}, - "skyInfo": {"name": "Sky Info", "unit_of_measurement": None}, - "skyDescription": {"name": "Sky Description", "unit_of_measurement": None}, - "temperature": {"name": "Temperature", "unit_of_measurement": "°C"}, - "temperatureDesc": {"name": "Temperature Description", "unit_of_measurement": None}, - "comfort": {"name": "Comfort", "unit_of_measurement": "°C"}, - "humidity": {"name": "Humidity", "unit_of_measurement": "%"}, - "dewPoint": {"name": "Dew Point", "unit_of_measurement": "°C"}, - "precipitationProbability": { - "name": "Precipitation Probability", - "unit_of_measurement": "%", + "daySegment": { + "name": "Day Segment", + "unit_of_measurement": None, + "device_class": None, }, - "precipitationDesc": { - "name": "Precipitation Description", + "temperature": { + "name": "Temperature", + "unit_of_measurement": TEMP_CELSIUS, + "device_class": DEVICE_CLASS_TEMPERATURE, + }, + "beaufortScale": { + "name": "Beaufort Scale", "unit_of_measurement": None, + "device_class": None, }, - "rainFall": {"name": "Rain Fall", "unit_of_measurement": "cm"}, - "snowFall": {"name": "Snow Fall", "unit_of_measurement": "cm"}, - "airInfo": {"name": "Air Info", "unit_of_measurement": None}, - "airDescription": {"name": "Air Description", "unit_of_measurement": None}, - "windSpeed": {"name": "Wind Speed", "unit_of_measurement": "km/h"}, - "windDirection": {"name": "Wind Direction", "unit_of_measurement": "°"}, - "windDesc": {"name": "Wind Description", "unit_of_measurement": "cm"}, - "windDescShort": {"name": "Wind Description Short", "unit_of_measurement": "cm"}, - "beaufortScale": {"name": "Beaufort Scale", "unit_of_measurement": None}, "beaufortDescription": { "name": "Beaufort Scale Description", "unit_of_measurement": None, + "device_class": None, + }, + "visibility": { + "name": "Visibility", + "unit_of_measurement": LENGTH_KILOMETERS, + "device_class": None, }, - "visibility": {"name": "Visibility", "unit_of_measurement": "km"}, - "icon": {"name": "Icon", "unit_of_measurement": None}, - "iconName": {"name": "Icon Name", "unit_of_measurement": None}, - "iconLink": {"name": "Icon Link", "unit_of_measurement": None}, - "dayOfWeek": {"name": "Day of Week", "unit_of_measurement": None}, - "weekday": {"name": "Week Day", "unit_of_measurement": None}, - "utcTime": {"name": "UTC Time", "unit_of_measurement": None}, + **NON_OBERSATION_ATTRIBUTES, } OBSERVATION_ATTRIBUTES: dict[str, dict[str, str | None]] = { - "daylight": {"name": "Daylight", "unit_of_measurement": None}, - "description": {"name": "Description", "unit_of_measurement": None}, - "skyInfo": {"name": "Sky Info", "unit_of_measurement": None}, - "skyDescription": {"name": "Sky Description", "unit_of_measurement": None}, - "temperature": {"name": "Temperature", "unit_of_measurement": "°C"}, - "temperatureDesc": {"name": "Temperature Description", "unit_of_measurement": None}, - "comfort": {"name": "Comfort", "unit_of_measurement": "°C"}, - "highTemperature": {"name": "High Temperature", "unit_of_measurement": "°C"}, - "lowTemperature": {"name": "Low Temperature", "unit_of_measurement": "°C"}, - "humidity": {"name": "Humidity", "unit_of_measurement": "%"}, - "dewPoint": {"name": "Dew Point", "unit_of_measurement": "°C"}, + "temperature": { + "name": "Temperature", + "unit_of_measurement": TEMP_CELSIUS, + "device_class": DEVICE_CLASS_TEMPERATURE, + }, + "highTemperature": { + "name": "High Temperature", + "unit_of_measurement": TEMP_CELSIUS, + "device_class": DEVICE_CLASS_TEMPERATURE, + }, + "lowTemperature": { + "name": "Low Temperature", + "unit_of_measurement": TEMP_CELSIUS, + "device_class": DEVICE_CLASS_TEMPERATURE, + }, "precipitation1H": { "name": "Precipitation Over 1 Hour", - "unit_of_measurement": "cm", + "unit_of_measurement": LENGTH_CENTIMETERS, + "device_class": None, }, "precipitation3H": { "name": "Precipitation Over 3 Hours", - "unit_of_measurement": "cm", + "unit_of_measurement": LENGTH_CENTIMETERS, + "device_class": None, }, "precipitation6H": { "name": "Precipitation Over 6 Hours", - "unit_of_measurement": "cm", + "unit_of_measurement": LENGTH_CENTIMETERS, + "device_class": None, }, "precipitation12H": { "name": "Precipitation Over 12 Hours", - "unit_of_measurement": "cm", + "unit_of_measurement": LENGTH_CENTIMETERS, + "device_class": None, }, "precipitation24H": { "name": "Precipitation Over 24 Hours", - "unit_of_measurement": "cm", + "unit_of_measurement": LENGTH_CENTIMETERS, + "device_class": None, }, - "precipitationDesc": { - "name": "Precipitation Description", - "unit_of_measurement": None, + "barometerPressure": { + "name": "Barometric Pressure", + "unit_of_measurement": PRESSURE_MBAR, + "device_class": DEVICE_CLASS_PRESSURE, }, - "airInfo": {"name": "Air Info", "unit_of_measurement": None}, - "airDescription": {"name": "Air Description", "unit_of_measurement": None}, - "windSpeed": {"name": "Wind Speed", "unit_of_measurement": "km/h"}, - "windDirection": {"name": "Wind Direction", "unit_of_measurement": "°"}, - "windDesc": {"name": "Wind Description", "unit_of_measurement": "cm"}, - "windDescShort": {"name": "Wind Description Short", "unit_of_measurement": None}, - "barometerPressure": {"name": "Barometric Pressure", "unit_of_measurement": "mbar"}, "barometerTrend": { "name": "Barometric Pressure Trend", "unit_of_measurement": None, + "device_class": None, + }, + "visibility": { + "name": "Visibility", + "unit_of_measurement": LENGTH_KILOMETERS, + "device_class": None, + }, + "snowCover": { + "name": "Snow Cover", + "unit_of_measurement": LENGTH_CENTIMETERS, + "device_class": None, + }, + "activeAlerts": { + "name": "Active Alerts", + "unit_of_measurement": None, + "device_class": None, }, - "visibility": {"name": "Visibility", "unit_of_measurement": "km"}, - "snowCover": {"name": "Snow Cover", "unit_of_measurement": "cm"}, - "icon": {"name": "Icon", "unit_of_measurement": None}, - "iconName": {"name": "Icon Name", "unit_of_measurement": None}, - "iconLink": {"name": "Icon Link", "unit_of_measurement": None}, - "activeAlerts": {"name": "Active Alerts", "unit_of_measurement": None}, - "country": {"name": "Country", "unit_of_measurement": None}, - "state": {"name": "State", "unit_of_measurement": None}, - "city": {"name": "City", "unit_of_measurement": None}, + "country": {"name": "Country", "unit_of_measurement": None, "device_class": None}, + "state": {"name": "State", "unit_of_measurement": None, "device_class": None}, + "city": {"name": "City", "unit_of_measurement": None, "device_class": None}, + **COMMON_ATTRIBUTES, } SENSOR_TYPES: dict[str, dict[str, dict[str, str | None]]] = { diff --git a/homeassistant/components/here_weather/sensor.py b/homeassistant/components/here_weather/sensor.py index e5f17a3ed176fe..68271faf6ea989 100644 --- a/homeassistant/components/here_weather/sensor.py +++ b/homeassistant/components/here_weather/sensor.py @@ -60,6 +60,9 @@ def __init__( self.coordinator.hass.config.units.name, SENSOR_TYPES[sensor_type][weather_attribute]["unit_of_measurement"], ) + self._device_class = SENSOR_TYPES[sensor_type][weather_attribute][ + "device_class" + ] self._unique_id = "".join( f"{self._latitude}_{self._longitude}_{self._sensor_type}_{self._name_suffix}_{self._sensor_number}".lower().split() ) @@ -82,13 +85,11 @@ def unique_id(self) -> str: @property def state(self) -> StateType: """Return the state of the device.""" - if self.coordinator.data is not None: - return get_attribute_from_here_data( - self.coordinator.data, - self._weather_attribute, - self._sensor_number, - ) - return None + return get_attribute_from_here_data( + self.coordinator.data, + self._weather_attribute, + self._sensor_number, + ) @property def unit_of_measurement(self) -> str | None: @@ -105,3 +106,8 @@ def device_info(self) -> DeviceInfo: "manufacturer": "here.com", "entry_type": "service", } + + @property + def device_class(self) -> str | None: + """Return the class of this device.""" + return self._device_class diff --git a/homeassistant/components/here_weather/weather.py b/homeassistant/components/here_weather/weather.py index a1cc812852451b..ec5e2f8b080fda 100644 --- a/homeassistant/components/here_weather/weather.py +++ b/homeassistant/components/here_weather/weather.py @@ -83,16 +83,12 @@ def unique_id(self): @property def condition(self) -> str | None: """Return the current condition.""" - if self.coordinator.data is not None: - return get_condition_from_here_data(self.coordinator.data) - return None + return get_condition_from_here_data(self.coordinator.data) @property def temperature(self) -> float | None: """Return the temperature.""" - if self.coordinator.data is not None: - return get_temperature_from_here_data(self.coordinator.data, self._mode) - return None + return get_temperature_from_here_data(self.coordinator.data, self._mode) @property def temperature_unit(self) -> str: @@ -104,36 +100,27 @@ def temperature_unit(self) -> str: @property def pressure(self) -> float | None: """Return the pressure.""" - if self.coordinator.data is not None: - return get_pressure_from_here_data(self.coordinator.data, self._mode) - return None + return get_pressure_from_here_data(self.coordinator.data, self._mode) @property def humidity(self) -> float | None: """Return the humidity.""" - if self.coordinator.data is not None: - if ( - humidity := get_attribute_from_here_data( - self.coordinator.data, "humidity" - ) - is not None - ): - return float(humidity) + if ( + humidity := get_attribute_from_here_data(self.coordinator.data, "humidity") + is not None + ): + return float(humidity) return None @property def wind_speed(self) -> float | None: """Return the wind speed.""" - if self.coordinator.data is not None: - return get_wind_speed_from_here_data(self.coordinator.data) - return None + return get_wind_speed_from_here_data(self.coordinator.data) @property def wind_bearing(self) -> float | str | None: """Return the wind bearing.""" - if self.coordinator.data is not None: - return get_wind_bearing_from_here_data(self.coordinator.data) - return None + return get_wind_bearing_from_here_data(self.coordinator.data) @property def attribution(self) -> str | None: @@ -144,21 +131,18 @@ def attribution(self) -> str | None: def visibility(self) -> float | None: """Return the visibility.""" if "visibility" in SENSOR_TYPES[self._mode]: - if self.coordinator.data is not None: - if ( - visibility := get_attribute_from_here_data( - self.coordinator.data, "visibility" - ) - is not None - ): - return float(visibility) + if ( + visibility := get_attribute_from_here_data( + self.coordinator.data, "visibility" + ) + is not None + ): + return float(visibility) return None @property def forecast(self) -> list[Forecast] | None: """Return the forecast array.""" - if self.coordinator.data is None: - return None data: list[Forecast] = [] for offset in range(len(self.coordinator.data)): data.append( diff --git a/tests/components/here_weather/test_weather.py b/tests/components/here_weather/test_weather.py index 5ff700587d9c7a..8e9af5597c13f2 100644 --- a/tests/components/here_weather/test_weather.py +++ b/tests/components/here_weather/test_weather.py @@ -60,7 +60,7 @@ async def test_weather_daily(hass): registry.async_get_or_create( "weather", DOMAIN, - "here_weather_forecast_7days", + "40.79962_-73.970314_forecast_7days", suggested_object_id="here_weather_forecast_7days", disabled_by=None, ) From 388ee89d5a35380fd94dd09d6963302e12f7ea8d Mon Sep 17 00:00:00 2001 From: Kevin Eifinger Date: Fri, 23 Jul 2021 00:42:25 +0200 Subject: [PATCH 44/56] Remove options --- .../components/here_weather/__init__.py | 47 ++++++++----------- .../components/here_weather/config_flow.py | 43 ++--------------- .../components/here_weather/const.py | 9 +++- .../components/here_weather/strings.json | 10 ---- .../components/here_weather/utils.py | 14 ++++++ .../here_weather/test_config_flow.py | 38 +-------------- 6 files changed, 46 insertions(+), 115 deletions(-) diff --git a/homeassistant/components/here_weather/__init__.py b/homeassistant/components/here_weather/__init__.py index f06273655873e2..58a684f5cde242 100644 --- a/homeassistant/components/here_weather/__init__.py +++ b/homeassistant/components/here_weather/__init__.py @@ -7,18 +7,23 @@ import async_timeout import herepy +from homeassistant.components.here_weather.utils import active_here_clients from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, - CONF_SCAN_INTERVAL, CONF_UNIT_SYSTEM_METRIC, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import CONF_MODES, DEFAULT_SCAN_INTERVAL, DOMAIN +from .const import ( + CONF_MODES, + DEFAULT_SCAN_INTERVAL, + DOMAIN, + MAX_UPDATE_RATE_FOR_ONE_CLIENT, +) _LOGGER = logging.getLogger(__name__) @@ -63,32 +68,22 @@ def __init__(self, hass: HomeAssistant, entry: ConfigEntry, mode: str) -> None: async def async_setup(self) -> None: """Set up the here_weather integration.""" - self.add_options() - self.entry.async_on_unload( - self.entry.add_update_listener(self.async_options_updated) - ) self.coordinator = DataUpdateCoordinator( self.hass, _LOGGER, name=DOMAIN, update_method=self.async_update, - update_interval=timedelta(seconds=self.entry.options[CONF_SCAN_INTERVAL]), + update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL), ) await self.coordinator.async_config_entry_first_refresh() - def add_options(self) -> None: - """Add options for here_weather integration.""" - if not self.entry.options: - options = { - CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL, - } - self.hass.config_entries.async_update_entry(self.entry, options=options) - async def async_update(self): """Handle data update with the DataUpdateCoordinator.""" try: async with async_timeout.timeout(10): - return await self.hass.async_add_executor_job(self._get_data) + data = await self.hass.async_add_executor_job(self._get_data) + self._set_update_interval() + return data except herepy.InvalidRequestError as error: raise UpdateFailed( f"Unable to fetch data from HERE: {error.message}" @@ -107,18 +102,16 @@ def _get_data(self): data, self.weather_product_type ) - @staticmethod - async def async_options_updated(hass: HomeAssistant, entry: ConfigEntry) -> None: - """Triggered by config entry options updates.""" - for mode in CONF_MODES: - hass.data[DOMAIN][entry.entry_id][mode].set_update_interval( - entry.options[CONF_SCAN_INTERVAL] + def _set_update_interval(self) -> int: + """Throttle the default update rate based on the number of active clients.""" + if ( + update_interval := ( + active_here_clients(self.hass) * MAX_UPDATE_RATE_FOR_ONE_CLIENT * 2 ) - - def set_update_interval(self, update_interval: int) -> None: - """Set the coordinator update_interval to the supplied update_interval.""" - if self.coordinator is not None: - self.coordinator.update_interval = timedelta(seconds=update_interval) + ) > DEFAULT_SCAN_INTERVAL: + _LOGGER.debug("Setting update_interval to %s", update_interval) + return update_interval + return DEFAULT_SCAN_INTERVAL def extract_data_from_payload_for_product_type( diff --git a/homeassistant/components/here_weather/config_flow.py b/homeassistant/components/here_weather/config_flow.py index 5e94dadb343443..2208f5231a8acf 100644 --- a/homeassistant/components/here_weather/config_flow.py +++ b/homeassistant/components/here_weather/config_flow.py @@ -5,18 +5,11 @@ import voluptuous as vol from homeassistant import config_entries -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - CONF_API_KEY, - CONF_LATITUDE, - CONF_LONGITUDE, - CONF_NAME, - CONF_SCAN_INTERVAL, -) -from homeassistant.core import HomeAssistant, callback +from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv -from .const import DEFAULT_MODE, DEFAULT_SCAN_INTERVAL, DOMAIN +from .const import DEFAULT_MODE, DOMAIN async def async_validate_user_input(hass: HomeAssistant, user_input: dict) -> None: @@ -35,12 +28,6 @@ class HereWeatherConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - @staticmethod - @callback - def async_get_options_flow(config_entry: ConfigEntry): - """Get the options flow for this handler.""" - return HereWeatherOptionsFlowHandler(config_entry) - async def async_step_user(self, user_input=None): """Handle the initial step.""" errors = {} @@ -95,27 +82,3 @@ def _get_schema(self, user_input: dict | None) -> vol.Schema: def _unique_id(user_input: dict) -> str: return f"{user_input[CONF_LATITUDE]}_{user_input[CONF_LONGITUDE]}" - - -class HereWeatherOptionsFlowHandler(config_entries.OptionsFlow): - """Handle here_weather options.""" - - def __init__(self, config_entry): - """Initialize here_weather options flow.""" - self.config_entry = config_entry - - async def async_step_init(self, user_input=None): - """Manage the here_weather options.""" - if user_input is not None: - return self.async_create_entry(title="", data=user_input) - - options = { - vol.Optional( - CONF_SCAN_INTERVAL, - default=self.config_entry.options.get( - CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL - ), - ): int, - } - - return self.async_show_form(step_id="init", data_schema=vol.Schema(options)) diff --git a/homeassistant/components/here_weather/const.py b/homeassistant/components/here_weather/const.py index decae962b9b1a7..01cc8cb94a04cf 100644 --- a/homeassistant/components/here_weather/const.py +++ b/homeassistant/components/here_weather/const.py @@ -1,6 +1,8 @@ """Constants for the HERE Destination Weather service.""" from __future__ import annotations +import math + from homeassistant.const import ( DEGREE, DEVICE_CLASS_HUMIDITY, @@ -17,7 +19,12 @@ DOMAIN = "here_weather" -DEFAULT_SCAN_INTERVAL = 120 +DEFAULT_SCAN_INTERVAL = 300 + +FREEMIUM_REQUESTS_PER_MONTH = 250000 +MAX_UPDATE_RATE_FOR_ONE_CLIENT = math.ceil( + (31 * 24 * 3600) / FREEMIUM_REQUESTS_PER_MONTH +) CONF_LANGUAGE = "language" CONF_OFFSET = "offset" diff --git a/homeassistant/components/here_weather/strings.json b/homeassistant/components/here_weather/strings.json index 1765f2984bf019..e1b7f1b5613ccf 100644 --- a/homeassistant/components/here_weather/strings.json +++ b/homeassistant/components/here_weather/strings.json @@ -15,15 +15,5 @@ "invalid_request": "HERE reported an invalid request. This indicates the supplied location is not valid.", "unauthorized": "Invalid credentials. This error is returned if the specified token was invalid or no contract could be found for this token." } - }, - "options": { - "step": { - "init": { - "description": "Configure options for HERE Destination Weather", - "data": { - "scan_interval": "Update interval." - } - } - } } } \ No newline at end of file diff --git a/homeassistant/components/here_weather/utils.py b/homeassistant/components/here_weather/utils.py index 13dc85ebb73934..83b9d395146065 100644 --- a/homeassistant/components/here_weather/utils.py +++ b/homeassistant/components/here_weather/utils.py @@ -14,6 +14,9 @@ TEMP_CELSIUS, TEMP_FAHRENHEIT, ) +from homeassistant.core import HomeAssistant + +from .const import DOMAIN def convert_temperature_unit_of_measurement_if_needed( @@ -60,3 +63,14 @@ def convert_asterisk_to_none(state: str) -> str | None: if state == "*": return None return state + + +def active_here_clients(hass: HomeAssistant) -> int: + """Return the number of active herepy clients.""" + active_coordinators = 0 + if config_entries := hass.data.get(DOMAIN): + for here_weather_data_dicts in config_entries.values(): + for here_weather_data in here_weather_data_dicts.values(): + if len(here_weather_data.coordinator._listeners) > 0: + active_coordinators += 1 + return active_coordinators diff --git a/tests/components/here_weather/test_config_flow.py b/tests/components/here_weather/test_config_flow.py index 007f47217e0170..11315ba9085aea 100644 --- a/tests/components/here_weather/test_config_flow.py +++ b/tests/components/here_weather/test_config_flow.py @@ -5,17 +5,9 @@ from homeassistant import config_entries, setup from homeassistant.components.here_weather.const import DOMAIN -from homeassistant.const import ( - CONF_API_KEY, - CONF_LATITUDE, - CONF_LONGITUDE, - CONF_NAME, - CONF_SCAN_INTERVAL, -) +from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.core import HomeAssistant -from . import mock_weather_for_coordinates - from tests.common import MockConfigEntry @@ -111,31 +103,3 @@ async def test_invalid_request(hass): ) assert result["type"] == "form" assert result["errors"]["base"] == "invalid_request" - - -async def test_options(hass): - """Test the options flow.""" - with patch( - "herepy.DestinationWeatherApi.weather_for_coordinates", - side_effect=mock_weather_for_coordinates, - ): - entry = MockConfigEntry( - domain=DOMAIN, - data={ - CONF_API_KEY: "test", - CONF_NAME: DOMAIN, - CONF_LATITUDE: "40.79962", - CONF_LONGITUDE: "-73.970314", - }, - ) - entry.add_to_hass(hass) - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - result = await hass.config_entries.options.async_init(entry.entry_id) - result = await hass.config_entries.options.async_configure( - result["flow_id"], - {CONF_SCAN_INTERVAL: 10}, - ) - await hass.async_block_till_done() - assert result["type"] == "create_entry" - assert result["data"][CONF_SCAN_INTERVAL] == 10 From a1ae5ba8583500a81a56ada8a50e60981be5ce68 Mon Sep 17 00:00:00 2001 From: Kevin Eifinger Date: Fri, 23 Jul 2021 00:43:43 +0200 Subject: [PATCH 45/56] Fix unique_id and state --- .../components/here_weather/manifest.json | 2 +- homeassistant/components/here_weather/sensor.py | 7 +++++-- homeassistant/components/here_weather/weather.py | 15 ++++++--------- requirements_all.txt | 4 +++- requirements_test_all.txt | 4 +++- 5 files changed, 18 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/here_weather/manifest.json b/homeassistant/components/here_weather/manifest.json index 5569254e4e75d1..690faf62c630da 100644 --- a/homeassistant/components/here_weather/manifest.json +++ b/homeassistant/components/here_weather/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/here_weather", "requirements": [ - "herepy==3.0.2" + "herepy==3.5.3" ], "codeowners": [ "@eifinger" diff --git a/homeassistant/components/here_weather/sensor.py b/homeassistant/components/here_weather/sensor.py index 68271faf6ea989..8536aae51ee37a 100644 --- a/homeassistant/components/here_weather/sensor.py +++ b/homeassistant/components/here_weather/sensor.py @@ -66,6 +66,9 @@ def __init__( self._unique_id = "".join( f"{self._latitude}_{self._longitude}_{self._sensor_type}_{self._name_suffix}_{self._sensor_number}".lower().split() ) + self._unique_device_id = "".join( + f"{self._latitude}_{self._longitude}_{self._sensor_type}".lower().split() + ) @property def entity_registry_enabled_default(self): @@ -101,8 +104,8 @@ def device_info(self) -> DeviceInfo: """Return a device description for device registry.""" return { - "identifiers": {(DOMAIN, self._unique_id)}, - "name": self._base_name, + "identifiers": {(DOMAIN, self._unique_device_id)}, + "name": f"{self._base_name} {self._sensor_type}", "manufacturer": "here.com", "entry_type": "service", } diff --git a/homeassistant/components/here_weather/weather.py b/homeassistant/components/here_weather/weather.py index ec5e2f8b080fda..870775c3b61878 100644 --- a/homeassistant/components/here_weather/weather.py +++ b/homeassistant/components/here_weather/weather.py @@ -107,8 +107,7 @@ def humidity(self) -> float | None: """Return the humidity.""" if ( humidity := get_attribute_from_here_data(self.coordinator.data, "humidity") - is not None - ): + ) is not None: return float(humidity) return None @@ -135,8 +134,7 @@ def visibility(self) -> float | None: visibility := get_attribute_from_here_data( self.coordinator.data, "visibility" ) - is not None - ): + ) is not None: return float(visibility) return None @@ -188,8 +186,9 @@ def device_info(self) -> DeviceInfo: """Return a device description for device registry.""" return { "identifiers": {(DOMAIN, self._unique_id)}, - "name": self._name, + "name": self.name, "manufacturer": "here.com", + "entry_type": "service", } @@ -223,8 +222,7 @@ def get_pressure_from_here_data( pressure := get_attribute_from_here_data( here_data, "barometerPressure", offset ) - is not None - ): + ) is not None: return float(pressure) return None @@ -238,8 +236,7 @@ def get_precipitation_probability( precipitation_probability := get_attribute_from_here_data( here_data, "precipitationProbability", offset ) - is not None - ): + ) is not None: return int(precipitation_probability) return None diff --git a/requirements_all.txt b/requirements_all.txt index 8da52882ab92c8..d222fe1b3c01df 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -766,9 +766,11 @@ hdate==0.10.2 heatmiserV3==1.1.18 # homeassistant.components.here_travel_time -# homeassistant.components.here_weather herepy==3.0.2 +# homeassistant.components.here_weather +herepy==3.5.3 + # homeassistant.components.hikvisioncam hikvision==0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 05b88a7842d71f..5bae176f5062d4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -442,9 +442,11 @@ hatasmota==0.2.20 hdate==0.10.2 # homeassistant.components.here_travel_time -# homeassistant.components.here_weather herepy==3.0.2 +# homeassistant.components.here_weather +herepy==3.5.3 + # homeassistant.components.hlk_sw16 hlk-sw16==0.0.9 From c95d5e4f5c3d8ebc24eb58b5ad2e7d0f14682372 Mon Sep 17 00:00:00 2001 From: Kevin Eifinger Date: Fri, 23 Jul 2021 00:52:32 +0200 Subject: [PATCH 46/56] herepy 3.5.3 for here_travel_time --- homeassistant/components/here_travel_time/manifest.json | 2 +- requirements_all.txt | 2 -- requirements_test_all.txt | 2 -- 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/homeassistant/components/here_travel_time/manifest.json b/homeassistant/components/here_travel_time/manifest.json index b5505f6d27bada..3d29299a95bacc 100644 --- a/homeassistant/components/here_travel_time/manifest.json +++ b/homeassistant/components/here_travel_time/manifest.json @@ -2,7 +2,7 @@ "domain": "here_travel_time", "name": "HERE Travel Time", "documentation": "https://www.home-assistant.io/integrations/here_travel_time", - "requirements": ["herepy==3.0.2"], + "requirements": ["herepy==3.5.3"], "codeowners": ["@eifinger"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index d222fe1b3c01df..f6b8452e0f3a1a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -766,8 +766,6 @@ hdate==0.10.2 heatmiserV3==1.1.18 # homeassistant.components.here_travel_time -herepy==3.0.2 - # homeassistant.components.here_weather herepy==3.5.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5bae176f5062d4..c6ab3cebfc1a3f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -442,8 +442,6 @@ hatasmota==0.2.20 hdate==0.10.2 # homeassistant.components.here_travel_time -herepy==3.0.2 - # homeassistant.components.here_weather herepy==3.5.3 From 9a9f9feef13609625e23ac61f3a76a8b8e3722d6 Mon Sep 17 00:00:00 2001 From: Kevin Eifinger Date: Fri, 23 Jul 2021 00:56:58 +0200 Subject: [PATCH 47/56] Add test for observation --- tests/components/here_weather/test_weather.py | 39 ++++++++++++++++++- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/tests/components/here_weather/test_weather.py b/tests/components/here_weather/test_weather.py index 8e9af5597c13f2..79b8cd01b795d5 100644 --- a/tests/components/here_weather/test_weather.py +++ b/tests/components/here_weather/test_weather.py @@ -37,12 +37,11 @@ async def test_weather(hass): async def test_weather_daily(hass): - """Test that weather has a value.""" + """Test that weather has a value for mode daily.""" with patch( "herepy.DestinationWeatherApi.weather_for_coordinates", side_effect=mock_weather_for_coordinates, ): - hass.config.units = IMPERIAL_SYSTEM entry = MockConfigEntry( domain=DOMAIN, data={ @@ -71,3 +70,39 @@ async def test_weather_daily(hass): sensor = hass.states.get("weather.here_weather_forecast_7days") assert sensor.state == "cloudy" + + +async def test_weather_observation(hass): + """Test that weather has a value for mode observation.""" + with patch( + "herepy.DestinationWeatherApi.weather_for_coordinates", + side_effect=mock_weather_for_coordinates, + ): + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_API_KEY: "test", + CONF_NAME: DOMAIN, + CONF_LATITUDE: "40.79962", + CONF_LONGITUDE: "-73.970314", + }, + ) + entry.add_to_hass(hass) + + registry = await hass.helpers.entity_registry.async_get_registry() + + # Pre-create registry entries for disabled by default sensors + registry.async_get_or_create( + "weather", + DOMAIN, + "40.79962_-73.970314_observation", + suggested_object_id="here_weather_observation", + disabled_by=None, + ) + + await hass.config_entries.async_setup(entry.entry_id) + + await hass.async_block_till_done() + + sensor = hass.states.get("weather.here_weather_observation") + assert sensor.state == "cloudy" From ac010a6f8648ab45cc10e0c8ace2dd599323ffcc Mon Sep 17 00:00:00 2001 From: Kevin Eifinger Date: Fri, 23 Jul 2021 10:30:46 +0200 Subject: [PATCH 48/56] Simplify try block --- homeassistant/components/here_weather/config_flow.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/here_weather/config_flow.py b/homeassistant/components/here_weather/config_flow.py index 2208f5231a8acf..73e5d39d2539ae 100644 --- a/homeassistant/components/here_weather/config_flow.py +++ b/homeassistant/components/here_weather/config_flow.py @@ -32,17 +32,18 @@ async def async_step_user(self, user_input=None): """Handle the initial step.""" errors = {} if user_input is not None: + await self.async_set_unique_id(_unique_id(user_input)) + self._abort_if_unique_id_configured() try: - await self.async_set_unique_id(_unique_id(user_input)) - self._abort_if_unique_id_configured() await async_validate_user_input(self.hass, user_input) - return self.async_create_entry( - title=user_input[CONF_NAME], data=user_input - ) except herepy.InvalidRequestError: errors["base"] = "invalid_request" except herepy.UnauthorizedError: errors["base"] = "unauthorized" + else: + return self.async_create_entry( + title=user_input[CONF_NAME], data=user_input + ) return self.async_show_form( step_id="user", data_schema=self._get_schema(user_input), From e1b7683866b634cf21c81d55115f2f650a057c48 Mon Sep 17 00:00:00 2001 From: Kevin Eifinger Date: Fri, 23 Jul 2021 10:31:36 +0200 Subject: [PATCH 49/56] Use PERCENTAGE --- homeassistant/components/here_weather/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/here_weather/const.py b/homeassistant/components/here_weather/const.py index 01cc8cb94a04cf..df7acc3f654c0a 100644 --- a/homeassistant/components/here_weather/const.py +++ b/homeassistant/components/here_weather/const.py @@ -51,7 +51,7 @@ "moonset": {"name": "Moonset", "unit_of_measurement": None, "device_class": None}, "moonPhase": { "name": "Moon Phase", - "unit_of_measurement": "%", + "unit_of_measurement": PERCENTAGE, "device_class": None, }, "moonPhaseDesc": { From 32ed66dbc1799dcd432dfc3bf7e572155d38048a Mon Sep 17 00:00:00 2001 From: Kevin Eifinger Date: Fri, 23 Jul 2021 10:33:04 +0200 Subject: [PATCH 50/56] Use relative import --- homeassistant/components/here_weather/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/here_weather/__init__.py b/homeassistant/components/here_weather/__init__.py index 58a684f5cde242..2830eab3e3b0c8 100644 --- a/homeassistant/components/here_weather/__init__.py +++ b/homeassistant/components/here_weather/__init__.py @@ -7,7 +7,6 @@ import async_timeout import herepy -from homeassistant.components.here_weather.utils import active_here_clients from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_API_KEY, @@ -24,6 +23,7 @@ DOMAIN, MAX_UPDATE_RATE_FOR_ONE_CLIENT, ) +from .utils import active_here_clients _LOGGER = logging.getLogger(__name__) From c4a62e0bad6f5a8541455614326fb64b616981f4 Mon Sep 17 00:00:00 2001 From: Kevin Eifinger Date: Fri, 23 Jul 2021 11:44:05 +0200 Subject: [PATCH 51/56] Use custom UpdateCoordinator --- .../components/here_weather/__init__.py | 31 ++++---- .../components/here_weather/sensor.py | 4 +- .../components/here_weather/utils.py | 6 +- .../components/here_weather/weather.py | 4 +- tests/components/here_weather/test_init.py | 76 ++++++++++++++++++- 5 files changed, 94 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/here_weather/__init__.py b/homeassistant/components/here_weather/__init__.py index 2830eab3e3b0c8..658ff64b00a42c 100644 --- a/homeassistant/components/here_weather/__init__.py +++ b/homeassistant/components/here_weather/__init__.py @@ -32,12 +32,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up here_weather from a config entry.""" - here_weather_data_dict = {} + here_weather_coordinators = {} for mode in CONF_MODES: - here_weather_data = HEREWeatherData(hass, entry, mode) - await here_weather_data.async_setup() - here_weather_data_dict[mode] = here_weather_data - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = here_weather_data_dict + coordinator = HEREWeatherDataUpdateCoordinator(hass, entry, mode) + await coordinator.async_config_entry_first_refresh() + here_weather_coordinators[mode] = coordinator + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = here_weather_coordinators hass.config_entries.async_setup_platforms(entry, PLATFORMS) @@ -53,32 +53,29 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload_ok -class HEREWeatherData: +class HEREWeatherDataUpdateCoordinator(DataUpdateCoordinator): """Get the latest data from HERE.""" def __init__(self, hass: HomeAssistant, entry: ConfigEntry, mode: str) -> None: """Initialize the data object.""" - self.hass = hass - self.entry = entry self.here_client = herepy.DestinationWeatherApi(entry.data[CONF_API_KEY]) self.latitude = entry.data[CONF_LATITUDE] self.longitude = entry.data[CONF_LONGITUDE] self.weather_product_type = herepy.WeatherProductType[mode] - self.coordinator: DataUpdateCoordinator | None = None - async def async_setup(self) -> None: - """Set up the here_weather integration.""" - self.coordinator = DataUpdateCoordinator( - self.hass, + super().__init__( + hass, _LOGGER, name=DOMAIN, - update_method=self.async_update, update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL), ) - await self.coordinator.async_config_entry_first_refresh() - async def async_update(self): - """Handle data update with the DataUpdateCoordinator.""" + def number_of_listeners(self) -> int: + """Return the number ob active listeners registered against this coordinator.""" + return len(self._listeners) + + async def _async_update_data(self) -> list: + """Perform data update.""" try: async with async_timeout.timeout(10): data = await self.hass.async_add_executor_job(self._get_data) diff --git a/homeassistant/components/here_weather/sensor.py b/homeassistant/components/here_weather/sensor.py index 8536aae51ee37a..62bbf2413dd465 100644 --- a/homeassistant/components/here_weather/sensor.py +++ b/homeassistant/components/here_weather/sensor.py @@ -20,7 +20,7 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities ): """Add here_weather entities from a ConfigEntry.""" - here_weather_data_dict = hass.data[DOMAIN][entry.entry_id] + here_weather_coordinators = hass.data[DOMAIN][entry.entry_id] sensors_to_add = [] for sensor_type, weather_attributes in SENSOR_TYPES.items(): @@ -28,7 +28,7 @@ async def async_setup_entry( sensors_to_add.append( HEREDestinationWeatherSensor( entry, - here_weather_data_dict[sensor_type].coordinator, + here_weather_coordinators[sensor_type], sensor_type, weather_attribute, ) diff --git a/homeassistant/components/here_weather/utils.py b/homeassistant/components/here_weather/utils.py index 83b9d395146065..e8f6b1175ecb87 100644 --- a/homeassistant/components/here_weather/utils.py +++ b/homeassistant/components/here_weather/utils.py @@ -69,8 +69,8 @@ def active_here_clients(hass: HomeAssistant) -> int: """Return the number of active herepy clients.""" active_coordinators = 0 if config_entries := hass.data.get(DOMAIN): - for here_weather_data_dicts in config_entries.values(): - for here_weather_data in here_weather_data_dicts.values(): - if len(here_weather_data.coordinator._listeners) > 0: + for here_weather_coordinators in config_entries.values(): + for coordinator in here_weather_coordinators.values(): + if coordinator.number_of_listeners() > 0: active_coordinators += 1 return active_coordinators diff --git a/homeassistant/components/here_weather/weather.py b/homeassistant/components/here_weather/weather.py index 870775c3b61878..524d7c3e8c5901 100644 --- a/homeassistant/components/here_weather/weather.py +++ b/homeassistant/components/here_weather/weather.py @@ -41,7 +41,7 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities ): """Add here_weather entities from a ConfigEntry.""" - here_weather_data_dict = hass.data[DOMAIN][entry.entry_id] + here_weather_coordinators = hass.data[DOMAIN][entry.entry_id] entities_to_add = [] for sensor_type in SENSOR_TYPES: @@ -49,7 +49,7 @@ async def async_setup_entry( entities_to_add.append( HEREDestinationWeather( entry, - here_weather_data_dict[sensor_type].coordinator, + here_weather_coordinators[sensor_type], sensor_type, ) ) diff --git a/tests/components/here_weather/test_init.py b/tests/components/here_weather/test_init.py index 2fd27a6c778342..1922e00d189710 100644 --- a/tests/components/here_weather/test_init.py +++ b/tests/components/here_weather/test_init.py @@ -1,13 +1,83 @@ """Tests for the here_weather integration.""" - +from datetime import timedelta from unittest.mock import patch -from homeassistant.components.here_weather.const import DOMAIN +from homeassistant.components.here_weather.const import DEFAULT_SCAN_INTERVAL, DOMAIN from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME +import homeassistant.util.dt as dt_util from . import mock_weather_for_coordinates -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed + + +async def test_number_of_listeners(hass): + """Test that number_of_listerners works.""" + utcnow = dt_util.utcnow() + # Patching 'utcnow' to gain more control over the timed update. + with patch("homeassistant.util.dt.utcnow", return_value=utcnow): + with patch( + "herepy.DestinationWeatherApi.weather_for_coordinates", + side_effect=mock_weather_for_coordinates, + ): + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_API_KEY: "test", + CONF_NAME: DOMAIN, + CONF_LATITUDE: "40.79962", + CONF_LONGITUDE: "-73.970314", + }, + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + + await hass.async_block_till_done() + + sensor = hass.states.get("weather.here_weather_forecast_7days_simple") + assert sensor.state == "cloudy" + async_fire_time_changed(hass, utcnow + timedelta(DEFAULT_SCAN_INTERVAL * 2)) + await hass.async_block_till_done() + sensor = hass.states.get("weather.here_weather_forecast_7days_simple") + assert sensor.state == "cloudy" + + +async def test_update_interval(hass): + """Test that update_interval is correctly set.""" + utcnow = dt_util.utcnow() + # Patching 'utcnow' to gain more control over the timed update. + with patch("homeassistant.util.dt.utcnow", return_value=utcnow): + with patch( + "herepy.DestinationWeatherApi.weather_for_coordinates", + side_effect=mock_weather_for_coordinates, + ): + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_API_KEY: "test", + CONF_NAME: DOMAIN, + CONF_LATITUDE: "40.79962", + CONF_LONGITUDE: "-73.970314", + }, + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + + await hass.async_block_till_done() + + sensor = hass.states.get("weather.here_weather_forecast_7days_simple") + assert sensor.state == "cloudy" + with patch( + "homeassistant.components.here_weather.active_here_clients", + return_value=1000, + ) as mock_active_here_clients: + async_fire_time_changed( + hass, utcnow + timedelta(DEFAULT_SCAN_INTERVAL * 2) + ) + await hass.async_block_till_done() + assert len(mock_active_here_clients.mock_calls) == 1 async def test_unload_entry(hass): From c8e5b2b9970e6284ff44f224c0ae356c4154360c Mon Sep 17 00:00:00 2001 From: Kevin Eifinger Date: Fri, 23 Jul 2021 13:29:46 +0200 Subject: [PATCH 52/56] Fix known_api_keys for configflow --- homeassistant/components/here_weather/config_flow.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/here_weather/config_flow.py b/homeassistant/components/here_weather/config_flow.py index 73e5d39d2539ae..389ef4f97acb41 100644 --- a/homeassistant/components/here_weather/config_flow.py +++ b/homeassistant/components/here_weather/config_flow.py @@ -51,13 +51,15 @@ async def async_step_user(self, user_input=None): ) def _get_schema(self, user_input: dict | None) -> vol.Schema: - known_api_keys = [ + known_api_keys = { entry.data[CONF_API_KEY] for entry in self._async_current_entries() - ] + } if user_input is not None: return vol.Schema( { - vol.Required(CONF_API_KEY, default=user_input[CONF_API_KEY]): str, + vol.Required( + CONF_API_KEY, default=user_input[CONF_API_KEY] + ): vol.In(known_api_keys), vol.Required(CONF_NAME, default=user_input[CONF_NAME]): str, vol.Required( CONF_LATITUDE, default=user_input[CONF_LATITUDE] @@ -69,7 +71,7 @@ def _get_schema(self, user_input: dict | None) -> vol.Schema: ) return vol.Schema( { - vol.Required(CONF_API_KEY, default=known_api_keys): str, + vol.Required(CONF_API_KEY): vol.In(known_api_keys), vol.Required(CONF_NAME, default=DOMAIN): str, vol.Required( CONF_LATITUDE, default=self.hass.config.latitude From b89c1100b3e1d33180eda91ad8e8abf199f09bc1 Mon Sep 17 00:00:00 2001 From: Kevin Eifinger Date: Fri, 23 Jul 2021 13:30:51 +0200 Subject: [PATCH 53/56] Remove unused constants --- homeassistant/components/here_weather/const.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/homeassistant/components/here_weather/const.py b/homeassistant/components/here_weather/const.py index df7acc3f654c0a..4fb2c37deb0b1d 100644 --- a/homeassistant/components/here_weather/const.py +++ b/homeassistant/components/here_weather/const.py @@ -26,10 +26,6 @@ (31 * 24 * 3600) / FREEMIUM_REQUESTS_PER_MONTH ) -CONF_LANGUAGE = "language" -CONF_OFFSET = "offset" -CONF_OPTION = "option" - MODE_ASTRONOMY = "forecast_astronomy" MODE_HOURLY = "forecast_hourly" MODE_DAILY = "forecast_7days" From 8f53aa92dd0f4d457d2f41ffbd18849278ddbd1b Mon Sep 17 00:00:00 2001 From: Kevin Eifinger Date: Fri, 23 Jul 2021 13:32:27 +0200 Subject: [PATCH 54/56] fix typo --- homeassistant/components/here_weather/const.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/here_weather/const.py b/homeassistant/components/here_weather/const.py index 4fb2c37deb0b1d..b376943e709301 100644 --- a/homeassistant/components/here_weather/const.py +++ b/homeassistant/components/here_weather/const.py @@ -150,7 +150,7 @@ }, } -NON_OBERSATION_ATTRIBUTES: dict[str, dict[str, str | None]] = { +NON_OBSERVATION_ATTRIBUTES: dict[str, dict[str, str | None]] = { "rainFall": { "name": "Rain Fall", "unit_of_measurement": LENGTH_CENTIMETERS, @@ -192,7 +192,7 @@ "unit_of_measurement": LENGTH_KILOMETERS, "device_class": None, }, - **NON_OBERSATION_ATTRIBUTES, + **NON_OBSERVATION_ATTRIBUTES, } DAILY_SIMPLE_ATTRIBUTES: dict[str, dict[str, str | None]] = { @@ -227,7 +227,7 @@ "unit_of_measurement": PRESSURE_MBAR, "device_class": DEVICE_CLASS_PRESSURE, }, - **NON_OBERSATION_ATTRIBUTES, + **NON_OBSERVATION_ATTRIBUTES, } DAILY_ATTRIBUTES: dict[str, dict[str, str | None]] = { @@ -256,7 +256,7 @@ "unit_of_measurement": LENGTH_KILOMETERS, "device_class": None, }, - **NON_OBERSATION_ATTRIBUTES, + **NON_OBSERVATION_ATTRIBUTES, } OBSERVATION_ATTRIBUTES: dict[str, dict[str, str | None]] = { From 6fe874905a9e445a83a7a41e4358662b91907ae3 Mon Sep 17 00:00:00 2001 From: Kevin Eifinger Date: Fri, 23 Jul 2021 13:38:16 +0200 Subject: [PATCH 55/56] Remove known_api_key feature --- homeassistant/components/here_weather/config_flow.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/here_weather/config_flow.py b/homeassistant/components/here_weather/config_flow.py index 389ef4f97acb41..d07cbd5f076478 100644 --- a/homeassistant/components/here_weather/config_flow.py +++ b/homeassistant/components/here_weather/config_flow.py @@ -51,15 +51,10 @@ async def async_step_user(self, user_input=None): ) def _get_schema(self, user_input: dict | None) -> vol.Schema: - known_api_keys = { - entry.data[CONF_API_KEY] for entry in self._async_current_entries() - } if user_input is not None: return vol.Schema( { - vol.Required( - CONF_API_KEY, default=user_input[CONF_API_KEY] - ): vol.In(known_api_keys), + vol.Required(CONF_API_KEY, default=user_input[CONF_API_KEY]): str, vol.Required(CONF_NAME, default=user_input[CONF_NAME]): str, vol.Required( CONF_LATITUDE, default=user_input[CONF_LATITUDE] @@ -71,7 +66,7 @@ def _get_schema(self, user_input: dict | None) -> vol.Schema: ) return vol.Schema( { - vol.Required(CONF_API_KEY): vol.In(known_api_keys), + vol.Required(CONF_API_KEY): str, vol.Required(CONF_NAME, default=DOMAIN): str, vol.Required( CONF_LATITUDE, default=self.hass.config.latitude From 1720b37b715d79fbb70b4a4ae9c033a14581c7c4 Mon Sep 17 00:00:00 2001 From: Kevin Eifinger Date: Fri, 23 Jul 2021 14:58:55 +0200 Subject: [PATCH 56/56] Remove update_rate throttling --- .../components/here_weather/__init__.py | 24 +----- .../components/here_weather/const.py | 9 +-- .../components/here_weather/utils.py | 14 ---- tests/components/here_weather/test_init.py | 75 +------------------ 4 files changed, 4 insertions(+), 118 deletions(-) diff --git a/homeassistant/components/here_weather/__init__.py b/homeassistant/components/here_weather/__init__.py index 658ff64b00a42c..cc832f83006c2f 100644 --- a/homeassistant/components/here_weather/__init__.py +++ b/homeassistant/components/here_weather/__init__.py @@ -17,13 +17,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import ( - CONF_MODES, - DEFAULT_SCAN_INTERVAL, - DOMAIN, - MAX_UPDATE_RATE_FOR_ONE_CLIENT, -) -from .utils import active_here_clients +from .const import CONF_MODES, DEFAULT_SCAN_INTERVAL, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -70,16 +64,11 @@ def __init__(self, hass: HomeAssistant, entry: ConfigEntry, mode: str) -> None: update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL), ) - def number_of_listeners(self) -> int: - """Return the number ob active listeners registered against this coordinator.""" - return len(self._listeners) - async def _async_update_data(self) -> list: """Perform data update.""" try: async with async_timeout.timeout(10): data = await self.hass.async_add_executor_job(self._get_data) - self._set_update_interval() return data except herepy.InvalidRequestError as error: raise UpdateFailed( @@ -99,17 +88,6 @@ def _get_data(self): data, self.weather_product_type ) - def _set_update_interval(self) -> int: - """Throttle the default update rate based on the number of active clients.""" - if ( - update_interval := ( - active_here_clients(self.hass) * MAX_UPDATE_RATE_FOR_ONE_CLIENT * 2 - ) - ) > DEFAULT_SCAN_INTERVAL: - _LOGGER.debug("Setting update_interval to %s", update_interval) - return update_interval - return DEFAULT_SCAN_INTERVAL - def extract_data_from_payload_for_product_type( data: herepy.DestinationWeatherResponse, product_type: herepy.WeatherProductType diff --git a/homeassistant/components/here_weather/const.py b/homeassistant/components/here_weather/const.py index b376943e709301..9f9684dd233d14 100644 --- a/homeassistant/components/here_weather/const.py +++ b/homeassistant/components/here_weather/const.py @@ -1,8 +1,6 @@ """Constants for the HERE Destination Weather service.""" from __future__ import annotations -import math - from homeassistant.const import ( DEGREE, DEVICE_CLASS_HUMIDITY, @@ -19,12 +17,7 @@ DOMAIN = "here_weather" -DEFAULT_SCAN_INTERVAL = 300 - -FREEMIUM_REQUESTS_PER_MONTH = 250000 -MAX_UPDATE_RATE_FOR_ONE_CLIENT = math.ceil( - (31 * 24 * 3600) / FREEMIUM_REQUESTS_PER_MONTH -) +DEFAULT_SCAN_INTERVAL = 1800 # 30 minutes MODE_ASTRONOMY = "forecast_astronomy" MODE_HOURLY = "forecast_hourly" diff --git a/homeassistant/components/here_weather/utils.py b/homeassistant/components/here_weather/utils.py index e8f6b1175ecb87..13dc85ebb73934 100644 --- a/homeassistant/components/here_weather/utils.py +++ b/homeassistant/components/here_weather/utils.py @@ -14,9 +14,6 @@ TEMP_CELSIUS, TEMP_FAHRENHEIT, ) -from homeassistant.core import HomeAssistant - -from .const import DOMAIN def convert_temperature_unit_of_measurement_if_needed( @@ -63,14 +60,3 @@ def convert_asterisk_to_none(state: str) -> str | None: if state == "*": return None return state - - -def active_here_clients(hass: HomeAssistant) -> int: - """Return the number of active herepy clients.""" - active_coordinators = 0 - if config_entries := hass.data.get(DOMAIN): - for here_weather_coordinators in config_entries.values(): - for coordinator in here_weather_coordinators.values(): - if coordinator.number_of_listeners() > 0: - active_coordinators += 1 - return active_coordinators diff --git a/tests/components/here_weather/test_init.py b/tests/components/here_weather/test_init.py index 1922e00d189710..213255fc5727f2 100644 --- a/tests/components/here_weather/test_init.py +++ b/tests/components/here_weather/test_init.py @@ -1,83 +1,12 @@ """Tests for the here_weather integration.""" -from datetime import timedelta from unittest.mock import patch -from homeassistant.components.here_weather.const import DEFAULT_SCAN_INTERVAL, DOMAIN +from homeassistant.components.here_weather.const import DOMAIN from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME -import homeassistant.util.dt as dt_util from . import mock_weather_for_coordinates -from tests.common import MockConfigEntry, async_fire_time_changed - - -async def test_number_of_listeners(hass): - """Test that number_of_listerners works.""" - utcnow = dt_util.utcnow() - # Patching 'utcnow' to gain more control over the timed update. - with patch("homeassistant.util.dt.utcnow", return_value=utcnow): - with patch( - "herepy.DestinationWeatherApi.weather_for_coordinates", - side_effect=mock_weather_for_coordinates, - ): - entry = MockConfigEntry( - domain=DOMAIN, - data={ - CONF_API_KEY: "test", - CONF_NAME: DOMAIN, - CONF_LATITUDE: "40.79962", - CONF_LONGITUDE: "-73.970314", - }, - ) - entry.add_to_hass(hass) - - await hass.config_entries.async_setup(entry.entry_id) - - await hass.async_block_till_done() - - sensor = hass.states.get("weather.here_weather_forecast_7days_simple") - assert sensor.state == "cloudy" - async_fire_time_changed(hass, utcnow + timedelta(DEFAULT_SCAN_INTERVAL * 2)) - await hass.async_block_till_done() - sensor = hass.states.get("weather.here_weather_forecast_7days_simple") - assert sensor.state == "cloudy" - - -async def test_update_interval(hass): - """Test that update_interval is correctly set.""" - utcnow = dt_util.utcnow() - # Patching 'utcnow' to gain more control over the timed update. - with patch("homeassistant.util.dt.utcnow", return_value=utcnow): - with patch( - "herepy.DestinationWeatherApi.weather_for_coordinates", - side_effect=mock_weather_for_coordinates, - ): - entry = MockConfigEntry( - domain=DOMAIN, - data={ - CONF_API_KEY: "test", - CONF_NAME: DOMAIN, - CONF_LATITUDE: "40.79962", - CONF_LONGITUDE: "-73.970314", - }, - ) - entry.add_to_hass(hass) - - await hass.config_entries.async_setup(entry.entry_id) - - await hass.async_block_till_done() - - sensor = hass.states.get("weather.here_weather_forecast_7days_simple") - assert sensor.state == "cloudy" - with patch( - "homeassistant.components.here_weather.active_here_clients", - return_value=1000, - ) as mock_active_here_clients: - async_fire_time_changed( - hass, utcnow + timedelta(DEFAULT_SCAN_INTERVAL * 2) - ) - await hass.async_block_till_done() - assert len(mock_active_here_clients.mock_calls) == 1 +from tests.common import MockConfigEntry async def test_unload_entry(hass):