From 1ba44356936e9a3cda93af753a682eac9ecb2061 Mon Sep 17 00:00:00 2001 From: Brian Cribbs Date: Mon, 1 May 2017 13:12:43 -0400 Subject: [PATCH 1/7] repairing functionality for non-zero based ranges --- homeassistant/components/cover/mqtt.py | 52 +++++++++++++++--- tests/components/cover/test_mqtt.py | 73 ++++++++++++++++++++++++++ 2 files changed, 117 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/cover/mqtt.py b/homeassistant/components/cover/mqtt.py index 829c3748d2f..9d97851b5b4 100644 --- a/homeassistant/components/cover/mqtt.py +++ b/homeassistant/components/cover/mqtt.py @@ -40,6 +40,7 @@ CONF_TILT_OPEN_POSITION = 'tilt_opened_value' CONF_TILT_MIN = 'tilt_min' CONF_TILT_MAX = 'tilt_max' CONF_TILT_STATE_OPTIMISTIC = 'tilt_optimistic' +CONF_TILT_INVERT_STATE = "tilt_invert_state" DEFAULT_NAME = 'MQTT Cover' DEFAULT_PAYLOAD_OPEN = 'OPEN' @@ -52,6 +53,7 @@ DEFAULT_TILT_OPEN_POSITION = 100 DEFAULT_TILT_MIN = 0 DEFAULT_TILT_MAX = 100 DEFAULT_TILT_OPTIMISTIC = False +DEFAULT_TILT_INVERT_STATE = False TILT_FEATURES = (SUPPORT_OPEN_TILT | SUPPORT_CLOSE_TILT | SUPPORT_STOP_TILT | SUPPORT_SET_TILT_POSITION) @@ -74,6 +76,8 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ vol.Optional(CONF_TILT_MAX, default=DEFAULT_TILT_MAX): int, vol.Optional(CONF_TILT_STATE_OPTIMISTIC, default=DEFAULT_TILT_OPTIMISTIC): cv.boolean, + vol.Optional(CONF_TILT_INVERT_STATE, + default=DEFAULT_TILT_INVERT_STATE): cv.boolean, }) @@ -104,6 +108,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): config.get(CONF_TILT_MIN), config.get(CONF_TILT_MAX), config.get(CONF_TILT_STATE_OPTIMISTIC), + config.get(CONF_TILT_INVERT_STATE), )]) @@ -114,7 +119,8 @@ class MqttCover(CoverDevice): tilt_status_topic, qos, retain, state_open, state_closed, payload_open, payload_close, payload_stop, optimistic, value_template, tilt_open_position, - tilt_closed_position, tilt_min, tilt_max, tilt_optimistic): + tilt_closed_position, tilt_min, tilt_max, tilt_optimistic, + tilt_invert): """Initialize the cover.""" self._position = None self._state = None @@ -138,6 +144,7 @@ class MqttCover(CoverDevice): self._tilt_min = tilt_min self._tilt_max = tilt_max self._tilt_optimistic = tilt_optimistic + self._tilt_invert = tilt_invert @asyncio.coroutine def async_added_to_hass(self): @@ -150,8 +157,8 @@ class MqttCover(CoverDevice): """Handle tilt updates.""" if (payload.isnumeric() and self._tilt_min <= int(payload) <= self._tilt_max): - tilt_range = self._tilt_max - self._tilt_min - level = round(float(payload) / tilt_range * 100.0) + + level = self.find_percentage_in_range(float(payload)) self._tilt_value = level self.hass.async_add_job(self.async_update_ha_state()) @@ -278,7 +285,8 @@ class MqttCover(CoverDevice): def async_open_cover_tilt(self, **kwargs): """Tilt the cover open.""" mqtt.async_publish(self.hass, self._tilt_command_topic, - self._tilt_open_position, self._qos, self._retain) + self._tilt_open_position, self._qos, + self._retain) if self._tilt_optimistic: self._tilt_value = self._tilt_open_position self.hass.async_add_job(self.async_update_ha_state()) @@ -287,7 +295,8 @@ class MqttCover(CoverDevice): def async_close_cover_tilt(self, **kwargs): """Tilt the cover closed.""" mqtt.async_publish(self.hass, self._tilt_command_topic, - self._tilt_closed_position, self._qos, self._retain) + self._tilt_closed_position, self._qos, + self._retain) if self._tilt_optimistic: self._tilt_value = self._tilt_closed_position self.hass.async_add_job(self.async_update_ha_state()) @@ -301,9 +310,36 @@ class MqttCover(CoverDevice): position = float(kwargs[ATTR_TILT_POSITION]) # The position needs to be between min and max - tilt_range = self._tilt_max - self._tilt_min - percentage = position / 100.0 - level = round(tilt_range * percentage) + level = self.find_in_range_from_percent(position) mqtt.async_publish(self.hass, self._tilt_command_topic, level, self._qos, self._retain) + + def find_percentage_in_range(self, position): + """Find the 0-100% value within the specified range.""" + # the range of motion as defined by the min max values + tilt_range = self._tilt_max - self._tilt_min + # offset to be zero based + offset_position = position - self._tilt_min + # the percentage value within the range + position_percentage = float(offset_position) / tilt_range * 100.0 + if self._tilt_invert: + return 100 - position_percentage + else: + return position_percentage + + def find_in_range_from_percent(self, percentage): + """Find the adjusted value for 0-100% within the specified range.""" + # if the range is 80-180 and the percentage is 90 + # this method would determine the value to send on the topic + # by offsetting the max and min, getting the percentage value and + # returning the offset + offset = self._tilt_min + tilt_range = self._tilt_max - self._tilt_min + + position = round(tilt_range * (percentage / 100.0)) + position += offset + + if self._tilt_invert: + position = self._tilt_max - position + offset + return position diff --git a/tests/components/cover/test_mqtt.py b/tests/components/cover/test_mqtt.py index b2dcf8e175d..e685a51f56c 100644 --- a/tests/components/cover/test_mqtt.py +++ b/tests/components/cover/test_mqtt.py @@ -4,6 +4,7 @@ import unittest from homeassistant.setup import setup_component from homeassistant.const import STATE_OPEN, STATE_CLOSED, STATE_UNKNOWN import homeassistant.components.cover as cover +from homeassistant.components.cover.mqtt import MqttCover from tests.common import ( get_test_home_assistant, mock_mqtt_component, fire_mqtt_message) @@ -450,3 +451,75 @@ class TestCoverMQTT(unittest.TestCase): self.assertEqual(('tilt-command-topic', 25, 0, False), self.mock_publish.mock_calls[-2][1]) + + def test_find_percentage_in_range_defaults(self): + """Test find percentage in range with default range.""" + mqtt_cover = MqttCover( + 'cover.test', 'foo', 'bar', 'fooBar', "fooBarBaz", 0, False, + 'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', False, None, + 100, 0, 0, 100, False, False) + + self.assertEqual(44, mqtt_cover.find_percentage_in_range(44)) + + def test_find_percentage_in_range_altered(self): + """Test find percentage in range with altered range.""" + mqtt_cover = MqttCover( + 'cover.test', 'foo', 'bar', 'fooBar', "fooBarBaz", 0, False, + 'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', False, None, + 180, 80, 80, 180, False, False) + + self.assertEqual(40, mqtt_cover.find_percentage_in_range(120)) + + def test_find_percentage_in_range_defaults_inverted(self): + """Test find percentage in range with default range but inverted.""" + mqtt_cover = MqttCover( + 'cover.test', 'foo', 'bar', 'fooBar', "fooBarBaz", 0, False, + 'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', False, None, + 100, 0, 0, 100, False, True) + + self.assertEqual(56, mqtt_cover.find_percentage_in_range(44)) + + def test_find_percentage_in_range_altered_inverted(self): + """Test find percentage in range with altered range and inverted.""" + mqtt_cover = MqttCover( + 'cover.test', 'foo', 'bar', 'fooBar', "fooBarBaz", 0, False, + 'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', False, None, + 180, 80, 80, 180, False, True) + + self.assertEqual(60, mqtt_cover.find_percentage_in_range(120)) + + def test_find_in_range_defaults(self): + """Test find in range with default range.""" + mqtt_cover = MqttCover( + 'cover.test', 'foo', 'bar', 'fooBar', "fooBarBaz", 0, False, + 'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', False, None, + 100, 0, 0, 100, False, False) + + self.assertEqual(44, mqtt_cover.find_in_range_from_percent(44)) + + def test_find_in_range_altered(self): + """Test find in range with altered range.""" + mqtt_cover = MqttCover( + 'cover.test', 'foo', 'bar', 'fooBar', "fooBarBaz", 0, False, + 'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', False, None, + 180, 80, 80, 180, False, False) + + self.assertEqual(120, mqtt_cover.find_in_range_from_percent(40)) + + def test_find_in_range_defaults_inverted(self): + """Test find in range with default range but inverted.""" + mqtt_cover = MqttCover( + 'cover.test', 'foo', 'bar', 'fooBar', "fooBarBaz", 0, False, + 'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', False, None, + 100, 0, 0, 100, False, True) + + self.assertEqual(44, mqtt_cover.find_in_range_from_percent(56)) + + def test_find_in_range_altered_inverted(self): + """Test find in range with altered range and inverted.""" + mqtt_cover = MqttCover( + 'cover.test', 'foo', 'bar', 'fooBar', "fooBarBaz", 0, False, + 'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', False, None, + 180, 80, 80, 180, False, True) + + self.assertEqual(120, mqtt_cover.find_in_range_from_percent(60)) From 0c94df9fcfd56561e27f8dc1485009b2c79ab115 Mon Sep 17 00:00:00 2001 From: Brian Cribbs Date: Mon, 1 May 2017 13:34:34 -0400 Subject: [PATCH 2/7] fixing documentation --- homeassistant/components/cover/mqtt.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/cover/mqtt.py b/homeassistant/components/cover/mqtt.py index 9d97851b5b4..91dddbfc70a 100644 --- a/homeassistant/components/cover/mqtt.py +++ b/homeassistant/components/cover/mqtt.py @@ -329,11 +329,14 @@ class MqttCover(CoverDevice): return position_percentage def find_in_range_from_percent(self, percentage): - """Find the adjusted value for 0-100% within the specified range.""" - # if the range is 80-180 and the percentage is 90 - # this method would determine the value to send on the topic - # by offsetting the max and min, getting the percentage value and - # returning the offset + """ + Find the adjusted value for 0-100% within the specified range. + + if the range is 80-180 and the percentage is 90 + this method would determine the value to send on the topic + by offsetting the max and min, getting the percentage value and + returning the offset + """ offset = self._tilt_min tilt_range = self._tilt_max - self._tilt_min From 9b920b3b40c34382c7c649e5678caeb74eeb0283 Mon Sep 17 00:00:00 2001 From: Brian Cribbs Date: Tue, 2 May 2017 15:41:45 -0400 Subject: [PATCH 3/7] fixing nits --- homeassistant/components/cover/mqtt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/cover/mqtt.py b/homeassistant/components/cover/mqtt.py index 91dddbfc70a..d44d011bcb1 100644 --- a/homeassistant/components/cover/mqtt.py +++ b/homeassistant/components/cover/mqtt.py @@ -40,7 +40,7 @@ CONF_TILT_OPEN_POSITION = 'tilt_opened_value' CONF_TILT_MIN = 'tilt_min' CONF_TILT_MAX = 'tilt_max' CONF_TILT_STATE_OPTIMISTIC = 'tilt_optimistic' -CONF_TILT_INVERT_STATE = "tilt_invert_state" +CONF_TILT_INVERT_STATE = 'tilt_invert_state' DEFAULT_NAME = 'MQTT Cover' DEFAULT_PAYLOAD_OPEN = 'OPEN' From fcdfebefd9d452952d16669c1257292865d7405b Mon Sep 17 00:00:00 2001 From: pezinek Date: Sat, 6 May 2017 19:11:31 +0200 Subject: [PATCH 4/7] Forecasts for weather underground (#7062) --- .../components/sensor/wunderground.py | 708 +++++++++++++++--- tests/components/sensor/test_wunderground.py | 72 +- 2 files changed, 666 insertions(+), 114 deletions(-) diff --git a/homeassistant/components/sensor/wunderground.py b/homeassistant/components/sensor/wunderground.py index d50f6b0897c..4d684f405f8 100644 --- a/homeassistant/components/sensor/wunderground.py +++ b/homeassistant/components/sensor/wunderground.py @@ -14,13 +14,13 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( CONF_MONITORED_CONDITIONS, CONF_API_KEY, TEMP_FAHRENHEIT, TEMP_CELSIUS, - STATE_UNKNOWN, ATTR_ATTRIBUTION) + LENGTH_INCHES, LENGTH_KILOMETERS, LENGTH_MILES, LENGTH_FEET, + STATE_UNKNOWN, ATTR_ATTRIBUTION, ATTR_FRIENDLY_NAME) from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv -_RESOURCE = 'http://api.wunderground.com/api/{}/conditions/{}/q/' -_ALERTS = 'http://api.wunderground.com/api/{}/alerts/{}/q/' +_RESOURCE = 'http://api.wunderground.com/api/{}/{}/{}/q/' _LOGGER = logging.getLogger(__name__) CONF_ATTRIBUTION = "Data provided by the WUnderground weather service" @@ -29,50 +29,562 @@ CONF_LANG = 'lang' DEFAULT_LANG = 'EN' -MIN_TIME_BETWEEN_UPDATES_ALERTS = timedelta(minutes=15) -MIN_TIME_BETWEEN_UPDATES_OBSERVATION = timedelta(minutes=5) +MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5) + + +# Helper classes for declaring sensor configurations + +class WUSensorConfig(object): + """WU Sensor Configuration. + + defines basic HA properties of the weather sensor and + stores callbacks that can parse sensor values out of + the json data received by WU API. + """ + + def __init__(self, friendly_name, feature, value, + unit_of_measurement=None, entity_picture=None, + icon="mdi:gauge", device_state_attributes=None): + """Constructor. + + Args: + friendly_name (string|func): Friendly name + feature (string): WU feature. See: + https://www.wunderground.com/weather/api/d/docs?d=data/index + value (function(WUndergroundData)): callback that + extracts desired value from WUndergroundData object + unit_of_measurement (string): unit of meassurement + entity_picture (string): value or callback returning + URL of entity picture + icon (string): icon name or URL + device_state_attributes (dict): dictionary of attributes, + or callable that returns it + """ + self.friendly_name = friendly_name + self.unit_of_measurement = unit_of_measurement + self.feature = feature + self.value = value + self.entity_picture = entity_picture + self.icon = icon + self.device_state_attributes = device_state_attributes or {} + + +class WUCurrentConditionsSensorConfig(WUSensorConfig): + """Helper for defining sensor configurations for current conditions.""" + + def __init__(self, friendly_name, field, icon="mdi:gauge", + unit_of_measurement=None): + """Constructor. + + Args: + friendly_name (string|func): Friendly name of sensor + field (string): Field name in the "current_observation" + dictionary. + icon (string): icon name or URL, if None sensor + will use current weather symbol + unit_of_measurement (string): unit of meassurement + """ + super().__init__( + friendly_name, + "conditions", + value=lambda wu: wu.data['current_observation'][field], + icon=icon, + unit_of_measurement=unit_of_measurement, + entity_picture=lambda wu: wu.data['current_observation'][ + 'icon_url'] if icon is None else None, + device_state_attributes={ + 'date': lambda wu: wu.data['current_observation'][ + 'observation_time'] + } + ) + + +class WUDailyTextForecastSensorConfig(WUSensorConfig): + """Helper for defining sensor configurations for daily text forecasts.""" + + def __init__(self, period, field, unit_of_measurement=None): + """Constructor. + + Args: + period (int): forecast period number + field (string): field name to use as value + unit_of_measurement(string): unit of measurement + """ + super().__init__( + friendly_name=lambda wu: wu.data['forecast']['txt_forecast'][ + 'forecastday'][period]['title'], + feature='forecast', + value=lambda wu: wu.data['forecast']['txt_forecast'][ + 'forecastday'][period][field], + entity_picture=lambda wu: wu.data['forecast']['txt_forecast'][ + 'forecastday'][period]['icon_url'], + unit_of_measurement=unit_of_measurement, + device_state_attributes={ + 'date': lambda wu: wu.data['forecast']['txt_forecast']['date'] + } + ) + + +class WUDailySimpleForecastSensorConfig(WUSensorConfig): + """Helper for defining sensor configurations for daily simpleforecasts.""" + + def __init__(self, friendly_name, period, field, wu_unit=None, + ha_unit=None, icon=None): + """Constructor. + + Args: + period (int): forecast period number + field (string): field name to use as value + wu_unit (string): "fahrenheit", "celsius", "degrees" etc. + see the example json at: + https://www.wunderground.com/weather/api/d/docs?d=data/forecast&MR=1 + ha_unit (string): coresponding unit in home assistant + title (string): friendly_name of the sensor + """ + super().__init__( + friendly_name=friendly_name, + feature='forecast', + value=(lambda wu: wu.data['forecast']['simpleforecast'][ + 'forecastday'][period][field][wu_unit]) + if wu_unit else + (lambda wu: wu.data['forecast']['simpleforecast'][ + 'forecastday'][period][field]), + unit_of_measurement=ha_unit, + entity_picture=lambda wu: wu.data['forecast']['simpleforecast'][ + 'forecastday'][period]['icon_url'] if not icon else None, + icon=icon, + device_state_attributes={ + 'date': lambda wu: wu.data['forecast']['simpleforecast'][ + 'forecastday'][period]['date']['pretty'] + } + ) + + +class WUHourlyForecastSensorConfig(WUSensorConfig): + """Helper for defining sensor configurations for hourly text forecasts.""" + + def __init__(self, period, field): + """Constructor. + + Args: + period (int): forecast period number + field (int): field name to use as value + """ + super().__init__( + friendly_name=lambda wu: "{} {}".format( + wu.data['hourly_forecast'][period]['FCTTIME'][ + 'weekday_name_abbrev'], + wu.data['hourly_forecast'][period]['FCTTIME'][ + 'civil']), + feature='hourly', + value=lambda wu: wu.data['hourly_forecast'][period][ + field], + entity_picture=lambda wu: wu.data['hourly_forecast'][ + period]["icon_url"], + device_state_attributes={ + 'temp_c': lambda wu: wu.data['hourly_forecast'][ + period]['temp']['metric'], + 'temp_f': lambda wu: wu.data['hourly_forecast'][ + period]['temp']['english'], + 'dewpoint_c': lambda wu: wu.data['hourly_forecast'][ + period]['dewpoint']['metric'], + 'dewpoint_f': lambda wu: wu.data['hourly_forecast'][ + period]['dewpoint']['english'], + 'precip_prop': lambda wu: wu.data['hourly_forecast'][ + period]['pop'], + 'sky': lambda wu: wu.data['hourly_forecast'][ + period]['sky'], + 'precip_mm': lambda wu: wu.data['hourly_forecast'][ + period]['qpf']['metric'], + 'precip_in': lambda wu: wu.data['hourly_forecast'][ + period]['qpf']['english'], + 'humidity': lambda wu: wu.data['hourly_forecast'][ + period]['humidity'], + 'wind_kph': lambda wu: wu.data['hourly_forecast'][ + period]['wspd']['metric'], + 'wind_mph': lambda wu: wu.data['hourly_forecast'][ + period]['wspd']['english'], + 'pressure_mb': lambda wu: wu.data['hourly_forecast'][ + period]['mslp']['metric'], + 'pressure_inHg': lambda wu: wu.data['hourly_forecast'][ + period]['mslp']['english'], + 'date': lambda wu: wu.data['hourly_forecast'][ + period]['FCTTIME']['pretty'], + }, + ) + + +class WUAlmanacSensorConfig(WUSensorConfig): + """Helper for defining field configurations for almanac sensors.""" + + def __init__(self, friendly_name, field, value_type, wu_unit, + unit_of_measurement, icon): + """Constructor. + + Args: + friendly_name (string|func): Friendly name + field (string): value name returned in 'almanac' dict + as returned by the WU API + value_type (string): "record" or "normal" + wu_unit (string): unit name in WU API + icon (string): icon name or URL + unit_of_measurement (string): unit of meassurement + """ + super().__init__( + friendly_name=friendly_name, + feature="almanac", + value=lambda wu: wu.data['almanac'][field][value_type][wu_unit], + unit_of_measurement=unit_of_measurement, + icon=icon + ) + + +class WUAlertsSensorConfig(WUSensorConfig): + """Helper for defining field configuration for alerts.""" + + def __init__(self, friendly_name): + """Constructor. + + Args: + friendly_name (string|func): Friendly name + """ + super().__init__( + friendly_name=friendly_name, + feature="alerts", + value=lambda wu: len(wu.data['alerts']), + icon=lambda wu: "mdi:alert-circle-outline" + if len(wu.data['alerts']) > 0 + else "mdi:check-circle-outline", + device_state_attributes=self._get_attributes + ) + + @staticmethod + def _get_attributes(rest): + + attrs = {} + + if 'alerts' not in rest.data: + return attrs + + alerts = rest.data['alerts'] + multiple_alerts = len(alerts) > 1 + for data in alerts: + for alert in ALERTS_ATTRS: + if data[alert]: + if multiple_alerts: + dkey = alert.capitalize() + '_' + data['type'] + else: + dkey = alert.capitalize() + attrs[dkey] = data[alert] + return attrs + + +# Declaration of supported WU sensors +# (see above helper classes for argument explanation) -# Sensor types are defined like: Name, units SENSOR_TYPES = { - 'alerts': ['Alerts', None], - 'dewpoint_c': ['Dewpoint (°C)', TEMP_CELSIUS], - 'dewpoint_f': ['Dewpoint (°F)', TEMP_FAHRENHEIT], - 'dewpoint_string': ['Dewpoint Summary', None], - 'feelslike_c': ['Feels Like (°C)', TEMP_CELSIUS], - 'feelslike_f': ['Feels Like (°F)', TEMP_FAHRENHEIT], - 'feelslike_string': ['Feels Like', None], - 'heat_index_c': ['Dewpoint (°C)', TEMP_CELSIUS], - 'heat_index_f': ['Dewpoint (°F)', TEMP_FAHRENHEIT], - 'heat_index_string': ['Heat Index Summary', None], - 'elevation': ['Elevation', 'ft'], - 'location': ['Location', None], - 'observation_time': ['Observation Time', None], - 'precip_1hr_in': ['Precipation 1hr', 'in'], - 'precip_1hr_metric': ['Precipation 1hr', 'mm'], - 'precip_1hr_string': ['Precipation 1hr', None], - 'precip_today_in': ['Precipation Today', 'in'], - 'precip_today_metric': ['Precipitation Today', 'mm'], - 'precip_today_string': ['Precipitation today', None], - 'pressure_in': ['Pressure', 'in'], - 'pressure_mb': ['Pressure', 'mb'], - 'pressure_trend': ['Pressure Trend', None], - 'relative_humidity': ['Relative Humidity', '%'], - 'station_id': ['Station ID', None], - 'solarradiation': ['Solar Radiation', None], - 'temperature_string': ['Temperature Summary', None], - 'temp_c': ['Temperature (°C)', TEMP_CELSIUS], - 'temp_f': ['Temperature (°F)', TEMP_FAHRENHEIT], - 'UV': ['UV', None], - 'visibility_km': ['Visibility (km)', 'km'], - 'visibility_mi': ['Visibility (miles)', 'mi'], - 'weather': ['Weather Summary', None], - 'wind_degrees': ['Wind Degrees', None], - 'wind_dir': ['Wind Direction', None], - 'wind_gust_kph': ['Wind Gust', 'kph'], - 'wind_gust_mph': ['Wind Gust', 'mph'], - 'wind_kph': ['Wind Speed', 'kph'], - 'wind_mph': ['Wind Speed', 'mph'], - 'wind_string': ['Wind Summary', None], + 'alerts': WUAlertsSensorConfig('Alerts'), + 'dewpoint_c': WUCurrentConditionsSensorConfig( + 'Dewpoint', 'dewpoint_c', 'mdi:water', TEMP_CELSIUS), + 'dewpoint_f': WUCurrentConditionsSensorConfig( + 'Dewpoint', 'dewpoint_f', 'mdi:water', TEMP_FAHRENHEIT), + 'dewpoint_string': WUCurrentConditionsSensorConfig( + 'Dewpoint Summary', 'dewpoint_string', 'mdi:water'), + 'feelslike_c': WUCurrentConditionsSensorConfig( + 'Feels Like', 'feelslike_c', 'mdi:thermometer', TEMP_CELSIUS), + 'feelslike_f': WUCurrentConditionsSensorConfig( + 'Feels Like', 'feelslike_f', 'mdi:thermometer', TEMP_FAHRENHEIT), + 'feelslike_string': WUCurrentConditionsSensorConfig( + 'Feels Like', 'feelslike_string', "mdi:thermometer"), + 'heat_index_c': WUCurrentConditionsSensorConfig( + 'Heat index', 'heat_index_c', "mdi:thermometer", TEMP_CELSIUS), + 'heat_index_f': WUCurrentConditionsSensorConfig( + 'Heat index', 'heat_index_f', "mdi:thermometer", TEMP_FAHRENHEIT), + 'heat_index_string': WUCurrentConditionsSensorConfig( + 'Heat Index Summary', 'heat_index_string', "mdi:thermometer"), + 'elevation': WUSensorConfig( + 'Elevation', + 'conditions', + value=lambda wu: wu.data['current_observation'][ + 'observation_location']['elevation'].split()[0], + unit_of_measurement=LENGTH_FEET, + icon="mdi:elevation-rise"), + 'location': WUSensorConfig( + 'Location', + 'conditions', + value=lambda wu: wu.data['current_observation'][ + 'display_location']['full'], + icon="mdi:map-marker"), + 'observation_time': WUCurrentConditionsSensorConfig( + 'Observation Time', 'observation_time', "mdi:clock"), + 'precip_1hr_in': WUCurrentConditionsSensorConfig( + 'Precipitation 1hr', 'precip_1hr_in', "mdi:umbrella", LENGTH_INCHES), + 'precip_1hr_metric': WUCurrentConditionsSensorConfig( + 'Precipitation 1hr', 'precip_1hr_metric', "mdi:umbrella", 'mm'), + 'precip_1hr_string': WUCurrentConditionsSensorConfig( + 'Precipitation 1hr', 'precip_1hr_string', "mdi:umbrella"), + 'precip_today_in': WUCurrentConditionsSensorConfig( + 'Precipitation Today', 'precip_today_in', "mdi:umbrella", + LENGTH_INCHES), + 'precip_today_metric': WUCurrentConditionsSensorConfig( + 'Precipitation Today', 'precip_today_metric', "mdi:umbrella", 'mm'), + 'precip_today_string': WUCurrentConditionsSensorConfig( + 'Precipitation Today', 'precip_today_string', "mdi:umbrella"), + 'pressure_in': WUCurrentConditionsSensorConfig( + 'Pressure', 'pressure_in', "mdi:gauge", 'inHg'), + 'pressure_mb': WUCurrentConditionsSensorConfig( + 'Pressure', 'pressure_mb', "mdi:gauge", 'mb'), + 'pressure_trend': WUCurrentConditionsSensorConfig( + 'Pressure Trend', 'pressure_trend', "mdi:gauge"), + 'relative_humidity': WUSensorConfig( + 'Relative Humidity', + 'conditions', + value=lambda wu: int(wu.data['current_observation'][ + 'relative_humidity'][:-1]), + unit_of_measurement='%', + icon="mdi:water-percent"), + 'station_id': WUCurrentConditionsSensorConfig( + 'Station ID', 'station_id', "mdi:home"), + 'solarradiation': WUCurrentConditionsSensorConfig( + 'Solar Radiation', 'solarradiation', "mdi:weather-sunny", "w/m2"), + 'temperature_string': WUCurrentConditionsSensorConfig( + 'Temperature Summary', 'temperature_string', "mdi:thermometer"), + 'temp_c': WUCurrentConditionsSensorConfig( + 'Temperature', 'temp_c', "mdi:thermometer", TEMP_CELSIUS), + 'temp_f': WUCurrentConditionsSensorConfig( + 'Temperature', 'temp_f', "mdi:thermometer", TEMP_FAHRENHEIT), + 'UV': WUCurrentConditionsSensorConfig( + 'UV', 'UV', "mdi:sunglasses"), + 'visibility_km': WUCurrentConditionsSensorConfig( + 'Visibility (km)', 'visibility_km', "mdi:eye", LENGTH_KILOMETERS), + 'visibility_mi': WUCurrentConditionsSensorConfig( + 'Visibility (miles)', 'visibility_mi', "mdi:eye", LENGTH_MILES), + 'weather': WUCurrentConditionsSensorConfig( + 'Weather Summary', 'weather', None), + 'wind_degrees': WUCurrentConditionsSensorConfig( + 'Wind Degrees', 'wind_degrees', "mdi:weather-windy", "°"), + 'wind_dir': WUCurrentConditionsSensorConfig( + 'Wind Direction', 'wind_dir', "mdi:weather-windy"), + 'wind_gust_kph': WUCurrentConditionsSensorConfig( + 'Wind Gust', 'wind_gust_kph', "mdi:weather-windy", 'kph'), + 'wind_gust_mph': WUCurrentConditionsSensorConfig( + 'Wind Gust', 'wind_gust_mph', "mdi:weather-windy", 'mph'), + 'wind_kph': WUCurrentConditionsSensorConfig( + 'Wind Speed', 'wind_kph', "mdi:weather-windy", 'kph'), + 'wind_mph': WUCurrentConditionsSensorConfig( + 'Wind Speed', 'wind_mph', "mdi:weather-windy", 'mph'), + 'wind_string': WUCurrentConditionsSensorConfig( + 'Wind Summary', 'wind_string', "mdi:weather-windy"), + 'temp_high_record_c': WUAlmanacSensorConfig( + lambda wu: 'High Temperature Record ({})'.format( + wu.data['almanac']['temp_high']['recordyear']), + 'temp_high', 'record', 'C', TEMP_CELSIUS, 'mdi:thermometer'), + 'temp_high_record_f': WUAlmanacSensorConfig( + lambda wu: 'High Temperature Record ({})'.format( + wu.data['almanac']['temp_high']['recordyear']), + 'temp_high', 'record', 'F', TEMP_FAHRENHEIT, 'mdi:thermometer'), + 'temp_low_record_c': WUAlmanacSensorConfig( + lambda wu: 'Low Temperature Record ({})'.format( + wu.data['almanac']['temp_low']['recordyear']), + 'temp_low', 'record', 'C', TEMP_CELSIUS, 'mdi:thermometer'), + 'temp_low_record_f': WUAlmanacSensorConfig( + lambda wu: 'Low Temperature Record ({})'.format( + wu.data['almanac']['temp_low']['recordyear']), + 'temp_low', 'record', 'F', TEMP_FAHRENHEIT, 'mdi:thermometer'), + 'temp_low_avg_c': WUAlmanacSensorConfig( + 'Historic Average of Low Temperatures for Today', + 'temp_low', 'normal', 'C', TEMP_CELSIUS, 'mdi:thermometer'), + 'temp_low_avg_f': WUAlmanacSensorConfig( + 'Historic Average of Low Temperatures for Today', + 'temp_low', 'normal', 'F', TEMP_FAHRENHEIT, 'mdi:thermometer'), + 'temp_high_avg_c': WUAlmanacSensorConfig( + 'Historic Average of High Temperatures for Today', + 'temp_high', 'normal', 'C', TEMP_CELSIUS, "mdi:thermometer"), + 'temp_high_avg_f': WUAlmanacSensorConfig( + 'Historic Average of High Temperatures for Today', + 'temp_high', 'normal', 'F', TEMP_FAHRENHEIT, "mdi:thermometer"), + 'weather_1d': WUDailyTextForecastSensorConfig(0, "fcttext"), + 'weather_1d_metric': WUDailyTextForecastSensorConfig(0, "fcttext_metric"), + 'weather_1n': WUDailyTextForecastSensorConfig(1, "fcttext"), + 'weather_1n_metric': WUDailyTextForecastSensorConfig(1, "fcttext_metric"), + 'weather_2d': WUDailyTextForecastSensorConfig(2, "fcttext"), + 'weather_2d_metric': WUDailyTextForecastSensorConfig(2, "fcttext_metric"), + 'weather_2n': WUDailyTextForecastSensorConfig(3, "fcttext"), + 'weather_2n_metric': WUDailyTextForecastSensorConfig(3, "fcttext_metric"), + 'weather_3d': WUDailyTextForecastSensorConfig(4, "fcttext"), + 'weather_3d_metric': WUDailyTextForecastSensorConfig(4, "fcttext_metric"), + 'weather_3n': WUDailyTextForecastSensorConfig(5, "fcttext"), + 'weather_3n_metric': WUDailyTextForecastSensorConfig(5, "fcttext_metric"), + 'weather_4d': WUDailyTextForecastSensorConfig(6, "fcttext"), + 'weather_4d_metric': WUDailyTextForecastSensorConfig(6, "fcttext_metric"), + 'weather_4n': WUDailyTextForecastSensorConfig(7, "fcttext"), + 'weather_4n_metric': WUDailyTextForecastSensorConfig(7, "fcttext_metric"), + 'weather_1h': WUHourlyForecastSensorConfig(0, "condition"), + 'weather_2h': WUHourlyForecastSensorConfig(1, "condition"), + 'weather_3h': WUHourlyForecastSensorConfig(2, "condition"), + 'weather_4h': WUHourlyForecastSensorConfig(3, "condition"), + 'weather_5h': WUHourlyForecastSensorConfig(4, "condition"), + 'weather_6h': WUHourlyForecastSensorConfig(5, "condition"), + 'weather_7h': WUHourlyForecastSensorConfig(6, "condition"), + 'weather_8h': WUHourlyForecastSensorConfig(7, "condition"), + 'weather_9h': WUHourlyForecastSensorConfig(8, "condition"), + 'weather_10h': WUHourlyForecastSensorConfig(9, "condition"), + 'weather_11h': WUHourlyForecastSensorConfig(10, "condition"), + 'weather_12h': WUHourlyForecastSensorConfig(11, "condition"), + 'weather_13h': WUHourlyForecastSensorConfig(12, "condition"), + 'weather_14h': WUHourlyForecastSensorConfig(13, "condition"), + 'weather_15h': WUHourlyForecastSensorConfig(14, "condition"), + 'weather_16h': WUHourlyForecastSensorConfig(15, "condition"), + 'weather_17h': WUHourlyForecastSensorConfig(16, "condition"), + 'weather_18h': WUHourlyForecastSensorConfig(17, "condition"), + 'weather_19h': WUHourlyForecastSensorConfig(18, "condition"), + 'weather_20h': WUHourlyForecastSensorConfig(19, "condition"), + 'weather_21h': WUHourlyForecastSensorConfig(20, "condition"), + 'weather_22h': WUHourlyForecastSensorConfig(21, "condition"), + 'weather_23h': WUHourlyForecastSensorConfig(22, "condition"), + 'weather_24h': WUHourlyForecastSensorConfig(23, "condition"), + 'weather_25h': WUHourlyForecastSensorConfig(24, "condition"), + 'weather_26h': WUHourlyForecastSensorConfig(25, "condition"), + 'weather_27h': WUHourlyForecastSensorConfig(26, "condition"), + 'weather_28h': WUHourlyForecastSensorConfig(27, "condition"), + 'weather_29h': WUHourlyForecastSensorConfig(28, "condition"), + 'weather_30h': WUHourlyForecastSensorConfig(29, "condition"), + 'weather_31h': WUHourlyForecastSensorConfig(30, "condition"), + 'weather_32h': WUHourlyForecastSensorConfig(31, "condition"), + 'weather_33h': WUHourlyForecastSensorConfig(32, "condition"), + 'weather_34h': WUHourlyForecastSensorConfig(33, "condition"), + 'weather_35h': WUHourlyForecastSensorConfig(34, "condition"), + 'weather_36h': WUHourlyForecastSensorConfig(35, "condition"), + 'temp_high_1d_c': WUDailySimpleForecastSensorConfig( + "High Temperature Today", 0, "high", "celsius", TEMP_CELSIUS, + "mdi:thermometer"), + 'temp_high_2d_c': WUDailySimpleForecastSensorConfig( + "High Temperature Tomorrow", 1, "high", "celsius", TEMP_CELSIUS, + "mdi:thermometer"), + 'temp_high_3d_c': WUDailySimpleForecastSensorConfig( + "High Temperature in 3 Days", 2, "high", "celsius", TEMP_CELSIUS, + "mdi:thermometer"), + 'temp_high_4d_c': WUDailySimpleForecastSensorConfig( + "High Temperature in 4 Days", 3, "high", "celsius", TEMP_CELSIUS, + "mdi:thermometer"), + 'temp_high_1d_f': WUDailySimpleForecastSensorConfig( + "High Temperature Today", 0, "high", "fahrenheit", TEMP_FAHRENHEIT, + "mdi:thermometer"), + 'temp_high_2d_f': WUDailySimpleForecastSensorConfig( + "High Temperature Tomorrow", 1, "high", "fahrenheit", TEMP_FAHRENHEIT, + "mdi:thermometer"), + 'temp_high_3d_f': WUDailySimpleForecastSensorConfig( + "High Temperature in 3 Days", 2, "high", "fahrenheit", TEMP_FAHRENHEIT, + "mdi:thermometer"), + 'temp_high_4d_f': WUDailySimpleForecastSensorConfig( + "High Temperature in 4 Days", 3, "high", "fahrenheit", TEMP_FAHRENHEIT, + "mdi:thermometer"), + 'temp_low_1d_c': WUDailySimpleForecastSensorConfig( + "Low Temperature Today", 0, "low", "celsius", TEMP_CELSIUS, + "mdi:thermometer"), + 'temp_low_2d_c': WUDailySimpleForecastSensorConfig( + "Low Temperature Tomorrow", 1, "low", "celsius", TEMP_CELSIUS, + "mdi:thermometer"), + 'temp_low_3d_c': WUDailySimpleForecastSensorConfig( + "Low Temperature in 3 Days", 2, "low", "celsius", TEMP_CELSIUS, + "mdi:thermometer"), + 'temp_low_4d_c': WUDailySimpleForecastSensorConfig( + "Low Temperature in 4 Days", 3, "low", "celsius", TEMP_CELSIUS, + "mdi:thermometer"), + 'temp_low_1d_f': WUDailySimpleForecastSensorConfig( + "Low Temperature Today", 0, "low", "fahrenheit", TEMP_FAHRENHEIT, + "mdi:thermometer"), + 'temp_low_2d_f': WUDailySimpleForecastSensorConfig( + "Low Temperature Tomorrow", 1, "low", "fahrenheit", TEMP_FAHRENHEIT, + "mdi:thermometer"), + 'temp_low_3d_f': WUDailySimpleForecastSensorConfig( + "Low Temperature in 3 Days", 2, "low", "fahrenheit", TEMP_FAHRENHEIT, + "mdi:thermometer"), + 'temp_low_4d_f': WUDailySimpleForecastSensorConfig( + "Low Temperature in 4 Days", 3, "low", "fahrenheit", TEMP_FAHRENHEIT, + "mdi:thermometer"), + 'wind_gust_1d_kph': WUDailySimpleForecastSensorConfig( + "Max. Wind Today", 0, "maxwind", "kph", "kph", "mdi:weather-windy"), + 'wind_gust_2d_kph': WUDailySimpleForecastSensorConfig( + "Max. Wind Tomorrow", 1, "maxwind", "kph", "kph", "mdi:weather-windy"), + 'wind_gust_3d_kph': WUDailySimpleForecastSensorConfig( + "Max. Wind in 3 Days", 2, "maxwind", "kph", "kph", + "mdi:weather-windy"), + 'wind_gust_4d_kph': WUDailySimpleForecastSensorConfig( + "Max. Wind in 4 Days", 3, "maxwind", "kph", "kph", + "mdi:weather-windy"), + 'wind_gust_1d_mph': WUDailySimpleForecastSensorConfig( + "Max. Wind Today", 0, "maxwind", "mph", "mph", + "mdi:weather-windy"), + 'wind_gust_2d_mph': WUDailySimpleForecastSensorConfig( + "Max. Wind Tomorrow", 1, "maxwind", "mph", "mph", + "mdi:weather-windy"), + 'wind_gust_3d_mph': WUDailySimpleForecastSensorConfig( + "Max. Wind in 3 Days", 2, "maxwind", "mph", "mph", + "mdi:weather-windy"), + 'wind_gust_4d_mph': WUDailySimpleForecastSensorConfig( + "Max. Wind in 4 Days", 3, "maxwind", "mph", "mph", + "mdi:weather-windy"), + 'wind_1d_kph': WUDailySimpleForecastSensorConfig( + "Avg. Wind Today", 0, "avewind", "kph", "kph", + "mdi:weather-windy"), + 'wind_2d_kph': WUDailySimpleForecastSensorConfig( + "Avg. Wind Tomorrow", 1, "avewind", "kph", "kph", + "mdi:weather-windy"), + 'wind_3d_kph': WUDailySimpleForecastSensorConfig( + "Avg. Wind in 3 Days", 2, "avewind", "kph", "kph", + "mdi:weather-windy"), + 'wind_4d_kph': WUDailySimpleForecastSensorConfig( + "Avg. Wind in 4 Days", 3, "avewind", "kph", "kph", + "mdi:weather-windy"), + 'wind_1d_mph': WUDailySimpleForecastSensorConfig( + "Avg. Wind Today", 0, "avewind", "mph", "mph", + "mdi:weather-windy"), + 'wind_2d_mph': WUDailySimpleForecastSensorConfig( + "Avg. Wind Tomorrow", 1, "avewind", "mph", "mph", + "mdi:weather-windy"), + 'wind_3d_mph': WUDailySimpleForecastSensorConfig( + "Avg. Wind in 3 Days", 2, "avewind", "mph", "mph", + "mdi:weather-windy"), + 'wind_4d_mph': WUDailySimpleForecastSensorConfig( + "Avg. Wind in 4 Days", 3, "avewind", "mph", "mph", + "mdi:weather-windy"), + 'precip_1d_mm': WUDailySimpleForecastSensorConfig( + "Precipitation Intensity Today", 0, 'qpf_allday', 'mm', 'mm', + "mdi:umbrella"), + 'precip_2d_mm': WUDailySimpleForecastSensorConfig( + "Precipitation Intensity Tomorrow", 1, 'qpf_allday', 'mm', 'mm', + "mdi:umbrella"), + 'precip_3d_mm': WUDailySimpleForecastSensorConfig( + "Precipitation Intensity in 3 Days", 2, 'qpf_allday', 'mm', 'mm', + "mdi:umbrella"), + 'precip_4d_mm': WUDailySimpleForecastSensorConfig( + "Precipitation Intensity in 4 Days", 3, 'qpf_allday', 'mm', 'mm', + "mdi:umbrella"), + 'precip_1d_in': WUDailySimpleForecastSensorConfig( + "Precipitation Intensity Today", 0, 'qpf_allday', 'in', + LENGTH_INCHES, "mdi:umbrella"), + 'precip_2d_in': WUDailySimpleForecastSensorConfig( + "Precipitation Intensity Tomorrow", 1, 'qpf_allday', 'in', + LENGTH_INCHES, "mdi:umbrella"), + 'precip_3d_in': WUDailySimpleForecastSensorConfig( + "Precipitation Intensity in 3 Days", 2, 'qpf_allday', 'in', + LENGTH_INCHES, "mdi:umbrella"), + 'precip_4d_in': WUDailySimpleForecastSensorConfig( + "Precipitation Intensity in 4 Days", 3, 'qpf_allday', 'in', + LENGTH_INCHES, "mdi:umbrella"), + 'precip_1d': WUDailySimpleForecastSensorConfig( + "Percipitation Probability Today", 0, "pop", None, "%", + "mdi:umbrella"), + 'precip_2d': WUDailySimpleForecastSensorConfig( + "Percipitation Probability Tomorrow", 1, "pop", None, "%", + "mdi:umbrella"), + 'precip_3d': WUDailySimpleForecastSensorConfig( + "Percipitation Probability in 3 Days", 2, "pop", None, "%", + "mdi:umbrella"), + 'precip_4d': WUDailySimpleForecastSensorConfig( + "Percipitation Probability in 4 Days", 3, "pop", None, "%", + "mdi:umbrella"), } # Alert Attributes @@ -105,9 +617,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_API_KEY): cv.string, vol.Optional(CONF_PWS_ID): cv.string, vol.Optional(CONF_LANG, default=DEFAULT_LANG): - vol.All(vol.In(LANG_CODES)), + vol.All(vol.In(LANG_CODES)), vol.Required(CONF_MONITORED_CONDITIONS, default=[]): - vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), + vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), }) @@ -138,6 +650,20 @@ class WUndergroundSensor(Entity): """Initialize the sensor.""" self.rest = rest self._condition = condition + self.rest.request_feature(SENSOR_TYPES[condition].feature) + + def _cfg_expand(self, what, default=None): + cfg = SENSOR_TYPES[self._condition] + val = getattr(cfg, what) + try: + val = val(self.rest) + except (KeyError, IndexError) as err: + _LOGGER.error("Failed to parse response from WU API: %s", err) + val = default + except TypeError: + pass # val was not callable - keep original value + + return val @property def name(self): @@ -147,69 +673,42 @@ class WUndergroundSensor(Entity): @property def state(self): """Return the state of the sensor.""" - if self.rest.data: - - if self._condition == 'elevation' and self._condition in \ - self.rest.data['observation_location']: - return self.rest.data['observation_location'][self._condition]\ - .split()[0] - - if self._condition == 'location' and \ - 'full' in self.rest.data['display_location']: - return self.rest.data['display_location']['full'] - - if self._condition in self.rest.data: - if self._condition == 'relative_humidity': - return int(self.rest.data[self._condition][:-1]) - else: - return self.rest.data[self._condition] - - if self._condition == 'alerts': - if self.rest.alerts: - return len(self.rest.alerts) - else: - return 0 - return STATE_UNKNOWN + return self._cfg_expand("value", STATE_UNKNOWN) @property def device_state_attributes(self): """Return the state attributes.""" - attrs = {} + attrs = self._cfg_expand("device_state_attributes", {}) + for (attr, callback) in attrs.items(): + try: + attrs[attr] = callback(self.rest) + except TypeError: + attrs[attr] = callback attrs[ATTR_ATTRIBUTION] = CONF_ATTRIBUTION - - if not self.rest.alerts or self._condition != 'alerts': - return attrs - - multiple_alerts = len(self.rest.alerts) > 1 - for data in self.rest.alerts: - for alert in ALERTS_ATTRS: - if data[alert]: - if multiple_alerts: - dkey = alert.capitalize() + '_' + data['type'] - else: - dkey = alert.capitalize() - attrs[dkey] = data[alert] + attrs[ATTR_FRIENDLY_NAME] = self._cfg_expand("friendly_name") return attrs + @property + def icon(self): + """Return icon.""" + return self._cfg_expand("icon", super().icon) + @property def entity_picture(self): """Return the entity picture.""" - if self.rest.data and self._condition == 'weather': - url = self.rest.data['icon_url'] + url = self._cfg_expand("entity_picture") + if url is not None: return re.sub(r'^http://', 'https://', url, flags=re.IGNORECASE) @property def unit_of_measurement(self): """Return the units of measurement.""" - return SENSOR_TYPES[self._condition][1] + return self._cfg_expand("unit_of_measurement") def update(self): """Update current conditions.""" - if self._condition == 'alerts': - self.rest.update_alerts() - else: - self.rest.update() + self.rest.update() class WUndergroundData(object): @@ -223,11 +722,16 @@ class WUndergroundData(object): self._lang = 'lang:{}'.format(lang) self._latitude = hass.config.latitude self._longitude = hass.config.longitude + self._features = set() self.data = None - self.alerts = None + + def request_feature(self, feature): + """Register feature to be fetched from WU API.""" + self._features.add(feature) def _build_url(self, baseurl=_RESOURCE): - url = baseurl.format(self._api_key, self._lang) + url = baseurl.format( + self._api_key, "/".join(self._features), self._lang) if self._pws_id: url = url + 'pws:{}'.format(self._pws_id) else: @@ -235,7 +739,7 @@ class WUndergroundData(object): return url + '.json' - @Throttle(MIN_TIME_BETWEEN_UPDATES_OBSERVATION) + @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the latest data from WUnderground.""" try: @@ -244,21 +748,7 @@ class WUndergroundData(object): raise ValueError(result['response']["error"] ["description"]) else: - self.data = result["current_observation"] + self.data = result except ValueError as err: _LOGGER.error("Check WUnderground API %s", err.args) self.data = None - - @Throttle(MIN_TIME_BETWEEN_UPDATES_ALERTS) - def update_alerts(self): - """Get the latest alerts data from WUnderground.""" - try: - result = requests.get(self._build_url(_ALERTS), timeout=10).json() - if "error" in result['response']: - raise ValueError(result['response']["error"] - ["description"]) - else: - self.alerts = result["alerts"] - except ValueError as err: - _LOGGER.error("Check WUnderground API %s", err.args) - self.alerts = None diff --git a/tests/components/sensor/test_wunderground.py b/tests/components/sensor/test_wunderground.py index 286f9d959e2..1a3c0304b00 100644 --- a/tests/components/sensor/test_wunderground.py +++ b/tests/components/sensor/test_wunderground.py @@ -2,7 +2,7 @@ import unittest from homeassistant.components.sensor import wunderground -from homeassistant.const import TEMP_CELSIUS +from homeassistant.const import TEMP_CELSIUS, LENGTH_INCHES from tests.common import get_test_home_assistant @@ -19,7 +19,8 @@ VALID_CONFIG = { 'platform': 'wunderground', 'api_key': 'foo', 'monitored_conditions': [ - 'weather', 'feelslike_c', 'alerts', 'elevation', 'location' + 'weather', 'feelslike_c', 'alerts', 'elevation', 'location', + 'weather_1d_metric', 'precip_1d_in' ] } @@ -37,6 +38,8 @@ FEELS_LIKE = '40' WEATHER = 'Clear' HTTPS_ICON_URL = 'https://icons.wxug.com/i/c/k/clear.gif' ALERT_MESSAGE = 'This is a test alert message' +FORECAST_TEXT = 'Mostly Cloudy. Fog overnight.' +PRECIP_IN = 0.03 def mocked_requests_get(*args, **kwargs): @@ -60,7 +63,9 @@ def mocked_requests_get(*args, **kwargs): "termsofService": "http://www.wunderground.com/weather/api/d/terms.html", "features": { - "conditions": 1 + "conditions": 1, + "alerts": 1, + "forecast": 1, } }, "current_observation": { "image": { @@ -90,7 +95,58 @@ def mocked_requests_get(*args, **kwargs): "message": ALERT_MESSAGE, }, - ], + ], "forecast": { + "txt_forecast": { + "date": "22:35 CEST", + "forecastday": [ + { + "period": 0, + "icon_url": + "http://icons.wxug.com/i/c/k/clear.gif", + "title": "Tuesday", + "fcttext": FORECAST_TEXT, + "fcttext_metric": FORECAST_TEXT, + "pop": "0" + }, + ], + }, "simpleforecast": { + "forecastday": [ + { + "date": { + "pretty": "19:00 CEST 4. Duben 2017", + }, + "period": 1, + "high": { + "fahrenheit": "56", + "celsius": "13", + }, + "low": { + "fahrenheit": "43", + "celsius": "6", + }, + "conditions": "Možnost deště", + "icon_url": + "http://icons.wxug.com/i/c/k/chancerain.gif", + "qpf_allday": { + "in": PRECIP_IN, + "mm": 1, + }, + "maxwind": { + "mph": 0, + "kph": 0, + "dir": "", + "degrees": 0, + }, + "avewind": { + "mph": 0, + "kph": 0, + "dir": "severní", + "degrees": 0 + } + }, + ], + }, + }, }, 200) else: return MockResponse({ @@ -168,7 +224,13 @@ class TestWundergroundSetup(unittest.TestCase): self.assertEqual('Holly Springs, NC', device.state) elif device.name == 'PWS_elevation': self.assertEqual('413', device.state) - else: + elif device.name == 'PWS_feelslike_c': self.assertIsNone(device.entity_picture) self.assertEqual(FEELS_LIKE, device.state) self.assertEqual(TEMP_CELSIUS, device.unit_of_measurement) + elif device.name == 'PWS_weather_1d_metric': + self.assertEqual(FORECAST_TEXT, device.state) + else: + self.assertEqual(device.name, 'PWS_precip_1d_in') + self.assertEqual(PRECIP_IN, device.state) + self.assertEqual(LENGTH_INCHES, device.unit_of_measurement) From 2ab45441a8ebff8fd55860b5d61ac25dedf288ce Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sun, 7 May 2017 04:10:17 +0200 Subject: [PATCH 5/7] Upgrade pymysensors to 0.10.0 (#7469) --- homeassistant/components/mysensors.py | 4 +--- requirements_all.txt | 6 +++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/mysensors.py b/homeassistant/components/mysensors.py index 984ff8a4606..ef863bfb34f 100644 --- a/homeassistant/components/mysensors.py +++ b/homeassistant/components/mysensors.py @@ -21,9 +21,7 @@ from homeassistant.const import ( from homeassistant.helpers import discovery from homeassistant.loader import get_component -REQUIREMENTS = [ - 'https://github.com/theolind/pymysensors/archive/' - 'c6990eaaa741444a638608e6e00488195e2ca74c.zip#pymysensors==0.9.1'] +REQUIREMENTS = ['pymysensors==0.10.0'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 4f0edd805e1..65ad330b0e2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -318,9 +318,6 @@ https://github.com/tfriedel/python-lightify/archive/1bb1db0e7bd5b14304d7bb267e23 # homeassistant.components.lutron https://github.com/thecynic/pylutron/archive/v0.1.0.zip#pylutron==0.1.0 -# homeassistant.components.mysensors -https://github.com/theolind/pymysensors/archive/c6990eaaa741444a638608e6e00488195e2ca74c.zip#pymysensors==0.9.1 - # homeassistant.components.sensor.modem_callerid https://github.com/vroomfonde1/basicmodem/archive/0.7.zip#basicmodem==0.7 @@ -593,6 +590,9 @@ pymailgunner==1.4 # homeassistant.components.mochad pymochad==0.1.1 +# homeassistant.components.mysensors +pymysensors==0.10.0 + # homeassistant.components.device_tracker.netgear pynetgear==0.3.3 From f87b9b7b857550982d37baff999eea64a262b261 Mon Sep 17 00:00:00 2001 From: Marc Egli Date: Sun, 7 May 2017 15:15:18 +0200 Subject: [PATCH 6/7] Fix plant MIN_TEMPERATURE, MAX_TEMPERATURE validation (#7476) * Fix plant MIN_TEMPERATURE, MAX_TEMPERATURE validation small_float only allows values from 0 to 1 so we should use float instead * Do not use vol.All for a single validation --- homeassistant/components/plant.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/plant.py b/homeassistant/components/plant.py index 2215d7c2f30..2070c22fb97 100644 --- a/homeassistant/components/plant.py +++ b/homeassistant/components/plant.py @@ -58,8 +58,8 @@ SCHEMA_SENSORS = vol.Schema({ PLANT_SCHEMA = vol.Schema({ vol.Required(CONF_SENSORS): vol.Schema(SCHEMA_SENSORS), vol.Optional(CONF_MIN_BATTERY_LEVEL): cv.positive_int, - vol.Optional(CONF_MIN_TEMPERATURE): cv.small_float, - vol.Optional(CONF_MAX_TEMPERATURE): cv.small_float, + vol.Optional(CONF_MIN_TEMPERATURE): vol.Coerce(float), + vol.Optional(CONF_MAX_TEMPERATURE): vol.Coerce(float), vol.Optional(CONF_MIN_MOISTURE): cv.positive_int, vol.Optional(CONF_MAX_MOISTURE): cv.positive_int, vol.Optional(CONF_MIN_CONDUCTIVITY): cv.positive_int, From 4165629f975322ce1b96491035d7892a0e172c2f Mon Sep 17 00:00:00 2001 From: Caleb Date: Sun, 7 May 2017 00:39:21 -0500 Subject: [PATCH 7/7] Update to pyunifi 2.12 (#7468) * Update to pyunifi 2.12 * Update requirements_all.txt --- homeassistant/components/device_tracker/unifi.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/device_tracker/unifi.py b/homeassistant/components/device_tracker/unifi.py index 42b5070b046..b0409e99883 100644 --- a/homeassistant/components/device_tracker/unifi.py +++ b/homeassistant/components/device_tracker/unifi.py @@ -15,7 +15,7 @@ from homeassistant.components.device_tracker import ( from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD from homeassistant.const import CONF_VERIFY_SSL -REQUIREMENTS = ['pyunifi==2.0'] +REQUIREMENTS = ['pyunifi==2.12'] _LOGGER = logging.getLogger(__name__) CONF_PORT = 'port' diff --git a/requirements_all.txt b/requirements_all.txt index 65ad330b0e2..4f0ba65105b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -698,7 +698,7 @@ pytrackr==0.0.5 pytradfri==1.1 # homeassistant.components.device_tracker.unifi -pyunifi==2.0 +pyunifi==2.12 # homeassistant.components.keyboard # pyuserinput==0.1.11