From a2c9834852a95aaeed4716c2435ea04090d9a639 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 27 Mar 2019 22:09:25 -0700 Subject: [PATCH 01/85] Bumped version to 0.91.0b0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 2d2f00f1e16..5194c221bed 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 91 -PATCH_VERSION = '0.dev0' +PATCH_VERSION = '0b0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 3a406f5677c2e88e899193e860733a6195dfda2a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 27 Mar 2019 23:49:10 -0700 Subject: [PATCH 02/85] Fix YAML --- homeassistant/components/camera/services.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/camera/services.yaml b/homeassistant/components/camera/services.yaml index 45a0f4cfec0..a3e42300cbd 100644 --- a/homeassistant/components/camera/services.yaml +++ b/homeassistant/components/camera/services.yaml @@ -61,7 +61,8 @@ record: description: Template of a Filename. Variable is entity_id. Must be mp4. example: '/tmp/snapshot_{{ entity_id }}.mp4' duration: - description: (Optional) Target recording length (in seconds). Default: 30 + description: (Optional) Target recording length (in seconds). + default: 30 example: 30 lookback: description: (Optional) Target lookback period (in seconds) to include in addition to duration. Only available if there is currently an active HLS stream. From 615b1cbfc7422bab3464fd42e4597d9dce9e36ad Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 27 Mar 2019 23:50:58 -0700 Subject: [PATCH 03/85] Bumped version to 0.91.0b1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 5194c221bed..eacd1812485 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 91 -PATCH_VERSION = '0b0' +PATCH_VERSION = '0b1' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 9aa5b904c6bf73e30be791a477f573b2874afe5f Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Fri, 29 Mar 2019 21:41:13 +0100 Subject: [PATCH 04/85] Fix regression of the xiaomi_aqara config validation (#22435) * Fix regression of the xiaomi_aqara config validation * Make the key optional again * Add base schema * Remove the GW_MAC default --- .../components/xiaomi_aqara/__init__.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/xiaomi_aqara/__init__.py b/homeassistant/components/xiaomi_aqara/__init__.py index e98655f9d76..9b113170f8a 100644 --- a/homeassistant/components/xiaomi_aqara/__init__.py +++ b/homeassistant/components/xiaomi_aqara/__init__.py @@ -61,7 +61,7 @@ SERVICE_SCHEMA_REMOVE_DEVICE = vol.Schema({ }) -GATEWAY_CONFIG_MAC_OPT = vol.Schema({ +GATEWAY_CONFIG = vol.Schema({ vol.Optional(CONF_KEY): vol.All(cv.string, vol.Length(min=16, max=16)), vol.Optional(CONF_HOST): cv.string, @@ -69,12 +69,12 @@ GATEWAY_CONFIG_MAC_OPT = vol.Schema({ vol.Optional(CONF_DISABLE, default=False): cv.boolean, }) -GATEWAY_CONFIG_MAC_REQ = vol.Schema({ - vol.Required(CONF_KEY): - vol.All(cv.string, vol.Length(min=16, max=16)), - vol.Optional(CONF_HOST): cv.string, - vol.Optional(CONF_PORT, default=9898): cv.port, - vol.Optional(CONF_DISABLE, default=False): cv.boolean, +GATEWAY_CONFIG_MAC_OPTIONAL = GATEWAY_CONFIG.extend({ + vol.Optional(CONF_MAC): GW_MAC, +}) + +GATEWAY_CONFIG_MAC_REQUIRED = GATEWAY_CONFIG.extend({ + vol.Required(CONF_MAC): GW_MAC, }) @@ -97,8 +97,8 @@ CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Optional(CONF_GATEWAYS, default={}): vol.All(cv.ensure_list, vol.Any( - vol.All([GATEWAY_CONFIG_MAC_OPT], vol.Length(max=1)), - vol.All([GATEWAY_CONFIG_MAC_REQ], vol.Length(min=2)) + vol.All([GATEWAY_CONFIG_MAC_OPTIONAL], vol.Length(max=1)), + vol.All([GATEWAY_CONFIG_MAC_REQUIRED], vol.Length(min=2)) ), [_fix_conf_defaults]), vol.Optional(CONF_INTERFACE, default='any'): cv.string, vol.Optional(CONF_DISCOVERY_RETRY, default=3): cv.positive_int From 21917f4dc4818dab9e24817d439a3c9dd8cfcac4 Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Thu, 28 Mar 2019 03:09:12 -0700 Subject: [PATCH 05/85] Fix dev branch (#22493) --- .../components/homekit_controller/__init__.py | 3 +-- homeassistant/loader.py | 14 +++++++++++++- tests/helpers/test_entity_component.py | 2 +- tests/test_config_entries.py | 2 +- 4 files changed, 16 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index f9fd0409c9c..44af8bffe26 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -1,6 +1,5 @@ """Support for Homekit device discovery.""" import logging -import os from homeassistant.components.discovery import SERVICE_HOMEKIT from homeassistant.helpers import discovery @@ -9,7 +8,7 @@ from homeassistant.helpers.entity import Entity from .config_flow import load_old_pairings from .connection import get_accessory_information, HKDevice from .const import ( - CONTROLLER, HOMEKIT_DIR, KNOWN_DEVICES, PAIRING_FILE + CONTROLLER, KNOWN_DEVICES ) from .const import DOMAIN # noqa: pylint: disable=unused-import diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 7f0d50f93d4..8ccbcaa33c4 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -83,7 +83,11 @@ def get_platform(hass, # type: HomeAssistant """ # If the platform has a component, we will limit the platform loading path # to be the same source (custom/built-in). - component = _load_file(hass, platform_name, LOOKUP_PATHS) + if domain not in ['automation', 'mqtt', 'telegram_bot']: + component = _load_file(hass, platform_name, LOOKUP_PATHS) + else: + # avoid load component for legacy platform + component = None # Until we have moved all platforms under their component/own folder, it # can be that the component is None. @@ -99,6 +103,14 @@ def get_platform(hass, # type: HomeAssistant if platform is not None: return platform + # Legacy platform check for automation: components/automation/event.py + if component is None and domain in ['automation', 'mqtt', 'telegram_bot']: + platform = _load_file( + hass, + PLATFORM_FORMAT.format(domain=platform_name, platform=domain), + base_paths + ) + # Legacy platform check for custom: custom_components/light/hue.py # Only check if the component was also in custom components. if component is None or base_paths[0] == PACKAGE_CUSTOM_COMPONENTS: diff --git a/tests/helpers/test_entity_component.py b/tests/helpers/test_entity_component.py index 163261a4b81..6da3293d597 100644 --- a/tests/helpers/test_entity_component.py +++ b/tests/helpers/test_entity_component.py @@ -324,7 +324,7 @@ def test_setup_dependencies_platform(hass): loader.set_component(hass, 'test_component2', MockModule('test_component2')) loader.set_component( - hass, 'test_domain.test_component', + hass, 'test_component.test_domain', MockPlatform(dependencies=['test_component', 'test_component2'])) component = EntityComponent(_LOGGER, DOMAIN, hass) diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 324db971583..32532761ccf 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -192,7 +192,7 @@ async def test_remove_entry(hass, manager): async_remove_entry=mock_remove_entry )) loader.set_component( - hass, 'light.test', + hass, 'test.light', MockPlatform(async_setup_entry=mock_setup_entry_platform)) MockConfigEntry(domain='test', entry_id='test1').add_to_manager(manager) From a95fb809a5269ba2aaa7d54224a108c0f34012cb Mon Sep 17 00:00:00 2001 From: mvn23 Date: Fri, 29 Mar 2019 08:28:50 +0100 Subject: [PATCH 06/85] Update pyotgw to 0.4b3 (#22496) --- homeassistant/components/opentherm_gw/__init__.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/opentherm_gw/__init__.py b/homeassistant/components/opentherm_gw/__init__.py index acb277c0ef5..1476363c6bd 100644 --- a/homeassistant/components/opentherm_gw/__init__.py +++ b/homeassistant/components/opentherm_gw/__init__.py @@ -15,7 +15,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['pyotgw==0.4b2'] +REQUIREMENTS = ['pyotgw==0.4b3'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 4af334dd01e..04c5f93ceb0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1198,7 +1198,7 @@ pyoppleio==1.0.5 pyota==2.0.5 # homeassistant.components.opentherm_gw -pyotgw==0.4b2 +pyotgw==0.4b3 # homeassistant.auth.mfa_modules.notify # homeassistant.auth.mfa_modules.totp From fae8265a37944254181dea19f2f72fc178c16db4 Mon Sep 17 00:00:00 2001 From: zewelor Date: Fri, 29 Mar 2019 18:43:29 +0100 Subject: [PATCH 07/85] Fixes for yeelight availbility state (#22502) --- homeassistant/components/yeelight/__init__.py | 28 +++++-- homeassistant/components/yeelight/light.py | 81 ++++++++----------- 2 files changed, 55 insertions(+), 54 deletions(-) diff --git a/homeassistant/components/yeelight/__init__.py b/homeassistant/components/yeelight/__init__.py index 14b4656c403..fb218a67698 100644 --- a/homeassistant/components/yeelight/__init__.py +++ b/homeassistant/components/yeelight/__init__.py @@ -212,6 +212,7 @@ class YeelightDevice: self._name = config.get(CONF_NAME) self._model = config.get(CONF_MODEL) self._bulb_device = None + self._available = False @property def bulb(self): @@ -224,7 +225,9 @@ class YeelightDevice: # force init for type self.update() + self._available = True except yeelight.BulbException as ex: + self._available = False _LOGGER.error("Failed to connect to bulb %s, %s: %s", self._ipaddr, self._name, ex) @@ -245,10 +248,15 @@ class YeelightDevice: """Return ip address.""" return self._ipaddr + @property + def available(self): + """Return true is device is available.""" + return self._available + @property def is_nightlight_enabled(self) -> bool: """Return true / false if nightlight is currently enabled.""" - if self._bulb_device is None: + if self.bulb is None: return False return self.bulb.last_properties.get('active_mode') == '1' @@ -271,7 +279,7 @@ class YeelightDevice: light_type = yeelight.enums.LightType.Main try: - self._bulb_device.turn_on(duration=duration, light_type=light_type) + self.bulb.turn_on(duration=duration, light_type=light_type) except yeelight.BulbException as ex: _LOGGER.error("Unable to turn the bulb on: %s", ex) return @@ -284,16 +292,24 @@ class YeelightDevice: light_type = yeelight.enums.LightType.Main try: - self._bulb_device.turn_off(duration=duration, - light_type=light_type) + self.bulb.turn_off(duration=duration, light_type=light_type) except yeelight.BulbException as ex: - _LOGGER.error("Unable to turn the bulb on: %s", ex) + _LOGGER.error("Unable to turn the bulb off: %s", ex) return def update(self): """Read new properties from the device.""" + import yeelight + if not self.bulb: return - self._bulb_device.get_properties(UPDATE_REQUEST_PROPERTIES) + try: + self.bulb.get_properties(UPDATE_REQUEST_PROPERTIES) + self._available = True + except yeelight.BulbException as ex: + if self._available: # just inform once + _LOGGER.error("Unable to update bulb status: %s", ex) + self._available = False + dispatcher_send(self._hass, DATA_UPDATED, self._ipaddr) diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index cc3810c4968..92b668c6987 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -162,7 +162,6 @@ class YeelightLight(Light): self._device = device self._supported_features = SUPPORT_YEELIGHT - self._available = False self._brightness = None self._color_temp = None @@ -196,7 +195,7 @@ class YeelightLight(Light): @property def available(self) -> bool: """Return if bulb is available.""" - return self._available + return self.device.available @property def supported_features(self) -> int: @@ -304,14 +303,7 @@ class YeelightLight(Light): # F821: https://github.com/PyCQA/pyflakes/issues/373 @property def _bulb(self) -> 'yeelight.Bulb': # noqa: F821 - bulb = self.device.bulb - - if bulb: - self._available = True - return bulb - - self._available = False - return None + return self.device.bulb def set_music_mode(self, mode) -> None: """Set the music mode on or off.""" @@ -323,52 +315,45 @@ class YeelightLight(Light): def update(self) -> None: """Update properties from the bulb.""" import yeelight - try: - bulb_type = self._bulb.bulb_type - - if bulb_type == yeelight.BulbType.Color: - self._supported_features = SUPPORT_YEELIGHT_RGB - elif self.light_type == yeelight.enums.LightType.Ambient: - self._supported_features = SUPPORT_YEELIGHT_RGB - elif bulb_type in (yeelight.BulbType.WhiteTemp, - yeelight.BulbType.WhiteTempMood): - if self._is_nightlight_enabled: - self._supported_features = SUPPORT_YEELIGHT - else: - self._supported_features = SUPPORT_YEELIGHT_WHITE_TEMP - - if self.min_mireds is None: - model_specs = self._bulb.get_model_specs() - self._min_mireds = \ - kelvin_to_mired(model_specs['color_temp']['max']) - self._max_mireds = \ - kelvin_to_mired(model_specs['color_temp']['min']) - - if bulb_type == yeelight.BulbType.WhiteTempMood: - self._is_on = self._get_property('main_power') == 'on' - else: - self._is_on = self._get_property('power') == 'on' + bulb_type = self._bulb.bulb_type + if bulb_type == yeelight.BulbType.Color: + self._supported_features = SUPPORT_YEELIGHT_RGB + elif self.light_type == yeelight.enums.LightType.Ambient: + self._supported_features = SUPPORT_YEELIGHT_RGB + elif bulb_type in (yeelight.BulbType.WhiteTemp, + yeelight.BulbType.WhiteTempMood): if self._is_nightlight_enabled: - bright = self._get_property('nl_br', None) + self._supported_features = SUPPORT_YEELIGHT else: - bright = self._get_property('bright', None) + self._supported_features = SUPPORT_YEELIGHT_WHITE_TEMP - if bright: - self._brightness = round(255 * (int(bright) / 100)) + if self.min_mireds is None: + model_specs = self._bulb.get_model_specs() + self._min_mireds = \ + kelvin_to_mired(model_specs['color_temp']['max']) + self._max_mireds = \ + kelvin_to_mired(model_specs['color_temp']['min']) - temp_in_k = self._get_property('ct') + if bulb_type == yeelight.BulbType.WhiteTempMood: + self._is_on = self._get_property('main_power') == 'on' + else: + self._is_on = self._get_property('power') == 'on' - if temp_in_k: - self._color_temp = kelvin_to_mired(int(temp_in_k)) + if self._is_nightlight_enabled: + bright = self._get_property('nl_br') + else: + bright = self._get_property('bright') - self._hs = self._get_hs_from_properties() + if bright: + self._brightness = round(255 * (int(bright) / 100)) - self._available = True - except yeelight.BulbException as ex: - if self._available: # just inform once - _LOGGER.error("Unable to update bulb status: %s", ex) - self._available = False + temp_in_k = self._get_property('ct') + + if temp_in_k: + self._color_temp = kelvin_to_mired(int(temp_in_k)) + + self._hs = self._get_hs_from_properties() @_cmd def set_brightness(self, brightness, duration) -> None: From 77f7a53d9ff1d96355648f042336165c7d9572a4 Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Thu, 28 Mar 2019 14:37:44 -0700 Subject: [PATCH 08/85] Remove botocore dependency from credstash script (#22511) * Remove botocore dependency from credstash script * Update requirements_all.txt * Update pylintrc * Update credstash.py --- homeassistant/scripts/credstash.py | 7 +++---- pylintrc | 1 - requirements_all.txt | 3 --- 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/homeassistant/scripts/credstash.py b/homeassistant/scripts/credstash.py index 302910c5b08..6dd9f90197a 100644 --- a/homeassistant/scripts/credstash.py +++ b/homeassistant/scripts/credstash.py @@ -4,7 +4,7 @@ import getpass from homeassistant.util.yaml import _SECRET_NAMESPACE -REQUIREMENTS = ['credstash==1.15.0', 'botocore==1.7.34'] +REQUIREMENTS = ['credstash==1.15.0'] def run(args): @@ -24,16 +24,15 @@ def run(args): 'value', help="The value to save when putting a secret", nargs='?', default=None) - # pylint: disable=import-error, no-member + # pylint: disable=no-member import credstash - import botocore args = parser.parse_args(args) table = _SECRET_NAMESPACE try: credstash.listSecrets(table=table) - except botocore.errorfactory.ClientError: + except Exception: # pylint: disable=broad-except credstash.createDdbTable(table=table) if args.action == 'list': diff --git a/pylintrc b/pylintrc index a88aabe1936..7d349033f70 100644 --- a/pylintrc +++ b/pylintrc @@ -42,7 +42,6 @@ reports=no [TYPECHECK] # For attrs ignored-classes=_CountingAttr -generated-members=botocore.errorfactory [FORMAT] expected-line-ending-format=LF diff --git a/requirements_all.txt b/requirements_all.txt index 04c5f93ceb0..a43a04240ba 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -235,9 +235,6 @@ blockchain==1.4.4 # homeassistant.components.aws_sqs.notify boto3==1.9.16 -# homeassistant.scripts.credstash -botocore==1.7.34 - # homeassistant.components.braviatv.media_player braviarc-homeassistant==0.3.7.dev0 From 9f72764cffd849fe9658d742332ede61a021695c Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Thu, 28 Mar 2019 15:33:21 -0700 Subject: [PATCH 09/85] Fix lint on dev (#22512) ## Description: Fix a lint issue in credstash script. **Related issue (if applicable):** fixes # **Pull request in [home-assistant.io](https://github.com/home-assistant/home-assistant.io) with documentation (if applicable):** home-assistant/home-assistant.io# ## Example entry for `configuration.yaml` (if applicable): ```yaml ``` ## Checklist: - [ ] The code change is tested and works locally. - [ ] Local tests pass with `tox`. **Your PR cannot be merged unless tests pass** - [ ] There is no commented out code in this PR. If user exposed functionality or configuration variables are added/changed: - [ ] Documentation added/updated in [home-assistant.io](https://github.com/home-assistant/home-assistant.io) If the code communicates with devices, web services, or third-party tools: - [ ] New dependencies have been added to the `REQUIREMENTS` variable ([example][ex-requir]). - [ ] New dependencies are only imported inside functions that use them ([example][ex-import]). - [ ] New or updated dependencies have been added to `requirements_all.txt` by running `script/gen_requirements_all.py`. - [ ] New files were added to `.coveragerc`. If the code does not interact with devices: - [ ] Tests have been added to verify that the new code works. [ex-requir]: https://github.com/home-assistant/home-assistant/blob/dev/homeassistant/components/keyboard/__init__.py#L14 [ex-import]: https://github.com/home-assistant/home-assistant/blob/dev/homeassistant/components/keyboard/__init__.py#L23 --- homeassistant/scripts/credstash.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/scripts/credstash.py b/homeassistant/scripts/credstash.py index 6dd9f90197a..e2950f8d7a0 100644 --- a/homeassistant/scripts/credstash.py +++ b/homeassistant/scripts/credstash.py @@ -24,7 +24,7 @@ def run(args): 'value', help="The value to save when putting a secret", nargs='?', default=None) - # pylint: disable=no-member + # pylint: disable=import-error, no-member import credstash args = parser.parse_args(args) From 173ef7cac59ab8e01fce02b4783583e9c8b5d0e6 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Fri, 29 Mar 2019 11:52:13 -0400 Subject: [PATCH 10/85] Do not use zha default light polling (#22513) * don't use default light polling * review comment --- homeassistant/components/zha/light.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index 8b2cd349b9d..6ba4efa9b0f 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -11,6 +11,7 @@ from homeassistant.components import light from homeassistant.const import STATE_ON from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.event import async_track_time_interval import homeassistant.util.color as color_util from .const import ( DATA_ZHA, DATA_ZHA_DISPATCHERS, ZHA_DISCOVERY_NEW, COLOR_CHANNEL, @@ -96,11 +97,6 @@ class Light(ZhaEntity, light.Light): self._supported_features |= light.SUPPORT_COLOR self._hs_color = (0, 0) - @property - def should_poll(self) -> bool: - """Poll state from device.""" - return True - @property def is_on(self) -> bool: """Return true if entity is on.""" @@ -157,6 +153,7 @@ class Light(ZhaEntity, light.Light): if self._level_channel: await self.async_accept_signal( self._level_channel, SIGNAL_SET_LEVEL, self.set_level) + async_track_time_interval(self.hass, self.refresh, SCAN_INTERVAL) @callback def async_restore_last_state(self, last_state): @@ -247,3 +244,7 @@ class Light(ZhaEntity, light.Light): if self._level_channel: self._brightness = await self._level_channel.get_attribute_value( 'current_level') + + async def refresh(self, time): + """Call async_update at an interval.""" + await self.async_update() From 53595e76d8abcb58e2348f954c9bef1782b51ac7 Mon Sep 17 00:00:00 2001 From: ktnrg45 <38207570+ktnrg45@users.noreply.github.com> Date: Fri, 29 Mar 2019 12:10:28 -0700 Subject: [PATCH 11/85] PS4 bump to 0.5.2 (#22523) * Bump pyps4 to 0.5.2 * Bump pyps4 to 0.5.2 * Bump pyps4 to 0.5.2 --- homeassistant/components/ps4/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ps4/__init__.py b/homeassistant/components/ps4/__init__.py index d5833ae1673..9183bbe1989 100644 --- a/homeassistant/components/ps4/__init__.py +++ b/homeassistant/components/ps4/__init__.py @@ -14,7 +14,7 @@ from .const import DOMAIN # noqa: pylint: disable=unused-import _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['pyps4-homeassistant==0.5.0'] +REQUIREMENTS = ['pyps4-homeassistant==0.5.2'] async def async_setup(hass, config): diff --git a/requirements_all.txt b/requirements_all.txt index a43a04240ba..4468c87fde3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1222,7 +1222,7 @@ pypoint==1.1.1 pypollencom==2.2.3 # homeassistant.components.ps4 -pyps4-homeassistant==0.5.0 +pyps4-homeassistant==0.5.2 # homeassistant.components.qwikswitch pyqwikswitch==0.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 60d9697ed19..965faa6eb5c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -227,7 +227,7 @@ pyopenuv==1.0.9 pyotp==2.2.6 # homeassistant.components.ps4 -pyps4-homeassistant==0.5.0 +pyps4-homeassistant==0.5.2 # homeassistant.components.qwikswitch pyqwikswitch==0.8 From b7bc520a0ea52079af727d68e4a771c6c1f8d1ce Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Fri, 29 Mar 2019 16:41:04 -0400 Subject: [PATCH 12/85] clean up channel configuration (#22534) --- .../components/zha/core/channels/__init__.py | 36 ++++++++++--------- homeassistant/components/zha/core/device.py | 8 ++--- homeassistant/components/zha/core/gateway.py | 5 +++ 3 files changed, 28 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/zha/core/channels/__init__.py b/homeassistant/components/zha/core/channels/__init__.py index d8a3918889d..10370c42c66 100644 --- a/homeassistant/components/zha/core/channels/__init__.py +++ b/homeassistant/components/zha/core/channels/__init__.py @@ -15,7 +15,7 @@ from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_send from ..helpers import ( bind_configure_reporting, construct_unique_id, - safe_read, get_attr_id_by_name) + safe_read, get_attr_id_by_name, bind_cluster) from ..const import ( REPORT_CONFIG_DEFAULT, SIGNAL_ATTR_UPDATED, ATTRIBUTE_CHANNEL, EVENT_RELAY_CHANNEL, ZDO_CHANNEL @@ -141,22 +141,24 @@ class ZigbeeChannel: manufacturer_code = self._zha_device.manufacturer_code if self.cluster.cluster_id >= 0xfc00 and manufacturer_code: manufacturer = manufacturer_code - - skip_bind = False # bind cluster only for the 1st configured attr - for report_config in self._report_config: - attr = report_config.get('attr') - min_report_interval, max_report_interval, change = \ - report_config.get('config') - await bind_configure_reporting( - self._unique_id, self.cluster, attr, - min_report=min_report_interval, - max_report=max_report_interval, - reportable_change=change, - skip_bind=skip_bind, - manufacturer=manufacturer - ) - skip_bind = True - await asyncio.sleep(uniform(0.1, 0.5)) + if self.cluster.bind_only: + await bind_cluster(self._unique_id, self.cluster) + else: + skip_bind = False # bind cluster only for the 1st configured attr + for report_config in self._report_config: + attr = report_config.get('attr') + min_report_interval, max_report_interval, change = \ + report_config.get('config') + await bind_configure_reporting( + self._unique_id, self.cluster, attr, + min_report=min_report_interval, + max_report=max_report_interval, + reportable_change=change, + skip_bind=skip_bind, + manufacturer=manufacturer + ) + skip_bind = True + await asyncio.sleep(uniform(0.1, 0.5)) _LOGGER.debug( "%s: finished channel configuration", self._unique_id diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index 0ddb67484c6..435ab25acc6 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -49,7 +49,7 @@ class ZHADevice: self._zha_gateway = zha_gateway self.cluster_channels = {} self._relay_channels = {} - self._all_channels = {} + self._all_channels = [] self._name = "{} {}".format( self.manufacturer, self.model @@ -135,7 +135,7 @@ class ZHADevice: @property def all_channels(self): """Return cluster channels and relay channels for device.""" - return self._all_channels.values() + return self._all_channels @property def available_signal(self): @@ -195,10 +195,10 @@ class ZHADevice: if isinstance(cluster_channel, EventRelayChannel): self._relay_channels[cluster_channel.unique_id] = cluster_channel - self._all_channels[cluster_channel.unique_id] = cluster_channel + self._all_channels.append(cluster_channel) else: self.cluster_channels[cluster_channel.name] = cluster_channel - self._all_channels[cluster_channel.name] = cluster_channel + self._all_channels.append(cluster_channel) async def async_configure(self): """Configure the device.""" diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 4f1e24aad5b..71e41c2509b 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -266,6 +266,11 @@ class ZHAGateway: self._hass, self._config, endpoint_id, endpoint, discovery_infos, device, zha_device, is_new_join ) + if endpoint_id != 0: + for cluster in endpoint.in_clusters.values(): + cluster.bind_only = False + for cluster in endpoint.out_clusters.values(): + cluster.bind_only = True if is_new_join: # configure the device From ab642ca4eb95319b9369dc5c3b44b3e710077fc3 Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Fri, 29 Mar 2019 11:45:02 -0700 Subject: [PATCH 13/85] Fix tts Great Migration issue (#22539) --- homeassistant/components/amazon_polly/__init__.py | 1 + homeassistant/components/amazon_polly/tts.py | 3 +-- homeassistant/components/baidu/__init__.py | 1 + homeassistant/components/baidu/tts.py | 3 +-- homeassistant/components/marytts/__init__.py | 1 + homeassistant/components/marytts/tts.py | 3 +-- homeassistant/components/microsoft/__init__.py | 1 + homeassistant/components/microsoft/tts.py | 3 +-- homeassistant/components/picotts/__init__.py | 1 + homeassistant/components/picotts/tts.py | 2 +- homeassistant/components/voicerss/__init__.py | 1 + homeassistant/components/voicerss/tts.py | 3 +-- homeassistant/components/yandextts/__init__.py | 1 + homeassistant/components/yandextts/tts.py | 3 +-- requirements_all.txt | 7 +++++++ 15 files changed, 21 insertions(+), 13 deletions(-) create mode 100644 homeassistant/components/amazon_polly/__init__.py create mode 100644 homeassistant/components/baidu/__init__.py create mode 100644 homeassistant/components/marytts/__init__.py create mode 100644 homeassistant/components/microsoft/__init__.py create mode 100644 homeassistant/components/picotts/__init__.py create mode 100644 homeassistant/components/voicerss/__init__.py create mode 100644 homeassistant/components/yandextts/__init__.py diff --git a/homeassistant/components/amazon_polly/__init__.py b/homeassistant/components/amazon_polly/__init__.py new file mode 100644 index 00000000000..0fab4af43e6 --- /dev/null +++ b/homeassistant/components/amazon_polly/__init__.py @@ -0,0 +1 @@ +"""Support for Amazon Polly integration.""" diff --git a/homeassistant/components/amazon_polly/tts.py b/homeassistant/components/amazon_polly/tts.py index 12383df115a..167cd9cfc78 100644 --- a/homeassistant/components/amazon_polly/tts.py +++ b/homeassistant/components/amazon_polly/tts.py @@ -8,10 +8,9 @@ import logging import voluptuous as vol +from homeassistant.components.tts import PLATFORM_SCHEMA, Provider import homeassistant.helpers.config_validation as cv -from . import PLATFORM_SCHEMA, Provider - REQUIREMENTS = ['boto3==1.9.16'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/baidu/__init__.py b/homeassistant/components/baidu/__init__.py new file mode 100644 index 00000000000..8a332cf52e1 --- /dev/null +++ b/homeassistant/components/baidu/__init__.py @@ -0,0 +1 @@ +"""Support for Baidu integration.""" diff --git a/homeassistant/components/baidu/tts.py b/homeassistant/components/baidu/tts.py index e7a1f368f1d..07b69d41dfd 100644 --- a/homeassistant/components/baidu/tts.py +++ b/homeassistant/components/baidu/tts.py @@ -9,11 +9,10 @@ import logging import voluptuous as vol +from homeassistant.components.tts import CONF_LANG, PLATFORM_SCHEMA, Provider from homeassistant.const import CONF_API_KEY import homeassistant.helpers.config_validation as cv -from . import CONF_LANG, PLATFORM_SCHEMA, Provider - REQUIREMENTS = ["baidu-aip==1.6.6"] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/marytts/__init__.py b/homeassistant/components/marytts/__init__.py new file mode 100644 index 00000000000..ec85cb6d4ab --- /dev/null +++ b/homeassistant/components/marytts/__init__.py @@ -0,0 +1 @@ +"""Support for MaryTTS integration.""" diff --git a/homeassistant/components/marytts/tts.py b/homeassistant/components/marytts/tts.py index 8f6a46b0c3e..f5d19c977a4 100644 --- a/homeassistant/components/marytts/tts.py +++ b/homeassistant/components/marytts/tts.py @@ -12,12 +12,11 @@ import aiohttp import async_timeout import voluptuous as vol +from homeassistant.components.tts import CONF_LANG, PLATFORM_SCHEMA, Provider from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from homeassistant.components.tts import CONF_LANG, PLATFORM_SCHEMA, Provider - _LOGGER = logging.getLogger(__name__) SUPPORT_LANGUAGES = [ diff --git a/homeassistant/components/microsoft/__init__.py b/homeassistant/components/microsoft/__init__.py new file mode 100644 index 00000000000..2d281cd2bd8 --- /dev/null +++ b/homeassistant/components/microsoft/__init__.py @@ -0,0 +1 @@ +"""Support for Microsoft integration.""" diff --git a/homeassistant/components/microsoft/tts.py b/homeassistant/components/microsoft/tts.py index ab9fb576c28..55cf7a4ae7a 100644 --- a/homeassistant/components/microsoft/tts.py +++ b/homeassistant/components/microsoft/tts.py @@ -9,11 +9,10 @@ import logging import voluptuous as vol +from homeassistant.components.tts import CONF_LANG, PLATFORM_SCHEMA, Provider from homeassistant.const import CONF_API_KEY, CONF_TYPE import homeassistant.helpers.config_validation as cv -from . import CONF_LANG, PLATFORM_SCHEMA, Provider - CONF_GENDER = 'gender' CONF_OUTPUT = 'output' CONF_RATE = 'rate' diff --git a/homeassistant/components/picotts/__init__.py b/homeassistant/components/picotts/__init__.py new file mode 100644 index 00000000000..7ffc80db2f9 --- /dev/null +++ b/homeassistant/components/picotts/__init__.py @@ -0,0 +1 @@ +"""Support for pico integration.""" diff --git a/homeassistant/components/picotts/tts.py b/homeassistant/components/picotts/tts.py index 99d3b5e9786..c164e7fb85d 100644 --- a/homeassistant/components/picotts/tts.py +++ b/homeassistant/components/picotts/tts.py @@ -12,7 +12,7 @@ import tempfile import voluptuous as vol -from . import CONF_LANG, PLATFORM_SCHEMA, Provider +from homeassistant.components.tts import CONF_LANG, PLATFORM_SCHEMA, Provider _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/voicerss/__init__.py b/homeassistant/components/voicerss/__init__.py new file mode 100644 index 00000000000..4894ca30bbd --- /dev/null +++ b/homeassistant/components/voicerss/__init__.py @@ -0,0 +1 @@ +"""Support for VoiceRSS integration.""" diff --git a/homeassistant/components/voicerss/tts.py b/homeassistant/components/voicerss/tts.py index 20e0ee11db3..436f070e503 100644 --- a/homeassistant/components/voicerss/tts.py +++ b/homeassistant/components/voicerss/tts.py @@ -11,12 +11,11 @@ import aiohttp import async_timeout import voluptuous as vol +from homeassistant.components.tts import CONF_LANG, PLATFORM_SCHEMA, Provider from homeassistant.const import CONF_API_KEY from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from homeassistant.components.tts import CONF_LANG, PLATFORM_SCHEMA, Provider - _LOGGER = logging.getLogger(__name__) VOICERSS_API_URL = "https://api.voicerss.org/" diff --git a/homeassistant/components/yandextts/__init__.py b/homeassistant/components/yandextts/__init__.py new file mode 100644 index 00000000000..86ac9b58f73 --- /dev/null +++ b/homeassistant/components/yandextts/__init__.py @@ -0,0 +1 @@ +"""Support for the yandex speechkit tts integration.""" diff --git a/homeassistant/components/yandextts/tts.py b/homeassistant/components/yandextts/tts.py index 281839a2d74..e60b890e84f 100644 --- a/homeassistant/components/yandextts/tts.py +++ b/homeassistant/components/yandextts/tts.py @@ -11,12 +11,11 @@ import aiohttp import async_timeout import voluptuous as vol +from homeassistant.components.tts import CONF_LANG, PLATFORM_SCHEMA, Provider from homeassistant.const import CONF_API_KEY from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from homeassistant.components.tts import CONF_LANG, PLATFORM_SCHEMA, Provider - _LOGGER = logging.getLogger(__name__) YANDEX_API_URL = "https://tts.voicetech.yandex.net/generate?" diff --git a/requirements_all.txt b/requirements_all.txt index 4468c87fde3..b63c60cffe4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -191,6 +191,9 @@ av==6.1.2 # homeassistant.components.axis axis==17 +# homeassistant.components.baidu.tts +baidu-aip==1.6.6 + # homeassistant.components.modem_callerid.sensor basicmodem==0.7 @@ -230,6 +233,7 @@ blockchain==1.4.4 # bme680==1.0.5 # homeassistant.components.route53 +# homeassistant.components.amazon_polly.tts # homeassistant.components.aws_lambda.notify # homeassistant.components.aws_sns.notify # homeassistant.components.aws_sqs.notify @@ -984,6 +988,9 @@ pycomfoconnect==0.3 # homeassistant.components.coolmaster.climate pycoolmasternet==0.0.4 +# homeassistant.components.microsoft.tts +pycsspeechtts==1.0.2 + # homeassistant.components.cups.sensor # pycups==1.9.73 From ae18705c45057410fc164ca70ba3f18fc7e32653 Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Fri, 29 Mar 2019 21:41:50 +0100 Subject: [PATCH 14/85] Upgrade to async_upnp_client==0.14.7 (#22543) --- homeassistant/components/dlna_dmr/media_player.py | 2 +- homeassistant/components/upnp/__init__.py | 2 +- requirements_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/dlna_dmr/media_player.py b/homeassistant/components/dlna_dmr/media_player.py index 9cf42bfec60..71195d66c69 100644 --- a/homeassistant/components/dlna_dmr/media_player.py +++ b/homeassistant/components/dlna_dmr/media_player.py @@ -32,7 +32,7 @@ from homeassistant.helpers.typing import HomeAssistantType import homeassistant.helpers.config_validation as cv from homeassistant.util import get_local_ip -REQUIREMENTS = ['async-upnp-client==0.14.6'] +REQUIREMENTS = ['async-upnp-client==0.14.7'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/upnp/__init__.py b/homeassistant/components/upnp/__init__.py index ce72eff2ba8..5f4abcb24c7 100644 --- a/homeassistant/components/upnp/__init__.py +++ b/homeassistant/components/upnp/__init__.py @@ -23,7 +23,7 @@ from .const import DOMAIN from .const import LOGGER as _LOGGER from .device import Device -REQUIREMENTS = ['async-upnp-client==0.14.6'] +REQUIREMENTS = ['async-upnp-client==0.14.7'] NOTIFICATION_ID = 'upnp_notification' NOTIFICATION_TITLE = 'UPnP/IGD Setup' diff --git a/requirements_all.txt b/requirements_all.txt index b63c60cffe4..4cfd687db99 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -180,7 +180,7 @@ asterisk_mbox==0.5.0 # homeassistant.components.upnp # homeassistant.components.dlna_dmr.media_player -async-upnp-client==0.14.6 +async-upnp-client==0.14.7 # homeassistant.components.stream av==6.1.2 From 24095c0d7b28a88455d1a478c6ef119649bcd36e Mon Sep 17 00:00:00 2001 From: damarco Date: Fri, 29 Mar 2019 22:01:51 +0100 Subject: [PATCH 15/85] Bump zigpy (#22545) --- homeassistant/components/zha/__init__.py | 8 ++++---- requirements_all.txt | 8 ++++---- requirements_test_all.txt | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index adc092dcbe1..292b4fde61f 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -27,11 +27,11 @@ from .core.channels.registry import populate_channel_registry from .core.patches import apply_cluster_listener_patch REQUIREMENTS = [ - 'bellows-homeassistant==0.7.1', - 'zigpy-homeassistant==0.3.0', - 'zigpy-xbee-homeassistant==0.1.2', + 'bellows-homeassistant==0.7.2', + 'zigpy-homeassistant==0.3.1', + 'zigpy-xbee-homeassistant==0.1.3', 'zha-quirks==0.0.7', - 'zigpy-deconz==0.1.2' + 'zigpy-deconz==0.1.3' ] DEVICE_CONFIG_SCHEMA_ENTRY = vol.Schema({ diff --git a/requirements_all.txt b/requirements_all.txt index 4cfd687db99..237077e908b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -209,7 +209,7 @@ batinfo==0.4.2 beautifulsoup4==4.7.1 # homeassistant.components.zha -bellows-homeassistant==0.7.1 +bellows-homeassistant==0.7.2 # homeassistant.components.bmw_connected_drive bimmer_connected==0.5.3 @@ -1843,13 +1843,13 @@ zhong_hong_hvac==1.0.9 ziggo-mediabox-xl==1.1.0 # homeassistant.components.zha -zigpy-deconz==0.1.2 +zigpy-deconz==0.1.3 # homeassistant.components.zha -zigpy-homeassistant==0.3.0 +zigpy-homeassistant==0.3.1 # homeassistant.components.zha -zigpy-xbee-homeassistant==0.1.2 +zigpy-xbee-homeassistant==0.1.3 # homeassistant.components.zoneminder zm-py==0.3.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 965faa6eb5c..4af0b078c0b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -63,7 +63,7 @@ av==6.1.2 axis==17 # homeassistant.components.zha -bellows-homeassistant==0.7.1 +bellows-homeassistant==0.7.2 # homeassistant.components.caldav.calendar caldav==0.5.0 @@ -319,4 +319,4 @@ vultr==0.1.2 wakeonlan==1.1.6 # homeassistant.components.zha -zigpy-homeassistant==0.3.0 +zigpy-homeassistant==0.3.1 From c4a4af7c29f92a852d2ad8420f8ea1049028006a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 29 Mar 2019 14:07:22 -0700 Subject: [PATCH 16/85] Bumped version to 0.91.0b2 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index eacd1812485..2fb2dc5450a 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 91 -PATCH_VERSION = '0b1' +PATCH_VERSION = '0b2' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 65c47824a0863cc74f75ca20553871d9c775132e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 29 Mar 2019 16:46:15 -0700 Subject: [PATCH 17/85] Updated frontend to 20190329.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 80b5b744488..3baea2008b1 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -21,7 +21,7 @@ from homeassistant.loader import bind_hass from .storage import async_setup_frontend_storage -REQUIREMENTS = ['home-assistant-frontend==20190327.0'] +REQUIREMENTS = ['home-assistant-frontend==20190329.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/requirements_all.txt b/requirements_all.txt index 237077e908b..c0b458119e2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -548,7 +548,7 @@ hole==0.3.0 holidays==0.9.10 # homeassistant.components.frontend -home-assistant-frontend==20190327.0 +home-assistant-frontend==20190329.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4af0b078c0b..003c9fa43cb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -129,7 +129,7 @@ hdate==0.8.7 holidays==0.9.10 # homeassistant.components.frontend -home-assistant-frontend==20190327.0 +home-assistant-frontend==20190329.0 # homeassistant.components.homekit_controller homekit[IP]==0.13.0 From f9f100b57590aa9058610ef4d8586ca7a1c21af4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 29 Mar 2019 17:03:02 -0700 Subject: [PATCH 18/85] Add support for streaming to ffmpeg (#22549) --- homeassistant/components/ffmpeg/camera.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/ffmpeg/camera.py b/homeassistant/components/ffmpeg/camera.py index dbb51bf27c7..d897293124b 100644 --- a/homeassistant/components/ffmpeg/camera.py +++ b/homeassistant/components/ffmpeg/camera.py @@ -9,7 +9,8 @@ import logging import voluptuous as vol -from homeassistant.components.camera import PLATFORM_SCHEMA, Camera +from homeassistant.components.camera import ( + PLATFORM_SCHEMA, Camera, SUPPORT_STREAM) from homeassistant.const import CONF_NAME from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream import homeassistant.helpers.config_validation as cv @@ -46,6 +47,16 @@ class FFmpegCamera(Camera): self._input = config.get(CONF_INPUT) self._extra_arguments = config.get(CONF_EXTRA_ARGUMENTS) + @property + def supported_features(self): + """Return supported features.""" + return SUPPORT_STREAM + + @property + def stream_source(self): + """Return the stream source.""" + return self._input.split(' ')[-1] + async def async_camera_image(self): """Return a still image response from the camera.""" from haffmpeg.tools import ImageFrame, IMAGE_JPEG From 3ad4419cb6da23d708e6678f32606421a38a300e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 29 Mar 2019 17:04:59 -0700 Subject: [PATCH 19/85] Fix platform warnings (#22551) --- homeassistant/loader.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 8ccbcaa33c4..4ca19935206 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -37,6 +37,7 @@ DATA_KEY = 'components' PACKAGE_CUSTOM_COMPONENTS = 'custom_components' PACKAGE_BUILTIN = 'homeassistant.components' LOOKUP_PATHS = [PACKAGE_CUSTOM_COMPONENTS, PACKAGE_BUILTIN] +COMPONENTS_WITH_BAD_PLATFORMS = ['automation', 'mqtt', 'telegram_bot'] class LoaderError(Exception): @@ -83,7 +84,7 @@ def get_platform(hass, # type: HomeAssistant """ # If the platform has a component, we will limit the platform loading path # to be the same source (custom/built-in). - if domain not in ['automation', 'mqtt', 'telegram_bot']: + if domain not in COMPONENTS_WITH_BAD_PLATFORMS: component = _load_file(hass, platform_name, LOOKUP_PATHS) else: # avoid load component for legacy platform @@ -104,7 +105,7 @@ def get_platform(hass, # type: HomeAssistant return platform # Legacy platform check for automation: components/automation/event.py - if component is None and domain in ['automation', 'mqtt', 'telegram_bot']: + if component is None and domain in COMPONENTS_WITH_BAD_PLATFORMS: platform = _load_file( hass, PLATFORM_FORMAT.format(domain=platform_name, platform=domain), @@ -129,10 +130,11 @@ def get_platform(hass, # type: HomeAssistant _LOGGER.error("Unable to find platform %s.%s", platform_name, extra) return None - _LOGGER.error( - "Integrations need to be in their own folder. Change %s/%s.py to " - "%s/%s.py. This will stop working soon.", - domain, platform_name, platform_name, domain) + if domain not in COMPONENTS_WITH_BAD_PLATFORMS: + _LOGGER.error( + "Integrations need to be in their own folder. Change %s/%s.py to " + "%s/%s.py. This will stop working soon.", + domain, platform_name, platform_name, domain) return platform From b1a6539290057af792ecf82407196c46e2cfa368 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 29 Mar 2019 17:05:40 -0700 Subject: [PATCH 20/85] Bumped version to 0.91.0b3 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 2fb2dc5450a..48476c4fa90 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 91 -PATCH_VERSION = '0b2' +PATCH_VERSION = '0b3' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From ec9a58442b6a5324a36a9ad5e1245567e7505112 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 31 Mar 2019 19:52:44 -0700 Subject: [PATCH 21/85] Updated frontend to 20190331.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 3baea2008b1..f0358dbd6cc 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -21,7 +21,7 @@ from homeassistant.loader import bind_hass from .storage import async_setup_frontend_storage -REQUIREMENTS = ['home-assistant-frontend==20190329.0'] +REQUIREMENTS = ['home-assistant-frontend==20190331.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/requirements_all.txt b/requirements_all.txt index c0b458119e2..09a447b2a20 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -548,7 +548,7 @@ hole==0.3.0 holidays==0.9.10 # homeassistant.components.frontend -home-assistant-frontend==20190329.0 +home-assistant-frontend==20190331.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 003c9fa43cb..27d96c5e606 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -129,7 +129,7 @@ hdate==0.8.7 holidays==0.9.10 # homeassistant.components.frontend -home-assistant-frontend==20190329.0 +home-assistant-frontend==20190331.0 # homeassistant.components.homekit_controller homekit[IP]==0.13.0 From 8af70d5d19f7bc980a41d1e06425d7de2f29969a Mon Sep 17 00:00:00 2001 From: giefca Date: Sat, 30 Mar 2019 04:51:47 +0100 Subject: [PATCH 22/85] Google assistant: add blinds trait for covers (#22336) * Update const.py * Update smart_home.py * Update trait.py * Update test_trait.py * Update smart_home.py * Update test_trait.py * Update trait.py * Update trait.py * Update test_trait.py * Update test_trait.py * Update __init__.py * Update test_trait.py * Change email * Trying to correct CLA * Update __init__.py * Update trait.py * Update trait.py * Update trait.py * Update trait.py * Update __init__.py * Update test_trait.py * Update test_google_assistant.py * Update trait.py * Update trait.py * Update test_trait.py * Update test_trait.py --- .../components/google_assistant/const.py | 1 + .../components/google_assistant/smart_home.py | 4 +- .../components/google_assistant/trait.py | 92 +++++++++++++----- tests/components/google_assistant/__init__.py | 20 ++-- .../components/google_assistant/test_trait.py | 94 ++++++------------- 5 files changed, 110 insertions(+), 101 deletions(-) diff --git a/homeassistant/components/google_assistant/const.py b/homeassistant/components/google_assistant/const.py index 543404dd34e..852ea2469a2 100644 --- a/homeassistant/components/google_assistant/const.py +++ b/homeassistant/components/google_assistant/const.py @@ -29,6 +29,7 @@ TYPE_SCENE = PREFIX_TYPES + 'SCENE' TYPE_FAN = PREFIX_TYPES + 'FAN' TYPE_THERMOSTAT = PREFIX_TYPES + 'THERMOSTAT' TYPE_LOCK = PREFIX_TYPES + 'LOCK' +TYPE_BLINDS = PREFIX_TYPES + 'BLINDS' SERVICE_REQUEST_SYNC = 'request_sync' HOMEGRAPH_URL = 'https://homegraph.googleapis.com/' diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index 88cbea345b1..d84c8037c60 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -31,7 +31,7 @@ from homeassistant.components import ( from . import trait from .const import ( TYPE_LIGHT, TYPE_LOCK, TYPE_SCENE, TYPE_SWITCH, TYPE_VACUUM, - TYPE_THERMOSTAT, TYPE_FAN, TYPE_CAMERA, + TYPE_THERMOSTAT, TYPE_FAN, TYPE_CAMERA, TYPE_BLINDS, CONF_ALIASES, CONF_ROOM_HINT, ERR_FUNCTION_NOT_SUPPORTED, ERR_PROTOCOL_ERROR, ERR_DEVICE_OFFLINE, ERR_UNKNOWN_ERROR, @@ -45,7 +45,7 @@ _LOGGER = logging.getLogger(__name__) DOMAIN_TO_GOOGLE_TYPES = { camera.DOMAIN: TYPE_CAMERA, climate.DOMAIN: TYPE_THERMOSTAT, - cover.DOMAIN: TYPE_SWITCH, + cover.DOMAIN: TYPE_BLINDS, fan.DOMAIN: TYPE_FAN, group.DOMAIN: TYPE_SWITCH, input_boolean.DOMAIN: TYPE_SWITCH, diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index bd903575762..81918ff2e88 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -48,6 +48,7 @@ TRAIT_TEMPERATURE_SETTING = PREFIX_TRAITS + 'TemperatureSetting' TRAIT_LOCKUNLOCK = PREFIX_TRAITS + 'LockUnlock' TRAIT_FANSPEED = PREFIX_TRAITS + 'FanSpeed' TRAIT_MODES = PREFIX_TRAITS + 'Modes' +TRAIT_OPENCLOSE = PREFIX_TRAITS + 'OpenClose' PREFIX_COMMANDS = 'action.devices.commands.' COMMAND_ONOFF = PREFIX_COMMANDS + 'OnOff' @@ -66,6 +67,7 @@ COMMAND_THERMOSTAT_SET_MODE = PREFIX_COMMANDS + 'ThermostatSetMode' COMMAND_LOCKUNLOCK = PREFIX_COMMANDS + 'LockUnlock' COMMAND_FANSPEED = PREFIX_COMMANDS + 'SetFanSpeed' COMMAND_MODES = PREFIX_COMMANDS + 'SetModes' +COMMAND_OPENCLOSE = PREFIX_COMMANDS + 'OpenClose' TRAITS = [] @@ -128,8 +130,6 @@ class BrightnessTrait(_Trait): """Test if state is supported.""" if domain == light.DOMAIN: return features & light.SUPPORT_BRIGHTNESS - if domain == cover.DOMAIN: - return features & cover.SUPPORT_SET_POSITION if domain == media_player.DOMAIN: return features & media_player.SUPPORT_VOLUME_SET @@ -149,11 +149,6 @@ class BrightnessTrait(_Trait): if brightness is not None: response['brightness'] = int(100 * (brightness / 255)) - elif domain == cover.DOMAIN: - position = self.state.attributes.get(cover.ATTR_CURRENT_POSITION) - if position is not None: - response['brightness'] = position - elif domain == media_player.DOMAIN: level = self.state.attributes.get( media_player.ATTR_MEDIA_VOLUME_LEVEL) @@ -173,12 +168,6 @@ class BrightnessTrait(_Trait): ATTR_ENTITY_ID: self.state.entity_id, light.ATTR_BRIGHTNESS_PCT: params['brightness'] }, blocking=True, context=data.context) - elif domain == cover.DOMAIN: - await self.hass.services.async_call( - cover.DOMAIN, cover.SERVICE_SET_COVER_POSITION, { - ATTR_ENTITY_ID: self.state.entity_id, - cover.ATTR_POSITION: params['brightness'] - }, blocking=True, context=data.context) elif domain == media_player.DOMAIN: await self.hass.services.async_call( media_player.DOMAIN, media_player.SERVICE_VOLUME_SET, { @@ -254,7 +243,6 @@ class OnOffTrait(_Trait): switch.DOMAIN, fan.DOMAIN, light.DOMAIN, - cover.DOMAIN, media_player.DOMAIN, ) @@ -264,22 +252,13 @@ class OnOffTrait(_Trait): def query_attributes(self): """Return OnOff query attributes.""" - if self.state.domain == cover.DOMAIN: - return {'on': self.state.state != cover.STATE_CLOSED} return {'on': self.state.state != STATE_OFF} async def execute(self, command, data, params): """Execute an OnOff command.""" domain = self.state.domain - if domain == cover.DOMAIN: - service_domain = domain - if params['on']: - service = cover.SERVICE_OPEN_COVER - else: - service = cover.SERVICE_CLOSE_COVER - - elif domain == group.DOMAIN: + if domain == group.DOMAIN: service_domain = HA_DOMAIN service = SERVICE_TURN_ON if params['on'] else SERVICE_TURN_OFF @@ -1047,3 +1026,68 @@ class ModesTrait(_Trait): ATTR_ENTITY_ID: self.state.entity_id, media_player.ATTR_INPUT_SOURCE: source }, blocking=True, context=data.context) + + +@register_trait +class OpenCloseTrait(_Trait): + """Trait to open and close a cover. + + https://developers.google.com/actions/smarthome/traits/openclose + """ + + name = TRAIT_OPENCLOSE + commands = [ + COMMAND_OPENCLOSE + ] + + @staticmethod + def supported(domain, features): + """Test if state is supported.""" + return domain == cover.DOMAIN + + def sync_attributes(self): + """Return opening direction.""" + return {} + + def query_attributes(self): + """Return state query attributes.""" + domain = self.state.domain + response = {} + + if domain == cover.DOMAIN: + position = self.state.attributes.get(cover.ATTR_CURRENT_POSITION) + if position is not None: + response['openPercent'] = position + else: + if self.state.state != cover.STATE_CLOSED: + response['openPercent'] = 100 + else: + response['openPercent'] = 0 + + return response + + async def execute(self, command, data, params): + """Execute an Open, close, Set position command.""" + domain = self.state.domain + + if domain == cover.DOMAIN: + position = self.state.attributes.get(cover.ATTR_CURRENT_POSITION) + if position is not None: + await self.hass.services.async_call( + cover.DOMAIN, cover.SERVICE_SET_COVER_POSITION, { + ATTR_ENTITY_ID: self.state.entity_id, + cover.ATTR_POSITION: params['openPercent'] + }, blocking=True, context=data.context) + else: + if self.state.state != cover.STATE_CLOSED: + if params['openPercent'] < 100: + await self.hass.services.async_call( + cover.DOMAIN, cover.SERVICE_CLOSE_COVER, { + ATTR_ENTITY_ID: self.state.entity_id + }, blocking=True, context=data.context) + else: + if params['openPercent'] > 0: + await self.hass.services.async_call( + cover.DOMAIN, cover.SERVICE_OPEN_COVER, { + ATTR_ENTITY_ID: self.state.entity_id + }, blocking=True, context=data.context) diff --git a/tests/components/google_assistant/__init__.py b/tests/components/google_assistant/__init__.py index a8ea4a3f888..331c6d2d9f5 100644 --- a/tests/components/google_assistant/__init__.py +++ b/tests/components/google_assistant/__init__.py @@ -1,3 +1,5 @@ + + """Tests for the Google Assistant integration.""" DEMO_DEVICES = [{ @@ -93,9 +95,9 @@ DEMO_DEVICES = [{ 'name': 'Living Room Window' }, 'traits': - ['action.devices.traits.OnOff', 'action.devices.traits.Brightness'], + ['action.devices.traits.OpenClose'], 'type': - 'action.devices.types.SWITCH', + 'action.devices.types.BLINDS', 'willReportState': False }, { @@ -105,9 +107,9 @@ DEMO_DEVICES = [{ 'name': 'Hall Window' }, 'traits': - ['action.devices.traits.OnOff', 'action.devices.traits.Brightness'], + ['action.devices.traits.OpenClose'], 'type': - 'action.devices.types.SWITCH', + 'action.devices.types.BLINDS', 'willReportState': False }, { @@ -115,16 +117,18 @@ DEMO_DEVICES = [{ 'name': { 'name': 'Garage Door' }, - 'traits': ['action.devices.traits.OnOff'], - 'type': 'action.devices.types.SWITCH', + 'traits': ['action.devices.traits.OpenClose'], + 'type': + 'action.devices.types.BLINDS', 'willReportState': False }, { 'id': 'cover.kitchen_window', 'name': { 'name': 'Kitchen Window' }, - 'traits': ['action.devices.traits.OnOff'], - 'type': 'action.devices.types.SWITCH', + 'traits': ['action.devices.traits.OpenClose'], + 'type': + 'action.devices.types.BLINDS', 'willReportState': False }, { 'id': 'group.all_covers', diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index e42e4bdc915..a0a710d3d8c 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -83,33 +83,6 @@ async def test_brightness_light(hass): } -async def test_brightness_cover(hass): - """Test brightness trait support for cover domain.""" - assert trait.BrightnessTrait.supported(cover.DOMAIN, - cover.SUPPORT_SET_POSITION) - - trt = trait.BrightnessTrait(hass, State('cover.bla', cover.STATE_OPEN, { - cover.ATTR_CURRENT_POSITION: 75 - }), BASIC_CONFIG) - - assert trt.sync_attributes() == {} - - assert trt.query_attributes() == { - 'brightness': 75 - } - - calls = async_mock_service( - hass, cover.DOMAIN, cover.SERVICE_SET_COVER_POSITION) - await trt.execute( - trait.COMMAND_BRIGHTNESS_ABSOLUTE, BASIC_DATA, - {'brightness': 50}) - assert len(calls) == 1 - assert calls[0].data == { - ATTR_ENTITY_ID: 'cover.bla', - cover.ATTR_POSITION: 50 - } - - async def test_brightness_media_player(hass): """Test brightness trait support for media player domain.""" assert trait.BrightnessTrait.supported(media_player.DOMAIN, @@ -358,46 +331,6 @@ async def test_onoff_light(hass): } -async def test_onoff_cover(hass): - """Test OnOff trait support for cover domain.""" - assert trait.OnOffTrait.supported(cover.DOMAIN, 0) - - trt_on = trait.OnOffTrait(hass, State('cover.bla', cover.STATE_OPEN), - BASIC_CONFIG) - - assert trt_on.sync_attributes() == {} - - assert trt_on.query_attributes() == { - 'on': True - } - - trt_off = trait.OnOffTrait(hass, State('cover.bla', cover.STATE_CLOSED), - BASIC_CONFIG) - - assert trt_off.query_attributes() == { - 'on': False - } - - on_calls = async_mock_service(hass, cover.DOMAIN, cover.SERVICE_OPEN_COVER) - await trt_on.execute( - trait.COMMAND_ONOFF, BASIC_DATA, - {'on': True}) - assert len(on_calls) == 1 - assert on_calls[0].data == { - ATTR_ENTITY_ID: 'cover.bla', - } - - off_calls = async_mock_service(hass, cover.DOMAIN, - cover.SERVICE_CLOSE_COVER) - await trt_on.execute( - trait.COMMAND_ONOFF, BASIC_DATA, - {'on': False}) - assert len(off_calls) == 1 - assert off_calls[0].data == { - ATTR_ENTITY_ID: 'cover.bla', - } - - async def test_onoff_media_player(hass): """Test OnOff trait support for media_player domain.""" assert trait.OnOffTrait.supported(media_player.DOMAIN, 0) @@ -1119,3 +1052,30 @@ async def test_modes(hass): 'entity_id': 'media_player.living_room', 'source': 'media' } + + +async def test_openclose_cover(hass): + """Test cover trait.""" + assert trait.OpenCloseTrait.supported(cover.DOMAIN, + cover.SUPPORT_SET_POSITION) + + trt = trait.OpenCloseTrait(hass, State('cover.bla', cover.STATE_OPEN, { + cover.ATTR_CURRENT_POSITION: 75 + }), BASIC_CONFIG) + + assert trt.sync_attributes() == {} + + assert trt.query_attributes() == { + 'openPercent': 75 + } + + calls = async_mock_service( + hass, cover.DOMAIN, cover.SERVICE_SET_COVER_POSITION) + await trt.execute( + trait.COMMAND_OPENCLOSE, BASIC_DATA, + {'openPercent': 50}) + assert len(calls) == 1 + assert calls[0].data == { + ATTR_ENTITY_ID: 'cover.bla', + cover.ATTR_POSITION: 50 + } From a71fcfb6e5631d9231e99e99a59d093c723beecd Mon Sep 17 00:00:00 2001 From: drjared88 Date: Fri, 29 Mar 2019 21:53:01 -0600 Subject: [PATCH 23/85] Update Amcrest component to SUPPORT_STREAM (#22553) * Update camera.py Update Amcrest component to SUPPORT_STREAM to allow streaming in the UI and Google Assistant. * Update camera.py --- homeassistant/components/amcrest/camera.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/amcrest/camera.py b/homeassistant/components/amcrest/camera.py index 35d5e18fdd3..63c2c720781 100644 --- a/homeassistant/components/amcrest/camera.py +++ b/homeassistant/components/amcrest/camera.py @@ -2,7 +2,8 @@ import asyncio import logging -from homeassistant.components.camera import Camera +from homeassistant.components.camera import ( + Camera, SUPPORT_STREAM) from homeassistant.components.ffmpeg import DATA_FFMPEG from homeassistant.const import CONF_NAME from homeassistant.helpers.aiohttp_client import ( @@ -98,6 +99,11 @@ class AmcrestCam(Camera): """Return the name of this camera.""" return self._name + @property + def supported_features(self): + """Return supported features.""" + return SUPPORT_STREAM + @property def stream_source(self): """Return the source of the stream.""" From de1605936592bde3733f59f9c56e47dbcdcec8d2 Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Sat, 30 Mar 2019 08:30:21 -0700 Subject: [PATCH 24/85] Fix name conflict in tests (#22556) * Fix name conflict in tests * Lint * Lint --- .../components/config/test_config_entries.py | 36 +++++++++++-------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 852a5adf6a2..6d2304433ab 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -31,10 +31,30 @@ def client(hass, hass_client): yield hass.loop.run_until_complete(hass_client()) +@HANDLERS.register('comp1') +class Comp1ConfigFlow: + """Config flow with options flow.""" + + @staticmethod + @callback + def async_get_options_flow(config, options): + """Get options flow.""" + pass + + +@HANDLERS.register('comp2') +class Comp2ConfigFlow: + """Config flow without options flow.""" + + def __init__(self): + """Init.""" + pass + + async def test_get_entries(hass, client): """Test get entries.""" MockConfigEntry( - domain='comp', + domain='comp1', title='Test 1', source='bla', connection_class=core_ce.CONN_CLASS_LOCAL_POLL, @@ -47,18 +67,6 @@ async def test_get_entries(hass, client): connection_class=core_ce.CONN_CLASS_ASSUMED, ).add_to_hass(hass) - class CompConfigFlow: - @staticmethod - @callback - def async_get_options_flow(config, options): - pass - HANDLERS['comp'] = CompConfigFlow() - - class Comp2ConfigFlow: - def __init__(self): - pass - HANDLERS['comp2'] = Comp2ConfigFlow() - resp = await client.get('/api/config/config_entries/entry') assert resp.status == 200 data = await resp.json() @@ -66,7 +74,7 @@ async def test_get_entries(hass, client): entry.pop('entry_id') assert data == [ { - 'domain': 'comp', + 'domain': 'comp1', 'title': 'Test 1', 'source': 'bla', 'state': 'not_loaded', From 2e61ead4fd278cdf2f3ca93aa40726cec76404f1 Mon Sep 17 00:00:00 2001 From: drjared88 Date: Sun, 31 Mar 2019 16:12:55 -0600 Subject: [PATCH 25/85] Update ONVIF component to SUPPORT_STREAM (#22569) * Update Onvif component to SUPPORT_STREAM * Update camera.py * Update camera.py * Update camera.py Remove extra spaces. * lookup URL when camera is added to hass and add extra guards --- homeassistant/components/onvif/camera.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/onvif/camera.py b/homeassistant/components/onvif/camera.py index 36f1b18eebf..09d47c3c0c9 100644 --- a/homeassistant/components/onvif/camera.py +++ b/homeassistant/components/onvif/camera.py @@ -13,7 +13,8 @@ import voluptuous as vol from homeassistant.const import ( CONF_NAME, CONF_HOST, CONF_USERNAME, CONF_PASSWORD, CONF_PORT, ATTR_ENTITY_ID) -from homeassistant.components.camera import Camera, PLATFORM_SCHEMA +from homeassistant.components.camera import ( + Camera, PLATFORM_SCHEMA, SUPPORT_STREAM) from homeassistant.components.camera.const import DOMAIN from homeassistant.components.ffmpeg import ( DATA_FFMPEG, CONF_EXTRA_ARGUMENTS) @@ -187,13 +188,14 @@ class ONVIFHassCamera(Camera): self.hass.data[ONVIF_DATA] = {} self.hass.data[ONVIF_DATA][ENTITIES] = [] self.hass.data[ONVIF_DATA][ENTITIES].append(self) + await self.hass.async_add_executor_job(self.obtain_input_uri) async def async_camera_image(self): """Return a still image response from the camera.""" from haffmpeg.tools import ImageFrame, IMAGE_JPEG if not self._input: - await self.hass.async_add_job(self.obtain_input_uri) + await self.hass.async_add_executor_job(self.obtain_input_uri) if not self._input: return None @@ -210,7 +212,7 @@ class ONVIFHassCamera(Camera): from haffmpeg.camera import CameraMjpeg if not self._input: - await self.hass.async_add_job(self.obtain_input_uri) + await self.hass.async_add_executor_job(self.obtain_input_uri) if not self._input: return None @@ -228,6 +230,18 @@ class ONVIFHassCamera(Camera): finally: await stream.close() + @property + def supported_features(self): + """Return supported features.""" + if self._input: + return SUPPORT_STREAM + return 0 + + @property + def stream_source(self): + """Return the stream source.""" + return self._input + @property def name(self): """Return the name of this camera.""" From 0e42cb64d6dde8bda63274b3eaa4aab9fe977b32 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 31 Mar 2019 17:14:19 -0700 Subject: [PATCH 26/85] Add stream to the default config (#22602) --- homeassistant/components/default_config/__init__.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/default_config/__init__.py b/homeassistant/components/default_config/__init__.py index 888a4d51c95..6743893888d 100644 --- a/homeassistant/components/default_config/__init__.py +++ b/homeassistant/components/default_config/__init__.py @@ -1,7 +1,11 @@ """Component providing default configuration for new users.""" +try: + import av +except ImportError: + av = None DOMAIN = 'default_config' -DEPENDENCIES = ( +DEPENDENCIES = [ 'automation', 'cloud', 'config', @@ -17,7 +21,10 @@ DEPENDENCIES = ( 'system_health', 'updater', 'zeroconf', -) +] +# Only automatically set up the stream component when dependency installed +if av is not None: + DEPENDENCIES.append('stream') async def async_setup(hass, config): From fbb28c401ea459b55a94fac35cc5aa2aaae50b73 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 31 Mar 2019 20:30:30 -0700 Subject: [PATCH 27/85] Bumped version to 0.91.0b4 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 48476c4fa90..ea4b51e4bd9 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 91 -PATCH_VERSION = '0b3' +PATCH_VERSION = '0b4' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 5cb69cf1631b4145da68c13df0234a60c0ddb9d7 Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Mon, 1 Apr 2019 17:43:29 -0700 Subject: [PATCH 28/85] Add trusted networks deprecating warning (#22487) * Add trusted networks deprecating warning * Update auth.py * Update auth.py * Update auth.py * Update auth.py * Tweak --- homeassistant/components/http/auth.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/components/http/auth.py b/homeassistant/components/http/auth.py index 4736ef12391..7b8508894ce 100644 --- a/homeassistant/components/http/auth.py +++ b/homeassistant/components/http/auth.py @@ -190,6 +190,12 @@ def setup_auth(hass, app): elif (trusted_networks and await async_validate_trusted_networks(request)): + _LOGGER.warning( + 'Access from trusted networks without auth token is going to ' + 'be removed in Home Assistant 0.96. Configure the trusted ' + 'networks auth provider or use long-lived access tokens to ' + 'access %s from %s', + request.path, request[KEY_REAL_IP]) authenticated = True elif (support_legacy and HTTP_HEADER_HA_AUTH in request.headers and From 6f345c55c966b761b67671cf3209e07e84f6cee1 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 1 Apr 2019 14:16:16 +0200 Subject: [PATCH 29/85] Hass.io ingress (#22505) * Fix API stream of snapshot / Add ingress * fix lint * Fix stream handling * Cleanup api handling * fix typing * Set proxy header * Use header constant * Enable the ingress setup * fix lint * Fix name * Fix tests * fix lint * forward params * Add tests for ingress * Cleanup cookie handling with aiohttp 3.5 * Add more tests * Fix tests * Fix lint * Fix header handling for steam * forward header too * fix lint * fix flake --- homeassistant/components/hassio/__init__.py | 4 + homeassistant/components/hassio/const.py | 7 +- homeassistant/components/hassio/http.py | 79 +++++--- homeassistant/components/hassio/ingress.py | 210 ++++++++++++++++++++ tests/components/hassio/test_http.py | 80 +++----- tests/components/hassio/test_ingress.py | 162 +++++++++++++++ tests/components/hassio/test_init.py | 2 +- tests/test_util/aiohttp.py | 2 +- 8 files changed, 458 insertions(+), 88 deletions(-) create mode 100644 homeassistant/components/hassio/ingress.py create mode 100644 tests/components/hassio/test_ingress.py diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 073974200a0..e8d04b1596d 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -20,6 +20,7 @@ from .auth import async_setup_auth from .discovery import async_setup_discovery from .handler import HassIO, HassioAPIError from .http import HassIOView +from .ingress import async_setup_ingress _LOGGER = logging.getLogger(__name__) @@ -270,4 +271,7 @@ async def async_setup(hass, config): # Init auth Hass.io feature async_setup_auth(hass) + # Init ingress Hass.io feature + async_setup_ingress(hass, host) + return True diff --git a/homeassistant/components/hassio/const.py b/homeassistant/components/hassio/const.py index 964f94bfb41..e4132562c31 100644 --- a/homeassistant/components/hassio/const.py +++ b/homeassistant/components/hassio/const.py @@ -9,6 +9,7 @@ ATTR_UUID = 'uuid' ATTR_USERNAME = 'username' ATTR_PASSWORD = 'password' -X_HASSIO = 'X-HASSIO-KEY' -X_HASS_USER_ID = 'X-HASS-USER-ID' -X_HASS_IS_ADMIN = 'X-HASS-IS-ADMIN' +X_HASSIO = 'X-Hassio-Key' +X_INGRESS_PATH = "X-Ingress-Path" +X_HASS_USER_ID = 'X-Hass-User-ID' +X_HASS_IS_ADMIN = 'X-Hass-Is-Admin' diff --git a/homeassistant/components/hassio/http.py b/homeassistant/components/hassio/http.py index 01ded9ca11d..7284004d72f 100644 --- a/homeassistant/components/hassio/http.py +++ b/homeassistant/components/hassio/http.py @@ -3,10 +3,11 @@ import asyncio import logging import os import re +from typing import Dict, Union import aiohttp from aiohttp import web -from aiohttp.hdrs import CONTENT_TYPE +from aiohttp.hdrs import CONTENT_TYPE, CONTENT_LENGTH from aiohttp.web_exceptions import HTTPBadGateway import async_timeout @@ -20,7 +21,8 @@ _LOGGER = logging.getLogger(__name__) NO_TIMEOUT = re.compile( r'^(?:' r'|homeassistant/update' - r'|host/update' + r'|hassos/update' + r'|hassos/update/cli' r'|supervisor/update' r'|addons/[^/]+/(?:update|install|rebuild)' r'|snapshots/.+/full' @@ -44,25 +46,26 @@ class HassIOView(HomeAssistantView): url = "/api/hassio/{path:.+}" requires_auth = False - def __init__(self, host, websession): + def __init__(self, host: str, websession: aiohttp.ClientSession): """Initialize a Hass.io base view.""" self._host = host self._websession = websession - async def _handle(self, request, path): + async def _handle( + self, request: web.Request, path: str + ) -> Union[web.Response, web.StreamResponse]: """Route data to Hass.io.""" if _need_auth(path) and not request[KEY_AUTHENTICATED]: return web.Response(status=401) - client = await self._command_proxy(path, request) - - data = await client.read() - return _create_response(client, data) + return await self._command_proxy(path, request) get = _handle post = _handle - async def _command_proxy(self, path, request): + async def _command_proxy( + self, path: str, request: web.Request + ) -> Union[web.Response, web.StreamResponse]: """Return a client request with proxy origin for Hass.io supervisor. This method is a coroutine. @@ -71,29 +74,38 @@ class HassIOView(HomeAssistantView): hass = request.app['hass'] data = None - headers = { - X_HASSIO: os.environ.get('HASSIO_TOKEN', ""), - } - user = request.get('hass_user') - if user is not None: - headers[X_HASS_USER_ID] = request['hass_user'].id - headers[X_HASS_IS_ADMIN] = str(int(request['hass_user'].is_admin)) + headers = _init_header(request) try: with async_timeout.timeout(10, loop=hass.loop): data = await request.read() - if data: - headers[CONTENT_TYPE] = request.content_type - else: - data = None method = getattr(self._websession, request.method.lower()) client = await method( "http://{}/{}".format(self._host, path), data=data, headers=headers, timeout=read_timeout ) + print(client.headers) - return client + # Simple request + if int(client.headers.get(CONTENT_LENGTH, 0)) < 4194000: + # Return Response + body = await client.read() + return web.Response( + content_type=client.content_type, + status=client.status, + body=body, + ) + + # Stream response + response = web.StreamResponse(status=client.status) + response.content_type = client.content_type + + await response.prepare(request) + async for data in client.content.iter_chunked(4096): + await response.write(data) + + return response except aiohttp.ClientError as err: _LOGGER.error("Client error on api %s request %s", path, err) @@ -104,23 +116,30 @@ class HassIOView(HomeAssistantView): raise HTTPBadGateway() -def _create_response(client, data): - """Convert a response from client request.""" - return web.Response( - body=data, - status=client.status, - content_type=client.content_type, - ) +def _init_header(request: web.Request) -> Dict[str, str]: + """Create initial header.""" + headers = { + X_HASSIO: os.environ.get('HASSIO_TOKEN', ""), + CONTENT_TYPE: request.content_type, + } + + # Add user data + user = request.get('hass_user') + if user is not None: + headers[X_HASS_USER_ID] = request['hass_user'].id + headers[X_HASS_IS_ADMIN] = str(int(request['hass_user'].is_admin)) + + return headers -def _get_timeout(path): +def _get_timeout(path: str) -> int: """Return timeout for a URL path.""" if NO_TIMEOUT.match(path): return 0 return 300 -def _need_auth(path): +def _need_auth(path: str) -> bool: """Return if a path need authentication.""" if NO_AUTH.match(path): return False diff --git a/homeassistant/components/hassio/ingress.py b/homeassistant/components/hassio/ingress.py new file mode 100644 index 00000000000..6c1ef389712 --- /dev/null +++ b/homeassistant/components/hassio/ingress.py @@ -0,0 +1,210 @@ +"""Hass.io Add-on ingress service.""" +import asyncio +from ipaddress import ip_address +import os +from typing import Dict, Union + +import aiohttp +from aiohttp import web +from aiohttp import hdrs +from aiohttp.web_exceptions import HTTPBadGateway +from multidict import CIMultiDict + +from homeassistant.core import callback +from homeassistant.components.http import HomeAssistantView +from homeassistant.helpers.typing import HomeAssistantType + +from .const import X_HASSIO, X_INGRESS_PATH + + +@callback +def async_setup_ingress(hass: HomeAssistantType, host: str): + """Auth setup.""" + websession = hass.helpers.aiohttp_client.async_get_clientsession() + + hassio_ingress = HassIOIngress(host, websession) + hass.http.register_view(hassio_ingress) + + +class HassIOIngress(HomeAssistantView): + """Hass.io view to handle base part.""" + + name = "api:hassio:ingress" + url = "/api/hassio_ingress/{addon}/{path:.+}" + requires_auth = False + + def __init__(self, host: str, websession: aiohttp.ClientSession): + """Initialize a Hass.io ingress view.""" + self._host = host + self._websession = websession + + def _create_url(self, addon: str, path: str) -> str: + """Create URL to service.""" + return "http://{}/addons/{}/web/{}".format(self._host, addon, path) + + async def _handle( + self, request: web.Request, addon: str, path: str + ) -> Union[web.Response, web.StreamResponse, web.WebSocketResponse]: + """Route data to Hass.io ingress service.""" + try: + # Websocket + if _is_websocket(request): + return await self._handle_websocket(request, addon, path) + + # Request + return await self._handle_request(request, addon, path) + + except aiohttp.ClientError: + pass + + raise HTTPBadGateway() from None + + get = _handle + post = _handle + put = _handle + delete = _handle + + async def _handle_websocket( + self, request: web.Request, addon: str, path: str + ) -> web.WebSocketResponse: + """Ingress route for websocket.""" + ws_server = web.WebSocketResponse() + await ws_server.prepare(request) + + url = self._create_url(addon, path) + source_header = _init_header(request, addon) + + # Start proxy + async with self._websession.ws_connect( + url, headers=source_header + ) as ws_client: + # Proxy requests + await asyncio.wait( + [ + _websocket_forward(ws_server, ws_client), + _websocket_forward(ws_client, ws_server), + ], + return_when=asyncio.FIRST_COMPLETED + ) + + return ws_server + + async def _handle_request( + self, request: web.Request, addon: str, path: str + ) -> Union[web.Response, web.StreamResponse]: + """Ingress route for request.""" + url = self._create_url(addon, path) + data = await request.read() + source_header = _init_header(request, addon) + + async with self._websession.request( + request.method, url, headers=source_header, + params=request.query, data=data, cookies=request.cookies + ) as result: + headers = _response_header(result) + + # Simple request + if hdrs.CONTENT_LENGTH in result.headers and \ + int(result.headers.get(hdrs.CONTENT_LENGTH, 0)) < 4194000: + # Return Response + body = await result.read() + return web.Response( + headers=headers, + status=result.status, + body=body + ) + + # Stream response + response = web.StreamResponse( + status=result.status, headers=headers) + response.content_type = result.content_type + + try: + await response.prepare(request) + async for data in result.content: + await response.write(data) + + except (aiohttp.ClientError, aiohttp.ClientPayloadError): + pass + + return response + + +def _init_header( + request: web.Request, addon: str +) -> Union[CIMultiDict, Dict[str, str]]: + """Create initial header.""" + headers = {} + + # filter flags + for name, value in request.headers.items(): + if name in (hdrs.CONTENT_LENGTH, hdrs.CONTENT_TYPE): + continue + headers[name] = value + + # Inject token / cleanup later on Supervisor + headers[X_HASSIO] = os.environ.get('HASSIO_TOKEN', "") + + # Ingress information + headers[X_INGRESS_PATH] = "/api/hassio_ingress/{}".format(addon) + + # Set X-Forwarded-For + forward_for = request.headers.get(hdrs.X_FORWARDED_FOR) + connected_ip = ip_address(request.transport.get_extra_info('peername')[0]) + if forward_for: + forward_for = "{}, {!s}".format(forward_for, connected_ip) + else: + forward_for = "{!s}".format(connected_ip) + headers[hdrs.X_FORWARDED_FOR] = forward_for + + # Set X-Forwarded-Host + forward_host = request.headers.get(hdrs.X_FORWARDED_HOST) + if not forward_host: + forward_host = request.host + headers[hdrs.X_FORWARDED_HOST] = forward_host + + # Set X-Forwarded-Proto + forward_proto = request.headers.get(hdrs.X_FORWARDED_PROTO) + if not forward_proto: + forward_proto = request.url.scheme + headers[hdrs.X_FORWARDED_PROTO] = forward_proto + + return headers + + +def _response_header(response: aiohttp.ClientResponse) -> Dict[str, str]: + """Create response header.""" + headers = {} + + for name, value in response.headers.items(): + if name in (hdrs.TRANSFER_ENCODING, hdrs.CONTENT_LENGTH, + hdrs.CONTENT_TYPE): + continue + headers[name] = value + + return headers + + +def _is_websocket(request: web.Request) -> bool: + """Return True if request is a websocket.""" + headers = request.headers + + if headers.get(hdrs.CONNECTION) == "Upgrade" and \ + headers.get(hdrs.UPGRADE) == "websocket": + return True + return False + + +async def _websocket_forward(ws_from, ws_to): + """Handle websocket message directly.""" + async for msg in ws_from: + if msg.type == aiohttp.WSMsgType.TEXT: + await ws_to.send_str(msg.data) + elif msg.type == aiohttp.WSMsgType.BINARY: + await ws_to.send_bytes(msg.data) + elif msg.type == aiohttp.WSMsgType.PING: + await ws_to.ping() + elif msg.type == aiohttp.WSMsgType.PONG: + await ws_to.pong() + elif ws_to.closed: + await ws_to.close(code=ws_to.close_code, message=msg.extra) diff --git a/tests/components/hassio/test_http.py b/tests/components/hassio/test_http.py index 3f58c6e697e..3a58048735b 100644 --- a/tests/components/hassio/test_http.py +++ b/tests/components/hassio/test_http.py @@ -1,29 +1,22 @@ """The tests for the hassio component.""" import asyncio -from unittest.mock import patch, Mock, MagicMock +from unittest.mock import patch import pytest from homeassistant.const import HTTP_HEADER_HA_AUTH -from tests.common import mock_coro from . import API_PASSWORD @asyncio.coroutine -def test_forward_request(hassio_client): +def test_forward_request(hassio_client, aioclient_mock): """Test fetching normal path.""" - response = MagicMock() - response.read.return_value = mock_coro('data') + aioclient_mock.post("http://127.0.0.1/beer", text="response") - with patch('homeassistant.components.hassio.HassIOView._command_proxy', - Mock(return_value=mock_coro(response))), \ - patch('homeassistant.components.hassio.http' - '._create_response') as mresp: - mresp.return_value = 'response' - resp = yield from hassio_client.post('/api/hassio/beer', headers={ - HTTP_HEADER_HA_AUTH: API_PASSWORD - }) + resp = yield from hassio_client.post('/api/hassio/beer', headers={ + HTTP_HEADER_HA_AUTH: API_PASSWORD + }) # Check we got right response assert resp.status == 200 @@ -31,8 +24,7 @@ def test_forward_request(hassio_client): assert body == 'response' # Check we forwarded command - assert len(mresp.mock_calls) == 1 - assert mresp.mock_calls[0][1] == (response, 'data') + assert len(aioclient_mock.mock_calls) == 1 @asyncio.coroutine @@ -55,18 +47,13 @@ def test_auth_required_forward_request(hassio_noauth_client, build_type): 'app/index.html', 'app/hassio-app.html', 'app/index.html', 'app/hassio-app.html', 'app/some-chunk.js', 'app/app.js', ]) -def test_forward_request_no_auth_for_panel(hassio_client, build_type): +def test_forward_request_no_auth_for_panel( + hassio_client, build_type, aioclient_mock): """Test no auth needed for .""" - response = MagicMock() - response.read.return_value = mock_coro('data') + aioclient_mock.get( + "http://127.0.0.1/{}".format(build_type), text="response") - with patch('homeassistant.components.hassio.HassIOView._command_proxy', - Mock(return_value=mock_coro(response))), \ - patch('homeassistant.components.hassio.http.' - '_create_response') as mresp: - mresp.return_value = 'response' - resp = yield from hassio_client.get( - '/api/hassio/{}'.format(build_type)) + resp = yield from hassio_client.get('/api/hassio/{}'.format(build_type)) # Check we got right response assert resp.status == 200 @@ -74,22 +61,16 @@ def test_forward_request_no_auth_for_panel(hassio_client, build_type): assert body == 'response' # Check we forwarded command - assert len(mresp.mock_calls) == 1 - assert mresp.mock_calls[0][1] == (response, 'data') + assert len(aioclient_mock.mock_calls) == 1 @asyncio.coroutine -def test_forward_request_no_auth_for_logo(hassio_client): +def test_forward_request_no_auth_for_logo(hassio_client, aioclient_mock): """Test no auth needed for .""" - response = MagicMock() - response.read.return_value = mock_coro('data') + aioclient_mock.get( + "http://127.0.0.1/addons/bl_b392/logo", text="response") - with patch('homeassistant.components.hassio.HassIOView._command_proxy', - Mock(return_value=mock_coro(response))), \ - patch('homeassistant.components.hassio.http.' - '_create_response') as mresp: - mresp.return_value = 'response' - resp = yield from hassio_client.get('/api/hassio/addons/bl_b392/logo') + resp = yield from hassio_client.get('/api/hassio/addons/bl_b392/logo') # Check we got right response assert resp.status == 200 @@ -97,24 +78,18 @@ def test_forward_request_no_auth_for_logo(hassio_client): assert body == 'response' # Check we forwarded command - assert len(mresp.mock_calls) == 1 - assert mresp.mock_calls[0][1] == (response, 'data') + assert len(aioclient_mock.mock_calls) == 1 @asyncio.coroutine -def test_forward_log_request(hassio_client): +def test_forward_log_request(hassio_client, aioclient_mock): """Test fetching normal log path doesn't remove ANSI color escape codes.""" - response = MagicMock() - response.read.return_value = mock_coro('data') + aioclient_mock.get( + "http://127.0.0.1/beer/logs", text="\033[32mresponse\033[0m") - with patch('homeassistant.components.hassio.HassIOView._command_proxy', - Mock(return_value=mock_coro(response))), \ - patch('homeassistant.components.hassio.http.' - '_create_response') as mresp: - mresp.return_value = '\033[32mresponse\033[0m' - resp = yield from hassio_client.get('/api/hassio/beer/logs', headers={ - HTTP_HEADER_HA_AUTH: API_PASSWORD - }) + resp = yield from hassio_client.get('/api/hassio/beer/logs', headers={ + HTTP_HEADER_HA_AUTH: API_PASSWORD + }) # Check we got right response assert resp.status == 200 @@ -122,8 +97,7 @@ def test_forward_log_request(hassio_client): assert body == '\033[32mresponse\033[0m' # Check we forwarded command - assert len(mresp.mock_calls) == 1 - assert mresp.mock_calls[0][1] == (response, 'data') + assert len(aioclient_mock.mock_calls) == 1 @asyncio.coroutine @@ -151,5 +125,5 @@ async def test_forwarding_user_info(hassio_client, hass_admin_user, assert len(aioclient_mock.mock_calls) == 1 req_headers = aioclient_mock.mock_calls[0][-1] - req_headers['X-HASS-USER-ID'] == hass_admin_user.id - req_headers['X-HASS-IS-ADMIN'] == '1' + req_headers['X-Hass-User-ID'] == hass_admin_user.id + req_headers['X-Hass-Is-Admin'] == '1' diff --git a/tests/components/hassio/test_ingress.py b/tests/components/hassio/test_ingress.py new file mode 100644 index 00000000000..4e071ba24fd --- /dev/null +++ b/tests/components/hassio/test_ingress.py @@ -0,0 +1,162 @@ +"""The tests for the hassio component.""" + +from aiohttp.hdrs import X_FORWARDED_FOR, X_FORWARDED_HOST, X_FORWARDED_PROTO +from aiohttp.client_exceptions import WSServerHandshakeError +import pytest + + +@pytest.mark.parametrize( + 'build_type', [ + ("a3_vl", "test/beer/ping?index=1"), ("core", "index.html"), + ("local", "panel/config"), ("jk_921", "editor.php?idx=3&ping=5") + ]) +async def test_ingress_request_get( + hassio_client, build_type, aioclient_mock): + """Test no auth needed for .""" + aioclient_mock.get("http://127.0.0.1/addons/{}/web/{}".format( + build_type[0], build_type[1]), text="test") + + resp = await hassio_client.get( + '/api/hassio_ingress/{}/{}'.format(build_type[0], build_type[1]), + headers={"X-Test-Header": "beer"} + ) + + # Check we got right response + assert resp.status == 200 + body = await resp.text() + assert body == "test" + + # Check we forwarded command + assert len(aioclient_mock.mock_calls) == 1 + assert aioclient_mock.mock_calls[-1][3]["X-Hassio-Key"] == "123456" + assert aioclient_mock.mock_calls[-1][3]["X-Ingress-Path"] == \ + "/api/hassio_ingress/{}".format(build_type[0]) + assert aioclient_mock.mock_calls[-1][3]["X-Test-Header"] == "beer" + assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_FOR] + assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_HOST] + assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_PROTO] + + +@pytest.mark.parametrize( + 'build_type', [ + ("a3_vl", "test/beer/ping?index=1"), ("core", "index.html"), + ("local", "panel/config"), ("jk_921", "editor.php?idx=3&ping=5") + ]) +async def test_ingress_request_post( + hassio_client, build_type, aioclient_mock): + """Test no auth needed for .""" + aioclient_mock.post("http://127.0.0.1/addons/{}/web/{}".format( + build_type[0], build_type[1]), text="test") + + resp = await hassio_client.post( + '/api/hassio_ingress/{}/{}'.format(build_type[0], build_type[1]), + headers={"X-Test-Header": "beer"} + ) + + # Check we got right response + assert resp.status == 200 + body = await resp.text() + assert body == "test" + + # Check we forwarded command + assert len(aioclient_mock.mock_calls) == 1 + assert aioclient_mock.mock_calls[-1][3]["X-Hassio-Key"] == "123456" + assert aioclient_mock.mock_calls[-1][3]["X-Ingress-Path"] == \ + "/api/hassio_ingress/{}".format(build_type[0]) + assert aioclient_mock.mock_calls[-1][3]["X-Test-Header"] == "beer" + assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_FOR] + assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_HOST] + assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_PROTO] + + +@pytest.mark.parametrize( + 'build_type', [ + ("a3_vl", "test/beer/ping?index=1"), ("core", "index.html"), + ("local", "panel/config"), ("jk_921", "editor.php?idx=3&ping=5") + ]) +async def test_ingress_request_put( + hassio_client, build_type, aioclient_mock): + """Test no auth needed for .""" + aioclient_mock.put("http://127.0.0.1/addons/{}/web/{}".format( + build_type[0], build_type[1]), text="test") + + resp = await hassio_client.put( + '/api/hassio_ingress/{}/{}'.format(build_type[0], build_type[1]), + headers={"X-Test-Header": "beer"} + ) + + # Check we got right response + assert resp.status == 200 + body = await resp.text() + assert body == "test" + + # Check we forwarded command + assert len(aioclient_mock.mock_calls) == 1 + assert aioclient_mock.mock_calls[-1][3]["X-Hassio-Key"] == "123456" + assert aioclient_mock.mock_calls[-1][3]["X-Ingress-Path"] == \ + "/api/hassio_ingress/{}".format(build_type[0]) + assert aioclient_mock.mock_calls[-1][3]["X-Test-Header"] == "beer" + assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_FOR] + assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_HOST] + assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_PROTO] + + +@pytest.mark.parametrize( + 'build_type', [ + ("a3_vl", "test/beer/ping?index=1"), ("core", "index.html"), + ("local", "panel/config"), ("jk_921", "editor.php?idx=3&ping=5") + ]) +async def test_ingress_request_delete( + hassio_client, build_type, aioclient_mock): + """Test no auth needed for .""" + aioclient_mock.delete("http://127.0.0.1/addons/{}/web/{}".format( + build_type[0], build_type[1]), text="test") + + resp = await hassio_client.delete( + '/api/hassio_ingress/{}/{}'.format(build_type[0], build_type[1]), + headers={"X-Test-Header": "beer"} + ) + + # Check we got right response + assert resp.status == 200 + body = await resp.text() + assert body == "test" + + # Check we forwarded command + assert len(aioclient_mock.mock_calls) == 1 + assert aioclient_mock.mock_calls[-1][3]["X-Hassio-Key"] == "123456" + assert aioclient_mock.mock_calls[-1][3]["X-Ingress-Path"] == \ + "/api/hassio_ingress/{}".format(build_type[0]) + assert aioclient_mock.mock_calls[-1][3]["X-Test-Header"] == "beer" + assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_FOR] + assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_HOST] + assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_PROTO] + + +@pytest.mark.parametrize( + 'build_type', [ + ("a3_vl", "test/beer/ws"), ("core", "ws.php"), + ("local", "panel/config/stream"), ("jk_921", "hulk") + ]) +async def test_ingress_websocket( + hassio_client, build_type, aioclient_mock): + """Test no auth needed for .""" + aioclient_mock.get("http://127.0.0.1/addons/{}/web/{}".format( + build_type[0], build_type[1])) + + # Ignore error because we can setup a full IO infrastructure + with pytest.raises(WSServerHandshakeError): + await hassio_client.ws_connect( + '/api/hassio_ingress/{}/{}'.format(build_type[0], build_type[1]), + headers={"X-Test-Header": "beer"} + ) + + # Check we forwarded command + assert len(aioclient_mock.mock_calls) == 1 + assert aioclient_mock.mock_calls[-1][3]["X-Hassio-Key"] == "123456" + assert aioclient_mock.mock_calls[-1][3]["X-Ingress-Path"] == \ + "/api/hassio_ingress/{}".format(build_type[0]) + assert aioclient_mock.mock_calls[-1][3]["X-Test-Header"] == "beer" + assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_FOR] + assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_HOST] + assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_PROTO] diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index fc4661e7544..f1f148f8495 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -207,7 +207,7 @@ def test_setup_hassio_no_additional_data(hass, aioclient_mock): assert result assert aioclient_mock.call_count == 3 - assert aioclient_mock.mock_calls[-1][3]['X-HASSIO-KEY'] == "123456" + assert aioclient_mock.mock_calls[-1][3]['X-Hassio-Key'] == "123456" @asyncio.coroutine diff --git a/tests/test_util/aiohttp.py b/tests/test_util/aiohttp.py index 8b3b057bfc0..ab759f03058 100644 --- a/tests/test_util/aiohttp.py +++ b/tests/test_util/aiohttp.py @@ -102,7 +102,7 @@ class AiohttpClientMocker: async def match_request(self, method, url, *, data=None, auth=None, params=None, headers=None, allow_redirects=None, - timeout=None, json=None): + timeout=None, json=None, cookies=None): """Match a request against pre-registered requests.""" data = data or json url = URL(url) From e0b4e88544df16b13b1eaecee335433d0d1564fd Mon Sep 17 00:00:00 2001 From: Chris Helming Date: Sun, 31 Mar 2019 00:01:58 -0400 Subject: [PATCH 30/85] Update Foscam component to support stream source (#22568) * Update Foscam to support stream source * Removing spaces and tabs * Changing to Python3-style string formatting * Adding '_media_port' to hopefully cover other models --- homeassistant/components/foscam/camera.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/foscam/camera.py b/homeassistant/components/foscam/camera.py index ceec57f7755..a11d2f48f62 100644 --- a/homeassistant/components/foscam/camera.py +++ b/homeassistant/components/foscam/camera.py @@ -8,7 +8,8 @@ import logging import voluptuous as vol -from homeassistant.components.camera import (Camera, PLATFORM_SCHEMA) +from homeassistant.components.camera import ( + Camera, PLATFORM_SCHEMA, SUPPORT_STREAM) from homeassistant.const import ( CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_PORT) from homeassistant.helpers import config_validation as cv @@ -57,6 +58,8 @@ class FoscamCam(Camera): self._foscam_session = FoscamCamera( ip_address, port, self._username, self._password, verbose=False) + self._media_port = self._foscam_session.get_port_info()[1]['mediaPort'] + def camera_image(self): """Return a still image response from the camera.""" # Send the request to snap a picture and return raw jpg data @@ -67,6 +70,20 @@ class FoscamCam(Camera): return response + @property + def supported_features(self): + """Return supported features.""" + return SUPPORT_STREAM + + @property + def stream_source(self): + """Return the stream source.""" + return 'rtsp://{}:{}@{}:{}/videoMain'.format( + self._username, + self._password, + self._foscam_session.host, + self._media_port) + @property def motion_detection_enabled(self): """Camera Motion Detection Status.""" From 56c75d77063ad39bfe926c9a6d4e2ccaabc897a8 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 1 Apr 2019 19:00:25 +0200 Subject: [PATCH 31/85] Update face_recognition to 1.2.3 (#22622) --- homeassistant/components/dlib_face_detect/image_processing.py | 2 +- homeassistant/components/dlib_face_identify/image_processing.py | 2 +- requirements_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/dlib_face_detect/image_processing.py b/homeassistant/components/dlib_face_detect/image_processing.py index cb9ea5ff5f9..fea756395e4 100644 --- a/homeassistant/components/dlib_face_detect/image_processing.py +++ b/homeassistant/components/dlib_face_detect/image_processing.py @@ -13,7 +13,7 @@ from homeassistant.components.image_processing import PLATFORM_SCHEMA # noqa from homeassistant.components.image_processing import ( ImageProcessingFaceEntity, CONF_SOURCE, CONF_ENTITY_ID, CONF_NAME) -REQUIREMENTS = ['face_recognition==1.0.0'] +REQUIREMENTS = ['face_recognition==1.2.3'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/dlib_face_identify/image_processing.py b/homeassistant/components/dlib_face_identify/image_processing.py index d8c3f5f621f..6611fb0edfb 100644 --- a/homeassistant/components/dlib_face_identify/image_processing.py +++ b/homeassistant/components/dlib_face_identify/image_processing.py @@ -15,7 +15,7 @@ from homeassistant.components.image_processing import ( CONF_NAME) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['face_recognition==1.0.0'] +REQUIREMENTS = ['face_recognition==1.2.3'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 09a447b2a20..057790cbc6a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -415,7 +415,7 @@ evohomeclient==0.2.8 # homeassistant.components.dlib_face_detect.image_processing # homeassistant.components.dlib_face_identify.image_processing -# face_recognition==1.0.0 +# face_recognition==1.2.3 # homeassistant.components.fastdotcom fastdotcom==0.0.3 From c7576999ca31bd426bd0b5ae2f69059e5efe932c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 1 Apr 2019 10:20:13 -0700 Subject: [PATCH 32/85] Disable Z-Wave autoheal (#22628) --- homeassistant/components/zwave/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/zwave/const.py b/homeassistant/components/zwave/const.py index fece48655df..67b5341a4e6 100644 --- a/homeassistant/components/zwave/const.py +++ b/homeassistant/components/zwave/const.py @@ -29,7 +29,7 @@ CONF_USB_STICK_PATH = 'usb_path' CONF_CONFIG_PATH = 'config_path' CONF_NETWORK_KEY = 'network_key' -DEFAULT_CONF_AUTOHEAL = True +DEFAULT_CONF_AUTOHEAL = False DEFAULT_CONF_USB_STICK_PATH = '/zwaveusbstick' DEFAULT_POLLING_INTERVAL = 60000 DEFAULT_DEBUG = False From a5c7f131eefb98a44a49cd31e1838a19b51cee0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Mon, 1 Apr 2019 19:33:38 +0200 Subject: [PATCH 33/85] Handle disonnect bug in Tibber library (#22629) --- homeassistant/components/tibber/__init__.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tibber/__init__.py b/homeassistant/components/tibber/__init__.py index 135437801d9..19cf6fe6525 100644 --- a/homeassistant/components/tibber/__init__.py +++ b/homeassistant/components/tibber/__init__.py @@ -11,7 +11,7 @@ from homeassistant.const import (EVENT_HOMEASSISTANT_STOP, CONF_ACCESS_TOKEN, from homeassistant.helpers import discovery from homeassistant.helpers.aiohttp_client import async_get_clientsession -REQUIREMENTS = ['pyTibber==0.10.0'] +REQUIREMENTS = ['pyTibber==0.10.1'] DOMAIN = 'tibber' diff --git a/requirements_all.txt b/requirements_all.txt index 057790cbc6a..a773c9c8c31 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -922,7 +922,7 @@ pyRFXtrx==0.23 # pySwitchmate==0.4.5 # homeassistant.components.tibber -pyTibber==0.10.0 +pyTibber==0.10.1 # homeassistant.components.dlink.switch pyW215==0.6.0 From 6d741d68b74e81be4c72399160aee9d69213bb24 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 2 Apr 2019 02:41:08 +0200 Subject: [PATCH 34/85] Support GET params for websocket ingress path (#22638) --- homeassistant/components/hassio/ingress.py | 5 +++++ tests/components/hassio/test_ingress.py | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/hassio/ingress.py b/homeassistant/components/hassio/ingress.py index 6c1ef389712..04241f53de9 100644 --- a/homeassistant/components/hassio/ingress.py +++ b/homeassistant/components/hassio/ingress.py @@ -71,9 +71,14 @@ class HassIOIngress(HomeAssistantView): ws_server = web.WebSocketResponse() await ws_server.prepare(request) + # Preparing url = self._create_url(addon, path) source_header = _init_header(request, addon) + # Support GET query + if request.query_string: + url = "{}?{}".format(url, request.query_string) + # Start proxy async with self._websession.ws_connect( url, headers=source_header diff --git a/tests/components/hassio/test_ingress.py b/tests/components/hassio/test_ingress.py index 4e071ba24fd..4b72eda4c25 100644 --- a/tests/components/hassio/test_ingress.py +++ b/tests/components/hassio/test_ingress.py @@ -136,7 +136,8 @@ async def test_ingress_request_delete( @pytest.mark.parametrize( 'build_type', [ ("a3_vl", "test/beer/ws"), ("core", "ws.php"), - ("local", "panel/config/stream"), ("jk_921", "hulk") + ("local", "panel/config/stream"), ("jk_921", "hulk"), + ("demo", "ws/connection?id=9&token=SJAKWS283") ]) async def test_ingress_websocket( hassio_client, build_type, aioclient_mock): From e3ca1e6203fabc2363e248186e548122fed5c254 Mon Sep 17 00:00:00 2001 From: Chris Helming Date: Tue, 2 Apr 2019 12:59:38 -0400 Subject: [PATCH 35/85] Return 0 for failed Foscam streams (#22651) * Update Foscam to support stream source * Removing spaces and tabs * Changing to Python3-style string formatting * Adding '_media_port' to hopefully cover other models * changing logic for success and return none --- homeassistant/components/foscam/camera.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/foscam/camera.py b/homeassistant/components/foscam/camera.py index a11d2f48f62..b6f2162d57a 100644 --- a/homeassistant/components/foscam/camera.py +++ b/homeassistant/components/foscam/camera.py @@ -58,7 +58,10 @@ class FoscamCam(Camera): self._foscam_session = FoscamCamera( ip_address, port, self._username, self._password, verbose=False) - self._media_port = self._foscam_session.get_port_info()[1]['mediaPort'] + self._media_port = None + result, response = self._foscam_session.get_port_info() + if result == 0: + self._media_port = response['mediaPort'] def camera_image(self): """Return a still image response from the camera.""" @@ -73,16 +76,20 @@ class FoscamCam(Camera): @property def supported_features(self): """Return supported features.""" - return SUPPORT_STREAM + if self._media_port: + return SUPPORT_STREAM + return 0 @property def stream_source(self): """Return the stream source.""" - return 'rtsp://{}:{}@{}:{}/videoMain'.format( - self._username, - self._password, - self._foscam_session.host, - self._media_port) + if self._media_port: + return 'rtsp://{}:{}@{}:{}/videoMain'.format( + self._username, + self._password, + self._foscam_session.host, + self._media_port) + return None @property def motion_detection_enabled(self): From 31ac965b163fb95681a0c26e52533f2db990f3ea Mon Sep 17 00:00:00 2001 From: Jc2k Date: Tue, 2 Apr 2019 08:57:58 +0100 Subject: [PATCH 36/85] Fix racy homekit_controller platform setup caused by #22368 (#22655) --- homeassistant/components/homekit_controller/__init__.py | 3 +-- homeassistant/components/homekit_controller/connection.py | 5 ++++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index 44af8bffe26..2a43d0ac9ce 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -191,8 +191,7 @@ def setup(hass, config): return _LOGGER.debug('Discovered unique device %s', hkid) - device = HKDevice(hass, host, port, model, hkid, config_num, config) - hass.data[KNOWN_DEVICES][hkid] = device + HKDevice(hass, host, port, model, hkid, config_num, config) hass.data[KNOWN_DEVICES] = {} discovery.listen(hass, SERVICE_HOMEKIT, discovery_dispatch) diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py index d875b91eb2c..2ca568b547f 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -7,7 +7,8 @@ from homeassistant.helpers import discovery from homeassistant.helpers.event import call_later from .const import ( - CONTROLLER, DOMAIN, HOMEKIT_ACCESSORY_DISPATCH, PAIRING_FILE, HOMEKIT_DIR + CONTROLLER, DOMAIN, HOMEKIT_ACCESSORY_DISPATCH, KNOWN_DEVICES, + PAIRING_FILE, HOMEKIT_DIR ) @@ -76,6 +77,8 @@ class HKDevice(): self.pairing = self.controller.pairings.get(hkid) + hass.data[KNOWN_DEVICES][hkid] = self + if self.pairing is not None: self.accessory_setup() else: From 33575962156f6cbff0928b020230258065c0ae91 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 2 Apr 2019 11:42:01 -0700 Subject: [PATCH 37/85] Bumped version to 0.91.0b5 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index ea4b51e4bd9..4091bc2ed02 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 91 -PATCH_VERSION = '0b4' +PATCH_VERSION = '0b5' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 51c7cbc6b93a8bb2ed2acdba84897e15af01c3a2 Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Wed, 3 Apr 2019 05:21:25 -0700 Subject: [PATCH 38/85] Add mobile_app notify platform (#22580) * Add mobile_app notify platform * Requested changes * Fix incorrect param for status code * Move push_registrations to notify platform file * Trim down registration information sent in push * quotes * Use async version of load_platform * Add warning for duplicate device names * Switch to async_get_service * add mobile_app.notify test * Update tests/components/mobile_app/test_notify.py * Update tests/components/mobile_app/test_notify.py --- .../components/mobile_app/__init__.py | 13 +- homeassistant/components/mobile_app/const.py | 2 + homeassistant/components/mobile_app/notify.py | 134 ++++++++++++++++++ tests/components/mobile_app/test_notify.py | 81 +++++++++++ 4 files changed, 225 insertions(+), 5 deletions(-) create mode 100644 homeassistant/components/mobile_app/notify.py create mode 100644 tests/components/mobile_app/test_notify.py diff --git a/homeassistant/components/mobile_app/__init__.py b/homeassistant/components/mobile_app/__init__.py index ecbe8d70847..a4ae78959cf 100644 --- a/homeassistant/components/mobile_app/__init__.py +++ b/homeassistant/components/mobile_app/__init__.py @@ -2,13 +2,13 @@ from homeassistant import config_entries from homeassistant.const import CONF_WEBHOOK_ID from homeassistant.components.webhook import async_register as webhook_register -from homeassistant.helpers import device_registry as dr +from homeassistant.helpers import device_registry as dr, discovery from homeassistant.helpers.typing import ConfigType, HomeAssistantType -from .const import (ATTR_DEVICE_ID, ATTR_DEVICE_NAME, ATTR_MANUFACTURER, - ATTR_MODEL, ATTR_OS_VERSION, DATA_BINARY_SENSOR, - DATA_CONFIG_ENTRIES, DATA_DELETED_IDS, DATA_DEVICES, - DATA_SENSOR, DATA_STORE, DOMAIN, STORAGE_KEY, +from .const import (ATTR_DEVICE_ID, ATTR_DEVICE_NAME, + ATTR_MANUFACTURER, ATTR_MODEL, ATTR_OS_VERSION, + DATA_BINARY_SENSOR, DATA_CONFIG_ENTRIES, DATA_DELETED_IDS, + DATA_DEVICES, DATA_SENSOR, DATA_STORE, DOMAIN, STORAGE_KEY, STORAGE_VERSION) from .http_api import RegistrationsView @@ -52,6 +52,9 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType): except ValueError: pass + hass.async_create_task(discovery.async_load_platform( + hass, 'notify', DOMAIN, {}, config)) + return True diff --git a/homeassistant/components/mobile_app/const.py b/homeassistant/components/mobile_app/const.py index 3aa4626da29..61c50e97c6e 100644 --- a/homeassistant/components/mobile_app/const.py +++ b/homeassistant/components/mobile_app/const.py @@ -40,6 +40,8 @@ ATTR_MANUFACTURER = 'manufacturer' ATTR_MODEL = 'model' ATTR_OS_NAME = 'os_name' ATTR_OS_VERSION = 'os_version' +ATTR_PUSH_TOKEN = 'push_token' +ATTR_PUSH_URL = 'push_url' ATTR_SUPPORTS_ENCRYPTION = 'supports_encryption' ATTR_EVENT_DATA = 'event_data' diff --git a/homeassistant/components/mobile_app/notify.py b/homeassistant/components/mobile_app/notify.py new file mode 100644 index 00000000000..0120b1a6ffb --- /dev/null +++ b/homeassistant/components/mobile_app/notify.py @@ -0,0 +1,134 @@ +"""Support for mobile_app push notifications.""" +import asyncio +from datetime import datetime, timezone +import logging + +import async_timeout + +from homeassistant.components.notify import ( + ATTR_DATA, ATTR_MESSAGE, ATTR_TARGET, ATTR_TITLE, ATTR_TITLE_DEFAULT, + BaseNotificationService) +from homeassistant.components.mobile_app.const import ( + ATTR_APP_DATA, ATTR_APP_ID, ATTR_APP_VERSION, ATTR_DEVICE_NAME, + ATTR_OS_VERSION, ATTR_PUSH_TOKEN, ATTR_PUSH_URL, DATA_CONFIG_ENTRIES, + DOMAIN) +from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.util.dt as dt_util + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['mobile_app'] + + +def push_registrations(hass): + """Return a dictionary of push enabled registrations.""" + targets = {} + for webhook_id, entry in hass.data[DOMAIN][DATA_CONFIG_ENTRIES].items(): + data = entry.data + app_data = data[ATTR_APP_DATA] + if ATTR_PUSH_TOKEN in app_data and ATTR_PUSH_URL in app_data: + device_name = data[ATTR_DEVICE_NAME] + if device_name in targets: + _LOGGER.warning("Found duplicate device name %s", device_name) + continue + targets[device_name] = webhook_id + return targets + + +# pylint: disable=invalid-name +def log_rate_limits(hass, device_name, resp, level=logging.INFO): + """Output rate limit log line at given level.""" + rate_limits = resp['rateLimits'] + resetsAt = dt_util.parse_datetime(rate_limits['resetsAt']) + resetsAtTime = resetsAt - datetime.now(timezone.utc) + rate_limit_msg = ("mobile_app push notification rate limits for %s: " + "%d sent, %d allowed, %d errors, " + "resets in %s") + _LOGGER.log(level, rate_limit_msg, + device_name, + rate_limits['successful'], + rate_limits['maximum'], rate_limits['errors'], + str(resetsAtTime).split(".")[0]) + + +async def async_get_service(hass, config, discovery_info=None): + """Get the mobile_app notification service.""" + session = async_get_clientsession(hass) + return MobileAppNotificationService(session) + + +class MobileAppNotificationService(BaseNotificationService): + """Implement the notification service for mobile_app.""" + + def __init__(self, session): + """Initialize the service.""" + self._session = session + + @property + def targets(self): + """Return a dictionary of registered targets.""" + return push_registrations(self.hass) + + async def async_send_message(self, message="", **kwargs): + """Send a message to the Lambda APNS gateway.""" + data = {ATTR_MESSAGE: message} + + if kwargs.get(ATTR_TITLE) is not None: + # Remove default title from notifications. + if kwargs.get(ATTR_TITLE) != ATTR_TITLE_DEFAULT: + data[ATTR_TITLE] = kwargs.get(ATTR_TITLE) + + targets = kwargs.get(ATTR_TARGET) + + if not targets: + targets = push_registrations(self.hass) + + if kwargs.get(ATTR_DATA) is not None: + data[ATTR_DATA] = kwargs.get(ATTR_DATA) + + for target in targets: + + entry = self.hass.data[DOMAIN][DATA_CONFIG_ENTRIES][target] + entry_data = entry.data + + app_data = entry_data[ATTR_APP_DATA] + push_token = app_data[ATTR_PUSH_TOKEN] + push_url = app_data[ATTR_PUSH_URL] + + data[ATTR_PUSH_TOKEN] = push_token + + reg_info = { + ATTR_APP_ID: entry_data[ATTR_APP_ID], + ATTR_APP_VERSION: entry_data[ATTR_APP_VERSION], + } + if ATTR_OS_VERSION in entry_data: + reg_info[ATTR_OS_VERSION] = entry_data[ATTR_OS_VERSION] + + data['registration_info'] = reg_info + + try: + with async_timeout.timeout(10, loop=self.hass.loop): + response = await self._session.post(push_url, json=data) + result = await response.json() + + if response.status == 201: + log_rate_limits(self.hass, + entry_data[ATTR_DEVICE_NAME], result) + return + + fallback_error = result.get("errorMessage", + "Unknown error") + fallback_message = ("Internal server error, " + "please try again later: " + "{}").format(fallback_error) + message = result.get("message", fallback_message) + if response.status == 429: + _LOGGER.warning(message) + log_rate_limits(self.hass, + entry_data[ATTR_DEVICE_NAME], + result, logging.WARNING) + else: + _LOGGER.error(message) + + except asyncio.TimeoutError: + _LOGGER.error("Timeout sending notification to %s", push_url) diff --git a/tests/components/mobile_app/test_notify.py b/tests/components/mobile_app/test_notify.py new file mode 100644 index 00000000000..395dee6c117 --- /dev/null +++ b/tests/components/mobile_app/test_notify.py @@ -0,0 +1,81 @@ +"""Notify platform tests for mobile_app.""" +# pylint: disable=redefined-outer-name +import pytest + +from homeassistant.setup import async_setup_component + +from homeassistant.components.mobile_app.const import DOMAIN + +from tests.common import MockConfigEntry + + +@pytest.fixture +async def setup_push_receiver(hass, aioclient_mock): + """Fixture that sets up a mocked push receiver.""" + push_url = 'https://mobile-push.home-assistant.dev/push' + + from datetime import datetime, timedelta + now = (datetime.now() + timedelta(hours=24)) + iso_time = now.strftime("%Y-%m-%dT%H:%M:%SZ") + + aioclient_mock.post(push_url, json={ + 'rateLimits': { + 'attempts': 1, + 'successful': 1, + 'errors': 0, + 'total': 1, + 'maximum': 150, + 'remaining': 149, + 'resetsAt': iso_time + } + }) + + entry = MockConfigEntry( + connection_class="cloud_push", + data={ + "app_data": { + "push_token": "PUSH_TOKEN", + "push_url": push_url + }, + "app_id": "io.homeassistant.mobile_app", + "app_name": "mobile_app tests", + "app_version": "1.0", + "device_id": "4d5e6f", + "device_name": "Test", + "manufacturer": "Home Assistant", + "model": "mobile_app", + "os_name": "Linux", + "os_version": "5.0.6", + "secret": "123abc", + "supports_encryption": False, + "user_id": "1a2b3c", + "webhook_id": "webhook_id" + }, + domain=DOMAIN, + source="registration", + title="mobile_app test entry", + version=1 + ) + entry.add_to_hass(hass) + + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + await hass.async_block_till_done() + + +async def test_notify_works(hass, aioclient_mock, setup_push_receiver): + """Test notify works.""" + assert hass.services.has_service('notify', 'mobile_app_test') is True + assert await hass.services.async_call('notify', 'mobile_app_test', + {'message': 'Hello world'}, + blocking=True) + + assert len(aioclient_mock.mock_calls) == 1 + call = aioclient_mock.mock_calls + + call_json = call[0][2] + + assert call_json["push_token"] == "PUSH_TOKEN" + assert call_json["message"] == "Hello world" + assert call_json["registration_info"]["app_id"] == \ + "io.homeassistant.mobile_app" + assert call_json["registration_info"]["app_version"] == "1.0" From 81a659be0dd4c29f2c30c49307dad87dd542267e Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 3 Apr 2019 04:23:33 +0200 Subject: [PATCH 39/85] Hass.io discovery flow deconz (#22623) * Add Hass.io deCONZ discovery flow * add bridge ID * fix attribute * fix strings * Address comments * Add test * Add only instance / changed maybe later --- .../components/deconz/config_flow.py | 59 +++++++++++++++++-- homeassistant/components/deconz/const.py | 3 + homeassistant/components/deconz/strings.json | 10 +++- homeassistant/components/hassio/discovery.py | 4 +- tests/components/deconz/test_config_flow.py | 49 +++++++++++++++ 5 files changed, 116 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index cabb5b46ece..38849fb37b3 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -1,17 +1,18 @@ """Config flow to configure deCONZ component.""" import asyncio + import async_timeout import voluptuous as vol from homeassistant import config_entries -from homeassistant.core import callback from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT +from homeassistant.core import callback from homeassistant.helpers import aiohttp_client from .const import ( - CONF_ALLOW_DECONZ_GROUPS, CONF_ALLOW_CLIP_SENSOR, DEFAULT_PORT, DOMAIN) - -CONF_BRIDGEID = 'bridgeid' + CONF_ALLOW_CLIP_SENSOR, CONF_ALLOW_DECONZ_GROUPS, CONF_BRIDGEID, + DEFAULT_ALLOW_CLIP_SENSOR, DEFAULT_ALLOW_DECONZ_GROUPS, DEFAULT_PORT, + DOMAIN) @callback @@ -28,6 +29,8 @@ class DeconzFlowHandler(config_entries.ConfigFlow): VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH + _hassio_discovery = None + def __init__(self): """Initialize the deCONZ config flow.""" self.bridges = [] @@ -151,8 +154,10 @@ class DeconzFlowHandler(config_entries.ConfigFlow): return self.async_show_form( step_id='options', data_schema=vol.Schema({ - vol.Optional(CONF_ALLOW_CLIP_SENSOR): bool, - vol.Optional(CONF_ALLOW_DECONZ_GROUPS): bool, + vol.Optional(CONF_ALLOW_CLIP_SENSOR, + default=DEFAULT_ALLOW_CLIP_SENSOR): bool, + vol.Optional(CONF_ALLOW_DECONZ_GROUPS, + default=DEFAULT_ALLOW_DECONZ_GROUPS): bool, }), ) @@ -191,3 +196,45 @@ class DeconzFlowHandler(config_entries.ConfigFlow): user_input = {CONF_ALLOW_CLIP_SENSOR: True, CONF_ALLOW_DECONZ_GROUPS: True} return await self.async_step_options(user_input=user_input) + + async def async_step_hassio(self, user_input=None): + """Prepare configuration for a Hass.io deCONZ bridge. + + This flow is triggered by the discovery component. + """ + if configured_hosts(self.hass): + return self.async_abort(reason='one_instance_only') + + self._hassio_discovery = user_input + + return await self.async_step_hassio_confirm() + + async def async_step_hassio_confirm(self, user_input=None): + """Confirm a Hass.io discovery.""" + if user_input is not None: + data = self._hassio_discovery + + return self.async_create_entry( + title=data['addon'], data={ + CONF_HOST: data[CONF_HOST], + CONF_PORT: data[CONF_PORT], + CONF_BRIDGEID: data['serial'], + CONF_API_KEY: data[CONF_API_KEY], + CONF_ALLOW_CLIP_SENSOR: + user_input[CONF_ALLOW_CLIP_SENSOR], + CONF_ALLOW_DECONZ_GROUPS: + user_input[CONF_ALLOW_DECONZ_GROUPS], + }) + + return self.async_show_form( + step_id='hassio_confirm', + description_placeholders={ + 'addon': self._hassio_discovery['addon'] + }, + data_schema=vol.Schema({ + vol.Optional(CONF_ALLOW_CLIP_SENSOR, + default=DEFAULT_ALLOW_CLIP_SENSOR): bool, + vol.Optional(CONF_ALLOW_DECONZ_GROUPS, + default=DEFAULT_ALLOW_DECONZ_GROUPS): bool, + }) + ) diff --git a/homeassistant/components/deconz/const.py b/homeassistant/components/deconz/const.py index e728430f202..b26fddd9147 100644 --- a/homeassistant/components/deconz/const.py +++ b/homeassistant/components/deconz/const.py @@ -6,9 +6,12 @@ _LOGGER = logging.getLogger('.') DOMAIN = 'deconz' DEFAULT_PORT = 80 +DEFAULT_ALLOW_CLIP_SENSOR = False +DEFAULT_ALLOW_DECONZ_GROUPS = False CONF_ALLOW_CLIP_SENSOR = 'allow_clip_sensor' CONF_ALLOW_DECONZ_GROUPS = 'allow_deconz_groups' +CONF_BRIDGEID = 'bridgeid' SUPPORTED_PLATFORMS = ['binary_sensor', 'climate', 'cover', 'light', 'scene', 'sensor', 'switch'] diff --git a/homeassistant/components/deconz/strings.json b/homeassistant/components/deconz/strings.json index 1bf7235713a..d0ae34e7c7a 100644 --- a/homeassistant/components/deconz/strings.json +++ b/homeassistant/components/deconz/strings.json @@ -19,6 +19,14 @@ "allow_clip_sensor": "Allow importing virtual sensors", "allow_deconz_groups": "Allow importing deCONZ groups" } + }, + "hassio_confirm": { + "title": "deCONZ Zigbee gateway via Hass.io add-on", + "description": "Do you want to configure Home Assistant to connect to the deCONZ gateway provided by the hass.io add-on {addon}?", + "data": { + "allow_clip_sensor": "Allow importing virtual sensors", + "allow_deconz_groups": "Allow importing deCONZ groups" + } } }, "error": { @@ -30,4 +38,4 @@ "one_instance_only": "Component only supports one deCONZ instance" } } -} \ No newline at end of file +} diff --git a/homeassistant/components/hassio/discovery.py b/homeassistant/components/hassio/discovery.py index 804247d2407..09a98edc148 100644 --- a/homeassistant/components/hassio/discovery.py +++ b/homeassistant/components/hassio/discovery.py @@ -81,7 +81,7 @@ class HassIODiscovery(HomeAssistantView): service = data[ATTR_SERVICE] config_data = data[ATTR_CONFIG] - # Read addinional Add-on info + # Read additional Add-on info try: addon_info = await self.hassio.get_addon_info(data[ATTR_ADDON]) except HassioAPIError as err: @@ -98,7 +98,7 @@ class HassIODiscovery(HomeAssistantView): service = data[ATTR_SERVICE] uuid = data[ATTR_UUID] - # Check if realy deletet / prevent injections + # Check if really deletet / prevent injections try: data = await self.hassio.get_discovery_message(uuid) except HassioAPIError: diff --git a/tests/components/deconz/test_config_flow.py b/tests/components/deconz/test_config_flow.py index 20c74a82883..863e4e93fc5 100644 --- a/tests/components/deconz/test_config_flow.py +++ b/tests/components/deconz/test_config_flow.py @@ -265,3 +265,52 @@ async def test_options(hass, aioclient_mock): 'allow_clip_sensor': False, 'allow_deconz_groups': False } + + +async def test_hassio_single_instance(hass): + """Test we only allow a single config flow.""" + MockConfigEntry(domain='deconz', data={ + 'host': '1.2.3.4' + }).add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + 'deconz', context={'source': 'hassio'}) + assert result['type'] == 'abort' + assert result['reason'] == 'one_instance_only' + + +async def test_hassio_confirm(hass): + """Test we can finish a config flow.""" + result = await hass.config_entries.flow.async_init( + 'deconz', + data={ + 'addon': 'Mock Addon', + 'host': 'mock-deconz', + 'port': 8080, + 'serial': 'aa:bb', + 'api_key': '1234567890ABCDEF', + }, + context={'source': 'hassio'} + ) + assert result['type'] == 'form' + assert result['step_id'] == 'hassio_confirm' + assert result['description_placeholders'] == { + 'addon': 'Mock Addon', + } + + result = await hass.config_entries.flow.async_configure( + result['flow_id'], { + 'allow_clip_sensor': True, + 'allow_deconz_groups': True, + } + ) + + assert result['type'] == 'create_entry' + assert result['result'].data == { + 'host': 'mock-deconz', + 'port': 8080, + 'bridgeid': 'aa:bb', + 'api_key': '1234567890ABCDEF', + 'allow_clip_sensor': True, + 'allow_deconz_groups': True, + } From e90d980e678156004eac2909f54a5ff3b52080b6 Mon Sep 17 00:00:00 2001 From: mvn23 Date: Tue, 2 Apr 2019 22:57:38 +0200 Subject: [PATCH 40/85] Don't use room setpoint override in climate.opentherm_gw (#22656) * Dont use DATA_ROOM_SETPOINT_OVRD in climate.opentherm_gw as it is unreliable with some thermostats. * Show new target temperature immediately until the backend notices a change * Only update target temp on the gateway if the value differs from the current target_temperature. --- homeassistant/components/opentherm_gw/climate.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/opentherm_gw/climate.py b/homeassistant/components/opentherm_gw/climate.py index 58ce49a9b02..60f1901d43e 100644 --- a/homeassistant/components/opentherm_gw/climate.py +++ b/homeassistant/components/opentherm_gw/climate.py @@ -39,6 +39,7 @@ class OpenThermGateway(ClimateDevice): self.temp_precision = config.get(CONF_PRECISION) self._current_operation = STATE_IDLE self._current_temperature = None + self._new_target_temperature = None self._target_temperature = None self._away_mode_a = None self._away_mode_b = None @@ -63,11 +64,10 @@ class OpenThermGateway(ClimateDevice): else: self._current_operation = STATE_IDLE self._current_temperature = status.get(self._gw_vars.DATA_ROOM_TEMP) - - temp = status.get(self._gw_vars.DATA_ROOM_SETPOINT_OVRD) - if temp is None: - temp = status.get(self._gw_vars.DATA_ROOM_SETPOINT) - self._target_temperature = temp + temp_upd = status.get(self._gw_vars.DATA_ROOM_SETPOINT) + if self._target_temperature != temp_upd: + self._new_target_temperature = None + self._target_temperature = temp_upd # GPIO mode 5: 0 == Away # GPIO mode 6: 1 == Away @@ -138,7 +138,7 @@ class OpenThermGateway(ClimateDevice): @property def target_temperature(self): """Return the temperature we try to reach.""" - return self._target_temperature + return self._new_target_temperature or self._target_temperature @property def target_temperature_step(self): @@ -154,7 +154,9 @@ class OpenThermGateway(ClimateDevice): """Set new target temperature.""" if ATTR_TEMPERATURE in kwargs: temp = float(kwargs[ATTR_TEMPERATURE]) - self._target_temperature = await self._gateway.set_target_temp( + if temp == self.target_temperature: + return + self._new_target_temperature = await self._gateway.set_target_temp( temp) self.async_schedule_update_ha_state() From 167d8cbaba61c20fe5b8c3338528f47b1e2cf5d0 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Wed, 3 Apr 2019 07:49:53 +0100 Subject: [PATCH 41/85] Fix #22648 - Utility_meter would try to cancel a non existing task (#22669) * don't cancel tariff that are paused * test tariffs --- .../components/utility_meter/sensor.py | 3 +- tests/components/utility_meter/test_sensor.py | 43 ++++++++++++++++--- 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/utility_meter/sensor.py b/homeassistant/components/utility_meter/sensor.py index dd1514f5e43..2c151634a95 100644 --- a/homeassistant/components/utility_meter/sensor.py +++ b/homeassistant/components/utility_meter/sensor.py @@ -118,7 +118,8 @@ class UtilityMeterSensor(RestoreEntity): self._collecting = async_track_state_change( self.hass, self._sensor_source_id, self.async_reading) else: - self._collecting() + if self._collecting: + self._collecting() self._collecting = None _LOGGER.debug("%s - %s - source <%s>", self._name, diff --git a/tests/components/utility_meter/test_sensor.py b/tests/components/utility_meter/test_sensor.py index ee291439a2c..6b8705bf776 100644 --- a/tests/components/utility_meter/test_sensor.py +++ b/tests/components/utility_meter/test_sensor.py @@ -6,10 +6,11 @@ from unittest.mock import patch from contextlib import contextmanager from tests.common import async_fire_time_changed -from homeassistant.const import EVENT_HOMEASSISTANT_START +from homeassistant.const import EVENT_HOMEASSISTANT_START, ATTR_ENTITY_ID from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from homeassistant.components.utility_meter.const import DOMAIN +from homeassistant.components.utility_meter.const import ( + DOMAIN, SERVICE_SELECT_TARIFF, ATTR_TARIFF) from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN _LOGGER = logging.getLogger(__name__) @@ -31,7 +32,7 @@ async def test_state(hass): 'utility_meter': { 'energy_bill': { 'source': 'sensor.energy', - } + 'tariffs': ['onpeak', 'midpeak', 'offpeak']}, } } @@ -51,11 +52,43 @@ async def test_state(hass): force_update=True) await hass.async_block_till_done() - state = hass.states.get('sensor.energy_bill') + state = hass.states.get('sensor.energy_bill_onpeak') assert state is not None - assert state.state == '1' + state = hass.states.get('sensor.energy_bill_midpeak') + assert state is not None + assert state.state == '0' + + state = hass.states.get('sensor.energy_bill_offpeak') + assert state is not None + assert state.state == '0' + + await hass.services.async_call(DOMAIN, SERVICE_SELECT_TARIFF, { + ATTR_ENTITY_ID: 'utility_meter.energy_bill', ATTR_TARIFF: 'offpeak' + }, blocking=True) + + await hass.async_block_till_done() + + now = dt_util.utcnow() + timedelta(seconds=20) + with patch('homeassistant.util.dt.utcnow', + return_value=now): + hass.states.async_set(entity_id, 6, {"unit_of_measurement": "kWh"}, + force_update=True) + await hass.async_block_till_done() + + state = hass.states.get('sensor.energy_bill_onpeak') + assert state is not None + assert state.state == '1' + + state = hass.states.get('sensor.energy_bill_midpeak') + assert state is not None + assert state.state == '0' + + state = hass.states.get('sensor.energy_bill_offpeak') + assert state is not None + assert state.state == '3' + async def test_net_consumption(hass): """Test utility sensor state.""" From 9eb4f89da4190d70490c38fb7cd010e4c0f2d223 Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Tue, 2 Apr 2019 21:23:59 -0700 Subject: [PATCH 42/85] Fix trusted networks auth provider warning message (#22671) * Fix trusted networks auth provider warning message * Update auth.py --- homeassistant/components/http/auth.py | 16 ++++++++++------ homeassistant/components/http/view.py | 2 ++ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/http/auth.py b/homeassistant/components/http/auth.py index 7b8508894ce..0d8e327e086 100644 --- a/homeassistant/components/http/auth.py +++ b/homeassistant/components/http/auth.py @@ -190,12 +190,16 @@ def setup_auth(hass, app): elif (trusted_networks and await async_validate_trusted_networks(request)): - _LOGGER.warning( - 'Access from trusted networks without auth token is going to ' - 'be removed in Home Assistant 0.96. Configure the trusted ' - 'networks auth provider or use long-lived access tokens to ' - 'access %s from %s', - request.path, request[KEY_REAL_IP]) + if request.path not in old_auth_warning: + # When removing this, don't forget to remove the print logic + # in http/view.py + request['deprecate_warning_message'] = \ + 'Access from trusted networks without auth token is ' \ + 'going to be removed in Home Assistant 0.96. Configure ' \ + 'the trusted networks auth provider or use long-lived ' \ + 'access tokens to access {} from {}'.format( + request.path, request[KEY_REAL_IP]) + old_auth_warning.add(request.path) authenticated = True elif (support_legacy and HTTP_HEADER_HA_AUTH in request.headers and diff --git a/homeassistant/components/http/view.py b/homeassistant/components/http/view.py index d68cabebacf..8d5e0ee88b1 100644 --- a/homeassistant/components/http/view.py +++ b/homeassistant/components/http/view.py @@ -98,6 +98,8 @@ def request_handler_factory(view, handler): if view.requires_auth: if authenticated: + if 'deprecate_warning_message' in request: + _LOGGER.warning(request['deprecate_warning_message']) await process_success_login(request) else: raise HTTPUnauthorized() From 7cf92c2210c45651e551817c2cd8fbc1c737ec5f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 3 Apr 2019 04:53:44 -0700 Subject: [PATCH 43/85] Deal with cover assumed state (#22673) * Deal with cover assumed state * Add docs --- .../components/google_assistant/trait.py | 17 ++++++++++---- .../components/google_assistant/test_trait.py | 23 +++++++++++++++++-- 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 81918ff2e88..de3a9530b50 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -27,6 +27,7 @@ from homeassistant.const import ( TEMP_FAHRENHEIT, ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE, + ATTR_ASSUMED_STATE, ) from homeassistant.core import DOMAIN as HA_DOMAIN from homeassistant.util import color as color_util, temperature as temp_util @@ -1055,11 +1056,19 @@ class OpenCloseTrait(_Trait): response = {} if domain == cover.DOMAIN: - position = self.state.attributes.get(cover.ATTR_CURRENT_POSITION) - if position is not None: - response['openPercent'] = position + # When it's an assumed state, we will always report it as 50% + # Google will not issue an open command if the assumed state is + # open, even if that is currently incorrect. + if self.state.attributes.get(ATTR_ASSUMED_STATE): + response['openPercent'] = 50 else: - if self.state.state != cover.STATE_CLOSED: + position = self.state.attributes.get( + cover.ATTR_CURRENT_POSITION + ) + + if position is not None: + response['openPercent'] = position + elif self.state.state != cover.STATE_CLOSED: response['openPercent'] = 100 else: response['openPercent'] = 0 diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index a0a710d3d8c..81a7fbe1bf7 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -21,7 +21,8 @@ from homeassistant.components.climate import const as climate from homeassistant.components.google_assistant import trait, helpers, const from homeassistant.const import ( STATE_ON, STATE_OFF, ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF, - TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE) + TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE, + ATTR_ASSUMED_STATE) from homeassistant.core import State, DOMAIN as HA_DOMAIN, EVENT_CALL_SERVICE from homeassistant.util import color from tests.common import async_mock_service, mock_coro @@ -1059,12 +1060,30 @@ async def test_openclose_cover(hass): assert trait.OpenCloseTrait.supported(cover.DOMAIN, cover.SUPPORT_SET_POSITION) + # No position + trt = trait.OpenCloseTrait(hass, State('cover.bla', cover.STATE_OPEN, { + }), BASIC_CONFIG) + + assert trt.sync_attributes() == {} + assert trt.query_attributes() == { + 'openPercent': 100 + } + + # Assumed state + trt = trait.OpenCloseTrait(hass, State('cover.bla', cover.STATE_OPEN, { + ATTR_ASSUMED_STATE: True, + }), BASIC_CONFIG) + + assert trt.sync_attributes() == {} + assert trt.query_attributes() == { + 'openPercent': 50 + } + trt = trait.OpenCloseTrait(hass, State('cover.bla', cover.STATE_OPEN, { cover.ATTR_CURRENT_POSITION: 75 }), BASIC_CONFIG) assert trt.sync_attributes() == {} - assert trt.query_attributes() == { 'openPercent': 75 } From 836aab283fe683e9d0b0520e01135678683939aa Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 3 Apr 2019 13:46:41 +0200 Subject: [PATCH 44/85] Fix ffmpeg default extra options (#22682) --- homeassistant/components/amcrest/__init__.py | 4 +++- homeassistant/components/arlo/camera.py | 7 ++++--- homeassistant/components/canary/camera.py | 7 ++++--- homeassistant/components/ffmpeg/camera.py | 4 +++- homeassistant/components/onvif/camera.py | 2 +- homeassistant/components/xiaomi/camera.py | 3 ++- homeassistant/components/yi/camera.py | 3 ++- 7 files changed, 19 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/amcrest/__init__.py b/homeassistant/components/amcrest/__init__.py index 84dc0b5bb01..9f43941505b 100644 --- a/homeassistant/components/amcrest/__init__.py +++ b/homeassistant/components/amcrest/__init__.py @@ -26,6 +26,7 @@ DEFAULT_NAME = 'Amcrest Camera' DEFAULT_PORT = 80 DEFAULT_RESOLUTION = 'high' DEFAULT_STREAM_SOURCE = 'snapshot' +DEFAULT_ARGUMENTS = '-pred 1' TIMEOUT = 10 DATA_AMCREST = 'amcrest' @@ -77,7 +78,8 @@ CONFIG_SCHEMA = vol.Schema({ vol.All(vol.In(RESOLUTION_LIST)), vol.Optional(CONF_STREAM_SOURCE, default=DEFAULT_STREAM_SOURCE): vol.All(vol.In(STREAM_SOURCE_LIST)), - vol.Optional(CONF_FFMPEG_ARGUMENTS): cv.string, + vol.Optional(CONF_FFMPEG_ARGUMENTS, default=DEFAULT_ARGUMENTS): + cv.string, vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL): cv.time_period, vol.Optional(CONF_SENSORS): diff --git a/homeassistant/components/arlo/camera.py b/homeassistant/components/arlo/camera.py index 95d11318bf7..d4b00f00625 100644 --- a/homeassistant/components/arlo/camera.py +++ b/homeassistant/components/arlo/camera.py @@ -13,6 +13,8 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from . import DATA_ARLO, DEFAULT_BRAND, SIGNAL_UPDATE_ARLO +DEPENDENCIES = ['arlo', 'ffmpeg'] + _LOGGER = logging.getLogger(__name__) ARLO_MODE_ARMED = 'armed' @@ -28,8 +30,7 @@ ATTR_UNSEEN_VIDEOS = 'unseen_videos' ATTR_LAST_REFRESH = 'last_refresh' CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments' - -DEPENDENCIES = ['arlo', 'ffmpeg'] +DEFAULT_ARGUMENTS = '-pred 1' POWERSAVE_MODE_MAPPING = { 1: 'best_battery_life', @@ -38,7 +39,7 @@ POWERSAVE_MODE_MAPPING = { } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_FFMPEG_ARGUMENTS): cv.string, + vol.Optional(CONF_FFMPEG_ARGUMENTS, default=DEFAULT_ARGUMENTS): cv.string, }) diff --git a/homeassistant/components/canary/camera.py b/homeassistant/components/canary/camera.py index 63c27d31d93..c3a3af32450 100644 --- a/homeassistant/components/canary/camera.py +++ b/homeassistant/components/canary/camera.py @@ -18,16 +18,17 @@ from homeassistant.util import Throttle from . import DATA_CANARY, DEFAULT_TIMEOUT -CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments' - DEPENDENCIES = ['canary', 'ffmpeg'] _LOGGER = logging.getLogger(__name__) +CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments' +DEFAULT_ARGUMENTS = '-pred 1' + MIN_TIME_BETWEEN_SESSION_RENEW = timedelta(seconds=90) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_FFMPEG_ARGUMENTS): cv.string, + vol.Optional(CONF_FFMPEG_ARGUMENTS, default=DEFAULT_ARGUMENTS): cv.string, }) diff --git a/homeassistant/components/ffmpeg/camera.py b/homeassistant/components/ffmpeg/camera.py index d897293124b..07e56cfd51f 100644 --- a/homeassistant/components/ffmpeg/camera.py +++ b/homeassistant/components/ffmpeg/camera.py @@ -20,11 +20,13 @@ from . import CONF_EXTRA_ARGUMENTS, CONF_INPUT, DATA_FFMPEG _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['ffmpeg'] + DEFAULT_NAME = 'FFmpeg' +DEFAULT_ARGUMENTS = "-pred 1" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_INPUT): cv.string, - vol.Optional(CONF_EXTRA_ARGUMENTS): cv.string, + vol.Optional(CONF_EXTRA_ARGUMENTS, default=DEFAULT_ARGUMENTS): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, }) diff --git a/homeassistant/components/onvif/camera.py b/homeassistant/components/onvif/camera.py index 09d47c3c0c9..a196f87cd10 100644 --- a/homeassistant/components/onvif/camera.py +++ b/homeassistant/components/onvif/camera.py @@ -33,7 +33,7 @@ DEFAULT_NAME = 'ONVIF Camera' DEFAULT_PORT = 5000 DEFAULT_USERNAME = 'admin' DEFAULT_PASSWORD = '888888' -DEFAULT_ARGUMENTS = '-q:v 2' +DEFAULT_ARGUMENTS = '-pred 1' DEFAULT_PROFILE = 0 CONF_PROFILE = "profile" diff --git a/homeassistant/components/xiaomi/camera.py b/homeassistant/components/xiaomi/camera.py index d8cd59129ab..b0acf50ec8c 100644 --- a/homeassistant/components/xiaomi/camera.py +++ b/homeassistant/components/xiaomi/camera.py @@ -23,6 +23,7 @@ DEFAULT_BRAND = 'Xiaomi Home Camera' DEFAULT_PATH = '/media/mmcblk0p1/record' DEFAULT_PORT = 21 DEFAULT_USERNAME = 'root' +DEFAULT_ARGUMENTS = '-pred 1' CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments' CONF_MODEL = 'model' @@ -39,7 +40,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_PATH, default=DEFAULT_PATH): cv.string, vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_FFMPEG_ARGUMENTS): cv.string + vol.Optional(CONF_FFMPEG_ARGUMENTS, default=DEFAULT_ARGUMENTS): cv.string }) diff --git a/homeassistant/components/yi/camera.py b/homeassistant/components/yi/camera.py index c60d4971fb8..f82c8c38129 100644 --- a/homeassistant/components/yi/camera.py +++ b/homeassistant/components/yi/camera.py @@ -26,6 +26,7 @@ DEFAULT_PASSWORD = '' DEFAULT_PATH = '/tmp/sd/record' DEFAULT_PORT = 21 DEFAULT_USERNAME = 'root' +DEFAULT_ARGUMENTS = '-pred 1' CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments' @@ -36,7 +37,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_PATH, default=DEFAULT_PATH): cv.string, vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_FFMPEG_ARGUMENTS): cv.string + vol.Optional(CONF_FFMPEG_ARGUMENTS, default=DEFAULT_ARGUMENTS): cv.string }) From 2e8c69003315ce6d9c463be8150aead9c2b97a96 Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Thu, 28 Mar 2019 09:54:49 -0700 Subject: [PATCH 45/85] A very basic Circleci setup (#22503) * Add circleci support * Add buildpack-deps * Install libudev-dev * sudo * always run test * Add test report * no sugar * quite pytest * better junit test result * Add $CODE_COVERAGE env var --- .circleci/config.yml | 79 ++++++++++++++++++++++++++++++++++++++++++++ .gitignore | 1 + 2 files changed, 80 insertions(+) create mode 100644 .circleci/config.yml diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 00000000000..b6a57a28381 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,79 @@ +# Python CircleCI 2.0 configuration file +# +# Check https://circleci.com/docs/2.0/language-python/ for more details +# +version: 2.1 +jobs: + build: + docker: + # specify the version you desire here + # use `-browsers` prefix for selenium tests, e.g. `3.6.1-browsers` + - image: circleci/python:3.7.2 + + # Specify service dependencies here if necessary + # CircleCI maintains a library of pre-built images + # documented at https://circleci.com/docs/2.0/circleci-images/ + # - image: circleci/postgres:9.4 + - image: circleci/buildpack-deps:stretch + + working_directory: ~/repo + + steps: + - checkout + + - run: + name: setup docker prereqs + command: sudo apt-get update && sudo apt-get install -y --no-install-recommends libudev-dev + + # Download and cache dependencies + - restore_cache: + keys: + - v1-dependencies-{{ checksum "requirements_all.txt" }} + # fallback to using the latest cache if no exact match is found + - v1-dependencies- + + - run: + name: install dependencies + command: | + python3 -m venv venv + . venv/bin/activate + pip install --progress-bar off -r requirements_all.txt -r requirements_test.txt -c homeassistant/package_constraints.txt + + - save_cache: + paths: + - ./venv + key: v1-dependencies-{{ checksum "requirements_all.txt" }} + + - run: + name: install + command: | + . venv/bin/activate + pip install --progress-bar off -e . + + - run: + name: run lint + command: | + . venv/bin/activate + python script/gen_requirements_all.py validate + flake8 + pylint homeassistant + + - run: + name: run tests + command: | + . venv/bin/activate + if [ -z "$CODE_COVERAGE" ]; then CC_SWITCH=""; else CC_SWITCH="--cov --cov-report html:htmlcov"; fi + pytest --timeout=9 --duration=10 --junitxml=test-reports/homeassistant/results.xml -qq -o junit_family=xunit2 -o junit_suite_name=homeassistant -o console_output_style=count -p no:sugar $CC_SWITCH + script/check_dirty + when: always + + - store_test_results: + path: test-reports + + - store_artifacts: + path: htmlcov + destination: cov-reports + + - store_artifacts: + path: test-reports + destination: test-reports \ No newline at end of file diff --git a/.gitignore b/.gitignore index 91b8d024aed..b486032c741 100644 --- a/.gitignore +++ b/.gitignore @@ -55,6 +55,7 @@ pip-log.txt .tox nosetests.xml htmlcov/ +test-reports/ # Translations *.mo From 273007fa190d4dc5affc0b2d093806e15296615f Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Thu, 28 Mar 2019 14:37:10 -0700 Subject: [PATCH 46/85] Fix Circleci config (#22509) * Add libav depends on circleci * tweak circleci config --- .circleci/config.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index b6a57a28381..112ce2284dd 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -23,26 +23,26 @@ jobs: - run: name: setup docker prereqs - command: sudo apt-get update && sudo apt-get install -y --no-install-recommends libudev-dev + command: sudo apt-get update && sudo apt-get install -y --no-install-recommends + libudev-dev libavformat-dev libavcodec-dev libavdevice-dev libavutil-dev + libswscale-dev libswresample-dev libavfilter-dev - # Download and cache dependencies + # Download and cache dependencies, we don't use fallback cache - restore_cache: keys: - - v1-dependencies-{{ checksum "requirements_all.txt" }} - # fallback to using the latest cache if no exact match is found - - v1-dependencies- + - v1-dependencies-{{ checksum "requirements_all.txt" }}-{{ checksum "requirements_test.txt" }} - run: name: install dependencies command: | python3 -m venv venv . venv/bin/activate - pip install --progress-bar off -r requirements_all.txt -r requirements_test.txt -c homeassistant/package_constraints.txt + pip install -q --progress-bar off -r requirements_all.txt -r requirements_test.txt -c homeassistant/package_constraints.txt - save_cache: paths: - ./venv - key: v1-dependencies-{{ checksum "requirements_all.txt" }} + key: v1-dependencies-{{ checksum "requirements_all.txt" }}-{{ checksum "requirements_test.txt" }} - run: name: install @@ -76,4 +76,4 @@ jobs: - store_artifacts: path: test-reports - destination: test-reports \ No newline at end of file + destination: test-reports From 5dd444fcd81c6a4be70d85f9487d9c0381867cc2 Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Fri, 29 Mar 2019 16:37:45 -0700 Subject: [PATCH 47/85] Set up Circleci workflow (#22519) * Set up Circleci workflow * Update python tag * Add pre-test job to cache the requirements * Upgrade pip itself * Use 3.7 for lint * Parallelize pylint * Tweak run gen_requirements_all * tweak cache key --- .circleci/config.yml | 201 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 173 insertions(+), 28 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 112ce2284dd..f9eb28bdf4a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -3,67 +3,174 @@ # Check https://circleci.com/docs/2.0/language-python/ for more details # version: 2.1 -jobs: - build: + +executors: + + python: + parameters: + tag: + type: string + default: latest docker: - # specify the version you desire here - # use `-browsers` prefix for selenium tests, e.g. `3.6.1-browsers` - - image: circleci/python:3.7.2 - - # Specify service dependencies here if necessary - # CircleCI maintains a library of pre-built images - # documented at https://circleci.com/docs/2.0/circleci-images/ - # - image: circleci/postgres:9.4 + - image: circleci/python:<< parameters.tag >> - image: circleci/buildpack-deps:stretch - working_directory: ~/repo +commands: + + docker-prereqs: + description: Set up docker prerequisite requirement steps: - - checkout + - run: sudo apt-get update && sudo apt-get install -y --no-install-recommends + libudev-dev libavformat-dev libavcodec-dev libavdevice-dev libavutil-dev + libswscale-dev libswresample-dev libavfilter-dev - - run: - name: setup docker prereqs - command: sudo apt-get update && sudo apt-get install -y --no-install-recommends - libudev-dev libavformat-dev libavcodec-dev libavdevice-dev libavutil-dev - libswscale-dev libswresample-dev libavfilter-dev - - # Download and cache dependencies, we don't use fallback cache + install-requirements: + description: Set up venv and install requirements python packages with cache support + parameters: + python: + type: string + default: latest + all: + description: pip install -r requirements_all.txt + type: boolean + default: false + test: + description: pip install -r requirements_test.txt + type: boolean + default: false + test_all: + description: pip install -r requirements_test_all.txt + type: boolean + default: false + steps: - restore_cache: keys: - - v1-dependencies-{{ checksum "requirements_all.txt" }}-{{ checksum "requirements_test.txt" }} - + - v1-<< parameters.python >>-{{ checksum "homeassistant/package_constraints.txt" }}-<<# parameters.all >>{{ checksum "requirements_all.txt" }}<>-<<# parameters.test >>{{ checksum "requirements_test.txt" }}<>-<<# parameters.test_all >>{{ checksum "requirements_test_all.txt" }}<> - run: name: install dependencies command: | python3 -m venv venv . venv/bin/activate - pip install -q --progress-bar off -r requirements_all.txt -r requirements_test.txt -c homeassistant/package_constraints.txt - + pip install -U pip + <<# parameters.all >>pip install -q --progress-bar off -r requirements_all.txt -c homeassistant/package_constraints.txt<> + <<# parameters.test >>pip install -q --progress-bar off -r requirements_test.txt -c homeassistant/package_constraints.txt<> + <<# parameters.test_all >>pip install -q --progress-bar off -r requirements_test_all.txt -c homeassistant/package_constraints.txt<> - save_cache: paths: - ./venv - key: v1-dependencies-{{ checksum "requirements_all.txt" }}-{{ checksum "requirements_test.txt" }} + key: v1-<< parameters.python >>-{{ checksum "homeassistant/package_constraints.txt" }}-<<# parameters.all >>{{ checksum "requirements_all.txt" }}<>-<<# parameters.test >>{{ checksum "requirements_test.txt" }}<>-<<# parameters.test_all >>{{ checksum "requirements_test_all.txt" }}<> + install: + description: Install Home Assistant + steps: - run: name: install command: | . venv/bin/activate pip install --progress-bar off -e . +jobs: + + static-check: + executor: + name: python + tag: 3.7-stretch + + steps: + - checkout + - docker-prereqs + - run: - name: run lint + name: run static check + command: | + python3 -m venv venv + . venv/bin/activate + pip install -U pip + pip install --progress-bar off flake8 + flake8 + + - install + - run: + name: run gen_requirements_all command: | . venv/bin/activate python script/gen_requirements_all.py validate - flake8 - pylint homeassistant + + pre-install-all-requirements: + executor: + name: python + tag: 3.7-stretch + + steps: + - checkout + - docker-prereqs + - install-requirements: + python: 3.7-stretch + all: true + test: true + + pylint: + executor: + name: python + tag: 3.7-stretch + parallelism: 3 + + steps: + - checkout + - docker-prereqs + - install-requirements: + python: 3.7-stretch + all: true + test: true + - install + + - run: + name: run pylint + command: | + . venv/bin/activate + PYFILES=$(circleci tests glob "homeassistant/**/*.py" | circleci tests split) + pylint ${PYFILES} + + pre-test: + parameters: + python: + type: string + executor: + name: python + tag: << parameters.python >> + + steps: + - checkout + - docker-prereqs + - install-requirements: + python: << parameters.python >> + test_all: true + + test: + parameters: + python: + type: string + executor: + name: python + tag: << parameters.python >> + parallelism: 3 + + steps: + - checkout + - docker-prereqs + - install-requirements: + python: << parameters.python >> + test_all: true + - install - run: name: run tests command: | . venv/bin/activate + TESTFILES=$(circleci tests glob "tests/**/test_*.py" | circleci tests split --split-by=timings) if [ -z "$CODE_COVERAGE" ]; then CC_SWITCH=""; else CC_SWITCH="--cov --cov-report html:htmlcov"; fi - pytest --timeout=9 --duration=10 --junitxml=test-reports/homeassistant/results.xml -qq -o junit_family=xunit2 -o junit_suite_name=homeassistant -o console_output_style=count -p no:sugar $CC_SWITCH + pytest --timeout=9 --duration=10 --junitxml=test-reports/homeassistant/results.xml -qq -o junit_family=xunit2 -o junit_suite_name=homeassistant -o console_output_style=count -p no:sugar $CC_SWITCH -- ${TESTFILES} script/check_dirty when: always @@ -77,3 +184,41 @@ jobs: - store_artifacts: path: test-reports destination: test-reports + +workflows: + version: 2 + build: + jobs: + - static-check + - pre-install-all-requirements + - pylint: + requires: + - pre-install-all-requirements + - pre-test: + name: pre-test 3.5.5 + python: 3.5.5-stretch + - pre-test: + name: pre-test 3.6 + python: 3.6-stretch + - pre-test: + name: pre-test 3.7 + python: 3.7-stretch + - test: + name: test 3.5.5 + requires: + - pre-test 3.5.5 + python: 3.5.5-stretch + - test: + name: test 3.6 + requires: + - pre-test 3.6 + python: 3.6-stretch + - test: + name: test 3.7 + requires: + - pre-test 3.7 + python: 3.7-stretch + # CircleCI does not allow failure yet + # - test: + # name: test 3.8 + # python: 3.8-rc-stretch From 2c105632050bbe31973faf9c930aac60d413194b Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Mon, 1 Apr 2019 07:12:59 -0700 Subject: [PATCH 48/85] Config CircleCI workflow (#22590) * Add mypyrc to control typing check, add mypy to circle * Add translation upload circlci job --- .circleci/config.yml | 47 ++++++++++++++++++++++++++++++-------- README.rst | 6 +++-- mypyrc | 21 +++++++++++++++++ script/translations_upload | 3 ++- tox.ini | 2 +- 5 files changed, 66 insertions(+), 13 deletions(-) create mode 100644 mypyrc diff --git a/.circleci/config.yml b/.circleci/config.yml index f9eb28bdf4a..b4f22601bb5 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -52,7 +52,7 @@ commands: command: | python3 -m venv venv . venv/bin/activate - pip install -U pip + pip install -q -U pip <<# parameters.all >>pip install -q --progress-bar off -r requirements_all.txt -c homeassistant/package_constraints.txt<> <<# parameters.test >>pip install -q --progress-bar off -r requirements_test.txt -c homeassistant/package_constraints.txt<> <<# parameters.test_all >>pip install -q --progress-bar off -r requirements_test_all.txt -c homeassistant/package_constraints.txt<> @@ -68,28 +68,35 @@ commands: name: install command: | . venv/bin/activate - pip install --progress-bar off -e . + pip install -q --progress-bar off -e . jobs: static-check: executor: name: python - tag: 3.7-stretch + tag: 3.5.5-stretch steps: - checkout - docker-prereqs + - install-requirements: + python: 3.5.5-stretch + test: true - run: name: run static check command: | - python3 -m venv venv . venv/bin/activate - pip install -U pip - pip install --progress-bar off flake8 flake8 + - run: + name: run static type check + command: | + . venv/bin/activate + TYPING_FILES=$(cat mypyrc) + mypy $TYPING_FILES + - install - run: name: run gen_requirements_all @@ -114,7 +121,7 @@ jobs: executor: name: python tag: 3.7-stretch - parallelism: 3 + parallelism: 2 steps: - checkout @@ -154,7 +161,7 @@ jobs: executor: name: python tag: << parameters.python >> - parallelism: 3 + parallelism: 2 steps: - checkout @@ -172,7 +179,6 @@ jobs: if [ -z "$CODE_COVERAGE" ]; then CC_SWITCH=""; else CC_SWITCH="--cov --cov-report html:htmlcov"; fi pytest --timeout=9 --duration=10 --junitxml=test-reports/homeassistant/results.xml -qq -o junit_family=xunit2 -o junit_suite_name=homeassistant -o console_output_style=count -p no:sugar $CC_SWITCH -- ${TESTFILES} script/check_dirty - when: always - store_test_results: path: test-reports @@ -185,6 +191,23 @@ jobs: path: test-reports destination: test-reports + # This job use machine executor, e.g. classic CircleCI VM because we need both lokalise-cli and a Python runtime. + # Classic CircleCI included python 2.7.12 and python 3.5.2 managed by pyenv, the Python version may need change if + # CircleCI changed its VM in future. + upload-translations: + machine: true + + steps: + - checkout + + - run: + name: upload english translations + command: | + pyenv versions + pyenv global 3.5.2 + docker pull lokalise/lokalise-cli@sha256:2198814ebddfda56ee041a4b427521757dd57f75415ea9693696a64c550cef21 + script/translations_upload + workflows: version: 2 build: @@ -222,3 +245,9 @@ workflows: # - test: # name: test 3.8 # python: 3.8-rc-stretch + - upload-translations: + requires: + - static-check + filters: + branches: + only: dev diff --git a/README.rst b/README.rst index f231d6c5514..941a463fb37 100644 --- a/README.rst +++ b/README.rst @@ -1,4 +1,4 @@ -Home Assistant |Build Status| |Coverage Status| |Chat Status| +Home Assistant |Build Status| |CI Status| |Coverage Status| |Chat Status| ================================================================================= Home Assistant is a home automation platform running on Python 3. It is able to track and control all devices at home and offer a platform for automating control. @@ -27,8 +27,10 @@ components `__ of our website for further help and information. -.. |Build Status| image:: https://travis-ci.org/home-assistant/home-assistant.svg?branch=master +.. |Build Status| image:: https://travis-ci.org/home-assistant/home-assistant.svg?branch=dev :target: https://travis-ci.org/home-assistant/home-assistant +.. |CI Status| image:: https://circleci.com/gh/home-assistant/home-assistant.svg?style=shield + :target: https://circleci.com/gh/home-assistant/home-assistant .. |Coverage Status| image:: https://img.shields.io/coveralls/home-assistant/home-assistant.svg :target: https://coveralls.io/r/home-assistant/home-assistant?branch=master .. |Chat Status| image:: https://img.shields.io/discord/330944238910963714.svg diff --git a/mypyrc b/mypyrc new file mode 100644 index 00000000000..7c73d12e381 --- /dev/null +++ b/mypyrc @@ -0,0 +1,21 @@ +homeassistant/*.py +homeassistant/auth/ +homeassistant/util/ +homeassistant/helpers/__init__.py +homeassistant/helpers/aiohttp_client.py +homeassistant/helpers/area_registry.py +homeassistant/helpers/condition.py +homeassistant/helpers/deprecation.py +homeassistant/helpers/dispatcher.py +homeassistant/helpers/entity_values.py +homeassistant/helpers/entityfilter.py +homeassistant/helpers/icon.py +homeassistant/helpers/intent.py +homeassistant/helpers/json.py +homeassistant/helpers/location.py +homeassistant/helpers/signal.py +homeassistant/helpers/state.py +homeassistant/helpers/sun.py +homeassistant/helpers/temperature.py +homeassistant/helpers/translation.py +homeassistant/helpers/typing.py diff --git a/script/translations_upload b/script/translations_upload index 5bf9fe1e121..52045e41d60 100755 --- a/script/translations_upload +++ b/script/translations_upload @@ -26,7 +26,8 @@ LANG_ISO=en CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD) -if [ "${CURRENT_BRANCH-}" != "dev" ] && [ "${TRAVIS_BRANCH-}" != "dev" ] ; then +# Check Travis and CircleCI environment as well +if [ "${CURRENT_BRANCH-}" != "dev" ] && [ "${TRAVIS_BRANCH-}" != "dev" ] && [ "${CIRCLE_BRANCH-}" != "dev" ]; then echo "Please only run the translations upload script from a clean checkout of dev." exit 1 fi diff --git a/tox.ini b/tox.ini index 8423141df60..b8995d9e877 100644 --- a/tox.ini +++ b/tox.ini @@ -42,4 +42,4 @@ deps = -r{toxinidir}/requirements_test.txt -c{toxinidir}/homeassistant/package_constraints.txt commands = - /bin/bash -c 'mypy homeassistant/*.py homeassistant/{auth,util}/ homeassistant/helpers/{__init__,aiohttp_client,area_registry,condition,deprecation,dispatcher,entity_values,entityfilter,icon,intent,json,location,signal,state,sun,temperature,translation,typing}.py' + /bin/bash -c 'TYPING_FILES=$(cat mypyrc); mypy $TYPING_FILES' From b30c1406484641c8ed773c0ac762197f8dc8b514 Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Mon, 1 Apr 2019 17:42:04 -0700 Subject: [PATCH 49/85] Require static-check success first for rest of workflow (#22635) * Require static-check success first * Update config.yml --- .circleci/config.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index b4f22601bb5..9c9d75d934b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -213,18 +213,26 @@ workflows: build: jobs: - static-check - - pre-install-all-requirements + - pre-install-all-requirements: + requires: + - static-check - pylint: requires: - pre-install-all-requirements - pre-test: name: pre-test 3.5.5 + requires: + - static-check python: 3.5.5-stretch - pre-test: name: pre-test 3.6 + requires: + - static-check python: 3.6-stretch - pre-test: name: pre-test 3.7 + requires: + - static-check python: 3.7-stretch - test: name: test 3.5.5 From 685de23a4e36f8d5c3234071c826f9a3115d47de Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 1 Apr 2019 21:51:43 -0700 Subject: [PATCH 50/85] Run PyLint under Python 3.5 (#22642) * Run PyLint under Python 3.5 * Remove -q from pip install to debug * Upgrade setuptools before install * Use correct cache key for pylint --- .circleci/config.yml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 9c9d75d934b..c8d7c7ee6b2 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -10,7 +10,7 @@ executors: parameters: tag: type: string - default: latest + default: latest docker: - image: circleci/python:<< parameters.tag >> - image: circleci/buildpack-deps:stretch @@ -53,6 +53,7 @@ commands: python3 -m venv venv . venv/bin/activate pip install -q -U pip + pip install -q -U setuptools <<# parameters.all >>pip install -q --progress-bar off -r requirements_all.txt -c homeassistant/package_constraints.txt<> <<# parameters.test >>pip install -q --progress-bar off -r requirements_test.txt -c homeassistant/package_constraints.txt<> <<# parameters.test_all >>pip install -q --progress-bar off -r requirements_test_all.txt -c homeassistant/package_constraints.txt<> @@ -107,27 +108,27 @@ jobs: pre-install-all-requirements: executor: name: python - tag: 3.7-stretch + tag: 3.5.5-stretch steps: - checkout - docker-prereqs - install-requirements: - python: 3.7-stretch + python: 3.5.5-stretch all: true test: true pylint: executor: name: python - tag: 3.7-stretch + tag: 3.5.5-stretch parallelism: 2 steps: - checkout - docker-prereqs - install-requirements: - python: 3.7-stretch + python: 3.5.5-stretch all: true test: true - install From 63d8dd9f7a4058526c31605084fdce2e9b83d89a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 3 Apr 2019 11:27:57 -0700 Subject: [PATCH 51/85] Bumped version to 0.91.0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 4091bc2ed02..36c61a51916 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 91 -PATCH_VERSION = '0b5' +PATCH_VERSION = '0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 8eb93a8beae51b6c9ccc5728aab3972ed83e6fdd Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 4 Apr 2019 11:10:44 +0200 Subject: [PATCH 52/85] Change URL handling (#22713) --- homeassistant/components/hassio/ingress.py | 28 +++++++++++----------- tests/components/hassio/test_ingress.py | 10 ++++---- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/hassio/ingress.py b/homeassistant/components/hassio/ingress.py index 04241f53de9..9b62bb89c94 100644 --- a/homeassistant/components/hassio/ingress.py +++ b/homeassistant/components/hassio/ingress.py @@ -30,7 +30,7 @@ class HassIOIngress(HomeAssistantView): """Hass.io view to handle base part.""" name = "api:hassio:ingress" - url = "/api/hassio_ingress/{addon}/{path:.+}" + url = "/api/hassio_ingress/{token}/{path:.+}" requires_auth = False def __init__(self, host: str, websession: aiohttp.ClientSession): @@ -38,21 +38,21 @@ class HassIOIngress(HomeAssistantView): self._host = host self._websession = websession - def _create_url(self, addon: str, path: str) -> str: + def _create_url(self, token: str, path: str) -> str: """Create URL to service.""" - return "http://{}/addons/{}/web/{}".format(self._host, addon, path) + return "http://{}/ingress/{}/{}".format(self._host, token, path) async def _handle( - self, request: web.Request, addon: str, path: str + self, request: web.Request, token: str, path: str ) -> Union[web.Response, web.StreamResponse, web.WebSocketResponse]: """Route data to Hass.io ingress service.""" try: # Websocket if _is_websocket(request): - return await self._handle_websocket(request, addon, path) + return await self._handle_websocket(request, token, path) # Request - return await self._handle_request(request, addon, path) + return await self._handle_request(request, token, path) except aiohttp.ClientError: pass @@ -65,15 +65,15 @@ class HassIOIngress(HomeAssistantView): delete = _handle async def _handle_websocket( - self, request: web.Request, addon: str, path: str + self, request: web.Request, token: str, path: str ) -> web.WebSocketResponse: """Ingress route for websocket.""" ws_server = web.WebSocketResponse() await ws_server.prepare(request) # Preparing - url = self._create_url(addon, path) - source_header = _init_header(request, addon) + url = self._create_url(token, path) + source_header = _init_header(request, token) # Support GET query if request.query_string: @@ -95,12 +95,12 @@ class HassIOIngress(HomeAssistantView): return ws_server async def _handle_request( - self, request: web.Request, addon: str, path: str + self, request: web.Request, token: str, path: str ) -> Union[web.Response, web.StreamResponse]: """Ingress route for request.""" - url = self._create_url(addon, path) + url = self._create_url(token, path) data = await request.read() - source_header = _init_header(request, addon) + source_header = _init_header(request, token) async with self._websession.request( request.method, url, headers=source_header, @@ -136,7 +136,7 @@ class HassIOIngress(HomeAssistantView): def _init_header( - request: web.Request, addon: str + request: web.Request, token: str ) -> Union[CIMultiDict, Dict[str, str]]: """Create initial header.""" headers = {} @@ -151,7 +151,7 @@ def _init_header( headers[X_HASSIO] = os.environ.get('HASSIO_TOKEN', "") # Ingress information - headers[X_INGRESS_PATH] = "/api/hassio_ingress/{}".format(addon) + headers[X_INGRESS_PATH] = "/api/hassio_ingress/{}".format(token) # Set X-Forwarded-For forward_for = request.headers.get(hdrs.X_FORWARDED_FOR) diff --git a/tests/components/hassio/test_ingress.py b/tests/components/hassio/test_ingress.py index 4b72eda4c25..0dda69b2b17 100644 --- a/tests/components/hassio/test_ingress.py +++ b/tests/components/hassio/test_ingress.py @@ -13,7 +13,7 @@ import pytest async def test_ingress_request_get( hassio_client, build_type, aioclient_mock): """Test no auth needed for .""" - aioclient_mock.get("http://127.0.0.1/addons/{}/web/{}".format( + aioclient_mock.get("http://127.0.0.1/ingress/{}/{}".format( build_type[0], build_type[1]), text="test") resp = await hassio_client.get( @@ -45,7 +45,7 @@ async def test_ingress_request_get( async def test_ingress_request_post( hassio_client, build_type, aioclient_mock): """Test no auth needed for .""" - aioclient_mock.post("http://127.0.0.1/addons/{}/web/{}".format( + aioclient_mock.post("http://127.0.0.1/ingress/{}/{}".format( build_type[0], build_type[1]), text="test") resp = await hassio_client.post( @@ -77,7 +77,7 @@ async def test_ingress_request_post( async def test_ingress_request_put( hassio_client, build_type, aioclient_mock): """Test no auth needed for .""" - aioclient_mock.put("http://127.0.0.1/addons/{}/web/{}".format( + aioclient_mock.put("http://127.0.0.1/ingress/{}/{}".format( build_type[0], build_type[1]), text="test") resp = await hassio_client.put( @@ -109,7 +109,7 @@ async def test_ingress_request_put( async def test_ingress_request_delete( hassio_client, build_type, aioclient_mock): """Test no auth needed for .""" - aioclient_mock.delete("http://127.0.0.1/addons/{}/web/{}".format( + aioclient_mock.delete("http://127.0.0.1/ingress/{}/{}".format( build_type[0], build_type[1]), text="test") resp = await hassio_client.delete( @@ -142,7 +142,7 @@ async def test_ingress_request_delete( async def test_ingress_websocket( hassio_client, build_type, aioclient_mock): """Test no auth needed for .""" - aioclient_mock.get("http://127.0.0.1/addons/{}/web/{}".format( + aioclient_mock.get("http://127.0.0.1/ingress/{}/{}".format( build_type[0], build_type[1])) # Ignore error because we can setup a full IO infrastructure From 193b608ee073736778f670f30bc212e977529404 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Thu, 4 Apr 2019 09:06:54 -0400 Subject: [PATCH 53/85] fix device class lookup for binary sensors (#22724) --- homeassistant/components/zha/core/const.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index b7f418253d8..a34a049fad2 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -77,7 +77,6 @@ ELECTRICAL_MEASUREMENT = 'electrical_measurement' GENERIC = 'generic' UNKNOWN = 'unknown' OPENING = 'opening' -ZONE = 'zone' OCCUPANCY = 'occupancy' ACCELERATION = 'acceleration' @@ -90,7 +89,7 @@ BASIC_CHANNEL = 'basic' COLOR_CHANNEL = 'light_color' FAN_CHANNEL = 'fan' LEVEL_CHANNEL = ATTR_LEVEL -ZONE_CHANNEL = 'ias_zone' +ZONE_CHANNEL = ZONE = 'ias_zone' ELECTRICAL_MEASUREMENT_CHANNEL = 'electrical_measurement' POWER_CONFIGURATION_CHANNEL = 'power' EVENT_RELAY_CHANNEL = 'event_relay' From ec07affe0dc93bb44e50647863e210ed2cb57d39 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 4 Apr 2019 17:43:18 +0200 Subject: [PATCH 54/85] Fix ingress routing with / (#22728) --- homeassistant/components/hassio/ingress.py | 2 +- tests/components/hassio/test_ingress.py | 12 ++++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/hassio/ingress.py b/homeassistant/components/hassio/ingress.py index 9b62bb89c94..49e949c5789 100644 --- a/homeassistant/components/hassio/ingress.py +++ b/homeassistant/components/hassio/ingress.py @@ -30,7 +30,7 @@ class HassIOIngress(HomeAssistantView): """Hass.io view to handle base part.""" name = "api:hassio:ingress" - url = "/api/hassio_ingress/{token}/{path:.+}" + url = "/api/hassio_ingress/{token}/{path:.*}" requires_auth = False def __init__(self, host: str, websession: aiohttp.ClientSession): diff --git a/tests/components/hassio/test_ingress.py b/tests/components/hassio/test_ingress.py index 0dda69b2b17..343068375de 100644 --- a/tests/components/hassio/test_ingress.py +++ b/tests/components/hassio/test_ingress.py @@ -8,7 +8,8 @@ import pytest @pytest.mark.parametrize( 'build_type', [ ("a3_vl", "test/beer/ping?index=1"), ("core", "index.html"), - ("local", "panel/config"), ("jk_921", "editor.php?idx=3&ping=5") + ("local", "panel/config"), ("jk_921", "editor.php?idx=3&ping=5"), + ("fsadjf10312", "") ]) async def test_ingress_request_get( hassio_client, build_type, aioclient_mock): @@ -40,7 +41,8 @@ async def test_ingress_request_get( @pytest.mark.parametrize( 'build_type', [ ("a3_vl", "test/beer/ping?index=1"), ("core", "index.html"), - ("local", "panel/config"), ("jk_921", "editor.php?idx=3&ping=5") + ("local", "panel/config"), ("jk_921", "editor.php?idx=3&ping=5"), + ("fsadjf10312", "") ]) async def test_ingress_request_post( hassio_client, build_type, aioclient_mock): @@ -72,7 +74,8 @@ async def test_ingress_request_post( @pytest.mark.parametrize( 'build_type', [ ("a3_vl", "test/beer/ping?index=1"), ("core", "index.html"), - ("local", "panel/config"), ("jk_921", "editor.php?idx=3&ping=5") + ("local", "panel/config"), ("jk_921", "editor.php?idx=3&ping=5"), + ("fsadjf10312", "") ]) async def test_ingress_request_put( hassio_client, build_type, aioclient_mock): @@ -104,7 +107,8 @@ async def test_ingress_request_put( @pytest.mark.parametrize( 'build_type', [ ("a3_vl", "test/beer/ping?index=1"), ("core", "index.html"), - ("local", "panel/config"), ("jk_921", "editor.php?idx=3&ping=5") + ("local", "panel/config"), ("jk_921", "editor.php?idx=3&ping=5"), + ("fsadjf10312", "") ]) async def test_ingress_request_delete( hassio_client, build_type, aioclient_mock): From 79facb82c6a9b4d29bdfee95b40f159e5a55d8e4 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 5 Apr 2019 08:41:13 +0200 Subject: [PATCH 55/85] Fix chunk streaming (#22730) * Fix chunk streaming * Cleanup a print * Better error handling * Fix import order --- homeassistant/components/hassio/http.py | 1 - homeassistant/components/hassio/ingress.py | 22 ++++++++++++---------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/hassio/http.py b/homeassistant/components/hassio/http.py index 7284004d72f..a798d312c25 100644 --- a/homeassistant/components/hassio/http.py +++ b/homeassistant/components/hassio/http.py @@ -85,7 +85,6 @@ class HassIOView(HomeAssistantView): "http://{}/{}".format(self._host, path), data=data, headers=headers, timeout=read_timeout ) - print(client.headers) # Simple request if int(client.headers.get(CONTENT_LENGTH, 0)) < 4194000: diff --git a/homeassistant/components/hassio/ingress.py b/homeassistant/components/hassio/ingress.py index 49e949c5789..27a7f05ec14 100644 --- a/homeassistant/components/hassio/ingress.py +++ b/homeassistant/components/hassio/ingress.py @@ -1,21 +1,23 @@ """Hass.io Add-on ingress service.""" import asyncio -from ipaddress import ip_address +import logging import os +from ipaddress import ip_address from typing import Dict, Union import aiohttp -from aiohttp import web -from aiohttp import hdrs +from aiohttp import hdrs, web from aiohttp.web_exceptions import HTTPBadGateway from multidict import CIMultiDict -from homeassistant.core import callback from homeassistant.components.http import HomeAssistantView +from homeassistant.core import callback from homeassistant.helpers.typing import HomeAssistantType from .const import X_HASSIO, X_INGRESS_PATH +_LOGGER = logging.getLogger(__name__) + @callback def async_setup_ingress(hass: HomeAssistantType, host: str): @@ -54,8 +56,8 @@ class HassIOIngress(HomeAssistantView): # Request return await self._handle_request(request, token, path) - except aiohttp.ClientError: - pass + except aiohttp.ClientError as err: + _LOGGER.debug("Ingress error with %s / %s: %s", token, path, err) raise HTTPBadGateway() from None @@ -126,11 +128,11 @@ class HassIOIngress(HomeAssistantView): try: await response.prepare(request) - async for data in result.content: + async for data in result.content.iter_chunked(4096): await response.write(data) - except (aiohttp.ClientError, aiohttp.ClientPayloadError): - pass + except (aiohttp.ClientError, aiohttp.ClientPayloadError) as err: + _LOGGER.debug("Stream error %s / %s: %s", token, path, err) return response @@ -183,7 +185,7 @@ def _response_header(response: aiohttp.ClientResponse) -> Dict[str, str]: for name, value in response.headers.items(): if name in (hdrs.TRANSFER_ENCODING, hdrs.CONTENT_LENGTH, - hdrs.CONTENT_TYPE): + hdrs.CONTENT_TYPE, hdrs.CONTENT_ENCODING): continue headers[name] = value From bab966fb2900a1b705aa5c0a84b649c72d447a29 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 4 Apr 2019 13:50:10 -0600 Subject: [PATCH 56/85] Fix incorrect "Unavailable" Ambient sensors (#22734) * Fix incorrect "Unavailable" Ambient sensors * Removed unnecessary cast --- homeassistant/components/ambient_station/__init__.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ambient_station/__init__.py b/homeassistant/components/ambient_station/__init__.py index 2383d011945..d0f74da4170 100644 --- a/homeassistant/components/ambient_station/__init__.py +++ b/homeassistant/components/ambient_station/__init__.py @@ -417,9 +417,8 @@ class AmbientWeatherEntity(Entity): @property def available(self): """Return True if entity is available.""" - return bool( - self._ambient.stations[self._mac_address][ATTR_LAST_DATA].get( - self._sensor_type)) + return self._ambient.stations[self._mac_address][ATTR_LAST_DATA].get( + self._sensor_type) is not None @property def device_info(self): From 74a7d4117e4d22e83ef745749193e6466bbc50db Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 4 Apr 2019 13:43:21 -0600 Subject: [PATCH 57/85] Bump aioambient to 0.2.0 (#22736) --- homeassistant/components/ambient_station/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ambient_station/__init__.py b/homeassistant/components/ambient_station/__init__.py index d0f74da4170..cb0a2067d9f 100644 --- a/homeassistant/components/ambient_station/__init__.py +++ b/homeassistant/components/ambient_station/__init__.py @@ -20,7 +20,7 @@ from .const import ( ATTR_LAST_DATA, CONF_APP_KEY, DATA_CLIENT, DOMAIN, TOPIC_UPDATE, TYPE_BINARY_SENSOR, TYPE_SENSOR) -REQUIREMENTS = ['aioambient==0.1.3'] +REQUIREMENTS = ['aioambient==0.2.0'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index a773c9c8c31..231637fef8d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -97,7 +97,7 @@ abodepy==0.15.0 afsapi==0.0.4 # homeassistant.components.ambient_station -aioambient==0.1.3 +aioambient==0.2.0 # homeassistant.components.asuswrt aioasuswrt==1.1.21 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 27d96c5e606..48390ff101a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -35,7 +35,7 @@ PyTransportNSW==0.1.1 YesssSMS==0.2.3 # homeassistant.components.ambient_station -aioambient==0.1.3 +aioambient==0.2.0 # homeassistant.components.automatic.device_tracker aioautomatic==0.6.5 From dc185b994d5838ea87a581be13de171876bf5449 Mon Sep 17 00:00:00 2001 From: Chris Helming Date: Fri, 5 Apr 2019 02:40:47 -0400 Subject: [PATCH 58/85] Update Foscam stream for newer models (#22744) * Update Foscam to support stream source * Removing spaces and tabs * Changing to Python3-style string formatting * Adding '_media_port' to hopefully cover other models * changing logic for success and return none * Update Foscam stream for newer models * change if to or --- homeassistant/components/foscam/camera.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/foscam/camera.py b/homeassistant/components/foscam/camera.py index b6f2162d57a..1b4ee039053 100644 --- a/homeassistant/components/foscam/camera.py +++ b/homeassistant/components/foscam/camera.py @@ -58,10 +58,11 @@ class FoscamCam(Camera): self._foscam_session = FoscamCamera( ip_address, port, self._username, self._password, verbose=False) - self._media_port = None + self._rtsp_port = None result, response = self._foscam_session.get_port_info() if result == 0: - self._media_port = response['mediaPort'] + self._rtsp_port = response.get('rtspPort') or \ + response.get('mediaPort') def camera_image(self): """Return a still image response from the camera.""" @@ -76,19 +77,19 @@ class FoscamCam(Camera): @property def supported_features(self): """Return supported features.""" - if self._media_port: + if self._rtsp_port: return SUPPORT_STREAM return 0 @property def stream_source(self): """Return the stream source.""" - if self._media_port: + if self._rtsp_port: return 'rtsp://{}:{}@{}:{}/videoMain'.format( self._username, self._password, self._foscam_session.host, - self._media_port) + self._rtsp_port) return None @property From 5252c92670e3d364d7f6f6e11896713ccc1c8e82 Mon Sep 17 00:00:00 2001 From: Jason Hunter Date: Fri, 5 Apr 2019 02:40:22 -0400 Subject: [PATCH 59/85] use the input stream codec as the template for the output streams (#22747) --- homeassistant/components/stream/worker.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index 3ca8ac079e3..0292fd30596 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -29,11 +29,7 @@ def create_stream_buffer(stream_output, video_stream, audio_frame): segment = io.BytesIO() output = av.open( segment, mode='w', format=stream_output.format) - vstream = output.add_stream( - stream_output.video_codec, video_stream.rate) - # Fix format - vstream.codec_context.format = \ - video_stream.codec_context.format + vstream = output.add_stream(template=video_stream) # Check if audio is requested astream = None if stream_output.audio_codec: From e9d55bf1c072366b272f393d114f8af54a1f573b Mon Sep 17 00:00:00 2001 From: Fredrik Erlandsson Date: Fri, 5 Apr 2019 08:48:41 +0200 Subject: [PATCH 60/85] fixes configuration flow #22706 (#22754) --- homeassistant/components/daikin/config_flow.py | 7 +++++-- tests/components/daikin/test_config_flow.py | 7 ++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/daikin/config_flow.py b/homeassistant/components/daikin/config_flow.py index 590b5a02738..3c5daac4653 100644 --- a/homeassistant/components/daikin/config_flow.py +++ b/homeassistant/components/daikin/config_flow.py @@ -38,9 +38,12 @@ class FlowHandler(config_entries.ConfigFlow): """Create device.""" from pydaikin.appliance import Appliance try: + device = Appliance( + host, + self.hass.helpers.aiohttp_client.async_get_clientsession(), + ) with async_timeout.timeout(10): - device = await self.hass.async_add_executor_job( - Appliance, host) + await device.init() except asyncio.TimeoutError: return self.async_abort(reason='device_timeout') except Exception: # pylint: disable=broad-except diff --git a/tests/components/daikin/test_config_flow.py b/tests/components/daikin/test_config_flow.py index f6b2f0b1d41..fa288f6c2ef 100644 --- a/tests/components/daikin/test_config_flow.py +++ b/tests/components/daikin/test_config_flow.py @@ -24,9 +24,14 @@ def init_config_flow(hass): @pytest.fixture def mock_daikin(): - """Mock tellduslive.""" + """Mock pydaikin.""" + async def mock_daikin_init(): + """Mock the init function in pydaikin.""" + pass + with MockDependency('pydaikin.appliance') as mock_daikin_: mock_daikin_.Appliance().values.get.return_value = 'AABBCCDDEEFF' + mock_daikin_.Appliance().init = mock_daikin_init yield mock_daikin_ From cd3f51f7b12d0113110ddea3a778531db49e2525 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 4 Apr 2019 23:49:37 -0700 Subject: [PATCH 61/85] Bumped version to 0.91.1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 36c61a51916..6ece276307e 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 91 -PATCH_VERSION = '0' +PATCH_VERSION = '1' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 9198047ad5b3decc1b2090f97350eea8d4a930c8 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 5 Apr 2019 13:29:43 +0200 Subject: [PATCH 62/85] Cleanup cookie handling (#22757) --- homeassistant/components/hassio/ingress.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hassio/ingress.py b/homeassistant/components/hassio/ingress.py index 27a7f05ec14..91224c6f54d 100644 --- a/homeassistant/components/hassio/ingress.py +++ b/homeassistant/components/hassio/ingress.py @@ -106,7 +106,7 @@ class HassIOIngress(HomeAssistantView): async with self._websession.request( request.method, url, headers=source_header, - params=request.query, data=data, cookies=request.cookies + params=request.query, data=data ) as result: headers = _response_header(result) @@ -145,7 +145,8 @@ def _init_header( # filter flags for name, value in request.headers.items(): - if name in (hdrs.CONTENT_LENGTH, hdrs.CONTENT_TYPE): + if name in (hdrs.CONTENT_LENGTH, hdrs.CONTENT_TYPE, + hdrs.CONTENT_ENCODING): continue headers[name] = value From 9eb32728f1069385dc425a4648a4881f3a321626 Mon Sep 17 00:00:00 2001 From: zewelor Date: Fri, 5 Apr 2019 13:32:46 +0200 Subject: [PATCH 63/85] Fix yeelight recorder warning (#22756) --- homeassistant/components/yeelight/__init__.py | 34 +--------------- homeassistant/components/yeelight/light.py | 39 +++++++++++++++++-- 2 files changed, 36 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/yeelight/__init__.py b/homeassistant/components/yeelight/__init__.py index fb218a67698..99382bb1da9 100644 --- a/homeassistant/components/yeelight/__init__.py +++ b/homeassistant/components/yeelight/__init__.py @@ -111,37 +111,6 @@ UPDATE_REQUEST_PROPERTIES = [ ] -def _transitions_config_parser(transitions): - """Parse transitions config into initialized objects.""" - import yeelight - - transition_objects = [] - for transition_config in transitions: - transition, params = list(transition_config.items())[0] - transition_objects.append(getattr(yeelight, transition)(*params)) - - return transition_objects - - -def _parse_custom_effects(effects_config): - import yeelight - - effects = {} - for config in effects_config: - params = config[CONF_FLOW_PARAMS] - action = yeelight.Flow.actions[params[ATTR_ACTION]] - transitions = _transitions_config_parser( - params[ATTR_TRANSITIONS]) - - effects[config[CONF_NAME]] = { - ATTR_COUNT: params[ATTR_COUNT], - ATTR_ACTION: action, - ATTR_TRANSITIONS: transitions - } - - return effects - - def setup(hass, config): """Set up the Yeelight bulbs.""" conf = config.get(DOMAIN, {}) @@ -192,9 +161,8 @@ def _setup_device(hass, hass_config, ipaddr, device_config): platform_config = device_config.copy() platform_config[CONF_HOST] = ipaddr - platform_config[CONF_CUSTOM_EFFECTS] = _parse_custom_effects( + platform_config[CONF_CUSTOM_EFFECTS] = \ hass_config.get(DOMAIN, {}).get(CONF_CUSTOM_EFFECTS, {}) - ) load_platform(hass, LIGHT_DOMAIN, DOMAIN, platform_config, hass_config) load_platform(hass, BINARY_SENSOR_DOMAIN, DOMAIN, platform_config, diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index 92b668c6987..912a4f99c92 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -7,7 +7,7 @@ from homeassistant.helpers.service import extract_entity_ids from homeassistant.util.color import ( color_temperature_mired_to_kelvin as mired_to_kelvin, color_temperature_kelvin_to_mired as kelvin_to_mired) -from homeassistant.const import CONF_HOST, ATTR_ENTITY_ID +from homeassistant.const import CONF_HOST, ATTR_ENTITY_ID, CONF_NAME from homeassistant.core import callback from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_HS_COLOR, ATTR_TRANSITION, ATTR_COLOR_TEMP, @@ -19,8 +19,8 @@ from homeassistant.components.yeelight import ( CONF_TRANSITION, DATA_YEELIGHT, CONF_MODE_MUSIC, CONF_SAVE_ON_CHANGE, CONF_CUSTOM_EFFECTS, DATA_UPDATED, YEELIGHT_SERVICE_SCHEMA, DOMAIN, ATTR_TRANSITIONS, - YEELIGHT_FLOW_TRANSITION_SCHEMA, _transitions_config_parser, - ACTION_RECOVER) + YEELIGHT_FLOW_TRANSITION_SCHEMA, ACTION_RECOVER, CONF_FLOW_PARAMS, + ATTR_ACTION, ATTR_COUNT) DEPENDENCIES = ['yeelight'] @@ -81,6 +81,37 @@ YEELIGHT_EFFECT_LIST = [ EFFECT_STOP] +def _transitions_config_parser(transitions): + """Parse transitions config into initialized objects.""" + import yeelight + + transition_objects = [] + for transition_config in transitions: + transition, params = list(transition_config.items())[0] + transition_objects.append(getattr(yeelight, transition)(*params)) + + return transition_objects + + +def _parse_custom_effects(effects_config): + import yeelight + + effects = {} + for config in effects_config: + params = config[CONF_FLOW_PARAMS] + action = yeelight.Flow.actions[params[ATTR_ACTION]] + transitions = _transitions_config_parser( + params[ATTR_TRANSITIONS]) + + effects[config[CONF_NAME]] = { + ATTR_COUNT: params[ATTR_COUNT], + ATTR_ACTION: action, + ATTR_TRANSITIONS: transitions + } + + return effects + + def _cmd(func): """Define a wrapper to catch exceptions from the bulb.""" def _wrap(self, *args, **kwargs): @@ -109,7 +140,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): device = hass.data[DATA_YEELIGHT][discovery_info[CONF_HOST]] _LOGGER.debug("Adding %s", device.name) - custom_effects = discovery_info[CONF_CUSTOM_EFFECTS] + custom_effects = _parse_custom_effects(discovery_info[CONF_CUSTOM_EFFECTS]) lights = [YeelightLight(device, custom_effects=custom_effects)] From a44966f483795e3e723ae11065423d2e83927265 Mon Sep 17 00:00:00 2001 From: Rohan Kapoor Date: Fri, 5 Apr 2019 09:15:35 -0700 Subject: [PATCH 64/85] Correctly load Mopar's config (#22771) --- homeassistant/components/mopar/__init__.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/mopar/__init__.py b/homeassistant/components/mopar/__init__.py index d845d585765..4ee9f3219b4 100644 --- a/homeassistant/components/mopar/__init__.py +++ b/homeassistant/components/mopar/__init__.py @@ -53,12 +53,13 @@ def setup(hass, config): """Set up the Mopar component.""" import motorparts + conf = config[DOMAIN] cookie = hass.config.path(COOKIE_FILE) try: session = motorparts.get_session( - config[CONF_USERNAME], - config[CONF_PASSWORD], - config[CONF_PIN], + conf[CONF_USERNAME], + conf[CONF_PASSWORD], + conf[CONF_PIN], cookie_path=cookie ) except motorparts.MoparError: @@ -69,7 +70,7 @@ def setup(hass, config): data.update(now=None) track_time_interval( - hass, data.update, config[CONF_SCAN_INTERVAL] + hass, data.update, conf[CONF_SCAN_INTERVAL] ) def handle_horn(call): From dbe53a39474405552250650cbf6472b969c0ba4a Mon Sep 17 00:00:00 2001 From: Nate Clark Date: Fri, 5 Apr 2019 18:22:57 -0400 Subject: [PATCH 65/85] Fix konnected unique_id computation for switches (#22777) --- homeassistant/components/konnected/switch.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/konnected/switch.py b/homeassistant/components/konnected/switch.py index 7384d62900e..3db602215b9 100644 --- a/homeassistant/components/konnected/switch.py +++ b/homeassistant/components/konnected/switch.py @@ -41,9 +41,10 @@ class KonnectedSwitch(ToggleEntity): self._pause = self._data.get(CONF_PAUSE) self._repeat = self._data.get(CONF_REPEAT) self._state = self._boolean_state(self._data.get(ATTR_STATE)) - self._unique_id = '{}-{}'.format(device_id, hash(frozenset( - {self._pin_num, self._momentary, self._pause, self._repeat}))) self._name = self._data.get(CONF_NAME) + self._unique_id = '{}-{}-{}-{}-{}'.format( + device_id, self._pin_num, self._momentary, + self._pause, self._repeat) @property def unique_id(self) -> str: From f004f440d3e04cb073ea1d36fa1a9552bb478c67 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Fri, 5 Apr 2019 19:50:20 -0400 Subject: [PATCH 66/85] make the custom polling actually request state (#22778) --- homeassistant/components/zha/light.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index 6ba4efa9b0f..27a8e37e3ab 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -238,13 +238,21 @@ class Light(ZhaEntity, light.Light): async def async_update(self): """Attempt to retrieve on off state from the light.""" await super().async_update() + await self.async_get_state() + + async def async_get_state(self, from_cache=True): + """Attempt to retrieve on off state from the light.""" if self._on_off_channel: self._state = await self._on_off_channel.get_attribute_value( - 'on_off') + 'on_off', from_cache=from_cache) if self._level_channel: self._brightness = await self._level_channel.get_attribute_value( - 'current_level') + 'current_level', from_cache=from_cache) async def refresh(self, time): - """Call async_update at an interval.""" - await self.async_update() + """Call async_get_state at an interval.""" + await self.async_get_state(from_cache=False) + + def debug(self, msg, *args): + """Log debug message.""" + _LOGGER.debug('%s: ' + msg, self.entity_id, *args) From a69b1a359da0e288c670979125f1531bed107fcb Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Fri, 5 Apr 2019 19:06:41 -0400 Subject: [PATCH 67/85] ZHA Light debug logging. (#22776) --- homeassistant/components/zha/light.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index 27a8e37e3ab..fc29fd0cdd2 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -172,6 +172,7 @@ class Light(ZhaEntity, light.Light): duration = transition * 10 if transition else DEFAULT_DURATION brightness = kwargs.get(light.ATTR_BRIGHTNESS) + t_log = {} if (brightness is not None or transition) and \ self._supported_features & light.SUPPORT_BRIGHTNESS: if brightness is not None: @@ -182,7 +183,9 @@ class Light(ZhaEntity, light.Light): level, duration ) + t_log['move_to_level_with_on_off'] = success if not success: + self.debug("turned on: %s", t_log) return self._state = bool(level) if level: @@ -190,7 +193,9 @@ class Light(ZhaEntity, light.Light): if brightness is None or brightness: success = await self._on_off_channel.on() + t_log['on_off'] = success if not success: + self.debug("turned on: %s", t_log) return self._state = True @@ -199,7 +204,9 @@ class Light(ZhaEntity, light.Light): temperature = kwargs[light.ATTR_COLOR_TEMP] success = await self._color_channel.move_to_color_temp( temperature, duration) + t_log['move_to_color_temp'] = success if not success: + self.debug("turned on: %s", t_log) return self._color_temp = temperature @@ -212,10 +219,13 @@ class Light(ZhaEntity, light.Light): int(xy_color[1] * 65535), duration, ) + t_log['move_to_color'] = success if not success: + self.debug("turned on: %s", t_log) return self._hs_color = hs_color + self.debug("turned on: %s", t_log) self.async_schedule_update_ha_state() async def async_turn_off(self, **kwargs): @@ -229,7 +239,7 @@ class Light(ZhaEntity, light.Light): ) else: success = await self._on_off_channel.off() - _LOGGER.debug("%s was turned off: %s", self.entity_id, success) + self.debug("turned off: %s", success) if not success: return self._state = False From d8119b2281d742c6120c195b5e04b390b76d997b Mon Sep 17 00:00:00 2001 From: Wolfgang Malgadey Date: Fri, 5 Apr 2019 02:52:06 +0200 Subject: [PATCH 68/85] Fix tado turn on off (#22291) * fix for turn on and off, with new pyTado missing blank line * removed, because can't push * uploaded the file through github again --- homeassistant/components/tado/__init__.py | 5 +++++ homeassistant/components/tado/climate.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tado/__init__.py b/homeassistant/components/tado/__init__.py index 6808729685e..8d3f541972e 100644 --- a/homeassistant/components/tado/__init__.py +++ b/homeassistant/components/tado/__init__.py @@ -121,3 +121,8 @@ class TadoDataStore: """Wrap for setZoneOverlay(..).""" self.tado.setZoneOverlay(zone_id, mode, temperature, duration) self.update(no_throttle=True) # pylint: disable=unexpected-keyword-arg + + def set_zone_off(self, zone_id, mode): + """Set a zone to off.""" + self.tado.setZoneOverlay(zone_id, mode, None, None, 'HEATING', 'OFF') + self.update(no_throttle=True) # pylint: disable=unexpected-keyword-arg diff --git a/homeassistant/components/tado/climate.py b/homeassistant/components/tado/climate.py index 56c670184b5..90d5f076974 100644 --- a/homeassistant/components/tado/climate.py +++ b/homeassistant/components/tado/climate.py @@ -363,7 +363,7 @@ class TadoClimate(ClimateDevice): if self._current_operation == CONST_MODE_OFF: _LOGGER.info("Switching mytado.com to OFF for zone %s", self.zone_name) - self._store.set_zone_overlay(self.zone_id, CONST_OVERLAY_MANUAL) + self._store.set_zone_off(self.zone_id, CONST_OVERLAY_MANUAL) self._overlay_mode = self._current_operation return From ed9d1e776fd078a313a20f1f66658728f27ad2cf Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Sun, 31 Mar 2019 21:30:45 -0700 Subject: [PATCH 69/85] Add new mobile_app webhook command: get_zones (#22604) ## Description: Adds a new `mobile_app` webhook command, `get_zones`, which just returns all zones. ## Checklist: - [x] The code change is tested and works locally. - [x] Local tests pass with `tox`. **Your PR cannot be merged unless tests pass** - [x] There is no commented out code in this PR. --- homeassistant/components/mobile_app/const.py | 5 +-- .../components/mobile_app/helpers.py | 5 +-- .../components/mobile_app/webhook.py | 32 +++++++++++++------ homeassistant/components/zone/config_flow.py | 2 +- tests/components/mobile_app/test_webhook.py | 26 +++++++++++++++ 5 files changed, 55 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/mobile_app/const.py b/homeassistant/components/mobile_app/const.py index 61c50e97c6e..b59c631ba99 100644 --- a/homeassistant/components/mobile_app/const.py +++ b/homeassistant/components/mobile_app/const.py @@ -67,6 +67,7 @@ ERR_SENSOR_DUPLICATE_UNIQUE_ID = 'duplicate_unique_id' WEBHOOK_TYPE_CALL_SERVICE = 'call_service' WEBHOOK_TYPE_FIRE_EVENT = 'fire_event' +WEBHOOK_TYPE_GET_ZONES = 'get_zones' WEBHOOK_TYPE_REGISTER_SENSOR = 'register_sensor' WEBHOOK_TYPE_RENDER_TEMPLATE = 'render_template' WEBHOOK_TYPE_UPDATE_LOCATION = 'update_location' @@ -74,8 +75,8 @@ WEBHOOK_TYPE_UPDATE_REGISTRATION = 'update_registration' WEBHOOK_TYPE_UPDATE_SENSOR_STATES = 'update_sensor_states' WEBHOOK_TYPES = [WEBHOOK_TYPE_CALL_SERVICE, WEBHOOK_TYPE_FIRE_EVENT, - WEBHOOK_TYPE_REGISTER_SENSOR, WEBHOOK_TYPE_RENDER_TEMPLATE, - WEBHOOK_TYPE_UPDATE_LOCATION, + WEBHOOK_TYPE_GET_ZONES, WEBHOOK_TYPE_REGISTER_SENSOR, + WEBHOOK_TYPE_RENDER_TEMPLATE, WEBHOOK_TYPE_UPDATE_LOCATION, WEBHOOK_TYPE_UPDATE_REGISTRATION, WEBHOOK_TYPE_UPDATE_SENSOR_STATES] diff --git a/homeassistant/components/mobile_app/helpers.py b/homeassistant/components/mobile_app/helpers.py index 60bd8b4e1d6..ee593588ef8 100644 --- a/homeassistant/components/mobile_app/helpers.py +++ b/homeassistant/components/mobile_app/helpers.py @@ -6,6 +6,7 @@ from typing import Callable, Dict, Tuple from aiohttp.web import json_response, Response from homeassistant.core import Context +from homeassistant.helpers.json import JSONEncoder from homeassistant.helpers.typing import HomeAssistantType from .const import (ATTR_APP_DATA, ATTR_APP_ID, ATTR_APP_NAME, @@ -133,9 +134,9 @@ def savable_state(hass: HomeAssistantType) -> Dict: def webhook_response(data, *, registration: Dict, status: int = 200, headers: Dict = None) -> Response: """Return a encrypted response if registration supports it.""" - data = json.dumps(data) + data = json.dumps(data, cls=JSONEncoder) - if CONF_SECRET in registration: + if registration[ATTR_SUPPORTS_ENCRYPTION]: keylen, encrypt = setup_encrypt() key = registration[CONF_SECRET].encode("utf-8") diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py index aafa6046d11..71c6d0d6673 100644 --- a/homeassistant/components/mobile_app/webhook.py +++ b/homeassistant/components/mobile_app/webhook.py @@ -9,6 +9,8 @@ from homeassistant.components.device_tracker import (ATTR_ATTRIBUTES, DOMAIN as DT_DOMAIN, SERVICE_SEE as DT_SEE) +from homeassistant.components.zone.const import DOMAIN as ZONE_DOMAIN + from homeassistant.const import (ATTR_DOMAIN, ATTR_SERVICE, ATTR_SERVICE_DATA, CONF_WEBHOOK_ID, HTTP_BAD_REQUEST, HTTP_CREATED) @@ -33,9 +35,10 @@ from .const import (ATTR_ALTITUDE, ATTR_BATTERY, ATTR_COURSE, ATTR_DEVICE_ID, DATA_STORE, DOMAIN, ERR_ENCRYPTION_REQUIRED, ERR_SENSOR_DUPLICATE_UNIQUE_ID, ERR_SENSOR_NOT_REGISTERED, SIGNAL_SENSOR_UPDATE, WEBHOOK_PAYLOAD_SCHEMA, - WEBHOOK_SCHEMAS, WEBHOOK_TYPE_CALL_SERVICE, - WEBHOOK_TYPE_FIRE_EVENT, WEBHOOK_TYPE_REGISTER_SENSOR, - WEBHOOK_TYPE_RENDER_TEMPLATE, WEBHOOK_TYPE_UPDATE_LOCATION, + WEBHOOK_SCHEMAS, WEBHOOK_TYPES, WEBHOOK_TYPE_CALL_SERVICE, + WEBHOOK_TYPE_FIRE_EVENT, WEBHOOK_TYPE_GET_ZONES, + WEBHOOK_TYPE_REGISTER_SENSOR, WEBHOOK_TYPE_RENDER_TEMPLATE, + WEBHOOK_TYPE_UPDATE_LOCATION, WEBHOOK_TYPE_UPDATE_REGISTRATION, WEBHOOK_TYPE_UPDATE_SENSOR_STATES) @@ -87,16 +90,19 @@ async def handle_webhook(hass: HomeAssistantType, webhook_id: str, enc_data = req_data[ATTR_WEBHOOK_ENCRYPTED_DATA] webhook_payload = _decrypt_payload(registration[CONF_SECRET], enc_data) - if webhook_type not in WEBHOOK_SCHEMAS: + if webhook_type not in WEBHOOK_TYPES: _LOGGER.error('Received invalid webhook type: %s', webhook_type) return empty_okay_response() - try: - data = WEBHOOK_SCHEMAS[webhook_type](webhook_payload) - except vol.Invalid as ex: - err = vol.humanize.humanize_error(webhook_payload, ex) - _LOGGER.error('Received invalid webhook payload: %s', err) - return empty_okay_response(headers=headers) + data = webhook_payload + + if webhook_type in WEBHOOK_SCHEMAS: + try: + data = WEBHOOK_SCHEMAS[webhook_type](webhook_payload) + except vol.Invalid as ex: + err = vol.humanize.humanize_error(webhook_payload, ex) + _LOGGER.error('Received invalid webhook payload: %s', err) + return empty_okay_response(headers=headers) context = registration_context(registration) @@ -261,3 +267,9 @@ async def handle_webhook(hass: HomeAssistantType, webhook_id: str, return webhook_response(resp, registration=registration, headers=headers) + + if webhook_type == WEBHOOK_TYPE_GET_ZONES: + zones = (hass.states.get(entity_id) for entity_id + in sorted(hass.states.async_entity_ids(ZONE_DOMAIN))) + return webhook_response(list(zones), registration=registration, + headers=headers) diff --git a/homeassistant/components/zone/config_flow.py b/homeassistant/components/zone/config_flow.py index bf221a828ad..a7b968676d6 100644 --- a/homeassistant/components/zone/config_flow.py +++ b/homeassistant/components/zone/config_flow.py @@ -14,7 +14,7 @@ from .const import CONF_PASSIVE, DOMAIN, HOME_ZONE @callback def configured_zones(hass): - """Return a set of the configured hosts.""" + """Return a set of the configured zones.""" return set((slugify(entry.data[CONF_NAME])) for entry in hass.config_entries.async_entries(DOMAIN)) diff --git a/tests/components/mobile_app/test_webhook.py b/tests/components/mobile_app/test_webhook.py index a70e8ba1275..ad19a70a806 100644 --- a/tests/components/mobile_app/test_webhook.py +++ b/tests/components/mobile_app/test_webhook.py @@ -4,8 +4,10 @@ import logging import pytest from homeassistant.components.mobile_app.const import CONF_SECRET +from homeassistant.components.zone import DOMAIN as ZONE_DOMAIN from homeassistant.const import CONF_WEBHOOK_ID from homeassistant.core import callback +from homeassistant.setup import async_setup_component from tests.common import async_mock_service @@ -100,6 +102,30 @@ async def test_webhook_update_registration(webhook_client, hass_client): # noqa assert CONF_SECRET not in update_json +async def test_webhook_handle_get_zones(hass, create_registrations, # noqa: F401, F811, E501 + webhook_client): # noqa: F811 + """Test that we can get zones properly.""" + await async_setup_component(hass, ZONE_DOMAIN, { + ZONE_DOMAIN: { + 'name': 'test', + 'latitude': 32.880837, + 'longitude': -117.237561, + 'radius': 250, + } + }) + + resp = await webhook_client.post( + '/api/webhook/{}'.format(create_registrations[1]['webhook_id']), + json={'type': 'get_zones'} + ) + + assert resp.status == 200 + + json = await resp.json() + assert len(json) == 1 + assert json[0]['entity_id'] == 'zone.home' + + async def test_webhook_returns_error_incorrect_json(webhook_client, # noqa: F401, F811, E501 create_registrations, # noqa: F401, F811, E501 caplog): # noqa: E501 F811 From 7a8aa79f191ed633babc1134238017c164b306f3 Mon Sep 17 00:00:00 2001 From: Chris Helming Date: Fri, 5 Apr 2019 23:02:38 -0400 Subject: [PATCH 70/85] Add optional rtsp_port for Foscam (#22786) * add optional rtsp port for config * getting rid of default=None * removing vol.Any --- homeassistant/components/foscam/camera.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/foscam/camera.py b/homeassistant/components/foscam/camera.py index 1b4ee039053..8adac658625 100644 --- a/homeassistant/components/foscam/camera.py +++ b/homeassistant/components/foscam/camera.py @@ -19,6 +19,7 @@ _LOGGER = logging.getLogger(__name__) REQUIREMENTS = ['libpyfoscam==1.0'] CONF_IP = 'ip' +CONF_RTSP_PORT = 'rtsp_port' DEFAULT_NAME = 'Foscam Camera' DEFAULT_PORT = 88 @@ -31,6 +32,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_USERNAME): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_RTSP_PORT): cv.port }) @@ -58,11 +60,12 @@ class FoscamCam(Camera): self._foscam_session = FoscamCamera( ip_address, port, self._username, self._password, verbose=False) - self._rtsp_port = None - result, response = self._foscam_session.get_port_info() - if result == 0: - self._rtsp_port = response.get('rtspPort') or \ - response.get('mediaPort') + self._rtsp_port = device_info.get(CONF_RTSP_PORT) + if not self._rtsp_port: + result, response = self._foscam_session.get_port_info() + if result == 0: + self._rtsp_port = response.get('rtspPort') or \ + response.get('mediaPort') def camera_image(self): """Return a still image response from the camera.""" From abb531c06bf2dba25f49cdd4992b34202233cf5c Mon Sep 17 00:00:00 2001 From: zewelor Date: Sun, 7 Apr 2019 16:07:34 +0200 Subject: [PATCH 71/85] Improve yeelight imports (#22804) --- homeassistant/components/yeelight/__init__.py | 20 +++++++------------ homeassistant/components/yeelight/light.py | 17 ++++++++-------- 2 files changed, 16 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/yeelight/__init__.py b/homeassistant/components/yeelight/__init__.py index 99382bb1da9..7a14a4d1842 100644 --- a/homeassistant/components/yeelight/__init__.py +++ b/homeassistant/components/yeelight/__init__.py @@ -185,8 +185,8 @@ class YeelightDevice: @property def bulb(self): """Return bulb device.""" - import yeelight if self._bulb_device is None: + import yeelight try: self._bulb_device = yeelight.Bulb(self._ipaddr, model=self._model) @@ -241,33 +241,27 @@ class YeelightDevice: def turn_on(self, duration=DEFAULT_TRANSITION, light_type=None): """Turn on device.""" - import yeelight - - if not light_type: - light_type = yeelight.enums.LightType.Main + from yeelight import BulbException try: self.bulb.turn_on(duration=duration, light_type=light_type) - except yeelight.BulbException as ex: + except BulbException as ex: _LOGGER.error("Unable to turn the bulb on: %s", ex) return def turn_off(self, duration=DEFAULT_TRANSITION, light_type=None): """Turn off device.""" - import yeelight - - if not light_type: - light_type = yeelight.enums.LightType.Main + from yeelight import BulbException try: self.bulb.turn_off(duration=duration, light_type=light_type) - except yeelight.BulbException as ex: + except BulbException as ex: _LOGGER.error("Unable to turn the bulb off: %s", ex) return def update(self): """Read new properties from the device.""" - import yeelight + from yeelight import BulbException if not self.bulb: return @@ -275,7 +269,7 @@ class YeelightDevice: try: self.bulb.get_properties(UPDATE_REQUEST_PROPERTIES) self._available = True - except yeelight.BulbException as ex: + except BulbException as ex: if self._available: # just inform once _LOGGER.error("Unable to update bulb status: %s", ex) self._available = False diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index 912a4f99c92..e842d91bf2a 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -189,6 +189,8 @@ class YeelightLight(Light): def __init__(self, device, custom_effects=None): """Initialize the Yeelight light.""" + from yeelight.enums import LightType + self.config = device.config self._device = device @@ -202,6 +204,8 @@ class YeelightLight(Light): self._min_mireds = None self._max_mireds = None + self._light_type = LightType.Main + if custom_effects: self._custom_effects = custom_effects else: @@ -281,8 +285,7 @@ class YeelightLight(Light): @property def light_type(self): """Return light type.""" - import yeelight - return yeelight.enums.LightType.Main + return self._light_type def _get_hs_from_properties(self): rgb = self._get_property('rgb') @@ -589,21 +592,19 @@ class YeelightAmbientLight(YeelightLight): def __init__(self, *args, **kwargs): """Initialize the Yeelight Ambient light.""" + from yeelight.enums import LightType + super().__init__(*args, **kwargs) self._min_mireds = kelvin_to_mired(6500) self._max_mireds = kelvin_to_mired(1700) + self._light_type = LightType.Ambient + @property def name(self) -> str: """Return the name of the device if any.""" return "{} ambilight".format(self.device.name) - @property - def light_type(self): - """Return light type.""" - import yeelight - return yeelight.enums.LightType.Ambient - @property def _is_nightlight_enabled(self): return False From 82f6bed3a3f24cf3928ddd4574db7d28aa53f908 Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Sun, 7 Apr 2019 01:16:54 -0700 Subject: [PATCH 72/85] Add a new mobile_app webhook command to get config (#22813) * Add a new mobile_app webhook command to get config * Limit fields returned --- homeassistant/components/mobile_app/const.py | 6 ++-- .../components/mobile_app/webhook.py | 24 ++++++++++--- tests/components/mobile_app/test_webhook.py | 34 +++++++++++++++++++ 3 files changed, 58 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/mobile_app/const.py b/homeassistant/components/mobile_app/const.py index b59c631ba99..52ca0aa3993 100644 --- a/homeassistant/components/mobile_app/const.py +++ b/homeassistant/components/mobile_app/const.py @@ -67,6 +67,7 @@ ERR_SENSOR_DUPLICATE_UNIQUE_ID = 'duplicate_unique_id' WEBHOOK_TYPE_CALL_SERVICE = 'call_service' WEBHOOK_TYPE_FIRE_EVENT = 'fire_event' +WEBHOOK_TYPE_GET_CONFIG = 'get_config' WEBHOOK_TYPE_GET_ZONES = 'get_zones' WEBHOOK_TYPE_REGISTER_SENSOR = 'register_sensor' WEBHOOK_TYPE_RENDER_TEMPLATE = 'render_template' @@ -75,8 +76,9 @@ WEBHOOK_TYPE_UPDATE_REGISTRATION = 'update_registration' WEBHOOK_TYPE_UPDATE_SENSOR_STATES = 'update_sensor_states' WEBHOOK_TYPES = [WEBHOOK_TYPE_CALL_SERVICE, WEBHOOK_TYPE_FIRE_EVENT, - WEBHOOK_TYPE_GET_ZONES, WEBHOOK_TYPE_REGISTER_SENSOR, - WEBHOOK_TYPE_RENDER_TEMPLATE, WEBHOOK_TYPE_UPDATE_LOCATION, + WEBHOOK_TYPE_GET_CONFIG, WEBHOOK_TYPE_GET_ZONES, + WEBHOOK_TYPE_REGISTER_SENSOR, WEBHOOK_TYPE_RENDER_TEMPLATE, + WEBHOOK_TYPE_UPDATE_LOCATION, WEBHOOK_TYPE_UPDATE_REGISTRATION, WEBHOOK_TYPE_UPDATE_SENSOR_STATES] diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py index 71c6d0d6673..a57d3930f50 100644 --- a/homeassistant/components/mobile_app/webhook.py +++ b/homeassistant/components/mobile_app/webhook.py @@ -8,7 +8,7 @@ from homeassistant.components.device_tracker import (ATTR_ATTRIBUTES, ATTR_DEV_ID, DOMAIN as DT_DOMAIN, SERVICE_SEE as DT_SEE) - +from homeassistant.components.frontend import MANIFEST_JSON from homeassistant.components.zone.const import DOMAIN as ZONE_DOMAIN from homeassistant.const import (ATTR_DOMAIN, ATTR_SERVICE, ATTR_SERVICE_DATA, @@ -36,9 +36,9 @@ from .const import (ATTR_ALTITUDE, ATTR_BATTERY, ATTR_COURSE, ATTR_DEVICE_ID, ERR_SENSOR_DUPLICATE_UNIQUE_ID, ERR_SENSOR_NOT_REGISTERED, SIGNAL_SENSOR_UPDATE, WEBHOOK_PAYLOAD_SCHEMA, WEBHOOK_SCHEMAS, WEBHOOK_TYPES, WEBHOOK_TYPE_CALL_SERVICE, - WEBHOOK_TYPE_FIRE_EVENT, WEBHOOK_TYPE_GET_ZONES, - WEBHOOK_TYPE_REGISTER_SENSOR, WEBHOOK_TYPE_RENDER_TEMPLATE, - WEBHOOK_TYPE_UPDATE_LOCATION, + WEBHOOK_TYPE_FIRE_EVENT, WEBHOOK_TYPE_GET_CONFIG, + WEBHOOK_TYPE_GET_ZONES, WEBHOOK_TYPE_REGISTER_SENSOR, + WEBHOOK_TYPE_RENDER_TEMPLATE, WEBHOOK_TYPE_UPDATE_LOCATION, WEBHOOK_TYPE_UPDATE_REGISTRATION, WEBHOOK_TYPE_UPDATE_SENSOR_STATES) @@ -273,3 +273,19 @@ async def handle_webhook(hass: HomeAssistantType, webhook_id: str, in sorted(hass.states.async_entity_ids(ZONE_DOMAIN))) return webhook_response(list(zones), registration=registration, headers=headers) + + if webhook_type == WEBHOOK_TYPE_GET_CONFIG: + + hass_config = hass.config.as_dict() + + return webhook_response({ + 'latitude': hass_config['latitude'], + 'longitude': hass_config['longitude'], + 'elevation': hass_config['elevation'], + 'unit_system': hass_config['unit_system'], + 'location_name': hass_config['location_name'], + 'time_zone': hass_config['time_zone'], + 'components': hass_config['components'], + 'version': hass_config['version'], + 'theme_color': MANIFEST_JSON['theme_color'], + }, registration=registration, headers=headers) diff --git a/tests/components/mobile_app/test_webhook.py b/tests/components/mobile_app/test_webhook.py index ad19a70a806..43eac28ec18 100644 --- a/tests/components/mobile_app/test_webhook.py +++ b/tests/components/mobile_app/test_webhook.py @@ -126,6 +126,40 @@ async def test_webhook_handle_get_zones(hass, create_registrations, # noqa: F40 assert json[0]['entity_id'] == 'zone.home' +async def test_webhook_handle_get_config(hass, create_registrations, # noqa: F401, F811, E501 + webhook_client): # noqa: F811 + """Test that we can get config properly.""" + resp = await webhook_client.post( + '/api/webhook/{}'.format(create_registrations[1]['webhook_id']), + json={'type': 'get_config'} + ) + + assert resp.status == 200 + + json = await resp.json() + if 'components' in json: + json['components'] = set(json['components']) + if 'whitelist_external_dirs' in json: + json['whitelist_external_dirs'] = \ + set(json['whitelist_external_dirs']) + + hass_config = hass.config.as_dict() + + expected_dict = { + 'latitude': hass_config['latitude'], + 'longitude': hass_config['longitude'], + 'elevation': hass_config['elevation'], + 'unit_system': hass_config['unit_system'], + 'location_name': hass_config['location_name'], + 'time_zone': hass_config['time_zone'], + 'components': hass_config['components'], + 'version': hass_config['version'], + 'theme_color': '#03A9F4', # Default frontend theme color + } + + assert expected_dict == json + + async def test_webhook_returns_error_incorrect_json(webhook_client, # noqa: F401, F811, E501 create_registrations, # noqa: F401, F811, E501 caplog): # noqa: E501 F811 From 474fc21c66105a6b6ae5ac61639c04be9e372279 Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Sun, 7 Apr 2019 01:17:14 -0700 Subject: [PATCH 73/85] Fix for optional values in the update_location webhook call (#22817) * Fix for optional values in the update_location webhook call * Square brackets instead of .get --- .../components/mobile_app/webhook.py | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py index a57d3930f50..7a83ba4e978 100644 --- a/homeassistant/components/mobile_app/webhook.py +++ b/homeassistant/components/mobile_app/webhook.py @@ -145,18 +145,26 @@ async def handle_webhook(hass: HomeAssistantType, webhook_id: str, if webhook_type == WEBHOOK_TYPE_UPDATE_LOCATION: see_payload = { ATTR_DEV_ID: registration[ATTR_DEVICE_ID], - ATTR_LOCATION_NAME: data.get(ATTR_LOCATION_NAME), - ATTR_GPS: data.get(ATTR_GPS), - ATTR_GPS_ACCURACY: data.get(ATTR_GPS_ACCURACY), - ATTR_BATTERY: data.get(ATTR_BATTERY), - ATTR_ATTRIBUTES: { - ATTR_SPEED: data.get(ATTR_SPEED), - ATTR_ALTITUDE: data.get(ATTR_ALTITUDE), - ATTR_COURSE: data.get(ATTR_COURSE), - ATTR_VERTICAL_ACCURACY: data.get(ATTR_VERTICAL_ACCURACY), - } + ATTR_GPS: data[ATTR_GPS], + ATTR_GPS_ACCURACY: data[ATTR_GPS_ACCURACY], } + for key in (ATTR_LOCATION_NAME, ATTR_BATTERY): + value = data.get(key) + if value is not None: + see_payload[key] = value + + attrs = {} + + for key in (ATTR_ALTITUDE, ATTR_COURSE, + ATTR_SPEED, ATTR_VERTICAL_ACCURACY): + value = data.get(key) + if value is not None: + attrs[key] = value + + if attrs: + see_payload[ATTR_ATTRIBUTES] = attrs + try: await hass.services.async_call(DT_DOMAIN, DT_SEE, see_payload, From f51e8c3012e30490ed3210278eb6f5a309c6e787 Mon Sep 17 00:00:00 2001 From: Jason Hunter Date: Sun, 7 Apr 2019 13:08:08 -0400 Subject: [PATCH 74/85] coerce duration and lookback to int so they can be used in template automation (#22819) --- homeassistant/components/camera/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index e453cdfd1a1..9ec081166bb 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -88,8 +88,8 @@ CAMERA_SERVICE_PLAY_STREAM = CAMERA_SERVICE_SCHEMA.extend({ CAMERA_SERVICE_RECORD = CAMERA_SERVICE_SCHEMA.extend({ vol.Required(CONF_FILENAME): cv.template, - vol.Optional(CONF_DURATION, default=30): int, - vol.Optional(CONF_LOOKBACK, default=0): int, + vol.Optional(CONF_DURATION, default=30): vol.Coerce(int), + vol.Optional(CONF_LOOKBACK, default=0): vol.Coerce(int), }) WS_TYPE_CAMERA_THUMBNAIL = 'camera_thumbnail' From 236e484dc27a88712c88f30e04511d028924b554 Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Sun, 7 Apr 2019 07:37:27 -0700 Subject: [PATCH 75/85] Fix for rate limits should be optional (#22823) --- homeassistant/components/mobile_app/const.py | 5 ++++ homeassistant/components/mobile_app/notify.py | 28 +++++++++++++------ 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/mobile_app/const.py b/homeassistant/components/mobile_app/const.py index 52ca0aa3993..31364ba063d 100644 --- a/homeassistant/components/mobile_app/const.py +++ b/homeassistant/components/mobile_app/const.py @@ -42,6 +42,11 @@ ATTR_OS_NAME = 'os_name' ATTR_OS_VERSION = 'os_version' ATTR_PUSH_TOKEN = 'push_token' ATTR_PUSH_URL = 'push_url' +ATTR_PUSH_RATE_LIMITS = 'rateLimits' +ATTR_PUSH_RATE_LIMITS_ERRORS = 'errors' +ATTR_PUSH_RATE_LIMITS_MAXIMUM = 'maximum' +ATTR_PUSH_RATE_LIMITS_RESETS_AT = 'resetsAt' +ATTR_PUSH_RATE_LIMITS_SUCCESSFUL = 'successful' ATTR_SUPPORTS_ENCRYPTION = 'supports_encryption' ATTR_EVENT_DATA = 'event_data' diff --git a/homeassistant/components/mobile_app/notify.py b/homeassistant/components/mobile_app/notify.py index 0120b1a6ffb..8d2ac1b97ec 100644 --- a/homeassistant/components/mobile_app/notify.py +++ b/homeassistant/components/mobile_app/notify.py @@ -8,13 +8,18 @@ import async_timeout from homeassistant.components.notify import ( ATTR_DATA, ATTR_MESSAGE, ATTR_TARGET, ATTR_TITLE, ATTR_TITLE_DEFAULT, BaseNotificationService) -from homeassistant.components.mobile_app.const import ( - ATTR_APP_DATA, ATTR_APP_ID, ATTR_APP_VERSION, ATTR_DEVICE_NAME, - ATTR_OS_VERSION, ATTR_PUSH_TOKEN, ATTR_PUSH_URL, DATA_CONFIG_ENTRIES, - DOMAIN) + from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.util.dt as dt_util +from .const import (ATTR_APP_DATA, ATTR_APP_ID, ATTR_APP_VERSION, + ATTR_DEVICE_NAME, ATTR_OS_VERSION, ATTR_PUSH_RATE_LIMITS, + ATTR_PUSH_RATE_LIMITS_ERRORS, + ATTR_PUSH_RATE_LIMITS_MAXIMUM, + ATTR_PUSH_RATE_LIMITS_RESETS_AT, + ATTR_PUSH_RATE_LIMITS_SUCCESSFUL, ATTR_PUSH_TOKEN, + ATTR_PUSH_URL, DATA_CONFIG_ENTRIES, DOMAIN) + _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['mobile_app'] @@ -38,16 +43,21 @@ def push_registrations(hass): # pylint: disable=invalid-name def log_rate_limits(hass, device_name, resp, level=logging.INFO): """Output rate limit log line at given level.""" - rate_limits = resp['rateLimits'] - resetsAt = dt_util.parse_datetime(rate_limits['resetsAt']) - resetsAtTime = resetsAt - datetime.now(timezone.utc) + if ATTR_PUSH_RATE_LIMITS not in resp: + return + + rate_limits = resp[ATTR_PUSH_RATE_LIMITS] + resetsAt = rate_limits[ATTR_PUSH_RATE_LIMITS_RESETS_AT] + resetsAtTime = (dt_util.parse_datetime(resetsAt) - + datetime.now(timezone.utc)) rate_limit_msg = ("mobile_app push notification rate limits for %s: " "%d sent, %d allowed, %d errors, " "resets in %s") _LOGGER.log(level, rate_limit_msg, device_name, - rate_limits['successful'], - rate_limits['maximum'], rate_limits['errors'], + rate_limits[ATTR_PUSH_RATE_LIMITS_SUCCESSFUL], + rate_limits[ATTR_PUSH_RATE_LIMITS_MAXIMUM], + rate_limits[ATTR_PUSH_RATE_LIMITS_ERRORS], str(resetsAtTime).split(".")[0]) From c5d4b7c24336777ac289579f99c5f19ea269ad0f Mon Sep 17 00:00:00 2001 From: zewelor Date: Sun, 7 Apr 2019 16:07:15 +0200 Subject: [PATCH 76/85] Use relative imports in yeelight (#22839) --- homeassistant/components/yeelight/binary_sensor.py | 2 +- homeassistant/components/yeelight/light.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/yeelight/binary_sensor.py b/homeassistant/components/yeelight/binary_sensor.py index cf7bbc5244e..d39af08f768 100644 --- a/homeassistant/components/yeelight/binary_sensor.py +++ b/homeassistant/components/yeelight/binary_sensor.py @@ -4,7 +4,7 @@ import logging from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.components.yeelight import DATA_YEELIGHT, DATA_UPDATED +from . import DATA_YEELIGHT, DATA_UPDATED DEPENDENCIES = ['yeelight'] diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index e842d91bf2a..74796a524b0 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -15,7 +15,7 @@ from homeassistant.components.light import ( SUPPORT_COLOR, SUPPORT_TRANSITION, SUPPORT_COLOR_TEMP, SUPPORT_FLASH, SUPPORT_EFFECT, Light) import homeassistant.util.color as color_util -from homeassistant.components.yeelight import ( +from . import ( CONF_TRANSITION, DATA_YEELIGHT, CONF_MODE_MUSIC, CONF_SAVE_ON_CHANGE, CONF_CUSTOM_EFFECTS, DATA_UPDATED, YEELIGHT_SERVICE_SCHEMA, DOMAIN, ATTR_TRANSITIONS, From 3f15b6b2d3ff6f02d3950001ca81925ed081b6ee Mon Sep 17 00:00:00 2001 From: zewelor Date: Sun, 7 Apr 2019 22:05:38 +0200 Subject: [PATCH 77/85] Fix yeelight possible array change during iteration (#22849) --- homeassistant/components/yeelight/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/yeelight/__init__.py b/homeassistant/components/yeelight/__init__.py index 7a14a4d1842..0cb9d41fe4b 100644 --- a/homeassistant/components/yeelight/__init__.py +++ b/homeassistant/components/yeelight/__init__.py @@ -134,7 +134,7 @@ def setup(hass, config): discovery.listen(hass, SERVICE_YEELIGHT, device_discovered) def update(event): - for device in yeelight_data.values(): + for device in list(yeelight_data.values()): device.update() track_time_interval( From 3a79e37cde39d861789b37dcb6451e9765251bc2 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 8 Apr 2019 09:22:55 +0200 Subject: [PATCH 78/85] Fix content_type handling ingress (#22864) --- homeassistant/components/hassio/ingress.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hassio/ingress.py b/homeassistant/components/hassio/ingress.py index 91224c6f54d..4f7c99618c1 100644 --- a/homeassistant/components/hassio/ingress.py +++ b/homeassistant/components/hassio/ingress.py @@ -118,6 +118,7 @@ class HassIOIngress(HomeAssistantView): return web.Response( headers=headers, status=result.status, + content_type=result.content_type, body=body ) @@ -145,8 +146,7 @@ def _init_header( # filter flags for name, value in request.headers.items(): - if name in (hdrs.CONTENT_LENGTH, hdrs.CONTENT_TYPE, - hdrs.CONTENT_ENCODING): + if name in (hdrs.CONTENT_LENGTH, hdrs.CONTENT_ENCODING): continue headers[name] = value From 3f739739700eea212b9747a72f46d6e7de5a2ee7 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 8 Apr 2019 08:00:22 +0000 Subject: [PATCH 79/85] Bumped version to 0.91.2 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 6ece276307e..28bdf2f7fc3 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 91 -PATCH_VERSION = '1' +PATCH_VERSION = '2' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From a04d44d97a59760ebb81517f25db9081d8ba4421 Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Mon, 8 Apr 2019 01:13:26 -0700 Subject: [PATCH 80/85] Minor sensor fixes (#22884) * Minor sensor fixes * Fix tests --- homeassistant/components/mobile_app/const.py | 2 +- homeassistant/components/mobile_app/sensor.py | 2 +- homeassistant/components/mobile_app/webhook.py | 4 ++-- tests/components/mobile_app/test_entity.py | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/mobile_app/const.py b/homeassistant/components/mobile_app/const.py index 31364ba063d..05d240da909 100644 --- a/homeassistant/components/mobile_app/const.py +++ b/homeassistant/components/mobile_app/const.py @@ -171,7 +171,7 @@ REGISTER_SENSOR_SCHEMA = vol.Schema({ vol.Required(ATTR_SENSOR_NAME): cv.string, vol.Required(ATTR_SENSOR_TYPE): vol.In(SENSOR_TYPES), vol.Required(ATTR_SENSOR_UNIQUE_ID): cv.string, - vol.Required(ATTR_SENSOR_UOM): cv.string, + vol.Optional(ATTR_SENSOR_UOM): cv.string, vol.Required(ATTR_SENSOR_STATE): vol.Any(bool, str, int, float), vol.Optional(ATTR_SENSOR_ICON, default='mdi:cellphone'): cv.icon, }) diff --git a/homeassistant/components/mobile_app/sensor.py b/homeassistant/components/mobile_app/sensor.py index c6a53ce57ec..b2846a6002b 100644 --- a/homeassistant/components/mobile_app/sensor.py +++ b/homeassistant/components/mobile_app/sensor.py @@ -55,4 +55,4 @@ class MobileAppSensor(MobileAppEntity): @property def unit_of_measurement(self): """Return the unit of measurement this sensor expresses itself in.""" - return self._config[ATTR_SENSOR_UOM] + return self._config.get(ATTR_SENSOR_UOM) diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py index 7a83ba4e978..28ef6bccd6a 100644 --- a/homeassistant/components/mobile_app/webhook.py +++ b/homeassistant/components/mobile_app/webhook.py @@ -228,7 +228,7 @@ async def handle_webhook(hass: HomeAssistantType, webhook_id: str, data[ATTR_SENSOR_TYPE]) async_dispatcher_send(hass, register_signal, data) - return webhook_response({"status": "registered"}, + return webhook_response({'success': True}, registration=registration, status=HTTP_CREATED, headers=headers) @@ -271,7 +271,7 @@ async def handle_webhook(hass: HomeAssistantType, webhook_id: str, async_dispatcher_send(hass, SIGNAL_SENSOR_UPDATE, new_state) - resp[unique_id] = {"status": "okay"} + resp[unique_id] = {'success': True} return webhook_response(resp, registration=registration, headers=headers) diff --git a/tests/components/mobile_app/test_entity.py b/tests/components/mobile_app/test_entity.py index 5dc285cfe9e..3d8e575f686 100644 --- a/tests/components/mobile_app/test_entity.py +++ b/tests/components/mobile_app/test_entity.py @@ -35,7 +35,7 @@ async def test_sensor(hass, create_registrations, webhook_client): # noqa: F401 assert reg_resp.status == 201 json = await reg_resp.json() - assert json == {'status': 'registered'} + assert json == {'success': True} entity = hass.states.get('sensor.battery_state') assert entity is not None @@ -122,7 +122,7 @@ async def test_sensor_id_no_dupes(hass, create_registrations, # noqa: F401, F81 assert reg_resp.status == 201 reg_json = await reg_resp.json() - assert reg_json == {'status': 'registered'} + assert reg_json == {'success': True} dupe_resp = await webhook_client.post(webhook_url, json=payload) From 9e56283eaf998f59e4e07dd61999c4c5ac8d919c Mon Sep 17 00:00:00 2001 From: John Raahauge <43510812+AZDane@users.noreply.github.com> Date: Mon, 8 Apr 2019 05:53:00 -0700 Subject: [PATCH 81/85] Fix position of add_entities of binary sensor (#22866) * Bugfix - binary_sensor.py * Added features to Concord232 Alarm Panel * Added New Line End Of File * Deleted Whitespace * Back to original Removed added feature and sticking to bugfix --- homeassistant/components/concord232/binary_sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/concord232/binary_sensor.py b/homeassistant/components/concord232/binary_sensor.py index 26f35d60305..3e4d0526db4 100644 --- a/homeassistant/components/concord232/binary_sensor.py +++ b/homeassistant/components/concord232/binary_sensor.py @@ -79,7 +79,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) ) - add_entities(sensors, True) + add_entities(sensors, True) def get_opening_type(zone): From a267df2abb97428dce40ea101349f438ae7c1178 Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Tue, 9 Apr 2019 02:48:59 -0700 Subject: [PATCH 82/85] More Mobile app sensor fixes (#22914) * Ensure we only add a sensor once * Ensure that we dont process updates for entities that arent what we were setup for * Add debug logging to ease development of apps * Use str representation --- .../components/mobile_app/binary_sensor.py | 13 ++++++++++++- homeassistant/components/mobile_app/entity.py | 14 ++++++++++++-- homeassistant/components/mobile_app/sensor.py | 15 +++++++++++++-- homeassistant/components/mobile_app/webhook.py | 3 +++ 4 files changed, 40 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/mobile_app/binary_sensor.py b/homeassistant/components/mobile_app/binary_sensor.py index 289a50584c9..50943bb6504 100644 --- a/homeassistant/components/mobile_app/binary_sensor.py +++ b/homeassistant/components/mobile_app/binary_sensor.py @@ -8,9 +8,10 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import (ATTR_SENSOR_STATE, ATTR_SENSOR_TYPE_BINARY_SENSOR as ENTITY_TYPE, + ATTR_SENSOR_UNIQUE_ID, DATA_DEVICES, DOMAIN) -from .entity import MobileAppEntity +from .entity import MobileAppEntity, sensor_id DEPENDENCIES = ['mobile_app'] @@ -36,6 +37,16 @@ async def async_setup_entry(hass, config_entry, async_add_entities): if data[CONF_WEBHOOK_ID] != webhook_id: return + unique_id = sensor_id(data[CONF_WEBHOOK_ID], + data[ATTR_SENSOR_UNIQUE_ID]) + + entity = hass.data[DOMAIN][ENTITY_TYPE][unique_id] + + if 'added' in entity: + return + + entity['added'] = True + device = hass.data[DOMAIN][DATA_DEVICES][data[CONF_WEBHOOK_ID]] async_add_entities([MobileAppBinarySensor(data, device, config_entry)]) diff --git a/homeassistant/components/mobile_app/entity.py b/homeassistant/components/mobile_app/entity.py index 05736b3a689..eca9d2b024b 100644 --- a/homeassistant/components/mobile_app/entity.py +++ b/homeassistant/components/mobile_app/entity.py @@ -13,6 +13,11 @@ from .const import (ATTR_DEVICE_ID, ATTR_DEVICE_NAME, ATTR_MANUFACTURER, DOMAIN, SIGNAL_SENSOR_UPDATE) +def sensor_id(webhook_id, unique_id): + """Return a unique sensor ID.""" + return "{}_{}".format(webhook_id, unique_id) + + class MobileAppEntity(Entity): """Representation of an mobile app entity.""" @@ -22,8 +27,8 @@ class MobileAppEntity(Entity): self._device = device self._entry = entry self._registration = entry.data - self._sensor_id = "{}_{}".format(self._registration[CONF_WEBHOOK_ID], - config[ATTR_SENSOR_UNIQUE_ID]) + self._sensor_id = sensor_id(self._registration[CONF_WEBHOOK_ID], + config[ATTR_SENSOR_UNIQUE_ID]) self._entity_type = config[ATTR_SENSOR_TYPE] self.unsub_dispatcher = None @@ -94,5 +99,10 @@ class MobileAppEntity(Entity): @callback def _handle_update(self, data): """Handle async event updates.""" + incoming_id = sensor_id(data[CONF_WEBHOOK_ID], + data[ATTR_SENSOR_UNIQUE_ID]) + if incoming_id != self._sensor_id: + return + self._config = data self.async_schedule_update_ha_state() diff --git a/homeassistant/components/mobile_app/sensor.py b/homeassistant/components/mobile_app/sensor.py index b2846a6002b..64ad69c5758 100644 --- a/homeassistant/components/mobile_app/sensor.py +++ b/homeassistant/components/mobile_app/sensor.py @@ -7,9 +7,10 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import (ATTR_SENSOR_STATE, ATTR_SENSOR_TYPE_SENSOR as ENTITY_TYPE, - ATTR_SENSOR_UOM, DATA_DEVICES, DOMAIN) + ATTR_SENSOR_UNIQUE_ID, ATTR_SENSOR_UOM, DATA_DEVICES, + DOMAIN) -from .entity import MobileAppEntity +from .entity import MobileAppEntity, sensor_id DEPENDENCIES = ['mobile_app'] @@ -35,6 +36,16 @@ async def async_setup_entry(hass, config_entry, async_add_entities): if data[CONF_WEBHOOK_ID] != webhook_id: return + unique_id = sensor_id(data[CONF_WEBHOOK_ID], + data[ATTR_SENSOR_UNIQUE_ID]) + + entity = hass.data[DOMAIN][ENTITY_TYPE][unique_id] + + if 'added' in entity: + return + + entity['added'] = True + device = hass.data[DOMAIN][DATA_DEVICES][data[CONF_WEBHOOK_ID]] async_add_entities([MobileAppSensor(data, device, config_entry)]) diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py index 28ef6bccd6a..7a868c6ac6a 100644 --- a/homeassistant/components/mobile_app/webhook.py +++ b/homeassistant/components/mobile_app/webhook.py @@ -96,6 +96,9 @@ async def handle_webhook(hass: HomeAssistantType, webhook_id: str, data = webhook_payload + _LOGGER.debug("Received webhook payload for type %s: %s", webhook_type, + data) + if webhook_type in WEBHOOK_SCHEMAS: try: data = WEBHOOK_SCHEMAS[webhook_type](webhook_payload) From e7a17b710d52845cc92f95cca29d0b47860642e5 Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Tue, 9 Apr 2019 02:47:57 -0700 Subject: [PATCH 83/85] Add cloudhook and remote UI vals to get_config (#22921) --- .../components/mobile_app/webhook.py | 34 +++++++++++++------ 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py index 7a868c6ac6a..1ef5f4ce531 100644 --- a/homeassistant/components/mobile_app/webhook.py +++ b/homeassistant/components/mobile_app/webhook.py @@ -4,6 +4,8 @@ import logging from aiohttp.web import HTTPBadRequest, Response, Request import voluptuous as vol +from homeassistant.components.cloud import (async_remote_ui_url, + CloudNotAvailable) from homeassistant.components.device_tracker import (ATTR_ATTRIBUTES, ATTR_DEV_ID, DOMAIN as DT_DOMAIN, @@ -31,14 +33,15 @@ from .const import (ATTR_ALTITUDE, ATTR_BATTERY, ATTR_COURSE, ATTR_DEVICE_ID, ATTR_TEMPLATE_VARIABLES, ATTR_VERTICAL_ACCURACY, ATTR_WEBHOOK_DATA, ATTR_WEBHOOK_ENCRYPTED, ATTR_WEBHOOK_ENCRYPTED_DATA, ATTR_WEBHOOK_TYPE, - CONF_SECRET, DATA_CONFIG_ENTRIES, DATA_DELETED_IDS, - DATA_STORE, DOMAIN, ERR_ENCRYPTION_REQUIRED, - ERR_SENSOR_DUPLICATE_UNIQUE_ID, ERR_SENSOR_NOT_REGISTERED, - SIGNAL_SENSOR_UPDATE, WEBHOOK_PAYLOAD_SCHEMA, - WEBHOOK_SCHEMAS, WEBHOOK_TYPES, WEBHOOK_TYPE_CALL_SERVICE, - WEBHOOK_TYPE_FIRE_EVENT, WEBHOOK_TYPE_GET_CONFIG, - WEBHOOK_TYPE_GET_ZONES, WEBHOOK_TYPE_REGISTER_SENSOR, - WEBHOOK_TYPE_RENDER_TEMPLATE, WEBHOOK_TYPE_UPDATE_LOCATION, + CONF_CLOUDHOOK_URL, CONF_REMOTE_UI_URL, CONF_SECRET, + DATA_CONFIG_ENTRIES, DATA_DELETED_IDS, DATA_STORE, DOMAIN, + ERR_ENCRYPTION_REQUIRED, ERR_SENSOR_DUPLICATE_UNIQUE_ID, + ERR_SENSOR_NOT_REGISTERED, SIGNAL_SENSOR_UPDATE, + WEBHOOK_PAYLOAD_SCHEMA, WEBHOOK_SCHEMAS, WEBHOOK_TYPES, + WEBHOOK_TYPE_CALL_SERVICE, WEBHOOK_TYPE_FIRE_EVENT, + WEBHOOK_TYPE_GET_CONFIG, WEBHOOK_TYPE_GET_ZONES, + WEBHOOK_TYPE_REGISTER_SENSOR, WEBHOOK_TYPE_RENDER_TEMPLATE, + WEBHOOK_TYPE_UPDATE_LOCATION, WEBHOOK_TYPE_UPDATE_REGISTRATION, WEBHOOK_TYPE_UPDATE_SENSOR_STATES) @@ -289,7 +292,7 @@ async def handle_webhook(hass: HomeAssistantType, webhook_id: str, hass_config = hass.config.as_dict() - return webhook_response({ + resp = { 'latitude': hass_config['latitude'], 'longitude': hass_config['longitude'], 'elevation': hass_config['elevation'], @@ -299,4 +302,15 @@ async def handle_webhook(hass: HomeAssistantType, webhook_id: str, 'components': hass_config['components'], 'version': hass_config['version'], 'theme_color': MANIFEST_JSON['theme_color'], - }, registration=registration, headers=headers) + } + + if CONF_CLOUDHOOK_URL in registration: + resp[CONF_CLOUDHOOK_URL] = registration[CONF_CLOUDHOOK_URL] + + try: + resp[CONF_REMOTE_UI_URL] = async_remote_ui_url(hass) + except CloudNotAvailable: + pass + + return webhook_response(resp, registration=registration, + headers=headers) From eab575e65db29fd3a6f633ec4bf88b31e6b29795 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 10 Apr 2019 05:13:39 +0200 Subject: [PATCH 84/85] Bugfix: pass protocol out of header to application layer (#22955) --- homeassistant/components/hassio/ingress.py | 41 +++++++++++++++------- 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/hassio/ingress.py b/homeassistant/components/hassio/ingress.py index 4f7c99618c1..f4cc97c3853 100644 --- a/homeassistant/components/hassio/ingress.py +++ b/homeassistant/components/hassio/ingress.py @@ -70,7 +70,18 @@ class HassIOIngress(HomeAssistantView): self, request: web.Request, token: str, path: str ) -> web.WebSocketResponse: """Ingress route for websocket.""" - ws_server = web.WebSocketResponse() + if hdrs.SEC_WEBSOCKET_PROTOCOL in request.headers: + req_protocols = [ + str(proto.strip()) + for proto in + request.headers[hdrs.SEC_WEBSOCKET_PROTOCOL].split(",") + ] + else: + req_protocols = () + + ws_server = web.WebSocketResponse( + protocols=req_protocols, autoclose=False, autoping=False + ) await ws_server.prepare(request) # Preparing @@ -83,7 +94,8 @@ class HassIOIngress(HomeAssistantView): # Start proxy async with self._websession.ws_connect( - url, headers=source_header + url, headers=source_header, protocols=req_protocols, + autoclose=False, autoping=False, ) as ws_client: # Proxy requests await asyncio.wait( @@ -205,14 +217,17 @@ def _is_websocket(request: web.Request) -> bool: async def _websocket_forward(ws_from, ws_to): """Handle websocket message directly.""" - async for msg in ws_from: - if msg.type == aiohttp.WSMsgType.TEXT: - await ws_to.send_str(msg.data) - elif msg.type == aiohttp.WSMsgType.BINARY: - await ws_to.send_bytes(msg.data) - elif msg.type == aiohttp.WSMsgType.PING: - await ws_to.ping() - elif msg.type == aiohttp.WSMsgType.PONG: - await ws_to.pong() - elif ws_to.closed: - await ws_to.close(code=ws_to.close_code, message=msg.extra) + try: + async for msg in ws_from: + if msg.type == aiohttp.WSMsgType.TEXT: + await ws_to.send_str(msg.data) + elif msg.type == aiohttp.WSMsgType.BINARY: + await ws_to.send_bytes(msg.data) + elif msg.type == aiohttp.WSMsgType.PING: + await ws_to.ping() + elif msg.type == aiohttp.WSMsgType.PONG: + await ws_to.pong() + elif ws_to.closed: + await ws_to.close(code=ws_to.close_code, message=msg.extra) + except RuntimeError: + _LOGGER.debug("Ingress Websocket runtime error") From 984af45bb299faf0bf613547734c3e1b9e70e629 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 10 Apr 2019 13:22:19 +0000 Subject: [PATCH 85/85] Bumped version to 0.91.3 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 28bdf2f7fc3..6a8a7fe6a1d 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 91 -PATCH_VERSION = '2' +PATCH_VERSION = '3' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3)