From eb8a8f6d0b13780ecf967fa0b43d83091f86dcef Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 16 Nov 2017 22:10:40 -0800 Subject: [PATCH 01/14] Version bump to 0.58.0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index d08308de820..d8b4dfcb044 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 58 -PATCH_VERSION = '0.dev0' +PATCH_VERSION = '0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 4, 2) From b3d66e5881a2b54dc90c5899a6f23e0ede209f7e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 17 Nov 2017 19:09:47 -0800 Subject: [PATCH 02/14] Update frontend to 20171118.0 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index bac00b8a57a..e7cfcf8d88c 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -23,7 +23,7 @@ from homeassistant.const import CONF_NAME, EVENT_THEMES_UPDATED from homeassistant.core import callback from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20171117.1'] +REQUIREMENTS = ['home-assistant-frontend==20171118.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index 04432aa26a2..004535fd14f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -331,7 +331,7 @@ hipnotify==1.0.8 holidays==0.8.1 # homeassistant.components.frontend -home-assistant-frontend==20171117.1 +home-assistant-frontend==20171118.0 # homeassistant.components.camera.onvif http://github.com/tgaugry/suds-passworddigest-py3/archive/86fc50e39b4d2b8997481967d6a7fe1c57118999.zip#suds-passworddigest-py3==0.1.2a diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 32c2feb7427..c9ea20494d4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -74,7 +74,7 @@ hbmqtt==0.9.1 holidays==0.8.1 # homeassistant.components.frontend -home-assistant-frontend==20171117.1 +home-assistant-frontend==20171118.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From 0202e966ea250188ef4da3e4a0b29d929060e912 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 17 Nov 2017 13:11:05 -0700 Subject: [PATCH 03/14] Fixes AirVisual bug regarding incorrect location data (#10054) * Fixes AirVisual bug regarding incorrect location data * Owner-requested changes --- homeassistant/components/sensor/airvisual.py | 47 ++++++++++---------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/sensor/airvisual.py b/homeassistant/components/sensor/airvisual.py index 56ddf7adcab..5ea24dab823 100644 --- a/homeassistant/components/sensor/airvisual.py +++ b/homeassistant/components/sensor/airvisual.py @@ -126,7 +126,7 @@ class AirVisualBaseSensor(Entity): def __init__(self, data, name, icon, locale): """Initialize the sensor.""" - self._data = data + self.data = data self._icon = icon self._locale = locale self._name = name @@ -136,20 +136,17 @@ class AirVisualBaseSensor(Entity): @property def device_state_attributes(self): """Return the device state attributes.""" - attrs = { + attrs = merge_two_dicts({ ATTR_ATTRIBUTION: CONF_ATTRIBUTION, - ATTR_CITY: self._data.city, - ATTR_COUNTRY: self._data.country, - ATTR_REGION: self._data.state, - ATTR_TIMESTAMP: self._data.pollution_info.get('ts') - } + ATTR_TIMESTAMP: self.data.pollution_info.get('ts') + }, self.data.attrs) - if self._data.show_on_map: - attrs[ATTR_LATITUDE] = self._data.latitude - attrs[ATTR_LONGITUDE] = self._data.longitude + if self.data.show_on_map: + attrs[ATTR_LATITUDE] = self.data.latitude + attrs[ATTR_LONGITUDE] = self.data.longitude else: - attrs['lati'] = self._data.latitude - attrs['long'] = self._data.longitude + attrs['lati'] = self.data.latitude + attrs['long'] = self.data.longitude return attrs @@ -174,9 +171,9 @@ class AirPollutionLevelSensor(AirVisualBaseSensor): def update(self): """Update the status of the sensor.""" - self._data.update() + self.data.update() - aqi = self._data.pollution_info.get('aqi{0}'.format(self._locale)) + aqi = self.data.pollution_info.get('aqi{0}'.format(self._locale)) try: [level] = [ i for i in POLLUTANT_LEVEL_MAPPING @@ -199,9 +196,9 @@ class AirQualityIndexSensor(AirVisualBaseSensor): def update(self): """Update the status of the sensor.""" - self._data.update() + self.data.update() - self._state = self._data.pollution_info.get( + self._state = self.data.pollution_info.get( 'aqi{0}'.format(self._locale)) @@ -224,9 +221,9 @@ class MainPollutantSensor(AirVisualBaseSensor): def update(self): """Update the status of the sensor.""" - self._data.update() + self.data.update() - symbol = self._data.pollution_info.get('main{0}'.format(self._locale)) + symbol = self.data.pollution_info.get('main{0}'.format(self._locale)) pollution_info = POLLUTANT_MAPPING.get(symbol, {}) self._state = pollution_info.get('label') self._unit = pollution_info.get('unit') @@ -239,6 +236,7 @@ class AirVisualData(object): def __init__(self, client, **kwargs): """Initialize the AirVisual data element.""" self._client = client + self.attrs = {} self.pollution_info = None self.city = kwargs.get(CONF_CITY) @@ -260,17 +258,20 @@ class AirVisualData(object): if self.city and self.state and self.country: resp = self._client.city( self.city, self.state, self.country).get('data') + self.longitude, self.latitude = resp.get('location').get( + 'coordinates') else: resp = self._client.nearest_city( self.latitude, self.longitude, self._radius).get('data') _LOGGER.debug("New data retrieved: %s", resp) - self.city = resp.get('city') - self.state = resp.get('state') - self.country = resp.get('country') - self.longitude, self.latitude = resp.get('location').get( - 'coordinates') self.pollution_info = resp.get('current', {}).get('pollution', {}) + + self.attrs = { + ATTR_CITY: resp.get('city'), + ATTR_REGION: resp.get('state'), + ATTR_COUNTRY: resp.get('country') + } except exceptions.HTTPError as exc_info: _LOGGER.error("Unable to retrieve data on this location: %s", self.__dict__) From bf8e2bd77ee743624a4f1a15936fa4885857f8f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cezar=20S=C3=A1=20Espinola?= Date: Fri, 17 Nov 2017 16:29:23 -0200 Subject: [PATCH 04/14] Make MQTT reconnection logic more resilient and fix race condition (#10133) --- homeassistant/components/mqtt/__init__.py | 34 ++++++++--------------- tests/components/mqtt/test_init.py | 28 ++++++++++++------- 2 files changed, 30 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 9decc9a14aa..3a6abec0ddf 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -438,7 +438,8 @@ class MQTT(object): self.broker = broker self.port = port self.keepalive = keepalive - self.topics = {} + self.wanted_topics = {} + self.subscribed_topics = {} self.progress = {} self.birth_message = birth_message self._mqttc = None @@ -526,15 +527,14 @@ class MQTT(object): raise HomeAssistantError("topic need to be a string!") with (yield from self._paho_lock): - if topic in self.topics: + if topic in self.subscribed_topics: return - + self.wanted_topics[topic] = qos result, mid = yield from self.hass.async_add_job( self._mqttc.subscribe, topic, qos) _raise_on_error(result) self.progress[mid] = topic - self.topics[topic] = None @asyncio.coroutine def async_unsubscribe(self, topic): @@ -542,6 +542,7 @@ class MQTT(object): This method is a coroutine. """ + self.wanted_topics.pop(topic, None) result, mid = yield from self.hass.async_add_job( self._mqttc.unsubscribe, topic) @@ -562,15 +563,10 @@ class MQTT(object): self._mqttc.disconnect() return - old_topics = self.topics - - self.topics = {key: value for key, value in self.topics.items() - if value is None} - - for topic, qos in old_topics.items(): - # qos is None if we were in process of subscribing - if qos is not None: - self.hass.add_job(self.async_subscribe, topic, qos) + self.progress = {} + self.subscribed_topics = {} + for topic, qos in self.wanted_topics.items(): + self.hass.add_job(self.async_subscribe, topic, qos) if self.birth_message: self.hass.add_job(self.async_publish( @@ -584,7 +580,7 @@ class MQTT(object): topic = self.progress.pop(mid, None) if topic is None: return - self.topics[topic] = granted_qos[0] + self.subscribed_topics[topic] = granted_qos[0] def _mqtt_on_message(self, _mqttc, _userdata, msg): """Message received callback.""" @@ -598,18 +594,12 @@ class MQTT(object): topic = self.progress.pop(mid, None) if topic is None: return - self.topics.pop(topic, None) + self.subscribed_topics.pop(topic, None) def _mqtt_on_disconnect(self, _mqttc, _userdata, result_code): """Disconnected callback.""" self.progress = {} - self.topics = {key: value for key, value in self.topics.items() - if value is not None} - - # Remove None values from topic list - for key in list(self.topics): - if self.topics[key] is None: - self.topics.pop(key) + self.subscribed_topics = {} # When disconnected because of calling disconnect() if result_code == 0: diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 3d068224243..55ff0e9ff05 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -388,9 +388,12 @@ class TestMQTTCallbacks(unittest.TestCase): @mock.patch('homeassistant.components.mqtt.time.sleep') def test_mqtt_disconnect_tries_reconnect(self, mock_sleep): """Test the re-connect tries.""" - self.hass.data['mqtt'].topics = { + self.hass.data['mqtt'].subscribed_topics = { 'test/topic': 1, - 'test/progress': None + } + self.hass.data['mqtt'].wanted_topics = { + 'test/progress': 0, + 'test/topic': 2, } self.hass.data['mqtt'].progress = { 1: 'test/progress' @@ -403,7 +406,9 @@ class TestMQTTCallbacks(unittest.TestCase): self.assertEqual([1, 2, 4], [call[1][0] for call in mock_sleep.mock_calls]) - self.assertEqual({'test/topic': 1}, self.hass.data['mqtt'].topics) + self.assertEqual({'test/topic': 2, 'test/progress': 0}, + self.hass.data['mqtt'].wanted_topics) + self.assertEqual({}, self.hass.data['mqtt'].subscribed_topics) self.assertEqual({}, self.hass.data['mqtt'].progress) def test_invalid_mqtt_topics(self): @@ -556,12 +561,15 @@ def test_mqtt_subscribes_topics_on_connect(hass): """Test subscription to topic on connect.""" mqtt_client = yield from mock_mqtt_client(hass) - prev_topics = OrderedDict() - prev_topics['topic/test'] = 1, - prev_topics['home/sensor'] = 2, - prev_topics['still/pending'] = None + subscribed_topics = OrderedDict() + subscribed_topics['topic/test'] = 1 + subscribed_topics['home/sensor'] = 2 - hass.data['mqtt'].topics = prev_topics + wanted_topics = subscribed_topics.copy() + wanted_topics['still/pending'] = 0 + + hass.data['mqtt'].wanted_topics = wanted_topics + hass.data['mqtt'].subscribed_topics = subscribed_topics hass.data['mqtt'].progress = {1: 'still/pending'} # Return values for subscribe calls (rc, mid) @@ -574,7 +582,7 @@ def test_mqtt_subscribes_topics_on_connect(hass): assert not mqtt_client.disconnect.called - expected = [(topic, qos) for topic, qos in prev_topics.items() - if qos is not None] + expected = [(topic, qos) for topic, qos in wanted_topics.items()] assert [call[1][1:] for call in hass.add_job.mock_calls] == expected + assert hass.data['mqtt'].progress == {} From e449ceeeff8ebe0392653a2e4f7833d8e01d8c51 Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Fri, 17 Nov 2017 09:14:22 -0800 Subject: [PATCH 05/14] Alexa improvements (#10632) * Initial scene support * Initial fan support * ordering * Initial lock support * Scenes cant be deactivated; Correct the scene display category * Initial input_boolean support * Support customization of Alexa discovered entities * Initial media player support * Add input_boolean to tests * Add play/pause/stop/next/previous to media player * Add missing functions and pylint * Set manufacturerName to Home Assistant since the value is displayed in app * Add scene test * Add fan tests * Add lock test * Fix volume logic * Add volume tests * settup -> setup * Remove unused variable * Set required scene description as per docs * Allow setting scene category (ACTIVITY_TRIGGER/SCENE_TRIGGER) * Add alert, automation and group support/tests * Change display categories to match docs * simplify down the display category props into a single prop which can be used on any entity * Fix tests to expect proper display categories * Add cover support * sort things * Use generic homeassistant domain for turn on/off --- homeassistant/components/alexa/smart_home.py | 334 +++++++++++- tests/components/alexa/test_smart_home.py | 546 ++++++++++++++++++- 2 files changed, 853 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/alexa/smart_home.py b/homeassistant/components/alexa/smart_home.py index a96386cbdf9..c5a849ad560 100644 --- a/homeassistant/components/alexa/smart_home.py +++ b/homeassistant/components/alexa/smart_home.py @@ -4,9 +4,16 @@ import logging import math from uuid import uuid4 +import homeassistant.core as ha from homeassistant.const import ( - ATTR_SUPPORTED_FEATURES, ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF) -from homeassistant.components import switch, light, script + ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, SERVICE_LOCK, + SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY, + SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_STOP, + SERVICE_SET_COVER_POSITION, SERVICE_TURN_OFF, SERVICE_TURN_ON, + SERVICE_UNLOCK, SERVICE_VOLUME_SET) +from homeassistant.components import ( + alert, automation, cover, fan, group, input_boolean, light, lock, + media_player, scene, script, switch) import homeassistant.util.color as color_util from homeassistant.util.decorator import Registry @@ -14,15 +21,32 @@ HANDLERS = Registry() _LOGGER = logging.getLogger(__name__) API_DIRECTIVE = 'directive' +API_ENDPOINT = 'endpoint' API_EVENT = 'event' API_HEADER = 'header' API_PAYLOAD = 'payload' -API_ENDPOINT = 'endpoint' + +ATTR_ALEXA_DESCRIPTION = 'alexa_description' +ATTR_ALEXA_DISPLAY_CATEGORIES = 'alexa_display_categories' +ATTR_ALEXA_HIDDEN = 'alexa_hidden' +ATTR_ALEXA_NAME = 'alexa_name' MAPPING_COMPONENT = { - script.DOMAIN: ['SWITCH', ('Alexa.PowerController',), None], - switch.DOMAIN: ['SWITCH', ('Alexa.PowerController',), None], + alert.DOMAIN: ['OTHER', ('Alexa.PowerController',), None], + automation.DOMAIN: ['OTHER', ('Alexa.PowerController',), None], + cover.DOMAIN: [ + 'DOOR', ('Alexa.PowerController',), { + cover.SUPPORT_SET_POSITION: 'Alexa.PercentageController', + } + ], + fan.DOMAIN: [ + 'OTHER', ('Alexa.PowerController',), { + fan.SUPPORT_SET_SPEED: 'Alexa.PercentageController', + } + ], + group.DOMAIN: ['OTHER', ('Alexa.PowerController',), None], + input_boolean.DOMAIN: ['OTHER', ('Alexa.PowerController',), None], light.DOMAIN: [ 'LIGHT', ('Alexa.PowerController',), { light.SUPPORT_BRIGHTNESS: 'Alexa.BrightnessController', @@ -31,6 +55,20 @@ MAPPING_COMPONENT = { light.SUPPORT_COLOR_TEMP: 'Alexa.ColorTemperatureController', } ], + lock.DOMAIN: ['SMARTLOCK', ('Alexa.LockController',), None], + media_player.DOMAIN: [ + 'TV', ('Alexa.PowerController',), { + media_player.SUPPORT_VOLUME_SET: 'Alexa.Speaker', + media_player.SUPPORT_PLAY: 'Alexa.PlaybackController', + media_player.SUPPORT_PAUSE: 'Alexa.PlaybackController', + media_player.SUPPORT_STOP: 'Alexa.PlaybackController', + media_player.SUPPORT_NEXT_TRACK: 'Alexa.PlaybackController', + media_player.SUPPORT_PREVIOUS_TRACK: 'Alexa.PlaybackController', + } + ], + scene.DOMAIN: ['ACTIVITY_TRIGGER', ('Alexa.SceneController',), None], + script.DOMAIN: ['OTHER', ('Alexa.PowerController',), None], + switch.DOMAIN: ['SWITCH', ('Alexa.PowerController',), None], } @@ -108,18 +146,33 @@ def async_api_discovery(hass, request): discovery_endpoints = [] for entity in hass.states.async_all(): + if entity.attributes.get(ATTR_ALEXA_HIDDEN, False): + continue + class_data = MAPPING_COMPONENT.get(entity.domain) if not class_data: continue + friendly_name = entity.attributes.get(ATTR_ALEXA_NAME, entity.name) + description = entity.attributes.get(ATTR_ALEXA_DESCRIPTION, + entity.entity_id) + + # Required description as per Amazon Scene docs + if entity.domain == scene.DOMAIN: + scene_fmt = '%s (Scene connected via Home Assistant)' + description = scene_fmt.format(description) + + cat_key = ATTR_ALEXA_DISPLAY_CATEGORIES + display_categories = entity.attributes.get(cat_key, class_data[0]) + endpoint = { - 'displayCategories': [class_data[0]], + 'displayCategories': [display_categories], 'additionalApplianceDetails': {}, 'endpointId': entity.entity_id.replace('.', '#'), - 'friendlyName': entity.name, - 'description': '', - 'manufacturerName': 'Unknown', + 'friendlyName': friendly_name, + 'description': description, + 'manufacturerName': 'Home Assistant', } actions = set() @@ -175,7 +228,7 @@ def extract_entity(funct): @asyncio.coroutine def async_api_turn_on(hass, request, entity): """Process a turn on request.""" - yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, { + yield from hass.services.async_call(ha.DOMAIN, SERVICE_TURN_ON, { ATTR_ENTITY_ID: entity.entity_id }, blocking=True) @@ -187,7 +240,7 @@ def async_api_turn_on(hass, request, entity): @asyncio.coroutine def async_api_turn_off(hass, request, entity): """Process a turn off request.""" - yield from hass.services.async_call(entity.domain, SERVICE_TURN_OFF, { + yield from hass.services.async_call(ha.DOMAIN, SERVICE_TURN_OFF, { ATTR_ENTITY_ID: entity.entity_id }, blocking=True) @@ -310,3 +363,262 @@ def async_api_increase_color_temp(hass, request, entity): }, blocking=True) return api_message(request) + + +@HANDLERS.register(('Alexa.SceneController', 'Activate')) +@extract_entity +@asyncio.coroutine +def async_api_activate(hass, request, entity): + """Process a activate request.""" + yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, { + ATTR_ENTITY_ID: entity.entity_id + }, blocking=True) + + return api_message(request) + + +@HANDLERS.register(('Alexa.PercentageController', 'SetPercentage')) +@extract_entity +@asyncio.coroutine +def async_api_set_percentage(hass, request, entity): + """Process a set percentage request.""" + percentage = int(request[API_PAYLOAD]['percentage']) + service = None + data = {ATTR_ENTITY_ID: entity.entity_id} + + if entity.domain == fan.DOMAIN: + service = fan.SERVICE_SET_SPEED + speed = "off" + + if percentage <= 33: + speed = "low" + elif percentage <= 66: + speed = "medium" + elif percentage <= 100: + speed = "high" + data[fan.ATTR_SPEED] = speed + + elif entity.domain == cover.DOMAIN: + service = SERVICE_SET_COVER_POSITION + data[cover.ATTR_POSITION] = percentage + + yield from hass.services.async_call(entity.domain, service, + data, blocking=True) + + return api_message(request) + + +@HANDLERS.register(('Alexa.PercentageController', 'AdjustPercentage')) +@extract_entity +@asyncio.coroutine +def async_api_adjust_percentage(hass, request, entity): + """Process a adjust percentage request.""" + percentage_delta = int(request[API_PAYLOAD]['percentageDelta']) + service = None + data = {ATTR_ENTITY_ID: entity.entity_id} + + if entity.domain == fan.DOMAIN: + service = fan.SERVICE_SET_SPEED + speed = entity.attributes.get(fan.ATTR_SPEED) + + if speed == "off": + current = 0 + elif speed == "low": + current = 33 + elif speed == "medium": + current = 66 + elif speed == "high": + current = 100 + + # set percentage + percentage = max(0, percentage_delta + current) + speed = "off" + + if percentage <= 33: + speed = "low" + elif percentage <= 66: + speed = "medium" + elif percentage <= 100: + speed = "high" + + data[fan.ATTR_SPEED] = speed + + elif entity.domain == cover.DOMAIN: + service = SERVICE_SET_COVER_POSITION + + current = entity.attributes.get(cover.ATTR_POSITION) + + data[cover.ATTR_POSITION] = max(0, percentage_delta + current) + + yield from hass.services.async_call(entity.domain, service, + data, blocking=True) + + return api_message(request) + + +@HANDLERS.register(('Alexa.LockController', 'Lock')) +@extract_entity +@asyncio.coroutine +def async_api_lock(hass, request, entity): + """Process a lock request.""" + yield from hass.services.async_call(entity.domain, SERVICE_LOCK, { + ATTR_ENTITY_ID: entity.entity_id + }, blocking=True) + + return api_message(request) + + +# Not supported by Alexa yet +@HANDLERS.register(('Alexa.LockController', 'Unlock')) +@extract_entity +@asyncio.coroutine +def async_api_unlock(hass, request, entity): + """Process a unlock request.""" + yield from hass.services.async_call(entity.domain, SERVICE_UNLOCK, { + ATTR_ENTITY_ID: entity.entity_id + }, blocking=True) + + return api_message(request) + + +@HANDLERS.register(('Alexa.Speaker', 'SetVolume')) +@extract_entity +@asyncio.coroutine +def async_api_set_volume(hass, request, entity): + """Process a set volume request.""" + volume = round(float(request[API_PAYLOAD]['volume'] / 100), 2) + + data = { + ATTR_ENTITY_ID: entity.entity_id, + media_player.ATTR_MEDIA_VOLUME_LEVEL: volume, + } + + yield from hass.services.async_call(entity.domain, SERVICE_VOLUME_SET, + data, blocking=True) + + return api_message(request) + + +@HANDLERS.register(('Alexa.Speaker', 'AdjustVolume')) +@extract_entity +@asyncio.coroutine +def async_api_adjust_volume(hass, request, entity): + """Process a adjust volume request.""" + volume_delta = int(request[API_PAYLOAD]['volume']) + + current_level = entity.attributes.get(media_player.ATTR_MEDIA_VOLUME_LEVEL) + + # read current state + try: + current = math.floor(int(current_level * 100)) + except ZeroDivisionError: + current = 0 + + volume = float(max(0, volume_delta + current) / 100) + + data = { + ATTR_ENTITY_ID: entity.entity_id, + media_player.ATTR_MEDIA_VOLUME_LEVEL: volume, + } + + yield from hass.services.async_call(entity.domain, + media_player.SERVICE_VOLUME_SET, + data, blocking=True) + + return api_message(request) + + +@HANDLERS.register(('Alexa.Speaker', 'SetMute')) +@extract_entity +@asyncio.coroutine +def async_api_set_mute(hass, request, entity): + """Process a set mute request.""" + mute = bool(request[API_PAYLOAD]['mute']) + + data = { + ATTR_ENTITY_ID: entity.entity_id, + media_player.ATTR_MEDIA_VOLUME_MUTED: mute, + } + + yield from hass.services.async_call(entity.domain, + media_player.SERVICE_VOLUME_MUTE, + data, blocking=True) + + return api_message(request) + + +@HANDLERS.register(('Alexa.PlaybackController', 'Play')) +@extract_entity +@asyncio.coroutine +def async_api_play(hass, request, entity): + """Process a play request.""" + data = { + ATTR_ENTITY_ID: entity.entity_id + } + + yield from hass.services.async_call(entity.domain, SERVICE_MEDIA_PLAY, + data, blocking=True) + + return api_message(request) + + +@HANDLERS.register(('Alexa.PlaybackController', 'Pause')) +@extract_entity +@asyncio.coroutine +def async_api_pause(hass, request, entity): + """Process a pause request.""" + data = { + ATTR_ENTITY_ID: entity.entity_id + } + + yield from hass.services.async_call(entity.domain, SERVICE_MEDIA_PAUSE, + data, blocking=True) + + return api_message(request) + + +@HANDLERS.register(('Alexa.PlaybackController', 'Stop')) +@extract_entity +@asyncio.coroutine +def async_api_stop(hass, request, entity): + """Process a stop request.""" + data = { + ATTR_ENTITY_ID: entity.entity_id + } + + yield from hass.services.async_call(entity.domain, SERVICE_MEDIA_STOP, + data, blocking=True) + + return api_message(request) + + +@HANDLERS.register(('Alexa.PlaybackController', 'Next')) +@extract_entity +@asyncio.coroutine +def async_api_next(hass, request, entity): + """Process a next request.""" + data = { + ATTR_ENTITY_ID: entity.entity_id + } + + yield from hass.services.async_call(entity.domain, + SERVICE_MEDIA_NEXT_TRACK, + data, blocking=True) + + return api_message(request) + + +@HANDLERS.register(('Alexa.PlaybackController', 'Previous')) +@extract_entity +@asyncio.coroutine +def async_api_previous(hass, request, entity): + """Process a previous request.""" + data = { + ATTR_ENTITY_ID: entity.entity_id + } + + yield from hass.services.async_call(entity.domain, + SERVICE_MEDIA_PREVIOUS_TRACK, + data, blocking=True) + + return api_message(request) diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index eadb72f91c0..3fe9145f2d6 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -99,7 +99,7 @@ def test_discovery_request(hass): """Test alexa discovery request.""" request = get_new_request('Alexa.Discovery', 'Discover') - # settup test devices + # setup test devices hass.states.async_set( 'switch.test', 'on', {'friendly_name': "Test switch"}) @@ -117,12 +117,52 @@ def test_discovery_request(hass): hass.states.async_set( 'script.test', 'off', {'friendly_name': "Test script"}) + hass.states.async_set( + 'input_boolean.test', 'off', {'friendly_name': "Test input boolean"}) + + hass.states.async_set( + 'scene.test', 'off', {'friendly_name': "Test scene"}) + + hass.states.async_set( + 'fan.test_1', 'off', {'friendly_name': "Test fan 1"}) + + hass.states.async_set( + 'fan.test_2', 'off', { + 'friendly_name': "Test fan 2", 'supported_features': 1, + 'speed_list': ['low', 'medium', 'high'] + }) + + hass.states.async_set( + 'lock.test', 'off', {'friendly_name': "Test lock"}) + + hass.states.async_set( + 'media_player.test', 'off', { + 'friendly_name': "Test media player", + 'supported_features': 20925, + 'volume_level': 1 + }) + + hass.states.async_set( + 'alert.test', 'off', {'friendly_name': "Test alert"}) + + hass.states.async_set( + 'automation.test', 'off', {'friendly_name': "Test automation"}) + + hass.states.async_set( + 'group.test', 'off', {'friendly_name': "Test group"}) + + hass.states.async_set( + 'cover.test', 'off', { + 'friendly_name': "Test cover", 'supported_features': 255, + 'position': 85 + }) + msg = yield from smart_home.async_handle_message(hass, request) assert 'event' in msg msg = msg['event'] - assert len(msg['payload']['endpoints']) == 5 + assert len(msg['payload']['endpoints']) == 15 assert msg['header']['name'] == 'Discover.Response' assert msg['header']['namespace'] == 'Alexa.Discovery' @@ -174,13 +214,108 @@ def test_discovery_request(hass): continue if appliance['endpointId'] == 'script#test': - assert appliance['displayCategories'][0] == "SWITCH" + assert appliance['displayCategories'][0] == "OTHER" assert appliance['friendlyName'] == "Test script" assert len(appliance['capabilities']) == 1 assert appliance['capabilities'][-1]['interface'] == \ 'Alexa.PowerController' continue + if appliance['endpointId'] == 'input_boolean#test': + assert appliance['displayCategories'][0] == "OTHER" + assert appliance['friendlyName'] == "Test input boolean" + assert len(appliance['capabilities']) == 1 + assert appliance['capabilities'][-1]['interface'] == \ + 'Alexa.PowerController' + continue + + if appliance['endpointId'] == 'scene#test': + assert appliance['displayCategories'][0] == "ACTIVITY_TRIGGER" + assert appliance['friendlyName'] == "Test scene" + assert len(appliance['capabilities']) == 1 + assert appliance['capabilities'][-1]['interface'] == \ + 'Alexa.SceneController' + continue + + if appliance['endpointId'] == 'fan#test_1': + assert appliance['displayCategories'][0] == "OTHER" + assert appliance['friendlyName'] == "Test fan 1" + assert len(appliance['capabilities']) == 1 + assert appliance['capabilities'][-1]['interface'] == \ + 'Alexa.PowerController' + continue + + if appliance['endpointId'] == 'fan#test_2': + assert appliance['displayCategories'][0] == "OTHER" + assert appliance['friendlyName'] == "Test fan 2" + assert len(appliance['capabilities']) == 2 + + caps = set() + for feature in appliance['capabilities']: + caps.add(feature['interface']) + + assert 'Alexa.PercentageController' in caps + assert 'Alexa.PowerController' in caps + continue + + if appliance['endpointId'] == 'lock#test': + assert appliance['displayCategories'][0] == "SMARTLOCK" + assert appliance['friendlyName'] == "Test lock" + assert len(appliance['capabilities']) == 1 + assert appliance['capabilities'][-1]['interface'] == \ + 'Alexa.LockController' + continue + + if appliance['endpointId'] == 'media_player#test': + assert appliance['displayCategories'][0] == "TV" + assert appliance['friendlyName'] == "Test media player" + assert len(appliance['capabilities']) == 3 + caps = set() + for feature in appliance['capabilities']: + caps.add(feature['interface']) + + assert 'Alexa.PowerController' in caps + assert 'Alexa.Speaker' in caps + assert 'Alexa.PlaybackController' in caps + continue + + if appliance['endpointId'] == 'alert#test': + assert appliance['displayCategories'][0] == "OTHER" + assert appliance['friendlyName'] == "Test alert" + assert len(appliance['capabilities']) == 1 + assert appliance['capabilities'][-1]['interface'] == \ + 'Alexa.PowerController' + continue + + if appliance['endpointId'] == 'automation#test': + assert appliance['displayCategories'][0] == "OTHER" + assert appliance['friendlyName'] == "Test automation" + assert len(appliance['capabilities']) == 1 + assert appliance['capabilities'][-1]['interface'] == \ + 'Alexa.PowerController' + continue + + if appliance['endpointId'] == 'group#test': + assert appliance['displayCategories'][0] == "OTHER" + assert appliance['friendlyName'] == "Test group" + assert len(appliance['capabilities']) == 1 + assert appliance['capabilities'][-1]['interface'] == \ + 'Alexa.PowerController' + continue + + if appliance['endpointId'] == 'cover#test': + assert appliance['displayCategories'][0] == "DOOR" + assert appliance['friendlyName'] == "Test cover" + assert len(appliance['capabilities']) == 2 + + caps = set() + for feature in appliance['capabilities']: + caps.add(feature['interface']) + + assert 'Alexa.PercentageController' in caps + assert 'Alexa.PowerController' in caps + continue + raise AssertionError("Unknown appliance!") @@ -217,19 +352,21 @@ def test_api_function_not_implemented(hass): @asyncio.coroutine -@pytest.mark.parametrize("domain", ['light', 'switch', 'script']) +@pytest.mark.parametrize("domain", ['alert', 'automation', 'group', + 'input_boolean', 'light', 'script', + 'switch']) def test_api_turn_on(hass, domain): """Test api turn on process.""" request = get_new_request( 'Alexa.PowerController', 'TurnOn', '{}#test'.format(domain)) - # settup test devices + # setup test devices hass.states.async_set( '{}.test'.format(domain), 'off', { 'friendly_name': "Test {}".format(domain) }) - call = async_mock_service(hass, domain, 'turn_on') + call = async_mock_service(hass, 'homeassistant', 'turn_on') msg = yield from smart_home.async_handle_message(hass, request) @@ -242,19 +379,21 @@ def test_api_turn_on(hass, domain): @asyncio.coroutine -@pytest.mark.parametrize("domain", ['light', 'switch', 'script']) +@pytest.mark.parametrize("domain", ['alert', 'automation', 'group', + 'input_boolean', 'light', 'script', + 'switch']) def test_api_turn_off(hass, domain): """Test api turn on process.""" request = get_new_request( 'Alexa.PowerController', 'TurnOff', '{}#test'.format(domain)) - # settup test devices + # setup test devices hass.states.async_set( '{}.test'.format(domain), 'on', { 'friendly_name': "Test {}".format(domain) }) - call = async_mock_service(hass, domain, 'turn_off') + call = async_mock_service(hass, 'homeassistant', 'turn_off') msg = yield from smart_home.async_handle_message(hass, request) @@ -275,7 +414,7 @@ def test_api_set_brightness(hass): # add payload request['directive']['payload']['brightness'] = '50' - # settup test devices + # setup test devices hass.states.async_set( 'light.test', 'off', {'friendly_name': "Test light"}) @@ -303,7 +442,7 @@ def test_api_adjust_brightness(hass, result, adjust): # add payload request['directive']['payload']['brightnessDelta'] = adjust - # settup test devices + # setup test devices hass.states.async_set( 'light.test', 'off', { 'friendly_name': "Test light", 'brightness': '77' @@ -335,7 +474,7 @@ def test_api_set_color_rgb(hass): 'brightness': '0.342', } - # settup test devices + # setup test devices hass.states.async_set( 'light.test', 'off', { 'friendly_name': "Test light", @@ -368,7 +507,7 @@ def test_api_set_color_xy(hass): 'brightness': '0.342', } - # settup test devices + # setup test devices hass.states.async_set( 'light.test', 'off', { 'friendly_name': "Test light", @@ -399,7 +538,7 @@ def test_api_set_color_temperature(hass): # add payload request['directive']['payload']['colorTemperatureInKelvin'] = '7500' - # settup test devices + # setup test devices hass.states.async_set( 'light.test', 'off', {'friendly_name': "Test light"}) @@ -424,7 +563,7 @@ def test_api_decrease_color_temp(hass, result, initial): 'Alexa.ColorTemperatureController', 'DecreaseColorTemperature', 'light#test') - # settup test devices + # setup test devices hass.states.async_set( 'light.test', 'off', { 'friendly_name': "Test light", 'color_temp': initial, @@ -452,7 +591,7 @@ def test_api_increase_color_temp(hass, result, initial): 'Alexa.ColorTemperatureController', 'IncreaseColorTemperature', 'light#test') - # settup test devices + # setup test devices hass.states.async_set( 'light.test', 'off', { 'friendly_name': "Test light", 'color_temp': initial, @@ -470,3 +609,378 @@ def test_api_increase_color_temp(hass, result, initial): assert call_light[0].data['entity_id'] == 'light.test' assert call_light[0].data['color_temp'] == result assert msg['header']['name'] == 'Response' + + +@asyncio.coroutine +@pytest.mark.parametrize("domain", ['scene']) +def test_api_activate(hass, domain): + """Test api activate process.""" + request = get_new_request( + 'Alexa.SceneController', 'Activate', '{}#test'.format(domain)) + + # setup test devices + hass.states.async_set( + '{}.test'.format(domain), 'off', { + 'friendly_name': "Test {}".format(domain) + }) + + call = async_mock_service(hass, domain, 'turn_on') + + msg = yield from smart_home.async_handle_message(hass, request) + + assert 'event' in msg + msg = msg['event'] + + assert len(call) == 1 + assert call[0].data['entity_id'] == '{}.test'.format(domain) + assert msg['header']['name'] == 'Response' + + +@asyncio.coroutine +def test_api_set_percentage_fan(hass): + """Test api set percentage for fan process.""" + request = get_new_request( + 'Alexa.PercentageController', 'SetPercentage', 'fan#test_2') + + # add payload + request['directive']['payload']['percentage'] = '50' + + # setup test devices + hass.states.async_set( + 'fan.test_2', 'off', {'friendly_name': "Test fan"}) + + call_fan = async_mock_service(hass, 'fan', 'set_speed') + + msg = yield from smart_home.async_handle_message(hass, request) + + assert 'event' in msg + msg = msg['event'] + + assert len(call_fan) == 1 + assert call_fan[0].data['entity_id'] == 'fan.test_2' + assert call_fan[0].data['speed'] == 'medium' + assert msg['header']['name'] == 'Response' + + +@asyncio.coroutine +def test_api_set_percentage_cover(hass): + """Test api set percentage for cover process.""" + request = get_new_request( + 'Alexa.PercentageController', 'SetPercentage', 'cover#test') + + # add payload + request['directive']['payload']['percentage'] = '50' + + # setup test devices + hass.states.async_set( + 'cover.test', 'closed', { + 'friendly_name': "Test cover" + }) + + call_cover = async_mock_service(hass, 'cover', 'set_cover_position') + + msg = yield from smart_home.async_handle_message(hass, request) + + assert 'event' in msg + msg = msg['event'] + + assert len(call_cover) == 1 + assert call_cover[0].data['entity_id'] == 'cover.test' + assert call_cover[0].data['position'] == 50 + assert msg['header']['name'] == 'Response' + + +@asyncio.coroutine +@pytest.mark.parametrize( + "result,adjust", [('high', '-5'), ('off', '5'), ('low', '-80')]) +def test_api_adjust_percentage_fan(hass, result, adjust): + """Test api adjust percentage for fan process.""" + request = get_new_request( + 'Alexa.PercentageController', 'AdjustPercentage', 'fan#test_2') + + # add payload + request['directive']['payload']['percentageDelta'] = adjust + + # setup test devices + hass.states.async_set( + 'fan.test_2', 'on', { + 'friendly_name': "Test fan 2", 'speed': 'high' + }) + + call_fan = async_mock_service(hass, 'fan', 'set_speed') + + msg = yield from smart_home.async_handle_message(hass, request) + + assert 'event' in msg + msg = msg['event'] + + assert len(call_fan) == 1 + assert call_fan[0].data['entity_id'] == 'fan.test_2' + assert call_fan[0].data['speed'] == result + assert msg['header']['name'] == 'Response' + + +@asyncio.coroutine +@pytest.mark.parametrize( + "result,adjust", [(25, '-5'), (35, '5'), (0, '-80')]) +def test_api_adjust_percentage_cover(hass, result, adjust): + """Test api adjust percentage for cover process.""" + request = get_new_request( + 'Alexa.PercentageController', 'AdjustPercentage', 'cover#test') + + # add payload + request['directive']['payload']['percentageDelta'] = adjust + + # setup test devices + hass.states.async_set( + 'cover.test', 'closed', { + 'friendly_name': "Test cover", + 'position': 30 + }) + + call_cover = async_mock_service(hass, 'cover', 'set_cover_position') + + msg = yield from smart_home.async_handle_message(hass, request) + + assert 'event' in msg + msg = msg['event'] + + assert len(call_cover) == 1 + assert call_cover[0].data['entity_id'] == 'cover.test' + assert call_cover[0].data['position'] == result + assert msg['header']['name'] == 'Response' + + +@asyncio.coroutine +@pytest.mark.parametrize("domain", ['lock']) +def test_api_lock(hass, domain): + """Test api lock process.""" + request = get_new_request( + 'Alexa.LockController', 'Lock', '{}#test'.format(domain)) + + # setup test devices + hass.states.async_set( + '{}.test'.format(domain), 'off', { + 'friendly_name': "Test {}".format(domain) + }) + + call = async_mock_service(hass, domain, 'lock') + + msg = yield from smart_home.async_handle_message(hass, request) + + assert 'event' in msg + msg = msg['event'] + + assert len(call) == 1 + assert call[0].data['entity_id'] == '{}.test'.format(domain) + assert msg['header']['name'] == 'Response' + + +@asyncio.coroutine +@pytest.mark.parametrize("domain", ['media_player']) +def test_api_play(hass, domain): + """Test api play process.""" + request = get_new_request( + 'Alexa.PlaybackController', 'Play', '{}#test'.format(domain)) + + # setup test devices + hass.states.async_set( + '{}.test'.format(domain), 'off', { + 'friendly_name': "Test {}".format(domain) + }) + + call = async_mock_service(hass, domain, 'media_play') + + msg = yield from smart_home.async_handle_message(hass, request) + + assert 'event' in msg + msg = msg['event'] + + assert len(call) == 1 + assert call[0].data['entity_id'] == '{}.test'.format(domain) + assert msg['header']['name'] == 'Response' + + +@asyncio.coroutine +@pytest.mark.parametrize("domain", ['media_player']) +def test_api_pause(hass, domain): + """Test api pause process.""" + request = get_new_request( + 'Alexa.PlaybackController', 'Pause', '{}#test'.format(domain)) + + # setup test devices + hass.states.async_set( + '{}.test'.format(domain), 'off', { + 'friendly_name': "Test {}".format(domain) + }) + + call = async_mock_service(hass, domain, 'media_pause') + + msg = yield from smart_home.async_handle_message(hass, request) + + assert 'event' in msg + msg = msg['event'] + + assert len(call) == 1 + assert call[0].data['entity_id'] == '{}.test'.format(domain) + assert msg['header']['name'] == 'Response' + + +@asyncio.coroutine +@pytest.mark.parametrize("domain", ['media_player']) +def test_api_stop(hass, domain): + """Test api stop process.""" + request = get_new_request( + 'Alexa.PlaybackController', 'Stop', '{}#test'.format(domain)) + + # setup test devices + hass.states.async_set( + '{}.test'.format(domain), 'off', { + 'friendly_name': "Test {}".format(domain) + }) + + call = async_mock_service(hass, domain, 'media_stop') + + msg = yield from smart_home.async_handle_message(hass, request) + + assert 'event' in msg + msg = msg['event'] + + assert len(call) == 1 + assert call[0].data['entity_id'] == '{}.test'.format(domain) + assert msg['header']['name'] == 'Response' + + +@asyncio.coroutine +@pytest.mark.parametrize("domain", ['media_player']) +def test_api_next(hass, domain): + """Test api next process.""" + request = get_new_request( + 'Alexa.PlaybackController', 'Next', '{}#test'.format(domain)) + + # setup test devices + hass.states.async_set( + '{}.test'.format(domain), 'off', { + 'friendly_name': "Test {}".format(domain) + }) + + call = async_mock_service(hass, domain, 'media_next_track') + + msg = yield from smart_home.async_handle_message(hass, request) + + assert 'event' in msg + msg = msg['event'] + + assert len(call) == 1 + assert call[0].data['entity_id'] == '{}.test'.format(domain) + assert msg['header']['name'] == 'Response' + + +@asyncio.coroutine +@pytest.mark.parametrize("domain", ['media_player']) +def test_api_previous(hass, domain): + """Test api previous process.""" + request = get_new_request( + 'Alexa.PlaybackController', 'Previous', '{}#test'.format(domain)) + + # setup test devices + hass.states.async_set( + '{}.test'.format(domain), 'off', { + 'friendly_name': "Test {}".format(domain) + }) + + call = async_mock_service(hass, domain, 'media_previous_track') + + msg = yield from smart_home.async_handle_message(hass, request) + + assert 'event' in msg + msg = msg['event'] + + assert len(call) == 1 + assert call[0].data['entity_id'] == '{}.test'.format(domain) + assert msg['header']['name'] == 'Response' + + +@asyncio.coroutine +def test_api_set_volume(hass): + """Test api set volume process.""" + request = get_new_request( + 'Alexa.Speaker', 'SetVolume', 'media_player#test') + + # add payload + request['directive']['payload']['volume'] = 50 + + # setup test devices + hass.states.async_set( + 'media_player.test', 'off', { + 'friendly_name': "Test media player", 'volume_level': 0 + }) + + call_media_player = async_mock_service(hass, 'media_player', 'volume_set') + + msg = yield from smart_home.async_handle_message(hass, request) + + assert 'event' in msg + msg = msg['event'] + + assert len(call_media_player) == 1 + assert call_media_player[0].data['entity_id'] == 'media_player.test' + assert call_media_player[0].data['volume_level'] == 0.5 + assert msg['header']['name'] == 'Response' + + +@asyncio.coroutine +@pytest.mark.parametrize( + "result,adjust", [(0.7, '-5'), (0.8, '5'), (0, '-80')]) +def test_api_adjust_volume(hass, result, adjust): + """Test api adjust volume process.""" + request = get_new_request( + 'Alexa.Speaker', 'AdjustVolume', 'media_player#test') + + # add payload + request['directive']['payload']['volume'] = adjust + + # setup test devices + hass.states.async_set( + 'media_player.test', 'off', { + 'friendly_name': "Test media player", 'volume_level': 0.75 + }) + + call_media_player = async_mock_service(hass, 'media_player', 'volume_set') + + msg = yield from smart_home.async_handle_message(hass, request) + + assert 'event' in msg + msg = msg['event'] + + assert len(call_media_player) == 1 + assert call_media_player[0].data['entity_id'] == 'media_player.test' + assert call_media_player[0].data['volume_level'] == result + assert msg['header']['name'] == 'Response' + + +@asyncio.coroutine +@pytest.mark.parametrize("domain", ['media_player']) +def test_api_mute(hass, domain): + """Test api mute process.""" + request = get_new_request( + 'Alexa.Speaker', 'SetMute', '{}#test'.format(domain)) + + request['directive']['payload']['mute'] = True + + # setup test devices + hass.states.async_set( + '{}.test'.format(domain), 'off', { + 'friendly_name': "Test {}".format(domain) + }) + + call = async_mock_service(hass, domain, 'volume_mute') + + msg = yield from smart_home.async_handle_message(hass, request) + + assert 'event' in msg + msg = msg['event'] + + assert len(call) == 1 + assert call[0].data['entity_id'] == '{}.test'.format(domain) + assert msg['header']['name'] == 'Response' From b86110a15d6d93ea06707f3b00d338b9ea8e55a1 Mon Sep 17 00:00:00 2001 From: Andrey Date: Fri, 17 Nov 2017 18:36:18 +0200 Subject: [PATCH 06/14] Print entity type in "too slow" warnings (#10641) * Update entity.py * Update entity.py --- homeassistant/helpers/entity.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 4a967e50995..78db0890ab1 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -235,10 +235,10 @@ class Entity(object): if not self._slow_reported and end - start > 0.4: self._slow_reported = True - _LOGGER.warning("Updating state for %s took %.3f seconds. " + _LOGGER.warning("Updating state for %s (%s) took %.3f seconds. " "Please report platform to the developers at " "https://goo.gl/Nvioub", self.entity_id, - end - start) + type(self), end - start) # Overwrite properties that have been set in the config file. if DATA_CUSTOMIZE in self.hass.data: From 35699273da5dabbf48659f56020250cd54a90862 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20St=C3=A5hl?= Date: Sat, 18 Nov 2017 00:00:15 +0100 Subject: [PATCH 07/14] Bump pyatv to 0.3.8 (#10643) Fixes AirPlay issues on newer versions of tvOS. --- homeassistant/components/apple_tv.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/apple_tv.py b/homeassistant/components/apple_tv.py index 6e38f172c4c..c8eb1841c0d 100644 --- a/homeassistant/components/apple_tv.py +++ b/homeassistant/components/apple_tv.py @@ -18,7 +18,7 @@ from homeassistant.helpers import discovery from homeassistant.components.discovery import SERVICE_APPLE_TV import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['pyatv==0.3.6'] +REQUIREMENTS = ['pyatv==0.3.8'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 004535fd14f..405bafaf13a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -612,7 +612,7 @@ pyasn1-modules==0.1.5 pyasn1==0.3.7 # homeassistant.components.apple_tv -pyatv==0.3.6 +pyatv==0.3.8 # homeassistant.components.device_tracker.bbox # homeassistant.components.sensor.bbox From 425c027085f859adb0e650aa2a15bbd689ccd537 Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Fri, 17 Nov 2017 21:10:24 -0800 Subject: [PATCH 08/14] Implement entity and domain exclude/include for Alexa (#10647) * Implement entity and domain exclude/include for Alexa * Switch to using generate_filter * Use proper domain for turn on/off calls except for groups where we must use the generic homeassistant.turn_on/off * travis fixes * Untangle * Lint --- homeassistant/components/alexa/smart_home.py | 75 +++++---- homeassistant/components/cloud/__init__.py | 19 ++- homeassistant/components/cloud/iot.py | 4 +- homeassistant/components/mqtt_statestream.py | 12 +- homeassistant/helpers/config_validation.py | 16 +- homeassistant/helpers/entityfilter.py | 24 +++ tests/components/alexa/test_smart_home.py | 158 +++++++++++++++---- 7 files changed, 233 insertions(+), 75 deletions(-) diff --git a/homeassistant/components/alexa/smart_home.py b/homeassistant/components/alexa/smart_home.py index c5a849ad560..6e71fc67df1 100644 --- a/homeassistant/components/alexa/smart_home.py +++ b/homeassistant/components/alexa/smart_home.py @@ -1,5 +1,6 @@ """Support for alexa Smart Home Skill API.""" import asyncio +from collections import namedtuple import logging import math from uuid import uuid4 @@ -72,8 +73,11 @@ MAPPING_COMPONENT = { } +Config = namedtuple('AlexaConfig', 'filter') + + @asyncio.coroutine -def async_handle_message(hass, message): +def async_handle_message(hass, config, message): """Handle incoming API messages.""" assert message[API_DIRECTIVE][API_HEADER]['payloadVersion'] == '3' @@ -89,7 +93,7 @@ def async_handle_message(hass, message): "Unsupported API request %s/%s", namespace, name) return api_error(message) - return (yield from funct_ref(hass, message)) + return (yield from funct_ref(hass, config, message)) def api_message(request, name='Response', namespace='Alexa', payload=None): @@ -138,7 +142,7 @@ def api_error(request, error_type='INTERNAL_ERROR', error_message=""): @HANDLERS.register(('Alexa.Discovery', 'Discover')) @asyncio.coroutine -def async_api_discovery(hass, request): +def async_api_discovery(hass, config, request): """Create a API formatted discovery response. Async friendly. @@ -146,7 +150,14 @@ def async_api_discovery(hass, request): discovery_endpoints = [] for entity in hass.states.async_all(): + if not config.filter(entity.entity_id): + _LOGGER.debug("Not exposing %s because filtered by config", + entity.entity_id) + continue + if entity.attributes.get(ATTR_ALEXA_HIDDEN, False): + _LOGGER.debug("Not exposing %s because alexa_hidden is true", + entity.entity_id) continue class_data = MAPPING_COMPONENT.get(entity.domain) @@ -207,7 +218,7 @@ def async_api_discovery(hass, request): def extract_entity(funct): """Decorator for extract entity object from request.""" @asyncio.coroutine - def async_api_entity_wrapper(hass, request): + def async_api_entity_wrapper(hass, config, request): """Process a turn on request.""" entity_id = request[API_ENDPOINT]['endpointId'].replace('#', '.') @@ -218,7 +229,7 @@ def extract_entity(funct): request[API_HEADER]['name'], entity_id) return api_error(request, error_type='NO_SUCH_ENDPOINT') - return (yield from funct(hass, request, entity)) + return (yield from funct(hass, config, request, entity)) return async_api_entity_wrapper @@ -226,9 +237,13 @@ def extract_entity(funct): @HANDLERS.register(('Alexa.PowerController', 'TurnOn')) @extract_entity @asyncio.coroutine -def async_api_turn_on(hass, request, entity): +def async_api_turn_on(hass, config, request, entity): """Process a turn on request.""" - yield from hass.services.async_call(ha.DOMAIN, SERVICE_TURN_ON, { + domain = entity.domain + if entity.domain == group.DOMAIN: + domain = ha.DOMAIN + + yield from hass.services.async_call(domain, SERVICE_TURN_ON, { ATTR_ENTITY_ID: entity.entity_id }, blocking=True) @@ -238,9 +253,13 @@ def async_api_turn_on(hass, request, entity): @HANDLERS.register(('Alexa.PowerController', 'TurnOff')) @extract_entity @asyncio.coroutine -def async_api_turn_off(hass, request, entity): +def async_api_turn_off(hass, config, request, entity): """Process a turn off request.""" - yield from hass.services.async_call(ha.DOMAIN, SERVICE_TURN_OFF, { + domain = entity.domain + if entity.domain == group.DOMAIN: + domain = ha.DOMAIN + + yield from hass.services.async_call(domain, SERVICE_TURN_OFF, { ATTR_ENTITY_ID: entity.entity_id }, blocking=True) @@ -250,7 +269,7 @@ def async_api_turn_off(hass, request, entity): @HANDLERS.register(('Alexa.BrightnessController', 'SetBrightness')) @extract_entity @asyncio.coroutine -def async_api_set_brightness(hass, request, entity): +def async_api_set_brightness(hass, config, request, entity): """Process a set brightness request.""" brightness = int(request[API_PAYLOAD]['brightness']) @@ -265,7 +284,7 @@ def async_api_set_brightness(hass, request, entity): @HANDLERS.register(('Alexa.BrightnessController', 'AdjustBrightness')) @extract_entity @asyncio.coroutine -def async_api_adjust_brightness(hass, request, entity): +def async_api_adjust_brightness(hass, config, request, entity): """Process a adjust brightness request.""" brightness_delta = int(request[API_PAYLOAD]['brightnessDelta']) @@ -289,7 +308,7 @@ def async_api_adjust_brightness(hass, request, entity): @HANDLERS.register(('Alexa.ColorController', 'SetColor')) @extract_entity @asyncio.coroutine -def async_api_set_color(hass, request, entity): +def async_api_set_color(hass, config, request, entity): """Process a set color request.""" supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES) rgb = color_util.color_hsb_to_RGB( @@ -317,7 +336,7 @@ def async_api_set_color(hass, request, entity): @HANDLERS.register(('Alexa.ColorTemperatureController', 'SetColorTemperature')) @extract_entity @asyncio.coroutine -def async_api_set_color_temperature(hass, request, entity): +def async_api_set_color_temperature(hass, config, request, entity): """Process a set color temperature request.""" kelvin = int(request[API_PAYLOAD]['colorTemperatureInKelvin']) @@ -333,7 +352,7 @@ def async_api_set_color_temperature(hass, request, entity): ('Alexa.ColorTemperatureController', 'DecreaseColorTemperature')) @extract_entity @asyncio.coroutine -def async_api_decrease_color_temp(hass, request, entity): +def async_api_decrease_color_temp(hass, config, request, entity): """Process a decrease color temperature request.""" current = int(entity.attributes.get(light.ATTR_COLOR_TEMP)) max_mireds = int(entity.attributes.get(light.ATTR_MAX_MIREDS)) @@ -351,7 +370,7 @@ def async_api_decrease_color_temp(hass, request, entity): ('Alexa.ColorTemperatureController', 'IncreaseColorTemperature')) @extract_entity @asyncio.coroutine -def async_api_increase_color_temp(hass, request, entity): +def async_api_increase_color_temp(hass, config, request, entity): """Process a increase color temperature request.""" current = int(entity.attributes.get(light.ATTR_COLOR_TEMP)) min_mireds = int(entity.attributes.get(light.ATTR_MIN_MIREDS)) @@ -368,7 +387,7 @@ def async_api_increase_color_temp(hass, request, entity): @HANDLERS.register(('Alexa.SceneController', 'Activate')) @extract_entity @asyncio.coroutine -def async_api_activate(hass, request, entity): +def async_api_activate(hass, config, request, entity): """Process a activate request.""" yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, { ATTR_ENTITY_ID: entity.entity_id @@ -380,7 +399,7 @@ def async_api_activate(hass, request, entity): @HANDLERS.register(('Alexa.PercentageController', 'SetPercentage')) @extract_entity @asyncio.coroutine -def async_api_set_percentage(hass, request, entity): +def async_api_set_percentage(hass, config, request, entity): """Process a set percentage request.""" percentage = int(request[API_PAYLOAD]['percentage']) service = None @@ -411,7 +430,7 @@ def async_api_set_percentage(hass, request, entity): @HANDLERS.register(('Alexa.PercentageController', 'AdjustPercentage')) @extract_entity @asyncio.coroutine -def async_api_adjust_percentage(hass, request, entity): +def async_api_adjust_percentage(hass, config, request, entity): """Process a adjust percentage request.""" percentage_delta = int(request[API_PAYLOAD]['percentageDelta']) service = None @@ -459,7 +478,7 @@ def async_api_adjust_percentage(hass, request, entity): @HANDLERS.register(('Alexa.LockController', 'Lock')) @extract_entity @asyncio.coroutine -def async_api_lock(hass, request, entity): +def async_api_lock(hass, config, request, entity): """Process a lock request.""" yield from hass.services.async_call(entity.domain, SERVICE_LOCK, { ATTR_ENTITY_ID: entity.entity_id @@ -472,7 +491,7 @@ def async_api_lock(hass, request, entity): @HANDLERS.register(('Alexa.LockController', 'Unlock')) @extract_entity @asyncio.coroutine -def async_api_unlock(hass, request, entity): +def async_api_unlock(hass, config, request, entity): """Process a unlock request.""" yield from hass.services.async_call(entity.domain, SERVICE_UNLOCK, { ATTR_ENTITY_ID: entity.entity_id @@ -484,7 +503,7 @@ def async_api_unlock(hass, request, entity): @HANDLERS.register(('Alexa.Speaker', 'SetVolume')) @extract_entity @asyncio.coroutine -def async_api_set_volume(hass, request, entity): +def async_api_set_volume(hass, config, request, entity): """Process a set volume request.""" volume = round(float(request[API_PAYLOAD]['volume'] / 100), 2) @@ -502,7 +521,7 @@ def async_api_set_volume(hass, request, entity): @HANDLERS.register(('Alexa.Speaker', 'AdjustVolume')) @extract_entity @asyncio.coroutine -def async_api_adjust_volume(hass, request, entity): +def async_api_adjust_volume(hass, config, request, entity): """Process a adjust volume request.""" volume_delta = int(request[API_PAYLOAD]['volume']) @@ -531,7 +550,7 @@ def async_api_adjust_volume(hass, request, entity): @HANDLERS.register(('Alexa.Speaker', 'SetMute')) @extract_entity @asyncio.coroutine -def async_api_set_mute(hass, request, entity): +def async_api_set_mute(hass, config, request, entity): """Process a set mute request.""" mute = bool(request[API_PAYLOAD]['mute']) @@ -550,7 +569,7 @@ def async_api_set_mute(hass, request, entity): @HANDLERS.register(('Alexa.PlaybackController', 'Play')) @extract_entity @asyncio.coroutine -def async_api_play(hass, request, entity): +def async_api_play(hass, config, request, entity): """Process a play request.""" data = { ATTR_ENTITY_ID: entity.entity_id @@ -565,7 +584,7 @@ def async_api_play(hass, request, entity): @HANDLERS.register(('Alexa.PlaybackController', 'Pause')) @extract_entity @asyncio.coroutine -def async_api_pause(hass, request, entity): +def async_api_pause(hass, config, request, entity): """Process a pause request.""" data = { ATTR_ENTITY_ID: entity.entity_id @@ -580,7 +599,7 @@ def async_api_pause(hass, request, entity): @HANDLERS.register(('Alexa.PlaybackController', 'Stop')) @extract_entity @asyncio.coroutine -def async_api_stop(hass, request, entity): +def async_api_stop(hass, config, request, entity): """Process a stop request.""" data = { ATTR_ENTITY_ID: entity.entity_id @@ -595,7 +614,7 @@ def async_api_stop(hass, request, entity): @HANDLERS.register(('Alexa.PlaybackController', 'Next')) @extract_entity @asyncio.coroutine -def async_api_next(hass, request, entity): +def async_api_next(hass, config, request, entity): """Process a next request.""" data = { ATTR_ENTITY_ID: entity.entity_id @@ -611,7 +630,7 @@ def async_api_next(hass, request, entity): @HANDLERS.register(('Alexa.PlaybackController', 'Previous')) @extract_entity @asyncio.coroutine -def async_api_previous(hass, request, entity): +def async_api_previous(hass, config, request, entity): """Process a previous request.""" data = { ATTR_ENTITY_ID: entity.entity_id diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index 2d01399bc07..e6da2de40f2 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -9,7 +9,9 @@ import voluptuous as vol from homeassistant.const import ( EVENT_HOMEASSISTANT_START, CONF_REGION, CONF_MODE) +from homeassistant.helpers import entityfilter from homeassistant.util import dt as dt_util +from homeassistant.components.alexa import smart_home from . import http_api, iot from .const import CONFIG_DIR, DOMAIN, SERVERS @@ -18,6 +20,8 @@ REQUIREMENTS = ['warrant==0.5.0'] _LOGGER = logging.getLogger(__name__) +CONF_ALEXA = 'alexa' +CONF_ALEXA_FILTER = 'filter' CONF_COGNITO_CLIENT_ID = 'cognito_client_id' CONF_RELAYER = 'relayer' CONF_USER_POOL_ID = 'user_pool_id' @@ -26,6 +30,13 @@ MODE_DEV = 'development' DEFAULT_MODE = MODE_DEV DEPENDENCIES = ['http'] +ALEXA_SCHEMA = vol.Schema({ + vol.Optional( + CONF_ALEXA_FILTER, + default=lambda: entityfilter.generate_filter([], [], [], []) + ): entityfilter.FILTER_SCHEMA, +}) + CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Optional(CONF_MODE, default=DEFAULT_MODE): @@ -35,6 +46,7 @@ CONFIG_SCHEMA = vol.Schema({ vol.Required(CONF_USER_POOL_ID): str, vol.Required(CONF_REGION): str, vol.Required(CONF_RELAYER): str, + vol.Optional(CONF_ALEXA): ALEXA_SCHEMA }), }, extra=vol.ALLOW_EXTRA) @@ -47,6 +59,10 @@ def async_setup(hass, config): else: kwargs = {CONF_MODE: DEFAULT_MODE} + if CONF_ALEXA not in kwargs: + kwargs[CONF_ALEXA] = ALEXA_SCHEMA({}) + + kwargs[CONF_ALEXA] = smart_home.Config(**kwargs[CONF_ALEXA]) cloud = hass.data[DOMAIN] = Cloud(hass, **kwargs) @asyncio.coroutine @@ -64,10 +80,11 @@ class Cloud: """Store the configuration of the cloud connection.""" def __init__(self, hass, mode, cognito_client_id=None, user_pool_id=None, - region=None, relayer=None): + region=None, relayer=None, alexa=None): """Create an instance of Cloud.""" self.hass = hass self.mode = mode + self.alexa_config = alexa self.id_token = None self.access_token = None self.refresh_token = None diff --git a/homeassistant/components/cloud/iot.py b/homeassistant/components/cloud/iot.py index c0b6bb96da1..91ad1cfc6ff 100644 --- a/homeassistant/components/cloud/iot.py +++ b/homeassistant/components/cloud/iot.py @@ -206,7 +206,9 @@ def async_handle_message(hass, cloud, handler_name, payload): @asyncio.coroutine def async_handle_alexa(hass, cloud, payload): """Handle an incoming IoT message for Alexa.""" - return (yield from smart_home.async_handle_message(hass, payload)) + return (yield from smart_home.async_handle_message(hass, + cloud.alexa_config, + payload)) @HANDLERS.register('cloud') diff --git a/homeassistant/components/mqtt_statestream.py b/homeassistant/components/mqtt_statestream.py index fa1da879110..4427870c294 100644 --- a/homeassistant/components/mqtt_statestream.py +++ b/homeassistant/components/mqtt_statestream.py @@ -25,7 +25,17 @@ DEPENDENCIES = ['mqtt'] DOMAIN = 'mqtt_statestream' CONFIG_SCHEMA = vol.Schema({ - DOMAIN: cv.FILTER_SCHEMA.extend({ + DOMAIN: vol.Schema({ + vol.Optional(CONF_EXCLUDE, default={}): vol.Schema({ + vol.Optional(CONF_ENTITIES, default=[]): cv.entity_ids, + vol.Optional(CONF_DOMAINS, default=[]): + vol.All(cv.ensure_list, [cv.string]) + }), + vol.Optional(CONF_INCLUDE, default={}): vol.Schema({ + vol.Optional(CONF_ENTITIES, default=[]): cv.entity_ids, + vol.Optional(CONF_DOMAINS, default=[]): + vol.All(cv.ensure_list, [cv.string]) + }), vol.Required(CONF_BASE_TOPIC): valid_publish_topic, vol.Optional(CONF_PUBLISH_ATTRIBUTES, default=False): cv.boolean, vol.Optional(CONF_PUBLISH_TIMESTAMPS, default=False): cv.boolean diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index e5512b9140e..e5d0a34f76e 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -12,8 +12,7 @@ import voluptuous as vol from homeassistant.loader import get_platform from homeassistant.const import ( - CONF_DOMAINS, CONF_ENTITIES, CONF_EXCLUDE, CONF_INCLUDE, CONF_PLATFORM, - CONF_SCAN_INTERVAL, TEMP_CELSIUS, TEMP_FAHRENHEIT, + CONF_PLATFORM, CONF_SCAN_INTERVAL, TEMP_CELSIUS, TEMP_FAHRENHEIT, CONF_ALIAS, CONF_ENTITY_ID, CONF_VALUE_TEMPLATE, WEEKDAYS, CONF_CONDITION, CONF_BELOW, CONF_ABOVE, CONF_TIMEOUT, SUN_EVENT_SUNSET, SUN_EVENT_SUNRISE, CONF_UNIT_SYSTEM_IMPERIAL, CONF_UNIT_SYSTEM_METRIC) @@ -563,16 +562,3 @@ SCRIPT_SCHEMA = vol.All( [vol.Any(SERVICE_SCHEMA, _SCRIPT_DELAY_SCHEMA, _SCRIPT_WAIT_TEMPLATE_SCHEMA, EVENT_SCHEMA, CONDITION_SCHEMA)], ) - -FILTER_SCHEMA = vol.Schema({ - vol.Optional(CONF_EXCLUDE, default={}): vol.Schema({ - vol.Optional(CONF_ENTITIES, default=[]): entity_ids, - vol.Optional(CONF_DOMAINS, default=[]): - vol.All(ensure_list, [string]) - }), - vol.Optional(CONF_INCLUDE, default={}): vol.Schema({ - vol.Optional(CONF_ENTITIES, default=[]): entity_ids, - vol.Optional(CONF_DOMAINS, default=[]): - vol.All(ensure_list, [string]) - }) -}) diff --git a/homeassistant/helpers/entityfilter.py b/homeassistant/helpers/entityfilter.py index d8d3f1c9325..f78c70e57d3 100644 --- a/homeassistant/helpers/entityfilter.py +++ b/homeassistant/helpers/entityfilter.py @@ -1,6 +1,30 @@ """Helper class to implement include/exclude of entities and domains.""" +import voluptuous as vol + from homeassistant.core import split_entity_id +from homeassistant.helpers import config_validation as cv + +CONF_INCLUDE_DOMAINS = 'include_domains' +CONF_INCLUDE_ENTITIES = 'include_entities' +CONF_EXCLUDE_DOMAINS = 'exclude_domains' +CONF_EXCLUDE_ENTITIES = 'exclude_entities' + +FILTER_SCHEMA = vol.All( + vol.Schema({ + vol.Optional(CONF_EXCLUDE_DOMAINS, default=[]): + vol.All(cv.ensure_list, [cv.string]), + vol.Optional(CONF_EXCLUDE_ENTITIES, default=[]): cv.entity_ids, + vol.Optional(CONF_INCLUDE_DOMAINS, default=[]): + vol.All(cv.ensure_list, [cv.string]), + vol.Optional(CONF_INCLUDE_ENTITIES, default=[]): cv.entity_ids, + }), + lambda config: generate_filter( + config[CONF_INCLUDE_DOMAINS], + config[CONF_INCLUDE_ENTITIES], + config[CONF_EXCLUDE_DOMAINS], + config[CONF_EXCLUDE_ENTITIES], + )) def generate_filter(include_domains, include_entities, diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 3fe9145f2d6..55a412af1fd 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -5,9 +5,12 @@ from uuid import uuid4 import pytest from homeassistant.components.alexa import smart_home +from homeassistant.helpers import entityfilter from tests.common import async_mock_service +DEFAULT_CONFIG = smart_home.Config(filter=lambda entity_id: True) + def get_new_request(namespace, name, endpoint=None): """Generate a new API message.""" @@ -91,7 +94,7 @@ def test_wrong_version(hass): msg['directive']['header']['payloadVersion'] = '2' with pytest.raises(AssertionError): - yield from smart_home.async_handle_message(hass, msg) + yield from smart_home.async_handle_message(hass, DEFAULT_CONFIG, msg) @asyncio.coroutine @@ -157,7 +160,8 @@ def test_discovery_request(hass): 'position': 85 }) - msg = yield from smart_home.async_handle_message(hass, request) + msg = yield from smart_home.async_handle_message( + hass, DEFAULT_CONFIG, request) assert 'event' in msg msg = msg['event'] @@ -319,6 +323,67 @@ def test_discovery_request(hass): raise AssertionError("Unknown appliance!") +@asyncio.coroutine +def test_exclude_filters(hass): + """Test exclusion filters.""" + request = get_new_request('Alexa.Discovery', 'Discover') + + # setup test devices + hass.states.async_set( + 'switch.test', 'on', {'friendly_name': "Test switch"}) + + hass.states.async_set( + 'script.deny', 'off', {'friendly_name': "Blocked script"}) + + hass.states.async_set( + 'cover.deny', 'off', {'friendly_name': "Blocked cover"}) + + config = smart_home.Config(filter=entityfilter.generate_filter( + include_domains=[], + include_entities=[], + exclude_domains=['script'], + exclude_entities=['cover.deny'], + )) + + msg = yield from smart_home.async_handle_message(hass, config, request) + + msg = msg['event'] + + assert len(msg['payload']['endpoints']) == 1 + + +@asyncio.coroutine +def test_include_filters(hass): + """Test inclusion filters.""" + request = get_new_request('Alexa.Discovery', 'Discover') + + # setup test devices + hass.states.async_set( + 'switch.deny', 'on', {'friendly_name': "Blocked switch"}) + + hass.states.async_set( + 'script.deny', 'off', {'friendly_name': "Blocked script"}) + + hass.states.async_set( + 'automation.allow', 'off', {'friendly_name': "Allowed automation"}) + + hass.states.async_set( + 'group.allow', 'off', {'friendly_name': "Allowed group"}) + + config = smart_home.Config(filter=entityfilter.generate_filter( + include_domains=['automation', 'group'], + include_entities=['script.deny'], + exclude_domains=[], + exclude_entities=[], + )) + + msg = yield from smart_home.async_handle_message(hass, config, request) + + msg = msg['event'] + + assert len(msg['payload']['endpoints']) == 3 + + @asyncio.coroutine def test_api_entity_not_exists(hass): """Test api turn on process without entity.""" @@ -326,7 +391,8 @@ def test_api_entity_not_exists(hass): call_switch = async_mock_service(hass, 'switch', 'turn_on') - msg = yield from smart_home.async_handle_message(hass, request) + msg = yield from smart_home.async_handle_message( + hass, DEFAULT_CONFIG, request) assert 'event' in msg msg = msg['event'] @@ -341,7 +407,8 @@ def test_api_entity_not_exists(hass): def test_api_function_not_implemented(hass): """Test api call that is not implemented to us.""" request = get_new_request('Alexa.HAHAAH', 'Sweet') - msg = yield from smart_home.async_handle_message(hass, request) + msg = yield from smart_home.async_handle_message( + hass, DEFAULT_CONFIG, request) assert 'event' in msg msg = msg['event'] @@ -366,9 +433,15 @@ def test_api_turn_on(hass, domain): 'friendly_name': "Test {}".format(domain) }) - call = async_mock_service(hass, 'homeassistant', 'turn_on') + call_domain = domain - msg = yield from smart_home.async_handle_message(hass, request) + if domain == 'group': + call_domain = 'homeassistant' + + call = async_mock_service(hass, call_domain, 'turn_on') + + msg = yield from smart_home.async_handle_message( + hass, DEFAULT_CONFIG, request) assert 'event' in msg msg = msg['event'] @@ -393,9 +466,15 @@ def test_api_turn_off(hass, domain): 'friendly_name': "Test {}".format(domain) }) - call = async_mock_service(hass, 'homeassistant', 'turn_off') + call_domain = domain - msg = yield from smart_home.async_handle_message(hass, request) + if domain == 'group': + call_domain = 'homeassistant' + + call = async_mock_service(hass, call_domain, 'turn_off') + + msg = yield from smart_home.async_handle_message( + hass, DEFAULT_CONFIG, request) assert 'event' in msg msg = msg['event'] @@ -420,7 +499,8 @@ def test_api_set_brightness(hass): call_light = async_mock_service(hass, 'light', 'turn_on') - msg = yield from smart_home.async_handle_message(hass, request) + msg = yield from smart_home.async_handle_message( + hass, DEFAULT_CONFIG, request) assert 'event' in msg msg = msg['event'] @@ -450,7 +530,8 @@ def test_api_adjust_brightness(hass, result, adjust): call_light = async_mock_service(hass, 'light', 'turn_on') - msg = yield from smart_home.async_handle_message(hass, request) + msg = yield from smart_home.async_handle_message( + hass, DEFAULT_CONFIG, request) assert 'event' in msg msg = msg['event'] @@ -483,7 +564,8 @@ def test_api_set_color_rgb(hass): call_light = async_mock_service(hass, 'light', 'turn_on') - msg = yield from smart_home.async_handle_message(hass, request) + msg = yield from smart_home.async_handle_message( + hass, DEFAULT_CONFIG, request) assert 'event' in msg msg = msg['event'] @@ -516,7 +598,8 @@ def test_api_set_color_xy(hass): call_light = async_mock_service(hass, 'light', 'turn_on') - msg = yield from smart_home.async_handle_message(hass, request) + msg = yield from smart_home.async_handle_message( + hass, DEFAULT_CONFIG, request) assert 'event' in msg msg = msg['event'] @@ -544,7 +627,8 @@ def test_api_set_color_temperature(hass): call_light = async_mock_service(hass, 'light', 'turn_on') - msg = yield from smart_home.async_handle_message(hass, request) + msg = yield from smart_home.async_handle_message( + hass, DEFAULT_CONFIG, request) assert 'event' in msg msg = msg['event'] @@ -572,7 +656,8 @@ def test_api_decrease_color_temp(hass, result, initial): call_light = async_mock_service(hass, 'light', 'turn_on') - msg = yield from smart_home.async_handle_message(hass, request) + msg = yield from smart_home.async_handle_message( + hass, DEFAULT_CONFIG, request) assert 'event' in msg msg = msg['event'] @@ -600,7 +685,8 @@ def test_api_increase_color_temp(hass, result, initial): call_light = async_mock_service(hass, 'light', 'turn_on') - msg = yield from smart_home.async_handle_message(hass, request) + msg = yield from smart_home.async_handle_message( + hass, DEFAULT_CONFIG, request) assert 'event' in msg msg = msg['event'] @@ -626,7 +712,8 @@ def test_api_activate(hass, domain): call = async_mock_service(hass, domain, 'turn_on') - msg = yield from smart_home.async_handle_message(hass, request) + msg = yield from smart_home.async_handle_message( + hass, DEFAULT_CONFIG, request) assert 'event' in msg msg = msg['event'] @@ -651,7 +738,8 @@ def test_api_set_percentage_fan(hass): call_fan = async_mock_service(hass, 'fan', 'set_speed') - msg = yield from smart_home.async_handle_message(hass, request) + msg = yield from smart_home.async_handle_message( + hass, DEFAULT_CONFIG, request) assert 'event' in msg msg = msg['event'] @@ -679,7 +767,8 @@ def test_api_set_percentage_cover(hass): call_cover = async_mock_service(hass, 'cover', 'set_cover_position') - msg = yield from smart_home.async_handle_message(hass, request) + msg = yield from smart_home.async_handle_message( + hass, DEFAULT_CONFIG, request) assert 'event' in msg msg = msg['event'] @@ -709,7 +798,8 @@ def test_api_adjust_percentage_fan(hass, result, adjust): call_fan = async_mock_service(hass, 'fan', 'set_speed') - msg = yield from smart_home.async_handle_message(hass, request) + msg = yield from smart_home.async_handle_message( + hass, DEFAULT_CONFIG, request) assert 'event' in msg msg = msg['event'] @@ -740,7 +830,8 @@ def test_api_adjust_percentage_cover(hass, result, adjust): call_cover = async_mock_service(hass, 'cover', 'set_cover_position') - msg = yield from smart_home.async_handle_message(hass, request) + msg = yield from smart_home.async_handle_message( + hass, DEFAULT_CONFIG, request) assert 'event' in msg msg = msg['event'] @@ -766,7 +857,8 @@ def test_api_lock(hass, domain): call = async_mock_service(hass, domain, 'lock') - msg = yield from smart_home.async_handle_message(hass, request) + msg = yield from smart_home.async_handle_message( + hass, DEFAULT_CONFIG, request) assert 'event' in msg msg = msg['event'] @@ -791,7 +883,8 @@ def test_api_play(hass, domain): call = async_mock_service(hass, domain, 'media_play') - msg = yield from smart_home.async_handle_message(hass, request) + msg = yield from smart_home.async_handle_message( + hass, DEFAULT_CONFIG, request) assert 'event' in msg msg = msg['event'] @@ -816,7 +909,8 @@ def test_api_pause(hass, domain): call = async_mock_service(hass, domain, 'media_pause') - msg = yield from smart_home.async_handle_message(hass, request) + msg = yield from smart_home.async_handle_message( + hass, DEFAULT_CONFIG, request) assert 'event' in msg msg = msg['event'] @@ -841,7 +935,8 @@ def test_api_stop(hass, domain): call = async_mock_service(hass, domain, 'media_stop') - msg = yield from smart_home.async_handle_message(hass, request) + msg = yield from smart_home.async_handle_message( + hass, DEFAULT_CONFIG, request) assert 'event' in msg msg = msg['event'] @@ -866,7 +961,8 @@ def test_api_next(hass, domain): call = async_mock_service(hass, domain, 'media_next_track') - msg = yield from smart_home.async_handle_message(hass, request) + msg = yield from smart_home.async_handle_message( + hass, DEFAULT_CONFIG, request) assert 'event' in msg msg = msg['event'] @@ -891,7 +987,8 @@ def test_api_previous(hass, domain): call = async_mock_service(hass, domain, 'media_previous_track') - msg = yield from smart_home.async_handle_message(hass, request) + msg = yield from smart_home.async_handle_message( + hass, DEFAULT_CONFIG, request) assert 'event' in msg msg = msg['event'] @@ -918,7 +1015,8 @@ def test_api_set_volume(hass): call_media_player = async_mock_service(hass, 'media_player', 'volume_set') - msg = yield from smart_home.async_handle_message(hass, request) + msg = yield from smart_home.async_handle_message( + hass, DEFAULT_CONFIG, request) assert 'event' in msg msg = msg['event'] @@ -948,7 +1046,8 @@ def test_api_adjust_volume(hass, result, adjust): call_media_player = async_mock_service(hass, 'media_player', 'volume_set') - msg = yield from smart_home.async_handle_message(hass, request) + msg = yield from smart_home.async_handle_message( + hass, DEFAULT_CONFIG, request) assert 'event' in msg msg = msg['event'] @@ -976,7 +1075,8 @@ def test_api_mute(hass, domain): call = async_mock_service(hass, domain, 'volume_mute') - msg = yield from smart_home.async_handle_message(hass, request) + msg = yield from smart_home.async_handle_message( + hass, DEFAULT_CONFIG, request) assert 'event' in msg msg = msg['event'] From 34f06e8eef2ce580710b5a7bcc00b651056f2b5c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 20 Nov 2017 20:48:52 -0800 Subject: [PATCH 09/14] Bump frontend to 20171121.0 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index e7cfcf8d88c..3d83c524461 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -23,7 +23,7 @@ from homeassistant.const import CONF_NAME, EVENT_THEMES_UPDATED from homeassistant.core import callback from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20171118.0'] +REQUIREMENTS = ['home-assistant-frontend==20171121.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index 405bafaf13a..412405fab1c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -331,7 +331,7 @@ hipnotify==1.0.8 holidays==0.8.1 # homeassistant.components.frontend -home-assistant-frontend==20171118.0 +home-assistant-frontend==20171121.0 # homeassistant.components.camera.onvif http://github.com/tgaugry/suds-passworddigest-py3/archive/86fc50e39b4d2b8997481967d6a7fe1c57118999.zip#suds-passworddigest-py3==0.1.2a diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c9ea20494d4..ac39aef6e47 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -74,7 +74,7 @@ hbmqtt==0.9.1 holidays==0.8.1 # homeassistant.components.frontend -home-assistant-frontend==20171118.0 +home-assistant-frontend==20171121.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From d7f9be964080a569da2a08cc3fbbddeda8df9d55 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 20 Nov 2017 20:50:12 -0800 Subject: [PATCH 10/14] Version bump to 0.58.1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index d8b4dfcb044..706a3881831 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 58 -PATCH_VERSION = '0' +PATCH_VERSION = '1' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 4, 2) From 2ba5f1f45e513bce49cce575d1696ae8be310cb4 Mon Sep 17 00:00:00 2001 From: Lukas Barth Date: Sat, 18 Nov 2017 23:33:18 +0100 Subject: [PATCH 11/14] Fix yweather (#10661) --- homeassistant/components/weather/yweather.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/weather/yweather.py b/homeassistant/components/weather/yweather.py index 514eda0f09f..a043f3c2212 100644 --- a/homeassistant/components/weather/yweather.py +++ b/homeassistant/components/weather/yweather.py @@ -115,7 +115,7 @@ class YahooWeatherWeather(WeatherEntity): @property def temperature(self): """Return the temperature.""" - return self._data.yahoo.Now['temp'] + return int(self._data.yahoo.Now['temp']) @property def temperature_unit(self): From 4cb0e4b3c24995d51cd8a18f771acdcc6bb18c57 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Sun, 19 Nov 2017 05:20:31 +0100 Subject: [PATCH 12/14] Properly initialize Harmony remote (#10665) The delay_secs variable was not initialized if discovery was active and no matching configuration block existed (i.e. override was None). --- homeassistant/components/remote/harmony.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/remote/harmony.py b/homeassistant/components/remote/harmony.py index 7a398def5f9..40536a83602 100755 --- a/homeassistant/components/remote/harmony.py +++ b/homeassistant/components/remote/harmony.py @@ -60,6 +60,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): False) port = DEFAULT_PORT + delay_secs = DEFAULT_DELAY_SECS if override: activity = override.get(ATTR_ACTIVITY) delay_secs = override.get(ATTR_DELAY_SECS) From 8cb87d5e64f982fe7ba1e1829d024f202f3d64a9 Mon Sep 17 00:00:00 2001 From: Thibault Cohen Date: Mon, 20 Nov 2017 12:02:05 -0500 Subject: [PATCH 13/14] Handle the new version of HydroQuebec website (#10682) * Handle the new version of HydroQuebec website * Update requirements_all.txt --- homeassistant/components/sensor/hydroquebec.py | 5 +++-- requirements_all.txt | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensor/hydroquebec.py b/homeassistant/components/sensor/hydroquebec.py index 884f101c033..d857ce57fce 100644 --- a/homeassistant/components/sensor/hydroquebec.py +++ b/homeassistant/components/sensor/hydroquebec.py @@ -21,7 +21,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['pyhydroquebec==1.2.0'] +REQUIREMENTS = ['pyhydroquebec==1.3.1'] _LOGGER = logging.getLogger(__name__) @@ -34,6 +34,7 @@ DEFAULT_NAME = 'HydroQuebec' REQUESTS_TIMEOUT = 15 MIN_TIME_BETWEEN_UPDATES = timedelta(hours=1) +SCAN_INTERVAL = timedelta(hours=1) SENSOR_TYPES = { 'balance': @@ -115,7 +116,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): for variable in config[CONF_MONITORED_VARIABLES]: sensors.append(HydroQuebecSensor(hydroquebec_data, variable, name)) - add_devices(sensors, True) + add_devices(sensors) class HydroQuebecSensor(Entity): diff --git a/requirements_all.txt b/requirements_all.txt index 412405fab1c..4ce91ce57a7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -676,7 +676,7 @@ pyhik==0.1.4 pyhomematic==0.1.34 # homeassistant.components.sensor.hydroquebec -pyhydroquebec==1.2.0 +pyhydroquebec==1.3.1 # homeassistant.components.device_tracker.icloud pyicloud==0.9.1 From 235707d31c3c87dd43d6034ca190a520968bb0ef Mon Sep 17 00:00:00 2001 From: Egor Tsinko Date: Sun, 19 Nov 2017 20:41:30 -0700 Subject: [PATCH 14/14] Fix for time_date sensor (#10694) * fix to time_date sensor * cleaned up the code and added unit tests * fixed lint errors --- homeassistant/components/sensor/time_date.py | 2 +- tests/components/sensor/test_time_date.py | 27 ++++++++++++++++---- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/sensor/time_date.py b/homeassistant/components/sensor/time_date.py index 69723aea19a..bfdf0c3c3aa 100644 --- a/homeassistant/components/sensor/time_date.py +++ b/homeassistant/components/sensor/time_date.py @@ -90,7 +90,7 @@ class TimeDateSensor(Entity): if now is None: now = dt_util.utcnow() if self.type == 'date': - now = dt_util.start_of_local_day(now) + now = dt_util.start_of_local_day(dt_util.as_local(now)) return now + timedelta(seconds=86400) elif self.type == 'beat': interval = 86.4 diff --git a/tests/components/sensor/test_time_date.py b/tests/components/sensor/test_time_date.py index 98eb6e79428..1b3ab68988e 100644 --- a/tests/components/sensor/test_time_date.py +++ b/tests/components/sensor/test_time_date.py @@ -1,5 +1,6 @@ """The tests for Kira sensor platform.""" import unittest +from unittest.mock import patch from homeassistant.components.sensor import time_date as time_date import homeassistant.util.dt as dt_util @@ -36,11 +37,6 @@ class TestTimeDateSensor(unittest.TestCase): next_time = device.get_next_interval(now) assert next_time == dt_util.utc_from_timestamp(60) - device = time_date.TimeDateSensor(self.hass, 'date') - now = dt_util.utc_from_timestamp(12345) - next_time = device.get_next_interval(now) - assert next_time == dt_util.utc_from_timestamp(86400) - device = time_date.TimeDateSensor(self.hass, 'beat') now = dt_util.utc_from_timestamp(29) next_time = device.get_next_interval(now) @@ -89,6 +85,27 @@ class TestTimeDateSensor(unittest.TestCase): # so the second day was 18000 + 86400 assert next_time.timestamp() == 104400 + new_tz = dt_util.get_time_zone('America/Edmonton') + assert new_tz is not None + dt_util.set_default_time_zone(new_tz) + now = dt_util.parse_datetime('2017-11-13 19:47:19-07:00') + device = time_date.TimeDateSensor(self.hass, 'date') + next_time = device.get_next_interval(now) + assert (next_time.timestamp() == + dt_util.as_timestamp('2017-11-14 00:00:00-07:00')) + + @patch('homeassistant.util.dt.utcnow', + return_value=dt_util.parse_datetime('2017-11-14 02:47:19-00:00')) + def test_timezone_intervals_empty_parameter(self, _): + """Test get_interval() without parameters.""" + new_tz = dt_util.get_time_zone('America/Edmonton') + assert new_tz is not None + dt_util.set_default_time_zone(new_tz) + device = time_date.TimeDateSensor(self.hass, 'date') + next_time = device.get_next_interval() + assert (next_time.timestamp() == + dt_util.as_timestamp('2017-11-14 00:00:00-07:00')) + def test_icons(self): """Test attributes of sensors.""" device = time_date.TimeDateSensor(self.hass, 'time')