From b552fbe312351c630d1313bf10ee05265df7f7e7 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 8 May 2019 20:23:31 -0700 Subject: [PATCH 001/232] Version bump to 0.94.0.dev0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 9176c1b8939..8d21c9d191e 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ # coding: utf-8 """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 -MINOR_VERSION = 93 +MINOR_VERSION = 94 PATCH_VERSION = '0.dev0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) From 0e9d71f2327f4a67506cfe14b0cebd1a6c89c351 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Thu, 9 May 2019 06:08:07 +0200 Subject: [PATCH 002/232] Bump pyatmo to v1.11 (#23766) --- homeassistant/components/netatmo/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json index c7e91d645fc..a5e4e8aa7a7 100644 --- a/homeassistant/components/netatmo/manifest.json +++ b/homeassistant/components/netatmo/manifest.json @@ -3,7 +3,7 @@ "name": "Netatmo", "documentation": "https://www.home-assistant.io/components/netatmo", "requirements": [ - "pyatmo==1.10" + "pyatmo==1.11" ], "dependencies": [ "webhook" diff --git a/requirements_all.txt b/requirements_all.txt index 552bad7b31b..db555dab405 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -976,7 +976,7 @@ pyalarmdotcom==0.3.2 pyarlo==0.2.3 # homeassistant.components.netatmo -pyatmo==1.10 +pyatmo==1.11 # homeassistant.components.apple_tv pyatv==0.3.12 From 118d3bc11cf8a94efbc7ffd9afb73d225ed9d5c1 Mon Sep 17 00:00:00 2001 From: Markus Jankowski Date: Thu, 9 May 2019 09:57:03 +0200 Subject: [PATCH 003/232] Add Presence Detector Indoor to Homematic IP (#23755) * Add presence detector indoor use device classes constants * Add illuminance * isort --- .../homematicip_cloud/binary_sensor.py | 47 ++++++++++++++----- .../components/homematicip_cloud/sensor.py | 3 +- 2 files changed, 37 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/binary_sensor.py b/homeassistant/components/homematicip_cloud/binary_sensor.py index 19d35c47cdb..b006ec80686 100644 --- a/homeassistant/components/homematicip_cloud/binary_sensor.py +++ b/homeassistant/components/homematicip_cloud/binary_sensor.py @@ -3,14 +3,18 @@ import logging from homematicip.aio.device import ( AsyncDevice, AsyncMotionDetectorIndoor, AsyncMotionDetectorOutdoor, - AsyncMotionDetectorPushButton, AsyncRotaryHandleSensor, - AsyncShutterContact, AsyncSmokeDetector, AsyncWaterSensor, - AsyncWeatherSensor, AsyncWeatherSensorPlus, AsyncWeatherSensorPro) + AsyncMotionDetectorPushButton, AsyncPresenceDetectorIndoor, + AsyncRotaryHandleSensor, AsyncShutterContact, AsyncSmokeDetector, + AsyncWaterSensor, AsyncWeatherSensor, AsyncWeatherSensorPlus, + AsyncWeatherSensorPro) from homematicip.aio.group import AsyncSecurityGroup, AsyncSecurityZoneGroup from homematicip.aio.home import AsyncHome from homematicip.base.enums import SmokeDetectorAlarmType, WindowState -from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_BATTERY, DEVICE_CLASS_DOOR, DEVICE_CLASS_LIGHT, + DEVICE_CLASS_MOISTURE, DEVICE_CLASS_MOTION, DEVICE_CLASS_PRESENCE, + DEVICE_CLASS_SAFETY, DEVICE_CLASS_SMOKE, BinarySensorDevice) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -47,6 +51,8 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, AsyncMotionDetectorOutdoor, AsyncMotionDetectorPushButton)): devices.append(HomematicipMotionDetector(home, device)) + if isinstance(device, AsyncPresenceDetectorIndoor): + devices.append(HomematicipPresenceDetector(home, device)) if isinstance(device, AsyncSmokeDetector): devices.append(HomematicipSmokeDetector(home, device)) if isinstance(device, AsyncWaterSensor): @@ -77,7 +83,7 @@ class HomematicipShutterContact(HomematicipGenericDevice, BinarySensorDevice): @property def device_class(self) -> str: """Return the class of this sensor.""" - return 'door' + return DEVICE_CLASS_DOOR @property def is_on(self) -> bool: @@ -95,7 +101,7 @@ class HomematicipMotionDetector(HomematicipGenericDevice, BinarySensorDevice): @property def device_class(self) -> str: """Return the class of this sensor.""" - return 'motion' + return DEVICE_CLASS_MOTION @property def is_on(self) -> bool: @@ -105,13 +111,30 @@ class HomematicipMotionDetector(HomematicipGenericDevice, BinarySensorDevice): return self._device.motionDetected +class HomematicipPresenceDetector(HomematicipGenericDevice, + BinarySensorDevice): + """Representation of a HomematicIP Cloud presence detector.""" + + @property + def device_class(self) -> str: + """Return the class of this sensor.""" + return DEVICE_CLASS_PRESENCE + + @property + def is_on(self) -> bool: + """Return true if presence is detected.""" + if hasattr(self._device, 'sabotage') and self._device.sabotage: + return True + return self._device.presenceDetected + + class HomematicipSmokeDetector(HomematicipGenericDevice, BinarySensorDevice): """Representation of a HomematicIP Cloud smoke detector.""" @property def device_class(self) -> str: """Return the class of this sensor.""" - return 'smoke' + return DEVICE_CLASS_SMOKE @property def is_on(self) -> bool: @@ -126,7 +149,7 @@ class HomematicipWaterDetector(HomematicipGenericDevice, BinarySensorDevice): @property def device_class(self) -> str: """Return the class of this sensor.""" - return 'moisture' + return DEVICE_CLASS_MOISTURE @property def is_on(self) -> bool: @@ -162,7 +185,7 @@ class HomematicipRainSensor(HomematicipGenericDevice, BinarySensorDevice): @property def device_class(self) -> str: """Return the class of this sensor.""" - return 'moisture' + return DEVICE_CLASS_MOISTURE @property def is_on(self) -> bool: @@ -180,7 +203,7 @@ class HomematicipSunshineSensor(HomematicipGenericDevice, BinarySensorDevice): @property def device_class(self) -> str: """Return the class of this sensor.""" - return 'light' + return DEVICE_CLASS_LIGHT @property def is_on(self) -> bool: @@ -208,7 +231,7 @@ class HomematicipBatterySensor(HomematicipGenericDevice, BinarySensorDevice): @property def device_class(self) -> str: """Return the class of this sensor.""" - return 'battery' + return DEVICE_CLASS_BATTERY @property def is_on(self) -> bool: @@ -229,7 +252,7 @@ class HomematicipSecurityZoneSensorGroup(HomematicipGenericDevice, @property def device_class(self) -> str: """Return the class of this sensor.""" - return 'safety' + return DEVICE_CLASS_SAFETY @property def available(self) -> bool: diff --git a/homeassistant/components/homematicip_cloud/sensor.py b/homeassistant/components/homematicip_cloud/sensor.py index 3d91b25c2bd..b3e23bde2be 100644 --- a/homeassistant/components/homematicip_cloud/sensor.py +++ b/homeassistant/components/homematicip_cloud/sensor.py @@ -6,7 +6,7 @@ from homematicip.aio.device import ( AsyncHeatingThermostat, AsyncHeatingThermostatCompact, AsyncLightSensor, AsyncMotionDetectorIndoor, AsyncMotionDetectorOutdoor, AsyncMotionDetectorPushButton, AsyncPlugableSwitchMeasuring, - AsyncTemperatureHumiditySensorDisplay, + AsyncPresenceDetectorIndoor, AsyncTemperatureHumiditySensorDisplay, AsyncTemperatureHumiditySensorOutdoor, AsyncTemperatureHumiditySensorWithoutDisplay, AsyncWeatherSensor, AsyncWeatherSensorPlus, AsyncWeatherSensorPro) @@ -55,6 +55,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, if isinstance(device, (AsyncLightSensor, AsyncMotionDetectorIndoor, AsyncMotionDetectorOutdoor, AsyncMotionDetectorPushButton, + AsyncPresenceDetectorIndoor, AsyncWeatherSensor, AsyncWeatherSensorPlus, AsyncWeatherSensorPro)): From 4004867edaa42e4f71c42e6089bebab6ac4adfb0 Mon Sep 17 00:00:00 2001 From: Ties de Kock Date: Thu, 9 May 2019 09:07:56 -0700 Subject: [PATCH 004/232] Split up yaml loaders into multiple files (#23774) * Start moving parts of yaml utils to own module Move parts of yaml loader out of the single large file and start to create the structure of the yaml loaders in Ansible [0]. [0]: https://github.com/ansible/ansible/tree/devel/lib/ansible/parsing/yaml * Finish yaml migration, update tests and mocks * Move code around to finish the migration * Update the mocks so that `open` is patched in `homeassistant.util.yaml.loader` instead of `homeassistant.util.yaml`. * Updated mypy ignores * Updated external API of `homeasistant.util.yaml`, see below: Checked what part of the api of `homeassistant.util.yaml` was actually called from outside the tests and added an `__ALL__` that contains only these elements. Updated the tests so that references to internal parts of the API (e.g. the yaml module imported into `homeassistant.util.yaml.loader`) are referenced directly from `homeassistant.util.yaml.loader`. In `tests/test_yaml.py` the import `yaml` refers to `homeassistant.util.yaml` and `yaml_loader` refers to `~.loader`. Future work that remains for the next iteration is to create a custom SafeConstructor and refers to that instead of monkey patching `yaml` with custom loaders. * Update mocks in yaml dumper, check_config --- homeassistant/scripts/check_config.py | 25 ++-- homeassistant/util/yaml/__init__.py | 15 +++ homeassistant/util/yaml/const.py | 4 + homeassistant/util/yaml/dumper.py | 60 +++++++++ .../util/{yaml.py => yaml/loader.py} | 116 ++++-------------- homeassistant/util/yaml/objects.py | 13 ++ mypy.ini | 6 +- tests/common.py | 6 +- tests/components/scene/test_init.py | 4 +- tests/helpers/test_entity_registry.py | 2 +- tests/util/test_yaml.py | 67 +++++----- 11 files changed, 179 insertions(+), 139 deletions(-) create mode 100644 homeassistant/util/yaml/__init__.py create mode 100644 homeassistant/util/yaml/const.py create mode 100644 homeassistant/util/yaml/dumper.py rename homeassistant/util/{yaml.py => yaml/loader.py} (83%) create mode 100644 homeassistant/util/yaml/objects.py diff --git a/homeassistant/scripts/check_config.py b/homeassistant/scripts/check_config.py index 5fe4e95a480..27b2738871c 100644 --- a/homeassistant/scripts/check_config.py +++ b/homeassistant/scripts/check_config.py @@ -17,7 +17,8 @@ from homeassistant.config import ( CONF_PACKAGES, merge_packages_config, _format_config_error, find_config_file, load_yaml_config_file, extract_domain_configs, config_per_platform) -from homeassistant.util import yaml + +import homeassistant.util.yaml.loader as yaml_loader from homeassistant.exceptions import HomeAssistantError REQUIREMENTS = ('colorlog==4.0.2',) @@ -25,12 +26,14 @@ REQUIREMENTS = ('colorlog==4.0.2',) _LOGGER = logging.getLogger(__name__) # pylint: disable=protected-access MOCKS = { - 'load': ("homeassistant.util.yaml.load_yaml", yaml.load_yaml), - 'load*': ("homeassistant.config.load_yaml", yaml.load_yaml), - 'secrets': ("homeassistant.util.yaml.secret_yaml", yaml.secret_yaml), + 'load': ("homeassistant.util.yaml.loader.load_yaml", + yaml_loader.load_yaml), + 'load*': ("homeassistant.config.load_yaml", yaml_loader.load_yaml), + 'secrets': ("homeassistant.util.yaml.loader.secret_yaml", + yaml_loader.secret_yaml), } SILENCE = ( - 'homeassistant.scripts.check_config.yaml.clear_secret_cache', + 'homeassistant.scripts.check_config.yaml_loader.clear_secret_cache', ) PATCHES = {} @@ -195,7 +198,8 @@ def check(config_dir, secrets=False): if secrets: # Ensure !secrets point to the patched function - yaml.yaml.SafeLoader.add_constructor('!secret', yaml.secret_yaml) + yaml_loader.yaml.SafeLoader.add_constructor('!secret', + yaml_loader.secret_yaml) try: hass = core.HomeAssistant() @@ -203,7 +207,7 @@ def check(config_dir, secrets=False): res['components'] = hass.loop.run_until_complete( check_ha_config_file(hass)) - res['secret_cache'] = OrderedDict(yaml.__SECRET_CACHE) + res['secret_cache'] = OrderedDict(yaml_loader.__SECRET_CACHE) for err in res['components'].errors: domain = err.domain or ERROR_STR @@ -221,7 +225,8 @@ def check(config_dir, secrets=False): pat.stop() if secrets: # Ensure !secrets point to the original function - yaml.yaml.SafeLoader.add_constructor('!secret', yaml.secret_yaml) + yaml_loader.yaml.SafeLoader.add_constructor( + '!secret', yaml_loader.secret_yaml) bootstrap.clear_secret_cache() return res @@ -239,7 +244,7 @@ def line_info(obj, **kwargs): def dump_dict(layer, indent_count=3, listi=False, **kwargs): """Display a dict. - A friendly version of print yaml.yaml.dump(config). + A friendly version of print yaml_loader.yaml.dump(config). """ def sort_dict_key(val): """Return the dict key for sorting.""" @@ -311,7 +316,7 @@ async def check_ha_config_file(hass): return result.add_error( "Error loading {}: {}".format(config_path, err)) finally: - yaml.clear_secret_cache() + yaml_loader.clear_secret_cache() # Extract and validate core [homeassistant] config try: diff --git a/homeassistant/util/yaml/__init__.py b/homeassistant/util/yaml/__init__.py new file mode 100644 index 00000000000..da797a23074 --- /dev/null +++ b/homeassistant/util/yaml/__init__.py @@ -0,0 +1,15 @@ +"""YAML utility functions.""" +from .const import ( + SECRET_YAML, _SECRET_NAMESPACE +) +from .dumper import dump, save_yaml +from .loader import ( + clear_secret_cache, load_yaml, secret_yaml +) + + +__all__ = [ + 'SECRET_YAML', '_SECRET_NAMESPACE', + 'dump', 'save_yaml', + 'clear_secret_cache', 'load_yaml', 'secret_yaml', +] diff --git a/homeassistant/util/yaml/const.py b/homeassistant/util/yaml/const.py new file mode 100644 index 00000000000..9bd08d99326 --- /dev/null +++ b/homeassistant/util/yaml/const.py @@ -0,0 +1,4 @@ +"""Constants.""" +SECRET_YAML = 'secrets.yaml' + +_SECRET_NAMESPACE = 'homeassistant' diff --git a/homeassistant/util/yaml/dumper.py b/homeassistant/util/yaml/dumper.py new file mode 100644 index 00000000000..d8f766c6c2b --- /dev/null +++ b/homeassistant/util/yaml/dumper.py @@ -0,0 +1,60 @@ +"""Custom dumper and representers.""" +from collections import OrderedDict +import yaml + +from .objects import NodeListClass + + +def dump(_dict: dict) -> str: + """Dump YAML to a string and remove null.""" + return yaml.safe_dump( + _dict, default_flow_style=False, allow_unicode=True) \ + .replace(': null\n', ':\n') + + +def save_yaml(path: str, data: dict) -> None: + """Save YAML to a file.""" + # Dump before writing to not truncate the file if dumping fails + str_data = dump(data) + with open(path, 'w', encoding='utf-8') as outfile: + outfile.write(str_data) + + +# From: https://gist.github.com/miracle2k/3184458 +# pylint: disable=redefined-outer-name +def represent_odict(dump, tag, mapping, # type: ignore + flow_style=None) -> yaml.MappingNode: + """Like BaseRepresenter.represent_mapping but does not issue the sort().""" + value = [] # type: list + node = yaml.MappingNode(tag, value, flow_style=flow_style) + if dump.alias_key is not None: + dump.represented_objects[dump.alias_key] = node + best_style = True + if hasattr(mapping, 'items'): + mapping = mapping.items() + for item_key, item_value in mapping: + node_key = dump.represent_data(item_key) + node_value = dump.represent_data(item_value) + if not (isinstance(node_key, yaml.ScalarNode) and not node_key.style): + best_style = False + if not (isinstance(node_value, yaml.ScalarNode) and + not node_value.style): + best_style = False + value.append((node_key, node_value)) + if flow_style is None: + if dump.default_flow_style is not None: + node.flow_style = dump.default_flow_style + else: + node.flow_style = best_style + return node + + +yaml.SafeDumper.add_representer( + OrderedDict, + lambda dumper, value: + represent_odict(dumper, 'tag:yaml.org,2002:map', value)) + +yaml.SafeDumper.add_representer( + NodeListClass, + lambda dumper, value: + dumper.represent_sequence('tag:yaml.org,2002:seq', value)) diff --git a/homeassistant/util/yaml.py b/homeassistant/util/yaml/loader.py similarity index 83% rename from homeassistant/util/yaml.py rename to homeassistant/util/yaml/loader.py index f6d967b6e5a..7d228490c4c 100644 --- a/homeassistant/util/yaml.py +++ b/homeassistant/util/yaml/loader.py @@ -1,4 +1,4 @@ -"""YAML utility functions.""" +"""Custom loader.""" import logging import os import sys @@ -7,6 +7,7 @@ from collections import OrderedDict from typing import Union, List, Dict, Iterator, overload, TypeVar import yaml + try: import keyring except ImportError: @@ -19,25 +20,23 @@ except ImportError: from homeassistant.exceptions import HomeAssistantError +from .const import _SECRET_NAMESPACE, SECRET_YAML +from .objects import NodeListClass, NodeStrClass + + _LOGGER = logging.getLogger(__name__) -_SECRET_NAMESPACE = 'homeassistant' -SECRET_YAML = 'secrets.yaml' __SECRET_CACHE = {} # type: Dict[str, JSON_TYPE] JSON_TYPE = Union[List, Dict, str] # pylint: disable=invalid-name DICT_T = TypeVar('DICT_T', bound=Dict) # pylint: disable=invalid-name -class NodeListClass(list): - """Wrapper class to be able to add attributes on a list.""" +def clear_secret_cache() -> None: + """Clear the secret cache. - pass - - -class NodeStrClass(str): - """Wrapper class to be able to add attributes on a string.""" - - pass + Async friendly. + """ + __SECRET_CACHE.clear() # pylint: disable=too-many-ancestors @@ -54,6 +53,21 @@ class SafeLineLoader(yaml.SafeLoader): return node +def load_yaml(fname: str) -> JSON_TYPE: + """Load a YAML file.""" + try: + with open(fname, encoding='utf-8') as conf_file: + # If configuration file is empty YAML returns None + # We convert that to an empty dict + return yaml.load(conf_file, Loader=SafeLineLoader) or OrderedDict() + except yaml.YAMLError as exc: + _LOGGER.error(str(exc)) + raise HomeAssistantError(exc) + except UnicodeDecodeError as exc: + _LOGGER.error("Unable to read file %s: %s", fname, exc) + raise HomeAssistantError(exc) + + # pylint: disable=pointless-statement @overload def _add_reference(obj: Union[list, NodeListClass], @@ -86,44 +100,6 @@ def _add_reference(obj, loader: SafeLineLoader, # type: ignore # noqa: F811 return obj -def load_yaml(fname: str) -> JSON_TYPE: - """Load a YAML file.""" - try: - with open(fname, encoding='utf-8') as conf_file: - # If configuration file is empty YAML returns None - # We convert that to an empty dict - return yaml.load(conf_file, Loader=SafeLineLoader) or OrderedDict() - except yaml.YAMLError as exc: - _LOGGER.error(str(exc)) - raise HomeAssistantError(exc) - except UnicodeDecodeError as exc: - _LOGGER.error("Unable to read file %s: %s", fname, exc) - raise HomeAssistantError(exc) - - -def dump(_dict: dict) -> str: - """Dump YAML to a string and remove null.""" - return yaml.safe_dump( - _dict, default_flow_style=False, allow_unicode=True) \ - .replace(': null\n', ':\n') - - -def save_yaml(path: str, data: dict) -> None: - """Save YAML to a file.""" - # Dump before writing to not truncate the file if dumping fails - str_data = dump(data) - with open(path, 'w', encoding='utf-8') as outfile: - outfile.write(str_data) - - -def clear_secret_cache() -> None: - """Clear the secret cache. - - Async friendly. - """ - __SECRET_CACHE.clear() - - def _include_yaml(loader: SafeLineLoader, node: yaml.nodes.Node) -> JSON_TYPE: """Load another YAML file and embeds it using the !include tag. @@ -331,43 +307,3 @@ yaml.SafeLoader.add_constructor('!include_dir_merge_list', yaml.SafeLoader.add_constructor('!include_dir_named', _include_dir_named_yaml) yaml.SafeLoader.add_constructor('!include_dir_merge_named', _include_dir_merge_named_yaml) - - -# From: https://gist.github.com/miracle2k/3184458 -# pylint: disable=redefined-outer-name -def represent_odict(dump, tag, mapping, # type: ignore - flow_style=None) -> yaml.MappingNode: - """Like BaseRepresenter.represent_mapping but does not issue the sort().""" - value = [] # type: list - node = yaml.MappingNode(tag, value, flow_style=flow_style) - if dump.alias_key is not None: - dump.represented_objects[dump.alias_key] = node - best_style = True - if hasattr(mapping, 'items'): - mapping = mapping.items() - for item_key, item_value in mapping: - node_key = dump.represent_data(item_key) - node_value = dump.represent_data(item_value) - if not (isinstance(node_key, yaml.ScalarNode) and not node_key.style): - best_style = False - if not (isinstance(node_value, yaml.ScalarNode) and - not node_value.style): - best_style = False - value.append((node_key, node_value)) - if flow_style is None: - if dump.default_flow_style is not None: - node.flow_style = dump.default_flow_style - else: - node.flow_style = best_style - return node - - -yaml.SafeDumper.add_representer( - OrderedDict, - lambda dumper, value: - represent_odict(dumper, 'tag:yaml.org,2002:map', value)) - -yaml.SafeDumper.add_representer( - NodeListClass, - lambda dumper, value: - dumper.represent_sequence('tag:yaml.org,2002:seq', value)) diff --git a/homeassistant/util/yaml/objects.py b/homeassistant/util/yaml/objects.py new file mode 100644 index 00000000000..183c6c171d6 --- /dev/null +++ b/homeassistant/util/yaml/objects.py @@ -0,0 +1,13 @@ +"""Custom yaml object types.""" + + +class NodeListClass(list): + """Wrapper class to be able to add attributes on a list.""" + + pass + + +class NodeStrClass(str): + """Wrapper class to be able to add attributes on a string.""" + + pass diff --git a/mypy.ini b/mypy.ini index ee893476eed..2599eb079e0 100644 --- a/mypy.ini +++ b/mypy.ini @@ -17,7 +17,11 @@ disallow_untyped_defs = true [mypy-homeassistant.config_entries] disallow_untyped_defs = false -[mypy-homeassistant.util.yaml] +[mypy-homeassistant.util.yaml.dumper] +warn_return_any = false +disallow_untyped_calls = false + +[mypy-homeassistant.util.yaml.loader] warn_return_any = false disallow_untyped_calls = false diff --git a/tests/common.py b/tests/common.py index 46e30187d45..8b28d9db047 100644 --- a/tests/common.py +++ b/tests/common.py @@ -15,7 +15,8 @@ from io import StringIO from unittest.mock import MagicMock, Mock, patch import homeassistant.util.dt as date_util -import homeassistant.util.yaml as yaml +import homeassistant.util.yaml.loader as yaml_loader +import homeassistant.util.yaml.dumper as yaml_dumper from homeassistant import auth, config_entries, core as ha, loader from homeassistant.auth import ( @@ -680,7 +681,8 @@ def patch_yaml_files(files_dict, endswith=True): # Not found raise FileNotFoundError("File not found: {}".format(fname)) - return patch.object(yaml, 'open', mock_open_f, create=True) + return patch.object(yaml_loader, 'open', mock_open_f, create=True) + return patch.object(yaml_dumper, 'open', mock_open_f, create=True) def mock_coro(return_value=None, exception=None): diff --git a/tests/components/scene/test_init.py b/tests/components/scene/test_init.py index 940999c2dbe..99364d51e6c 100644 --- a/tests/components/scene/test_init.py +++ b/tests/components/scene/test_init.py @@ -4,7 +4,7 @@ import unittest from homeassistant.setup import setup_component from homeassistant.components import light, scene -from homeassistant.util import yaml +from homeassistant.util.yaml import loader as yaml_loader from tests.common import get_test_home_assistant from tests.components.light import common as common_light @@ -90,7 +90,7 @@ class TestScene(unittest.TestCase): self.light_1.entity_id, self.light_2.entity_id) with io.StringIO(config) as file: - doc = yaml.yaml.safe_load(file) + doc = yaml_loader.yaml.load(file) assert setup_component(self.hass, scene.DOMAIN, doc) common.activate(self.hass, 'scene.test') diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index 624adbb8ea3..3af9394a202 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -11,7 +11,7 @@ from homeassistant.helpers import entity_registry from tests.common import mock_registry, flush_store -YAML__OPEN_PATH = 'homeassistant.util.yaml.open' +YAML__OPEN_PATH = 'homeassistant.util.yaml.loader.open' @pytest.fixture diff --git a/tests/util/test_yaml.py b/tests/util/test_yaml.py index c7d1be3d58c..01a64f17b86 100644 --- a/tests/util/test_yaml.py +++ b/tests/util/test_yaml.py @@ -8,7 +8,8 @@ from unittest.mock import patch import pytest from homeassistant.exceptions import HomeAssistantError -from homeassistant.util import yaml +from homeassistant.util.yaml import loader as yaml_loader +import homeassistant.util.yaml as yaml from homeassistant.config import YAML_CONFIG_FILE, load_yaml_config_file from tests.common import get_test_config_dir, patch_yaml_files @@ -16,7 +17,7 @@ from tests.common import get_test_config_dir, patch_yaml_files @pytest.fixture(autouse=True) def mock_credstash(): """Mock credstash so it doesn't connect to the internet.""" - with patch.object(yaml, 'credstash') as mock_credstash: + with patch.object(yaml_loader, 'credstash') as mock_credstash: mock_credstash.getSecret.return_value = None yield mock_credstash @@ -25,7 +26,7 @@ def test_simple_list(): """Test simple list.""" conf = "config:\n - simple\n - list" with io.StringIO(conf) as file: - doc = yaml.yaml.safe_load(file) + doc = yaml_loader.yaml.safe_load(file) assert doc['config'] == ["simple", "list"] @@ -33,7 +34,7 @@ def test_simple_dict(): """Test simple dict.""" conf = "key: value" with io.StringIO(conf) as file: - doc = yaml.yaml.safe_load(file) + doc = yaml_loader.yaml.safe_load(file) assert doc['key'] == 'value' @@ -58,7 +59,7 @@ def test_environment_variable(): os.environ["PASSWORD"] = "secret_password" conf = "password: !env_var PASSWORD" with io.StringIO(conf) as file: - doc = yaml.yaml.safe_load(file) + doc = yaml_loader.yaml.safe_load(file) assert doc['password'] == "secret_password" del os.environ["PASSWORD"] @@ -67,7 +68,7 @@ def test_environment_variable_default(): """Test config file with default value for environment variable.""" conf = "password: !env_var PASSWORD secret_password" with io.StringIO(conf) as file: - doc = yaml.yaml.safe_load(file) + doc = yaml_loader.yaml.safe_load(file) assert doc['password'] == "secret_password" @@ -76,7 +77,7 @@ def test_invalid_environment_variable(): conf = "password: !env_var PASSWORD" with pytest.raises(HomeAssistantError): with io.StringIO(conf) as file: - yaml.yaml.safe_load(file) + yaml_loader.yaml.safe_load(file) def test_include_yaml(): @@ -84,17 +85,17 @@ def test_include_yaml(): with patch_yaml_files({'test.yaml': 'value'}): conf = 'key: !include test.yaml' with io.StringIO(conf) as file: - doc = yaml.yaml.safe_load(file) + doc = yaml_loader.yaml.safe_load(file) assert doc["key"] == "value" with patch_yaml_files({'test.yaml': None}): conf = 'key: !include test.yaml' with io.StringIO(conf) as file: - doc = yaml.yaml.safe_load(file) + doc = yaml_loader.yaml.safe_load(file) assert doc["key"] == {} -@patch('homeassistant.util.yaml.os.walk') +@patch('homeassistant.util.yaml.loader.os.walk') def test_include_dir_list(mock_walk): """Test include dir list yaml.""" mock_walk.return_value = [ @@ -107,11 +108,11 @@ def test_include_dir_list(mock_walk): }): conf = "key: !include_dir_list /tmp" with io.StringIO(conf) as file: - doc = yaml.yaml.safe_load(file) + doc = yaml_loader.yaml.safe_load(file) assert doc["key"] == sorted(["one", "two"]) -@patch('homeassistant.util.yaml.os.walk') +@patch('homeassistant.util.yaml.loader.os.walk') def test_include_dir_list_recursive(mock_walk): """Test include dir recursive list yaml.""" mock_walk.return_value = [ @@ -129,13 +130,13 @@ def test_include_dir_list_recursive(mock_walk): with io.StringIO(conf) as file: assert '.ignore' in mock_walk.return_value[0][1], \ "Expecting .ignore in here" - doc = yaml.yaml.safe_load(file) + doc = yaml_loader.yaml.safe_load(file) assert 'tmp2' in mock_walk.return_value[0][1] assert '.ignore' not in mock_walk.return_value[0][1] assert sorted(doc["key"]) == sorted(["zero", "one", "two"]) -@patch('homeassistant.util.yaml.os.walk') +@patch('homeassistant.util.yaml.loader.os.walk') def test_include_dir_named(mock_walk): """Test include dir named yaml.""" mock_walk.return_value = [ @@ -149,11 +150,11 @@ def test_include_dir_named(mock_walk): conf = "key: !include_dir_named /tmp" correct = {'first': 'one', 'second': 'two'} with io.StringIO(conf) as file: - doc = yaml.yaml.safe_load(file) + doc = yaml_loader.yaml.safe_load(file) assert doc["key"] == correct -@patch('homeassistant.util.yaml.os.walk') +@patch('homeassistant.util.yaml.loader.os.walk') def test_include_dir_named_recursive(mock_walk): """Test include dir named yaml.""" mock_walk.return_value = [ @@ -172,13 +173,13 @@ def test_include_dir_named_recursive(mock_walk): with io.StringIO(conf) as file: assert '.ignore' in mock_walk.return_value[0][1], \ "Expecting .ignore in here" - doc = yaml.yaml.safe_load(file) + doc = yaml_loader.yaml.safe_load(file) assert 'tmp2' in mock_walk.return_value[0][1] assert '.ignore' not in mock_walk.return_value[0][1] assert doc["key"] == correct -@patch('homeassistant.util.yaml.os.walk') +@patch('homeassistant.util.yaml.loader.os.walk') def test_include_dir_merge_list(mock_walk): """Test include dir merge list yaml.""" mock_walk.return_value = [['/tmp', [], ['first.yaml', 'second.yaml']]] @@ -189,11 +190,11 @@ def test_include_dir_merge_list(mock_walk): }): conf = "key: !include_dir_merge_list /tmp" with io.StringIO(conf) as file: - doc = yaml.yaml.safe_load(file) + doc = yaml_loader.yaml.safe_load(file) assert sorted(doc["key"]) == sorted(["one", "two", "three"]) -@patch('homeassistant.util.yaml.os.walk') +@patch('homeassistant.util.yaml.loader.os.walk') def test_include_dir_merge_list_recursive(mock_walk): """Test include dir merge list yaml.""" mock_walk.return_value = [ @@ -211,14 +212,14 @@ def test_include_dir_merge_list_recursive(mock_walk): with io.StringIO(conf) as file: assert '.ignore' in mock_walk.return_value[0][1], \ "Expecting .ignore in here" - doc = yaml.yaml.safe_load(file) + doc = yaml_loader.yaml.safe_load(file) assert 'tmp2' in mock_walk.return_value[0][1] assert '.ignore' not in mock_walk.return_value[0][1] assert sorted(doc["key"]) == sorted(["one", "two", "three", "four"]) -@patch('homeassistant.util.yaml.os.walk') +@patch('homeassistant.util.yaml.loader.os.walk') def test_include_dir_merge_named(mock_walk): """Test include dir merge named yaml.""" mock_walk.return_value = [['/tmp', [], ['first.yaml', 'second.yaml']]] @@ -231,7 +232,7 @@ def test_include_dir_merge_named(mock_walk): with patch_yaml_files(files): conf = "key: !include_dir_merge_named /tmp" with io.StringIO(conf) as file: - doc = yaml.yaml.safe_load(file) + doc = yaml_loader.yaml.safe_load(file) assert doc["key"] == { "key1": "one", "key2": "two", @@ -239,7 +240,7 @@ def test_include_dir_merge_named(mock_walk): } -@patch('homeassistant.util.yaml.os.walk') +@patch('homeassistant.util.yaml.loader.os.walk') def test_include_dir_merge_named_recursive(mock_walk): """Test include dir merge named yaml.""" mock_walk.return_value = [ @@ -257,7 +258,7 @@ def test_include_dir_merge_named_recursive(mock_walk): with io.StringIO(conf) as file: assert '.ignore' in mock_walk.return_value[0][1], \ "Expecting .ignore in here" - doc = yaml.yaml.safe_load(file) + doc = yaml_loader.yaml.safe_load(file) assert 'tmp2' in mock_walk.return_value[0][1] assert '.ignore' not in mock_walk.return_value[0][1] assert doc["key"] == { @@ -268,12 +269,12 @@ def test_include_dir_merge_named_recursive(mock_walk): } -@patch('homeassistant.util.yaml.open', create=True) +@patch('homeassistant.util.yaml.loader.open', create=True) def test_load_yaml_encoding_error(mock_open): """Test raising a UnicodeDecodeError.""" mock_open.side_effect = UnicodeDecodeError('', b'', 1, 0, '') with pytest.raises(HomeAssistantError): - yaml.load_yaml('test') + yaml_loader.load_yaml('test') def test_dump(): @@ -392,16 +393,16 @@ class TestSecrets(unittest.TestCase): def test_secrets_keyring(self): """Test keyring fallback & get_password.""" - yaml.keyring = None # Ensure its not there + yaml_loader.keyring = None # Ensure its not there yaml_str = 'http:\n api_password: !secret http_pw_keyring' - with pytest.raises(yaml.HomeAssistantError): + with pytest.raises(HomeAssistantError): load_yaml(self._yaml_path, yaml_str) - yaml.keyring = FakeKeyring({'http_pw_keyring': 'yeah'}) + yaml_loader.keyring = FakeKeyring({'http_pw_keyring': 'yeah'}) _yaml = load_yaml(self._yaml_path, yaml_str) assert {'http': {'api_password': 'yeah'}} == _yaml - @patch.object(yaml, 'credstash') + @patch.object(yaml_loader, 'credstash') def test_secrets_credstash(self, mock_credstash): """Test credstash fallback & get_password.""" mock_credstash.getSecret.return_value = 'yeah' @@ -413,10 +414,10 @@ class TestSecrets(unittest.TestCase): def test_secrets_logger_removed(self): """Ensure logger: debug was removed.""" - with pytest.raises(yaml.HomeAssistantError): + with pytest.raises(HomeAssistantError): load_yaml(self._yaml_path, 'api_password: !secret logger') - @patch('homeassistant.util.yaml._LOGGER.error') + @patch('homeassistant.util.yaml.loader._LOGGER.error') def test_bad_logger_value(self, mock_error): """Ensure logger: debug was removed.""" yaml.clear_secret_cache() From 45adb5c9c7019bce5e08cbd72b705201b7d6841b Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 9 May 2019 10:11:51 -0600 Subject: [PATCH 005/232] Add config entry for IQVIA (#23765) * Add config entry for IQVIA * Updated tests and requirements * Removed unnecessary dependency * Fixed tests * Reverted unintended change --- .../components/iqvia/.translations/en.json | 18 ++++ homeassistant/components/iqvia/__init__.py | 60 +++++++++--- homeassistant/components/iqvia/config_flow.py | 65 +++++++++++++ homeassistant/components/iqvia/const.py | 2 + homeassistant/components/iqvia/sensor.py | 9 +- homeassistant/components/iqvia/strings.json | 18 ++++ homeassistant/config_entries.py | 1 + requirements_test_all.txt | 3 + script/gen_requirements_all.py | 1 + tests/components/iqvia/__init__.py | 1 + tests/components/iqvia/test_config_flow.py | 97 +++++++++++++++++++ 11 files changed, 258 insertions(+), 17 deletions(-) create mode 100644 homeassistant/components/iqvia/.translations/en.json create mode 100644 homeassistant/components/iqvia/config_flow.py create mode 100644 homeassistant/components/iqvia/strings.json create mode 100644 tests/components/iqvia/__init__.py create mode 100644 tests/components/iqvia/test_config_flow.py diff --git a/homeassistant/components/iqvia/.translations/en.json b/homeassistant/components/iqvia/.translations/en.json new file mode 100644 index 00000000000..c3cc412d792 --- /dev/null +++ b/homeassistant/components/iqvia/.translations/en.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "identifier_exists": "ZIP code already registered", + "invalid_zip_code": "ZIP code is invalid" + }, + "step": { + "user": { + "data": { + "zip_code": "ZIP Code" + }, + "description": "Fill out your U.S. or Canadian ZIP code.", + "title": "IQVIA" + } + }, + "title": "IQVIA" + } +} \ No newline at end of file diff --git a/homeassistant/components/iqvia/__init__.py b/homeassistant/components/iqvia/__init__.py index 23803d7f17d..cf8f92c1bd2 100644 --- a/homeassistant/components/iqvia/__init__.py +++ b/homeassistant/components/iqvia/__init__.py @@ -8,28 +8,27 @@ from pyiqvia.errors import IQVIAError, InvalidZipError import voluptuous as vol +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ATTR_ATTRIBUTION, CONF_MONITORED_CONDITIONS from homeassistant.core import callback from homeassistant.helpers import aiohttp_client, config_validation as cv -from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send) from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval from homeassistant.util.decorator import Registry +from .config_flow import configured_instances from .const import ( - DATA_CLIENT, DATA_LISTENER, DOMAIN, SENSORS, TOPIC_DATA_UPDATE, - TYPE_ALLERGY_FORECAST, TYPE_ALLERGY_INDEX, TYPE_ALLERGY_OUTLOOK, - TYPE_ALLERGY_TODAY, TYPE_ALLERGY_TOMORROW, TYPE_ASTHMA_FORECAST, - TYPE_ASTHMA_INDEX, TYPE_ASTHMA_TODAY, TYPE_ASTHMA_TOMORROW, - TYPE_DISEASE_FORECAST, TYPE_DISEASE_INDEX, TYPE_DISEASE_TODAY) + CONF_ZIP_CODE, DATA_CLIENT, DATA_LISTENER, DOMAIN, SENSORS, + TOPIC_DATA_UPDATE, TYPE_ALLERGY_FORECAST, TYPE_ALLERGY_INDEX, + TYPE_ALLERGY_OUTLOOK, TYPE_ALLERGY_TODAY, TYPE_ALLERGY_TOMORROW, + TYPE_ASTHMA_FORECAST, TYPE_ASTHMA_INDEX, TYPE_ASTHMA_TODAY, + TYPE_ASTHMA_TOMORROW, TYPE_DISEASE_FORECAST, TYPE_DISEASE_INDEX, + TYPE_DISEASE_TODAY) _LOGGER = logging.getLogger(__name__) - -CONF_ZIP_CODE = 'zip_code' - DATA_CONFIG = 'config' DEFAULT_ATTRIBUTION = 'Data provided by IQVIA™' @@ -59,23 +58,39 @@ async def async_setup(hass, config): hass.data[DOMAIN][DATA_CLIENT] = {} hass.data[DOMAIN][DATA_LISTENER] = {} + if DOMAIN not in config: + return True + conf = config[DOMAIN] + if conf[CONF_ZIP_CODE] in configured_instances(hass): + return True + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={'source': SOURCE_IMPORT}, data=conf)) + + return True + + +async def async_setup_entry(hass, config_entry): + """Set up IQVIA as config entry.""" websession = aiohttp_client.async_get_clientsession(hass) try: iqvia = IQVIAData( - Client(conf[CONF_ZIP_CODE], websession), - conf[CONF_MONITORED_CONDITIONS]) + Client(config_entry.data[CONF_ZIP_CODE], websession), + config_entry.data.get(CONF_MONITORED_CONDITIONS, list(SENSORS))) await iqvia.async_update() except IQVIAError as err: _LOGGER.error('Unable to set up IQVIA: %s', err) return False - hass.data[DOMAIN][DATA_CLIENT] = iqvia + hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = iqvia hass.async_create_task( - async_load_platform(hass, 'sensor', DOMAIN, {}, config)) + hass.config_entries.async_forward_entry_setup( + config_entry, 'sensor')) async def refresh(event_time): """Refresh IQVIA data.""" @@ -83,8 +98,23 @@ async def async_setup(hass, config): await iqvia.async_update() async_dispatcher_send(hass, TOPIC_DATA_UPDATE) - hass.data[DOMAIN][DATA_LISTENER] = async_track_time_interval( - hass, refresh, DEFAULT_SCAN_INTERVAL) + hass.data[DOMAIN][DATA_LISTENER][ + config_entry.entry_id] = async_track_time_interval( + hass, refresh, DEFAULT_SCAN_INTERVAL) + + return True + + +async def async_unload_entry(hass, config_entry): + """Unload an OpenUV config entry.""" + hass.data[DOMAIN][DATA_CLIENT].pop(config_entry.entry_id) + + remove_listener = hass.data[DOMAIN][DATA_LISTENER].pop( + config_entry.entry_id) + remove_listener() + + await hass.config_entries.async_forward_entry_unload( + config_entry, 'sensor') return True diff --git a/homeassistant/components/iqvia/config_flow.py b/homeassistant/components/iqvia/config_flow.py new file mode 100644 index 00000000000..d9a8c693670 --- /dev/null +++ b/homeassistant/components/iqvia/config_flow.py @@ -0,0 +1,65 @@ +"""Config flow to configure the IQVIA component.""" + +from collections import OrderedDict +import voluptuous as vol + +from pyiqvia import Client +from pyiqvia.errors import IQVIAError + +from homeassistant import config_entries +from homeassistant.core import callback +from homeassistant.helpers import aiohttp_client + +from .const import CONF_ZIP_CODE, DOMAIN + + +@callback +def configured_instances(hass): + """Return a set of configured IQVIA instances.""" + return set( + entry.data[CONF_ZIP_CODE] + for entry in hass.config_entries.async_entries(DOMAIN)) + + +@config_entries.HANDLERS.register(DOMAIN) +class IQVIAFlowHandler(config_entries.ConfigFlow): + """Handle an IQVIA config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + def __init__(self): + """Initialize the config flow.""" + self.data_schema = OrderedDict() + self.data_schema[vol.Required(CONF_ZIP_CODE)] = str + + async def _show_form(self, errors=None): + """Show the form to the user.""" + return self.async_show_form( + step_id='user', + data_schema=vol.Schema(self.data_schema), + errors=errors if errors else {}, + ) + + async def async_step_import(self, import_config): + """Import a config entry from configuration.yaml.""" + return await self.async_step_user(import_config) + + async def async_step_user(self, user_input=None): + """Handle the start of the config flow.""" + if not user_input: + return await self._show_form() + + if user_input[CONF_ZIP_CODE] in configured_instances(self.hass): + return await self._show_form({CONF_ZIP_CODE: 'identifier_exists'}) + + websession = aiohttp_client.async_get_clientsession(self.hass) + client = Client(user_input[CONF_ZIP_CODE], websession) + + try: + await client.allergens.current() + except IQVIAError: + return await self._show_form({CONF_ZIP_CODE: 'invalid_zip_code'}) + + return self.async_create_entry( + title=user_input[CONF_ZIP_CODE], data=user_input) diff --git a/homeassistant/components/iqvia/const.py b/homeassistant/components/iqvia/const.py index 025fa8a9505..e9bffabcc43 100644 --- a/homeassistant/components/iqvia/const.py +++ b/homeassistant/components/iqvia/const.py @@ -1,6 +1,8 @@ """Define IQVIA constants.""" DOMAIN = 'iqvia' +CONF_ZIP_CODE = 'zip_code' + DATA_CLIENT = 'client' DATA_LISTENER = 'listener' diff --git a/homeassistant/components/iqvia/sensor.py b/homeassistant/components/iqvia/sensor.py index b0b09c3f977..5128b997b35 100644 --- a/homeassistant/components/iqvia/sensor.py +++ b/homeassistant/components/iqvia/sensor.py @@ -54,8 +54,13 @@ TREND_SUBSIDING = 'Subsiding' async def async_setup_platform( hass, config, async_add_entities, discovery_info=None): - """Configure the platform and add the sensors.""" - iqvia = hass.data[DOMAIN][DATA_CLIENT] + """Set up IQVIA sensors based on the old way.""" + pass + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up IQVIA sensors based on a config entry.""" + iqvia = hass.data[DOMAIN][DATA_CLIENT][entry.entry_id] sensor_class_mapping = { TYPE_ALLERGY_FORECAST: ForecastSensor, diff --git a/homeassistant/components/iqvia/strings.json b/homeassistant/components/iqvia/strings.json new file mode 100644 index 00000000000..00f383be502 --- /dev/null +++ b/homeassistant/components/iqvia/strings.json @@ -0,0 +1,18 @@ +{ + "config": { + "title": "IQVIA", + "step": { + "user": { + "title": "IQVIA", + "description": "Fill out your U.S. or Canadian ZIP code.", + "data": { + "zip_code": "ZIP Code" + } + } + }, + "error": { + "identifier_exists": "ZIP code already registered", + "invalid_zip_code": "ZIP code is invalid" + } + } +} diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index a2b34a00efd..593b402a3fd 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -160,6 +160,7 @@ FLOWS = [ 'ifttt', 'ios', 'ipma', + 'iqvia', 'lifx', 'locative', 'logi_circle', diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fbf5a701072..5e14fa57221 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -234,6 +234,9 @@ pyheos==0.5.2 # homeassistant.components.homematic pyhomematic==0.1.58 +# homeassistant.components.iqvia +pyiqvia==0.2.0 + # homeassistant.components.litejet pylitejet==0.1 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 14303bd6d65..057f5c9fd24 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -103,6 +103,7 @@ TEST_REQUIREMENTS = ( 'pydispatcher', 'pyheos', 'pyhomematic', + 'pyiqvia', 'pylitejet', 'pymonoprice', 'pynx584', diff --git a/tests/components/iqvia/__init__.py b/tests/components/iqvia/__init__.py new file mode 100644 index 00000000000..a4a57b8aafa --- /dev/null +++ b/tests/components/iqvia/__init__.py @@ -0,0 +1 @@ +"""Define tests for IQVIA.""" diff --git a/tests/components/iqvia/test_config_flow.py b/tests/components/iqvia/test_config_flow.py new file mode 100644 index 00000000000..97ab4014291 --- /dev/null +++ b/tests/components/iqvia/test_config_flow.py @@ -0,0 +1,97 @@ +"""Define tests for the IQVIA config flow.""" +from pyiqvia.errors import IQVIAError +import pytest + +from homeassistant import data_entry_flow +from homeassistant.components.iqvia import CONF_ZIP_CODE, DOMAIN, config_flow + +from tests.common import MockConfigEntry, MockDependency, mock_coro + + +@pytest.fixture +def allergens_current_response(): + """Define a fixture for a successful allergens.current response.""" + return mock_coro() + + +@pytest.fixture +def mock_pyiqvia(allergens_current_response): + """Mock the pyiqvia library.""" + with MockDependency('pyiqvia') as mock_pyiqvia_: + mock_pyiqvia_.Client().allergens.current.return_value = ( + allergens_current_response) + yield mock_pyiqvia_ + + +async def test_duplicate_error(hass): + """Test that errors are shown when duplicates are added.""" + conf = { + CONF_ZIP_CODE: '12345', + } + + MockConfigEntry(domain=DOMAIN, data=conf).add_to_hass(hass) + flow = config_flow.IQVIAFlowHandler() + flow.hass = hass + + result = await flow.async_step_user(user_input=conf) + assert result['errors'] == {CONF_ZIP_CODE: 'identifier_exists'} + + +@pytest.mark.parametrize( + 'allergens_current_response', [mock_coro(exception=IQVIAError)]) +async def test_invalid_zip_code(hass, mock_pyiqvia): + """Test that an invalid ZIP code key throws an error.""" + conf = { + CONF_ZIP_CODE: 'abcde', + } + + flow = config_flow.IQVIAFlowHandler() + flow.hass = hass + + result = await flow.async_step_user(user_input=conf) + assert result['errors'] == {CONF_ZIP_CODE: 'invalid_zip_code'} + + +async def test_show_form(hass): + """Test that the form is served with no input.""" + flow = config_flow.IQVIAFlowHandler() + flow.hass = hass + + result = await flow.async_step_user(user_input=None) + + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM + assert result['step_id'] == 'user' + + +async def test_step_import(hass, mock_pyiqvia): + """Test that the import step works.""" + conf = { + CONF_ZIP_CODE: '12345', + } + + flow = config_flow.IQVIAFlowHandler() + flow.hass = hass + + result = await flow.async_step_import(import_config=conf) + assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result['title'] == '12345' + assert result['data'] == { + CONF_ZIP_CODE: '12345', + } + + +async def test_step_user(hass, mock_pyiqvia): + """Test that the user step works.""" + conf = { + CONF_ZIP_CODE: '12345', + } + + flow = config_flow.IQVIAFlowHandler() + flow.hass = hass + + result = await flow.async_step_user(user_input=conf) + assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result['title'] == '12345' + assert result['data'] == { + CONF_ZIP_CODE: '12345', + } From c7a78ed522542c30fae797b292d0c9707de40460 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Thu, 9 May 2019 19:18:22 +0200 Subject: [PATCH 006/232] Add stepped volume to demo (#23759) * Add stepped volume to demo * Simplify somewhat to avoid extra check --- homeassistant/components/demo/media_player.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/demo/media_player.py b/homeassistant/components/demo/media_player.py index 5a97b43af86..e293632b71e 100644 --- a/homeassistant/components/demo/media_player.py +++ b/homeassistant/components/demo/media_player.py @@ -5,7 +5,8 @@ from homeassistant.components.media_player.const import ( SUPPORT_CLEAR_PLAYLIST, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, SUPPORT_SELECT_SOUND_MODE, SUPPORT_SELECT_SOURCE, SUPPORT_SHUFFLE_SET, - SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET) + SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, + SUPPORT_VOLUME_STEP) from homeassistant.const import STATE_OFF, STATE_PAUSED, STATE_PLAYING import homeassistant.util.dt as dt_util @@ -35,7 +36,7 @@ YOUTUBE_PLAYER_SUPPORT = \ MUSIC_PLAYER_SUPPORT = \ SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_CLEAR_PLAYLIST | \ - SUPPORT_PLAY | SUPPORT_SHUFFLE_SET | \ + SUPPORT_PLAY | SUPPORT_SHUFFLE_SET | SUPPORT_VOLUME_STEP | \ SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \ SUPPORT_SELECT_SOUND_MODE @@ -122,6 +123,16 @@ class AbstractDemoPlayer(MediaPlayerDevice): self._volume_muted = mute self.schedule_update_ha_state() + def volume_up(self): + """Increase volume.""" + self._volume_level = min(1.0, self._volume_level + 0.1) + self.schedule_update_ha_state() + + def volume_down(self): + """Decrease volume.""" + self._volume_level = max(0.0, self._volume_level - 0.1) + self.schedule_update_ha_state() + def set_volume_level(self, volume): """Set the volume level, range 0..1.""" self._volume_level = volume From 8ef3c6d4d346a0fcaed2d72299597ed8aa1b8594 Mon Sep 17 00:00:00 2001 From: sander76 Date: Thu, 9 May 2019 19:38:51 +0200 Subject: [PATCH 007/232] Add battery binary sensor to homematic (#23067) * first proposal * parameter rename * retrigger CI * remove separate binary sensor * remove batter_sensor * battery device distinction at binary sensor discovery --- .../components/homematic/__init__.py | 27 ++++++++++--- .../components/homematic/binary_sensor.py | 38 ++++++++++++++++++- 2 files changed, 58 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index 578fae064f8..7f6f9a6d522 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -27,8 +27,10 @@ DISCOVER_BINARY_SENSORS = 'homematic.binary_sensor' DISCOVER_COVER = 'homematic.cover' DISCOVER_CLIMATE = 'homematic.climate' DISCOVER_LOCKS = 'homematic.locks' +DISCOVER_BUTTONS = 'homematic.binary_sensor' ATTR_DISCOVER_DEVICES = 'devices' +ATTR_BATTERY_DEVICES = 'battery_devices' ATTR_PARAM = 'param' ATTR_CHANNEL = 'channel' ATTR_ADDRESS = 'address' @@ -42,6 +44,7 @@ ATTR_UNIQUE_ID = 'unique_id' ATTR_PARAMSET_KEY = 'paramset_key' ATTR_PARAMSET = 'paramset' + EVENT_KEYPRESS = 'homematic.keypress' EVENT_IMPULSE = 'homematic.impulse' EVENT_ERROR = 'homematic.error' @@ -84,7 +87,8 @@ HM_DEVICE_TYPES = { 'SmartwareMotion', 'IPWeatherSensorPlus', 'MotionIPV2', 'WaterIP', 'IPMultiIO', 'TiltIP', 'IPShutterContactSabotage'], DISCOVER_COVER: ['Blind', 'KeyBlind', 'IPKeyBlind', 'IPKeyBlindTilt'], - DISCOVER_LOCKS: ['KeyMatic'] + DISCOVER_LOCKS: ['KeyMatic'], + DISCOVER_BUTTONS: ['HmIP-WRC6', 'HmIP-RC8'] } HM_IGNORE_DISCOVERY_NODE = [ @@ -460,7 +464,8 @@ def _system_callback_handler(hass, config, src, *args): ('binary_sensor', DISCOVER_BINARY_SENSORS), ('sensor', DISCOVER_SENSORS), ('climate', DISCOVER_CLIMATE), - ('lock', DISCOVER_LOCKS)): + ('lock', DISCOVER_LOCKS), + ('binary_sensor', DISCOVER_SWITCHES)): # Get all devices of a specific type found_devices = _get_devices( hass, discovery_type, addresses, interface) @@ -468,9 +473,21 @@ def _system_callback_handler(hass, config, src, *args): # When devices of this type are found # they are setup in HASS and a discovery event is fired if found_devices: - discovery.load_platform(hass, component_name, DOMAIN, { - ATTR_DISCOVER_DEVICES: found_devices - }, config) + discovery_info = {ATTR_DISCOVER_DEVICES: found_devices, + ATTR_BATTERY_DEVICES: False} + + # Switches are skipped as a component. They will only + # appear in hass as a battery device. + if not discovery_type == DISCOVER_SWITCHES: + discovery.load_platform(hass, component_name, DOMAIN, + discovery_info, config) + + # Pass all devices to binary sensor discovery, + # check whether they are battery operated and + # add them as a battery operated binary sensor device. + discovery_info[ATTR_BATTERY_DEVICES] = True + discovery.load_platform(hass, 'binary_sensor', DOMAIN, + discovery_info, config) # Homegear error message elif src == 'error': diff --git a/homeassistant/components/homematic/binary_sensor.py b/homeassistant/components/homematic/binary_sensor.py index dfd7b7a72bd..91960cd8570 100644 --- a/homeassistant/components/homematic/binary_sensor.py +++ b/homeassistant/components/homematic/binary_sensor.py @@ -2,12 +2,16 @@ import logging from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.const import STATE_UNKNOWN +from homeassistant.components.homematic import ATTR_BATTERY_DEVICES +from homeassistant.const import STATE_UNKNOWN, DEVICE_CLASS_BATTERY from . import ATTR_DISCOVER_DEVICES, HMDevice _LOGGER = logging.getLogger(__name__) +ATTR_LOW_BAT = 'LOW_BAT' +ATTR_LOWBAT = 'LOWBAT' + SENSOR_TYPES_CLASS = { 'IPShutterContact': 'opening', 'MaxShutterContact': 'opening', @@ -30,8 +34,15 @@ def setup_platform(hass, config, add_entities, discovery_info=None): return devices = [] + battery_devices = discovery_info[ATTR_BATTERY_DEVICES] + for conf in discovery_info[ATTR_DISCOVER_DEVICES]: - new_device = HMBinarySensor(conf) + if battery_devices: + battery_device = conf.get(ATTR_LOWBAT) or conf.get(ATTR_LOW_BAT) + if battery_device: + new_device = HMBatterySensor(conf) + else: + new_device = HMBinarySensor(conf) devices.append(new_device) add_entities(devices) @@ -60,3 +71,26 @@ class HMBinarySensor(HMDevice, BinarySensorDevice): # Add state to data struct if self._state: self._data.update({self._state: STATE_UNKNOWN}) + + +class HMBatterySensor(HMDevice, BinarySensorDevice): + """Representation of an HomeMatic low battery sensor.""" + + @property + def device_class(self): + """Return battery as a device class.""" + return DEVICE_CLASS_BATTERY + + @property + def is_on(self): + """Return True if battery is low.""" + is_on = self._data.get(ATTR_LOW_BAT, False) or self._data.get( + ATTR_LOWBAT, False + ) + return is_on + + def _init_data_struct(self): + """Generate the data dictionary (self._data) from metadata.""" + # Add state to data struct + if self._state: + self._data.update({self._state: STATE_UNKNOWN}) From f21f32778f75b61e56c4263fad7b928c39ccb311 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 9 May 2019 15:49:12 -0700 Subject: [PATCH 008/232] Updated frontend to 20190509.0 --- homeassistant/components/frontend/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 44d42955269..559469c63ac 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/components/frontend", "requirements": [ - "home-assistant-frontend==20190508.0" + "home-assistant-frontend==20190509.0" ], "dependencies": [ "api", diff --git a/requirements_all.txt b/requirements_all.txt index db555dab405..d61b394c7c4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -563,7 +563,7 @@ hole==0.3.0 holidays==0.9.10 # homeassistant.components.frontend -home-assistant-frontend==20190508.0 +home-assistant-frontend==20190509.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5e14fa57221..ec4589c4403 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -145,7 +145,7 @@ hdate==0.8.7 holidays==0.9.10 # homeassistant.components.frontend -home-assistant-frontend==20190508.0 +home-assistant-frontend==20190509.0 # homeassistant.components.homekit_controller homekit[IP]==0.14.0 From 068749bcbe70317005a00edcadebdcb4425c8912 Mon Sep 17 00:00:00 2001 From: Pawel Date: Fri, 10 May 2019 01:13:13 +0200 Subject: [PATCH 009/232] fix two times creating JWT headers. (#23777) --- homeassistant/components/html5/notify.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/homeassistant/components/html5/notify.py b/homeassistant/components/html5/notify.py index 5ab2b39baed..c8cd207da3e 100644 --- a/homeassistant/components/html5/notify.py +++ b/homeassistant/components/html5/notify.py @@ -484,15 +484,6 @@ class HTML5NotificationService(BaseNotificationService): payload[ATTR_DATA][ATTR_JWT] = add_jwt( timestamp, target, payload[ATTR_TAG], info[ATTR_SUBSCRIPTION][ATTR_KEYS][ATTR_AUTH]) - import jwt - jwt_secret = info[ATTR_SUBSCRIPTION][ATTR_KEYS][ATTR_AUTH] - jwt_exp = (datetime.fromtimestamp(timestamp) + - timedelta(days=JWT_VALID_DAYS)) - jwt_claims = {'exp': jwt_exp, 'nbf': timestamp, - 'iat': timestamp, ATTR_TARGET: target, - ATTR_TAG: payload[ATTR_TAG]} - jwt_token = jwt.encode(jwt_claims, jwt_secret).decode('utf-8') - payload[ATTR_DATA][ATTR_JWT] = jwt_token webpusher = WebPusher(info[ATTR_SUBSCRIPTION]) if self._vapid_prv and self._vapid_email: vapid_headers = create_vapid_headers( From 05960fa29c9c1059a05e37e8955823a3cf88df76 Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Fri, 10 May 2019 01:17:47 +0200 Subject: [PATCH 010/232] Sort discovered entries by 'st' to ensure getting the same device each discovery (#23763) --- homeassistant/components/upnp/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/upnp/__init__.py b/homeassistant/components/upnp/__init__.py index 01f6d6159f0..fd2aa994ca4 100644 --- a/homeassistant/components/upnp/__init__.py +++ b/homeassistant/components/upnp/__init__.py @@ -1,5 +1,6 @@ """Open ports in your router for Home Assistant and provide statistics.""" from ipaddress import ip_address +from operator import itemgetter import voluptuous as vol @@ -88,6 +89,8 @@ async def async_discover_and_construct(hass, udn=None) -> Device: _LOGGER.warning('Wanted UPnP/IGD device with UDN "%s" not found, ' 'aborting', udn) return None + # ensure we're always taking the latest + filtered = sorted(filtered, key=itemgetter('st'), reverse=True) discovery_info = filtered[0] else: # get the first/any From df6846344d704ce20761d3fbfd2061c165dfc9fd Mon Sep 17 00:00:00 2001 From: Jason Hunter Date: Thu, 9 May 2019 23:17:56 -0400 Subject: [PATCH 011/232] Beta Fix: ONVIF (#23787) * bump package to include wsdl * update requirements all --- homeassistant/components/onvif/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/onvif/manifest.json b/homeassistant/components/onvif/manifest.json index 177a8e1eabd..f36a5cf4b1a 100644 --- a/homeassistant/components/onvif/manifest.json +++ b/homeassistant/components/onvif/manifest.json @@ -3,7 +3,7 @@ "name": "Onvif", "documentation": "https://www.home-assistant.io/components/onvif", "requirements": [ - "onvif-zeep-async==0.1.2" + "onvif-zeep-async==0.1.3" ], "dependencies": [ "ffmpeg" diff --git a/requirements_all.txt b/requirements_all.txt index d61b394c7c4..aded56852b3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -797,7 +797,7 @@ oemthermostat==1.1 onkyo-eiscp==1.2.4 # homeassistant.components.onvif -onvif-zeep-async==0.1.2 +onvif-zeep-async==0.1.3 # homeassistant.components.openevse openevsewifi==0.4 From 281445917b4dd4342692a2a9a1adbc269b9e0b68 Mon Sep 17 00:00:00 2001 From: dreed47 Date: Thu, 9 May 2019 23:18:28 -0400 Subject: [PATCH 012/232] Fix for issue #23739. Added unique_id property so (#23769) that entities will always get mapped to the same property ZPID code. --- homeassistant/components/zestimate/sensor.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/zestimate/sensor.py b/homeassistant/components/zestimate/sensor.py index 036422d6800..cb823dab0de 100644 --- a/homeassistant/components/zestimate/sensor.py +++ b/homeassistant/components/zestimate/sensor.py @@ -66,6 +66,11 @@ class ZestimateDataSensor(Entity): self.data = None self.address = None self._state = None + + @property + def unique_id(self): + """Return the ZPID.""" + return self.params['zpid'] @property def name(self): From 369afd7ddd563f273a1061b0bf9e642d2bc6fa0f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 9 May 2019 22:01:37 -0700 Subject: [PATCH 013/232] Update sensor.py --- homeassistant/components/zestimate/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/zestimate/sensor.py b/homeassistant/components/zestimate/sensor.py index cb823dab0de..10467b20cfa 100644 --- a/homeassistant/components/zestimate/sensor.py +++ b/homeassistant/components/zestimate/sensor.py @@ -66,7 +66,7 @@ class ZestimateDataSensor(Entity): self.data = None self.address = None self._state = None - + @property def unique_id(self): """Return the ZPID.""" From 4214a354a7cb0cb4584e24bab845217f46aabe52 Mon Sep 17 00:00:00 2001 From: Andrey Kupreychik Date: Fri, 10 May 2019 16:43:43 +0700 Subject: [PATCH 014/232] Bumped keenetic NDMS2 client version (#23786) --- homeassistant/components/keenetic_ndms2/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/keenetic_ndms2/manifest.json b/homeassistant/components/keenetic_ndms2/manifest.json index d95e6384606..91c0c69a4fa 100644 --- a/homeassistant/components/keenetic_ndms2/manifest.json +++ b/homeassistant/components/keenetic_ndms2/manifest.json @@ -3,7 +3,7 @@ "name": "Keenetic ndms2", "documentation": "https://www.home-assistant.io/components/keenetic_ndms2", "requirements": [ - "ndms2_client==0.0.6" + "ndms2_client==0.0.7" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index aded56852b3..98076ae1502 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -749,7 +749,7 @@ n26==0.2.7 nad_receiver==0.0.11 # homeassistant.components.keenetic_ndms2 -ndms2_client==0.0.6 +ndms2_client==0.0.7 # homeassistant.components.ness_alarm nessclient==0.9.15 From 5888e323603b48b932616d1e917872f8a0573c42 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 10 May 2019 05:33:50 -0700 Subject: [PATCH 015/232] Add support for an external step in config flow (#23782) * Add support for an external step in config flow * Types * Lint --- homeassistant/data_entry_flow.py | 69 ++++++++++++++++++++---- homeassistant/helpers/data_entry_flow.py | 2 +- tests/common.py | 13 +++++ tests/test_data_entry_flow.py | 56 +++++++++++++++++++ 4 files changed, 129 insertions(+), 11 deletions(-) diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index acd0befda4e..aa1d21a66d3 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -11,6 +11,11 @@ _LOGGER = logging.getLogger(__name__) RESULT_TYPE_FORM = 'form' RESULT_TYPE_CREATE_ENTRY = 'create_entry' RESULT_TYPE_ABORT = 'abort' +RESULT_TYPE_EXTERNAL_STEP = 'external' +RESULT_TYPE_EXTERNAL_STEP_DONE = 'external_done' + +# Event that is fired when a flow is progressed via external source. +EVENT_DATA_ENTRY_FLOW_PROGRESSED = 'data_entry_flow_progressed' class FlowError(HomeAssistantError): @@ -71,13 +76,31 @@ class FlowManager: if flow is None: raise UnknownFlow - step_id, data_schema = flow.cur_step + cur_step = flow.cur_step - if data_schema is not None and user_input is not None: - user_input = data_schema(user_input) + if cur_step.get('data_schema') is not None and user_input is not None: + user_input = cur_step['data_schema'](user_input) - return await self._async_handle_step( - flow, step_id, user_input) + result = await self._async_handle_step( + flow, cur_step['step_id'], user_input) + + if cur_step['type'] == RESULT_TYPE_EXTERNAL_STEP: + if result['type'] not in (RESULT_TYPE_EXTERNAL_STEP, + RESULT_TYPE_EXTERNAL_STEP_DONE): + raise ValueError("External step can only transition to " + "external step or external step done.") + + # If the result has changed from last result, fire event to update + # the frontend. + if cur_step['step_id'] != result.get('step_id'): + # Tell frontend to reload the flow state. + self.hass.bus.async_fire(EVENT_DATA_ENTRY_FLOW_PROGRESSED, { + 'handler': flow.handler, + 'flow_id': flow_id, + 'refresh': True + }) + + return result @callback def async_abort(self, flow_id: str) -> None: @@ -97,13 +120,15 @@ class FlowManager: result = await getattr(flow, method)(user_input) # type: Dict - if result['type'] not in (RESULT_TYPE_FORM, RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_ABORT): + if result['type'] not in (RESULT_TYPE_FORM, RESULT_TYPE_EXTERNAL_STEP, + RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_ABORT, + RESULT_TYPE_EXTERNAL_STEP_DONE): raise ValueError( 'Handler returned incorrect type: {}'.format(result['type'])) - if result['type'] == RESULT_TYPE_FORM: - flow.cur_step = (result['step_id'], result['data_schema']) + if result['type'] in (RESULT_TYPE_FORM, RESULT_TYPE_EXTERNAL_STEP, + RESULT_TYPE_EXTERNAL_STEP_DONE): + flow.cur_step = result return result # We pass a copy of the result because we're mutating our version @@ -111,7 +136,7 @@ class FlowManager: # _async_finish_flow may change result type, check it again if result['type'] == RESULT_TYPE_FORM: - flow.cur_step = (result['step_id'], result['data_schema']) + flow.cur_step = result return result # Abort and Success results both finish the flow @@ -180,3 +205,27 @@ class FlowHandler: 'reason': reason, 'description_placeholders': description_placeholders, } + + @callback + def async_external_step(self, *, step_id: str, url: str, + description_placeholders: Optional[Dict] = None) \ + -> Dict: + """Return the definition of an external step for the user to take.""" + return { + 'type': RESULT_TYPE_EXTERNAL_STEP, + 'flow_id': self.flow_id, + 'handler': self.handler, + 'step_id': step_id, + 'url': url, + 'description_placeholders': description_placeholders, + } + + @callback + def async_external_step_done(self, *, next_step_id: str) -> Dict: + """Return the definition of an external step for the user to take.""" + return { + 'type': RESULT_TYPE_EXTERNAL_STEP_DONE, + 'flow_id': self.flow_id, + 'handler': self.handler, + 'step_id': next_step_id, + } diff --git a/homeassistant/helpers/data_entry_flow.py b/homeassistant/helpers/data_entry_flow.py index 378febf8f6d..d3ac4763269 100644 --- a/homeassistant/helpers/data_entry_flow.py +++ b/homeassistant/helpers/data_entry_flow.py @@ -58,7 +58,7 @@ class FlowManagerIndexView(_BaseFlowManagerView): except data_entry_flow.UnknownHandler: return self.json_message('Invalid handler specified', 404) except data_entry_flow.UnknownStep: - return self.json_message('Handler does not support init', 400) + return self.json_message('Handler does not support user', 400) result = self._prepare_result_json(result) diff --git a/tests/common.py b/tests/common.py index 8b28d9db047..f5a2b1327fe 100644 --- a/tests/common.py +++ b/tests/common.py @@ -951,3 +951,16 @@ def mock_entity_platform(hass, platform_path, module): _LOGGER.info("Adding mock integration platform: %s", platform_path) module_cache["{}.{}".format(platform_name, domain)] = module + + +def async_capture_events(hass, event_name): + """Create a helper that captures events.""" + events = [] + + @ha.callback + def capture_events(event): + events.append(event) + + hass.bus.async_listen(event_name, capture_events) + + return events diff --git a/tests/test_data_entry_flow.py b/tests/test_data_entry_flow.py index aa8240ff567..f6e33d264b6 100644 --- a/tests/test_data_entry_flow.py +++ b/tests/test_data_entry_flow.py @@ -5,6 +5,8 @@ import voluptuous as vol from homeassistant import data_entry_flow from homeassistant.util.decorator import Registry +from tests.common import async_capture_events + @pytest.fixture def manager(): @@ -245,3 +247,57 @@ async def test_finish_callback_change_result_type(hass): result = await manager.async_configure(result['flow_id'], {'count': 2}) assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result['result'] == 2 + + +async def test_external_step(hass, manager): + """Test external step logic.""" + manager.hass = hass + + @manager.mock_reg_handler('test') + class TestFlow(data_entry_flow.FlowHandler): + VERSION = 5 + data = None + + async def async_step_init(self, user_input=None): + if not user_input: + return self.async_external_step( + step_id='init', + url='https://example.com', + ) + + self.data = user_input + return self.async_external_step_done(next_step_id='finish') + + async def async_step_finish(self, user_input=None): + return self.async_create_entry( + title=self.data['title'], + data=self.data + ) + + events = async_capture_events( + hass, data_entry_flow.EVENT_DATA_ENTRY_FLOW_PROGRESSED + ) + + result = await manager.async_init('test') + assert result['type'] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert len(manager.async_progress()) == 1 + + # Mimic external step + # Called by integrations: `hass.config_entries.flow.async_configure(…)` + result = await manager.async_configure(result['flow_id'], { + 'title': 'Hello' + }) + assert result['type'] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP_DONE + + await hass.async_block_till_done() + assert len(events) == 1 + assert events[0].data == { + 'handler': 'test', + 'flow_id': result['flow_id'], + 'refresh': True + } + + # Frontend refreshses the flow + result = await manager.async_configure(result['flow_id']) + assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result['title'] == "Hello" From 4347a0f6b7a24ee244d50462d85702fb89209b67 Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Fri, 10 May 2019 17:34:28 +0100 Subject: [PATCH 016/232] Centralize geniushub updates (#23764) * add hub/parent/manager * add hub/parent/manager 2 * working now * delint * add back water heater * make water_heater a child * make water_heater a child - delint * make water_heater a child - delint 2 * improve turn_on logic, and small tidy-up * improve turn_on logic 2 * improve turn_on logic 3 - better docstring * improve turn_on logic 3 - better docstring * remove unnecessary DICT.get()s * remove unnecessary DICT.get()s 2 * code tidy-up * de-lint * refactor for GeniusData * refactor for GeniusData 2 * code tidy-up * add missing should_poll = False --- .../components/geniushub/__init__.py | 51 +++++++--- homeassistant/components/geniushub/climate.py | 98 ++++++++++--------- .../components/geniushub/water_heater.py | 52 +++++----- 3 files changed, 115 insertions(+), 86 deletions(-) diff --git a/homeassistant/components/geniushub/__init__.py b/homeassistant/components/geniushub/__init__.py index 181e61a7e48..69c213c6aa5 100644 --- a/homeassistant/components/geniushub/__init__.py +++ b/homeassistant/components/geniushub/__init__.py @@ -1,18 +1,25 @@ -"""This module connects to a Genius hub and shares the data.""" +"""Support for a Genius Hub system.""" +from datetime import timedelta import logging import voluptuous as vol +from geniushubclient import GeniusHubClient + from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME) from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.discovery import async_load_platform +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.event import async_track_time_interval _LOGGER = logging.getLogger(__name__) DOMAIN = 'geniushub' +SCAN_INTERVAL = timedelta(seconds=60) + _V1_API_SCHEMA = vol.Schema({ vol.Required(CONF_TOKEN): cv.string, }) @@ -31,33 +38,45 @@ CONFIG_SCHEMA = vol.Schema({ async def async_setup(hass, hass_config): """Create a Genius Hub system.""" - from geniushubclient import GeniusHubClient # noqa; pylint: disable=no-name-in-module - - geniushub_data = hass.data[DOMAIN] = {} - kwargs = dict(hass_config[DOMAIN]) if CONF_HOST in kwargs: args = (kwargs.pop(CONF_HOST), ) else: args = (kwargs.pop(CONF_TOKEN), ) + hass.data[DOMAIN] = {} + data = hass.data[DOMAIN]['data'] = GeniusData(hass, args, kwargs) try: - client = geniushub_data['client'] = GeniusHubClient( - *args, **kwargs, session=async_get_clientsession(hass) - ) - - await client.hub.update() - + await data._client.hub.update() # pylint: disable=protected-access except AssertionError: # assert response.status == HTTP_OK _LOGGER.warning( - "setup(): Failed, check your configuration.", + "Setup failed, check your configuration.", exc_info=True) return False - hass.async_create_task(async_load_platform( - hass, 'climate', DOMAIN, {}, hass_config)) + async_track_time_interval(hass, data.async_update, SCAN_INTERVAL) - hass.async_create_task(async_load_platform( - hass, 'water_heater', DOMAIN, {}, hass_config)) + for platform in ['climate', 'water_heater']: + hass.async_create_task(async_load_platform( + hass, platform, DOMAIN, {}, hass_config)) return True + + +class GeniusData: + """Container for geniushub client and data.""" + + def __init__(self, hass, args, kwargs): + """Initialize the geniushub client.""" + self._hass = hass + self._client = hass.data[DOMAIN]['client'] = GeniusHubClient( + *args, **kwargs, session=async_get_clientsession(hass)) + + async def async_update(self, now, **kwargs): + """Update the geniushub client's data.""" + try: + await self._client.hub.update() + except AssertionError: # assert response.status == HTTP_OK + _LOGGER.warning("Update failed.", exc_info=True) + return + async_dispatcher_send(self._hass, DOMAIN) diff --git a/homeassistant/components/geniushub/climate.py b/homeassistant/components/geniushub/climate.py index b396f8d6dac..22761f6b184 100644 --- a/homeassistant/components/geniushub/climate.py +++ b/homeassistant/components/geniushub/climate.py @@ -1,5 +1,4 @@ """Support for Genius Hub climate devices.""" -import asyncio import logging from homeassistant.components.climate import ClimateDevice @@ -7,30 +6,33 @@ from homeassistant.components.climate.const import ( STATE_AUTO, STATE_ECO, STATE_HEAT, STATE_MANUAL, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE, SUPPORT_ON_OFF) from homeassistant.const import ( - ATTR_TEMPERATURE, TEMP_CELSIUS) + ATTR_TEMPERATURE, STATE_OFF, TEMP_CELSIUS) +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from . import DOMAIN _LOGGER = logging.getLogger(__name__) -GH_CLIMATE_DEVICES = ['radiator'] +GH_ZONES = ['radiator'] -GENIUSHUB_SUPPORT_FLAGS = \ +GH_SUPPORT_FLAGS = \ SUPPORT_TARGET_TEMPERATURE | \ SUPPORT_ON_OFF | \ SUPPORT_OPERATION_MODE -GENIUSHUB_MAX_TEMP = 28.0 -GENIUSHUB_MIN_TEMP = 4.0 +GH_MAX_TEMP = 28.0 +GH_MIN_TEMP = 4.0 # Genius Hub Zones support only Off, Override/Boost, Footprint & Timer modes HA_OPMODE_TO_GH = { + STATE_OFF: 'off', STATE_AUTO: 'timer', STATE_ECO: 'footprint', STATE_MANUAL: 'override', } -GH_OPMODE_OFF = 'off' GH_STATE_TO_HA = { + 'off': STATE_OFF, 'timer': STATE_AUTO, 'footprint': STATE_ECO, 'away': None, @@ -39,10 +41,9 @@ GH_STATE_TO_HA = { 'test': None, 'linked': None, 'other': None, -} # intentionally missing 'off': None - +} # temperature is repeated here, as it gives access to high-precision temps -GH_DEVICE_STATE_ATTRS = ['temperature', 'type', 'occupied', 'override'] +GH_STATE_ATTRS = ['temperature', 'type', 'occupied', 'override'] async def async_setup_platform(hass, hass_config, async_add_entities, @@ -50,60 +51,73 @@ async def async_setup_platform(hass, hass_config, async_add_entities, """Set up the Genius Hub climate entities.""" client = hass.data[DOMAIN]['client'] - entities = [GeniusClimate(client, z) - for z in client.hub.zone_objs if z.type in GH_CLIMATE_DEVICES] - - async_add_entities(entities) + async_add_entities([GeniusClimateZone(client, z) + for z in client.hub.zone_objs if z.type in GH_ZONES]) -class GeniusClimate(ClimateDevice): +class GeniusClimateZone(ClimateDevice): """Representation of a Genius Hub climate device.""" def __init__(self, client, zone): """Initialize the climate device.""" self._client = client - self._objref = zone - self._id = zone.id - self._name = zone.name + self._zone = zone # Only some zones have movement detectors, which allows footprint mode op_list = list(HA_OPMODE_TO_GH) - if not hasattr(self._objref, 'occupied'): + if not hasattr(self._zone, 'occupied'): op_list.remove(STATE_ECO) self._operation_list = op_list + self._supported_features = GH_SUPPORT_FLAGS + + async def async_added_to_hass(self): + """Run when entity about to be added.""" + async_dispatcher_connect(self.hass, DOMAIN, self._refresh) + + @callback + def _refresh(self): + self.async_schedule_update_ha_state(force_refresh=True) @property def name(self): """Return the name of the climate device.""" - return self._objref.name + return self._zone.name @property def device_state_attributes(self): """Return the device state attributes.""" - tmp = self._objref.__dict__.items() - state = {k: v for k, v in tmp if k in GH_DEVICE_STATE_ATTRS} + tmp = self._zone.__dict__.items() + return {'status': {k: v for k, v in tmp if k in GH_STATE_ATTRS}} - return {'status': state} + @property + def should_poll(self) -> bool: + """Return False as the geniushub devices should not be polled.""" + return False + + @property + def icon(self): + """Return the icon to use in the frontend UI.""" + return "mdi:radiator" @property def current_temperature(self): """Return the current temperature.""" - return self._objref.temperature + return self._zone.temperature @property def target_temperature(self): """Return the temperature we try to reach.""" - return self._objref.setpoint + return self._zone.setpoint @property def min_temp(self): """Return max valid temperature that can be set.""" - return GENIUSHUB_MIN_TEMP + return GH_MIN_TEMP @property def max_temp(self): """Return max valid temperature that can be set.""" - return GENIUSHUB_MAX_TEMP + return GH_MAX_TEMP @property def temperature_unit(self): @@ -113,7 +127,7 @@ class GeniusClimate(ClimateDevice): @property def supported_features(self): """Return the list of supported features.""" - return GENIUSHUB_SUPPORT_FLAGS + return self._supported_features @property def operation_list(self): @@ -123,34 +137,30 @@ class GeniusClimate(ClimateDevice): @property def current_operation(self): """Return the current operation mode.""" - return GH_STATE_TO_HA.get(self._objref.mode) + return GH_STATE_TO_HA[self._zone.mode] @property def is_on(self): """Return True if the device is on.""" - return self._objref.mode in GH_STATE_TO_HA + return self._zone.mode != HA_OPMODE_TO_GH[STATE_OFF] async def async_set_operation_mode(self, operation_mode): """Set a new operation mode for this zone.""" - await self._objref.set_mode(HA_OPMODE_TO_GH.get(operation_mode)) + await self._zone.set_mode(HA_OPMODE_TO_GH[operation_mode]) async def async_set_temperature(self, **kwargs): """Set a new target temperature for this zone.""" - temperature = kwargs.get(ATTR_TEMPERATURE) - await self._objref.set_override(temperature, 3600) # 1 hour + await self._zone.set_override(kwargs.get(ATTR_TEMPERATURE), 3600) async def async_turn_on(self): - """Turn on this heating zone.""" - await self._objref.set_mode(HA_OPMODE_TO_GH.get(STATE_AUTO)) + """Turn on this heating zone. + + Set a Zone to Footprint mode if they have a Room sensor, and to Timer + mode otherwise. + """ + mode = STATE_ECO if hasattr(self._zone, 'occupied') else STATE_AUTO + await self._zone.set_mode(HA_OPMODE_TO_GH[mode]) async def async_turn_off(self): """Turn off this heating zone (i.e. to frost protect).""" - await self._objref.set_mode(GH_OPMODE_OFF) - - async def async_update(self): - """Get the latest data from the hub.""" - try: - await self._objref.update() - except (AssertionError, asyncio.TimeoutError) as err: - _LOGGER.warning("Update for %s failed, message: %s", - self._id, err) + await self._zone.set_mode(HA_OPMODE_TO_GH[STATE_OFF]) diff --git a/homeassistant/components/geniushub/water_heater.py b/homeassistant/components/geniushub/water_heater.py index f5f09f9b1d5..6efbed514ee 100644 --- a/homeassistant/components/geniushub/water_heater.py +++ b/homeassistant/components/geniushub/water_heater.py @@ -1,5 +1,4 @@ """Support for Genius Hub water_heater devices.""" -import asyncio import logging from homeassistant.components.water_heater import ( @@ -7,6 +6,8 @@ from homeassistant.components.water_heater import ( SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE) from homeassistant.const import ( ATTR_TEMPERATURE, STATE_OFF, TEMP_CELSIUS) +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from . import DOMAIN @@ -15,15 +16,15 @@ STATE_MANUAL = 'manual' _LOGGER = logging.getLogger(__name__) -GH_WATER_HEATERS = ['hot water temperature'] +GH_HEATERS = ['hot water temperature'] -GENIUSHUB_SUPPORT_FLAGS = \ +GH_SUPPORT_FLAGS = \ SUPPORT_TARGET_TEMPERATURE | \ SUPPORT_OPERATION_MODE # HA does not have SUPPORT_ON_OFF for water_heater -GENIUSHUB_MAX_TEMP = 80.0 -GENIUSHUB_MIN_TEMP = 30.0 +GH_MAX_TEMP = 80.0 +GH_MIN_TEMP = 30.0 # Genius Hub HW supports only Off, Override/Boost & Timer modes HA_OPMODE_TO_GH = { @@ -31,7 +32,6 @@ HA_OPMODE_TO_GH = { STATE_AUTO: 'timer', STATE_MANUAL: 'override', } -GH_OPMODE_OFF = 'off' GH_STATE_TO_HA = { 'off': STATE_OFF, 'timer': STATE_AUTO, @@ -43,8 +43,7 @@ GH_STATE_TO_HA = { 'linked': None, 'other': None, } - -GH_DEVICE_STATE_ATTRS = ['type', 'override'] +GH_STATE_ATTRS = ['type', 'override'] async def async_setup_platform(hass, hass_config, async_add_entities, @@ -53,7 +52,7 @@ async def async_setup_platform(hass, hass_config, async_add_entities, client = hass.data[DOMAIN]['client'] entities = [GeniusWaterHeater(client, z) - for z in client.hub.zone_objs if z.type in GH_WATER_HEATERS] + for z in client.hub.zone_objs if z.type in GH_HEATERS] async_add_entities(entities) @@ -65,11 +64,17 @@ class GeniusWaterHeater(WaterHeaterDevice): """Initialize the water_heater device.""" self._client = client self._boiler = boiler - self._id = boiler.id - self._name = boiler.name self._operation_list = list(HA_OPMODE_TO_GH) + async def async_added_to_hass(self): + """Run when entity about to be added.""" + async_dispatcher_connect(self.hass, DOMAIN, self._refresh) + + @callback + def _refresh(self): + self.async_schedule_update_ha_state(force_refresh=True) + @property def name(self): """Return the name of the water_heater device.""" @@ -79,9 +84,12 @@ class GeniusWaterHeater(WaterHeaterDevice): def device_state_attributes(self): """Return the device state attributes.""" tmp = self._boiler.__dict__.items() - state = {k: v for k, v in tmp if k in GH_DEVICE_STATE_ATTRS} + return {'status': {k: v for k, v in tmp if k in GH_STATE_ATTRS}} - return {'status': state} + @property + def should_poll(self) -> bool: + """Return False as the geniushub devices should not be polled.""" + return False @property def current_temperature(self): @@ -96,12 +104,12 @@ class GeniusWaterHeater(WaterHeaterDevice): @property def min_temp(self): """Return max valid temperature that can be set.""" - return GENIUSHUB_MIN_TEMP + return GH_MIN_TEMP @property def max_temp(self): """Return max valid temperature that can be set.""" - return GENIUSHUB_MAX_TEMP + return GH_MAX_TEMP @property def temperature_unit(self): @@ -111,7 +119,7 @@ class GeniusWaterHeater(WaterHeaterDevice): @property def supported_features(self): """Return the list of supported features.""" - return GENIUSHUB_SUPPORT_FLAGS + return GH_SUPPORT_FLAGS @property def operation_list(self): @@ -121,21 +129,13 @@ class GeniusWaterHeater(WaterHeaterDevice): @property def current_operation(self): """Return the current operation mode.""" - return GH_STATE_TO_HA.get(self._boiler.mode) + return GH_STATE_TO_HA[self._boiler.mode] async def async_set_operation_mode(self, operation_mode): """Set a new operation mode for this boiler.""" - await self._boiler.set_mode(HA_OPMODE_TO_GH.get(operation_mode)) + await self._boiler.set_mode(HA_OPMODE_TO_GH[operation_mode]) async def async_set_temperature(self, **kwargs): """Set a new target temperature for this boiler.""" temperature = kwargs[ATTR_TEMPERATURE] await self._boiler.set_override(temperature, 3600) # 1 hour - - async def async_update(self): - """Get the latest data from the hub.""" - try: - await self._boiler.update() - except (AssertionError, asyncio.TimeoutError) as err: - _LOGGER.warning("Update for %s failed, message: %s", - self._id, err) From e9ea5c2ccbac428fce84190e98b9101ba25b9bca Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 10 May 2019 13:20:50 -0700 Subject: [PATCH 017/232] Move tests to right folder (#23790) * Move tests to right folder * Fix test leaving files behind --- .../{sensor/test_pilight.py => pilight/test_sensor.py} | 0 .../components/{cover/test_rfxtrx.py => rfxtrx/test_cover.py} | 0 .../components/{light/test_rfxtrx.py => rfxtrx/test_light.py} | 0 .../{sensor/test_rfxtrx.py => rfxtrx/test_sensor.py} | 0 .../{switch/test_rfxtrx.py => rfxtrx/test_switch.py} | 0 .../{light/test_tradfri.py => tradfri/test_light.py} | 0 tests/components/tts/test_init.py | 4 ++++ .../components/{switch/test_unifi.py => unifi/test_switch.py} | 0 8 files changed, 4 insertions(+) rename tests/components/{sensor/test_pilight.py => pilight/test_sensor.py} (100%) rename tests/components/{cover/test_rfxtrx.py => rfxtrx/test_cover.py} (100%) rename tests/components/{light/test_rfxtrx.py => rfxtrx/test_light.py} (100%) rename tests/components/{sensor/test_rfxtrx.py => rfxtrx/test_sensor.py} (100%) rename tests/components/{switch/test_rfxtrx.py => rfxtrx/test_switch.py} (100%) rename tests/components/{light/test_tradfri.py => tradfri/test_light.py} (100%) rename tests/components/{switch/test_unifi.py => unifi/test_switch.py} (100%) diff --git a/tests/components/sensor/test_pilight.py b/tests/components/pilight/test_sensor.py similarity index 100% rename from tests/components/sensor/test_pilight.py rename to tests/components/pilight/test_sensor.py diff --git a/tests/components/cover/test_rfxtrx.py b/tests/components/rfxtrx/test_cover.py similarity index 100% rename from tests/components/cover/test_rfxtrx.py rename to tests/components/rfxtrx/test_cover.py diff --git a/tests/components/light/test_rfxtrx.py b/tests/components/rfxtrx/test_light.py similarity index 100% rename from tests/components/light/test_rfxtrx.py rename to tests/components/rfxtrx/test_light.py diff --git a/tests/components/sensor/test_rfxtrx.py b/tests/components/rfxtrx/test_sensor.py similarity index 100% rename from tests/components/sensor/test_rfxtrx.py rename to tests/components/rfxtrx/test_sensor.py diff --git a/tests/components/switch/test_rfxtrx.py b/tests/components/rfxtrx/test_switch.py similarity index 100% rename from tests/components/switch/test_rfxtrx.py rename to tests/components/rfxtrx/test_switch.py diff --git a/tests/components/light/test_tradfri.py b/tests/components/tradfri/test_light.py similarity index 100% rename from tests/components/light/test_tradfri.py rename to tests/components/tradfri/test_light.py diff --git a/tests/components/tts/test_init.py b/tests/components/tts/test_init.py index 140a938201b..b34d74bd0c6 100644 --- a/tests/components/tts/test_init.py +++ b/tests/components/tts/test_init.py @@ -607,6 +607,10 @@ async def test_setup_component_and_web_get_url(hass, hass_client): ("{}/api/tts_proxy/265944c108cbb00b2a62" "1be5930513e03a0bb2cd_en_-_demo.mp3".format(hass.config.api.base_url)) + tts_cache = hass.config.path(tts.DEFAULT_CACHE_DIR) + if os.path.isdir(tts_cache): + shutil.rmtree(tts_cache) + async def test_setup_component_and_web_get_url_bad_config(hass, hass_client): """Set up the demo platform and receive wrong file from web.""" diff --git a/tests/components/switch/test_unifi.py b/tests/components/unifi/test_switch.py similarity index 100% rename from tests/components/switch/test_unifi.py rename to tests/components/unifi/test_switch.py From 34f6245e7418a49dce14664c3508b7ff2d2a6f0a Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Fri, 10 May 2019 22:37:03 +0200 Subject: [PATCH 018/232] Synchronize Sonos service calls (#23791) --- homeassistant/components/sonos/__init__.py | 6 ++++++ homeassistant/components/sonos/media_player.py | 5 ++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index d0e467f6964..5f7b2d04431 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -1,4 +1,5 @@ """Support to embed Sonos.""" +import asyncio import voluptuous as vol from homeassistant import config_entries @@ -80,12 +81,15 @@ SONOS_SET_OPTION_SCHEMA = vol.Schema({ vol.Optional(ATTR_SPEECH_ENHANCE): cv.boolean, }) +DATA_SERVICE_EVENT = 'sonos_service_idle' + async def async_setup(hass, config): """Set up the Sonos component.""" conf = config.get(DOMAIN) hass.data[DOMAIN] = conf or {} + hass.data[DATA_SERVICE_EVENT] = asyncio.Event() if conf is not None: hass.async_create_task(hass.config_entries.flow.async_init( @@ -93,7 +97,9 @@ async def async_setup(hass, config): async def service_handle(service): """Dispatch a service call.""" + hass.data[DATA_SERVICE_EVENT].clear() async_dispatcher_send(hass, DOMAIN, service.service, service.data) + await hass.data[DATA_SERVICE_EVENT].wait() hass.services.async_register( DOMAIN, SERVICE_JOIN, service_handle, diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 524353f8c54..5d1cd138260 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -25,7 +25,7 @@ from homeassistant.util.dt import utcnow from . import ( CONF_ADVERTISE_ADDR, CONF_HOSTS, CONF_INTERFACE_ADDR, - DOMAIN as SONOS_DOMAIN, + DATA_SERVICE_EVENT, DOMAIN as SONOS_DOMAIN, ATTR_ALARM_ID, ATTR_ENABLED, ATTR_INCLUDE_LINKED_ZONES, ATTR_MASTER, ATTR_NIGHT_SOUND, ATTR_SLEEP_TIME, ATTR_SPEECH_ENHANCE, ATTR_TIME, ATTR_VOLUME, ATTR_WITH_GROUP, @@ -155,6 +155,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): hass.async_add_executor_job(call, data) + # We are ready for the next service call + hass.data[DATA_SERVICE_EVENT].set() + async_dispatcher_connect(hass, SONOS_DOMAIN, async_service_handle) From c8d479e5945364fbf25862a874e9530a669c7ceb Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 10 May 2019 14:22:38 -0700 Subject: [PATCH 019/232] Updated frontend to 20190510.0 --- homeassistant/components/frontend/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 559469c63ac..082bc6328d0 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/components/frontend", "requirements": [ - "home-assistant-frontend==20190509.0" + "home-assistant-frontend==20190510.0" ], "dependencies": [ "api", diff --git a/requirements_all.txt b/requirements_all.txt index 98076ae1502..4dc5112fcaa 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -563,7 +563,7 @@ hole==0.3.0 holidays==0.9.10 # homeassistant.components.frontend -home-assistant-frontend==20190509.0 +home-assistant-frontend==20190510.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ec4589c4403..6d9913085ce 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -145,7 +145,7 @@ hdate==0.8.7 holidays==0.9.10 # homeassistant.components.frontend -home-assistant-frontend==20190509.0 +home-assistant-frontend==20190510.0 # homeassistant.components.homekit_controller homekit[IP]==0.14.0 From 910825580e41df68d50299af7cea95dc6db0cb04 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Fri, 10 May 2019 18:57:08 -0400 Subject: [PATCH 020/232] Do not add coordinator to the ZHA entities. (#23803) --- homeassistant/components/zha/core/gateway.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 4a16bfe5004..daf14297ec1 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -93,6 +93,8 @@ class ZHAGateway: init_tasks = [] for device in self.application_controller.devices.values(): + if device.nwk == 0x0000: + continue init_tasks.append(self.async_device_initialized(device, False)) await asyncio.gather(*init_tasks) From 03cd4480df5c8da2e6e1fecea09c55fa16c2ffaa Mon Sep 17 00:00:00 2001 From: Jason Hunter Date: Sat, 11 May 2019 02:15:21 -0400 Subject: [PATCH 021/232] fix onvif wsdl import - take 2 (#23807) --- homeassistant/components/onvif/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/onvif/manifest.json b/homeassistant/components/onvif/manifest.json index f36a5cf4b1a..d86ec38ccb7 100644 --- a/homeassistant/components/onvif/manifest.json +++ b/homeassistant/components/onvif/manifest.json @@ -3,7 +3,7 @@ "name": "Onvif", "documentation": "https://www.home-assistant.io/components/onvif", "requirements": [ - "onvif-zeep-async==0.1.3" + "onvif-zeep-async==0.2.0" ], "dependencies": [ "ffmpeg" diff --git a/requirements_all.txt b/requirements_all.txt index 4dc5112fcaa..3f1f822d940 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -797,7 +797,7 @@ oemthermostat==1.1 onkyo-eiscp==1.2.4 # homeassistant.components.onvif -onvif-zeep-async==0.1.3 +onvif-zeep-async==0.2.0 # homeassistant.components.openevse openevsewifi==0.4 From 2b7021407c8077decb2634e032b3eaf0bc6195bb Mon Sep 17 00:00:00 2001 From: Andre Lengwenus Date: Sat, 11 May 2019 10:24:02 +0200 Subject: [PATCH 022/232] Add LCN climate platform (#22542) * Add LCN climate component * Updates of ha_state are done async * Changes due to manifest.json --- homeassistant/components/lcn/__init__.py | 30 ++++- homeassistant/components/lcn/climate.py | 138 +++++++++++++++++++++++ homeassistant/components/lcn/const.py | 5 + 3 files changed, 167 insertions(+), 6 deletions(-) create mode 100644 homeassistant/components/lcn/climate.py diff --git a/homeassistant/components/lcn/__init__.py b/homeassistant/components/lcn/__init__.py index 7e7fb1430cc..b2dbccbce7b 100644 --- a/homeassistant/components/lcn/__init__.py +++ b/homeassistant/components/lcn/__init__.py @@ -5,20 +5,22 @@ import pypck from pypck.connection import PchkConnectionManager import voluptuous as vol +from homeassistant.components.climate import DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP from homeassistant.const import ( CONF_ADDRESS, CONF_BINARY_SENSORS, CONF_COVERS, CONF_HOST, CONF_LIGHTS, CONF_NAME, CONF_PASSWORD, CONF_PORT, CONF_SENSORS, CONF_SWITCHES, - CONF_UNIT_OF_MEASUREMENT, CONF_USERNAME) + CONF_UNIT_OF_MEASUREMENT, CONF_USERNAME, TEMP_CELSIUS, TEMP_FAHRENHEIT) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.entity import Entity from .const import ( - BINSENSOR_PORTS, CONF_CONNECTIONS, CONF_DIM_MODE, CONF_DIMMABLE, - CONF_MOTOR, CONF_OUTPUT, CONF_SK_NUM_TRIES, CONF_SOURCE, CONF_TRANSITION, - DATA_LCN, DEFAULT_NAME, DIM_MODES, DOMAIN, KEYS, LED_PORTS, LOGICOP_PORTS, - MOTOR_PORTS, OUTPUT_PORTS, PATTERN_ADDRESS, RELAY_PORTS, S0_INPUTS, - SETPOINTS, THRESHOLDS, VAR_UNITS, VARIABLES) + BINSENSOR_PORTS, CONF_CLIMATES, CONF_CONNECTIONS, CONF_DIM_MODE, + CONF_DIMMABLE, CONF_LOCKABLE, CONF_MAX_TEMP, CONF_MIN_TEMP, CONF_MOTOR, + CONF_OUTPUT, CONF_SETPOINT, CONF_SK_NUM_TRIES, CONF_SOURCE, + CONF_TRANSITION, DATA_LCN, DEFAULT_NAME, DIM_MODES, DOMAIN, KEYS, + LED_PORTS, LOGICOP_PORTS, MOTOR_PORTS, OUTPUT_PORTS, PATTERN_ADDRESS, + RELAY_PORTS, S0_INPUTS, SETPOINTS, THRESHOLDS, VAR_UNITS, VARIABLES) _LOGGER = logging.getLogger(__name__) @@ -72,6 +74,19 @@ BINARY_SENSORS_SCHEMA = vol.Schema({ BINSENSOR_PORTS)) }) +CLIMATES_SCHEMA = vol.Schema({ + vol.Required(CONF_NAME): cv.string, + vol.Required(CONF_ADDRESS): is_address, + vol.Required(CONF_SOURCE): vol.All(vol.Upper, vol.In(VARIABLES)), + vol.Required(CONF_SETPOINT): vol.All(vol.Upper, + vol.In(VARIABLES + SETPOINTS)), + vol.Optional(CONF_MAX_TEMP, default=DEFAULT_MAX_TEMP): vol.Coerce(float), + vol.Optional(CONF_MIN_TEMP, default=DEFAULT_MIN_TEMP): vol.Coerce(float), + vol.Optional(CONF_LOCKABLE, default=False): vol.Coerce(bool), + vol.Optional(CONF_UNIT_OF_MEASUREMENT, default=TEMP_CELSIUS): + vol.In(TEMP_CELSIUS, TEMP_FAHRENHEIT) +}) + COVERS_SCHEMA = vol.Schema({ vol.Required(CONF_NAME): cv.string, vol.Required(CONF_ADDRESS): is_address, @@ -124,6 +139,8 @@ CONFIG_SCHEMA = vol.Schema({ cv.ensure_list, has_unique_connection_names, [CONNECTION_SCHEMA]), vol.Optional(CONF_BINARY_SENSORS): vol.All( cv.ensure_list, [BINARY_SENSORS_SCHEMA]), + vol.Optional(CONF_CLIMATES): vol.All( + cv.ensure_list, [CLIMATES_SCHEMA]), vol.Optional(CONF_COVERS): vol.All( cv.ensure_list, [COVERS_SCHEMA]), vol.Optional(CONF_LIGHTS): vol.All( @@ -184,6 +201,7 @@ async def async_setup(hass, config): # load platforms for component, conf_key in (('binary_sensor', CONF_BINARY_SENSORS), + ('climate', CONF_CLIMATES), ('cover', CONF_COVERS), ('light', CONF_LIGHTS), ('sensor', CONF_SENSORS), diff --git a/homeassistant/components/lcn/climate.py b/homeassistant/components/lcn/climate.py new file mode 100644 index 00000000000..355a9e84c79 --- /dev/null +++ b/homeassistant/components/lcn/climate.py @@ -0,0 +1,138 @@ +"""Support for LCN climate control.""" +import pypck + +from homeassistant.components.climate import ClimateDevice, const +from homeassistant.const import ( + ATTR_TEMPERATURE, CONF_ADDRESS, CONF_UNIT_OF_MEASUREMENT) + +from . import LcnDevice, get_connection +from .const import ( + CONF_CONNECTIONS, CONF_LOCKABLE, CONF_MAX_TEMP, CONF_MIN_TEMP, + CONF_SETPOINT, CONF_SOURCE, DATA_LCN) + + +async def async_setup_platform(hass, hass_config, async_add_entities, + discovery_info=None): + """Set up the LCN climate platform.""" + if discovery_info is None: + return + + devices = [] + for config in discovery_info: + address, connection_id = config[CONF_ADDRESS] + addr = pypck.lcn_addr.LcnAddr(*address) + connections = hass.data[DATA_LCN][CONF_CONNECTIONS] + connection = get_connection(connections, connection_id) + address_connection = connection.get_address_conn(addr) + + devices.append(LcnClimate(config, address_connection)) + + async_add_entities(devices) + + +class LcnClimate(LcnDevice, ClimateDevice): + """Representation of a LCN climate device.""" + + def __init__(self, config, address_connection): + """Initialize of a LCN climate device.""" + super().__init__(config, address_connection) + + self.variable = self.pypck.lcn_defs.Var[config[CONF_SOURCE]] + self.setpoint = self.pypck.lcn_defs.Var[config[CONF_SETPOINT]] + self.unit = self.pypck.lcn_defs.VarUnit.parse( + config[CONF_UNIT_OF_MEASUREMENT]) + + self.regulator_id = \ + self.pypck.lcn_defs.Var.to_set_point_id(self.setpoint) + self.is_lockable = config[CONF_LOCKABLE] + self._max_temp = config[CONF_MAX_TEMP] + self._min_temp = config[CONF_MIN_TEMP] + + self._current_temperature = None + self._target_temperature = None + self._is_on = True + + self.support = const.SUPPORT_TARGET_TEMPERATURE + if self.is_lockable: + self.support |= const.SUPPORT_ON_OFF + + async def async_added_to_hass(self): + """Run when entity about to be added to hass.""" + await super().async_added_to_hass() + await self.address_connection.activate_status_request_handler( + self.variable) + await self.address_connection.activate_status_request_handler( + self.setpoint) + + @property + def supported_features(self): + """Return the list of supported features.""" + return self.support + + @property + def temperature_unit(self): + """Return the unit of measurement.""" + return self.unit.value + + @property + def current_temperature(self): + """Return the current temperature.""" + return self._current_temperature + + @property + def target_temperature(self): + """Return the temperature we try to reach.""" + return self._target_temperature + + @property + def is_on(self): + """Return true if the device is on.""" + return self._is_on + + @property + def max_temp(self): + """Return the maximum temperature.""" + return self._max_temp + + @property + def min_temp(self): + """Return the minimum temperature.""" + return self._min_temp + + async def async_turn_on(self): + """Turn on.""" + self._is_on = True + self.address_connection.lock_regulator(self.regulator_id, False) + await self.async_update_ha_state() + + async def async_turn_off(self): + """Turn off.""" + self._is_on = False + self.address_connection.lock_regulator(self.regulator_id, True) + self._target_temperature = None + await self.async_update_ha_state() + + async def async_set_temperature(self, **kwargs): + """Set new target temperature.""" + temperature = kwargs.get(ATTR_TEMPERATURE) + if temperature is None: + return + + self._target_temperature = temperature + self.address_connection.var_abs( + self.setpoint, self._target_temperature, self.unit) + await self.async_update_ha_state() + + def input_received(self, input_obj): + """Set temperature value when LCN input object is received.""" + if not isinstance(input_obj, self.pypck.inputs.ModStatusVar): + return + + if input_obj.get_var() == self.variable: + self._current_temperature = ( + input_obj.get_value().to_var_unit(self.unit)) + elif self._is_on and input_obj.get_var() == self.setpoint: + self._target_temperature = ( + input_obj.get_value().to_var_unit(self.unit)) + + self.async_schedule_update_ha_state() diff --git a/homeassistant/components/lcn/const.py b/homeassistant/components/lcn/const.py index b745d0636c2..68818984788 100644 --- a/homeassistant/components/lcn/const.py +++ b/homeassistant/components/lcn/const.py @@ -21,6 +21,11 @@ CONF_DIMMABLE = 'dimmable' CONF_TRANSITION = 'transition' CONF_MOTOR = 'motor' CONF_SOURCE = 'source' +CONF_SETPOINT = 'setpoint' +CONF_LOCKABLE = 'lockable' +CONF_CLIMATES = 'climates' +CONF_MAX_TEMP = 'max_temp' +CONF_MIN_TEMP = 'min_temp' DIM_MODES = ['STEPS50', 'STEPS200'] From d34214ad32ccce679b82e5151579ab8432194734 Mon Sep 17 00:00:00 2001 From: Stephen Benjamin Date: Sat, 11 May 2019 04:33:18 -0400 Subject: [PATCH 023/232] Bump venstarcolortouch to v0.7 (#23806) --- homeassistant/components/venstar/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/venstar/manifest.json b/homeassistant/components/venstar/manifest.json index e8b9158f721..cfa4dd6832d 100644 --- a/homeassistant/components/venstar/manifest.json +++ b/homeassistant/components/venstar/manifest.json @@ -3,7 +3,7 @@ "name": "Venstar", "documentation": "https://www.home-assistant.io/components/venstar", "requirements": [ - "venstarcolortouch==0.6" + "venstarcolortouch==0.7" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index 3f1f822d940..9e58676fb3f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1761,7 +1761,7 @@ uscisstatus==0.1.1 uvcclient==0.11.0 # homeassistant.components.venstar -venstarcolortouch==0.6 +venstarcolortouch==0.7 # homeassistant.components.volkszaehler volkszaehler==0.1.2 From 5c9a58f3e642cfe91e8c9c2f62dfac8af2028601 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 11 May 2019 16:15:09 +0200 Subject: [PATCH 024/232] Upgrade youtube_dl to 2019.05.11 (#23808) --- homeassistant/components/media_extractor/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_extractor/manifest.json b/homeassistant/components/media_extractor/manifest.json index 31086eab83d..dbdb64b8421 100644 --- a/homeassistant/components/media_extractor/manifest.json +++ b/homeassistant/components/media_extractor/manifest.json @@ -3,7 +3,7 @@ "name": "Media extractor", "documentation": "https://www.home-assistant.io/components/media_extractor", "requirements": [ - "youtube_dl==2019.04.30" + "youtube_dl==2019.05.11" ], "dependencies": [ "media_player" diff --git a/requirements_all.txt b/requirements_all.txt index 9e58676fb3f..b0046a26054 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1842,7 +1842,7 @@ yeelight==0.5.0 yeelightsunflower==0.0.10 # homeassistant.components.media_extractor -youtube_dl==2019.04.30 +youtube_dl==2019.05.11 # homeassistant.components.zengge zengge==0.2 From 6f8038992cda28a838739357e37b52ac1389d506 Mon Sep 17 00:00:00 2001 From: mvn23 Date: Sat, 11 May 2019 16:15:35 +0200 Subject: [PATCH 025/232] Bump pyotgw to 0.4b4, fix Opentherm Gateway name in manifest.json (#23810) --- homeassistant/components/opentherm_gw/manifest.json | 4 ++-- requirements_all.txt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/opentherm_gw/manifest.json b/homeassistant/components/opentherm_gw/manifest.json index 50bfa4d1122..560e30931a3 100644 --- a/homeassistant/components/opentherm_gw/manifest.json +++ b/homeassistant/components/opentherm_gw/manifest.json @@ -1,9 +1,9 @@ { "domain": "opentherm_gw", - "name": "Opentherm gw", + "name": "Opentherm Gateway", "documentation": "https://www.home-assistant.io/components/opentherm_gw", "requirements": [ - "pyotgw==0.4b3" + "pyotgw==0.4b4" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index b0046a26054..805056d33d3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1232,7 +1232,7 @@ pyoppleio==1.0.5 pyota==2.0.5 # homeassistant.components.opentherm_gw -pyotgw==0.4b3 +pyotgw==0.4b4 # homeassistant.auth.mfa_modules.notify # homeassistant.auth.mfa_modules.totp From 3ec4070d8c6d4a9313d7df9d7ca2098f1ed0532c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 11 May 2019 10:29:30 -0700 Subject: [PATCH 026/232] Fix patching right import (#23816) --- tests/components/hue/test_config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/hue/test_config_flow.py b/tests/components/hue/test_config_flow.py index fe3bffe5357..78b7ba0269c 100644 --- a/tests/components/hue/test_config_flow.py +++ b/tests/components/hue/test_config_flow.py @@ -130,7 +130,7 @@ async def test_flow_timeout_discovery(hass): flow = config_flow.HueFlowHandler() flow.hass = hass - with patch('aiohue.discovery.discover_nupnp', + with patch('homeassistant.components.hue.config_flow.discover_nupnp', side_effect=asyncio.TimeoutError): result = await flow.async_step_init() From 2a9fd9ae269e8929084e53ab12901e96aec93e7d Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Sun, 12 May 2019 12:40:10 +0100 Subject: [PATCH 027/232] Add incomfort climate and bump client (#23830) * Initial commit * bump client for bugfix * bump client for bugfix 2 * de-lint --- .../components/incomfort/__init__.py | 5 +- homeassistant/components/incomfort/climate.py | 93 +++++++++++++++++++ .../components/incomfort/manifest.json | 2 +- requirements_all.txt | 2 +- 4 files changed, 98 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/incomfort/climate.py diff --git a/homeassistant/components/incomfort/__init__.py b/homeassistant/components/incomfort/__init__.py index edff8c8299f..8aaa8e7e19d 100644 --- a/homeassistant/components/incomfort/__init__.py +++ b/homeassistant/components/incomfort/__init__.py @@ -44,7 +44,8 @@ async def async_setup(hass, hass_config): exc_info=True) return False - hass.async_create_task(async_load_platform( - hass, 'water_heater', DOMAIN, {}, hass_config)) + for platform in ['water_heater', 'climate']: + hass.async_create_task(async_load_platform( + hass, platform, DOMAIN, {}, hass_config)) return True diff --git a/homeassistant/components/incomfort/climate.py b/homeassistant/components/incomfort/climate.py new file mode 100644 index 00000000000..fa42ced32c2 --- /dev/null +++ b/homeassistant/components/incomfort/climate.py @@ -0,0 +1,93 @@ +"""Support for an Intergas boiler via an InComfort/InTouch Lan2RF gateway.""" +from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate.const import SUPPORT_TARGET_TEMPERATURE +from homeassistant.const import (ATTR_TEMPERATURE, TEMP_CELSIUS) +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from . import DOMAIN + +INTOUCH_SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE + +INTOUCH_MAX_TEMP = 30.0 +INTOUCH_MIN_TEMP = 5.0 + + +async def async_setup_platform(hass, hass_config, async_add_entities, + discovery_info=None): + """Set up an InComfort/InTouch climate device.""" + client = hass.data[DOMAIN]['client'] + heater = hass.data[DOMAIN]['heater'] + + rooms = [InComfortClimate(client, r) + for r in heater.rooms if not r.room_temp] + if rooms: + async_add_entities(rooms) + + +class InComfortClimate(ClimateDevice): + """Representation of an InComfort/InTouch climate device.""" + + def __init__(self, client, room): + """Initialize the climate device.""" + self._client = client + self._room = room + self._name = 'Room {}'.format(room.room_no) + + async def async_added_to_hass(self): + """Set up a listener when this entity is added to HA.""" + async_dispatcher_connect(self.hass, DOMAIN, self._refresh) + + @callback + def _refresh(self): + self.async_schedule_update_ha_state(force_refresh=True) + + @property + def name(self): + """Return the name of the climate device.""" + return self._name + + @property + def device_state_attributes(self): + """Return the device state attributes.""" + return {'status': self._room.status} + + @property + def current_temperature(self): + """Return the current temperature.""" + return self._room.room_temp + + @property + def target_temperature(self): + """Return the temperature we try to reach.""" + return self._room.override + + @property + def min_temp(self): + """Return max valid temperature that can be set.""" + return INTOUCH_MIN_TEMP + + @property + def max_temp(self): + """Return max valid temperature that can be set.""" + return INTOUCH_MAX_TEMP + + @property + def temperature_unit(self): + """Return the unit of measurement.""" + return TEMP_CELSIUS + + @property + def supported_features(self): + """Return the list of supported features.""" + return INTOUCH_SUPPORT_FLAGS + + async def async_set_temperature(self, **kwargs): + """Set a new target temperature for this zone.""" + temperature = kwargs.get(ATTR_TEMPERATURE) + await self._room.set_override(temperature) + + @property + def should_poll(self) -> bool: + """Return False as this device should never be polled.""" + return False diff --git a/homeassistant/components/incomfort/manifest.json b/homeassistant/components/incomfort/manifest.json index 028a741a673..1731c8c942f 100644 --- a/homeassistant/components/incomfort/manifest.json +++ b/homeassistant/components/incomfort/manifest.json @@ -3,7 +3,7 @@ "name": "Intergas InComfort/Intouch Lan2RF gateway", "documentation": "https://www.home-assistant.io/components/incomfort", "requirements": [ - "incomfort-client==0.2.8" + "incomfort-client==0.2.9" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 805056d33d3..5a8a72c7cd4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -602,7 +602,7 @@ iglo==1.2.7 ihcsdk==2.3.0 # homeassistant.components.incomfort -incomfort-client==0.2.8 +incomfort-client==0.2.9 # homeassistant.components.influxdb influxdb==5.2.0 From e687848152e0c47525976104cd94bed4c4759815 Mon Sep 17 00:00:00 2001 From: akloeckner Date: Sun, 12 May 2019 19:28:33 +0200 Subject: [PATCH 028/232] Make broadlink switch restore its state (#23829) * Make broadlink switch restore its state Method copied from pilight switch * style --- homeassistant/components/broadlink/switch.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/broadlink/switch.py b/homeassistant/components/broadlink/switch.py index d1b769e3d83..8474584ab37 100644 --- a/homeassistant/components/broadlink/switch.py +++ b/homeassistant/components/broadlink/switch.py @@ -10,9 +10,10 @@ from homeassistant.components.switch import ( ENTITY_ID_FORMAT, PLATFORM_SCHEMA, SwitchDevice) from homeassistant.const import ( CONF_COMMAND_OFF, CONF_COMMAND_ON, CONF_FRIENDLY_NAME, CONF_HOST, CONF_MAC, - CONF_SWITCHES, CONF_TIMEOUT, CONF_TYPE) + CONF_SWITCHES, CONF_TIMEOUT, CONF_TYPE, STATE_ON) import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle, slugify +from homeassistant.helpers.restore_state import RestoreEntity from . import async_setup_service, data_packet @@ -115,7 +116,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(switches) -class BroadlinkRMSwitch(SwitchDevice): +class BroadlinkRMSwitch(SwitchDevice, RestoreEntity): """Representation of an Broadlink switch.""" def __init__(self, name, friendly_name, device, command_on, command_off): @@ -127,6 +128,13 @@ class BroadlinkRMSwitch(SwitchDevice): self._command_off = command_off self._device = device + async def async_added_to_hass(self): + """Call when entity about to be added to hass.""" + await super().async_added_to_hass() + state = await self.async_get_last_state() + if state: + self._state = state.state == STATE_ON + @property def name(self): """Return the name of the switch.""" From b8f6d824fdfa54455dab959f55c8d77c1e1aeea5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 12 May 2019 23:54:55 -0700 Subject: [PATCH 029/232] Catch import error when processing config (#23833) --- homeassistant/config.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/config.py b/homeassistant/config.py index 1be3ba082e8..95be31d5bdb 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -770,7 +770,11 @@ async def async_process_component_config( This method must be run in the event loop. """ domain = integration.domain - component = integration.get_component() + try: + component = integration.get_component() + except ImportError as ex: + _LOGGER.error("Unable to import %s: %s", domain, ex) + return None if hasattr(component, 'CONFIG_SCHEMA'): try: From 3508622e3bdceb8f3e2b461ce693bf83bc1831b8 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 12 May 2019 23:55:16 -0700 Subject: [PATCH 030/232] Remove badges from README [skipci] (#23815) --- README.rst | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/README.rst b/README.rst index 941a463fb37..08f20778d70 100644 --- a/README.rst +++ b/README.rst @@ -1,4 +1,4 @@ -Home Assistant |Build Status| |CI Status| |Coverage Status| |Chat Status| +Home Assistant |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,12 +27,6 @@ components `__ of our website for further help and information. -.. |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 :target: https://discord.gg/c5DvZ4e .. |screenshot-states| image:: https://raw.github.com/home-assistant/home-assistant/master/docs/screenshots.png From b8cbd39985ee204dc15b72d32a9573111986b06b Mon Sep 17 00:00:00 2001 From: Jc2k Date: Mon, 13 May 2019 07:56:05 +0100 Subject: [PATCH 031/232] HomeKit Controller: Adopt config entries for pairing with homekit accessories (#23825) * Fix user initiated pairing + show more user friendly name * Add lock around async_refresh_entity_map * Migrate homekit_controller to config entries. * Improve docstring Co-Authored-By: Martin Hjelmare * Add dummy async_setup_platform * add_service -> async_add_service * Add missing returns * Enable coverage checks for homekit_controller --- .coveragerc | 1 - .../components/discovery/__init__.py | 2 +- .../components/homekit_controller/__init__.py | 70 ++----- .../homekit_controller/alarm_control_panel.py | 26 ++- .../homekit_controller/binary_sensor.py | 24 ++- .../components/homekit_controller/climate.py | 22 +- .../homekit_controller/config_flow.py | 30 ++- .../homekit_controller/connection.py | 192 +++++++----------- .../components/homekit_controller/cover.py | 34 +++- .../components/homekit_controller/light.py | 24 ++- .../components/homekit_controller/lock.py | 25 ++- .../homekit_controller/manifest.json | 2 +- .../components/homekit_controller/sensor.py | 40 ++-- .../components/homekit_controller/switch.py | 24 ++- homeassistant/config_entries.py | 1 + tests/components/homekit_controller/common.py | 51 +++-- .../specific_devices/test_ecobee3.py | 17 +- .../homekit_controller/test_config_flow.py | 22 +- .../homekit_controller/test_cover.py | 14 -- 19 files changed, 334 insertions(+), 287 deletions(-) diff --git a/.coveragerc b/.coveragerc index 2b5f328466c..ffe89740432 100644 --- a/.coveragerc +++ b/.coveragerc @@ -250,7 +250,6 @@ omit = homeassistant/components/hitron_coda/device_tracker.py homeassistant/components/hive/* homeassistant/components/hlk_sw16/* - homeassistant/components/homekit_controller/* homeassistant/components/homematic/* homeassistant/components/homematic/climate.py homeassistant/components/homematic/cover.py diff --git a/homeassistant/components/discovery/__init__.py b/homeassistant/components/discovery/__init__.py index 900cbda74d4..99879b60e66 100644 --- a/homeassistant/components/discovery/__init__.py +++ b/homeassistant/components/discovery/__init__.py @@ -62,6 +62,7 @@ CONFIG_ENTRY_HANDLERS = { SERVICE_IKEA_TRADFRI: 'tradfri', 'sonos': 'sonos', SERVICE_IGD: 'upnp', + SERVICE_HOMEKIT: 'homekit_controller', } SERVICE_HANDLERS = { @@ -101,7 +102,6 @@ SERVICE_HANDLERS = { } OPTIONAL_SERVICE_HANDLERS = { - SERVICE_HOMEKIT: ('homekit_controller', None), SERVICE_DLNA_DMR: ('media_player', 'dlna_dmr'), } diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index 1b1c7b96b58..f5e61c6060f 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -1,11 +1,11 @@ """Support for Homekit device discovery.""" import logging -from homeassistant.components.discovery import SERVICE_HOMEKIT -from homeassistant.helpers import discovery from homeassistant.helpers.entity import Entity +from homeassistant.exceptions import ConfigEntryNotReady -from .config_flow import load_old_pairings +# We need an import from .config_flow, without it .config_flow is never loaded. +from .config_flow import HomekitControllerFlowHandler # noqa: F401 from .connection import get_accessory_information, HKDevice from .const import ( CONTROLLER, ENTITY_MAP, KNOWN_DEVICES @@ -13,12 +13,6 @@ from .const import ( from .const import DOMAIN # noqa: pylint: disable=unused-import from .storage import EntityMapStorage -HOMEKIT_IGNORE = [ - 'BSB002', - 'Home Assistant Bridge', - 'TRADFRI gateway', -] - _LOGGER = logging.getLogger(__name__) @@ -150,61 +144,29 @@ class HomeKitEntity(Entity): raise NotImplementedError +async def async_setup_entry(hass, entry): + """Set up a HomeKit connection on a config entry.""" + conn = HKDevice(hass, entry, entry.data) + hass.data[KNOWN_DEVICES][conn.unique_id] = conn + + if not await conn.async_setup(): + del hass.data[KNOWN_DEVICES][conn.unique_id] + raise ConfigEntryNotReady + + return True + + async def async_setup(hass, config): """Set up for Homekit devices.""" # pylint: disable=import-error import homekit - from homekit.controller.ip_implementation import IpPairing map_storage = hass.data[ENTITY_MAP] = EntityMapStorage(hass) await map_storage.async_initialize() - hass.data[CONTROLLER] = controller = homekit.Controller() - - old_pairings = await hass.async_add_executor_job( - load_old_pairings, - hass - ) - for hkid, pairing_data in old_pairings.items(): - controller.pairings[hkid] = IpPairing(pairing_data) - - def discovery_dispatch(service, discovery_info): - """Dispatcher for Homekit discovery events.""" - # model, id - host = discovery_info['host'] - port = discovery_info['port'] - - # Fold property keys to lower case, making them effectively - # case-insensitive. Some HomeKit devices capitalize them. - properties = { - key.lower(): value - for (key, value) in discovery_info['properties'].items() - } - - model = properties['md'] - hkid = properties['id'] - config_num = int(properties['c#']) - - if model in HOMEKIT_IGNORE: - return - - # Only register a device once, but rescan if the config has changed - if hkid in hass.data[KNOWN_DEVICES]: - device = hass.data[KNOWN_DEVICES][hkid] - if config_num > device.config_num and \ - device.pairing is not None: - device.refresh_entity_map(config_num) - return - - _LOGGER.debug('Discovered unique device %s', hkid) - device = HKDevice(hass, host, port, model, hkid, config_num, config) - device.setup() - + hass.data[CONTROLLER] = homekit.Controller() hass.data[KNOWN_DEVICES] = {} - await hass.async_add_executor_job( - discovery.listen, hass, SERVICE_HOMEKIT, discovery_dispatch) - return True diff --git a/homeassistant/components/homekit_controller/alarm_control_panel.py b/homeassistant/components/homekit_controller/alarm_control_panel.py index fe15cfe2eab..93279bd626e 100644 --- a/homeassistant/components/homekit_controller/alarm_control_panel.py +++ b/homeassistant/components/homekit_controller/alarm_control_panel.py @@ -28,13 +28,25 @@ TARGET_STATE_MAP = { } -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up Homekit Alarm Control Panel support.""" - if discovery_info is None: - return - accessory = hass.data[KNOWN_DEVICES][discovery_info['serial']] - add_entities([HomeKitAlarmControlPanel(accessory, discovery_info)], - True) +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): + """Legacy set up platform.""" + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up Homekit alarm control panel.""" + hkid = config_entry.data['AccessoryPairingID'] + conn = hass.data[KNOWN_DEVICES][hkid] + + def async_add_service(aid, service): + if service['stype'] != 'security-system': + return False + info = {'aid': aid, 'iid': service['iid']} + async_add_entities([HomeKitAlarmControlPanel(conn, info)], True) + return True + + conn.add_listener(async_add_service) class HomeKitAlarmControlPanel(HomeKitEntity, AlarmControlPanel): diff --git a/homeassistant/components/homekit_controller/binary_sensor.py b/homeassistant/components/homekit_controller/binary_sensor.py index a5b70082002..b9922ea43bb 100644 --- a/homeassistant/components/homekit_controller/binary_sensor.py +++ b/homeassistant/components/homekit_controller/binary_sensor.py @@ -8,11 +8,25 @@ from . import KNOWN_DEVICES, HomeKitEntity _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up Homekit motion sensor support.""" - if discovery_info is not None: - accessory = hass.data[KNOWN_DEVICES][discovery_info['serial']] - add_entities([HomeKitMotionSensor(accessory, discovery_info)], True) +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): + """Legacy set up platform.""" + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up Homekit lighting.""" + hkid = config_entry.data['AccessoryPairingID'] + conn = hass.data[KNOWN_DEVICES][hkid] + + def async_add_service(aid, service): + if service['stype'] != 'motion': + return False + info = {'aid': aid, 'iid': service['iid']} + async_add_entities([HomeKitMotionSensor(conn, info)], True) + return True + + conn.add_listener(async_add_service) class HomeKitMotionSensor(HomeKitEntity, BinarySensorDevice): diff --git a/homeassistant/components/homekit_controller/climate.py b/homeassistant/components/homekit_controller/climate.py index 4c299d1c7d0..c5a6ee0c3dc 100644 --- a/homeassistant/components/homekit_controller/climate.py +++ b/homeassistant/components/homekit_controller/climate.py @@ -26,11 +26,25 @@ MODE_HASS_TO_HOMEKIT = {v: k for k, v in MODE_HOMEKIT_TO_HASS.items()} DEFAULT_VALID_MODES = list(MODE_HOMEKIT_TO_HASS) -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): + """Legacy set up platform.""" + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Homekit climate.""" - if discovery_info is not None: - accessory = hass.data[KNOWN_DEVICES][discovery_info['serial']] - add_entities([HomeKitClimateDevice(accessory, discovery_info)], True) + hkid = config_entry.data['AccessoryPairingID'] + conn = hass.data[KNOWN_DEVICES][hkid] + + def async_add_service(aid, service): + if service['stype'] != 'thermostat': + return False + info = {'aid': aid, 'iid': service['iid']} + async_add_entities([HomeKitClimateDevice(conn, info)], True) + return True + + conn.add_listener(async_add_service) class HomeKitClimateDevice(HomeKitEntity, ClimateDevice): diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index 197d15116b1..6c534bb0c64 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -78,9 +78,8 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow): if user_input is not None: key = user_input['device'] - props = self.devices[key]['properties'] - self.hkid = props['id'] - self.model = props['md'] + self.hkid = self.devices[key]['id'] + self.model = self.devices[key]['md'] return await self.async_step_pair() controller = homekit.Controller() @@ -90,11 +89,11 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow): self.devices = {} for host in all_hosts: - status_flags = int(host['properties']['sf']) + status_flags = int(host['sf']) paired = not status_flags & 0x01 if paired: continue - self.devices[host['properties']['id']] = host + self.devices[host['name']] = host if not self.devices: return self.async_abort( @@ -263,13 +262,26 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow): async def _entry_from_accessory(self, pairing): """Return a config entry from an initialized bridge.""" - accessories = await self.hass.async_add_executor_job( - pairing.list_accessories_and_characteristics - ) + # The bulk of the pairing record is stored on the config entry. + # A specific exception is the 'accessories' key. This is more + # volatile. We do cache it, but not against the config entry. + # So copy the pairing data and mutate the copy. + pairing_data = pairing.pairing_data.copy() + + # Use the accessories data from the pairing operation if it is + # available. Otherwise request a fresh copy from the API. + # This removes the 'accessories' key from pairing_data at + # the same time. + accessories = pairing_data.pop('accessories', None) + if not accessories: + accessories = await self.hass.async_add_executor_job( + pairing.list_accessories_and_characteristics + ) + bridge_info = get_bridge_information(accessories) name = get_accessory_name(bridge_info) return self.async_create_entry( title=name, - data=pairing.pairing_data, + data=pairing_data, ) diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py index af438c68164..080d6034237 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -1,14 +1,8 @@ """Helpers for managing a pairing with a HomeKit accessory or bridge.""" import asyncio import logging -import os -from homeassistant.helpers import discovery - -from .const import ( - CONTROLLER, DOMAIN, HOMEKIT_ACCESSORY_DISPATCH, KNOWN_DEVICES, - PAIRING_FILE, HOMEKIT_DIR, ENTITY_MAP -) +from .const import HOMEKIT_ACCESSORY_DISPATCH, ENTITY_MAP RETRY_INTERVAL = 60 # seconds @@ -53,75 +47,69 @@ def get_accessory_name(accessory_info): class HKDevice(): """HomeKit device.""" - def __init__(self, hass, host, port, model, hkid, config_num, config): + def __init__(self, hass, config_entry, pairing_data): """Initialise a generic HomeKit device.""" - _LOGGER.info("Setting up Homekit device %s", model) - self.hass = hass - self.controller = hass.data[CONTROLLER] + from homekit.controller.ip_implementation import IpPairing + + self.hass = hass + self.config_entry = config_entry + + # We copy pairing_data because homekit_python may mutate it, but we + # don't want to mutate a dict owned by a config entry. + self.pairing_data = pairing_data.copy() + + self.pairing = IpPairing(self.pairing_data) - self.host = host - self.port = port - self.model = model - self.hkid = hkid - self.config_num = config_num - self.config = config - self.configurator = hass.components.configurator self.accessories = {} + self.config_num = 0 + + # A list of callbacks that turn HK service metadata into entities + self.listeners = [] + + # The platorms we have forwarded the config entry so far. If a new + # accessory is added to a bridge we may have to load additional + # platforms. We don't want to load all platforms up front if its just + # a lightbulb. And we dont want to forward a config entry twice + # (triggers a Config entry already set up error) + self.platforms = set() # This just tracks aid/iid pairs so we know if a HK service has been # mapped to a HA entity. self.entities = [] + # There are multiple entities sharing a single connection - only + # allow one entity to use pairing at once. self.pairing_lock = asyncio.Lock(loop=hass.loop) - self.pairing = self.controller.pairings.get(hkid) - - hass.data[KNOWN_DEVICES][hkid] = self - - def setup(self): + async def async_setup(self): """Prepare to use a paired HomeKit device in homeassistant.""" - if self.pairing is None: - self.configure() - return - - self.pairing.pairing_data['AccessoryIP'] = self.host - self.pairing.pairing_data['AccessoryPort'] = self.port - cache = self.hass.data[ENTITY_MAP].get_map(self.unique_id) - if not cache or cache['config_num'] < self.config_num: - return self.refresh_entity_map(self.config_num) + if not cache: + return await self.async_refresh_entity_map(self.config_num) self.accessories = cache['accessories'] + self.config_num = cache['config_num'] # Ensure the Pairing object has access to the latest version of the # entity map. self.pairing.pairing_data['accessories'] = self.accessories + self.async_load_platforms() + self.add_entities() return True - def refresh_entity_map(self, config_num): - """ - Handle setup of a HomeKit accessory. - - The sync version will be removed when homekit_controller migrates to - config flow. - """ - self.hass.add_job( - self.async_refresh_entity_map, - config_num, - ) - async def async_refresh_entity_map(self, config_num): """Handle setup of a HomeKit accessory.""" # pylint: disable=import-error from homekit.exceptions import AccessoryDisconnectedError try: - self.accessories = await self.hass.async_add_executor_job( - self.pairing.list_accessories_and_characteristics, - ) + async with self.pairing_lock: + self.accessories = await self.hass.async_add_executor_job( + self.pairing.list_accessories_and_characteristics + ) except AccessoryDisconnectedError: # If we fail to refresh this data then we will naturally retry # later when Bonjour spots c# is still not up to date. @@ -139,94 +127,62 @@ class HKDevice(): # aid/iid to GATT characteristics. So push it to there as well. self.pairing.pairing_data['accessories'] = self.accessories - # Register add new entities that are available - await self.hass.async_add_executor_job(self.add_entities) + self.async_load_platforms() + + # Register and add new entities that are available + self.add_entities() return True + def add_listener(self, add_entities_cb): + """Add a callback to run when discovering new entities.""" + self.listeners.append(add_entities_cb) + self._add_new_entities([add_entities_cb]) + def add_entities(self): """Process the entity map and create HA entities.""" - # pylint: disable=import-error + self._add_new_entities(self.listeners) + + def _add_new_entities(self, callbacks): from homekit.model.services import ServicesTypes for accessory in self.accessories: aid = accessory['aid'] for service in accessory['services']: iid = service['iid'] + stype = ServicesTypes.get_short(service['type'].upper()) + service['stype'] = stype + if (aid, iid) in self.entities: # Don't add the same entity again continue - devtype = ServicesTypes.get_short(service['type']) - _LOGGER.debug("Found %s", devtype) - service_info = {'serial': self.hkid, - 'aid': aid, - 'iid': service['iid'], - 'model': self.model, - 'device-type': devtype} - component = HOMEKIT_ACCESSORY_DISPATCH.get(devtype, None) - if component is not None: - discovery.load_platform(self.hass, component, DOMAIN, - service_info, self.config) - self.entities.append((aid, iid)) + for listener in callbacks: + if listener(aid, service): + self.entities.append((aid, iid)) + break - def device_config_callback(self, callback_data): - """Handle initial pairing.""" - import homekit # pylint: disable=import-error - code = callback_data.get('code').strip() - try: - self.controller.perform_pairing(self.hkid, self.hkid, code) - except homekit.UnavailableError: - error_msg = "This accessory is already paired to another device. \ - Please reset the accessory and try again." - _configurator = self.hass.data[DOMAIN+self.hkid] - self.configurator.notify_errors(_configurator, error_msg) - return - except homekit.AuthenticationError: - error_msg = "Incorrect HomeKit code for {}. Please check it and \ - try again.".format(self.model) - _configurator = self.hass.data[DOMAIN+self.hkid] - self.configurator.notify_errors(_configurator, error_msg) - return - except homekit.UnknownError: - error_msg = "Received an unknown error. Please file a bug." - _configurator = self.hass.data[DOMAIN+self.hkid] - self.configurator.notify_errors(_configurator, error_msg) - raise + def async_load_platforms(self): + """Load any platforms needed by this HomeKit device.""" + from homekit.model.services import ServicesTypes - self.pairing = self.controller.pairings.get(self.hkid) - if self.pairing is not None: - pairing_dir = os.path.join( - self.hass.config.path(), - HOMEKIT_DIR, - ) - if not os.path.exists(pairing_dir): - os.makedirs(pairing_dir) - pairing_file = os.path.join( - pairing_dir, - PAIRING_FILE, - ) - self.controller.save_data(pairing_file) - _configurator = self.hass.data[DOMAIN+self.hkid] - self.configurator.request_done(_configurator) - self.setup() - else: - error_msg = "Unable to pair, please try again" - _configurator = self.hass.data[DOMAIN+self.hkid] - self.configurator.notify_errors(_configurator, error_msg) + for accessory in self.accessories: + for service in accessory['services']: + stype = ServicesTypes.get_short(service['type'].upper()) + if stype not in HOMEKIT_ACCESSORY_DISPATCH: + continue - def configure(self): - """Obtain the pairing code for a HomeKit device.""" - description = "Please enter the HomeKit code for your {}".format( - self.model) - self.hass.data[DOMAIN+self.hkid] = \ - self.configurator.request_config(self.model, - self.device_config_callback, - description=description, - submit_caption="submit", - fields=[{'id': 'code', - 'name': 'HomeKit code', - 'type': 'string'}]) + platform = HOMEKIT_ACCESSORY_DISPATCH[stype] + if platform in self.platforms: + continue + + self.hass.async_create_task( + self.hass.config_entries.async_forward_entry_setup( + self.config_entry, + platform, + ) + ) + self.platforms.add(platform) async def get_characteristics(self, *args, **kwargs): """Read latest state from homekit accessory.""" @@ -261,4 +217,4 @@ class HKDevice(): This id is random and will change if a device undergoes a hard reset. """ - return self.hkid + return self.pairing_data['AccessoryPairingID'] diff --git a/homeassistant/components/homekit_controller/cover.py b/homeassistant/components/homekit_controller/cover.py index bd466d074d0..7f3761d33a4 100644 --- a/homeassistant/components/homekit_controller/cover.py +++ b/homeassistant/components/homekit_controller/cover.py @@ -35,18 +35,30 @@ CURRENT_WINDOW_STATE_MAP = { } -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up HomeKit Cover support.""" - if discovery_info is None: - return - accessory = hass.data[KNOWN_DEVICES][discovery_info['serial']] +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): + """Legacy set up platform.""" + pass - if discovery_info['device-type'] == 'garage-door-opener': - add_entities([HomeKitGarageDoorCover(accessory, discovery_info)], - True) - else: - add_entities([HomeKitWindowCover(accessory, discovery_info)], - True) + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up Homekit covers.""" + hkid = config_entry.data['AccessoryPairingID'] + conn = hass.data[KNOWN_DEVICES][hkid] + + def async_add_service(aid, service): + info = {'aid': aid, 'iid': service['iid']} + if service['stype'] == 'garage-door-opener': + async_add_entities([HomeKitGarageDoorCover(conn, info)], True) + return True + + if service['stype'] in ('window-covering', 'window'): + async_add_entities([HomeKitWindowCover(conn, info)], True) + return True + + return False + + conn.add_listener(async_add_service) class HomeKitGarageDoorCover(HomeKitEntity, CoverDevice): diff --git a/homeassistant/components/homekit_controller/light.py b/homeassistant/components/homekit_controller/light.py index a139b1f2932..248412c91a3 100644 --- a/homeassistant/components/homekit_controller/light.py +++ b/homeassistant/components/homekit_controller/light.py @@ -10,11 +10,25 @@ from . import KNOWN_DEVICES, HomeKitEntity _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up Homekit lighting.""" - if discovery_info is not None: - accessory = hass.data[KNOWN_DEVICES][discovery_info['serial']] - add_entities([HomeKitLight(accessory, discovery_info)], True) +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): + """Legacy set up platform.""" + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up Homekit lightbulb.""" + hkid = config_entry.data['AccessoryPairingID'] + conn = hass.data[KNOWN_DEVICES][hkid] + + def async_add_service(aid, service): + if service['stype'] != 'lightbulb': + return False + info = {'aid': aid, 'iid': service['iid']} + async_add_entities([HomeKitLight(conn, info)], True) + return True + + conn.add_listener(async_add_service) class HomeKitLight(HomeKitEntity, Light): diff --git a/homeassistant/components/homekit_controller/lock.py b/homeassistant/components/homekit_controller/lock.py index 67de2bfaf3f..1449f265245 100644 --- a/homeassistant/components/homekit_controller/lock.py +++ b/homeassistant/components/homekit_controller/lock.py @@ -24,12 +24,25 @@ TARGET_STATE_MAP = { } -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up Homekit Lock support.""" - if discovery_info is None: - return - accessory = hass.data[KNOWN_DEVICES][discovery_info['serial']] - add_entities([HomeKitLock(accessory, discovery_info)], True) +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): + """Legacy set up platform.""" + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up Homekit lock.""" + hkid = config_entry.data['AccessoryPairingID'] + conn = hass.data[KNOWN_DEVICES][hkid] + + def async_add_service(aid, service): + if service['stype'] != 'lock-mechanism': + return False + info = {'aid': aid, 'iid': service['iid']} + async_add_entities([HomeKitLock(conn, info)], True) + return True + + conn.add_listener(async_add_service) class HomeKitLock(HomeKitEntity, LockDevice): diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index c1b923a5677..53476b8ba6d 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -5,7 +5,7 @@ "requirements": [ "homekit[IP]==0.14.0" ], - "dependencies": ["configurator"], + "dependencies": [], "codeowners": [ "@Jc2k" ] diff --git a/homeassistant/components/homekit_controller/sensor.py b/homeassistant/components/homekit_controller/sensor.py index b377da80142..f6f450b2b01 100644 --- a/homeassistant/components/homekit_controller/sensor.py +++ b/homeassistant/components/homekit_controller/sensor.py @@ -11,21 +11,35 @@ UNIT_PERCENT = "%" UNIT_LUX = "lux" -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up Homekit sensor support.""" - if discovery_info is not None: - accessory = hass.data[KNOWN_DEVICES][discovery_info['serial']] - devtype = discovery_info['device-type'] +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): + """Legacy set up platform.""" + pass + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up Homekit covers.""" + hkid = config_entry.data['AccessoryPairingID'] + conn = hass.data[KNOWN_DEVICES][hkid] + + def async_add_service(aid, service): + devtype = service['stype'] + info = {'aid': aid, 'iid': service['iid']} if devtype == 'humidity': - add_entities( - [HomeKitHumiditySensor(accessory, discovery_info)], True) - elif devtype == 'temperature': - add_entities( - [HomeKitTemperatureSensor(accessory, discovery_info)], True) - elif devtype == 'light': - add_entities( - [HomeKitLightSensor(accessory, discovery_info)], True) + async_add_entities([HomeKitHumiditySensor(conn, info)], True) + return True + + if devtype == 'temperature': + async_add_entities([HomeKitTemperatureSensor(conn, info)], True) + return True + + if devtype == 'light': + async_add_entities([HomeKitLightSensor(conn, info)], True) + return True + + return False + + conn.add_listener(async_add_service) class HomeKitHumiditySensor(HomeKitEntity): diff --git a/homeassistant/components/homekit_controller/switch.py b/homeassistant/components/homekit_controller/switch.py index c09502373a6..670ddd4db5b 100644 --- a/homeassistant/components/homekit_controller/switch.py +++ b/homeassistant/components/homekit_controller/switch.py @@ -10,11 +10,25 @@ OUTLET_IN_USE = "outlet_in_use" _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up Homekit switch support.""" - if discovery_info is not None: - accessory = hass.data[KNOWN_DEVICES][discovery_info['serial']] - add_entities([HomeKitSwitch(accessory, discovery_info)], True) +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): + """Legacy set up platform.""" + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up Homekit lock.""" + hkid = config_entry.data['AccessoryPairingID'] + conn = hass.data[KNOWN_DEVICES][hkid] + + def async_add_service(aid, service): + if service['stype'] not in ('switch', 'outlet'): + return False + info = {'aid': aid, 'iid': service['iid']} + async_add_entities([HomeKitSwitch(conn, info)], True) + return True + + conn.add_listener(async_add_service) class HomeKitSwitch(HomeKitEntity, SwitchDevice): diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 593b402a3fd..6f4e57203f1 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -155,6 +155,7 @@ FLOWS = [ 'gpslogger', 'hangouts', 'heos', + 'homekit_controller', 'homematicip_cloud', 'hue', 'ifttt', diff --git a/tests/components/homekit_controller/common.py b/tests/components/homekit_controller/common.py index 8635e0b6d05..87482f8e92c 100644 --- a/tests/components/homekit_controller/common.py +++ b/tests/components/homekit_controller/common.py @@ -9,13 +9,15 @@ from homekit.model.characteristics import ( AbstractCharacteristic, CharacteristicPermissions, CharacteristicsTypes) from homekit.model import Accessory, get_id from homekit.exceptions import AccessoryNotFoundError -from homeassistant.components.homekit_controller import SERVICE_HOMEKIT + +from homeassistant import config_entries from homeassistant.components.homekit_controller.const import ( CONTROLLER, DOMAIN, HOMEKIT_ACCESSORY_DISPATCH) +from homeassistant.components.homekit_controller import ( + async_setup_entry, config_flow) from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.common import ( - async_fire_time_changed, async_fire_service_discovered, load_fixture) +from tests.common import async_fire_time_changed, load_fixture class FakePairing: @@ -217,26 +219,36 @@ async def setup_platform(hass): return fake_controller -async def setup_test_accessories(hass, accessories, capitalize=False): - """Load a fake homekit accessory based on a homekit accessory model. - - If capitalize is True, property names will be in upper case. - """ +async def setup_test_accessories(hass, accessories): + """Load a fake homekit device based on captured JSON profile.""" fake_controller = await setup_platform(hass) pairing = fake_controller.add(accessories) discovery_info = { + 'name': 'TestDevice', 'host': '127.0.0.1', 'port': 8080, 'properties': { - ('MD' if capitalize else 'md'): 'TestDevice', - ('ID' if capitalize else 'id'): '00:00:00:00:00:00', - ('C#' if capitalize else 'c#'): 1, + 'md': 'TestDevice', + 'id': '00:00:00:00:00:00', + 'c#': 1, } } - async_fire_service_discovered(hass, SERVICE_HOMEKIT, discovery_info) - await hass.async_block_till_done() + pairing.pairing_data.update({ + 'AccessoryPairingID': discovery_info['properties']['id'], + }) + + config_entry = config_entries.ConfigEntry( + 1, 'homekit_controller', 'TestData', pairing.pairing_data, + 'test', config_entries.CONN_CLASS_LOCAL_PUSH + ) + + pairing_cls_loc = 'homekit.controller.ip_implementation.IpPairing' + with mock.patch(pairing_cls_loc) as pairing_cls: + pairing_cls.return_value = pairing + await async_setup_entry(hass, config_entry) + await hass.async_block_till_done() return pairing @@ -249,6 +261,7 @@ async def device_config_changed(hass, accessories): pairing.accessories = accessories discovery_info = { + 'name': 'TestDevice', 'host': '127.0.0.1', 'port': 8080, 'properties': { @@ -259,7 +272,14 @@ async def device_config_changed(hass, accessories): } } - async_fire_service_discovered(hass, SERVICE_HOMEKIT, discovery_info) + # Config Flow will abort and notify us if the discovery event is of + # interest - in this case c# has incremented + flow = config_flow.HomekitControllerFlowHandler() + flow.hass = hass + flow.context = {} + result = await flow.async_step_discovery(discovery_info) + assert result['type'] == 'abort' + assert result['reason'] == 'already_configured' # Wait for services to reconfigure await hass.async_block_till_done() @@ -285,7 +305,6 @@ async def setup_test_component(hass, services, capitalize=False, suffix=None): accessory = Accessory('TestDevice', 'example.com', 'Test', '0001', '0.1') accessory.services.extend(services) - pairing = await setup_test_accessories(hass, [accessory], capitalize) - + pairing = await setup_test_accessories(hass, [accessory]) entity = 'testdevice' if suffix is None else 'testdevice_{}'.format(suffix) return Helper(hass, '.'.join((domain, entity)), pairing, accessory) diff --git a/tests/components/homekit_controller/specific_devices/test_ecobee3.py b/tests/components/homekit_controller/specific_devices/test_ecobee3.py index 23d0a32f7ad..166ef32784b 100644 --- a/tests/components/homekit_controller/specific_devices/test_ecobee3.py +++ b/tests/components/homekit_controller/specific_devices/test_ecobee3.py @@ -7,11 +7,15 @@ https://github.com/home-assistant/home-assistant/issues/15336 from unittest import mock from homekit import AccessoryDisconnectedError +import pytest +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.components.climate.const import ( SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_HUMIDITY, SUPPORT_TARGET_HUMIDITY_HIGH, SUPPORT_TARGET_HUMIDITY_LOW, SUPPORT_OPERATION_MODE) + + from tests.components.homekit_controller.common import ( FakePairing, device_config_changed, setup_accessories_from_file, setup_test_accessories, Helper @@ -110,14 +114,19 @@ async def test_ecobee3_setup_connection_failure(hass): list_accessories = 'list_accessories_and_characteristics' with mock.patch.object(FakePairing, list_accessories) as laac: laac.side_effect = AccessoryDisconnectedError('Connection failed') - await setup_test_accessories(hass, accessories) + + # If there is no cached entity map and the accessory connection is + # failing then we have to fail the config entry setup. + with pytest.raises(ConfigEntryNotReady): + await setup_test_accessories(hass, accessories) climate = entity_registry.async_get('climate.homew') assert climate is None - # When a regular discovery event happens it should trigger another scan - # which should cause our entities to be added. - await device_config_changed(hass, accessories) + # When accessory raises ConfigEntryNoteReady HA will retry - lets make + # sure there is no cruft causing conflicts left behind by now doing + # a successful setup. + await setup_test_accessories(hass, accessories) climate = entity_registry.async_get('climate.homew') assert climate.unique_id == 'homekit-123456789012-16' diff --git a/tests/components/homekit_controller/test_config_flow.py b/tests/components/homekit_controller/test_config_flow.py index c8b81a88478..33160abaa55 100644 --- a/tests/components/homekit_controller/test_config_flow.py +++ b/tests/components/homekit_controller/test_config_flow.py @@ -627,12 +627,10 @@ async def test_user_works(hass): 'name': 'TestDevice', 'host': '127.0.0.1', 'port': 8080, - 'properties': { - 'md': 'TestDevice', - 'id': '00:00:00:00:00:00', - 'c#': 1, - 'sf': 1, - } + 'md': 'TestDevice', + 'id': '00:00:00:00:00:00', + 'c#': 1, + 'sf': 1, } pairing = mock.Mock(pairing_data={ @@ -666,7 +664,7 @@ async def test_user_works(hass): assert result['step_id'] == 'user' result = await flow.async_step_user({ - 'device': '00:00:00:00:00:00', + 'device': 'TestDevice', }) assert result['type'] == 'form' assert result['step_id'] == 'pair' @@ -701,12 +699,10 @@ async def test_user_no_unpaired_devices(hass): 'name': 'TestDevice', 'host': '127.0.0.1', 'port': 8080, - 'properties': { - 'md': 'TestDevice', - 'id': '00:00:00:00:00:00', - 'c#': 1, - 'sf': 0, - } + 'md': 'TestDevice', + 'id': '00:00:00:00:00:00', + 'c#': 1, + 'sf': 0, } with mock.patch('homekit.Controller') as controller_cls: diff --git a/tests/components/homekit_controller/test_cover.py b/tests/components/homekit_controller/test_cover.py index 19ccc21b7e8..66d4505d6fb 100644 --- a/tests/components/homekit_controller/test_cover.py +++ b/tests/components/homekit_controller/test_cover.py @@ -71,20 +71,6 @@ def create_window_covering_service_with_v_tilt(): return service -async def test_accept_capitalized_property_names(hass, utcnow): - """Test that we can handle a device with capitalized property names.""" - window_cover = create_window_covering_service() - helper = await setup_test_component(hass, [window_cover], capitalize=True) - - # The specific interaction we do here doesn't matter; we just need - # to do *something* to ensure that discovery properly dealt with the - # capitalized property names. - await hass.services.async_call('cover', 'open_cover', { - 'entity_id': helper.entity_id, - }, blocking=True) - assert helper.characteristics[POSITION_TARGET].value == 100 - - async def test_change_window_cover_state(hass, utcnow): """Test that we can turn a HomeKit alarm on and off again.""" window_cover = create_window_covering_service() From 1e22c8dacac252e41c152b1b0ce1b9d47ba8c7ec Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 13 May 2019 01:16:55 -0700 Subject: [PATCH 032/232] Automatically generate config flow list (#23802) * Add config flow to manifest.json * Still load config flows via config flow platform * Fix typo * Lint * Update config_flows.py" * Catch import error when setting up entry * Lint * Fix tests * Fix imports * Lint * Fix Unifi tests * Fix translation test * Add homekit_controller config flow --- .../components/ambiclimate/manifest.json | 1 + .../components/ambient_station/manifest.json | 1 + homeassistant/components/axis/manifest.json | 1 + homeassistant/components/cast/__init__.py | 15 +- homeassistant/components/cast/config_flow.py | 16 +++ homeassistant/components/cast/const.py | 3 + homeassistant/components/cast/manifest.json | 1 + .../components/config/config_entries.py | 3 +- homeassistant/components/daikin/manifest.json | 1 + homeassistant/components/deconz/manifest.json | 1 + .../components/dialogflow/__init__.py | 15 +- .../components/dialogflow/config_flow.py | 13 ++ homeassistant/components/dialogflow/const.py | 3 + .../components/dialogflow/manifest.json | 1 + .../components/emulated_roku/manifest.json | 1 + .../components/esphome/manifest.json | 1 + homeassistant/components/geofency/__init__.py | 12 +- .../components/geofency/config_flow.py | 12 ++ homeassistant/components/geofency/const.py | 3 + .../components/geofency/manifest.json | 1 + .../components/gpslogger/__init__.py | 11 +- .../components/gpslogger/config_flow.py | 12 ++ homeassistant/components/gpslogger/const.py | 3 + .../components/gpslogger/manifest.json | 1 + .../components/hangouts/manifest.json | 1 + homeassistant/components/heos/manifest.json | 1 + .../homekit_controller/manifest.json | 1 + .../homematicip_cloud/manifest.json | 1 + homeassistant/components/hue/manifest.json | 1 + homeassistant/components/ifttt/__init__.py | 13 +- homeassistant/components/ifttt/config_flow.py | 13 ++ homeassistant/components/ifttt/const.py | 3 + homeassistant/components/ifttt/manifest.json | 1 + homeassistant/components/ios/__init__.py | 8 +- homeassistant/components/ios/config_flow.py | 9 ++ homeassistant/components/ios/const.py | 3 + homeassistant/components/ios/manifest.json | 1 + homeassistant/components/ipma/manifest.json | 1 + homeassistant/components/iqvia/manifest.json | 1 + homeassistant/components/lifx/__init__.py | 16 +-- homeassistant/components/lifx/config_flow.py | 16 +++ homeassistant/components/lifx/const.py | 3 + homeassistant/components/lifx/manifest.json | 1 + homeassistant/components/locative/__init__.py | 9 -- .../components/locative/config_flow.py | 12 ++ homeassistant/components/locative/const.py | 3 + .../components/locative/manifest.json | 1 + .../components/logi_circle/manifest.json | 1 + .../components/luftdaten/manifest.json | 1 + homeassistant/components/mailgun/__init__.py | 14 +- .../components/mailgun/config_flow.py | 13 ++ homeassistant/components/mailgun/const.py | 3 + .../components/mailgun/manifest.json | 1 + .../components/mobile_app/__init__.py | 24 ---- .../components/mobile_app/config_flow.py | 26 ++++ .../components/mobile_app/manifest.json | 1 + homeassistant/components/mqtt/manifest.json | 1 + homeassistant/components/nest/manifest.json | 1 + homeassistant/components/openuv/manifest.json | 1 + .../components/owntracks/manifest.json | 1 + homeassistant/components/point/manifest.json | 1 + homeassistant/components/ps4/manifest.json | 1 + .../components/rainmachine/manifest.json | 1 + .../components/simplisafe/manifest.json | 1 + .../components/smartthings/manifest.json | 1 + homeassistant/components/smhi/manifest.json | 1 + homeassistant/components/sonos/__init__.py | 16 +-- homeassistant/components/sonos/config_flow.py | 15 ++ homeassistant/components/sonos/const.py | 3 + homeassistant/components/sonos/manifest.json | 1 + .../components/tellduslive/manifest.json | 1 + homeassistant/components/toon/manifest.json | 1 + homeassistant/components/tplink/__init__.py | 24 +--- .../components/tplink/config_flow.py | 20 +++ homeassistant/components/tplink/const.py | 3 + homeassistant/components/tplink/manifest.json | 1 + .../components/tradfri/manifest.json | 1 + homeassistant/components/twilio/__init__.py | 14 +- .../components/twilio/config_flow.py | 15 ++ homeassistant/components/twilio/const.py | 3 + homeassistant/components/twilio/manifest.json | 1 + homeassistant/components/unifi/__init__.py | 130 +----------------- homeassistant/components/unifi/config_flow.py | 130 ++++++++++++++++++ homeassistant/components/unifi/manifest.json | 1 + homeassistant/components/upnp/__init__.py | 8 -- homeassistant/components/upnp/config_flow.py | 13 ++ homeassistant/components/upnp/manifest.json | 1 + homeassistant/components/zha/manifest.json | 1 + homeassistant/components/zone/manifest.json | 1 + homeassistant/components/zwave/manifest.json | 1 + homeassistant/config_entries.py | 76 +++------- homeassistant/generated/config_flows.py | 55 ++++++++ homeassistant/helpers/translation.py | 4 +- pylintrc | 6 + script/hassfest/__main__.py | 3 +- script/hassfest/config_flow.py | 85 ++++++++++++ script/hassfest/manifest.py | 1 + script/hassfest/model.py | 8 ++ tests/common.py | 2 +- .../components/config/test_config_entries.py | 20 ++- tests/components/unifi/test_controller.py | 27 ++-- tests/components/unifi/test_init.py | 97 ++++++------- tests/components/unifi/test_switch.py | 20 +-- tests/helpers/test_config_entry_flow.py | 8 +- tests/helpers/test_translation.py | 4 +- tests/test_config_entries.py | 16 +++ 106 files changed, 742 insertions(+), 440 deletions(-) create mode 100644 homeassistant/components/cast/config_flow.py create mode 100644 homeassistant/components/cast/const.py create mode 100644 homeassistant/components/dialogflow/config_flow.py create mode 100644 homeassistant/components/dialogflow/const.py create mode 100644 homeassistant/components/geofency/config_flow.py create mode 100644 homeassistant/components/geofency/const.py create mode 100644 homeassistant/components/gpslogger/config_flow.py create mode 100644 homeassistant/components/gpslogger/const.py create mode 100644 homeassistant/components/ifttt/config_flow.py create mode 100644 homeassistant/components/ifttt/const.py create mode 100644 homeassistant/components/ios/config_flow.py create mode 100644 homeassistant/components/ios/const.py create mode 100644 homeassistant/components/lifx/config_flow.py create mode 100644 homeassistant/components/lifx/const.py create mode 100644 homeassistant/components/locative/config_flow.py create mode 100644 homeassistant/components/locative/const.py create mode 100644 homeassistant/components/mailgun/config_flow.py create mode 100644 homeassistant/components/mailgun/const.py create mode 100644 homeassistant/components/mobile_app/config_flow.py create mode 100644 homeassistant/components/sonos/config_flow.py create mode 100644 homeassistant/components/sonos/const.py create mode 100644 homeassistant/components/tplink/config_flow.py create mode 100644 homeassistant/components/tplink/const.py create mode 100644 homeassistant/components/twilio/config_flow.py create mode 100644 homeassistant/components/twilio/const.py create mode 100644 homeassistant/components/unifi/config_flow.py create mode 100644 homeassistant/components/upnp/config_flow.py create mode 100644 homeassistant/generated/config_flows.py create mode 100644 script/hassfest/config_flow.py diff --git a/homeassistant/components/ambiclimate/manifest.json b/homeassistant/components/ambiclimate/manifest.json index f3b3450f163..bd1117d86bc 100644 --- a/homeassistant/components/ambiclimate/manifest.json +++ b/homeassistant/components/ambiclimate/manifest.json @@ -1,6 +1,7 @@ { "domain": "ambiclimate", "name": "Ambiclimate", + "config_flow": true, "documentation": "https://www.home-assistant.io/components/ambiclimate", "requirements": [ "ambiclimate==0.1.1" diff --git a/homeassistant/components/ambient_station/manifest.json b/homeassistant/components/ambient_station/manifest.json index 11d2ad3668e..3e9bbf6a5b8 100644 --- a/homeassistant/components/ambient_station/manifest.json +++ b/homeassistant/components/ambient_station/manifest.json @@ -1,6 +1,7 @@ { "domain": "ambient_station", "name": "Ambient station", + "config_flow": true, "documentation": "https://www.home-assistant.io/components/ambient_station", "requirements": [ "aioambient==0.3.0" diff --git a/homeassistant/components/axis/manifest.json b/homeassistant/components/axis/manifest.json index f87718bfddd..0379ee3b03c 100644 --- a/homeassistant/components/axis/manifest.json +++ b/homeassistant/components/axis/manifest.json @@ -1,6 +1,7 @@ { "domain": "axis", "name": "Axis", + "config_flow": true, "documentation": "https://www.home-assistant.io/components/axis", "requirements": ["axis==22"], "dependencies": [], diff --git a/homeassistant/components/cast/__init__.py b/homeassistant/components/cast/__init__.py index 1a93020c229..f91b90c1e08 100644 --- a/homeassistant/components/cast/__init__.py +++ b/homeassistant/components/cast/__init__.py @@ -1,8 +1,7 @@ """Component to embed Google Cast.""" from homeassistant import config_entries -from homeassistant.helpers import config_entry_flow -DOMAIN = 'cast' +from .const import DOMAIN async def async_setup(hass, config): @@ -23,15 +22,3 @@ async def async_setup_entry(hass, entry): hass.async_create_task(hass.config_entries.async_forward_entry_setup( entry, 'media_player')) return True - - -async def _async_has_devices(hass): - """Return if there are devices that can be discovered.""" - from pychromecast.discovery import discover_chromecasts - - return await hass.async_add_executor_job(discover_chromecasts) - - -config_entry_flow.register_discovery_flow( - DOMAIN, 'Google Cast', _async_has_devices, - config_entries.CONN_CLASS_LOCAL_PUSH) diff --git a/homeassistant/components/cast/config_flow.py b/homeassistant/components/cast/config_flow.py new file mode 100644 index 00000000000..0f8696cf29c --- /dev/null +++ b/homeassistant/components/cast/config_flow.py @@ -0,0 +1,16 @@ +"""Config flow for Cast.""" +from homeassistant.helpers import config_entry_flow +from homeassistant import config_entries +from .const import DOMAIN + + +async def _async_has_devices(hass): + """Return if there are devices that can be discovered.""" + from pychromecast.discovery import discover_chromecasts + + return await hass.async_add_executor_job(discover_chromecasts) + + +config_entry_flow.register_discovery_flow( + DOMAIN, 'Google Cast', _async_has_devices, + config_entries.CONN_CLASS_LOCAL_PUSH) diff --git a/homeassistant/components/cast/const.py b/homeassistant/components/cast/const.py new file mode 100644 index 00000000000..48bb87ca5d7 --- /dev/null +++ b/homeassistant/components/cast/const.py @@ -0,0 +1,3 @@ +"""Consts for Cast integration.""" + +DOMAIN = 'cast' diff --git a/homeassistant/components/cast/manifest.json b/homeassistant/components/cast/manifest.json index dd189ac91e7..2d310cdda8f 100644 --- a/homeassistant/components/cast/manifest.json +++ b/homeassistant/components/cast/manifest.json @@ -1,6 +1,7 @@ { "domain": "cast", "name": "Cast", + "config_flow": true, "documentation": "https://www.home-assistant.io/components/cast", "requirements": [ "pychromecast==3.2.1" diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index 8865ff39cea..45e1df5907c 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -6,6 +6,7 @@ from homeassistant.components.http import HomeAssistantView from homeassistant.exceptions import Unauthorized from homeassistant.helpers.data_entry_flow import ( FlowManagerIndexView, FlowManagerResourceView) +from homeassistant.generated import config_flows async def async_setup(hass): @@ -172,7 +173,7 @@ class ConfigManagerAvailableFlowView(HomeAssistantView): async def get(self, request): """List available flow handlers.""" - return self.json(config_entries.FLOWS) + return self.json(config_flows.FLOWS) class OptionManagerFlowIndexView(FlowManagerIndexView): diff --git a/homeassistant/components/daikin/manifest.json b/homeassistant/components/daikin/manifest.json index ab842950e24..9891cce3b3e 100644 --- a/homeassistant/components/daikin/manifest.json +++ b/homeassistant/components/daikin/manifest.json @@ -1,6 +1,7 @@ { "domain": "daikin", "name": "Daikin", + "config_flow": true, "documentation": "https://www.home-assistant.io/components/daikin", "requirements": [ "pydaikin==1.4.0" diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json index 22947d40fb1..0692bd444b8 100644 --- a/homeassistant/components/deconz/manifest.json +++ b/homeassistant/components/deconz/manifest.json @@ -1,6 +1,7 @@ { "domain": "deconz", "name": "Deconz", + "config_flow": true, "documentation": "https://www.home-assistant.io/components/deconz", "requirements": [ "pydeconz==58" diff --git a/homeassistant/components/dialogflow/__init__.py b/homeassistant/components/dialogflow/__init__.py index a6134d4b19c..3bf11a46098 100644 --- a/homeassistant/components/dialogflow/__init__.py +++ b/homeassistant/components/dialogflow/__init__.py @@ -8,9 +8,10 @@ from homeassistant.const import CONF_WEBHOOK_ID from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import intent, template, config_entry_flow -_LOGGER = logging.getLogger(__name__) +from .const import DOMAIN -DOMAIN = 'dialogflow' + +_LOGGER = logging.getLogger(__name__) SOURCE = "Home Assistant Dialogflow" @@ -83,16 +84,6 @@ async def async_unload_entry(hass, entry): async_remove_entry = config_entry_flow.webhook_async_remove_entry -config_entry_flow.register_webhook_flow( - DOMAIN, - 'Dialogflow Webhook', - { - 'dialogflow_url': 'https://dialogflow.com/docs/fulfillment#webhook', - 'docs_url': 'https://www.home-assistant.io/components/dialogflow/' - } -) - - def dialogflow_error_response(message, error): """Return a response saying the error message.""" dialogflow_response = DialogflowResponse(message['result']['parameters']) diff --git a/homeassistant/components/dialogflow/config_flow.py b/homeassistant/components/dialogflow/config_flow.py new file mode 100644 index 00000000000..aa6f9f6f515 --- /dev/null +++ b/homeassistant/components/dialogflow/config_flow.py @@ -0,0 +1,13 @@ +"""Config flow for DialogFlow.""" +from homeassistant.helpers import config_entry_flow +from .const import DOMAIN + + +config_entry_flow.register_webhook_flow( + DOMAIN, + 'Dialogflow Webhook', + { + 'dialogflow_url': 'https://dialogflow.com/docs/fulfillment#webhook', + 'docs_url': 'https://www.home-assistant.io/components/dialogflow/' + } +) diff --git a/homeassistant/components/dialogflow/const.py b/homeassistant/components/dialogflow/const.py new file mode 100644 index 00000000000..476cb480d94 --- /dev/null +++ b/homeassistant/components/dialogflow/const.py @@ -0,0 +1,3 @@ +"""Const for DialogFlow.""" + +DOMAIN = "dialogflow" diff --git a/homeassistant/components/dialogflow/manifest.json b/homeassistant/components/dialogflow/manifest.json index d136b8a984d..aa8b584aeca 100644 --- a/homeassistant/components/dialogflow/manifest.json +++ b/homeassistant/components/dialogflow/manifest.json @@ -1,6 +1,7 @@ { "domain": "dialogflow", "name": "Dialogflow", + "config_flow": true, "documentation": "https://www.home-assistant.io/components/dialogflow", "requirements": [], "dependencies": [ diff --git a/homeassistant/components/emulated_roku/manifest.json b/homeassistant/components/emulated_roku/manifest.json index 3b8eba396ec..ba68ce94951 100644 --- a/homeassistant/components/emulated_roku/manifest.json +++ b/homeassistant/components/emulated_roku/manifest.json @@ -1,6 +1,7 @@ { "domain": "emulated_roku", "name": "Emulated roku", + "config_flow": true, "documentation": "https://www.home-assistant.io/components/emulated_roku", "requirements": [ "emulated_roku==0.1.8" diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 9d25ec6d034..b50d11dbd12 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -1,6 +1,7 @@ { "domain": "esphome", "name": "ESPHome", + "config_flow": true, "documentation": "https://www.home-assistant.io/components/esphome", "requirements": [ "aioesphomeapi==2.0.1" diff --git a/homeassistant/components/geofency/__init__.py b/homeassistant/components/geofency/__init__.py index 0b4b757ce9e..37d32a8860d 100644 --- a/homeassistant/components/geofency/__init__.py +++ b/homeassistant/components/geofency/__init__.py @@ -12,10 +12,11 @@ from homeassistant.helpers import config_entry_flow import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.util import slugify +from .const import DOMAIN + _LOGGER = logging.getLogger(__name__) -DOMAIN = 'geofency' CONF_MOBILE_BEACONS = 'mobile_beacons' CONFIG_SCHEMA = vol.Schema({ @@ -134,12 +135,3 @@ async def async_unload_entry(hass, entry): # pylint: disable=invalid-name async_remove_entry = config_entry_flow.webhook_async_remove_entry - - -config_entry_flow.register_webhook_flow( - DOMAIN, - 'Geofency Webhook', - { - 'docs_url': 'https://www.home-assistant.io/components/geofency/' - } -) diff --git a/homeassistant/components/geofency/config_flow.py b/homeassistant/components/geofency/config_flow.py new file mode 100644 index 00000000000..422343b16bb --- /dev/null +++ b/homeassistant/components/geofency/config_flow.py @@ -0,0 +1,12 @@ +"""Config flow for Geofency.""" +from homeassistant.helpers import config_entry_flow +from .const import DOMAIN + + +config_entry_flow.register_webhook_flow( + DOMAIN, + 'Geofency Webhook', + { + 'docs_url': 'https://www.home-assistant.io/components/geofency/' + } +) diff --git a/homeassistant/components/geofency/const.py b/homeassistant/components/geofency/const.py new file mode 100644 index 00000000000..f42fb97f168 --- /dev/null +++ b/homeassistant/components/geofency/const.py @@ -0,0 +1,3 @@ +"""Const for Geofency.""" + +DOMAIN = 'geofency' diff --git a/homeassistant/components/geofency/manifest.json b/homeassistant/components/geofency/manifest.json index 576d0e419a7..d593aec46a4 100644 --- a/homeassistant/components/geofency/manifest.json +++ b/homeassistant/components/geofency/manifest.json @@ -1,6 +1,7 @@ { "domain": "geofency", "name": "Geofency", + "config_flow": true, "documentation": "https://www.home-assistant.io/components/geofency", "requirements": [], "dependencies": [ diff --git a/homeassistant/components/gpslogger/__init__.py b/homeassistant/components/gpslogger/__init__.py index 6887b85d02d..016de66e9fd 100644 --- a/homeassistant/components/gpslogger/__init__.py +++ b/homeassistant/components/gpslogger/__init__.py @@ -11,10 +11,10 @@ from homeassistant.const import HTTP_UNPROCESSABLE_ENTITY, \ from homeassistant.helpers import config_entry_flow from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -DOMAIN = 'gpslogger' TRACKER_UPDATE = '{}_tracker_update'.format(DOMAIN) ATTR_ALTITUDE = 'altitude' @@ -105,12 +105,3 @@ async def async_unload_entry(hass, entry): # pylint: disable=invalid-name async_remove_entry = config_entry_flow.webhook_async_remove_entry - - -config_entry_flow.register_webhook_flow( - DOMAIN, - 'GPSLogger Webhook', - { - 'docs_url': 'https://www.home-assistant.io/components/gpslogger/' - } -) diff --git a/homeassistant/components/gpslogger/config_flow.py b/homeassistant/components/gpslogger/config_flow.py new file mode 100644 index 00000000000..f48d9abc680 --- /dev/null +++ b/homeassistant/components/gpslogger/config_flow.py @@ -0,0 +1,12 @@ +"""Config flow for GPSLogger.""" +from homeassistant.helpers import config_entry_flow +from .const import DOMAIN + + +config_entry_flow.register_webhook_flow( + DOMAIN, + 'GPSLogger Webhook', + { + 'docs_url': 'https://www.home-assistant.io/components/gpslogger/' + } +) diff --git a/homeassistant/components/gpslogger/const.py b/homeassistant/components/gpslogger/const.py new file mode 100644 index 00000000000..e37c7f0d77b --- /dev/null +++ b/homeassistant/components/gpslogger/const.py @@ -0,0 +1,3 @@ +"""Const for GPSLogger.""" + +DOMAIN = 'gpslogger' diff --git a/homeassistant/components/gpslogger/manifest.json b/homeassistant/components/gpslogger/manifest.json index 2d2166c1bb1..f039e50914b 100644 --- a/homeassistant/components/gpslogger/manifest.json +++ b/homeassistant/components/gpslogger/manifest.json @@ -1,6 +1,7 @@ { "domain": "gpslogger", "name": "Gpslogger", + "config_flow": true, "documentation": "https://www.home-assistant.io/components/gpslogger", "requirements": [], "dependencies": [ diff --git a/homeassistant/components/hangouts/manifest.json b/homeassistant/components/hangouts/manifest.json index 5d9bf3c7612..4a90e9c977e 100644 --- a/homeassistant/components/hangouts/manifest.json +++ b/homeassistant/components/hangouts/manifest.json @@ -1,6 +1,7 @@ { "domain": "hangouts", "name": "Hangouts", + "config_flow": true, "documentation": "https://www.home-assistant.io/components/hangouts", "requirements": [ "hangups==0.4.9" diff --git a/homeassistant/components/heos/manifest.json b/homeassistant/components/heos/manifest.json index f3a2ff4eccf..a1fc8030318 100644 --- a/homeassistant/components/heos/manifest.json +++ b/homeassistant/components/heos/manifest.json @@ -1,6 +1,7 @@ { "domain": "heos", "name": "HEOS", + "config_flow": true, "documentation": "https://www.home-assistant.io/components/heos", "requirements": [ "pyheos==0.5.2" diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 53476b8ba6d..8b0dfd199bb 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -1,6 +1,7 @@ { "domain": "homekit_controller", "name": "Homekit controller", + "config_flow": true, "documentation": "https://www.home-assistant.io/components/homekit_controller", "requirements": [ "homekit[IP]==0.14.0" diff --git a/homeassistant/components/homematicip_cloud/manifest.json b/homeassistant/components/homematicip_cloud/manifest.json index 030b4d5b79b..6ba04bfe3c0 100644 --- a/homeassistant/components/homematicip_cloud/manifest.json +++ b/homeassistant/components/homematicip_cloud/manifest.json @@ -1,6 +1,7 @@ { "domain": "homematicip_cloud", "name": "Homematicip cloud", + "config_flow": true, "documentation": "https://www.home-assistant.io/components/homematicip_cloud", "requirements": [ "homematicip==0.10.7" diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json index 54a3a11a189..d035e4468e4 100644 --- a/homeassistant/components/hue/manifest.json +++ b/homeassistant/components/hue/manifest.json @@ -1,6 +1,7 @@ { "domain": "hue", "name": "Philips Hue", + "config_flow": true, "documentation": "https://www.home-assistant.io/components/hue", "requirements": [ "aiohue==1.9.1" diff --git a/homeassistant/components/ifttt/__init__.py b/homeassistant/components/ifttt/__init__.py index 6b5934702aa..e6926ff0fb5 100644 --- a/homeassistant/components/ifttt/__init__.py +++ b/homeassistant/components/ifttt/__init__.py @@ -8,6 +8,7 @@ import voluptuous as vol from homeassistant.const import CONF_WEBHOOK_ID from homeassistant.helpers import config_entry_flow import homeassistant.helpers.config_validation as cv +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -21,8 +22,6 @@ ATTR_VALUE3 = 'value3' CONF_KEY = 'key' -DOMAIN = 'ifttt' - SERVICE_TRIGGER = 'trigger' SERVICE_TRIGGER_SCHEMA = vol.Schema({ @@ -108,13 +107,3 @@ async def async_unload_entry(hass, entry): # pylint: disable=invalid-name async_remove_entry = config_entry_flow.webhook_async_remove_entry - - -config_entry_flow.register_webhook_flow( - DOMAIN, - 'IFTTT Webhook', - { - 'applet_url': 'https://ifttt.com/maker_webhooks', - 'docs_url': 'https://www.home-assistant.io/components/ifttt/' - } -) diff --git a/homeassistant/components/ifttt/config_flow.py b/homeassistant/components/ifttt/config_flow.py new file mode 100644 index 00000000000..887a5c88013 --- /dev/null +++ b/homeassistant/components/ifttt/config_flow.py @@ -0,0 +1,13 @@ +"""Config flow for IFTTT.""" +from homeassistant.helpers import config_entry_flow +from .const import DOMAIN + + +config_entry_flow.register_webhook_flow( + DOMAIN, + 'IFTTT Webhook', + { + 'applet_url': 'https://ifttt.com/maker_webhooks', + 'docs_url': 'https://www.home-assistant.io/components/ifttt/' + } +) diff --git a/homeassistant/components/ifttt/const.py b/homeassistant/components/ifttt/const.py new file mode 100644 index 00000000000..03b948fc83a --- /dev/null +++ b/homeassistant/components/ifttt/const.py @@ -0,0 +1,3 @@ +"""Const for IFTTT.""" + +DOMAIN = "ifttt" diff --git a/homeassistant/components/ifttt/manifest.json b/homeassistant/components/ifttt/manifest.json index 007e0870023..58490569e65 100644 --- a/homeassistant/components/ifttt/manifest.json +++ b/homeassistant/components/ifttt/manifest.json @@ -1,6 +1,7 @@ { "domain": "ifttt", "name": "Ifttt", + "config_flow": true, "documentation": "https://www.home-assistant.io/components/ifttt", "requirements": [ "pyfttt==0.3" diff --git a/homeassistant/components/ios/__init__.py b/homeassistant/components/ios/__init__.py index a9395ed5f5d..3fc09781cd7 100644 --- a/homeassistant/components/ios/__init__.py +++ b/homeassistant/components/ios/__init__.py @@ -9,8 +9,7 @@ from homeassistant.components.http import HomeAssistantView from homeassistant.const import HTTP_BAD_REQUEST, HTTP_INTERNAL_SERVER_ERROR from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import ( - config_entry_flow, config_validation as cv, discovery) +from homeassistant.helpers import config_validation as cv, discovery from homeassistant.util.json import load_json, save_json _LOGGER = logging.getLogger(__name__) @@ -279,8 +278,3 @@ class iOSIdentifyDeviceView(HomeAssistantView): HTTP_INTERNAL_SERVER_ERROR) return self.json({"status": "registered"}) - - -config_entry_flow.register_discovery_flow( - DOMAIN, 'Home Assistant iOS', lambda *_: True, - config_entries.CONN_CLASS_CLOUD_PUSH) diff --git a/homeassistant/components/ios/config_flow.py b/homeassistant/components/ios/config_flow.py new file mode 100644 index 00000000000..c85d5066128 --- /dev/null +++ b/homeassistant/components/ios/config_flow.py @@ -0,0 +1,9 @@ +"""Config flow for iOS.""" +from homeassistant.helpers import config_entry_flow +from homeassistant import config_entries +from .const import DOMAIN + + +config_entry_flow.register_discovery_flow( + DOMAIN, 'Home Assistant iOS', lambda *_: True, + config_entries.CONN_CLASS_CLOUD_PUSH) diff --git a/homeassistant/components/ios/const.py b/homeassistant/components/ios/const.py new file mode 100644 index 00000000000..5fc921b7a44 --- /dev/null +++ b/homeassistant/components/ios/const.py @@ -0,0 +1,3 @@ +"""Const for iOS.""" + +DOMAIN = "ios" diff --git a/homeassistant/components/ios/manifest.json b/homeassistant/components/ios/manifest.json index 97c2e2ae28f..28c9ea1e952 100644 --- a/homeassistant/components/ios/manifest.json +++ b/homeassistant/components/ios/manifest.json @@ -1,6 +1,7 @@ { "domain": "ios", "name": "Ios", + "config_flow": true, "documentation": "https://www.home-assistant.io/components/ios", "requirements": [], "dependencies": [ diff --git a/homeassistant/components/ipma/manifest.json b/homeassistant/components/ipma/manifest.json index 29fc0429e86..093ccbf6a5b 100644 --- a/homeassistant/components/ipma/manifest.json +++ b/homeassistant/components/ipma/manifest.json @@ -1,6 +1,7 @@ { "domain": "ipma", "name": "Ipma", + "config_flow": true, "documentation": "https://www.home-assistant.io/components/ipma", "requirements": [ "pyipma==1.2.1" diff --git a/homeassistant/components/iqvia/manifest.json b/homeassistant/components/iqvia/manifest.json index 1757ffc2a22..a59caa1654c 100644 --- a/homeassistant/components/iqvia/manifest.json +++ b/homeassistant/components/iqvia/manifest.json @@ -1,6 +1,7 @@ { "domain": "iqvia", "name": "IQVIA", + "config_flow": true, "documentation": "https://www.home-assistant.io/components/iqvia", "requirements": [ "numpy==1.16.3", diff --git a/homeassistant/components/lifx/__init__.py b/homeassistant/components/lifx/__init__.py index 849fecad487..ceea489614a 100644 --- a/homeassistant/components/lifx/__init__.py +++ b/homeassistant/components/lifx/__init__.py @@ -4,10 +4,10 @@ import homeassistant.helpers.config_validation as cv from homeassistant import config_entries from homeassistant.const import CONF_PORT -from homeassistant.helpers import config_entry_flow from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from .const import DOMAIN + -DOMAIN = 'lifx' CONF_SERVER = 'server' CONF_BROADCAST = 'broadcast' @@ -55,15 +55,3 @@ async def async_unload_entry(hass, entry): await hass.config_entries.async_forward_entry_unload(entry, LIGHT_DOMAIN) return True - - -async def _async_has_devices(hass): - """Return if there are devices that can be discovered.""" - import aiolifx - - lifx_ip_addresses = await aiolifx.LifxScan(hass.loop).scan() - return len(lifx_ip_addresses) > 0 - - -config_entry_flow.register_discovery_flow( - DOMAIN, 'LIFX', _async_has_devices, config_entries.CONN_CLASS_LOCAL_POLL) diff --git a/homeassistant/components/lifx/config_flow.py b/homeassistant/components/lifx/config_flow.py new file mode 100644 index 00000000000..b701c4e4391 --- /dev/null +++ b/homeassistant/components/lifx/config_flow.py @@ -0,0 +1,16 @@ +"""Config flow flow LIFX.""" +from homeassistant.helpers import config_entry_flow +from homeassistant import config_entries +from .const import DOMAIN + + +async def _async_has_devices(hass): + """Return if there are devices that can be discovered.""" + import aiolifx + + lifx_ip_addresses = await aiolifx.LifxScan(hass.loop).scan() + return len(lifx_ip_addresses) > 0 + + +config_entry_flow.register_discovery_flow( + DOMAIN, 'LIFX', _async_has_devices, config_entries.CONN_CLASS_LOCAL_POLL) diff --git a/homeassistant/components/lifx/const.py b/homeassistant/components/lifx/const.py new file mode 100644 index 00000000000..fa54433e58f --- /dev/null +++ b/homeassistant/components/lifx/const.py @@ -0,0 +1,3 @@ +"""Const for LIFX.""" + +DOMAIN = 'lifx' diff --git a/homeassistant/components/lifx/manifest.json b/homeassistant/components/lifx/manifest.json index a8b1fd58afe..ca9b578432b 100644 --- a/homeassistant/components/lifx/manifest.json +++ b/homeassistant/components/lifx/manifest.json @@ -1,6 +1,7 @@ { "domain": "lifx", "name": "Lifx", + "config_flow": true, "documentation": "https://www.home-assistant.io/components/lifx", "requirements": [ "aiolifx==0.6.7", diff --git a/homeassistant/components/locative/__init__.py b/homeassistant/components/locative/__init__.py index f21c55af28a..c44b12c87d2 100644 --- a/homeassistant/components/locative/__init__.py +++ b/homeassistant/components/locative/__init__.py @@ -145,12 +145,3 @@ async def async_unload_entry(hass, entry): # pylint: disable=invalid-name async_remove_entry = config_entry_flow.webhook_async_remove_entry - - -config_entry_flow.register_webhook_flow( - DOMAIN, - 'Locative Webhook', - { - 'docs_url': 'https://www.home-assistant.io/components/locative/' - } -) diff --git a/homeassistant/components/locative/config_flow.py b/homeassistant/components/locative/config_flow.py new file mode 100644 index 00000000000..4a238e95358 --- /dev/null +++ b/homeassistant/components/locative/config_flow.py @@ -0,0 +1,12 @@ +"""Config flow for Locative.""" +from homeassistant.helpers import config_entry_flow +from .const import DOMAIN + + +config_entry_flow.register_webhook_flow( + DOMAIN, + 'Locative Webhook', + { + 'docs_url': 'https://www.home-assistant.io/components/locative/' + } +) diff --git a/homeassistant/components/locative/const.py b/homeassistant/components/locative/const.py new file mode 100644 index 00000000000..4dfaa54de78 --- /dev/null +++ b/homeassistant/components/locative/const.py @@ -0,0 +1,3 @@ +"""Const for Locative.""" + +DOMAIN = "locative" diff --git a/homeassistant/components/locative/manifest.json b/homeassistant/components/locative/manifest.json index afe2850caf8..be2eb07a23c 100644 --- a/homeassistant/components/locative/manifest.json +++ b/homeassistant/components/locative/manifest.json @@ -1,6 +1,7 @@ { "domain": "locative", "name": "Locative", + "config_flow": true, "documentation": "https://www.home-assistant.io/components/locative", "requirements": [], "dependencies": [ diff --git a/homeassistant/components/logi_circle/manifest.json b/homeassistant/components/logi_circle/manifest.json index 8cf6a157a01..b1767748395 100644 --- a/homeassistant/components/logi_circle/manifest.json +++ b/homeassistant/components/logi_circle/manifest.json @@ -1,6 +1,7 @@ { "domain": "logi_circle", "name": "Logi Circle", + "config_flow": true, "documentation": "https://www.home-assistant.io/components/logi_circle", "requirements": ["logi_circle==0.2.2"], "dependencies": ["ffmpeg"], diff --git a/homeassistant/components/luftdaten/manifest.json b/homeassistant/components/luftdaten/manifest.json index 0e6a46a5c5d..d0a3d48b60f 100644 --- a/homeassistant/components/luftdaten/manifest.json +++ b/homeassistant/components/luftdaten/manifest.json @@ -1,6 +1,7 @@ { "domain": "luftdaten", "name": "Luftdaten", + "config_flow": true, "documentation": "https://www.home-assistant.io/components/luftdaten", "requirements": [ "luftdaten==0.3.4" diff --git a/homeassistant/components/mailgun/__init__.py b/homeassistant/components/mailgun/__init__.py index 2f89904f12b..f74d105d98f 100644 --- a/homeassistant/components/mailgun/__init__.py +++ b/homeassistant/components/mailgun/__init__.py @@ -10,12 +10,14 @@ import homeassistant.helpers.config_validation as cv from homeassistant.const import CONF_API_KEY, CONF_DOMAIN, CONF_WEBHOOK_ID from homeassistant.helpers import config_entry_flow +from .const import DOMAIN + + _LOGGER = logging.getLogger(__name__) CONF_SANDBOX = 'sandbox' DEFAULT_SANDBOX = False -DOMAIN = 'mailgun' MESSAGE_RECEIVED = '{}_message_received'.format(DOMAIN) @@ -90,13 +92,3 @@ async def async_unload_entry(hass, entry): # pylint: disable=invalid-name async_remove_entry = config_entry_flow.webhook_async_remove_entry - - -config_entry_flow.register_webhook_flow( - DOMAIN, - 'Mailgun Webhook', - { - 'mailgun_url': 'https://documentation.mailgun.com/en/latest/user_manual.html#webhooks', # noqa: E501 pylint: disable=line-too-long - 'docs_url': 'https://www.home-assistant.io/components/mailgun/' - } -) diff --git a/homeassistant/components/mailgun/config_flow.py b/homeassistant/components/mailgun/config_flow.py new file mode 100644 index 00000000000..aeccd9a506f --- /dev/null +++ b/homeassistant/components/mailgun/config_flow.py @@ -0,0 +1,13 @@ +"""Config flow for Mailgun.""" +from homeassistant.helpers import config_entry_flow +from .const import DOMAIN + + +config_entry_flow.register_webhook_flow( + DOMAIN, + 'Mailgun Webhook', + { + 'mailgun_url': 'https://documentation.mailgun.com/en/latest/user_manual.html#webhooks', # noqa: E501 pylint: disable=line-too-long + 'docs_url': 'https://www.home-assistant.io/components/mailgun/' + } +) diff --git a/homeassistant/components/mailgun/const.py b/homeassistant/components/mailgun/const.py new file mode 100644 index 00000000000..4532c1cbc46 --- /dev/null +++ b/homeassistant/components/mailgun/const.py @@ -0,0 +1,3 @@ +"""Const for Mailgun.""" + +DOMAIN = "mailgun" diff --git a/homeassistant/components/mailgun/manifest.json b/homeassistant/components/mailgun/manifest.json index 2979b391ec2..9ed7a50a8e3 100644 --- a/homeassistant/components/mailgun/manifest.json +++ b/homeassistant/components/mailgun/manifest.json @@ -1,6 +1,7 @@ { "domain": "mailgun", "name": "Mailgun", + "config_flow": true, "documentation": "https://www.home-assistant.io/components/mailgun", "requirements": [ "pymailgunner==1.4" diff --git a/homeassistant/components/mobile_app/__init__.py b/homeassistant/components/mobile_app/__init__.py index 711963a0b24..abb7bcb7628 100644 --- a/homeassistant/components/mobile_app/__init__.py +++ b/homeassistant/components/mobile_app/__init__.py @@ -1,5 +1,4 @@ """Integrates Native Apps to Home Assistant.""" -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, discovery @@ -91,26 +90,3 @@ async def async_setup_entry(hass, entry): hass.config_entries.async_forward_entry_setup(entry, DATA_SENSOR)) return True - - -@config_entries.HANDLERS.register(DOMAIN) -class MobileAppFlowHandler(config_entries.ConfigFlow): - """Handle a Mobile App config flow.""" - - VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_PUSH - - async def async_step_user(self, user_input=None): - """Handle a flow initialized by the user.""" - placeholders = { - 'apps_url': - 'https://www.home-assistant.io/components/mobile_app/#apps' - } - - return self.async_abort(reason='install_app', - description_placeholders=placeholders) - - async def async_step_registration(self, user_input=None): - """Handle a flow initialized during registration.""" - return self.async_create_entry(title=user_input[ATTR_DEVICE_NAME], - data=user_input) diff --git a/homeassistant/components/mobile_app/config_flow.py b/homeassistant/components/mobile_app/config_flow.py new file mode 100644 index 00000000000..02fea3c6593 --- /dev/null +++ b/homeassistant/components/mobile_app/config_flow.py @@ -0,0 +1,26 @@ +"""Config flow for Mobile App.""" +from homeassistant import config_entries +from .const import DOMAIN, ATTR_DEVICE_NAME + + +@config_entries.HANDLERS.register(DOMAIN) +class MobileAppFlowHandler(config_entries.ConfigFlow): + """Handle a Mobile App config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_PUSH + + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user.""" + placeholders = { + 'apps_url': + 'https://www.home-assistant.io/components/mobile_app/#apps' + } + + return self.async_abort(reason='install_app', + description_placeholders=placeholders) + + async def async_step_registration(self, user_input=None): + """Handle a flow initialized during registration.""" + return self.async_create_entry(title=user_input[ATTR_DEVICE_NAME], + data=user_input) diff --git a/homeassistant/components/mobile_app/manifest.json b/homeassistant/components/mobile_app/manifest.json index 9c21858df1d..969817b62c7 100644 --- a/homeassistant/components/mobile_app/manifest.json +++ b/homeassistant/components/mobile_app/manifest.json @@ -1,6 +1,7 @@ { "domain": "mobile_app", "name": "Home Assistant Mobile App Support", + "config_flow": true, "documentation": "https://www.home-assistant.io/components/mobile_app", "requirements": [ "PyNaCl==1.3.0" diff --git a/homeassistant/components/mqtt/manifest.json b/homeassistant/components/mqtt/manifest.json index dd4d0323a51..d63d1707fac 100644 --- a/homeassistant/components/mqtt/manifest.json +++ b/homeassistant/components/mqtt/manifest.json @@ -1,6 +1,7 @@ { "domain": "mqtt", "name": "MQTT", + "config_flow": true, "documentation": "https://www.home-assistant.io/components/mqtt", "requirements": [ "hbmqtt==0.9.4", diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index 9f2e4202f93..8a6e8ec611a 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -1,6 +1,7 @@ { "domain": "nest", "name": "Nest", + "config_flow": true, "documentation": "https://www.home-assistant.io/components/nest", "requirements": [ "python-nest==4.1.0" diff --git a/homeassistant/components/openuv/manifest.json b/homeassistant/components/openuv/manifest.json index b94a409aa71..0cfb02e81d6 100644 --- a/homeassistant/components/openuv/manifest.json +++ b/homeassistant/components/openuv/manifest.json @@ -1,6 +1,7 @@ { "domain": "openuv", "name": "Openuv", + "config_flow": true, "documentation": "https://www.home-assistant.io/components/openuv", "requirements": [ "pyopenuv==1.0.9" diff --git a/homeassistant/components/owntracks/manifest.json b/homeassistant/components/owntracks/manifest.json index 60bce1bca3d..bc4fe97bc7f 100644 --- a/homeassistant/components/owntracks/manifest.json +++ b/homeassistant/components/owntracks/manifest.json @@ -1,6 +1,7 @@ { "domain": "owntracks", "name": "Owntracks", + "config_flow": true, "documentation": "https://www.home-assistant.io/components/owntracks", "requirements": [ "PyNaCl==1.3.0" diff --git a/homeassistant/components/point/manifest.json b/homeassistant/components/point/manifest.json index 8b888a3647a..fcc9265ce9b 100644 --- a/homeassistant/components/point/manifest.json +++ b/homeassistant/components/point/manifest.json @@ -1,6 +1,7 @@ { "domain": "point", "name": "Point", + "config_flow": true, "documentation": "https://www.home-assistant.io/components/point", "requirements": [ "pypoint==1.1.1" diff --git a/homeassistant/components/ps4/manifest.json b/homeassistant/components/ps4/manifest.json index 087f1618378..1cf613bf9b9 100644 --- a/homeassistant/components/ps4/manifest.json +++ b/homeassistant/components/ps4/manifest.json @@ -1,6 +1,7 @@ { "domain": "ps4", "name": "Ps4", + "config_flow": true, "documentation": "https://www.home-assistant.io/components/ps4", "requirements": [ "pyps4-homeassistant==0.7.3" diff --git a/homeassistant/components/rainmachine/manifest.json b/homeassistant/components/rainmachine/manifest.json index ad7bdada321..b99798bb4b6 100644 --- a/homeassistant/components/rainmachine/manifest.json +++ b/homeassistant/components/rainmachine/manifest.json @@ -1,6 +1,7 @@ { "domain": "rainmachine", "name": "Rainmachine", + "config_flow": true, "documentation": "https://www.home-assistant.io/components/rainmachine", "requirements": [ "regenmaschine==1.4.0" diff --git a/homeassistant/components/simplisafe/manifest.json b/homeassistant/components/simplisafe/manifest.json index eac586b355d..b6bb1285daa 100644 --- a/homeassistant/components/simplisafe/manifest.json +++ b/homeassistant/components/simplisafe/manifest.json @@ -1,6 +1,7 @@ { "domain": "simplisafe", "name": "Simplisafe", + "config_flow": true, "documentation": "https://www.home-assistant.io/components/simplisafe", "requirements": [ "simplisafe-python==3.4.1" diff --git a/homeassistant/components/smartthings/manifest.json b/homeassistant/components/smartthings/manifest.json index d31a90c6eb8..75b113354ff 100644 --- a/homeassistant/components/smartthings/manifest.json +++ b/homeassistant/components/smartthings/manifest.json @@ -1,6 +1,7 @@ { "domain": "smartthings", "name": "Smartthings", + "config_flow": true, "documentation": "https://www.home-assistant.io/components/smartthings", "requirements": [ "pysmartapp==0.3.2", diff --git a/homeassistant/components/smhi/manifest.json b/homeassistant/components/smhi/manifest.json index e4ad478e033..421eadca51c 100644 --- a/homeassistant/components/smhi/manifest.json +++ b/homeassistant/components/smhi/manifest.json @@ -1,6 +1,7 @@ { "domain": "smhi", "name": "Smhi", + "config_flow": true, "documentation": "https://www.home-assistant.io/components/smhi", "requirements": [ "smhi-pkg==1.0.10" diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index 5f7b2d04431..4d3df055bbf 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -5,10 +5,11 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.const import CONF_HOSTS, ATTR_ENTITY_ID, ATTR_TIME -from homeassistant.helpers import config_entry_flow, config_validation as cv +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send -DOMAIN = 'sonos' +from .const import DOMAIN + CONF_ADVERTISE_ADDR = 'advertise_addr' CONF_INTERFACE_ADDR = 'interface_addr' @@ -141,14 +142,3 @@ async def async_setup_entry(hass, entry): hass.async_create_task(hass.config_entries.async_forward_entry_setup( entry, MP_DOMAIN)) return True - - -async def _async_has_devices(hass): - """Return if there are devices that can be discovered.""" - import pysonos - - return await hass.async_add_executor_job(pysonos.discover) - - -config_entry_flow.register_discovery_flow( - DOMAIN, 'Sonos', _async_has_devices, config_entries.CONN_CLASS_LOCAL_PUSH) diff --git a/homeassistant/components/sonos/config_flow.py b/homeassistant/components/sonos/config_flow.py new file mode 100644 index 00000000000..ca3932a76c2 --- /dev/null +++ b/homeassistant/components/sonos/config_flow.py @@ -0,0 +1,15 @@ +"""Config flow for SONOS.""" +from homeassistant.helpers import config_entry_flow +from homeassistant import config_entries +from .const import DOMAIN + + +async def _async_has_devices(hass): + """Return if there are devices that can be discovered.""" + import pysonos + + return await hass.async_add_executor_job(pysonos.discover) + + +config_entry_flow.register_discovery_flow( + DOMAIN, 'Sonos', _async_has_devices, config_entries.CONN_CLASS_LOCAL_PUSH) diff --git a/homeassistant/components/sonos/const.py b/homeassistant/components/sonos/const.py new file mode 100644 index 00000000000..5858f2bca9b --- /dev/null +++ b/homeassistant/components/sonos/const.py @@ -0,0 +1,3 @@ +"""Const for Sonos.""" + +DOMAIN = "sonos" diff --git a/homeassistant/components/sonos/manifest.json b/homeassistant/components/sonos/manifest.json index 5eac580313e..58fa7b49f88 100644 --- a/homeassistant/components/sonos/manifest.json +++ b/homeassistant/components/sonos/manifest.json @@ -1,6 +1,7 @@ { "domain": "sonos", "name": "Sonos", + "config_flow": true, "documentation": "https://www.home-assistant.io/components/sonos", "requirements": [ "pysonos==0.0.12" diff --git a/homeassistant/components/tellduslive/manifest.json b/homeassistant/components/tellduslive/manifest.json index 2e6233f426c..7f431ba92b1 100644 --- a/homeassistant/components/tellduslive/manifest.json +++ b/homeassistant/components/tellduslive/manifest.json @@ -1,6 +1,7 @@ { "domain": "tellduslive", "name": "Tellduslive", + "config_flow": true, "documentation": "https://www.home-assistant.io/components/tellduslive", "requirements": [ "tellduslive==0.10.10" diff --git a/homeassistant/components/toon/manifest.json b/homeassistant/components/toon/manifest.json index 7dbf6768db6..eccaf7df9bc 100644 --- a/homeassistant/components/toon/manifest.json +++ b/homeassistant/components/toon/manifest.json @@ -1,6 +1,7 @@ { "domain": "toon", "name": "Toon", + "config_flow": true, "documentation": "https://www.home-assistant.io/components/toon", "requirements": [ "toonapilib==3.2.2" diff --git a/homeassistant/components/tplink/__init__.py b/homeassistant/components/tplink/__init__.py index 2ebf342c38d..4173c1aaa60 100644 --- a/homeassistant/components/tplink/__init__.py +++ b/homeassistant/components/tplink/__init__.py @@ -5,14 +5,12 @@ import voluptuous as vol from homeassistant.const import CONF_HOST from homeassistant import config_entries -from homeassistant.helpers import config_entry_flow import homeassistant.helpers.config_validation as cv - +from .config_flow import async_get_devices +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -DOMAIN = 'tplink' - TPLINK_HOST_SCHEMA = vol.Schema({ vol.Required(CONF_HOST): cv.string }) @@ -34,16 +32,6 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -async def _async_has_devices(hass): - """Return if there are devices that can be discovered.""" - from pyHS100 import Discover - - def discover(): - devs = Discover.discover() - return devs - return await hass.async_add_executor_job(discover) - - async def async_setup(hass, config): """Set up the TP-Link component.""" conf = config.get(DOMAIN) @@ -74,7 +62,7 @@ async def async_setup_entry(hass, config_entry): # If initialized from configure integrations, there's no config # so we default here to True if config_data is None or config_data[CONF_DISCOVERY]: - devs = await _async_has_devices(hass) + devs = await async_get_devices(hass) _LOGGER.info("Discovered %s TP-Link smart home device(s)", len(devs)) devices.update(devs) @@ -149,9 +137,3 @@ async def async_unload_entry(hass, entry): # We were not able to unload the platforms, either because there # were none or one of the forward_unloads failed. return False - - -config_entry_flow.register_discovery_flow(DOMAIN, - 'TP-Link Smart Home', - _async_has_devices, - config_entries.CONN_CLASS_LOCAL_POLL) diff --git a/homeassistant/components/tplink/config_flow.py b/homeassistant/components/tplink/config_flow.py new file mode 100644 index 00000000000..86b1acf4ff1 --- /dev/null +++ b/homeassistant/components/tplink/config_flow.py @@ -0,0 +1,20 @@ +"""Config flow for TP-Link.""" +from homeassistant.helpers import config_entry_flow +from homeassistant import config_entries +from .const import DOMAIN + + +async def async_get_devices(hass): + """Return if there are devices that can be discovered.""" + from pyHS100 import Discover + + def discover(): + devs = Discover.discover() + return devs + return await hass.async_add_executor_job(discover) + + +config_entry_flow.register_discovery_flow(DOMAIN, + 'TP-Link Smart Home', + async_get_devices, + config_entries.CONN_CLASS_LOCAL_POLL) diff --git a/homeassistant/components/tplink/const.py b/homeassistant/components/tplink/const.py new file mode 100644 index 00000000000..583c25e285c --- /dev/null +++ b/homeassistant/components/tplink/const.py @@ -0,0 +1,3 @@ +"""Const for TP-Link.""" + +DOMAIN = "tplink" diff --git a/homeassistant/components/tplink/manifest.json b/homeassistant/components/tplink/manifest.json index d164a526fc0..e0f85757afd 100644 --- a/homeassistant/components/tplink/manifest.json +++ b/homeassistant/components/tplink/manifest.json @@ -1,6 +1,7 @@ { "domain": "tplink", "name": "Tplink", + "config_flow": true, "documentation": "https://www.home-assistant.io/components/tplink", "requirements": [ "pyHS100==0.3.5", diff --git a/homeassistant/components/tradfri/manifest.json b/homeassistant/components/tradfri/manifest.json index 19e8348e987..c9a4fca3dc9 100644 --- a/homeassistant/components/tradfri/manifest.json +++ b/homeassistant/components/tradfri/manifest.json @@ -1,6 +1,7 @@ { "domain": "tradfri", "name": "Tradfri", + "config_flow": true, "documentation": "https://www.home-assistant.io/components/tradfri", "requirements": [ "pytradfri[async]==6.0.1" diff --git a/homeassistant/components/twilio/__init__.py b/homeassistant/components/twilio/__init__.py index 82011f499ba..8a1babaf1eb 100644 --- a/homeassistant/components/twilio/__init__.py +++ b/homeassistant/components/twilio/__init__.py @@ -4,8 +4,7 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.const import CONF_WEBHOOK_ID from homeassistant.helpers import config_entry_flow - -DOMAIN = 'twilio' +from .const import DOMAIN CONF_ACCOUNT_SID = 'account_sid' CONF_AUTH_TOKEN = 'auth_token' @@ -60,14 +59,3 @@ async def async_unload_entry(hass, entry): # pylint: disable=invalid-name async_remove_entry = config_entry_flow.webhook_async_remove_entry - - -config_entry_flow.register_webhook_flow( - DOMAIN, - 'Twilio Webhook', - { - 'twilio_url': - 'https://www.twilio.com/docs/glossary/what-is-a-webhook', - 'docs_url': 'https://www.home-assistant.io/components/twilio/' - } -) diff --git a/homeassistant/components/twilio/config_flow.py b/homeassistant/components/twilio/config_flow.py new file mode 100644 index 00000000000..686b6391b05 --- /dev/null +++ b/homeassistant/components/twilio/config_flow.py @@ -0,0 +1,15 @@ +"""Config flow for Twilio.""" +from homeassistant.helpers import config_entry_flow + +from .const import DOMAIN + + +config_entry_flow.register_webhook_flow( + DOMAIN, + 'Twilio Webhook', + { + 'twilio_url': + 'https://www.twilio.com/docs/glossary/what-is-a-webhook', + 'docs_url': 'https://www.home-assistant.io/components/twilio/' + } +) diff --git a/homeassistant/components/twilio/const.py b/homeassistant/components/twilio/const.py new file mode 100644 index 00000000000..7ca44590d6a --- /dev/null +++ b/homeassistant/components/twilio/const.py @@ -0,0 +1,3 @@ +"""Const for Twilio.""" + +DOMAIN = "twilio" diff --git a/homeassistant/components/twilio/manifest.json b/homeassistant/components/twilio/manifest.json index dfb7dd4b14d..f96afa18115 100644 --- a/homeassistant/components/twilio/manifest.json +++ b/homeassistant/components/twilio/manifest.json @@ -1,6 +1,7 @@ { "domain": "twilio", "name": "Twilio", + "config_flow": true, "documentation": "https://www.home-assistant.io/components/twilio", "requirements": [ "twilio==6.19.1" diff --git a/homeassistant/components/unifi/__init__.py b/homeassistant/components/unifi/__init__.py index 3af450acdbf..33b687bd178 100644 --- a/homeassistant/components/unifi/__init__.py +++ b/homeassistant/components/unifi/__init__.py @@ -1,20 +1,9 @@ """Support for devices connected to UniFi POE.""" -import voluptuous as vol - -from homeassistant import config_entries -from homeassistant.const import ( - CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME, CONF_VERIFY_SSL) +from homeassistant.const import CONF_HOST from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC -from .const import (CONF_CONTROLLER, CONF_POE_CONTROL, CONF_SITE_ID, - CONTROLLER_ID, DOMAIN, LOGGER) -from .controller import UniFiController, get_controller -from .errors import ( - AlreadyConfigured, AuthenticationRequired, CannotConnect, UserLevel) - -DEFAULT_PORT = 8443 -DEFAULT_SITE_ID = 'default' -DEFAULT_VERIFY_SSL = False +from .const import CONF_CONTROLLER, CONF_SITE_ID, CONTROLLER_ID, DOMAIN +from .controller import UniFiController async def async_setup(hass, config): @@ -64,116 +53,3 @@ async def async_unload_entry(hass, config_entry): ) controller = hass.data[DOMAIN].pop(controller_id) return await controller.async_reset() - - -@config_entries.HANDLERS.register(DOMAIN) -class UnifiFlowHandler(config_entries.ConfigFlow): - """Handle a UniFi config flow.""" - - VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL - - def __init__(self): - """Initialize the UniFi flow.""" - self.config = None - self.desc = None - self.sites = None - - async def async_step_user(self, user_input=None): - """Handle a flow initialized by the user.""" - errors = {} - - if user_input is not None: - - try: - self.config = { - CONF_HOST: user_input[CONF_HOST], - CONF_USERNAME: user_input[CONF_USERNAME], - CONF_PASSWORD: user_input[CONF_PASSWORD], - CONF_PORT: user_input.get(CONF_PORT), - CONF_VERIFY_SSL: user_input.get(CONF_VERIFY_SSL), - CONF_SITE_ID: DEFAULT_SITE_ID, - } - controller = await get_controller(self.hass, **self.config) - - self.sites = await controller.sites() - - return await self.async_step_site() - - except AuthenticationRequired: - errors['base'] = 'faulty_credentials' - - except CannotConnect: - errors['base'] = 'service_unavailable' - - except Exception: # pylint: disable=broad-except - LOGGER.error( - 'Unknown error connecting with UniFi Controller at %s', - user_input[CONF_HOST]) - return self.async_abort(reason='unknown') - - return self.async_show_form( - step_id='user', - data_schema=vol.Schema({ - vol.Required(CONF_HOST): str, - vol.Required(CONF_USERNAME): str, - vol.Required(CONF_PASSWORD): str, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): int, - vol.Optional( - CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): bool, - }), - errors=errors, - ) - - async def async_step_site(self, user_input=None): - """Select site to control.""" - errors = {} - - if user_input is not None: - - try: - desc = user_input.get(CONF_SITE_ID, self.desc) - for site in self.sites.values(): - if desc == site['desc']: - if site['role'] != 'admin': - raise UserLevel - self.config[CONF_SITE_ID] = site['name'] - break - - for entry in self._async_current_entries(): - controller = entry.data[CONF_CONTROLLER] - if controller[CONF_HOST] == self.config[CONF_HOST] and \ - controller[CONF_SITE_ID] == self.config[CONF_SITE_ID]: - raise AlreadyConfigured - - data = { - CONF_CONTROLLER: self.config, - CONF_POE_CONTROL: True - } - - return self.async_create_entry( - title=desc, - data=data - ) - - except AlreadyConfigured: - return self.async_abort(reason='already_configured') - - except UserLevel: - return self.async_abort(reason='user_privilege') - - if len(self.sites) == 1: - self.desc = next(iter(self.sites.values()))['desc'] - return await self.async_step_site(user_input={}) - - sites = [] - for site in self.sites.values(): - sites.append(site['desc']) - - return self.async_show_form( - step_id='site', - data_schema=vol.Schema({ - vol.Required(CONF_SITE_ID): vol.In(sites) - }), - errors=errors, - ) diff --git a/homeassistant/components/unifi/config_flow.py b/homeassistant/components/unifi/config_flow.py new file mode 100644 index 00000000000..b784aaa705a --- /dev/null +++ b/homeassistant/components/unifi/config_flow.py @@ -0,0 +1,130 @@ +"""Config flow for Unifi.""" +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import ( + CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME, CONF_VERIFY_SSL) + +from .const import (CONF_CONTROLLER, CONF_POE_CONTROL, CONF_SITE_ID, + DOMAIN, LOGGER) +from .controller import get_controller +from .errors import ( + AlreadyConfigured, AuthenticationRequired, CannotConnect, UserLevel) + + +DEFAULT_PORT = 8443 +DEFAULT_SITE_ID = 'default' +DEFAULT_VERIFY_SSL = False + + +@config_entries.HANDLERS.register(DOMAIN) +class UnifiFlowHandler(config_entries.ConfigFlow): + """Handle a UniFi config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + def __init__(self): + """Initialize the UniFi flow.""" + self.config = None + self.desc = None + self.sites = None + + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user.""" + errors = {} + + if user_input is not None: + + try: + self.config = { + CONF_HOST: user_input[CONF_HOST], + CONF_USERNAME: user_input[CONF_USERNAME], + CONF_PASSWORD: user_input[CONF_PASSWORD], + CONF_PORT: user_input.get(CONF_PORT), + CONF_VERIFY_SSL: user_input.get(CONF_VERIFY_SSL), + CONF_SITE_ID: DEFAULT_SITE_ID, + } + controller = await get_controller(self.hass, **self.config) + + self.sites = await controller.sites() + + return await self.async_step_site() + + except AuthenticationRequired: + errors['base'] = 'faulty_credentials' + + except CannotConnect: + errors['base'] = 'service_unavailable' + + except Exception: # pylint: disable=broad-except + LOGGER.error( + 'Unknown error connecting with UniFi Controller at %s', + user_input[CONF_HOST]) + return self.async_abort(reason='unknown') + + return self.async_show_form( + step_id='user', + data_schema=vol.Schema({ + vol.Required(CONF_HOST): str, + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): int, + vol.Optional( + CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): bool, + }), + errors=errors, + ) + + async def async_step_site(self, user_input=None): + """Select site to control.""" + errors = {} + + if user_input is not None: + + try: + desc = user_input.get(CONF_SITE_ID, self.desc) + for site in self.sites.values(): + if desc == site['desc']: + if site['role'] != 'admin': + raise UserLevel + self.config[CONF_SITE_ID] = site['name'] + break + + for entry in self._async_current_entries(): + controller = entry.data[CONF_CONTROLLER] + if controller[CONF_HOST] == self.config[CONF_HOST] and \ + controller[CONF_SITE_ID] == self.config[CONF_SITE_ID]: + raise AlreadyConfigured + + data = { + CONF_CONTROLLER: self.config, + CONF_POE_CONTROL: True + } + + return self.async_create_entry( + title=desc, + data=data + ) + + except AlreadyConfigured: + return self.async_abort(reason='already_configured') + + except UserLevel: + return self.async_abort(reason='user_privilege') + + if len(self.sites) == 1: + self.desc = next(iter(self.sites.values()))['desc'] + return await self.async_step_site(user_input={}) + + sites = [] + for site in self.sites.values(): + sites.append(site['desc']) + + return self.async_show_form( + step_id='site', + data_schema=vol.Schema({ + vol.Required(CONF_SITE_ID): vol.In(sites) + }), + errors=errors, + ) diff --git a/homeassistant/components/unifi/manifest.json b/homeassistant/components/unifi/manifest.json index 85a84539663..22ece5addaf 100644 --- a/homeassistant/components/unifi/manifest.json +++ b/homeassistant/components/unifi/manifest.json @@ -1,6 +1,7 @@ { "domain": "unifi", "name": "Unifi", + "config_flow": true, "documentation": "https://www.home-assistant.io/components/unifi", "requirements": [ "aiounifi==4", diff --git a/homeassistant/components/upnp/__init__.py b/homeassistant/components/upnp/__init__.py index fd2aa994ca4..219167366a5 100644 --- a/homeassistant/components/upnp/__init__.py +++ b/homeassistant/components/upnp/__init__.py @@ -7,7 +7,6 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.config_entries import ConfigEntry from homeassistant.const import EVENT_HOMEASSISTANT_STOP -from homeassistant.helpers import config_entry_flow from homeassistant.helpers import config_validation as cv from homeassistant.helpers import device_registry as dr from homeassistant.helpers import dispatcher @@ -204,10 +203,3 @@ async def async_unload_entry(hass: HomeAssistantType, dispatcher.async_dispatcher_send(hass, SIGNAL_REMOVE_SENSOR, device) return True - - -config_entry_flow.register_discovery_flow( - DOMAIN, - 'UPnP/IGD', - Device.async_discover, - config_entries.CONN_CLASS_LOCAL_POLL) diff --git a/homeassistant/components/upnp/config_flow.py b/homeassistant/components/upnp/config_flow.py new file mode 100644 index 00000000000..65a91858b57 --- /dev/null +++ b/homeassistant/components/upnp/config_flow.py @@ -0,0 +1,13 @@ +"""Config flow for UPNP.""" +from homeassistant.helpers import config_entry_flow +from homeassistant import config_entries + +from .const import DOMAIN +from .device import Device + + +config_entry_flow.register_discovery_flow( + DOMAIN, + 'UPnP/IGD', + Device.async_discover, + config_entries.CONN_CLASS_LOCAL_POLL) diff --git a/homeassistant/components/upnp/manifest.json b/homeassistant/components/upnp/manifest.json index 75213ecc9b9..4a189dc6dd1 100644 --- a/homeassistant/components/upnp/manifest.json +++ b/homeassistant/components/upnp/manifest.json @@ -1,6 +1,7 @@ { "domain": "upnp", "name": "Upnp", + "config_flow": true, "documentation": "https://www.home-assistant.io/components/upnp", "requirements": [ "async-upnp-client==0.14.7" diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index a8b459e9e96..a44550100f9 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -1,6 +1,7 @@ { "domain": "zha", "name": "Zigbee Home Automation", + "config_flow": true, "documentation": "https://www.home-assistant.io/components/zha", "requirements": [ "bellows-homeassistant==0.7.3", diff --git a/homeassistant/components/zone/manifest.json b/homeassistant/components/zone/manifest.json index 897908b61da..e9281fec3f7 100644 --- a/homeassistant/components/zone/manifest.json +++ b/homeassistant/components/zone/manifest.json @@ -1,6 +1,7 @@ { "domain": "zone", "name": "Zone", + "config_flow": true, "documentation": "https://www.home-assistant.io/components/zone", "requirements": [], "dependencies": [], diff --git a/homeassistant/components/zwave/manifest.json b/homeassistant/components/zwave/manifest.json index 598af58ad17..f88945fa281 100644 --- a/homeassistant/components/zwave/manifest.json +++ b/homeassistant/components/zwave/manifest.json @@ -1,6 +1,7 @@ { "domain": "zwave", "name": "Z-Wave", + "config_flow": true, "documentation": "https://www.home-assistant.io/components/zwave", "requirements": [ "homeassistant-pyozw==0.1.4", diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 6f4e57203f1..e96c10e17fa 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -140,57 +140,6 @@ SOURCE_DISCOVERY = 'discovery' SOURCE_IMPORT = 'import' HANDLERS = Registry() -# Components that have config flows. In future we will auto-generate this list. -FLOWS = [ - 'ambiclimate', - 'ambient_station', - 'axis', - 'cast', - 'daikin', - 'deconz', - 'dialogflow', - 'esphome', - 'emulated_roku', - 'geofency', - 'gpslogger', - 'hangouts', - 'heos', - 'homekit_controller', - 'homematicip_cloud', - 'hue', - 'ifttt', - 'ios', - 'ipma', - 'iqvia', - 'lifx', - 'locative', - 'logi_circle', - 'luftdaten', - 'mailgun', - 'mobile_app', - 'mqtt', - 'nest', - 'openuv', - 'owntracks', - 'point', - 'ps4', - 'rainmachine', - 'simplisafe', - 'smartthings', - 'smhi', - 'sonos', - 'tellduslive', - 'toon', - 'tplink', - 'tradfri', - 'twilio', - 'unifi', - 'upnp', - 'zha', - 'zone', - 'zwave', -] - STORAGE_KEY = 'core.config_entries' STORAGE_VERSION = 1 @@ -299,7 +248,17 @@ class ConfigEntry: if integration is None: integration = await loader.async_get_integration(hass, self.domain) - component = integration.get_component() + try: + component = integration.get_component() + if self.domain == integration.domain: + integration.get_platform('config_flow') + except ImportError as err: + _LOGGER.error( + 'Error importing integration %s to set up %s config entry: %s', + integration.domain, self.domain, err) + if self.domain == integration.domain: + self.state = ENTRY_STATE_SETUP_ERROR + return # Perform migration if integration.domain == self.domain: @@ -422,7 +381,8 @@ class ConfigEntry: if self.version == handler.VERSION: return True - component = getattr(hass.components, self.domain) + integration = await loader.async_get_integration(hass, self.domain) + component = integration.get_component() supports_migrate = hasattr(component, 'async_migrate_entry') if not supports_migrate: _LOGGER.error("Migration handler not found for entry %s for %s", @@ -430,7 +390,9 @@ class ConfigEntry: return False try: - result = await component.async_migrate_entry(hass, self) + result = await component.async_migrate_entry( # type: ignore + hass, self + ) if not isinstance(result, bool): _LOGGER.error('%s.async_migrate_entry did not return boolean', self.domain) @@ -441,7 +403,7 @@ class ConfigEntry: return result except Exception: # pylint: disable=broad-except _LOGGER.exception('Error migrating entry %s for %s', - self.title, component.DOMAIN) + self.title, self.domain) return False def add_update_listener(self, listener: Callable) -> Callable: @@ -714,10 +676,10 @@ class ConfigEntries: self.hass, self._hass_config, integration) try: - integration.get_component() + integration.get_platform('config_flow') except ImportError as err: _LOGGER.error( - 'Error occurred while loading integration %s: %s', + 'Error occurred loading config flow for integration %s: %s', handler_key, err) raise data_entry_flow.UnknownHandler diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py new file mode 100644 index 00000000000..c9a8c593b27 --- /dev/null +++ b/homeassistant/generated/config_flows.py @@ -0,0 +1,55 @@ +"""Automatically generated by hassfest. + +To update, run python3 -m hassfest +""" + + +FLOWS = [ + "ambiclimate", + "ambient_station", + "axis", + "cast", + "daikin", + "deconz", + "dialogflow", + "emulated_roku", + "esphome", + "geofency", + "gpslogger", + "hangouts", + "heos", + "homekit_controller", + "homematicip_cloud", + "hue", + "ifttt", + "ios", + "ipma", + "iqvia", + "lifx", + "locative", + "logi_circle", + "luftdaten", + "mailgun", + "mobile_app", + "mqtt", + "nest", + "openuv", + "owntracks", + "point", + "ps4", + "rainmachine", + "simplisafe", + "smartthings", + "smhi", + "sonos", + "tellduslive", + "toon", + "tplink", + "tradfri", + "twilio", + "unifi", + "upnp", + "zha", + "zone", + "zwave" +] diff --git a/homeassistant/helpers/translation.py b/homeassistant/helpers/translation.py index 4f655e692f7..f008551c0fa 100644 --- a/homeassistant/helpers/translation.py +++ b/homeassistant/helpers/translation.py @@ -2,9 +2,9 @@ import logging from typing import Any, Dict, Iterable, Optional -from homeassistant import config_entries from homeassistant.loader import async_get_integration, bind_hass from homeassistant.util.json import load_json +from homeassistant.generated import config_flows from .typing import HomeAssistantType _LOGGER = logging.getLogger(__name__) @@ -106,7 +106,7 @@ async def async_get_component_resources(hass: HomeAssistantType, translation_cache = hass.data[TRANSLATION_STRING_CACHE][language] # Get the set of components - components = hass.config.components | set(config_entries.FLOWS) + components = hass.config.components | set(config_flows.FLOWS) # Calculate the missing components missing_components = components - set(translation_cache) diff --git a/pylintrc b/pylintrc index 7d349033f70..1ba0bf2c82a 100644 --- a/pylintrc +++ b/pylintrc @@ -1,3 +1,9 @@ +[MASTER] +ignore=tests + +[BASIC] +good-names=i,j,k,ex,Run,_,fp + [MESSAGES CONTROL] # Reasons disabled: # locally-disabled - it spams too much diff --git a/script/hassfest/__main__.py b/script/hassfest/__main__.py index bca419126db..9e7797201ea 100644 --- a/script/hassfest/__main__.py +++ b/script/hassfest/__main__.py @@ -3,13 +3,14 @@ import pathlib import sys from .model import Integration, Config -from . import dependencies, manifest, codeowners, services +from . import dependencies, manifest, codeowners, services, config_flow PLUGINS = [ manifest, dependencies, codeowners, services, + config_flow, ] diff --git a/script/hassfest/config_flow.py b/script/hassfest/config_flow.py new file mode 100644 index 00000000000..2f204227f25 --- /dev/null +++ b/script/hassfest/config_flow.py @@ -0,0 +1,85 @@ +"""Generate config flow file.""" +import json +from typing import Dict + +from .model import Integration, Config + +BASE = """ +\"\"\"Automatically generated by hassfest. + +To update, run python3 -m hassfest +\"\"\" + + +FLOWS = {} +""".strip() + + +def validate_integration(integration: Integration): + """Validate we can load config flow without installing requirements.""" + if not (integration.path / "config_flow.py").is_file(): + integration.add_error( + 'config_flow', + "Config flows need to be defined in the file config_flow.py") + + # Currently not require being able to load config flow without + # installing requirements. + # try: + # integration.import_pkg('config_flow') + # except ImportError as err: + # integration.add_error( + # 'config_flow', + # "Unable to import config flow: {}. Config flows should be able " + # "to be imported without installing requirements.".format(err)) + # return + + # if integration.domain not in config_entries.HANDLERS: + # integration.add_error( + # 'config_flow', + # "Importing the config flow platform did not register a config " + # "flow handler.") + + +def generate_and_validate(integrations: Dict[str, Integration]): + """Validate and generate config flow data.""" + domains = [] + + for domain in sorted(integrations): + integration = integrations[domain] + + if not integration.manifest: + continue + + config_flow = integration.manifest.get('config_flow') + + if not config_flow: + continue + + validate_integration(integration) + + domains.append(domain) + + return BASE.format(json.dumps(domains, indent=4)) + + +def validate(integrations: Dict[str, Integration], config: Config): + """Validate config flow file.""" + config_flow_path = config.root / 'homeassistant/generated/config_flows.py' + config.cache['config_flow'] = content = generate_and_validate(integrations) + + with open(str(config_flow_path), 'r') as fp: + if fp.read().strip() != content: + config.add_error( + "config_flow", + "File config_flows.py is not up to date. " + "Run python3 -m script.hassfest", + fixable=True + ) + return + + +def generate(integrations: Dict[str, Integration], config: Config): + """Generate config flow file.""" + config_flow_path = config.root / 'homeassistant/generated/config_flows.py' + with open(str(config_flow_path), 'w') as fp: + fp.write(config.cache['config_flow'] + '\n') diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index 30f89231299..789b5fc0b41 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -10,6 +10,7 @@ from .model import Integration MANIFEST_SCHEMA = vol.Schema({ vol.Required('domain'): str, vol.Required('name'): str, + vol.Optional('config_flow'): bool, vol.Required('documentation'): str, vol.Required('requirements'): [str], vol.Required('dependencies'): [str], diff --git a/script/hassfest/model.py b/script/hassfest/model.py index de252715992..4815522cf94 100644 --- a/script/hassfest/model.py +++ b/script/hassfest/model.py @@ -2,6 +2,7 @@ import json from typing import List, Dict, Any import pathlib +import importlib import attr @@ -92,3 +93,10 @@ class Integration: return self.manifest = manifest + + def import_pkg(self, platform=None): + """Import the Python file.""" + pkg = "homeassistant.components.{}".format(self.domain) + if platform is not None: + pkg += ".{}".format(platform) + return importlib.import_module(pkg) diff --git a/tests/common.py b/tests/common.py index f5a2b1327fe..572cd19a006 100644 --- a/tests/common.py +++ b/tests/common.py @@ -926,7 +926,7 @@ async def get_system_health_info(hass, domain): def mock_integration(hass, module): """Mock an integration.""" integration = loader.Integration( - hass, 'homeassisant.components.{}'.format(module.DOMAIN), None, + hass, 'homeassistant.components.{}'.format(module.DOMAIN), None, module.mock_manifest()) _LOGGER.info("Adding mock integration: %s", module.DOMAIN) diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 1b5a40ade8a..cdce7433398 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -12,9 +12,11 @@ from homeassistant.config_entries import HANDLERS from homeassistant.core import callback from homeassistant.setup import async_setup_component from homeassistant.components.config import config_entries +from homeassistant.generated import config_flows from tests.common import ( - MockConfigEntry, MockModule, mock_coro_func, mock_integration) + MockConfigEntry, MockModule, mock_coro_func, mock_integration, + mock_entity_platform) @pytest.fixture(autouse=True) @@ -121,7 +123,7 @@ async def test_remove_entry_unauth(hass, client, hass_admin_user): @asyncio.coroutine def test_available_flows(hass, client): """Test querying the available flows.""" - with patch.object(core_ce, 'FLOWS', ['hello', 'world']): + with patch.object(config_flows, 'FLOWS', ['hello', 'world']): resp = yield from client.get( '/api/config/config_entries/flow_handlers') assert resp.status == 200 @@ -137,6 +139,8 @@ def test_available_flows(hass, client): @asyncio.coroutine def test_initialize_flow(hass, client): """Test we can initialize a flow.""" + mock_entity_platform(hass, 'config_flow.test', None) + class TestFlow(core_ce.ConfigFlow): @asyncio.coroutine def async_step_user(self, user_input=None): @@ -221,6 +225,8 @@ async def test_initialize_flow_unauth(hass, client, hass_admin_user): @asyncio.coroutine def test_abort(hass, client): """Test a flow that aborts.""" + mock_entity_platform(hass, 'config_flow.test', None) + class TestFlow(core_ce.ConfigFlow): @asyncio.coroutine def async_step_user(self, user_input=None): @@ -244,6 +250,8 @@ def test_abort(hass, client): @asyncio.coroutine def test_create_account(hass, client): """Test a flow that creates an account.""" + mock_entity_platform(hass, 'config_flow.test', None) + mock_integration( hass, MockModule('test', async_setup_entry=mock_coro_func(True))) @@ -286,6 +294,7 @@ def test_two_step_flow(hass, client): mock_integration( hass, MockModule('test', async_setup_entry=mock_coro_func(True))) + mock_entity_platform(hass, 'config_flow.test', None) class TestFlow(core_ce.ConfigFlow): VERSION = 1 @@ -352,6 +361,7 @@ async def test_continue_flow_unauth(hass, client, hass_admin_user): mock_integration( hass, MockModule('test', async_setup_entry=mock_coro_func(True))) + mock_entity_platform(hass, 'config_flow.test', None) class TestFlow(core_ce.ConfigFlow): VERSION = 1 @@ -402,6 +412,8 @@ async def test_continue_flow_unauth(hass, client, hass_admin_user): @asyncio.coroutine def test_get_progress_index(hass, client): """Test querying for the flows that are in progress.""" + mock_entity_platform(hass, 'config_flow.test', None) + class TestFlow(core_ce.ConfigFlow): VERSION = 5 @@ -441,6 +453,8 @@ async def test_get_progress_index_unauth(hass, client, hass_admin_user): @asyncio.coroutine def test_get_progress_flow(hass, client): """Test we can query the API for same result as we get from init a flow.""" + mock_entity_platform(hass, 'config_flow.test', None) + class TestFlow(core_ce.ConfigFlow): @asyncio.coroutine def async_step_user(self, user_input=None): @@ -474,6 +488,8 @@ def test_get_progress_flow(hass, client): async def test_get_progress_flow_unauth(hass, client, hass_admin_user): """Test we can can't query the API for result of flow.""" + mock_entity_platform(hass, 'config_flow.test', None) + class TestFlow(core_ce.ConfigFlow): async def async_step_user(self, user_input=None): schema = OrderedDict() diff --git a/tests/components/unifi/test_controller.py b/tests/components/unifi/test_controller.py index e5e1d84bfcd..d1db25a23cd 100644 --- a/tests/components/unifi/test_controller.py +++ b/tests/components/unifi/test_controller.py @@ -4,24 +4,27 @@ from unittest.mock import Mock, patch import pytest from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.components import unifi +from homeassistant.components.unifi.const import ( + CONF_POE_CONTROL, CONF_CONTROLLER, CONF_SITE_ID) +from homeassistant.const import ( + CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME, CONF_VERIFY_SSL) from homeassistant.components.unifi import controller, errors from tests.common import mock_coro CONTROLLER_DATA = { - unifi.CONF_HOST: '1.2.3.4', - unifi.CONF_USERNAME: 'username', - unifi.CONF_PASSWORD: 'password', - unifi.CONF_PORT: 1234, - unifi.CONF_SITE_ID: 'site', - unifi.CONF_VERIFY_SSL: True + CONF_HOST: '1.2.3.4', + CONF_USERNAME: 'username', + CONF_PASSWORD: 'password', + CONF_PORT: 1234, + CONF_SITE_ID: 'site', + CONF_VERIFY_SSL: True } ENTRY_CONFIG = { - unifi.CONF_CONTROLLER: CONTROLLER_DATA, - unifi.CONF_POE_CONTROL: True - } + CONF_CONTROLLER: CONTROLLER_DATA, + CONF_POE_CONTROL: True +} async def test_controller_setup(): @@ -173,7 +176,7 @@ async def test_reset_unloads_entry_without_poe_control(): hass = Mock() entry = Mock() entry.data = dict(ENTRY_CONFIG) - entry.data[unifi.CONF_POE_CONTROL] = False + entry.data[CONF_POE_CONTROL] = False api = Mock() api.initialize.return_value = mock_coro(True) @@ -201,7 +204,7 @@ async def test_get_controller(hass): async def test_get_controller_verify_ssl_false(hass): """Successful call with verify ssl set to false.""" controller_data = dict(CONTROLLER_DATA) - controller_data[unifi.CONF_VERIFY_SSL] = False + controller_data[CONF_VERIFY_SSL] = False with patch('aiounifi.Controller.login', return_value=mock_coro()): assert await controller.get_controller(hass, **controller_data) diff --git a/tests/components/unifi/test_init.py b/tests/components/unifi/test_init.py index 0115801eec6..d2d19204b40 100644 --- a/tests/components/unifi/test_init.py +++ b/tests/components/unifi/test_init.py @@ -2,7 +2,12 @@ from unittest.mock import Mock, patch from homeassistant.components import unifi +from homeassistant.components.unifi import config_flow from homeassistant.setup import async_setup_component +from homeassistant.components.unifi.const import ( + CONF_POE_CONTROL, CONF_CONTROLLER, CONF_SITE_ID) +from homeassistant.const import ( + CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME, CONF_VERIFY_SSL) from tests.common import mock_coro, MockConfigEntry @@ -137,7 +142,7 @@ async def test_unload_entry(hass): async def test_flow_works(hass, aioclient_mock): """Test config flow.""" - flow = unifi.UnifiFlowHandler() + flow = config_flow.UnifiFlowHandler() flow.hass = hass with patch('aiounifi.Controller') as mock_controller: @@ -157,11 +162,11 @@ async def test_flow_works(hass, aioclient_mock): }) await flow.async_step_user(user_input={ - unifi.CONF_HOST: '1.2.3.4', - unifi.CONF_USERNAME: 'username', - unifi.CONF_PASSWORD: 'password', - unifi.CONF_PORT: 1234, - unifi.CONF_VERIFY_SSL: True + CONF_HOST: '1.2.3.4', + CONF_USERNAME: 'username', + CONF_PASSWORD: 'password', + CONF_PORT: 1234, + CONF_VERIFY_SSL: True }) result = await flow.async_step_site(user_input={}) @@ -173,27 +178,27 @@ async def test_flow_works(hass, aioclient_mock): assert result['type'] == 'create_entry' assert result['title'] == 'site name' assert result['data'] == { - unifi.CONF_CONTROLLER: { - unifi.CONF_HOST: '1.2.3.4', - unifi.CONF_USERNAME: 'username', - unifi.CONF_PASSWORD: 'password', - unifi.CONF_PORT: 1234, - unifi.CONF_SITE_ID: 'default', - unifi.CONF_VERIFY_SSL: True + CONF_CONTROLLER: { + CONF_HOST: '1.2.3.4', + CONF_USERNAME: 'username', + CONF_PASSWORD: 'password', + CONF_PORT: 1234, + CONF_SITE_ID: 'default', + CONF_VERIFY_SSL: True }, - unifi.CONF_POE_CONTROL: True + CONF_POE_CONTROL: True } async def test_controller_multiple_sites(hass): """Test config flow.""" - flow = unifi.UnifiFlowHandler() + flow = config_flow.UnifiFlowHandler() flow.hass = hass flow.config = { - unifi.CONF_HOST: '1.2.3.4', - unifi.CONF_USERNAME: 'username', - unifi.CONF_PASSWORD: 'password', + CONF_HOST: '1.2.3.4', + CONF_USERNAME: 'username', + CONF_PASSWORD: 'password', } flow.sites = { 'site1': { @@ -215,7 +220,7 @@ async def test_controller_multiple_sites(hass): async def test_controller_site_already_configured(hass): """Test config flow.""" - flow = unifi.UnifiFlowHandler() + flow = config_flow.UnifiFlowHandler() flow.hass = hass entry = MockConfigEntry(domain=unifi.DOMAIN, data={ @@ -227,9 +232,9 @@ async def test_controller_site_already_configured(hass): entry.add_to_hass(hass) flow.config = { - unifi.CONF_HOST: '1.2.3.4', - unifi.CONF_USERNAME: 'username', - unifi.CONF_PASSWORD: 'password', + CONF_HOST: '1.2.3.4', + CONF_USERNAME: 'username', + CONF_PASSWORD: 'password', } flow.desc = 'site name' flow.sites = { @@ -245,7 +250,7 @@ async def test_controller_site_already_configured(hass): async def test_user_permissions_low(hass, aioclient_mock): """Test config flow.""" - flow = unifi.UnifiFlowHandler() + flow = config_flow.UnifiFlowHandler() flow.hass = hass with patch('aiounifi.Controller') as mock_controller: @@ -265,11 +270,11 @@ async def test_user_permissions_low(hass, aioclient_mock): }) await flow.async_step_user(user_input={ - unifi.CONF_HOST: '1.2.3.4', - unifi.CONF_USERNAME: 'username', - unifi.CONF_PASSWORD: 'password', - unifi.CONF_PORT: 1234, - unifi.CONF_VERIFY_SSL: True + CONF_HOST: '1.2.3.4', + CONF_USERNAME: 'username', + CONF_PASSWORD: 'password', + CONF_PORT: 1234, + CONF_VERIFY_SSL: True }) result = await flow.async_step_site(user_input={}) @@ -279,16 +284,16 @@ async def test_user_permissions_low(hass, aioclient_mock): async def test_user_credentials_faulty(hass, aioclient_mock): """Test config flow.""" - flow = unifi.UnifiFlowHandler() + flow = config_flow.UnifiFlowHandler() flow.hass = hass - with patch.object(unifi, 'get_controller', + with patch.object(config_flow, 'get_controller', side_effect=unifi.errors.AuthenticationRequired): result = await flow.async_step_user({ - unifi.CONF_HOST: '1.2.3.4', - unifi.CONF_USERNAME: 'username', - unifi.CONF_PASSWORD: 'password', - unifi.CONF_SITE_ID: 'default', + CONF_HOST: '1.2.3.4', + CONF_USERNAME: 'username', + CONF_PASSWORD: 'password', + CONF_SITE_ID: 'default', }) assert result['type'] == 'form' @@ -297,16 +302,16 @@ async def test_user_credentials_faulty(hass, aioclient_mock): async def test_controller_is_unavailable(hass, aioclient_mock): """Test config flow.""" - flow = unifi.UnifiFlowHandler() + flow = config_flow.UnifiFlowHandler() flow.hass = hass - with patch.object(unifi, 'get_controller', + with patch.object(config_flow, 'get_controller', side_effect=unifi.errors.CannotConnect): result = await flow.async_step_user({ - unifi.CONF_HOST: '1.2.3.4', - unifi.CONF_USERNAME: 'username', - unifi.CONF_PASSWORD: 'password', - unifi.CONF_SITE_ID: 'default', + CONF_HOST: '1.2.3.4', + CONF_USERNAME: 'username', + CONF_PASSWORD: 'password', + CONF_SITE_ID: 'default', }) assert result['type'] == 'form' @@ -315,16 +320,16 @@ async def test_controller_is_unavailable(hass, aioclient_mock): async def test_controller_unkown_problem(hass, aioclient_mock): """Test config flow.""" - flow = unifi.UnifiFlowHandler() + flow = config_flow.UnifiFlowHandler() flow.hass = hass - with patch.object(unifi, 'get_controller', + with patch.object(config_flow, 'get_controller', side_effect=Exception): result = await flow.async_step_user({ - unifi.CONF_HOST: '1.2.3.4', - unifi.CONF_USERNAME: 'username', - unifi.CONF_PASSWORD: 'password', - unifi.CONF_SITE_ID: 'default', + CONF_HOST: '1.2.3.4', + CONF_USERNAME: 'username', + CONF_PASSWORD: 'password', + CONF_SITE_ID: 'default', }) assert result['type'] == 'abort' diff --git a/tests/components/unifi/test_switch.py b/tests/components/unifi/test_switch.py index 67f1e416cf1..5a04b415f5d 100644 --- a/tests/components/unifi/test_switch.py +++ b/tests/components/unifi/test_switch.py @@ -10,7 +10,11 @@ from aiounifi.devices import Devices from homeassistant import config_entries from homeassistant.components import unifi +from homeassistant.components.unifi.const import ( + CONF_POE_CONTROL, CONF_CONTROLLER, CONF_SITE_ID) from homeassistant.setup import async_setup_component +from homeassistant.const import ( + CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME, CONF_VERIFY_SSL) import homeassistant.components.switch as switch @@ -167,17 +171,17 @@ DEVICE_1 = { } CONTROLLER_DATA = { - unifi.CONF_HOST: 'mock-host', - unifi.CONF_USERNAME: 'mock-user', - unifi.CONF_PASSWORD: 'mock-pswd', - unifi.CONF_PORT: 1234, - unifi.CONF_SITE_ID: 'mock-site', - unifi.CONF_VERIFY_SSL: True + CONF_HOST: 'mock-host', + CONF_USERNAME: 'mock-user', + CONF_PASSWORD: 'mock-pswd', + CONF_PORT: 1234, + CONF_SITE_ID: 'mock-site', + CONF_VERIFY_SSL: True } ENTRY_CONFIG = { - unifi.CONF_CONTROLLER: CONTROLLER_DATA, - unifi.CONF_POE_CONTROL: True + CONF_CONTROLLER: CONTROLLER_DATA, + CONF_POE_CONTROL: True } CONTROLLER_ID = unifi.CONTROLLER_ID.format(host='mock-host', site='mock-site') diff --git a/tests/helpers/test_config_entry_flow.py b/tests/helpers/test_config_entry_flow.py index 04c91cfdc08..5f8a642333a 100644 --- a/tests/helpers/test_config_entry_flow.py +++ b/tests/helpers/test_config_entry_flow.py @@ -6,7 +6,8 @@ import pytest from homeassistant import config_entries, data_entry_flow, setup from homeassistant.helpers import config_entry_flow from tests.common import ( - MockConfigEntry, MockModule, mock_coro, mock_integration) + MockConfigEntry, MockModule, mock_coro, mock_integration, + mock_entity_platform) @pytest.fixture @@ -102,7 +103,7 @@ async def test_discovery_confirmation(hass, discovery_flow_conf): async def test_multiple_discoveries(hass, discovery_flow_conf): """Test we only create one instance for multiple discoveries.""" - mock_integration(hass, MockModule('test')) + mock_entity_platform(hass, 'config_flow.test', None) result = await hass.config_entries.flow.async_init( 'test', context={'source': config_entries.SOURCE_DISCOVERY}, data={}) @@ -116,7 +117,7 @@ async def test_multiple_discoveries(hass, discovery_flow_conf): async def test_only_one_in_progress(hass, discovery_flow_conf): """Test a user initialized one will finish and cancel discovered one.""" - mock_integration(hass, MockModule('test')) + mock_entity_platform(hass, 'config_flow.test', None) # Discovery starts flow result = await hass.config_entries.flow.async_init( @@ -209,6 +210,7 @@ async def test_webhook_create_cloudhook(hass, webhook_flow_conf): async_unload_entry=async_unload_entry, async_remove_entry=config_entry_flow.webhook_async_remove_entry, )) + mock_entity_platform(hass, 'config_flow.test_single', None) result = await hass.config_entries.flow.async_init( 'test_single', context={'source': config_entries.SOURCE_USER}) diff --git a/tests/helpers/test_translation.py b/tests/helpers/test_translation.py index ebf883bfe12..de871c6f474 100644 --- a/tests/helpers/test_translation.py +++ b/tests/helpers/test_translation.py @@ -5,9 +5,9 @@ from unittest.mock import patch import pytest -from homeassistant import config_entries import homeassistant.helpers.translation as translation from homeassistant.setup import async_setup_component +from homeassistant.generated import config_flows from tests.common import mock_coro @@ -15,7 +15,7 @@ from tests.common import mock_coro def mock_config_flows(): """Mock the config flows.""" flows = [] - with patch.object(config_entries, 'FLOWS', flows): + with patch.object(config_flows, 'FLOWS', flows): yield flows diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index a8a1211f4c2..9de31a6d5ca 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -53,6 +53,7 @@ async def test_call_setup_entry(hass): hass, MockModule('comp', async_setup_entry=mock_setup_entry, async_migrate_entry=mock_migrate_entry)) + mock_entity_platform(hass, 'config_flow.comp', None) result = await async_setup_component(hass, 'comp', {}) assert result @@ -74,6 +75,7 @@ async def test_call_async_migrate_entry(hass): hass, MockModule('comp', async_setup_entry=mock_setup_entry, async_migrate_entry=mock_migrate_entry)) + mock_entity_platform(hass, 'config_flow.comp', None) result = await async_setup_component(hass, 'comp', {}) assert result @@ -95,6 +97,7 @@ async def test_call_async_migrate_entry_failure_false(hass): hass, MockModule('comp', async_setup_entry=mock_setup_entry, async_migrate_entry=mock_migrate_entry)) + mock_entity_platform(hass, 'config_flow.comp', None) result = await async_setup_component(hass, 'comp', {}) assert result @@ -117,6 +120,7 @@ async def test_call_async_migrate_entry_failure_exception(hass): hass, MockModule('comp', async_setup_entry=mock_setup_entry, async_migrate_entry=mock_migrate_entry)) + mock_entity_platform(hass, 'config_flow.comp', None) result = await async_setup_component(hass, 'comp', {}) assert result @@ -139,6 +143,7 @@ async def test_call_async_migrate_entry_failure_not_bool(hass): hass, MockModule('comp', async_setup_entry=mock_setup_entry, async_migrate_entry=mock_migrate_entry)) + mock_entity_platform(hass, 'config_flow.comp', None) result = await async_setup_component(hass, 'comp', {}) assert result @@ -158,6 +163,7 @@ async def test_call_async_migrate_entry_failure_not_supported(hass): mock_integration( hass, MockModule('comp', async_setup_entry=mock_setup_entry)) + mock_entity_platform(hass, 'config_flow.comp', None) result = await async_setup_component(hass, 'comp', {}) assert result @@ -201,6 +207,7 @@ async def test_remove_entry(hass, manager): mock_entity_platform( hass, 'light.test', MockPlatform(async_setup_entry=mock_setup_entry_platform)) + mock_entity_platform(hass, 'config_flow.test', None) MockConfigEntry( domain='test_other', entry_id='test1' @@ -361,6 +368,7 @@ def test_add_entry_calls_setup_entry(hass, manager): mock_integration( hass, MockModule('comp', async_setup_entry=mock_setup_entry)) + mock_entity_platform(hass, 'config_flow.comp', None) class TestFlow(config_entries.ConfigFlow): @@ -416,6 +424,7 @@ async def test_saving_and_loading(hass): """Test that we're saving and loading correctly.""" mock_integration(hass, MockModule( 'test', async_setup_entry=lambda *args: mock_coro(True))) + mock_entity_platform(hass, 'config_flow.test', None) class TestFlow(config_entries.ConfigFlow): VERSION = 5 @@ -511,6 +520,7 @@ async def test_forward_entry_does_not_setup_entry_if_setup_fails(hass): async def test_discovery_notification(hass): """Test that we create/dismiss a notification when source is discovery.""" mock_integration(hass, MockModule('test')) + mock_entity_platform(hass, 'config_flow.test', None) await async_setup_component(hass, 'persistent_notification', {}) class TestFlow(config_entries.ConfigFlow): @@ -548,6 +558,7 @@ async def test_discovery_notification(hass): async def test_discovery_notification_not_created(hass): """Test that we not create a notification when discovery is aborted.""" mock_integration(hass, MockModule('test')) + mock_entity_platform(hass, 'config_flow.test', None) await async_setup_component(hass, 'persistent_notification', {}) class TestFlow(config_entries.ConfigFlow): @@ -629,6 +640,7 @@ async def test_setup_raise_not_ready(hass, caplog): mock_setup_entry = MagicMock(side_effect=ConfigEntryNotReady) mock_integration( hass, MockModule('test', async_setup_entry=mock_setup_entry)) + mock_entity_platform(hass, 'config_flow.test', None) with patch('homeassistant.helpers.event.async_call_later') as mock_call: await entry.async_setup(hass) @@ -655,6 +667,7 @@ async def test_setup_retrying_during_unload(hass): mock_setup_entry = MagicMock(side_effect=ConfigEntryNotReady) mock_integration( hass, MockModule('test', async_setup_entry=mock_setup_entry)) + mock_entity_platform(hass, 'config_flow.test', None) with patch('homeassistant.helpers.event.async_call_later') as mock_call: await entry.async_setup(hass) @@ -720,6 +733,7 @@ async def test_entry_setup_succeed(hass, manager): async_setup=mock_setup, async_setup_entry=mock_setup_entry )) + mock_entity_platform(hass, 'config_flow.comp', None) assert await manager.async_setup(entry.entry_id) assert len(mock_setup.mock_calls) == 1 @@ -848,6 +862,7 @@ async def test_entry_reload_succeed(hass, manager): async_setup_entry=async_setup_entry, async_unload_entry=async_unload_entry )) + mock_entity_platform(hass, 'config_flow.comp', None) assert await manager.async_reload(entry.entry_id) assert len(async_unload_entry.mock_calls) == 1 @@ -879,6 +894,7 @@ async def test_entry_reload_not_loaded(hass, manager, state): async_setup_entry=async_setup_entry, async_unload_entry=async_unload_entry )) + mock_entity_platform(hass, 'config_flow.comp', None) assert await manager.async_reload(entry.entry_id) assert len(async_unload_entry.mock_calls) == 0 From 1a051f038d9040157f0b35e0ade9ada1f303cde6 Mon Sep 17 00:00:00 2001 From: Baptiste Candellier Date: Mon, 13 May 2019 12:35:31 +0200 Subject: [PATCH 033/232] Add new SmartHab light and cover platform (#21225) * Add SmartHab platform * Remove url config entry, improve error handling * Upgrade smarthab dependency * Address comments * Lint --- .coveragerc | 1 + CODEOWNERS | 1 + homeassistant/components/smarthab/__init__.py | 61 +++++++++++ homeassistant/components/smarthab/cover.py | 100 ++++++++++++++++++ homeassistant/components/smarthab/light.py | 70 ++++++++++++ .../components/smarthab/manifest.json | 10 ++ requirements_all.txt | 3 + 7 files changed, 246 insertions(+) create mode 100644 homeassistant/components/smarthab/__init__.py create mode 100644 homeassistant/components/smarthab/cover.py create mode 100644 homeassistant/components/smarthab/light.py create mode 100644 homeassistant/components/smarthab/manifest.json diff --git a/.coveragerc b/.coveragerc index ffe89740432..353aeaf5684 100644 --- a/.coveragerc +++ b/.coveragerc @@ -538,6 +538,7 @@ omit = homeassistant/components/slack/notify.py homeassistant/components/sma/sensor.py homeassistant/components/smappee/* + homeassistant/components/smarthab/* homeassistant/components/smtp/notify.py homeassistant/components/snapcast/media_player.py homeassistant/components/snmp/* diff --git a/CODEOWNERS b/CODEOWNERS index 90fb72378bc..cc4138b362d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -205,6 +205,7 @@ homeassistant/components/shiftr/* @fabaff homeassistant/components/shodan/* @fabaff homeassistant/components/simplisafe/* @bachya homeassistant/components/sma/* @kellerza +homeassistant/components/smarthab/* @outadoc homeassistant/components/smartthings/* @andrewsayre homeassistant/components/smtp/* @fabaff homeassistant/components/sonos/* @amelchio diff --git a/homeassistant/components/smarthab/__init__.py b/homeassistant/components/smarthab/__init__.py new file mode 100644 index 00000000000..af592b60a91 --- /dev/null +++ b/homeassistant/components/smarthab/__init__.py @@ -0,0 +1,61 @@ +""" +Support for SmartHab device integration. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/smarthab/ +""" +import logging + +import voluptuous as vol + +from homeassistant.helpers.discovery import load_platform +from homeassistant.const import CONF_EMAIL, CONF_PASSWORD +import homeassistant.helpers.config_validation as cv + +DOMAIN = 'smarthab' +DATA_HUB = 'hub' + +_LOGGER = logging.getLogger(__name__) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_EMAIL): cv.string, + vol.Required(CONF_PASSWORD): cv.string + }), +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config) -> bool: + """Set up the SmartHab platform.""" + import pysmarthab + + sh_conf = config.get(DOMAIN) + + # Assign configuration variables + username = sh_conf[CONF_EMAIL] + password = sh_conf[CONF_PASSWORD] + + # Setup connection with SmartHab API + hub = pysmarthab.SmartHab() + + try: + hub.login(username, password) + except pysmarthab.RequestFailedException as ex: + _LOGGER.error("Error while trying to reach SmartHab API.") + _LOGGER.debug(ex, exc_info=True) + return False + + # Verify that passed in configuration works + if not hub.is_logged_in(): + _LOGGER.error("Could not authenticate with SmartHab API") + return False + + # Pass hub object to child platforms + hass.data[DOMAIN] = { + DATA_HUB: hub + } + + load_platform(hass, 'light', DOMAIN, None, config) + load_platform(hass, 'cover', DOMAIN, None, config) + + return True diff --git a/homeassistant/components/smarthab/cover.py b/homeassistant/components/smarthab/cover.py new file mode 100644 index 00000000000..0b8cc0604e7 --- /dev/null +++ b/homeassistant/components/smarthab/cover.py @@ -0,0 +1,100 @@ +""" +Support for SmartHab device integration. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/smarthab/ +""" +import logging +from datetime import timedelta +from requests.exceptions import Timeout + +from homeassistant.components.cover import ( + CoverDevice, SUPPORT_OPEN, SUPPORT_CLOSE, SUPPORT_SET_POSITION, + ATTR_POSITION +) +from . import DOMAIN, DATA_HUB + +_LOGGER = logging.getLogger(__name__) + +SCAN_INTERVAL = timedelta(seconds=60) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the SmartHab roller shutters platform.""" + import pysmarthab + + hub = hass.data[DOMAIN][DATA_HUB] + devices = hub.get_device_list() + + _LOGGER.debug("Found a total of %s devices", str(len(devices))) + + entities = (SmartHabCover(cover) + for cover in devices if isinstance(cover, pysmarthab.Shutter)) + + add_entities(entities, True) + + +class SmartHabCover(CoverDevice): + """Representation a cover.""" + + def __init__(self, cover): + """Initialize a SmartHabCover.""" + self._cover = cover + + @property + def unique_id(self) -> str: + """Return a unique ID.""" + return self._cover.device_id + + @property + def name(self) -> str: + """Return the display name of this light.""" + return self._cover.label + + @property + def current_cover_position(self) -> int: + """Return current position of cover. + + None is unknown, 0 is closed, 100 is fully open. + """ + return self._cover.state + + @property + def supported_features(self) -> int: + """Flag supported features.""" + supported_features = SUPPORT_OPEN | SUPPORT_CLOSE + + if self.current_cover_position is not None: + supported_features |= SUPPORT_SET_POSITION + + return supported_features + + @property + def is_closed(self) -> bool: + """Return if the cover is closed or not.""" + return self._cover.state == 0 + + @property + def device_class(self) -> str: + """Return the class of this device, from component DEVICE_CLASSES.""" + return 'window' + + def open_cover(self, **kwargs): + """Open the cover.""" + self._cover.open() + + def close_cover(self, **kwargs): + """Close cover.""" + self._cover.close() + + def set_cover_position(self, **kwargs): + """Move the cover to a specific position.""" + self._cover.state = kwargs[ATTR_POSITION] + + def update(self): + """Fetch new state data for this cover.""" + try: + self._cover.update() + except Timeout: + _LOGGER.error("Reached timeout while updating cover %s from API", + self.entity_id) diff --git a/homeassistant/components/smarthab/light.py b/homeassistant/components/smarthab/light.py new file mode 100644 index 00000000000..9be49912a49 --- /dev/null +++ b/homeassistant/components/smarthab/light.py @@ -0,0 +1,70 @@ +""" +Support for SmartHab device integration. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/smarthab/ +""" +import logging +from datetime import timedelta +from requests.exceptions import Timeout + +from homeassistant.components.light import Light +from . import DOMAIN, DATA_HUB + +_LOGGER = logging.getLogger(__name__) + +SCAN_INTERVAL = timedelta(seconds=60) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the SmartHab lights platform.""" + import pysmarthab + + hub = hass.data[DOMAIN][DATA_HUB] + devices = hub.get_device_list() + + _LOGGER.debug("Found a total of %s devices", str(len(devices))) + + entities = (SmartHabLight(light) + for light in devices if isinstance(light, pysmarthab.Light)) + + add_entities(entities, True) + + +class SmartHabLight(Light): + """Representation of a SmartHab Light.""" + + def __init__(self, light): + """Initialize a SmartHabLight.""" + self._light = light + + @property + def unique_id(self) -> str: + """Return a unique ID.""" + return self._light.device_id + + @property + def name(self) -> str: + """Return the display name of this light.""" + return self._light.label + + @property + def is_on(self) -> bool: + """Return true if light is on.""" + return self._light.state + + def turn_on(self, **kwargs): + """Instruct the light to turn on.""" + self._light.turn_on() + + def turn_off(self, **kwargs): + """Instruct the light to turn off.""" + self._light.turn_off() + + def update(self): + """Fetch new state data for this light.""" + try: + self._light.update() + except Timeout: + _LOGGER.error("Reached timeout while updating light %s from API", + self.entity_id) diff --git a/homeassistant/components/smarthab/manifest.json b/homeassistant/components/smarthab/manifest.json new file mode 100644 index 00000000000..18b587bac92 --- /dev/null +++ b/homeassistant/components/smarthab/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "smarthab", + "name": "SmartHab", + "documentation": "https://www.home-assistant.io/components/smarthab", + "requirements": [ + "smarthab==0.20" + ], + "dependencies": [], + "codeowners": ["@outadoc"] +} \ No newline at end of file diff --git a/requirements_all.txt b/requirements_all.txt index 5a8a72c7cd4..a94dae8ce5a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1617,6 +1617,9 @@ slixmpp==1.4.2 # homeassistant.components.smappee smappy==0.2.16 +# homeassistant.components.smarthab +smarthab==0.20 + # homeassistant.components.bh1750 # homeassistant.components.bme280 # homeassistant.components.bme680 From 0ffcc197d4ff13e940f49e716c20316764652f2a Mon Sep 17 00:00:00 2001 From: Fredrik Erlandsson Date: Mon, 13 May 2019 15:38:33 +0200 Subject: [PATCH 034/232] Daikin adaptions for AirBase units (#23734) * updated list of supported fan_modes * AirBase units does not support Holiday-mode * AirBase units does not support outside temp * pydaikin version bump * don't modify constant --- homeassistant/components/daikin/climate.py | 12 +++++------- homeassistant/components/daikin/manifest.json | 2 +- homeassistant/components/daikin/sensor.py | 5 ++++- requirements_all.txt | 2 +- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/daikin/climate.py b/homeassistant/components/daikin/climate.py index 7ea4e117743..239aa78bfbf 100644 --- a/homeassistant/components/daikin/climate.py +++ b/homeassistant/components/daikin/climate.py @@ -81,12 +81,7 @@ class DaikinClimate(ClimateDevice): self._api = api self._list = { ATTR_OPERATION_MODE: list(HA_STATE_TO_DAIKIN), - ATTR_FAN_MODE: list( - map( - str.title, - appliance.daikin_values(HA_ATTR_TO_DAIKIN[ATTR_FAN_MODE]) - ) - ), + ATTR_FAN_MODE: self._api.device.fan_modes, ATTR_SWING_MODE: list( map( str.title, @@ -95,10 +90,13 @@ class DaikinClimate(ClimateDevice): ), } - self._supported_features = (SUPPORT_AWAY_MODE | SUPPORT_ON_OFF + self._supported_features = (SUPPORT_ON_OFF | SUPPORT_OPERATION_MODE | SUPPORT_TARGET_TEMPERATURE) + if self._api.device.support_away_mode: + self._supported_features |= SUPPORT_AWAY_MODE + if self._api.device.support_fan_mode: self._supported_features |= SUPPORT_FAN_MODE diff --git a/homeassistant/components/daikin/manifest.json b/homeassistant/components/daikin/manifest.json index 9891cce3b3e..a0141dd1f99 100644 --- a/homeassistant/components/daikin/manifest.json +++ b/homeassistant/components/daikin/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/components/daikin", "requirements": [ - "pydaikin==1.4.0" + "pydaikin==1.4.3" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/daikin/sensor.py b/homeassistant/components/daikin/sensor.py index c4f885f5081..8196acc5cf7 100644 --- a/homeassistant/components/daikin/sensor.py +++ b/homeassistant/components/daikin/sensor.py @@ -26,9 +26,12 @@ async def async_setup_platform( async def async_setup_entry(hass, entry, async_add_entities): """Set up Daikin climate based on config_entry.""" daikin_api = hass.data[DAIKIN_DOMAIN].get(entry.entry_id) + sensors = [ATTR_INSIDE_TEMPERATURE] + if daikin_api.device.support_outside_temperature: + sensors.append(ATTR_OUTSIDE_TEMPERATURE) async_add_entities([ DaikinClimateSensor(daikin_api, sensor, hass.config.units) - for sensor in SENSOR_TYPES + for sensor in sensors ]) diff --git a/requirements_all.txt b/requirements_all.txt index a94dae8ce5a..43d66a4c01b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1021,7 +1021,7 @@ pycsspeechtts==1.0.2 # pycups==1.9.73 # homeassistant.components.daikin -pydaikin==1.4.0 +pydaikin==1.4.3 # homeassistant.components.danfoss_air pydanfossair==0.1.0 From 990a9e80a2149d494ee5b79292df1071e9d34d95 Mon Sep 17 00:00:00 2001 From: damarco Date: Mon, 13 May 2019 19:13:57 +0200 Subject: [PATCH 035/232] Fix zha timed off (#23849) --- homeassistant/components/zha/core/channels/general.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zha/core/channels/general.py b/homeassistant/components/zha/core/channels/general.py index 031eb2464da..470cd6b38cf 100644 --- a/homeassistant/components/zha/core/channels/general.py +++ b/homeassistant/components/zha/core/channels/general.py @@ -27,6 +27,7 @@ class OnOffChannel(ZigbeeChannel): """Initialize OnOffChannel.""" super().__init__(cluster, device) self._state = None + self._off_listener = None @callback def cluster_command(self, tsn, command_id, args): @@ -48,9 +49,12 @@ class OnOffChannel(ZigbeeChannel): on_time = args[1] # 0 is always accept 1 is only accept when already on if should_accept == 0 or (should_accept == 1 and self._state): + if self._off_listener is not None: + self._off_listener() + self._off_listener = None self.attribute_updated(self.ON_OFF, True) if on_time > 0: - async_call_later( + self._off_listener = async_call_later( self.device.hass, (on_time / 10), # value is in 10ths of a second self.set_to_off @@ -61,6 +65,7 @@ class OnOffChannel(ZigbeeChannel): @callback def set_to_off(self, *_): """Set the state to off.""" + self._off_listener = None self.attribute_updated(self.ON_OFF, False) @callback From b2a1204bc5fe8006a5343248e769f2e5194798ea Mon Sep 17 00:00:00 2001 From: sander76 Date: Mon, 13 May 2019 20:52:55 +0200 Subject: [PATCH 036/232] Fix for battery device: new_device referenced before assignment. (#23793) * Fix for battery device: new_device referenced before assignment. * Fix buttons and switches mixup * Update __init__.py * Update binary_sensor.py * Update __init__.py * Update __init__.py * Update binary_sensor.py * Update __init__.py * Update binary_sensor.py * typo and indentation fixes * low_bat and lowbat to uppercase. --- .../components/homematic/__init__.py | 42 +++++++++---------- .../components/homematic/binary_sensor.py | 28 ++++--------- 2 files changed, 30 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index 7f6f9a6d522..b301e22597e 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -27,10 +27,9 @@ DISCOVER_BINARY_SENSORS = 'homematic.binary_sensor' DISCOVER_COVER = 'homematic.cover' DISCOVER_CLIMATE = 'homematic.climate' DISCOVER_LOCKS = 'homematic.locks' -DISCOVER_BUTTONS = 'homematic.binary_sensor' +DISCOVER_BATTERY = 'homematic.battery' ATTR_DISCOVER_DEVICES = 'devices' -ATTR_BATTERY_DEVICES = 'battery_devices' ATTR_PARAM = 'param' ATTR_CHANNEL = 'channel' ATTR_ADDRESS = 'address' @@ -43,6 +42,9 @@ ATTR_TIME = 'time' ATTR_UNIQUE_ID = 'unique_id' ATTR_PARAMSET_KEY = 'paramset_key' ATTR_PARAMSET = 'paramset' +ATTR_DISCOVERY_TYPE = 'discovery_type' +ATTR_LOW_BAT = 'LOW_BAT' +ATTR_LOWBAT = 'LOWBAT' EVENT_KEYPRESS = 'homematic.keypress' @@ -87,8 +89,7 @@ HM_DEVICE_TYPES = { 'SmartwareMotion', 'IPWeatherSensorPlus', 'MotionIPV2', 'WaterIP', 'IPMultiIO', 'TiltIP', 'IPShutterContactSabotage'], DISCOVER_COVER: ['Blind', 'KeyBlind', 'IPKeyBlind', 'IPKeyBlindTilt'], - DISCOVER_LOCKS: ['KeyMatic'], - DISCOVER_BUTTONS: ['HmIP-WRC6', 'HmIP-RC8'] + DISCOVER_LOCKS: ['KeyMatic'] } HM_IGNORE_DISCOVERY_NODE = [ @@ -465,7 +466,7 @@ def _system_callback_handler(hass, config, src, *args): ('sensor', DISCOVER_SENSORS), ('climate', DISCOVER_CLIMATE), ('lock', DISCOVER_LOCKS), - ('binary_sensor', DISCOVER_SWITCHES)): + ('binary_sensor', DISCOVER_BATTERY)): # Get all devices of a specific type found_devices = _get_devices( hass, discovery_type, addresses, interface) @@ -473,21 +474,10 @@ def _system_callback_handler(hass, config, src, *args): # When devices of this type are found # they are setup in HASS and a discovery event is fired if found_devices: - discovery_info = {ATTR_DISCOVER_DEVICES: found_devices, - ATTR_BATTERY_DEVICES: False} - - # Switches are skipped as a component. They will only - # appear in hass as a battery device. - if not discovery_type == DISCOVER_SWITCHES: - discovery.load_platform(hass, component_name, DOMAIN, - discovery_info, config) - - # Pass all devices to binary sensor discovery, - # check whether they are battery operated and - # add them as a battery operated binary sensor device. - discovery_info[ATTR_BATTERY_DEVICES] = True - discovery.load_platform(hass, 'binary_sensor', DOMAIN, - discovery_info, config) + discovery.load_platform(hass, component_name, DOMAIN, { + ATTR_DISCOVER_DEVICES: found_devices, + ATTR_DISCOVERY_TYPE: discovery_type, + }, config) # Homegear error message elif src == 'error': @@ -509,7 +499,8 @@ def _get_devices(hass, discovery_type, keys, interface): metadata = {} # Class not supported by discovery type - if class_name not in HM_DEVICE_TYPES[discovery_type]: + if discovery_type != DISCOVER_BATTERY and \ + class_name not in HM_DEVICE_TYPES[discovery_type]: continue # Load metadata needed to generate a parameter list @@ -517,6 +508,15 @@ def _get_devices(hass, discovery_type, keys, interface): metadata.update(device.SENSORNODE) elif discovery_type == DISCOVER_BINARY_SENSORS: metadata.update(device.BINARYNODE) + elif discovery_type == DISCOVER_BATTERY: + if ATTR_LOWBAT in device.ATTRIBUTENODE: + metadata.update( + {ATTR_LOWBAT: device.ATTRIBUTENODE[ATTR_LOWBAT]}) + elif ATTR_LOW_BAT in device.ATTRIBUTENODE: + metadata.update( + {ATTR_LOW_BAT: device.ATTRIBUTENODE[ATTR_LOW_BAT]}) + else: + continue else: metadata.update({None: device.ELEMENT}) diff --git a/homeassistant/components/homematic/binary_sensor.py b/homeassistant/components/homematic/binary_sensor.py index 91960cd8570..9d47f74df92 100644 --- a/homeassistant/components/homematic/binary_sensor.py +++ b/homeassistant/components/homematic/binary_sensor.py @@ -2,16 +2,14 @@ import logging from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.components.homematic import ATTR_BATTERY_DEVICES -from homeassistant.const import STATE_UNKNOWN, DEVICE_CLASS_BATTERY +from homeassistant.components.homematic import ( + ATTR_DISCOVERY_TYPE, DISCOVER_BATTERY) +from homeassistant.const import DEVICE_CLASS_BATTERY from . import ATTR_DISCOVER_DEVICES, HMDevice _LOGGER = logging.getLogger(__name__) -ATTR_LOW_BAT = 'LOW_BAT' -ATTR_LOWBAT = 'LOWBAT' - SENSOR_TYPES_CLASS = { 'IPShutterContact': 'opening', 'MaxShutterContact': 'opening', @@ -34,16 +32,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None): return devices = [] - battery_devices = discovery_info[ATTR_BATTERY_DEVICES] - for conf in discovery_info[ATTR_DISCOVER_DEVICES]: - if battery_devices: - battery_device = conf.get(ATTR_LOWBAT) or conf.get(ATTR_LOW_BAT) - if battery_device: - new_device = HMBatterySensor(conf) + if discovery_info[ATTR_DISCOVERY_TYPE] == DISCOVER_BATTERY: + devices.append(HMBatterySensor(conf)) else: - new_device = HMBinarySensor(conf) - devices.append(new_device) + devices.append(HMBinarySensor(conf)) add_entities(devices) @@ -70,7 +63,7 @@ class HMBinarySensor(HMDevice, BinarySensorDevice): """Generate the data dictionary (self._data) from metadata.""" # Add state to data struct if self._state: - self._data.update({self._state: STATE_UNKNOWN}) + self._data.update({self._state: None}) class HMBatterySensor(HMDevice, BinarySensorDevice): @@ -84,13 +77,10 @@ class HMBatterySensor(HMDevice, BinarySensorDevice): @property def is_on(self): """Return True if battery is low.""" - is_on = self._data.get(ATTR_LOW_BAT, False) or self._data.get( - ATTR_LOWBAT, False - ) - return is_on + return bool(self._hm_get_state()) def _init_data_struct(self): """Generate the data dictionary (self._data) from metadata.""" # Add state to data struct if self._state: - self._data.update({self._state: STATE_UNKNOWN}) + self._data.update({self._state: None}) From 45085dd97f670a1daf56a425be5a9330b0025975 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 14 May 2019 05:57:47 +0200 Subject: [PATCH 037/232] Better handle large amounts of data being sent over WS (#23842) * Better handle large amounts of data being sent over WS * Lint --- homeassistant/components/camera/__init__.py | 10 +++---- homeassistant/components/lovelace/__init__.py | 11 +++---- .../components/media_player/__init__.py | 4 +-- .../components/websocket_api/connection.py | 7 +++++ .../components/websocket_api/const.py | 5 ++++ .../components/websocket_api/http.py | 12 ++++---- .../websocket_api/test_connection.py | 30 +++++++++++++++++++ 7 files changed, 59 insertions(+), 20 deletions(-) create mode 100644 tests/components/websocket_api/test_connection.py diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 7a37dffe3b8..7098d8bcb75 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -522,12 +522,10 @@ async def websocket_camera_thumbnail(hass, connection, msg): """ try: image = await async_get_image(hass, msg['entity_id']) - connection.send_message(websocket_api.result_message( - msg['id'], { - 'content_type': image.content_type, - 'content': base64.b64encode(image.content).decode('utf-8') - } - )) + await connection.send_big_result(msg['id'], { + 'content_type': image.content_type, + 'content': base64.b64encode(image.content).decode('utf-8') + }) except HomeAssistantError: connection.send_message(websocket_api.error_message( msg['id'], 'image_fetch_failed', 'Unable to fetch image')) diff --git a/homeassistant/components/lovelace/__init__.py b/homeassistant/components/lovelace/__init__.py index 03b1cf06d68..996e3f7b296 100644 --- a/homeassistant/components/lovelace/__init__.py +++ b/homeassistant/components/lovelace/__init__.py @@ -176,18 +176,19 @@ def handle_yaml_errors(func): error = None try: result = await func(hass, connection, msg) - message = websocket_api.result_message( - msg['id'], result - ) except ConfigNotFound: error = 'config_not_found', 'No config found.' except HomeAssistantError as err: error = 'error', str(err) if error is not None: - message = websocket_api.error_message(msg['id'], *error) + connection.send_error(msg['id'], *error) + return - connection.send_message(message) + if msg is not None: + await connection.send_big_result(msg['id'], result) + else: + connection.send_result(msg['id'], result) return send_with_error_handling diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index ccfa968fa9a..b433a90f329 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -869,8 +869,8 @@ async def websocket_handle_thumbnail(hass, connection, msg): 'Failed to fetch thumbnail')) return - connection.send_message(websocket_api.result_message( + await connection.send_big_result( msg['id'], { 'content_type': content_type, 'content': base64.b64encode(data).decode('utf-8') - })) + }) diff --git a/homeassistant/components/websocket_api/connection.py b/homeassistant/components/websocket_api/connection.py index c09e8c4c6e2..1aa1efc0eca 100644 --- a/homeassistant/components/websocket_api/connection.py +++ b/homeassistant/components/websocket_api/connection.py @@ -36,6 +36,13 @@ class ActiveConnection: """Send a result message.""" self.send_message(messages.result_message(msg_id, result)) + async def send_big_result(self, msg_id, result): + """Send a result message that would be expensive to JSON serialize.""" + content = await self.hass.async_add_executor_job( + const.JSON_DUMP, messages.result_message(msg_id, result) + ) + self.send_message(content) + @callback def send_error(self, msg_id, code, message): """Send a error message.""" diff --git a/homeassistant/components/websocket_api/const.py b/homeassistant/components/websocket_api/const.py index 53ca680c4c9..9c776e3b949 100644 --- a/homeassistant/components/websocket_api/const.py +++ b/homeassistant/components/websocket_api/const.py @@ -1,6 +1,9 @@ """Websocket constants.""" import asyncio from concurrent import futures +from functools import partial +import json +from homeassistant.helpers.json import JSONEncoder DOMAIN = 'websocket_api' URL = '/api/websocket' @@ -27,3 +30,5 @@ SIGNAL_WEBSOCKET_DISCONNECTED = 'websocket_disconnected' # Data used to store the current connection list DATA_CONNECTIONS = DOMAIN + '.connections' + +JSON_DUMP = partial(json.dumps, cls=JSONEncoder, allow_nan=False) diff --git a/homeassistant/components/websocket_api/http.py b/homeassistant/components/websocket_api/http.py index 85051dcae73..80592cc7151 100644 --- a/homeassistant/components/websocket_api/http.py +++ b/homeassistant/components/websocket_api/http.py @@ -1,8 +1,6 @@ """View to accept incoming websocket connection.""" import asyncio from contextlib import suppress -from functools import partial -import json import logging from aiohttp import web, WSMsgType @@ -11,18 +9,15 @@ import async_timeout from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import callback from homeassistant.components.http import HomeAssistantView -from homeassistant.helpers.json import JSONEncoder from .const import ( MAX_PENDING_MSG, CANCELLATION_ERRORS, URL, ERR_UNKNOWN_ERROR, SIGNAL_WEBSOCKET_CONNECTED, SIGNAL_WEBSOCKET_DISCONNECTED, - DATA_CONNECTIONS) + DATA_CONNECTIONS, JSON_DUMP) from .auth import AuthPhase, auth_required_message from .error import Disconnect from .messages import error_message -JSON_DUMP = partial(json.dumps, cls=JSONEncoder, allow_nan=False) - class WebsocketAPIView(HomeAssistantView): """View to serve a websockets endpoint.""" @@ -62,7 +57,10 @@ class WebSocketHandler: break self._logger.debug("Sending %s", message) try: - await self.wsock.send_json(message, dumps=JSON_DUMP) + if isinstance(message, str): + await self.wsock.send_str(message) + else: + await self.wsock.send_json(message, dumps=JSON_DUMP) except (ValueError, TypeError) as err: self._logger.error('Unable to serialize to JSON: %s\n%s', err, message) diff --git a/tests/components/websocket_api/test_connection.py b/tests/components/websocket_api/test_connection.py new file mode 100644 index 00000000000..eeac9af24cd --- /dev/null +++ b/tests/components/websocket_api/test_connection.py @@ -0,0 +1,30 @@ +"""Test WebSocket Connection class.""" +from homeassistant.components import websocket_api +from homeassistant.components.websocket_api import const + + +async def test_send_big_result(hass, websocket_client): + """Test sending big results over the WS.""" + @websocket_api.websocket_command({ + 'type': 'big_result' + }) + @websocket_api.async_response + async def send_big_result(hass, connection, msg): + await connection.send_big_result( + msg['id'], {'big': 'result'} + ) + + hass.components.websocket_api.async_register_command( + send_big_result + ) + + await websocket_client.send_json({ + 'id': 5, + 'type': 'big_result', + }) + + msg = await websocket_client.receive_json() + assert msg['id'] == 5 + assert msg['type'] == const.TYPE_RESULT + assert msg['success'] + assert msg['result'] == {'big': 'result'} From 0d96095646ef6d5505a6204c189205732093e06f Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 14 May 2019 05:58:13 +0200 Subject: [PATCH 038/232] Zeroconf - replace library (#23835) * Use aiozeroconf in preparation for new zeroconf discovery * Update requirements * Remove sleep * Make stop zeroconf a coroutine * Remove unused import * Fix aiozeroconf dependency in default_config tests --- homeassistant/components/zeroconf/__init__.py | 25 +++++++++++-------- .../components/zeroconf/manifest.json | 2 +- requirements_all.txt | 6 ++--- tests/components/default_config/test_init.py | 11 +++++++- 4 files changed, 28 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index d2dcd907885..a14efb70411 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -19,11 +19,11 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -def setup(hass, config): +async def async_setup(hass, config): """Set up Zeroconf and make Home Assistant discoverable.""" - from zeroconf import Zeroconf, ServiceInfo + from aiozeroconf import Zeroconf, ServiceInfo - zeroconf = Zeroconf() + zeroconf = Zeroconf(hass.loop) zeroconf_name = '{}.{}'.format(hass.config.location_name, ZEROCONF_TYPE) @@ -38,19 +38,22 @@ def setup(hass, config): try: host_ip_pton = socket.inet_pton(socket.AF_INET, host_ip) + info = ServiceInfo(ZEROCONF_TYPE, zeroconf_name, address=host_ip_pton, + port=hass.http.server_port, weight=0, priority=0, + properties=params) except socket.error: host_ip_pton = socket.inet_pton(socket.AF_INET6, host_ip) + info = ServiceInfo(ZEROCONF_TYPE, zeroconf_name, address6=host_ip_pton, + port=hass.http.server_port, weight=0, priority=0, + properties=params) - info = ServiceInfo(ZEROCONF_TYPE, zeroconf_name, host_ip_pton, - hass.http.server_port, 0, 0, params) + await zeroconf.register_service(info) - zeroconf.register_service(info) - - def stop_zeroconf(event): + async def stop_zeroconf(event): """Stop Zeroconf.""" - zeroconf.unregister_service(info) - zeroconf.close() + await zeroconf.unregister_service(info) + await zeroconf.close() - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_zeroconf) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_zeroconf) return True diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index 7ef9b250363..5979ea12a58 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -3,7 +3,7 @@ "name": "Zeroconf", "documentation": "https://www.home-assistant.io/components/zeroconf", "requirements": [ - "zeroconf==0.22.0" + "aiozeroconf==0.1.8" ], "dependencies": [ "api" diff --git a/requirements_all.txt b/requirements_all.txt index 43d66a4c01b..1f28157235e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -154,6 +154,9 @@ aioswitcher==2019.3.21 # homeassistant.components.unifi aiounifi==4 +# homeassistant.components.zeroconf +aiozeroconf==0.1.8 + # homeassistant.components.aladdin_connect aladdin_connect==0.3 @@ -1850,9 +1853,6 @@ youtube_dl==2019.05.11 # homeassistant.components.zengge zengge==0.2 -# homeassistant.components.zeroconf -zeroconf==0.22.0 - # homeassistant.components.zha zha-quirks==0.0.12 diff --git a/tests/components/default_config/test_init.py b/tests/components/default_config/test_init.py index 94adf53cb2d..8e2766a857b 100644 --- a/tests/components/default_config/test_init.py +++ b/tests/components/default_config/test_init.py @@ -5,7 +5,16 @@ from homeassistant.setup import async_setup_component import pytest -from tests.common import MockDependency +from tests.common import MockDependency, mock_coro + + +@pytest.fixture(autouse=True) +def aiozeroconf_mock(): + """Mock aiozeroconf.""" + with MockDependency('aiozeroconf') as mocked_zeroconf: + mocked_zeroconf.Zeroconf.return_value.register_service \ + .return_value = mock_coro(True) + yield @pytest.fixture(autouse=True) From de1fd5a7faed11c16806ed2778d3d1697872a572 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 14 May 2019 07:09:11 +0200 Subject: [PATCH 039/232] WS: Improve service calling errors (#23840) * WS: Improve service calling errors * Docstyle * Types * Update text --- homeassistant/components/script/__init__.py | 10 ++++++-- .../components/websocket_api/commands.py | 14 +++++++---- homeassistant/exceptions.py | 4 ++++ tests/components/automation/test_init.py | 2 +- tests/components/script/test_init.py | 22 +++++++++++++++++ .../components/websocket_api/test_commands.py | 24 +++++++++++++++++++ 6 files changed, 68 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/script/__init__.py b/homeassistant/components/script/__init__.py index 528e454c4e6..90aefcc7aaa 100644 --- a/homeassistant/components/script/__init__.py +++ b/homeassistant/components/script/__init__.py @@ -168,8 +168,14 @@ class ScriptEntity(ToggleEntity): ATTR_NAME: self.script.name, ATTR_ENTITY_ID: self.entity_id, }, context=context) - await self.script.async_run( - kwargs.get(ATTR_VARIABLES), context) + try: + await self.script.async_run( + kwargs.get(ATTR_VARIABLES), context) + except Exception as err: # pylint: disable=broad-except + self.script.async_log_exception( + _LOGGER, "Error executing script {}".format(self.entity_id), + err) + raise err async def async_turn_off(self, **kwargs): """Turn script off.""" diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index 84178beef8b..b6a4185abfd 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -120,17 +120,21 @@ async def handle_call_service(hass, connection, msg): msg['domain'], msg['service'], msg.get('service_data'), blocking, connection.context(msg)) connection.send_message(messages.result_message(msg['id'])) - except ServiceNotFound: - connection.send_message(messages.error_message( - msg['id'], const.ERR_NOT_FOUND, 'Service not found.')) + except ServiceNotFound as err: + if err.domain == msg['domain'] and err.service == msg['service']: + connection.send_message(messages.error_message( + msg['id'], const.ERR_NOT_FOUND, 'Service not found.')) + else: + connection.send_message(messages.error_message( + msg['id'], const.ERR_HOME_ASSISTANT_ERROR, str(err))) except HomeAssistantError as err: connection.logger.exception(err) connection.send_message(messages.error_message( - msg['id'], const.ERR_HOME_ASSISTANT_ERROR, '{}'.format(err))) + msg['id'], const.ERR_HOME_ASSISTANT_ERROR, str(err))) except Exception as err: # pylint: disable=broad-except connection.logger.exception(err) connection.send_message(messages.error_message( - msg['id'], const.ERR_UNKNOWN_ERROR, '{}'.format(err))) + msg['id'], const.ERR_UNKNOWN_ERROR, str(err))) @callback diff --git a/homeassistant/exceptions.py b/homeassistant/exceptions.py index aadee3e792b..6a44af9943b 100644 --- a/homeassistant/exceptions.py +++ b/homeassistant/exceptions.py @@ -75,3 +75,7 @@ class ServiceNotFound(HomeAssistantError): self, "Service {}.{} not found".format(domain, service)) self.domain = domain self.service = service + + def __str__(self) -> str: + """Return string representation.""" + return "Unable to find service {}/{}".format(self.domain, self.service) diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index a019f65afcf..179c5f84895 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -893,4 +893,4 @@ async def test_automation_with_error_in_script(hass, caplog): hass.bus.async_fire('test_event') await hass.async_block_till_done() - assert 'Service test.automation not found' in caplog.text + assert 'Service not found' in caplog.text diff --git a/tests/components/script/test_init.py b/tests/components/script/test_init.py index 790d5c2e844..c2ff17d9444 100644 --- a/tests/components/script/test_init.py +++ b/tests/components/script/test_init.py @@ -3,6 +3,8 @@ import unittest from unittest.mock import patch, Mock +import pytest + from homeassistant.components import script from homeassistant.components.script import DOMAIN from homeassistant.const import ( @@ -11,6 +13,7 @@ from homeassistant.const import ( from homeassistant.core import Context, callback, split_entity_id from homeassistant.loader import bind_hass from homeassistant.setup import setup_component, async_setup_component +from homeassistant.exceptions import ServiceNotFound from tests.common import get_test_home_assistant @@ -300,3 +303,22 @@ async def test_shared_context(hass): state = hass.states.get('script.test') assert state is not None assert state.context == context + + +async def test_logging_script_error(hass, caplog): + """Test logging script error.""" + assert await async_setup_component(hass, 'script', { + 'script': { + 'hello': { + 'sequence': [ + {'service': 'non.existing'} + ] + } + } + }) + with pytest.raises(ServiceNotFound) as err: + await hass.services.async_call('script', 'hello', blocking=True) + + assert err.value.domain == 'non' + assert err.value.service == 'existing' + assert 'Error executing script' in caplog.text diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index 4f3be31b22c..d50501897d7 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -67,6 +67,30 @@ async def test_call_service_not_found(hass, websocket_client): assert msg['error']['code'] == const.ERR_NOT_FOUND +async def test_call_service_child_not_found(hass, websocket_client): + """Test not reporting not found errors if it's not the called service.""" + async def serv_handler(call): + await hass.services.async_call('non', 'existing') + + hass.services.async_register('domain_test', 'test_service', serv_handler) + + await websocket_client.send_json({ + 'id': 5, + 'type': 'call_service', + 'domain': 'domain_test', + 'service': 'test_service', + 'service_data': { + 'hello': 'world' + } + }) + + msg = await websocket_client.receive_json() + assert msg['id'] == 5 + assert msg['type'] == const.TYPE_RESULT + assert not msg['success'] + assert msg['error']['code'] == const.ERR_HOME_ASSISTANT_ERROR + + async def test_call_service_error(hass, websocket_client): """Test call service command with error.""" @callback From a1a6d4a631e1cc1572adcbf3f70064733b0ea643 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 14 May 2019 07:07:56 +0200 Subject: [PATCH 040/232] Updated frontend to 20190514.0 --- homeassistant/components/frontend/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 082bc6328d0..45b1f0ff351 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/components/frontend", "requirements": [ - "home-assistant-frontend==20190510.0" + "home-assistant-frontend==20190514.0" ], "dependencies": [ "api", diff --git a/requirements_all.txt b/requirements_all.txt index 1f28157235e..947db8a0e5b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -566,7 +566,7 @@ hole==0.3.0 holidays==0.9.10 # homeassistant.components.frontend -home-assistant-frontend==20190510.0 +home-assistant-frontend==20190514.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6d9913085ce..99c2e9e09dc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -145,7 +145,7 @@ hdate==0.8.7 holidays==0.9.10 # homeassistant.components.frontend -home-assistant-frontend==20190510.0 +home-assistant-frontend==20190514.0 # homeassistant.components.homekit_controller homekit[IP]==0.14.0 From eaf4a75402090b46b60fbf99d7a8e2c1f3d2e409 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Tue, 14 May 2019 01:15:31 -0400 Subject: [PATCH 041/232] bump zha-quirks (#23855) --- homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index a44550100f9..610498e6237 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -5,7 +5,7 @@ "documentation": "https://www.home-assistant.io/components/zha", "requirements": [ "bellows-homeassistant==0.7.3", - "zha-quirks==0.0.12", + "zha-quirks==0.0.13", "zigpy-deconz==0.1.4", "zigpy-homeassistant==0.3.3", "zigpy-xbee-homeassistant==0.2.1" diff --git a/requirements_all.txt b/requirements_all.txt index 947db8a0e5b..c6efe968cf9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1854,7 +1854,7 @@ youtube_dl==2019.05.11 zengge==0.2 # homeassistant.components.zha -zha-quirks==0.0.12 +zha-quirks==0.0.13 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 From 9b217743924ffedd39ca44202d6075f1afc806ca Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Tue, 14 May 2019 01:16:21 -0400 Subject: [PATCH 042/232] Fix ZHA battery when readings produce an unknown value (#23854) * check for unknown readings * only publish valid readings * remove unused constant --- homeassistant/components/zha/device_entity.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zha/device_entity.py b/homeassistant/components/zha/device_entity.py index 3937e597b78..94fe598b6ec 100644 --- a/homeassistant/components/zha/device_entity.py +++ b/homeassistant/components/zha/device_entity.py @@ -142,8 +142,8 @@ class ZhaDeviceEntity(ZhaEntity): """Get the latest battery reading from channels cache.""" battery = await self._battery_channel.get_attribute_value( 'battery_percentage_remaining') - if battery is not None: - # per zcl specs battery percent is reported at 200% ¯\_(ツ)_/¯ + # per zcl specs battery percent is reported at 200% ¯\_(ツ)_/¯ + if battery is not None and battery != -1: battery = battery / 2 battery = int(round(battery)) self._device_state_attributes['battery_level'] = battery From 128ce589e1a636d028a5ce6bd34ee384b68fe9fb Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Tue, 14 May 2019 01:16:41 -0400 Subject: [PATCH 043/232] Correct ZHA illumination conversion (#23853) * fix illumination values * correct formula * update illuminance calculation * update test --- homeassistant/components/zha/sensor.py | 8 ++++++++ tests/components/zha/test_sensor.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index 94f57ed9a0b..b2d246c3095 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -24,6 +24,13 @@ def pass_through_formatter(value): return value +def illuminance_formatter(value): + """Convert Illimination data.""" + if value is None: + return None + return round(pow(10, ((value - 1) / 10000)), 1) + + def temperature_formatter(value): """Convert temperature data.""" if value is None: @@ -58,6 +65,7 @@ FORMATTER_FUNC_REGISTRY = { TEMPERATURE: temperature_formatter, PRESSURE: pressure_formatter, ELECTRICAL_MEASUREMENT: active_power_formatter, + ILLUMINANCE: illuminance_formatter, GENERIC: pass_through_formatter, } diff --git a/tests/components/zha/test_sensor.py b/tests/components/zha/test_sensor.py index ec6af7f4aa1..cd8f4ba72b2 100644 --- a/tests/components/zha/test_sensor.py +++ b/tests/components/zha/test_sensor.py @@ -139,7 +139,7 @@ async def async_test_pressure(hass, device_info): async def async_test_illuminance(hass, device_info): """Test illuminance sensor.""" await send_attribute_report(hass, device_info["cluster"], 0, 10) - assert_state(hass, device_info, '10', 'lx') + assert_state(hass, device_info, '1.0', 'lx') async def async_test_metering(hass, device_info): From 6f9860b25e60f8efbe64064ecbb51083488d7f36 Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Tue, 14 May 2019 02:12:05 -0500 Subject: [PATCH 044/232] Fix improper usage of body attribute on web.Response. Should be text since we arent sending bytes (#23857) --- homeassistant/components/geofency/__init__.py | 2 +- homeassistant/components/locative/__init__.py | 4 ++-- homeassistant/components/mobile_app/helpers.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/geofency/__init__.py b/homeassistant/components/geofency/__init__.py index 37d32a8860d..e5698b997a4 100644 --- a/homeassistant/components/geofency/__init__.py +++ b/homeassistant/components/geofency/__init__.py @@ -73,7 +73,7 @@ async def handle_webhook(hass, webhook_id, request): data = WEBHOOK_SCHEMA(dict(await request.post())) except vol.MultipleInvalid as error: return web.Response( - body=error.error_message, + text=error.error_message, status=HTTP_UNPROCESSABLE_ENTITY ) diff --git a/homeassistant/components/locative/__init__.py b/homeassistant/components/locative/__init__.py index c44b12c87d2..66f917e5729 100644 --- a/homeassistant/components/locative/__init__.py +++ b/homeassistant/components/locative/__init__.py @@ -58,7 +58,7 @@ async def handle_webhook(hass, webhook_id, request): data = WEBHOOK_SCHEMA(dict(await request.post())) except vol.MultipleInvalid as error: return web.Response( - body=error.error_message, + text=error.error_message, status=HTTP_UNPROCESSABLE_ENTITY ) @@ -76,7 +76,7 @@ async def handle_webhook(hass, webhook_id, request): location_name ) return web.Response( - body='Setting location to {}'.format(location_name), + text='Setting location to {}'.format(location_name), status=HTTP_OK ) diff --git a/homeassistant/components/mobile_app/helpers.py b/homeassistant/components/mobile_app/helpers.py index ee593588ef8..6aec4307464 100644 --- a/homeassistant/components/mobile_app/helpers.py +++ b/homeassistant/components/mobile_app/helpers.py @@ -81,7 +81,7 @@ def registration_context(registration: Dict) -> Context: def empty_okay_response(headers: Dict = None, status: int = 200) -> Response: """Return a Response with empty JSON object and a 200.""" - return Response(body='{}', status=status, content_type='application/json', + return Response(text='{}', status=status, content_type='application/json', headers=headers) From a85999719049d0c80e97f9f215cc71815b0bcc0c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 14 May 2019 09:16:37 +0200 Subject: [PATCH 045/232] Allow deletion of automations and scripts (#23845) --- homeassistant/components/config/__init__.py | 37 ++++++++++++++++++- tests/components/config/test_automation.py | 38 +++++++++++++++++++ tests/components/config/test_script.py | 41 +++++++++++++++++++++ 3 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 tests/components/config/test_script.py diff --git a/homeassistant/components/config/__init__.py b/homeassistant/components/config/__init__.py index 3752d5d37bf..70c72e899c0 100644 --- a/homeassistant/components/config/__init__.py +++ b/homeassistant/components/config/__init__.py @@ -92,6 +92,10 @@ class BaseEditConfigView(HomeAssistantView): """Set value.""" raise NotImplementedError + def _delete_value(self, hass, data, config_key): + """Delete value.""" + raise NotImplementedError + async def get(self, request, config_key): """Fetch device specific config.""" hass = request.app['hass'] @@ -128,7 +132,27 @@ class BaseEditConfigView(HomeAssistantView): current = await self.read_config(hass) self._write_value(hass, current, config_key, data) - await hass.async_add_job(_write, path, current) + await hass.async_add_executor_job(_write, path, current) + + if self.post_write_hook is not None: + hass.async_create_task(self.post_write_hook(hass)) + + return self.json({ + 'result': 'ok', + }) + + async def delete(self, request, config_key): + """Remove an entry.""" + hass = request.app['hass'] + current = await self.read_config(hass) + value = self._get_value(hass, current, config_key) + path = hass.config.path(self.path) + + if value is None: + return self.json_message('Resource not found', 404) + + self._delete_value(hass, current, config_key) + await hass.async_add_executor_job(_write, path, current) if self.post_write_hook is not None: hass.async_create_task(self.post_write_hook(hass)) @@ -161,6 +185,10 @@ class EditKeyBasedConfigView(BaseEditConfigView): """Set value.""" data.setdefault(config_key, {}).update(new_value) + def _delete_value(self, hass, data, config_key): + """Delete value.""" + return data.pop(config_key) + class EditIdBasedConfigView(BaseEditConfigView): """Configure key based config entries.""" @@ -184,6 +212,13 @@ class EditIdBasedConfigView(BaseEditConfigView): value.update(new_value) + def _delete_value(self, hass, data, config_key): + """Delete value.""" + index = next( + idx for idx, val in enumerate(data) + if val.get(CONF_ID) == config_key) + data.pop(index) + def _read(path): """Read YAML helper.""" diff --git a/tests/components/config/test_automation.py b/tests/components/config/test_automation.py index f97559a224f..30b4f72b0bc 100644 --- a/tests/components/config/test_automation.py +++ b/tests/components/config/test_automation.py @@ -134,3 +134,41 @@ async def test_bad_formatted_automations(hass, hass_client): 'condition': [], 'action': [], } + + +async def test_delete_automation(hass, hass_client): + """Test deleting an automation.""" + with patch.object(config, 'SECTIONS', ['automation']): + await async_setup_component(hass, 'config', {}) + + client = await hass_client() + + orig_data = [ + { + 'id': 'sun', + }, + { + 'id': 'moon', + } + ] + + def mock_read(path): + """Mock reading data.""" + return orig_data + + written = [] + + def mock_write(path, data): + """Mock writing data.""" + written.append(data) + + with patch('homeassistant.components.config._read', mock_read), \ + patch('homeassistant.components.config._write', mock_write): + resp = await client.delete('/api/config/automation/config/sun') + + assert resp.status == 200 + result = await resp.json() + assert result == {'result': 'ok'} + + assert len(written) == 1 + assert written[0][0]['id'] == 'moon' diff --git a/tests/components/config/test_script.py b/tests/components/config/test_script.py new file mode 100644 index 00000000000..d0848d18dcc --- /dev/null +++ b/tests/components/config/test_script.py @@ -0,0 +1,41 @@ +"""Tests for config/script.""" +from unittest.mock import patch + +from homeassistant.bootstrap import async_setup_component +from homeassistant.components import config + + +async def test_delete_script(hass, hass_client): + """Test deleting a script.""" + with patch.object(config, 'SECTIONS', ['script']): + await async_setup_component(hass, 'config', {}) + + client = await hass_client() + + orig_data = { + 'one': {}, + 'two': {}, + } + + def mock_read(path): + """Mock reading data.""" + return orig_data + + written = [] + + def mock_write(path, data): + """Mock writing data.""" + written.append(data) + + with patch('homeassistant.components.config._read', mock_read), \ + patch('homeassistant.components.config._write', mock_write): + resp = await client.delete('/api/config/script/config/two') + + assert resp.status == 200 + result = await resp.json() + assert result == {'result': 'ok'} + + assert len(written) == 1 + assert written[0] == { + 'one': {} + } From 19cfa8cf229eedbb5d8d2bf02ebda54e35153c6e Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 14 May 2019 10:18:01 +0200 Subject: [PATCH 046/232] Update azure-pipelines.yml for Azure Pipelines Automated version updates --- azure-pipelines.yml | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 0ca9425d002..fd45c334cf3 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -16,6 +16,7 @@ variables: value: '0.3' - group: docker - group: wheels + - group: github jobs: @@ -141,3 +142,45 @@ jobs: -r https://github.com/home-assistant/hassio-homeassistant \ -t machine --docker-hub homeassistant displayName: 'Build Release' + + +- job: 'ReleasePublish' + condition: and(startsWith(variables['Build.SourceBranch'], 'refs/tags'), succeeded('Release')) + dependsOn: + - 'Release' + pool: + vmImage: 'ubuntu-16.04' + steps: + - script: | + sudo apt-get install -y --no-install-recommends \ + git jq + + git config --global user.name "Pascal Vizeli" + git config --global user.email "pvizeli@syshack.ch" + git config --global credential.helper store + + echo "https://$(githubToken):x-oauth-basic@github.com > $HOME\.git-credentials + displayName: 'Install requirements' + - script: | + set -e + + version="$(Build.SourceBranchName)" + + git clone https://github.com/home-assistant/hassio-version + cd hassio-version + + dev_version="$(jq --raw-output '.homeassistant.default' dev.json)" + beta_version="$(jq --raw-output '.homeassistant.default' beta.json)" + stable_version="$(jq --raw-output '.homeassistant.default' stable.json)" + + if [[ "$version" =~ b ]]; then + sed -i "s|$dev_version|$version|g" dev.json + sed -i "s|$beta_version|$version|g" beta.json + else + sed -i "s|$dev_version|$version|g" dev.json + sed -i "s|$beta_version|$version|g" beta.json + sed -i "s|$stable_version|$version|g" stable.json + fi + + git commit -am "Bump Home Assistant $version" + git push From 6fa8556033d4bcb91c6b199069ab9a10168c57cb Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 14 May 2019 11:59:11 +0200 Subject: [PATCH 047/232] Use Cloudhooks for OwnTracks (#23847) * Use Cloudhooks for OwnTracks * Update config_flow.py * Update config_flow.py --- .../components/owntracks/__init__.py | 18 ++++++ .../components/owntracks/config_flow.py | 29 +++++++-- .../components/owntracks/test_config_flow.py | 60 +++++++++++++++++++ tests/components/owntracks/test_init.py | 12 ---- 4 files changed, 101 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/owntracks/__init__.py b/homeassistant/components/owntracks/__init__.py index e746cbc01fa..979f3829454 100644 --- a/homeassistant/components/owntracks/__init__.py +++ b/homeassistant/components/owntracks/__init__.py @@ -91,6 +91,24 @@ async def async_setup_entry(hass, entry): return True +async def async_unload_entry(hass, entry): + """Unload an OwnTracks config entry.""" + hass.components.webhook.async_unregister(entry.data[CONF_WEBHOOK_ID]) + await hass.config_entries.async_forward_entry_unload( + entry, 'device_tracker') + return True + + +async def async_remove_entry(hass, entry): + """Remove an OwnTracks config entry.""" + if (not entry.data.get('cloudhook') or + 'cloud' not in hass.config.components): + return + + await hass.components.cloud.async_delete_cloudhook( + entry.data[CONF_WEBHOOK_ID]) + + async def async_connect_mqtt(hass, component): """Subscribe to MQTT topic.""" context = hass.data[DOMAIN]['context'] diff --git a/homeassistant/components/owntracks/config_flow.py b/homeassistant/components/owntracks/config_flow.py index 59e8c4825df..f157c5cb7ce 100644 --- a/homeassistant/components/owntracks/config_flow.py +++ b/homeassistant/components/owntracks/config_flow.py @@ -4,6 +4,7 @@ from homeassistant.const import CONF_WEBHOOK_ID from homeassistant.auth.util import generate_secret CONF_SECRET = 'secret' +CONF_CLOUDHOOK = 'cloudhook' def supports_encryption(): @@ -31,9 +32,7 @@ class OwnTracksFlow(config_entries.ConfigFlow): step_id='user', ) - webhook_id = self.hass.components.webhook.async_generate_id() - webhook_url = \ - self.hass.components.webhook.async_generate_url(webhook_id) + webhook_id, webhook_url, cloudhook = await self._get_webhook_id() secret = generate_secret(16) @@ -50,7 +49,8 @@ class OwnTracksFlow(config_entries.ConfigFlow): title="OwnTracks", data={ CONF_WEBHOOK_ID: webhook_id, - CONF_SECRET: secret + CONF_SECRET: secret, + CONF_CLOUDHOOK: cloudhook, }, description_placeholders={ 'secret': secret_desc, @@ -67,12 +67,29 @@ class OwnTracksFlow(config_entries.ConfigFlow): async def async_step_import(self, user_input): """Import a config flow from configuration.""" - webhook_id = self.hass.components.webhook.async_generate_id() + webhook_id, _webhook_url, cloudhook = await self._get_webhook_id() secret = generate_secret(16) return self.async_create_entry( title="OwnTracks", data={ CONF_WEBHOOK_ID: webhook_id, - CONF_SECRET: secret + CONF_SECRET: secret, + CONF_CLOUDHOOK: cloudhook, } ) + + async def _get_webhook_id(self): + """Generate webhook ID.""" + webhook_id = self.hass.components.webhook.async_generate_id() + if self.hass.components.cloud.async_active_subscription(): + webhook_url = \ + await self.hass.components.cloud.async_create_cloudhook( + webhook_id + ) + cloudhook = True + else: + webhook_url = \ + self.hass.components.webhook.async_generate_url(webhook_id) + cloudhook = False + + return webhook_id, webhook_url, cloudhook diff --git a/tests/components/owntracks/test_config_flow.py b/tests/components/owntracks/test_config_flow.py index 079fdfafea0..57f4cfd354e 100644 --- a/tests/components/owntracks/test_config_flow.py +++ b/tests/components/owntracks/test_config_flow.py @@ -1 +1,61 @@ """Tests for OwnTracks config flow.""" +from unittest.mock import patch + +from homeassistant.setup import async_setup_component +from tests.common import mock_coro + + +async def test_config_flow_import(hass): + """Test that we automatically create a config flow.""" + assert not hass.config_entries.async_entries('owntracks') + assert await async_setup_component(hass, 'owntracks', { + 'owntracks': { + + } + }) + await hass.async_block_till_done() + assert hass.config_entries.async_entries('owntracks') + + +async def test_config_flow_unload(hass): + """Test unloading a config flow.""" + with patch('homeassistant.config_entries.ConfigEntries' + '.async_forward_entry_setup') as mock_forward: + result = await hass.config_entries.flow.async_init( + 'owntracks', context={'source': 'import'}, + data={} + ) + + assert len(mock_forward.mock_calls) == 1 + entry = result['result'] + + assert mock_forward.mock_calls[0][1][0] is entry + assert mock_forward.mock_calls[0][1][1] == 'device_tracker' + assert entry.data['webhook_id'] in hass.data['webhook'] + + with patch('homeassistant.config_entries.ConfigEntries' + '.async_forward_entry_unload', return_value=mock_coro() + ) as mock_unload: + assert await hass.config_entries.async_unload(entry.entry_id) + + assert len(mock_unload.mock_calls) == 1 + assert mock_forward.mock_calls[0][1][0] is entry + assert mock_forward.mock_calls[0][1][1] == 'device_tracker' + assert entry.data['webhook_id'] not in hass.data['webhook'] + + +async def test_with_cloud_sub(hass): + """Test creating a config flow while subscribed.""" + with patch('homeassistant.components.cloud.async_active_subscription', + return_value=True), \ + patch('homeassistant.components.cloud.async_create_cloudhook', + return_value=mock_coro('https://hooks.nabu.casa/ABCD')): + result = await hass.config_entries.flow.async_init( + 'owntracks', context={'source': 'user'}, + data={} + ) + + entry = result['result'] + assert entry.data['cloudhook'] + assert result['description_placeholders']['webhook_url'] == \ + 'https://hooks.nabu.casa/ABCD' diff --git a/tests/components/owntracks/test_init.py b/tests/components/owntracks/test_init.py index 3d2d8d03e7c..fafe9678e78 100644 --- a/tests/components/owntracks/test_init.py +++ b/tests/components/owntracks/test_init.py @@ -160,15 +160,3 @@ def test_returns_error_missing_device(mock_client): json = yield from resp.json() assert json == [] - - -async def test_config_flow_import(hass): - """Test that we automatically create a config flow.""" - assert not hass.config_entries.async_entries('owntracks') - assert await async_setup_component(hass, 'owntracks', { - 'owntracks': { - - } - }) - await hass.async_block_till_done() - assert hass.config_entries.async_entries('owntracks') From 94a2fd542ea47abf59d2e3a21f17637a3d9af92f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 14 May 2019 11:59:27 +0200 Subject: [PATCH 048/232] Fix aiohttp response serialize (#23858) * Fix aiohttp response serialize * Suport bytes * Handle None --- homeassistant/components/cloud/utils.py | 16 ++++++++++-- tests/components/cloud/test_utils.py | 34 +++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/cloud/utils.py b/homeassistant/components/cloud/utils.py index da1d3809989..1d53681cbea 100644 --- a/homeassistant/components/cloud/utils.py +++ b/homeassistant/components/cloud/utils.py @@ -1,13 +1,25 @@ """Helper functions for cloud components.""" from typing import Any, Dict -from aiohttp import web +from aiohttp import web, payload def aiohttp_serialize_response(response: web.Response) -> Dict[str, Any]: """Serialize an aiohttp response to a dictionary.""" + body = response.body + + if body is None: + pass + elif isinstance(body, payload.StringPayload): + # pylint: disable=protected-access + body = body._value.decode(body.encoding) + elif isinstance(body, bytes): + body = body.decode(response.charset or 'utf-8') + else: + raise ValueError("Unknown payload encoding") + return { 'status': response.status, - 'body': response.text, + 'body': body, 'headers': dict(response.headers), } diff --git a/tests/components/cloud/test_utils.py b/tests/components/cloud/test_utils.py index 24de4ce6214..4543f6d5623 100644 --- a/tests/components/cloud/test_utils.py +++ b/tests/components/cloud/test_utils.py @@ -14,6 +14,40 @@ def test_serialize_text(): } +def test_serialize_body_str(): + """Test serializing a response with a str as body.""" + response = web.Response(status=201, body='Hello') + assert utils.aiohttp_serialize_response(response) == { + 'status': 201, + 'body': 'Hello', + 'headers': { + 'Content-Length': '5', + 'Content-Type': 'text/plain; charset=utf-8' + }, + } + + +def test_serialize_body_None(): + """Test serializing a response with a str as body.""" + response = web.Response(status=201, body=None) + assert utils.aiohttp_serialize_response(response) == { + 'status': 201, + 'body': None, + 'headers': { + }, + } + + +def test_serialize_body_bytes(): + """Test serializing a response with a str as body.""" + response = web.Response(status=201, body=b'Hello') + assert utils.aiohttp_serialize_response(response) == { + 'status': 201, + 'body': 'Hello', + 'headers': {}, + } + + def test_serialize_json(): """Test serializing a JSON response.""" response = web.json_response({"how": "what"}) From 18149dcb8c2f23d4694c65f3d3dce52f36c34e02 Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Tue, 14 May 2019 22:30:26 +0100 Subject: [PATCH 049/232] Add geniushub sensor and binary_sensor (#23811) * Initial commit * add lastComms and de-lint * dummy commit * dummy commit 2 * refactor to temp in favour of battery * back to battery, and no temp * use snake_case * Bump client * only v3 API exposes device attributes * delint * delint2 * Change GeniusSwitch to GensiusBinarySensor --- .../components/geniushub/__init__.py | 5 ++ .../components/geniushub/binary_sensor.py | 74 +++++++++++++++++ .../components/geniushub/manifest.json | 2 +- homeassistant/components/geniushub/sensor.py | 82 +++++++++++++++++++ requirements_all.txt | 2 +- 5 files changed, 163 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/geniushub/binary_sensor.py create mode 100644 homeassistant/components/geniushub/sensor.py diff --git a/homeassistant/components/geniushub/__init__.py b/homeassistant/components/geniushub/__init__.py index 69c213c6aa5..b9ab1515d32 100644 --- a/homeassistant/components/geniushub/__init__.py +++ b/homeassistant/components/geniushub/__init__.py @@ -60,6 +60,11 @@ async def async_setup(hass, hass_config): hass.async_create_task(async_load_platform( hass, platform, DOMAIN, {}, hass_config)) + if not data._client._api_v1: # pylint: disable=protected-access + for platform in ['sensor', 'binary_sensor']: + hass.async_create_task(async_load_platform( + hass, platform, DOMAIN, {}, hass_config)) + return True diff --git a/homeassistant/components/geniushub/binary_sensor.py b/homeassistant/components/geniushub/binary_sensor.py new file mode 100644 index 00000000000..cbea4147e73 --- /dev/null +++ b/homeassistant/components/geniushub/binary_sensor.py @@ -0,0 +1,74 @@ +"""Support for Genius Hub binary_sensor devices.""" +from datetime import datetime +import logging + +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from . import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +GH_IS_SWITCH = ['Dual Channel Receiver', 'Electric Switch', 'Smart Plug'] + + +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): + """Set up the Genius Hub sensor entities.""" + client = hass.data[DOMAIN]['client'] + + switches = [GeniusBinarySensor(client, d) + for d in client.hub.device_objs if d.type[:21] in GH_IS_SWITCH] + + async_add_entities(switches) + + +class GeniusBinarySensor(BinarySensorDevice): + """Representation of a Genius Hub binary_sensor.""" + + def __init__(self, client, device): + """Initialize the binary sensor.""" + self._client = client + self._device = device + + if device.type[:21] == 'Dual Channel Receiver': + self._name = 'Dual Channel Receiver {}'.format(device.id) + else: + self._name = '{} {}'.format(device.type, device.id) + + async def async_added_to_hass(self): + """Set up a listener when this entity is added to HA.""" + async_dispatcher_connect(self.hass, DOMAIN, self._refresh) + + @callback + def _refresh(self): + self.async_schedule_update_ha_state(force_refresh=True) + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def should_poll(self) -> bool: + """Return False as the geniushub devices should not be polled.""" + return False + + @property + def is_on(self): + """Return the status of the sensor.""" + return self._device.state['outputOnOff'] + + @property + def device_state_attributes(self): + """Return the device state attributes.""" + attrs = {} + attrs['assigned_zone'] = self._device.assignedZones[0]['name'] + + last_comms = self._device._info_raw['childValues']['lastComms']['val'] # noqa; pylint: disable=protected-access + if last_comms != 0: + attrs['last_comms'] = datetime.utcfromtimestamp( + last_comms).isoformat() + + return {**attrs} diff --git a/homeassistant/components/geniushub/manifest.json b/homeassistant/components/geniushub/manifest.json index 99449211a7d..06ba4644062 100644 --- a/homeassistant/components/geniushub/manifest.json +++ b/homeassistant/components/geniushub/manifest.json @@ -3,7 +3,7 @@ "name": "Genius Hub", "documentation": "https://www.home-assistant.io/components/geniushub", "requirements": [ - "geniushub-client==0.4.6" + "geniushub-client==0.4.7" ], "dependencies": [], "codeowners": ["@zxdavb"] diff --git a/homeassistant/components/geniushub/sensor.py b/homeassistant/components/geniushub/sensor.py new file mode 100644 index 00000000000..fc00c0db79e --- /dev/null +++ b/homeassistant/components/geniushub/sensor.py @@ -0,0 +1,82 @@ +"""Support for Genius Hub sensor devices.""" +from datetime import datetime +import logging + +from homeassistant.const import DEVICE_CLASS_BATTERY +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import Entity + +from . import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +GH_HAS_BATTERY = [ + 'Room Thermostat', 'Genius Valve', 'Room Sensor', 'Radiator Valve'] + + +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): + """Set up the Genius Hub sensor entities.""" + client = hass.data[DOMAIN]['client'] + + sensors = [GeniusDevice(client, d) + for d in client.hub.device_objs if d.type in GH_HAS_BATTERY] + + async_add_entities(sensors) + + +class GeniusDevice(Entity): + """Representation of a Genius Hub sensor.""" + + def __init__(self, client, device): + """Initialize the sensor.""" + self._client = client + self._device = device + + self._name = '{} {}'.format(device.type, device.id) + + async def async_added_to_hass(self): + """Set up a listener when this entity is added to HA.""" + async_dispatcher_connect(self.hass, DOMAIN, self._refresh) + + @callback + def _refresh(self): + self.async_schedule_update_ha_state(force_refresh=True) + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def device_class(self): + """Return the device class of the sensor.""" + return DEVICE_CLASS_BATTERY + + @property + def unit_of_measurement(self): + """Return the unit of measurement of the sensor.""" + return '%' + + @property + def should_poll(self) -> bool: + """Return False as the geniushub devices should not be polled.""" + return False + + @property + def state(self): + """Return the state of the sensor.""" + return self._device.state['batteryLevel'] + + @property + def device_state_attributes(self): + """Return the device state attributes.""" + attrs = {} + attrs['assigned_zone'] = self._device.assignedZones[0]['name'] + + last_comms = self._device._info_raw['childValues']['lastComms']['val'] # noqa; pylint: disable=protected-access + attrs['last_comms'] = datetime.utcfromtimestamp( + last_comms).isoformat() + + return {**attrs} diff --git a/requirements_all.txt b/requirements_all.txt index c6efe968cf9..4bec104e36f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -477,7 +477,7 @@ gearbest_parser==1.0.7 geizhals==0.0.9 # homeassistant.components.geniushub -geniushub-client==0.4.6 +geniushub-client==0.4.7 # homeassistant.components.geo_json_events # homeassistant.components.nsw_rural_fire_service_feed From 9da74dda431f7a6549695cdc9027da69948ed0bf Mon Sep 17 00:00:00 2001 From: Penny Wood Date: Wed, 15 May 2019 15:02:29 +0800 Subject: [PATCH 050/232] Quiet the chatty sun.sun (#23832) * Split up method to allow caching event * Lower frequency of updates. * Code review patches. * Minor changes to test * Skip end of period at fixed multiple of delta. Improved documentation. --- homeassistant/components/sun/__init__.py | 183 +++++++++++----- homeassistant/helpers/sun.py | 13 +- tests/components/sun/test_init.py | 264 ++++++++++++----------- 3 files changed, 289 insertions(+), 171 deletions(-) diff --git a/homeassistant/components/sun/__init__.py b/homeassistant/components/sun/__init__.py index 90718fd540e..dda692a8d80 100644 --- a/homeassistant/components/sun/__init__.py +++ b/homeassistant/components/sun/__init__.py @@ -6,10 +6,9 @@ from homeassistant.const import ( CONF_ELEVATION, SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET) from homeassistant.core import callback from homeassistant.helpers.entity import Entity -from homeassistant.helpers.event import ( - async_track_point_in_utc_time, async_track_utc_time_change) +from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.sun import ( - get_astral_location, get_astral_event_next) + get_astral_location, get_location_astral_event_next) from homeassistant.util import dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -23,6 +22,7 @@ STATE_BELOW_HORIZON = 'below_horizon' STATE_ATTR_AZIMUTH = 'azimuth' STATE_ATTR_ELEVATION = 'elevation' +STATE_ATTR_RISING = 'rising' STATE_ATTR_NEXT_DAWN = 'next_dawn' STATE_ATTR_NEXT_DUSK = 'next_dusk' STATE_ATTR_NEXT_MIDNIGHT = 'next_midnight' @@ -30,6 +30,39 @@ STATE_ATTR_NEXT_NOON = 'next_noon' STATE_ATTR_NEXT_RISING = 'next_rising' STATE_ATTR_NEXT_SETTING = 'next_setting' +# The algorithm used here is somewhat complicated. It aims to cut down +# the number of sensor updates over the day. It's documented best in +# the PR for the change, see the Discussion section of: +# https://github.com/home-assistant/home-assistant/pull/23832 + + +# As documented in wikipedia: https://en.wikipedia.org/wiki/Twilight +# sun is: +# < -18° of horizon - all stars visible +PHASE_NIGHT = 'night' +# 18°-12° - some stars not visible +PHASE_ASTRONOMICAL_TWILIGHT = 'astronomical_twilight' +# 12°-6° - horizon visible +PHASE_NAUTICAL_TWILIGHT = 'nautical_twilight' +# 6°-0° - objects visible +PHASE_TWILIGHT = 'twilight' +# 0°-10° above horizon, sun low on horizon +PHASE_SMALL_DAY = 'small_day' +# > 10° above horizon +PHASE_DAY = 'day' + +# 4 mins is one degree of arc change of the sun on its circle. +# During the night and the middle of the day we don't update +# that much since it's not important. +_PHASE_UPDATES = { + PHASE_NIGHT: timedelta(minutes=4*5), + PHASE_ASTRONOMICAL_TWILIGHT: timedelta(minutes=4*2), + PHASE_NAUTICAL_TWILIGHT: timedelta(minutes=4*2), + PHASE_TWILIGHT: timedelta(minutes=4), + PHASE_SMALL_DAY: timedelta(minutes=2), + PHASE_DAY: timedelta(minutes=4), +} + async def async_setup(hass, config): """Track the state of the sun.""" @@ -37,10 +70,7 @@ async def async_setup(hass, config): _LOGGER.warning( "Elevation is now configured in home assistant core. " "See https://home-assistant.io/docs/configuration/basic/") - - sun = Sun(hass, get_astral_location(hass)) - sun.point_in_time_listener(dt_util.utcnow()) - + Sun(hass, get_astral_location(hass)) return True @@ -57,8 +87,10 @@ class Sun(Entity): self.next_dawn = self.next_dusk = None self.next_midnight = self.next_noon = None self.solar_elevation = self.solar_azimuth = None + self.rising = self.phase = None - async_track_utc_time_change(hass, self.timer_update, second=30) + self._next_change = None + self.update_events(dt_util.utcnow()) @property def name(self): @@ -83,57 +115,110 @@ class Sun(Entity): STATE_ATTR_NEXT_NOON: self.next_noon.isoformat(), STATE_ATTR_NEXT_RISING: self.next_rising.isoformat(), STATE_ATTR_NEXT_SETTING: self.next_setting.isoformat(), - STATE_ATTR_ELEVATION: round(self.solar_elevation, 2), - STATE_ATTR_AZIMUTH: round(self.solar_azimuth, 2) + STATE_ATTR_ELEVATION: self.solar_elevation, + STATE_ATTR_AZIMUTH: self.solar_azimuth, + STATE_ATTR_RISING: self.rising, } - @property - def next_change(self): - """Datetime when the next change to the state is.""" - return min(self.next_dawn, self.next_dusk, self.next_midnight, - self.next_noon, self.next_rising, self.next_setting) + def _check_event(self, utc_point_in_time, event, before): + next_utc = get_location_astral_event_next( + self.location, event, utc_point_in_time) + if next_utc < self._next_change: + self._next_change = next_utc + self.phase = before + return next_utc @callback - def update_as_of(self, utc_point_in_time): + def update_events(self, utc_point_in_time): """Update the attributes containing solar events.""" - self.next_dawn = get_astral_event_next( - self.hass, 'dawn', utc_point_in_time) - self.next_dusk = get_astral_event_next( - self.hass, 'dusk', utc_point_in_time) - self.next_midnight = get_astral_event_next( - self.hass, 'solar_midnight', utc_point_in_time) - self.next_noon = get_astral_event_next( - self.hass, 'solar_noon', utc_point_in_time) - self.next_rising = get_astral_event_next( - self.hass, SUN_EVENT_SUNRISE, utc_point_in_time) - self.next_setting = get_astral_event_next( - self.hass, SUN_EVENT_SUNSET, utc_point_in_time) + self._next_change = utc_point_in_time + timedelta(days=400) + + # Work our way around the solar cycle, figure out the next + # phase. Some of these are stored. + self.location.solar_depression = 'astronomical' + self._check_event(utc_point_in_time, 'dawn', PHASE_NIGHT) + self.location.solar_depression = 'nautical' + self._check_event( + utc_point_in_time, 'dawn', PHASE_ASTRONOMICAL_TWILIGHT) + self.location.solar_depression = 'civil' + self.next_dawn = self._check_event( + utc_point_in_time, 'dawn', PHASE_NAUTICAL_TWILIGHT) + self.next_rising = self._check_event( + utc_point_in_time, SUN_EVENT_SUNRISE, PHASE_TWILIGHT) + self.location.solar_depression = -10 + self._check_event(utc_point_in_time, 'dawn', PHASE_SMALL_DAY) + self.next_noon = self._check_event( + utc_point_in_time, 'solar_noon', None) + self._check_event(utc_point_in_time, 'dusk', PHASE_DAY) + self.next_setting = self._check_event( + utc_point_in_time, SUN_EVENT_SUNSET, PHASE_SMALL_DAY) + self.location.solar_depression = 'civil' + self.next_dusk = self._check_event( + utc_point_in_time, 'dusk', PHASE_TWILIGHT) + self.location.solar_depression = 'nautical' + self._check_event( + utc_point_in_time, 'dusk', PHASE_NAUTICAL_TWILIGHT) + self.location.solar_depression = 'astronomical' + self._check_event( + utc_point_in_time, 'dusk', PHASE_ASTRONOMICAL_TWILIGHT) + self.next_midnight = self._check_event( + utc_point_in_time, 'solar_midnight', None) + + # if the event was solar midday or midnight, phase will now + # be None. Solar noon doesn't always happen when the sun is + # even in the day at the poles, so we can't rely on it. + # Need to calculate phase if next is noon or midnight + if self.phase is None: + elevation = self.location.solar_elevation(self._next_change) + if elevation >= 10: + self.phase = PHASE_DAY + elif elevation >= 0: + self.phase = PHASE_SMALL_DAY + elif elevation >= -6: + self.phase = PHASE_TWILIGHT + elif elevation >= -12: + self.phase = PHASE_NAUTICAL_TWILIGHT + elif elevation >= -18: + self.phase = PHASE_ASTRONOMICAL_TWILIGHT + else: + self.phase = PHASE_NIGHT + + self.rising = self.next_noon < self.next_midnight + + _LOGGER.debug( + "sun phase_update@%s: phase=%s", + utc_point_in_time.isoformat(), + self.phase, + ) + self.update_sun_position(utc_point_in_time) + + # Set timer for the next solar event + async_track_point_in_utc_time( + self.hass, self.update_events, + self._next_change) + _LOGGER.debug("next time: %s", self._next_change.isoformat()) @callback def update_sun_position(self, utc_point_in_time): """Calculate the position of the sun.""" - self.solar_azimuth = self.location.solar_azimuth(utc_point_in_time) - self.solar_elevation = self.location.solar_elevation(utc_point_in_time) + self.solar_azimuth = round( + self.location.solar_azimuth(utc_point_in_time), 2) + self.solar_elevation = round( + self.location.solar_elevation(utc_point_in_time), 2) - @callback - def point_in_time_listener(self, now): - """Run when the state of the sun has changed.""" - self.update_sun_position(now) - self.update_as_of(now) + _LOGGER.debug( + "sun position_update@%s: elevation=%s azimuth=%s", + utc_point_in_time.isoformat(), + self.solar_elevation, self.solar_azimuth + ) self.async_write_ha_state() - _LOGGER.debug("sun point_in_time_listener@%s: %s, %s", - now, self.state, self.state_attributes) - # Schedule next update at next_change+1 second so sun state has changed + # Next update as per the current phase + delta = _PHASE_UPDATES[self.phase] + # if the next update is within 1.25 of the next + # position update just drop it + if utc_point_in_time + delta*1.25 > self._next_change: + return async_track_point_in_utc_time( - self.hass, self.point_in_time_listener, - self.next_change + timedelta(seconds=1)) - _LOGGER.debug("next time: %s", self.next_change + timedelta(seconds=1)) - - @callback - def timer_update(self, time): - """Needed to update solar elevation and azimuth.""" - self.update_sun_position(time) - self.async_write_ha_state() - _LOGGER.debug("sun timer_update@%s: %s, %s", - time, self.state, self.state_attributes) + self.hass, self.update_sun_position, + utc_point_in_time + delta) diff --git a/homeassistant/helpers/sun.py b/homeassistant/helpers/sun.py index 049359a7313..bcb84234b84 100644 --- a/homeassistant/helpers/sun.py +++ b/homeassistant/helpers/sun.py @@ -43,9 +43,18 @@ def get_astral_event_next( utc_point_in_time: Optional[datetime.datetime] = None, offset: Optional[datetime.timedelta] = None) -> datetime.datetime: """Calculate the next specified solar event.""" - from astral import AstralError - location = get_astral_location(hass) + return get_location_astral_event_next( + location, event, utc_point_in_time, offset) + + +@callback +def get_location_astral_event_next( + location: 'astral.Location', event: str, + utc_point_in_time: Optional[datetime.datetime] = None, + offset: Optional[datetime.timedelta] = None) -> datetime.datetime: + """Calculate the next specified solar event.""" + from astral import AstralError if offset is None: offset = datetime.timedelta() diff --git a/tests/components/sun/test_init.py b/tests/components/sun/test_init.py index 2833efa62c4..374527e2c8a 100644 --- a/tests/components/sun/test_init.py +++ b/tests/components/sun/test_init.py @@ -1,153 +1,177 @@ """The tests for the Sun component.""" -# pylint: disable=protected-access -import unittest +from datetime import datetime, timedelta from unittest.mock import patch -from datetime import timedelta, datetime -from homeassistant.setup import setup_component +from pytest import mark + +import homeassistant.components.sun as sun import homeassistant.core as ha import homeassistant.util.dt as dt_util -import homeassistant.components.sun as sun - -from tests.common import get_test_home_assistant +from homeassistant.const import EVENT_STATE_CHANGED +from homeassistant.setup import async_setup_component -# pylint: disable=invalid-name -class TestSun(unittest.TestCase): - """Test the sun module.""" +async def test_setting_rising(hass): + """Test retrieving sun setting and rising.""" + utc_now = datetime(2016, 11, 1, 8, 0, 0, tzinfo=dt_util.UTC) + with patch('homeassistant.helpers.condition.dt_util.utcnow', + return_value=utc_now): + await async_setup_component(hass, sun.DOMAIN, { + sun.DOMAIN: {sun.CONF_ELEVATION: 0}}) - def setUp(self): - """Set up things to be run when tests are started.""" - self.hass = get_test_home_assistant() + await hass.async_block_till_done() + state = hass.states.get(sun.ENTITY_ID) - def tearDown(self): - """Stop everything that was started.""" - self.hass.stop() + from astral import Astral - def test_setting_rising(self): - """Test retrieving sun setting and rising.""" - utc_now = datetime(2016, 11, 1, 8, 0, 0, tzinfo=dt_util.UTC) - with patch('homeassistant.helpers.condition.dt_util.utcnow', - return_value=utc_now): - setup_component(self.hass, sun.DOMAIN, { - sun.DOMAIN: {sun.CONF_ELEVATION: 0}}) + astral = Astral() + utc_today = utc_now.date() - self.hass.block_till_done() - state = self.hass.states.get(sun.ENTITY_ID) + latitude = hass.config.latitude + longitude = hass.config.longitude - from astral import Astral + mod = -1 + while True: + next_dawn = (astral.dawn_utc( + utc_today + timedelta(days=mod), latitude, longitude)) + if next_dawn > utc_now: + break + mod += 1 - astral = Astral() - utc_today = utc_now.date() + mod = -1 + while True: + next_dusk = (astral.dusk_utc( + utc_today + timedelta(days=mod), latitude, longitude)) + if next_dusk > utc_now: + break + mod += 1 - latitude = self.hass.config.latitude - longitude = self.hass.config.longitude + mod = -1 + while True: + next_midnight = (astral.solar_midnight_utc( + utc_today + timedelta(days=mod), longitude)) + if next_midnight > utc_now: + break + mod += 1 - mod = -1 - while True: - next_dawn = (astral.dawn_utc( - utc_today + timedelta(days=mod), latitude, longitude)) - if next_dawn > utc_now: - break - mod += 1 + mod = -1 + while True: + next_noon = (astral.solar_noon_utc( + utc_today + timedelta(days=mod), longitude)) + if next_noon > utc_now: + break + mod += 1 - mod = -1 - while True: - next_dusk = (astral.dusk_utc( - utc_today + timedelta(days=mod), latitude, longitude)) - if next_dusk > utc_now: - break - mod += 1 + mod = -1 + while True: + next_rising = (astral.sunrise_utc( + utc_today + timedelta(days=mod), latitude, longitude)) + if next_rising > utc_now: + break + mod += 1 - mod = -1 - while True: - next_midnight = (astral.solar_midnight_utc( - utc_today + timedelta(days=mod), longitude)) - if next_midnight > utc_now: - break - mod += 1 + mod = -1 + while True: + next_setting = (astral.sunset_utc( + utc_today + timedelta(days=mod), latitude, longitude)) + if next_setting > utc_now: + break + mod += 1 - mod = -1 - while True: - next_noon = (astral.solar_noon_utc( - utc_today + timedelta(days=mod), longitude)) - if next_noon > utc_now: - break - mod += 1 + assert next_dawn == dt_util.parse_datetime( + state.attributes[sun.STATE_ATTR_NEXT_DAWN]) + assert next_dusk == dt_util.parse_datetime( + state.attributes[sun.STATE_ATTR_NEXT_DUSK]) + assert next_midnight == dt_util.parse_datetime( + state.attributes[sun.STATE_ATTR_NEXT_MIDNIGHT]) + assert next_noon == dt_util.parse_datetime( + state.attributes[sun.STATE_ATTR_NEXT_NOON]) + assert next_rising == dt_util.parse_datetime( + state.attributes[sun.STATE_ATTR_NEXT_RISING]) + assert next_setting == dt_util.parse_datetime( + state.attributes[sun.STATE_ATTR_NEXT_SETTING]) - mod = -1 - while True: - next_rising = (astral.sunrise_utc( - utc_today + timedelta(days=mod), latitude, longitude)) - if next_rising > utc_now: - break - mod += 1 - mod = -1 - while True: - next_setting = (astral.sunset_utc( - utc_today + timedelta(days=mod), latitude, longitude)) - if next_setting > utc_now: - break - mod += 1 +async def test_state_change(hass): + """Test if the state changes at next setting/rising.""" + now = datetime(2016, 6, 1, 8, 0, 0, tzinfo=dt_util.UTC) + with patch('homeassistant.helpers.condition.dt_util.utcnow', + return_value=now): + await async_setup_component(hass, sun.DOMAIN, { + sun.DOMAIN: {sun.CONF_ELEVATION: 0}}) - assert next_dawn == dt_util.parse_datetime( - state.attributes[sun.STATE_ATTR_NEXT_DAWN]) - assert next_dusk == dt_util.parse_datetime( - state.attributes[sun.STATE_ATTR_NEXT_DUSK]) - assert next_midnight == dt_util.parse_datetime( - state.attributes[sun.STATE_ATTR_NEXT_MIDNIGHT]) - assert next_noon == dt_util.parse_datetime( - state.attributes[sun.STATE_ATTR_NEXT_NOON]) - assert next_rising == dt_util.parse_datetime( - state.attributes[sun.STATE_ATTR_NEXT_RISING]) - assert next_setting == dt_util.parse_datetime( - state.attributes[sun.STATE_ATTR_NEXT_SETTING]) + await hass.async_block_till_done() - def test_state_change(self): - """Test if the state changes at next setting/rising.""" - now = datetime(2016, 6, 1, 8, 0, 0, tzinfo=dt_util.UTC) - with patch('homeassistant.helpers.condition.dt_util.utcnow', - return_value=now): - setup_component(self.hass, sun.DOMAIN, { - sun.DOMAIN: {sun.CONF_ELEVATION: 0}}) + test_time = dt_util.parse_datetime( + hass.states.get(sun.ENTITY_ID) + .attributes[sun.STATE_ATTR_NEXT_RISING]) + assert test_time is not None - self.hass.block_till_done() + assert sun.STATE_BELOW_HORIZON == \ + hass.states.get(sun.ENTITY_ID).state - test_time = dt_util.parse_datetime( - self.hass.states.get(sun.ENTITY_ID) - .attributes[sun.STATE_ATTR_NEXT_RISING]) - assert test_time is not None + hass.bus.async_fire( + ha.EVENT_TIME_CHANGED, + {ha.ATTR_NOW: test_time + timedelta(seconds=5)}) - assert sun.STATE_BELOW_HORIZON == \ - self.hass.states.get(sun.ENTITY_ID).state + await hass.async_block_till_done() - self.hass.bus.fire(ha.EVENT_TIME_CHANGED, - {ha.ATTR_NOW: test_time + timedelta(seconds=5)}) + assert sun.STATE_ABOVE_HORIZON == \ + hass.states.get(sun.ENTITY_ID).state - self.hass.block_till_done() - assert sun.STATE_ABOVE_HORIZON == \ - self.hass.states.get(sun.ENTITY_ID).state +async def test_norway_in_june(hass): + """Test location in Norway where the sun doesn't set in summer.""" + hass.config.latitude = 69.6 + hass.config.longitude = 18.8 - def test_norway_in_june(self): - """Test location in Norway where the sun doesn't set in summer.""" - self.hass.config.latitude = 69.6 - self.hass.config.longitude = 18.8 + june = datetime(2016, 6, 1, tzinfo=dt_util.UTC) - june = datetime(2016, 6, 1, tzinfo=dt_util.UTC) + with patch('homeassistant.helpers.condition.dt_util.utcnow', + return_value=june): + assert await async_setup_component(hass, sun.DOMAIN, { + sun.DOMAIN: {sun.CONF_ELEVATION: 0}}) - with patch('homeassistant.helpers.condition.dt_util.utcnow', - return_value=june): - assert setup_component(self.hass, sun.DOMAIN, { - sun.DOMAIN: {sun.CONF_ELEVATION: 0}}) + state = hass.states.get(sun.ENTITY_ID) + assert state is not None - state = self.hass.states.get(sun.ENTITY_ID) - assert state is not None + assert dt_util.parse_datetime( + state.attributes[sun.STATE_ATTR_NEXT_RISING]) == \ + datetime(2016, 7, 25, 23, 23, 39, tzinfo=dt_util.UTC) + assert dt_util.parse_datetime( + state.attributes[sun.STATE_ATTR_NEXT_SETTING]) == \ + datetime(2016, 7, 26, 22, 19, 1, tzinfo=dt_util.UTC) - assert dt_util.parse_datetime( - state.attributes[sun.STATE_ATTR_NEXT_RISING]) == \ - datetime(2016, 7, 25, 23, 23, 39, tzinfo=dt_util.UTC) - assert dt_util.parse_datetime( - state.attributes[sun.STATE_ATTR_NEXT_SETTING]) == \ - datetime(2016, 7, 26, 22, 19, 1, tzinfo=dt_util.UTC) + +@mark.skip +async def test_state_change_count(hass): + """Count the number of state change events in a location.""" + # Skipped because it's a bit slow. Has been validated with + # multiple lattitudes and dates + hass.config.latitude = 10 + hass.config.longitude = 0 + + now = datetime(2016, 6, 1, tzinfo=dt_util.UTC) + + with patch( + 'homeassistant.helpers.condition.dt_util.utcnow', + return_value=now): + assert await async_setup_component(hass, sun.DOMAIN, { + sun.DOMAIN: {sun.CONF_ELEVATION: 0}}) + + events = [] + @ha.callback + def state_change_listener(event): + if event.data.get('entity_id') == 'sun.sun': + events.append(event) + hass.bus.async_listen(EVENT_STATE_CHANGED, state_change_listener) + await hass.async_block_till_done() + + for _ in range(24*60*60): + now += timedelta(seconds=1) + hass.bus.async_fire( + ha.EVENT_TIME_CHANGED, + {ha.ATTR_NOW: now}) + await hass.async_block_till_done() + + assert len(events) < 721 From 2dc78e6f0c61911424b15213599c4d01283348cf Mon Sep 17 00:00:00 2001 From: Penny Wood Date: Wed, 15 May 2019 16:14:35 +0800 Subject: [PATCH 051/232] Take code owner for sun.sun (#23877) * Take code owner * Post hassfest --- CODEOWNERS | 2 +- homeassistant/components/sun/manifest.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index cc4138b362d..29d4adc8933 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -214,7 +214,7 @@ homeassistant/components/spider/* @peternijssen homeassistant/components/sql/* @dgomes homeassistant/components/statistics/* @fabaff homeassistant/components/stiebel_eltron/* @fucm -homeassistant/components/sun/* @home-assistant/core +homeassistant/components/sun/* @Swamp-Ig homeassistant/components/supla/* @mwegrzynek homeassistant/components/swiss_hydrological_data/* @fabaff homeassistant/components/swiss_public_transport/* @fabaff diff --git a/homeassistant/components/sun/manifest.json b/homeassistant/components/sun/manifest.json index 2ef89da8f69..e55131306dc 100644 --- a/homeassistant/components/sun/manifest.json +++ b/homeassistant/components/sun/manifest.json @@ -5,6 +5,6 @@ "requirements": [], "dependencies": [], "codeowners": [ - "@home-assistant/core" + "@Swamp-Ig" ] } From cf89f456976efe53670dd65c100c7fcabcdd3d28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Wed, 15 May 2019 14:13:57 +0300 Subject: [PATCH 052/232] Fix homekit test assert no messages (#23856) --- tests/components/homekit/test_type_media_players.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/homekit/test_type_media_players.py b/tests/components/homekit/test_type_media_players.py index 3c00867a2cf..98b4e2239f0 100644 --- a/tests/components/homekit/test_type_media_players.py +++ b/tests/components/homekit/test_type_media_players.py @@ -311,4 +311,4 @@ async def test_media_player_television_basic(hass, hk_driver, events, caplog): await hass.async_block_till_done() assert acc.char_active.value == 1 - assert 'Error' not in caplog.messages[-1] + assert not caplog.messages or 'Error' not in caplog.messages[-1] From 7a4238095d575fab2ce114b092b8d25c150e8677 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 15 May 2019 16:27:41 +0200 Subject: [PATCH 053/232] Fix auto discovery if the monitor condition (#23880) --- homeassistant/components/netatmo/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index 046bf5c57f8..7b71eaf659c 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -145,7 +145,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): # Only create sensors for monitored properties for condition in monitored_conditions: dev.append(NetatmoSensor( - data, module_name, condition)) + data, module_name, condition.lower())) for module_name, _ in not_handled.items(): _LOGGER.error('Module name: "%s" not found', module_name) From 70ed58a78d7cde35f8b4ac5284db53644ab42717 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 15 May 2019 23:43:45 +0200 Subject: [PATCH 054/232] Restructure device tracker (#23862) * Restructure device tracker * Docstyle * Fix typing * Lint * Lint * Fix tests --- .../bluetooth_le_tracker/device_tracker.py | 16 +- .../bluetooth_tracker/device_tracker.py | 20 +- .../components/device_tracker/__init__.py | 709 ++---------------- .../components/device_tracker/const.py | 40 + .../components/device_tracker/legacy.py | 528 +++++++++++++ .../components/device_tracker/setup.py | 199 +++++ .../components/icloud/device_tracker.py | 6 +- .../components/ping/device_tracker.py | 7 +- homeassistant/config.py | 14 +- tests/components/demo/test_init.py | 5 +- .../device_sun_light_trigger/test_init.py | 6 +- tests/components/device_tracker/test_init.py | 113 +-- tests/components/geofency/test_init.py | 2 +- tests/components/gpslogger/test_init.py | 2 +- tests/components/locative/test_init.py | 2 +- tests/components/mqtt/test_device_tracker.py | 11 +- .../mqtt_json/test_device_tracker.py | 43 +- .../components/tplink/test_device_tracker.py | 4 +- .../unifi_direct/test_device_tracker.py | 4 +- tests/conftest.py | 4 +- 20 files changed, 978 insertions(+), 757 deletions(-) create mode 100644 homeassistant/components/device_tracker/const.py create mode 100644 homeassistant/components/device_tracker/legacy.py create mode 100644 homeassistant/components/device_tracker/setup.py diff --git a/homeassistant/components/bluetooth_le_tracker/device_tracker.py b/homeassistant/components/bluetooth_le_tracker/device_tracker.py index d256f56e7fe..6b5fcd7df06 100644 --- a/homeassistant/components/bluetooth_le_tracker/device_tracker.py +++ b/homeassistant/components/bluetooth_le_tracker/device_tracker.py @@ -2,12 +2,15 @@ import logging from homeassistant.helpers.event import track_point_in_utc_time -from homeassistant.components.device_tracker import ( - YAML_DEVICES, CONF_TRACK_NEW, CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL, - load_config, SOURCE_TYPE_BLUETOOTH_LE +from homeassistant.components.device_tracker.legacy import ( + YAML_DEVICES, async_load_config +) +from homeassistant.components.device_tracker.const import ( + CONF_TRACK_NEW, CONF_SCAN_INTERVAL, SCAN_INTERVAL, SOURCE_TYPE_BLUETOOTH_LE ) from homeassistant.const import EVENT_HOMEASSISTANT_STOP import homeassistant.util.dt as dt_util +from homeassistant.util.async_ import run_coroutine_threadsafe _LOGGER = logging.getLogger(__name__) @@ -79,7 +82,10 @@ def setup_scanner(hass, config, see, discovery_info=None): # Load all known devices. # We just need the devices so set consider_home and home range # to 0 - for device in load_config(yaml_path, hass, 0): + for device in run_coroutine_threadsafe( + async_load_config(yaml_path, hass, 0), + hass.loop + ).result(): # check if device is a valid bluetooth device if device.mac and device.mac[:4].upper() == BLE_PREFIX: if device.track: @@ -97,7 +103,7 @@ def setup_scanner(hass, config, see, discovery_info=None): _LOGGER.warning("No Bluetooth LE devices to track!") return False - interval = config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) + interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) def update_ble(now): """Lookup Bluetooth LE devices and update status.""" diff --git a/homeassistant/components/bluetooth_tracker/device_tracker.py b/homeassistant/components/bluetooth_tracker/device_tracker.py index d464e87ce64..28b914a94ca 100644 --- a/homeassistant/components/bluetooth_tracker/device_tracker.py +++ b/homeassistant/components/bluetooth_tracker/device_tracker.py @@ -5,11 +5,16 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import track_point_in_utc_time -from homeassistant.components.device_tracker import ( - YAML_DEVICES, CONF_TRACK_NEW, CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL, - load_config, PLATFORM_SCHEMA, DEFAULT_TRACK_NEW, SOURCE_TYPE_BLUETOOTH, - DOMAIN) +from homeassistant.components.device_tracker import PLATFORM_SCHEMA +from homeassistant.components.device_tracker.legacy import ( + YAML_DEVICES, async_load_config +) +from homeassistant.components.device_tracker.const import ( + CONF_TRACK_NEW, CONF_SCAN_INTERVAL, SCAN_INTERVAL, DEFAULT_TRACK_NEW, + SOURCE_TYPE_BLUETOOTH, DOMAIN +) import homeassistant.util.dt as dt_util +from homeassistant.util.async_ import run_coroutine_threadsafe _LOGGER = logging.getLogger(__name__) @@ -60,7 +65,10 @@ def setup_scanner(hass, config, see, discovery_info=None): # Load all known devices. # We just need the devices so set consider_home and home range # to 0 - for device in load_config(yaml_path, hass, 0): + for device in run_coroutine_threadsafe( + async_load_config(yaml_path, hass, 0), + hass.loop + ).result(): # Check if device is a valid bluetooth device if device.mac and device.mac[:3].upper() == BT_PREFIX: if device.track: @@ -77,7 +85,7 @@ def setup_scanner(hass, config, see, discovery_info=None): devs_to_track.append(dev[0]) see_device(dev[0], dev[1]) - interval = config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) + interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) request_rssi = config.get(CONF_REQUEST_RSSI, False) diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index 60dac103a46..d7947fd5123 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -1,78 +1,53 @@ """Provide functionality to keep track of devices.""" import asyncio -from datetime import timedelta -import logging -from typing import Any, List, Sequence, Callable import voluptuous as vol -from homeassistant.setup import async_prepare_setup_platform -from homeassistant.core import callback +from homeassistant.helpers.entity_component import EntityComponent from homeassistant.loader import bind_hass -from homeassistant.components import group, zone -from homeassistant.components.group import ( - ATTR_ADD_ENTITIES, ATTR_ENTITIES, ATTR_OBJECT_ID, ATTR_VISIBLE, - DOMAIN as DOMAIN_GROUP, SERVICE_SET) -from homeassistant.components.zone.zone import async_active_zone -from homeassistant.config import load_yaml_config_file, async_log_exception -from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import config_per_platform, discovery +from homeassistant.components import group +from homeassistant.config import config_without_domain +from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.event import async_track_time_interval -from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import GPSType, ConfigType, HomeAssistantType -from homeassistant import util -from homeassistant.util.async_ import run_coroutine_threadsafe -import homeassistant.util.dt as dt_util -from homeassistant.util.yaml import dump from homeassistant.helpers.event import async_track_utc_time_change -from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_GPS_ACCURACY, ATTR_ICON, ATTR_LATITUDE, - ATTR_LONGITUDE, ATTR_NAME, CONF_ICON, CONF_MAC, CONF_NAME, - DEVICE_DEFAULT_NAME, STATE_NOT_HOME, STATE_HOME) +from homeassistant.const import ATTR_GPS_ACCURACY, STATE_HOME -_LOGGER = logging.getLogger(__name__) +from . import legacy, setup +from .legacy import DeviceScanner # noqa # pylint: disable=unused-import +from .const import ( + ATTR_ATTRIBUTES, + ATTR_BATTERY, + ATTR_CONSIDER_HOME, + ATTR_DEV_ID, + ATTR_GPS, + ATTR_HOST_NAME, + ATTR_LOCATION_NAME, + ATTR_MAC, + ATTR_SOURCE_TYPE, + CONF_AWAY_HIDE, + CONF_CONSIDER_HOME, + CONF_NEW_DEVICE_DEFAULTS, + CONF_SCAN_INTERVAL, + CONF_TRACK_NEW, + DEFAULT_AWAY_HIDE, + DEFAULT_CONSIDER_HOME, + DEFAULT_TRACK_NEW, + DOMAIN, + LOGGER, + PLATFORM_TYPE_LEGACY, + SCAN_INTERVAL, + SOURCE_TYPE_BLUETOOTH_LE, + SOURCE_TYPE_BLUETOOTH, + SOURCE_TYPE_GPS, + SOURCE_TYPE_ROUTER, +) -DOMAIN = 'device_tracker' -GROUP_NAME_ALL_DEVICES = 'all devices' ENTITY_ID_ALL_DEVICES = group.ENTITY_ID_FORMAT.format('all_devices') -ENTITY_ID_FORMAT = DOMAIN + '.{}' - -YAML_DEVICES = 'known_devices.yaml' - -CONF_TRACK_NEW = 'track_new_devices' -DEFAULT_TRACK_NEW = True -CONF_NEW_DEVICE_DEFAULTS = 'new_device_defaults' - -CONF_CONSIDER_HOME = 'consider_home' -DEFAULT_CONSIDER_HOME = timedelta(seconds=180) - -CONF_SCAN_INTERVAL = 'interval_seconds' -DEFAULT_SCAN_INTERVAL = timedelta(seconds=12) - -CONF_AWAY_HIDE = 'hide_if_away' -DEFAULT_AWAY_HIDE = False - -EVENT_NEW_DEVICE = 'device_tracker_new_device' - SERVICE_SEE = 'see' -ATTR_ATTRIBUTES = 'attributes' -ATTR_BATTERY = 'battery' -ATTR_DEV_ID = 'dev_id' -ATTR_GPS = 'gps' -ATTR_HOST_NAME = 'host_name' -ATTR_LOCATION_NAME = 'location_name' -ATTR_MAC = 'mac' -ATTR_SOURCE_TYPE = 'source_type' -ATTR_CONSIDER_HOME = 'consider_home' - -SOURCE_TYPE_GPS = 'gps' -SOURCE_TYPE_ROUTER = 'router' -SOURCE_TYPE_BLUETOOTH = 'bluetooth' -SOURCE_TYPE_BLUETOOTH_LE = 'bluetooth_le' SOURCE_TYPES = (SOURCE_TYPE_GPS, SOURCE_TYPE_ROUTER, SOURCE_TYPE_BLUETOOTH, SOURCE_TYPE_BLUETOOTH_LE) @@ -136,75 +111,52 @@ def see(hass: HomeAssistantType, mac: str = None, dev_id: str = None, async def async_setup(hass: HomeAssistantType, config: ConfigType): """Set up the device tracker.""" - yaml_path = hass.config.path(YAML_DEVICES) + tracker = await legacy.get_tracker(hass, config) - conf = config.get(DOMAIN, []) - conf = conf[0] if conf else {} - consider_home = conf.get(CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME) + async def setup_entry_helper(entry): + """Set up a config entry.""" + platform = await setup.async_create_platform_type( + hass, config, entry.domain, entry) - defaults = conf.get(CONF_NEW_DEVICE_DEFAULTS, {}) - track_new = conf.get(CONF_TRACK_NEW) - if track_new is None: - track_new = defaults.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW) - - devices = await async_load_config(yaml_path, hass, consider_home) - tracker = DeviceTracker( - hass, consider_home, track_new, defaults, devices) - - async def async_setup_platform(p_type, p_config, disc_info=None): - """Set up a device tracker platform.""" - platform = await async_prepare_setup_platform( - hass, config, DOMAIN, p_type) if platform is None: - return + return False - _LOGGER.info("Setting up %s.%s", DOMAIN, p_type) - try: - scanner = None - setup = None - if hasattr(platform, 'async_get_scanner'): - scanner = await platform.async_get_scanner( - hass, {DOMAIN: p_config}) - elif hasattr(platform, 'get_scanner'): - scanner = await hass.async_add_job( - platform.get_scanner, hass, {DOMAIN: p_config}) - elif hasattr(platform, 'async_setup_scanner'): - setup = await platform.async_setup_scanner( - hass, p_config, tracker.async_see, disc_info) - elif hasattr(platform, 'setup_scanner'): - setup = await hass.async_add_job( - platform.setup_scanner, hass, p_config, tracker.see, - disc_info) - elif hasattr(platform, 'async_setup_entry'): - setup = await platform.async_setup_entry( - hass, p_config, tracker.async_see) - else: - raise HomeAssistantError("Invalid device_tracker platform.") + await platform.async_setup_legacy(hass, tracker) - if scanner: - async_setup_scanner_platform( - hass, p_config, scanner, tracker.async_see, p_type) - return + return True - if not setup: - _LOGGER.error("Error setting up platform %s", p_type) - return + hass.data[DOMAIN] = setup_entry_helper + component = EntityComponent( + LOGGER, DOMAIN, hass, SCAN_INTERVAL) - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Error setting up platform %s", p_type) + legacy_platforms, entity_platforms = \ + await setup.async_extract_config(hass, config) - hass.data[DOMAIN] = async_setup_platform + setup_tasks = [ + legacy_platform.async_setup_legacy(hass, tracker) + for legacy_platform in legacy_platforms + ] + + if entity_platforms: + setup_tasks.append(component.async_setup({ + **config_without_domain(config, DOMAIN), + DOMAIN: [platform.config for platform in entity_platforms] + })) - setup_tasks = [async_setup_platform(p_type, p_config) for p_type, p_config - in config_per_platform(config, DOMAIN)] if setup_tasks: await asyncio.wait(setup_tasks, loop=hass.loop) tracker.async_setup_group() - async def async_platform_discovered(platform, info): + async def async_platform_discovered(p_type, info): """Load a platform.""" - await async_setup_platform(platform, {}, disc_info=info) + platform = await setup.async_create_platform_type( + hass, config, p_type, {}) + + if platform is None or platform.type != PLATFORM_TYPE_LEGACY: + return + + await platform.async_setup_legacy(hass, tracker, info) discovery.async_listen_platform(hass, DOMAIN, async_platform_discovered) @@ -230,533 +182,4 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType): async def async_setup_entry(hass, entry): """Set up an entry.""" - await hass.data[DOMAIN](entry.domain, entry) - return True - - -class DeviceTracker: - """Representation of a device tracker.""" - - def __init__(self, hass: HomeAssistantType, consider_home: timedelta, - track_new: bool, defaults: dict, - devices: Sequence) -> None: - """Initialize a device tracker.""" - self.hass = hass - self.devices = {dev.dev_id: dev for dev in devices} - self.mac_to_dev = {dev.mac: dev for dev in devices if dev.mac} - self.consider_home = consider_home - self.track_new = track_new if track_new is not None \ - else defaults.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW) - self.defaults = defaults - self.group = None - self._is_updating = asyncio.Lock(loop=hass.loop) - - for dev in devices: - if self.devices[dev.dev_id] is not dev: - _LOGGER.warning('Duplicate device IDs detected %s', dev.dev_id) - if dev.mac and self.mac_to_dev[dev.mac] is not dev: - _LOGGER.warning('Duplicate device MAC addresses detected %s', - dev.mac) - - def see(self, mac: str = None, dev_id: str = None, host_name: str = None, - location_name: str = None, gps: GPSType = None, - gps_accuracy: int = None, battery: int = None, - attributes: dict = None, source_type: str = SOURCE_TYPE_GPS, - picture: str = None, icon: str = None, - consider_home: timedelta = None): - """Notify the device tracker that you see a device.""" - self.hass.add_job( - self.async_see(mac, dev_id, host_name, location_name, gps, - gps_accuracy, battery, attributes, source_type, - picture, icon, consider_home) - ) - - async def async_see( - self, mac: str = None, dev_id: str = None, host_name: str = None, - location_name: str = None, gps: GPSType = None, - gps_accuracy: int = None, battery: int = None, - attributes: dict = None, source_type: str = SOURCE_TYPE_GPS, - picture: str = None, icon: str = None, - consider_home: timedelta = None): - """Notify the device tracker that you see a device. - - This method is a coroutine. - """ - if mac is None and dev_id is None: - raise HomeAssistantError('Neither mac or device id passed in') - if mac is not None: - mac = str(mac).upper() - device = self.mac_to_dev.get(mac) - if not device: - dev_id = util.slugify(host_name or '') or util.slugify(mac) - else: - dev_id = cv.slug(str(dev_id).lower()) - device = self.devices.get(dev_id) - - if device: - await device.async_seen( - host_name, location_name, gps, gps_accuracy, battery, - attributes, source_type, consider_home) - if device.track: - await device.async_update_ha_state() - return - - # If no device can be found, create it - dev_id = util.ensure_unique_string(dev_id, self.devices.keys()) - device = Device( - self.hass, consider_home or self.consider_home, self.track_new, - dev_id, mac, (host_name or dev_id).replace('_', ' '), - picture=picture, icon=icon, - hide_if_away=self.defaults.get(CONF_AWAY_HIDE, DEFAULT_AWAY_HIDE)) - self.devices[dev_id] = device - if mac is not None: - self.mac_to_dev[mac] = device - - await device.async_seen( - host_name, location_name, gps, gps_accuracy, battery, attributes, - source_type) - - if device.track: - await device.async_update_ha_state() - - # During init, we ignore the group - if self.group and self.track_new: - self.hass.async_create_task( - self.hass.async_call( - DOMAIN_GROUP, SERVICE_SET, { - ATTR_OBJECT_ID: util.slugify(GROUP_NAME_ALL_DEVICES), - ATTR_VISIBLE: False, - ATTR_NAME: GROUP_NAME_ALL_DEVICES, - ATTR_ADD_ENTITIES: [device.entity_id]})) - - self.hass.bus.async_fire(EVENT_NEW_DEVICE, { - ATTR_ENTITY_ID: device.entity_id, - ATTR_HOST_NAME: device.host_name, - ATTR_MAC: device.mac, - }) - - # update known_devices.yaml - self.hass.async_create_task( - self.async_update_config( - self.hass.config.path(YAML_DEVICES), dev_id, device) - ) - - async def async_update_config(self, path, dev_id, device): - """Add device to YAML configuration file. - - This method is a coroutine. - """ - async with self._is_updating: - await self.hass.async_add_executor_job( - update_config, self.hass.config.path(YAML_DEVICES), - dev_id, device) - - @callback - def async_setup_group(self): - """Initialize group for all tracked devices. - - This method must be run in the event loop. - """ - entity_ids = [dev.entity_id for dev in self.devices.values() - if dev.track] - - self.hass.async_create_task( - self.hass.services.async_call( - DOMAIN_GROUP, SERVICE_SET, { - ATTR_OBJECT_ID: util.slugify(GROUP_NAME_ALL_DEVICES), - ATTR_VISIBLE: False, - ATTR_NAME: GROUP_NAME_ALL_DEVICES, - ATTR_ENTITIES: entity_ids})) - - @callback - def async_update_stale(self, now: dt_util.dt.datetime): - """Update stale devices. - - This method must be run in the event loop. - """ - for device in self.devices.values(): - if (device.track and device.last_update_home) and \ - device.stale(now): - self.hass.async_create_task(device.async_update_ha_state(True)) - - async def async_setup_tracked_device(self): - """Set up all not exists tracked devices. - - This method is a coroutine. - """ - async def async_init_single_device(dev): - """Init a single device_tracker entity.""" - await dev.async_added_to_hass() - await dev.async_update_ha_state() - - tasks = [] - for device in self.devices.values(): - if device.track and not device.last_seen: - tasks.append(self.hass.async_create_task( - async_init_single_device(device))) - - if tasks: - await asyncio.wait(tasks, loop=self.hass.loop) - - -class Device(RestoreEntity): - """Represent a tracked device.""" - - host_name = None # type: str - location_name = None # type: str - gps = None # type: GPSType - gps_accuracy = 0 # type: int - last_seen = None # type: dt_util.dt.datetime - consider_home = None # type: dt_util.dt.timedelta - battery = None # type: int - attributes = None # type: dict - icon = None # type: str - - # Track if the last update of this device was HOME. - last_update_home = False - _state = STATE_NOT_HOME - - def __init__(self, hass: HomeAssistantType, consider_home: timedelta, - track: bool, dev_id: str, mac: str, name: str = None, - picture: str = None, gravatar: str = None, icon: str = None, - hide_if_away: bool = False) -> None: - """Initialize a device.""" - self.hass = hass - self.entity_id = ENTITY_ID_FORMAT.format(dev_id) - - # Timedelta object how long we consider a device home if it is not - # detected anymore. - self.consider_home = consider_home - - # Device ID - self.dev_id = dev_id - self.mac = mac - - # If we should track this device - self.track = track - - # Configured name - self.config_name = name - - # Configured picture - if gravatar is not None: - self.config_picture = get_gravatar_for_email(gravatar) - else: - self.config_picture = picture - - self.icon = icon - - self.away_hide = hide_if_away - - self.source_type = None - - self._attributes = {} - - @property - def name(self): - """Return the name of the entity.""" - return self.config_name or self.host_name or DEVICE_DEFAULT_NAME - - @property - def state(self): - """Return the state of the device.""" - return self._state - - @property - def entity_picture(self): - """Return the picture of the device.""" - return self.config_picture - - @property - def state_attributes(self): - """Return the device state attributes.""" - attr = { - ATTR_SOURCE_TYPE: self.source_type - } - - if self.gps: - attr[ATTR_LATITUDE] = self.gps[0] - attr[ATTR_LONGITUDE] = self.gps[1] - attr[ATTR_GPS_ACCURACY] = self.gps_accuracy - - if self.battery: - attr[ATTR_BATTERY] = self.battery - - return attr - - @property - def device_state_attributes(self): - """Return device state attributes.""" - return self._attributes - - @property - def hidden(self): - """If device should be hidden.""" - return self.away_hide and self.state != STATE_HOME - - async def async_seen( - self, host_name: str = None, location_name: str = None, - gps: GPSType = None, gps_accuracy=0, battery: int = None, - attributes: dict = None, - source_type: str = SOURCE_TYPE_GPS, - consider_home: timedelta = None): - """Mark the device as seen.""" - self.source_type = source_type - self.last_seen = dt_util.utcnow() - self.host_name = host_name - self.location_name = location_name - self.consider_home = consider_home or self.consider_home - - if battery: - self.battery = battery - if attributes: - self._attributes.update(attributes) - - self.gps = None - - if gps is not None: - try: - self.gps = float(gps[0]), float(gps[1]) - self.gps_accuracy = gps_accuracy or 0 - except (ValueError, TypeError, IndexError): - self.gps = None - self.gps_accuracy = 0 - _LOGGER.warning( - "Could not parse gps value for %s: %s", self.dev_id, gps) - - # pylint: disable=not-an-iterable - await self.async_update() - - def stale(self, now: dt_util.dt.datetime = None): - """Return if device state is stale. - - Async friendly. - """ - return self.last_seen is None or \ - (now or dt_util.utcnow()) - self.last_seen > self.consider_home - - def mark_stale(self): - """Mark the device state as stale.""" - self._state = STATE_NOT_HOME - self.gps = None - self.last_update_home = False - - async def async_update(self): - """Update state of entity. - - This method is a coroutine. - """ - if not self.last_seen: - return - if self.location_name: - self._state = self.location_name - elif self.gps is not None and self.source_type == SOURCE_TYPE_GPS: - zone_state = async_active_zone( - self.hass, self.gps[0], self.gps[1], self.gps_accuracy) - if zone_state is None: - self._state = STATE_NOT_HOME - elif zone_state.entity_id == zone.ENTITY_ID_HOME: - self._state = STATE_HOME - else: - self._state = zone_state.name - elif self.stale(): - self.mark_stale() - else: - self._state = STATE_HOME - self.last_update_home = True - - async def async_added_to_hass(self): - """Add an entity.""" - await super().async_added_to_hass() - state = await self.async_get_last_state() - if not state: - return - self._state = state.state - self.last_update_home = (state.state == STATE_HOME) - self.last_seen = dt_util.utcnow() - - for attr, var in ( - (ATTR_SOURCE_TYPE, 'source_type'), - (ATTR_GPS_ACCURACY, 'gps_accuracy'), - (ATTR_BATTERY, 'battery'), - ): - if attr in state.attributes: - setattr(self, var, state.attributes[attr]) - - if ATTR_LONGITUDE in state.attributes: - self.gps = (state.attributes[ATTR_LATITUDE], - state.attributes[ATTR_LONGITUDE]) - - -class DeviceScanner: - """Device scanner object.""" - - hass = None # type: HomeAssistantType - - def scan_devices(self) -> List[str]: - """Scan for devices.""" - raise NotImplementedError() - - def async_scan_devices(self) -> Any: - """Scan for devices. - - This method must be run in the event loop and returns a coroutine. - """ - return self.hass.async_add_job(self.scan_devices) - - def get_device_name(self, device: str) -> str: - """Get the name of a device.""" - raise NotImplementedError() - - def async_get_device_name(self, device: str) -> Any: - """Get the name of a device. - - This method must be run in the event loop and returns a coroutine. - """ - return self.hass.async_add_job(self.get_device_name, device) - - def get_extra_attributes(self, device: str) -> dict: - """Get the extra attributes of a device.""" - raise NotImplementedError() - - def async_get_extra_attributes(self, device: str) -> Any: - """Get the extra attributes of a device. - - This method must be run in the event loop and returns a coroutine. - """ - return self.hass.async_add_job(self.get_extra_attributes, device) - - -def load_config(path: str, hass: HomeAssistantType, consider_home: timedelta): - """Load devices from YAML configuration file.""" - return run_coroutine_threadsafe( - async_load_config(path, hass, consider_home), hass.loop).result() - - -async def async_load_config(path: str, hass: HomeAssistantType, - consider_home: timedelta): - """Load devices from YAML configuration file. - - This method is a coroutine. - """ - dev_schema = vol.Schema({ - vol.Required(CONF_NAME): cv.string, - vol.Optional(CONF_ICON, default=None): vol.Any(None, cv.icon), - vol.Optional('track', default=False): cv.boolean, - vol.Optional(CONF_MAC, default=None): - vol.Any(None, vol.All(cv.string, vol.Upper)), - vol.Optional(CONF_AWAY_HIDE, default=DEFAULT_AWAY_HIDE): cv.boolean, - vol.Optional('gravatar', default=None): vol.Any(None, cv.string), - vol.Optional('picture', default=None): vol.Any(None, cv.string), - vol.Optional(CONF_CONSIDER_HOME, default=consider_home): vol.All( - cv.time_period, cv.positive_timedelta), - }) - try: - result = [] - try: - devices = await hass.async_add_job( - load_yaml_config_file, path) - except HomeAssistantError as err: - _LOGGER.error("Unable to load %s: %s", path, str(err)) - return [] - - for dev_id, device in devices.items(): - # Deprecated option. We just ignore it to avoid breaking change - device.pop('vendor', None) - try: - device = dev_schema(device) - device['dev_id'] = cv.slugify(dev_id) - except vol.Invalid as exp: - async_log_exception(exp, dev_id, devices, hass) - else: - result.append(Device(hass, **device)) - return result - except (HomeAssistantError, FileNotFoundError): - # When YAML file could not be loaded/did not contain a dict - return [] - - -@callback -def async_setup_scanner_platform(hass: HomeAssistantType, config: ConfigType, - scanner: Any, async_see_device: Callable, - platform: str): - """Set up the connect scanner-based platform to device tracker. - - This method must be run in the event loop. - """ - interval = config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) - update_lock = asyncio.Lock(loop=hass.loop) - scanner.hass = hass - - # Initial scan of each mac we also tell about host name for config - seen = set() # type: Any - - async def async_device_tracker_scan(now: dt_util.dt.datetime): - """Handle interval matches.""" - if update_lock.locked(): - _LOGGER.warning( - "Updating device list from %s took longer than the scheduled " - "scan interval %s", platform, interval) - return - - async with update_lock: - found_devices = await scanner.async_scan_devices() - - for mac in found_devices: - if mac in seen: - host_name = None - else: - host_name = await scanner.async_get_device_name(mac) - seen.add(mac) - - try: - extra_attributes = \ - await scanner.async_get_extra_attributes(mac) - except NotImplementedError: - extra_attributes = dict() - - kwargs = { - 'mac': mac, - 'host_name': host_name, - 'source_type': SOURCE_TYPE_ROUTER, - 'attributes': { - 'scanner': scanner.__class__.__name__, - **extra_attributes - } - } - - zone_home = hass.states.get(zone.ENTITY_ID_HOME) - if zone_home: - kwargs['gps'] = [zone_home.attributes[ATTR_LATITUDE], - zone_home.attributes[ATTR_LONGITUDE]] - kwargs['gps_accuracy'] = 0 - - hass.async_create_task(async_see_device(**kwargs)) - - async_track_time_interval(hass, async_device_tracker_scan, interval) - hass.async_create_task(async_device_tracker_scan(None)) - - -def update_config(path: str, dev_id: str, device: Device): - """Add device to YAML configuration file.""" - with open(path, 'a') as out: - device = {device.dev_id: { - ATTR_NAME: device.name, - ATTR_MAC: device.mac, - ATTR_ICON: device.icon, - 'picture': device.config_picture, - 'track': device.track, - CONF_AWAY_HIDE: device.away_hide, - }} - out.write('\n') - out.write(dump(device)) - - -def get_gravatar_for_email(email: str): - """Return an 80px Gravatar for the given email address. - - Async friendly. - """ - import hashlib - url = 'https://www.gravatar.com/avatar/{}.jpg?s=80&d=wavatar' - return url.format(hashlib.md5(email.encode('utf-8').lower()).hexdigest()) + return await hass.data[DOMAIN](entry) diff --git a/homeassistant/components/device_tracker/const.py b/homeassistant/components/device_tracker/const.py new file mode 100644 index 00000000000..18ec486e693 --- /dev/null +++ b/homeassistant/components/device_tracker/const.py @@ -0,0 +1,40 @@ +"""Device tracker constants.""" +from datetime import timedelta +import logging + +LOGGER = logging.getLogger(__package__) + +DOMAIN = 'device_tracker' +ENTITY_ID_FORMAT = DOMAIN + '.{}' + +PLATFORM_TYPE_LEGACY = 'legacy' +PLATFORM_TYPE_ENTITY = 'entity_platform' + +SOURCE_TYPE_GPS = 'gps' +SOURCE_TYPE_ROUTER = 'router' +SOURCE_TYPE_BLUETOOTH = 'bluetooth' +SOURCE_TYPE_BLUETOOTH_LE = 'bluetooth_le' + +CONF_SCAN_INTERVAL = 'interval_seconds' +SCAN_INTERVAL = timedelta(seconds=12) + +CONF_TRACK_NEW = 'track_new_devices' +DEFAULT_TRACK_NEW = True + +CONF_AWAY_HIDE = 'hide_if_away' +DEFAULT_AWAY_HIDE = False + +CONF_CONSIDER_HOME = 'consider_home' +DEFAULT_CONSIDER_HOME = timedelta(seconds=180) + +CONF_NEW_DEVICE_DEFAULTS = 'new_device_defaults' + +ATTR_ATTRIBUTES = 'attributes' +ATTR_BATTERY = 'battery' +ATTR_DEV_ID = 'dev_id' +ATTR_GPS = 'gps' +ATTR_HOST_NAME = 'host_name' +ATTR_LOCATION_NAME = 'location_name' +ATTR_MAC = 'mac' +ATTR_SOURCE_TYPE = 'source_type' +ATTR_CONSIDER_HOME = 'consider_home' diff --git a/homeassistant/components/device_tracker/legacy.py b/homeassistant/components/device_tracker/legacy.py new file mode 100644 index 00000000000..73846480655 --- /dev/null +++ b/homeassistant/components/device_tracker/legacy.py @@ -0,0 +1,528 @@ +"""Legacy device tracker classes.""" +import asyncio +from datetime import timedelta +from typing import Any, List, Sequence + +import voluptuous as vol + +from homeassistant.core import callback +from homeassistant.components import zone +from homeassistant.components.group import ( + ATTR_ADD_ENTITIES, ATTR_ENTITIES, ATTR_OBJECT_ID, ATTR_VISIBLE, + DOMAIN as DOMAIN_GROUP, SERVICE_SET) +from homeassistant.components.zone.zone import async_active_zone +from homeassistant.config import load_yaml_config_file, async_log_exception +from homeassistant.exceptions import HomeAssistantError +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.helpers.typing import GPSType, HomeAssistantType +from homeassistant import util +import homeassistant.util.dt as dt_util +from homeassistant.util.yaml import dump + +from homeassistant.const import ( + ATTR_ENTITY_ID, ATTR_GPS_ACCURACY, ATTR_ICON, ATTR_LATITUDE, + ATTR_LONGITUDE, ATTR_NAME, CONF_ICON, CONF_MAC, CONF_NAME, + DEVICE_DEFAULT_NAME, STATE_NOT_HOME, STATE_HOME) + +from .const import ( + ATTR_BATTERY, + ATTR_HOST_NAME, + ATTR_MAC, + ATTR_SOURCE_TYPE, + CONF_AWAY_HIDE, + CONF_CONSIDER_HOME, + CONF_NEW_DEVICE_DEFAULTS, + CONF_TRACK_NEW, + DEFAULT_AWAY_HIDE, + DEFAULT_CONSIDER_HOME, + DEFAULT_TRACK_NEW, + DOMAIN, + ENTITY_ID_FORMAT, + LOGGER, + SOURCE_TYPE_GPS, +) + +YAML_DEVICES = 'known_devices.yaml' +GROUP_NAME_ALL_DEVICES = 'all devices' +EVENT_NEW_DEVICE = 'device_tracker_new_device' + + +async def get_tracker(hass, config): + """Create a tracker.""" + yaml_path = hass.config.path(YAML_DEVICES) + + conf = config.get(DOMAIN, []) + conf = conf[0] if conf else {} + consider_home = conf.get(CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME) + + defaults = conf.get(CONF_NEW_DEVICE_DEFAULTS, {}) + track_new = conf.get(CONF_TRACK_NEW) + if track_new is None: + track_new = defaults.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW) + + devices = await async_load_config(yaml_path, hass, consider_home) + tracker = DeviceTracker( + hass, consider_home, track_new, defaults, devices) + return tracker + + +class DeviceTracker: + """Representation of a device tracker.""" + + def __init__(self, hass: HomeAssistantType, consider_home: timedelta, + track_new: bool, defaults: dict, + devices: Sequence) -> None: + """Initialize a device tracker.""" + self.hass = hass + self.devices = {dev.dev_id: dev for dev in devices} + self.mac_to_dev = {dev.mac: dev for dev in devices if dev.mac} + self.consider_home = consider_home + self.track_new = track_new if track_new is not None \ + else defaults.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW) + self.defaults = defaults + self.group = None + self._is_updating = asyncio.Lock(loop=hass.loop) + + for dev in devices: + if self.devices[dev.dev_id] is not dev: + LOGGER.warning('Duplicate device IDs detected %s', dev.dev_id) + if dev.mac and self.mac_to_dev[dev.mac] is not dev: + LOGGER.warning('Duplicate device MAC addresses detected %s', + dev.mac) + + def see(self, mac: str = None, dev_id: str = None, host_name: str = None, + location_name: str = None, gps: GPSType = None, + gps_accuracy: int = None, battery: int = None, + attributes: dict = None, source_type: str = SOURCE_TYPE_GPS, + picture: str = None, icon: str = None, + consider_home: timedelta = None): + """Notify the device tracker that you see a device.""" + self.hass.add_job( + self.async_see(mac, dev_id, host_name, location_name, gps, + gps_accuracy, battery, attributes, source_type, + picture, icon, consider_home) + ) + + async def async_see( + self, mac: str = None, dev_id: str = None, host_name: str = None, + location_name: str = None, gps: GPSType = None, + gps_accuracy: int = None, battery: int = None, + attributes: dict = None, source_type: str = SOURCE_TYPE_GPS, + picture: str = None, icon: str = None, + consider_home: timedelta = None): + """Notify the device tracker that you see a device. + + This method is a coroutine. + """ + if mac is None and dev_id is None: + raise HomeAssistantError('Neither mac or device id passed in') + if mac is not None: + mac = str(mac).upper() + device = self.mac_to_dev.get(mac) + if not device: + dev_id = util.slugify(host_name or '') or util.slugify(mac) + else: + dev_id = cv.slug(str(dev_id).lower()) + device = self.devices.get(dev_id) + + if device: + await device.async_seen( + host_name, location_name, gps, gps_accuracy, battery, + attributes, source_type, consider_home) + if device.track: + await device.async_update_ha_state() + return + + # If no device can be found, create it + dev_id = util.ensure_unique_string(dev_id, self.devices.keys()) + device = Device( + self.hass, consider_home or self.consider_home, self.track_new, + dev_id, mac, (host_name or dev_id).replace('_', ' '), + picture=picture, icon=icon, + hide_if_away=self.defaults.get(CONF_AWAY_HIDE, DEFAULT_AWAY_HIDE)) + self.devices[dev_id] = device + if mac is not None: + self.mac_to_dev[mac] = device + + await device.async_seen( + host_name, location_name, gps, gps_accuracy, battery, attributes, + source_type) + + if device.track: + await device.async_update_ha_state() + + # During init, we ignore the group + if self.group and self.track_new: + self.hass.async_create_task( + self.hass.async_call( + DOMAIN_GROUP, SERVICE_SET, { + ATTR_OBJECT_ID: util.slugify(GROUP_NAME_ALL_DEVICES), + ATTR_VISIBLE: False, + ATTR_NAME: GROUP_NAME_ALL_DEVICES, + ATTR_ADD_ENTITIES: [device.entity_id]})) + + self.hass.bus.async_fire(EVENT_NEW_DEVICE, { + ATTR_ENTITY_ID: device.entity_id, + ATTR_HOST_NAME: device.host_name, + ATTR_MAC: device.mac, + }) + + # update known_devices.yaml + self.hass.async_create_task( + self.async_update_config( + self.hass.config.path(YAML_DEVICES), dev_id, device) + ) + + async def async_update_config(self, path, dev_id, device): + """Add device to YAML configuration file. + + This method is a coroutine. + """ + async with self._is_updating: + await self.hass.async_add_executor_job( + update_config, self.hass.config.path(YAML_DEVICES), + dev_id, device) + + @callback + def async_setup_group(self): + """Initialize group for all tracked devices. + + This method must be run in the event loop. + """ + entity_ids = [dev.entity_id for dev in self.devices.values() + if dev.track] + + self.hass.async_create_task( + self.hass.services.async_call( + DOMAIN_GROUP, SERVICE_SET, { + ATTR_OBJECT_ID: util.slugify(GROUP_NAME_ALL_DEVICES), + ATTR_VISIBLE: False, + ATTR_NAME: GROUP_NAME_ALL_DEVICES, + ATTR_ENTITIES: entity_ids})) + + @callback + def async_update_stale(self, now: dt_util.dt.datetime): + """Update stale devices. + + This method must be run in the event loop. + """ + for device in self.devices.values(): + if (device.track and device.last_update_home) and \ + device.stale(now): + self.hass.async_create_task(device.async_update_ha_state(True)) + + async def async_setup_tracked_device(self): + """Set up all not exists tracked devices. + + This method is a coroutine. + """ + async def async_init_single_device(dev): + """Init a single device_tracker entity.""" + await dev.async_added_to_hass() + await dev.async_update_ha_state() + + tasks = [] + for device in self.devices.values(): + if device.track and not device.last_seen: + tasks.append(self.hass.async_create_task( + async_init_single_device(device))) + + if tasks: + await asyncio.wait(tasks, loop=self.hass.loop) + + +class Device(RestoreEntity): + """Represent a tracked device.""" + + host_name = None # type: str + location_name = None # type: str + gps = None # type: GPSType + gps_accuracy = 0 # type: int + last_seen = None # type: dt_util.dt.datetime + consider_home = None # type: dt_util.dt.timedelta + battery = None # type: int + attributes = None # type: dict + icon = None # type: str + + # Track if the last update of this device was HOME. + last_update_home = False + _state = STATE_NOT_HOME + + def __init__(self, hass: HomeAssistantType, consider_home: timedelta, + track: bool, dev_id: str, mac: str, name: str = None, + picture: str = None, gravatar: str = None, icon: str = None, + hide_if_away: bool = False) -> None: + """Initialize a device.""" + self.hass = hass + self.entity_id = ENTITY_ID_FORMAT.format(dev_id) + + # Timedelta object how long we consider a device home if it is not + # detected anymore. + self.consider_home = consider_home + + # Device ID + self.dev_id = dev_id + self.mac = mac + + # If we should track this device + self.track = track + + # Configured name + self.config_name = name + + # Configured picture + if gravatar is not None: + self.config_picture = get_gravatar_for_email(gravatar) + else: + self.config_picture = picture + + self.icon = icon + + self.away_hide = hide_if_away + + self.source_type = None + + self._attributes = {} + + @property + def name(self): + """Return the name of the entity.""" + return self.config_name or self.host_name or DEVICE_DEFAULT_NAME + + @property + def state(self): + """Return the state of the device.""" + return self._state + + @property + def entity_picture(self): + """Return the picture of the device.""" + return self.config_picture + + @property + def state_attributes(self): + """Return the device state attributes.""" + attr = { + ATTR_SOURCE_TYPE: self.source_type + } + + if self.gps: + attr[ATTR_LATITUDE] = self.gps[0] + attr[ATTR_LONGITUDE] = self.gps[1] + attr[ATTR_GPS_ACCURACY] = self.gps_accuracy + + if self.battery: + attr[ATTR_BATTERY] = self.battery + + return attr + + @property + def device_state_attributes(self): + """Return device state attributes.""" + return self._attributes + + @property + def hidden(self): + """If device should be hidden.""" + return self.away_hide and self.state != STATE_HOME + + async def async_seen( + self, host_name: str = None, location_name: str = None, + gps: GPSType = None, gps_accuracy=0, battery: int = None, + attributes: dict = None, + source_type: str = SOURCE_TYPE_GPS, + consider_home: timedelta = None): + """Mark the device as seen.""" + self.source_type = source_type + self.last_seen = dt_util.utcnow() + self.host_name = host_name + self.location_name = location_name + self.consider_home = consider_home or self.consider_home + + if battery: + self.battery = battery + if attributes: + self._attributes.update(attributes) + + self.gps = None + + if gps is not None: + try: + self.gps = float(gps[0]), float(gps[1]) + self.gps_accuracy = gps_accuracy or 0 + except (ValueError, TypeError, IndexError): + self.gps = None + self.gps_accuracy = 0 + LOGGER.warning( + "Could not parse gps value for %s: %s", self.dev_id, gps) + + # pylint: disable=not-an-iterable + await self.async_update() + + def stale(self, now: dt_util.dt.datetime = None): + """Return if device state is stale. + + Async friendly. + """ + return self.last_seen is None or \ + (now or dt_util.utcnow()) - self.last_seen > self.consider_home + + def mark_stale(self): + """Mark the device state as stale.""" + self._state = STATE_NOT_HOME + self.gps = None + self.last_update_home = False + + async def async_update(self): + """Update state of entity. + + This method is a coroutine. + """ + if not self.last_seen: + return + if self.location_name: + self._state = self.location_name + elif self.gps is not None and self.source_type == SOURCE_TYPE_GPS: + zone_state = async_active_zone( + self.hass, self.gps[0], self.gps[1], self.gps_accuracy) + if zone_state is None: + self._state = STATE_NOT_HOME + elif zone_state.entity_id == zone.ENTITY_ID_HOME: + self._state = STATE_HOME + else: + self._state = zone_state.name + elif self.stale(): + self.mark_stale() + else: + self._state = STATE_HOME + self.last_update_home = True + + async def async_added_to_hass(self): + """Add an entity.""" + await super().async_added_to_hass() + state = await self.async_get_last_state() + if not state: + return + self._state = state.state + self.last_update_home = (state.state == STATE_HOME) + self.last_seen = dt_util.utcnow() + + for attr, var in ( + (ATTR_SOURCE_TYPE, 'source_type'), + (ATTR_GPS_ACCURACY, 'gps_accuracy'), + (ATTR_BATTERY, 'battery'), + ): + if attr in state.attributes: + setattr(self, var, state.attributes[attr]) + + if ATTR_LONGITUDE in state.attributes: + self.gps = (state.attributes[ATTR_LATITUDE], + state.attributes[ATTR_LONGITUDE]) + + +class DeviceScanner: + """Device scanner object.""" + + hass = None # type: HomeAssistantType + + def scan_devices(self) -> List[str]: + """Scan for devices.""" + raise NotImplementedError() + + def async_scan_devices(self) -> Any: + """Scan for devices. + + This method must be run in the event loop and returns a coroutine. + """ + return self.hass.async_add_job(self.scan_devices) + + def get_device_name(self, device: str) -> str: + """Get the name of a device.""" + raise NotImplementedError() + + def async_get_device_name(self, device: str) -> Any: + """Get the name of a device. + + This method must be run in the event loop and returns a coroutine. + """ + return self.hass.async_add_job(self.get_device_name, device) + + def get_extra_attributes(self, device: str) -> dict: + """Get the extra attributes of a device.""" + raise NotImplementedError() + + def async_get_extra_attributes(self, device: str) -> Any: + """Get the extra attributes of a device. + + This method must be run in the event loop and returns a coroutine. + """ + return self.hass.async_add_job(self.get_extra_attributes, device) + + +async def async_load_config(path: str, hass: HomeAssistantType, + consider_home: timedelta): + """Load devices from YAML configuration file. + + This method is a coroutine. + """ + dev_schema = vol.Schema({ + vol.Required(CONF_NAME): cv.string, + vol.Optional(CONF_ICON, default=None): vol.Any(None, cv.icon), + vol.Optional('track', default=False): cv.boolean, + vol.Optional(CONF_MAC, default=None): + vol.Any(None, vol.All(cv.string, vol.Upper)), + vol.Optional(CONF_AWAY_HIDE, default=DEFAULT_AWAY_HIDE): cv.boolean, + vol.Optional('gravatar', default=None): vol.Any(None, cv.string), + vol.Optional('picture', default=None): vol.Any(None, cv.string), + vol.Optional(CONF_CONSIDER_HOME, default=consider_home): vol.All( + cv.time_period, cv.positive_timedelta), + }) + try: + result = [] + try: + devices = await hass.async_add_job( + load_yaml_config_file, path) + except HomeAssistantError as err: + LOGGER.error("Unable to load %s: %s", path, str(err)) + return [] + + for dev_id, device in devices.items(): + # Deprecated option. We just ignore it to avoid breaking change + device.pop('vendor', None) + try: + device = dev_schema(device) + device['dev_id'] = cv.slugify(dev_id) + except vol.Invalid as exp: + async_log_exception(exp, dev_id, devices, hass) + else: + result.append(Device(hass, **device)) + return result + except (HomeAssistantError, FileNotFoundError): + # When YAML file could not be loaded/did not contain a dict + return [] + + +def update_config(path: str, dev_id: str, device: Device): + """Add device to YAML configuration file.""" + with open(path, 'a') as out: + device = {device.dev_id: { + ATTR_NAME: device.name, + ATTR_MAC: device.mac, + ATTR_ICON: device.icon, + 'picture': device.config_picture, + 'track': device.track, + CONF_AWAY_HIDE: device.away_hide, + }} + out.write('\n') + out.write(dump(device)) + + +def get_gravatar_for_email(email: str): + """Return an 80px Gravatar for the given email address. + + Async friendly. + """ + import hashlib + url = 'https://www.gravatar.com/avatar/{}.jpg?s=80&d=wavatar' + return url.format(hashlib.md5(email.encode('utf-8').lower()).hexdigest()) diff --git a/homeassistant/components/device_tracker/setup.py b/homeassistant/components/device_tracker/setup.py new file mode 100644 index 00000000000..e336821c758 --- /dev/null +++ b/homeassistant/components/device_tracker/setup.py @@ -0,0 +1,199 @@ +"""Device tracker helpers.""" +import asyncio +from typing import Dict, Any, Callable, Optional +from types import ModuleType + +import attr + +from homeassistant.core import callback +from homeassistant.setup import async_prepare_setup_platform +from homeassistant.helpers import config_per_platform +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.event import async_track_time_interval +from homeassistant.util import dt as dt_util +from homeassistant.const import ( + ATTR_LATITUDE, + ATTR_LONGITUDE, +) + + +from .const import ( + DOMAIN, + PLATFORM_TYPE_ENTITY, + PLATFORM_TYPE_LEGACY, + CONF_SCAN_INTERVAL, + SCAN_INTERVAL, + SOURCE_TYPE_ROUTER, + LOGGER, +) + + +@attr.s +class DeviceTrackerPlatform: + """Class to hold platform information.""" + + LEGACY_SETUP = ( + 'async_get_scanner', + 'get_scanner', + 'async_setup_scanner', + 'setup_scanner', + # Small steps, initially just legacy setup supported. + 'async_setup_entry' + ) + # ENTITY_PLATFORM_SETUP = ( + # 'setup_platform', + # 'async_setup_platform', + # 'async_setup_entry' + # ) + + name = attr.ib(type=str) + platform = attr.ib(type=ModuleType) + config = attr.ib(type=Dict) + + @property + def type(self): + """Return platform type.""" + for methods, platform_type in ( + (self.LEGACY_SETUP, PLATFORM_TYPE_LEGACY), + # (self.ENTITY_PLATFORM_SETUP, PLATFORM_TYPE_ENTITY), + ): + for meth in methods: + if hasattr(self.platform, meth): + return platform_type + + return None + + async def async_setup_legacy(self, hass, tracker, discovery_info=None): + """Set up a legacy platform.""" + LOGGER.info("Setting up %s.%s", DOMAIN, self.type) + try: + scanner = None + setup = None + if hasattr(self.platform, 'async_get_scanner'): + scanner = await self.platform.async_get_scanner( + hass, {DOMAIN: self.config}) + elif hasattr(self.platform, 'get_scanner'): + scanner = await hass.async_add_job( + self.platform.get_scanner, hass, {DOMAIN: self.config}) + elif hasattr(self.platform, 'async_setup_scanner'): + setup = await self.platform.async_setup_scanner( + hass, self.config, tracker.async_see, discovery_info) + elif hasattr(self.platform, 'setup_scanner'): + setup = await hass.async_add_job( + self.platform.setup_scanner, hass, self.config, + tracker.see, discovery_info) + elif hasattr(self.platform, 'async_setup_entry'): + setup = await self.platform.async_setup_entry( + hass, self.config, tracker.async_see) + else: + raise HomeAssistantError( + "Invalid legacy device_tracker platform.") + + if scanner: + async_setup_scanner_platform( + hass, self.config, scanner, tracker.async_see, self.type) + return + + if not setup: + LOGGER.error("Error setting up platform %s", self.type) + return + + except Exception: # pylint: disable=broad-except + LOGGER.exception("Error setting up platform %s", self.type) + + +async def async_extract_config(hass, config): + """Extract device tracker config and split between legacy and modern.""" + legacy = [] + entity_platform = [] + + for platform in await asyncio.gather(*[ + async_create_platform_type(hass, config, p_type, p_config) + for p_type, p_config in config_per_platform(config, DOMAIN) + ]): + if platform is None: + continue + + if platform.type == PLATFORM_TYPE_ENTITY: + entity_platform.append(platform) + elif platform.type == PLATFORM_TYPE_LEGACY: + legacy.append(platform) + else: + raise ValueError("Unable to determine type for {}: {}".format( + platform.name, platform.type)) + + return (legacy, entity_platform) + + +async def async_create_platform_type(hass, config, p_type, p_config) \ + -> Optional[DeviceTrackerPlatform]: + """Determine type of platform.""" + platform = await async_prepare_setup_platform( + hass, config, DOMAIN, p_type) + + if platform is None: + return None + + return DeviceTrackerPlatform(p_type, platform, p_config) + + +@callback +def async_setup_scanner_platform(hass: HomeAssistantType, config: ConfigType, + scanner: Any, async_see_device: Callable, + platform: str): + """Set up the connect scanner-based platform to device tracker. + + This method must be run in the event loop. + """ + interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) + update_lock = asyncio.Lock(loop=hass.loop) + scanner.hass = hass + + # Initial scan of each mac we also tell about host name for config + seen = set() # type: Any + + async def async_device_tracker_scan(now: dt_util.dt.datetime): + """Handle interval matches.""" + if update_lock.locked(): + LOGGER.warning( + "Updating device list from %s took longer than the scheduled " + "scan interval %s", platform, interval) + return + + async with update_lock: + found_devices = await scanner.async_scan_devices() + + for mac in found_devices: + if mac in seen: + host_name = None + else: + host_name = await scanner.async_get_device_name(mac) + seen.add(mac) + + try: + extra_attributes = \ + await scanner.async_get_extra_attributes(mac) + except NotImplementedError: + extra_attributes = dict() + + kwargs = { + 'mac': mac, + 'host_name': host_name, + 'source_type': SOURCE_TYPE_ROUTER, + 'attributes': { + 'scanner': scanner.__class__.__name__, + **extra_attributes + } + } + + zone_home = hass.states.get(hass.components.zone.ENTITY_ID_HOME) + if zone_home: + kwargs['gps'] = [zone_home.attributes[ATTR_LATITUDE], + zone_home.attributes[ATTR_LONGITUDE]] + kwargs['gps_accuracy'] = 0 + + hass.async_create_task(async_see_device(**kwargs)) + + async_track_time_interval(hass, async_device_tracker_scan, interval) + hass.async_create_task(async_device_tracker_scan(None)) diff --git a/homeassistant/components/icloud/device_tracker.py b/homeassistant/components/icloud/device_tracker.py index 908fe5ecf90..573da5fce63 100644 --- a/homeassistant/components/icloud/device_tracker.py +++ b/homeassistant/components/icloud/device_tracker.py @@ -6,8 +6,10 @@ import os import voluptuous as vol from homeassistant.const import CONF_USERNAME, CONF_PASSWORD -from homeassistant.components.device_tracker import ( - PLATFORM_SCHEMA, DOMAIN, ATTR_ATTRIBUTES, ENTITY_ID_FORMAT, DeviceScanner) +from homeassistant.components.device_tracker import PLATFORM_SCHEMA +from homeassistant.components.device_tracker.const import ( + DOMAIN, ATTR_ATTRIBUTES, ENTITY_ID_FORMAT) +from homeassistant.components.device_tracker.legacy import DeviceScanner from homeassistant.components.zone.zone import active_zone from homeassistant.helpers.event import track_utc_time_change import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/ping/device_tracker.py b/homeassistant/components/ping/device_tracker.py index 9f9bf4475b4..6cbb2147aa9 100644 --- a/homeassistant/components/ping/device_tracker.py +++ b/homeassistant/components/ping/device_tracker.py @@ -8,8 +8,9 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( - PLATFORM_SCHEMA, CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL, - SOURCE_TYPE_ROUTER) + PLATFORM_SCHEMA) +from homeassistant.components.device_tracker.const import ( + CONF_SCAN_INTERVAL, SCAN_INTERVAL, SOURCE_TYPE_ROUTER) from homeassistant import util from homeassistant import const @@ -68,7 +69,7 @@ def setup_scanner(hass, config, see, discovery_info=None): interval = config.get(CONF_SCAN_INTERVAL, timedelta(seconds=len(hosts) * config[CONF_PING_COUNT]) - + DEFAULT_SCAN_INTERVAL) + + SCAN_INTERVAL) _LOGGER.debug("Started ping tracker with interval=%s on hosts: %s", interval, ",".join([host.ip_address for host in hosts])) diff --git a/homeassistant/config.py b/homeassistant/config.py index 95be31d5bdb..9e3f1d80663 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -827,14 +827,22 @@ async def async_process_component_config( # Create a copy of the configuration with all config for current # component removed and add validated config back in. - filter_keys = extract_domain_configs(config, domain) - config = {key: value for key, value in config.items() - if key not in filter_keys} + config = config_without_domain(config, domain) config[domain] = platforms return config +@callback +def config_without_domain(config: Dict, domain: str) -> Dict: + """Return a config with all configuration for a domain removed.""" + filter_keys = extract_domain_configs(config, domain) + return { + key: value for key, value in config.items() + if key not in filter_keys + } + + async def async_check_ha_config_file(hass: HomeAssistant) -> Optional[str]: """Check if Home Assistant configuration file is valid. diff --git a/tests/components/demo/test_init.py b/tests/components/demo/test_init.py index fde2caecff4..cacc29cc5d5 100644 --- a/tests/components/demo/test_init.py +++ b/tests/components/demo/test_init.py @@ -5,7 +5,8 @@ import os import pytest from homeassistant.setup import async_setup_component -from homeassistant.components import demo, device_tracker +from homeassistant.components import demo +from homeassistant.components.device_tracker.legacy import YAML_DEVICES from homeassistant.helpers.json import JSONEncoder @@ -20,7 +21,7 @@ def demo_cleanup(hass): """Clean up device tracker demo file.""" yield try: - os.remove(hass.config.path(device_tracker.YAML_DEVICES)) + os.remove(hass.config.path(YAML_DEVICES)) except FileNotFoundError: pass diff --git a/tests/components/device_sun_light_trigger/test_init.py b/tests/components/device_sun_light_trigger/test_init.py index d4356ace48c..547ef74a0fd 100644 --- a/tests/components/device_sun_light_trigger/test_init.py +++ b/tests/components/device_sun_light_trigger/test_init.py @@ -8,6 +8,8 @@ from homeassistant.setup import async_setup_component from homeassistant.const import CONF_PLATFORM, STATE_HOME, STATE_NOT_HOME from homeassistant.components import ( device_tracker, light, device_sun_light_trigger) +from homeassistant.components.device_tracker.const import ( + ENTITY_ID_FORMAT as DT_ENTITY_ID_FORMAT) from homeassistant.util import dt as dt_util from tests.common import async_fire_time_changed @@ -26,7 +28,7 @@ def scanner(hass): getattr(hass.components, 'test.light').init() with patch( - 'homeassistant.components.device_tracker.load_yaml_config_file', + 'homeassistant.components.device_tracker.legacy.load_yaml_config_file', return_value={ 'device_1': { 'hide_if_away': False, @@ -102,7 +104,7 @@ async def test_lights_turn_on_when_coming_home_after_sun_set(hass, scanner): device_sun_light_trigger.DOMAIN: {}}) hass.states.async_set( - device_tracker.ENTITY_ID_FORMAT.format('device_2'), STATE_HOME) + DT_ENTITY_ID_FORMAT.format('device_2'), STATE_HOME) await hass.async_block_till_done() assert light.is_on(hass) diff --git a/tests/components/device_tracker/test_init.py b/tests/components/device_tracker/test_init.py index e2648c1c650..9a59855e8c1 100644 --- a/tests/components/device_tracker/test_init.py +++ b/tests/components/device_tracker/test_init.py @@ -10,9 +10,11 @@ import pytest from homeassistant.components import zone import homeassistant.components.device_tracker as device_tracker +from homeassistant.components.device_tracker import const, legacy from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_ENTITY_PICTURE, ATTR_FRIENDLY_NAME, ATTR_HIDDEN, - ATTR_ICON, CONF_PLATFORM, STATE_HOME, STATE_NOT_HOME) + ATTR_ICON, CONF_PLATFORM, STATE_HOME, STATE_NOT_HOME, + ATTR_LATITUDE, ATTR_LONGITUDE, ATTR_GPS_ACCURACY) from homeassistant.core import State, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import discovery @@ -33,7 +35,7 @@ _LOGGER = logging.getLogger(__name__) @pytest.fixture(name='yaml_devices') def mock_yaml_devices(hass): """Get a path for storing yaml devices.""" - yaml_devices = hass.config.path(device_tracker.YAML_DEVICES) + yaml_devices = hass.config.path(legacy.YAML_DEVICES) if os.path.isfile(yaml_devices): os.remove(yaml_devices) yield yaml_devices @@ -43,7 +45,7 @@ def mock_yaml_devices(hass): async def test_is_on(hass): """Test is_on method.""" - entity_id = device_tracker.ENTITY_ID_FORMAT.format('test') + entity_id = const.ENTITY_ID_FORMAT.format('test') hass.states.async_set(entity_id, STATE_HOME) @@ -65,21 +67,21 @@ async def test_reading_broken_yaml_config(hass): 'bad_device:\n nme: Device')} args = {'hass': hass, 'consider_home': timedelta(seconds=60)} with patch_yaml_files(files): - assert await device_tracker.async_load_config( + assert await legacy.async_load_config( 'empty.yaml', **args) == [] - assert await device_tracker.async_load_config( + assert await legacy.async_load_config( 'nodict.yaml', **args) == [] - assert await device_tracker.async_load_config( + assert await legacy.async_load_config( 'noname.yaml', **args) == [] - assert await device_tracker.async_load_config( + assert await legacy.async_load_config( 'badkey.yaml', **args) == [] - res = await device_tracker.async_load_config('allok.yaml', **args) + res = await legacy.async_load_config('allok.yaml', **args) assert len(res) == 1 assert res[0].name == 'Device' assert res[0].dev_id == 'my_device' - res = await device_tracker.async_load_config('oneok.yaml', **args) + res = await legacy.async_load_config('oneok.yaml', **args) assert len(res) == 1 assert res[0].name == 'Device' assert res[0].dev_id == 'my_device' @@ -88,17 +90,16 @@ async def test_reading_broken_yaml_config(hass): async def test_reading_yaml_config(hass, yaml_devices): """Test the rendering of the YAML configuration.""" dev_id = 'test' - device = device_tracker.Device( + device = legacy.Device( hass, timedelta(seconds=180), True, dev_id, 'AB:CD:EF:GH:IJ', 'Test name', picture='http://test.picture', hide_if_away=True, icon='mdi:kettle') await hass.async_add_executor_job( - device_tracker.update_config, yaml_devices, dev_id, device) - with assert_setup_component(1, device_tracker.DOMAIN): - assert await async_setup_component(hass, device_tracker.DOMAIN, - TEST_PLATFORM) - config = (await device_tracker.async_load_config(yaml_devices, hass, - device.consider_home))[0] + legacy.update_config, yaml_devices, dev_id, device) + assert await async_setup_component(hass, device_tracker.DOMAIN, + TEST_PLATFORM) + config = (await legacy.async_load_config(yaml_devices, hass, + device.consider_home))[0] assert device.dev_id == config.dev_id assert device.track == config.track assert device.mac == config.mac @@ -108,15 +109,15 @@ async def test_reading_yaml_config(hass, yaml_devices): assert device.icon == config.icon -@patch('homeassistant.components.device_tracker._LOGGER.warning') +@patch('homeassistant.components.device_tracker.const.LOGGER.warning') async def test_duplicate_mac_dev_id(mock_warning, hass): """Test adding duplicate MACs or device IDs to DeviceTracker.""" devices = [ - device_tracker.Device(hass, True, True, 'my_device', 'AB:01', - 'My device', None, None, False), - device_tracker.Device(hass, True, True, 'your_device', - 'AB:01', 'Your device', None, None, False)] - device_tracker.DeviceTracker(hass, False, True, {}, devices) + legacy.Device(hass, True, True, 'my_device', 'AB:01', + 'My device', None, None, False), + legacy.Device(hass, True, True, 'your_device', + 'AB:01', 'Your device', None, None, False)] + legacy.DeviceTracker(hass, False, True, {}, devices) _LOGGER.debug(mock_warning.call_args_list) assert mock_warning.call_count == 1, \ "The only warning call should be duplicates (check DEBUG)" @@ -126,11 +127,11 @@ async def test_duplicate_mac_dev_id(mock_warning, hass): mock_warning.reset_mock() devices = [ - device_tracker.Device(hass, True, True, 'my_device', - 'AB:01', 'My device', None, None, False), - device_tracker.Device(hass, True, True, 'my_device', - None, 'Your device', None, None, False)] - device_tracker.DeviceTracker(hass, False, True, {}, devices) + legacy.Device(hass, True, True, 'my_device', + 'AB:01', 'My device', None, None, False), + legacy.Device(hass, True, True, 'my_device', + None, 'Your device', None, None, False)] + legacy.DeviceTracker(hass, False, True, {}, devices) _LOGGER.debug(mock_warning.call_args_list) assert mock_warning.call_count == 1, \ @@ -150,7 +151,7 @@ async def test_setup_without_yaml_file(hass): async def test_gravatar(hass): """Test the Gravatar generation.""" dev_id = 'test' - device = device_tracker.Device( + device = legacy.Device( hass, timedelta(seconds=180), True, dev_id, 'AB:CD:EF:GH:IJ', 'Test name', gravatar='test@example.com') gravatar_url = ("https://www.gravatar.com/avatar/" @@ -161,7 +162,7 @@ async def test_gravatar(hass): async def test_gravatar_and_picture(hass): """Test that Gravatar overrides picture.""" dev_id = 'test' - device = device_tracker.Device( + device = legacy.Device( hass, timedelta(seconds=180), True, dev_id, 'AB:CD:EF:GH:IJ', 'Test name', picture='http://test.picture', gravatar='test@example.com') @@ -171,7 +172,7 @@ async def test_gravatar_and_picture(hass): @patch( - 'homeassistant.components.device_tracker.DeviceTracker.see') + 'homeassistant.components.device_tracker.legacy.DeviceTracker.see') @patch( 'homeassistant.components.demo.device_tracker.setup_scanner', autospec=True) @@ -196,7 +197,7 @@ async def test_update_stale(hass, mock_device_tracker_conf): register_time = datetime(2015, 9, 15, 23, tzinfo=dt_util.UTC) scan_time = datetime(2015, 9, 15, 23, 1, tzinfo=dt_util.UTC) - with patch('homeassistant.components.device_tracker.dt_util.utcnow', + with patch('homeassistant.components.device_tracker.legacy.dt_util.utcnow', return_value=register_time): with assert_setup_component(1, device_tracker.DOMAIN): assert await async_setup_component(hass, device_tracker.DOMAIN, { @@ -211,7 +212,7 @@ async def test_update_stale(hass, mock_device_tracker_conf): scanner.leave_home('DEV1') - with patch('homeassistant.components.device_tracker.dt_util.utcnow', + with patch('homeassistant.components.device_tracker.legacy.dt_util.utcnow', return_value=scan_time): async_fire_time_changed(hass, scan_time) await hass.async_block_till_done() @@ -224,12 +225,12 @@ async def test_entity_attributes(hass, mock_device_tracker_conf): """Test the entity attributes.""" devices = mock_device_tracker_conf dev_id = 'test_entity' - entity_id = device_tracker.ENTITY_ID_FORMAT.format(dev_id) + entity_id = const.ENTITY_ID_FORMAT.format(dev_id) friendly_name = 'Paulus' picture = 'http://placehold.it/200x200' icon = 'mdi:kettle' - device = device_tracker.Device( + device = legacy.Device( hass, timedelta(seconds=180), True, dev_id, None, friendly_name, picture, hide_if_away=True, icon=icon) devices.append(device) @@ -249,8 +250,8 @@ async def test_device_hidden(hass, mock_device_tracker_conf): """Test hidden devices.""" devices = mock_device_tracker_conf dev_id = 'test_entity' - entity_id = device_tracker.ENTITY_ID_FORMAT.format(dev_id) - device = device_tracker.Device( + entity_id = const.ENTITY_ID_FORMAT.format(dev_id) + device = legacy.Device( hass, timedelta(seconds=180), True, dev_id, None, hide_if_away=True) devices.append(device) @@ -269,8 +270,8 @@ async def test_group_all_devices(hass, mock_device_tracker_conf): """Test grouping of devices.""" devices = mock_device_tracker_conf dev_id = 'test_entity' - entity_id = device_tracker.ENTITY_ID_FORMAT.format(dev_id) - device = device_tracker.Device( + entity_id = const.ENTITY_ID_FORMAT.format(dev_id) + device = legacy.Device( hass, timedelta(seconds=180), True, dev_id, None, hide_if_away=True) devices.append(device) @@ -288,7 +289,8 @@ async def test_group_all_devices(hass, mock_device_tracker_conf): assert (entity_id,) == state.attributes.get(ATTR_ENTITY_ID) -@patch('homeassistant.components.device_tracker.DeviceTracker.async_see') +@patch('homeassistant.components.device_tracker.legacy.' + 'DeviceTracker.async_see') async def test_see_service(mock_see, hass): """Test the see service with a unicode dev_id and NO MAC.""" with assert_setup_component(1, device_tracker.DOMAIN): @@ -401,8 +403,8 @@ async def test_see_state(hass, yaml_devices): common.async_see(hass, **params) await hass.async_block_till_done() - config = await device_tracker.async_load_config(yaml_devices, hass, - timedelta(seconds=0)) + config = await legacy.async_load_config( + yaml_devices, hass, timedelta(seconds=0)) assert len(config) == 1 state = hass.states.get('device_tracker.example_com') @@ -442,7 +444,7 @@ async def test_see_passive_zone_state(hass, mock_device_tracker_conf): scanner.reset() scanner.come_home('dev1') - with patch('homeassistant.components.device_tracker.dt_util.utcnow', + with patch('homeassistant.components.device_tracker.legacy.dt_util.utcnow', return_value=register_time): with assert_setup_component(1, device_tracker.DOMAIN): assert await async_setup_component(hass, device_tracker.DOMAIN, { @@ -466,7 +468,7 @@ async def test_see_passive_zone_state(hass, mock_device_tracker_conf): scanner.leave_home('dev1') - with patch('homeassistant.components.device_tracker.dt_util.utcnow', + with patch('homeassistant.components.device_tracker.legacy.dt_util.utcnow', return_value=scan_time): async_fire_time_changed(hass, scan_time) await hass.async_block_till_done() @@ -484,11 +486,11 @@ async def test_see_passive_zone_state(hass, mock_device_tracker_conf): device_tracker.SOURCE_TYPE_ROUTER -@patch('homeassistant.components.device_tracker._LOGGER.warning') +@patch('homeassistant.components.device_tracker.const.LOGGER.warning') async def test_see_failures(mock_warning, hass, mock_device_tracker_conf): """Test that the device tracker see failures.""" devices = mock_device_tracker_conf - tracker = device_tracker.DeviceTracker( + tracker = legacy.DeviceTracker( hass, timedelta(seconds=60), 0, {}, []) # MAC is not a string (but added) @@ -512,16 +514,15 @@ async def test_see_failures(mock_warning, hass, mock_device_tracker_conf): async def test_async_added_to_hass(hass): """Test restoring state.""" attr = { - device_tracker.ATTR_LONGITUDE: 18, - device_tracker.ATTR_LATITUDE: -33, - device_tracker.ATTR_LATITUDE: -33, - device_tracker.ATTR_SOURCE_TYPE: 'gps', - device_tracker.ATTR_GPS_ACCURACY: 2, - device_tracker.ATTR_BATTERY: 100 + ATTR_LONGITUDE: 18, + ATTR_LATITUDE: -33, + const.ATTR_SOURCE_TYPE: 'gps', + ATTR_GPS_ACCURACY: 2, + const.ATTR_BATTERY: 100 } mock_restore_cache(hass, [State('device_tracker.jk', 'home', attr)]) - path = hass.config.path(device_tracker.YAML_DEVICES) + path = hass.config.path(legacy.YAML_DEVICES) files = { path: 'jk:\n name: JK Phone\n track: True', @@ -570,7 +571,7 @@ async def test_adding_unknown_device_to_config(mock_device_tracker_conf, hass): async def test_picture_and_icon_on_see_discovery(mock_device_tracker_conf, hass): """Test that picture and icon are set in initial see.""" - tracker = device_tracker.DeviceTracker( + tracker = legacy.DeviceTracker( hass, timedelta(seconds=60), False, {}, []) await tracker.async_see(dev_id=11, picture='pic_url', icon='mdi:icon') await hass.async_block_till_done() @@ -581,7 +582,7 @@ async def test_picture_and_icon_on_see_discovery(mock_device_tracker_conf, async def test_default_hide_if_away_is_used(mock_device_tracker_conf, hass): """Test that default track_new is used.""" - tracker = device_tracker.DeviceTracker( + tracker = legacy.DeviceTracker( hass, timedelta(seconds=60), False, {device_tracker.CONF_AWAY_HIDE: True}, []) await tracker.async_see(dev_id=12) @@ -593,7 +594,7 @@ async def test_default_hide_if_away_is_used(mock_device_tracker_conf, hass): async def test_backward_compatibility_for_track_new(mock_device_tracker_conf, hass): """Test backward compatibility for track new.""" - tracker = device_tracker.DeviceTracker( + tracker = legacy.DeviceTracker( hass, timedelta(seconds=60), False, {device_tracker.CONF_TRACK_NEW: True}, []) await tracker.async_see(dev_id=13) @@ -604,7 +605,7 @@ async def test_backward_compatibility_for_track_new(mock_device_tracker_conf, async def test_old_style_track_new_is_skipped(mock_device_tracker_conf, hass): """Test old style config is skipped.""" - tracker = device_tracker.DeviceTracker( + tracker = legacy.DeviceTracker( hass, timedelta(seconds=60), None, {device_tracker.CONF_TRACK_NEW: False}, []) await tracker.async_see(dev_id=14) diff --git a/tests/components/geofency/test_init.py b/tests/components/geofency/test_init.py index 98edd8b3af1..718eb259db5 100644 --- a/tests/components/geofency/test_init.py +++ b/tests/components/geofency/test_init.py @@ -125,7 +125,7 @@ async def geofency_client(loop, hass, aiohttp_client): }}) await hass.async_block_till_done() - with patch('homeassistant.components.device_tracker.update_config'): + with patch('homeassistant.components.device_tracker.legacy.update_config'): return await aiohttp_client(hass.http.app) diff --git a/tests/components/gpslogger/test_init.py b/tests/components/gpslogger/test_init.py index fce93d0a774..608456d44db 100644 --- a/tests/components/gpslogger/test_init.py +++ b/tests/components/gpslogger/test_init.py @@ -38,7 +38,7 @@ async def gpslogger_client(loop, hass, aiohttp_client): await hass.async_block_till_done() - with patch('homeassistant.components.device_tracker.update_config'): + with patch('homeassistant.components.device_tracker.legacy.update_config'): return await aiohttp_client(hass.http.app) diff --git a/tests/components/locative/test_init.py b/tests/components/locative/test_init.py index 6d541cac653..81248764971 100644 --- a/tests/components/locative/test_init.py +++ b/tests/components/locative/test_init.py @@ -30,7 +30,7 @@ async def locative_client(loop, hass, hass_client): }) await hass.async_block_till_done() - with patch('homeassistant.components.device_tracker.update_config'): + with patch('homeassistant.components.device_tracker.legacy.update_config'): return await hass_client() diff --git a/tests/components/mqtt/test_device_tracker.py b/tests/components/mqtt/test_device_tracker.py index 665be9b3477..3bbd4b013a5 100644 --- a/tests/components/mqtt/test_device_tracker.py +++ b/tests/components/mqtt/test_device_tracker.py @@ -3,6 +3,7 @@ from asynctest import patch import pytest from homeassistant.components import device_tracker +from homeassistant.components.device_tracker.const import ENTITY_ID_FORMAT from homeassistant.const import CONF_PLATFORM from homeassistant.setup import async_setup_component @@ -39,7 +40,7 @@ async def test_ensure_device_tracker_platform_validation(hass): async def test_new_message(hass, mock_device_tracker_conf): """Test new message.""" dev_id = 'paulus' - entity_id = device_tracker.ENTITY_ID_FORMAT.format(dev_id) + entity_id = ENTITY_ID_FORMAT.format(dev_id) topic = '/location/paulus' location = 'work' @@ -58,7 +59,7 @@ async def test_new_message(hass, mock_device_tracker_conf): async def test_single_level_wildcard_topic(hass, mock_device_tracker_conf): """Test single level wildcard topic.""" dev_id = 'paulus' - entity_id = device_tracker.ENTITY_ID_FORMAT.format(dev_id) + entity_id = ENTITY_ID_FORMAT.format(dev_id) subscription = '/location/+/paulus' topic = '/location/room/paulus' location = 'work' @@ -78,7 +79,7 @@ async def test_single_level_wildcard_topic(hass, mock_device_tracker_conf): async def test_multi_level_wildcard_topic(hass, mock_device_tracker_conf): """Test multi level wildcard topic.""" dev_id = 'paulus' - entity_id = device_tracker.ENTITY_ID_FORMAT.format(dev_id) + entity_id = ENTITY_ID_FORMAT.format(dev_id) subscription = '/location/#' topic = '/location/room/paulus' location = 'work' @@ -99,7 +100,7 @@ async def test_single_level_wildcard_topic_not_matching( hass, mock_device_tracker_conf): """Test not matching single level wildcard topic.""" dev_id = 'paulus' - entity_id = device_tracker.ENTITY_ID_FORMAT.format(dev_id) + entity_id = ENTITY_ID_FORMAT.format(dev_id) subscription = '/location/+/paulus' topic = '/location/paulus' location = 'work' @@ -120,7 +121,7 @@ async def test_multi_level_wildcard_topic_not_matching( hass, mock_device_tracker_conf): """Test not matching multi level wildcard topic.""" dev_id = 'paulus' - entity_id = device_tracker.ENTITY_ID_FORMAT.format(dev_id) + entity_id = ENTITY_ID_FORMAT.format(dev_id) subscription = '/location/#' topic = '/somewhere/room/paulus' location = 'work' diff --git a/tests/components/mqtt_json/test_device_tracker.py b/tests/components/mqtt_json/test_device_tracker.py index ea87be42bd6..f6270258429 100644 --- a/tests/components/mqtt_json/test_device_tracker.py +++ b/tests/components/mqtt_json/test_device_tracker.py @@ -1,12 +1,13 @@ """The tests for the JSON MQTT device tracker platform.""" import json -from asynctest import patch import logging import os +from asynctest import patch import pytest from homeassistant.setup import async_setup_component -from homeassistant.components import device_tracker +from homeassistant.components.device_tracker.legacy import ( + YAML_DEVICES, ENTITY_ID_FORMAT, DOMAIN as DT_DOMAIN) from homeassistant.const import CONF_PLATFORM from tests.common import async_mock_mqtt_component, async_fire_mqtt_message @@ -27,7 +28,7 @@ LOCATION_MESSAGE_INCOMPLETE = { def setup_comp(hass): """Initialize components.""" hass.loop.run_until_complete(async_mock_mqtt_component(hass)) - yaml_devices = hass.config.path(device_tracker.YAML_DEVICES) + yaml_devices = hass.config.path(YAML_DEVICES) yield if os.path.isfile(yaml_devices): os.remove(yaml_devices) @@ -45,8 +46,8 @@ async def test_ensure_device_tracker_platform_validation(hass): dev_id = 'paulus' topic = 'location/paulus' - assert await async_setup_component(hass, device_tracker.DOMAIN, { - device_tracker.DOMAIN: { + assert await async_setup_component(hass, DT_DOMAIN, { + DT_DOMAIN: { CONF_PLATFORM: 'mqtt_json', 'devices': {dev_id: topic} } @@ -60,8 +61,8 @@ async def test_json_message(hass): topic = 'location/zanzito' location = json.dumps(LOCATION_MESSAGE) - assert await async_setup_component(hass, device_tracker.DOMAIN, { - device_tracker.DOMAIN: { + assert await async_setup_component(hass, DT_DOMAIN, { + DT_DOMAIN: { CONF_PLATFORM: 'mqtt_json', 'devices': {dev_id: topic} } @@ -79,8 +80,8 @@ async def test_non_json_message(hass, caplog): topic = 'location/zanzito' location = 'home' - assert await async_setup_component(hass, device_tracker.DOMAIN, { - device_tracker.DOMAIN: { + assert await async_setup_component(hass, DT_DOMAIN, { + DT_DOMAIN: { CONF_PLATFORM: 'mqtt_json', 'devices': {dev_id: topic} } @@ -100,8 +101,8 @@ async def test_incomplete_message(hass, caplog): topic = 'location/zanzito' location = json.dumps(LOCATION_MESSAGE_INCOMPLETE) - assert await async_setup_component(hass, device_tracker.DOMAIN, { - device_tracker.DOMAIN: { + assert await async_setup_component(hass, DT_DOMAIN, { + DT_DOMAIN: { CONF_PLATFORM: 'mqtt_json', 'devices': {dev_id: topic} } @@ -123,8 +124,8 @@ async def test_single_level_wildcard_topic(hass): topic = 'location/room/zanzito' location = json.dumps(LOCATION_MESSAGE) - assert await async_setup_component(hass, device_tracker.DOMAIN, { - device_tracker.DOMAIN: { + assert await async_setup_component(hass, DT_DOMAIN, { + DT_DOMAIN: { CONF_PLATFORM: 'mqtt_json', 'devices': {dev_id: subscription} } @@ -143,8 +144,8 @@ async def test_multi_level_wildcard_topic(hass): topic = 'location/zanzito' location = json.dumps(LOCATION_MESSAGE) - assert await async_setup_component(hass, device_tracker.DOMAIN, { - device_tracker.DOMAIN: { + assert await async_setup_component(hass, DT_DOMAIN, { + DT_DOMAIN: { CONF_PLATFORM: 'mqtt_json', 'devices': {dev_id: subscription} } @@ -159,13 +160,13 @@ async def test_multi_level_wildcard_topic(hass): async def test_single_level_wildcard_topic_not_matching(hass): """Test not matching single level wildcard topic.""" dev_id = 'zanzito' - entity_id = device_tracker.ENTITY_ID_FORMAT.format(dev_id) + entity_id = ENTITY_ID_FORMAT.format(dev_id) subscription = 'location/+/zanzito' topic = 'location/zanzito' location = json.dumps(LOCATION_MESSAGE) - assert await async_setup_component(hass, device_tracker.DOMAIN, { - device_tracker.DOMAIN: { + assert await async_setup_component(hass, DT_DOMAIN, { + DT_DOMAIN: { CONF_PLATFORM: 'mqtt_json', 'devices': {dev_id: subscription} } @@ -178,13 +179,13 @@ async def test_single_level_wildcard_topic_not_matching(hass): async def test_multi_level_wildcard_topic_not_matching(hass): """Test not matching multi level wildcard topic.""" dev_id = 'zanzito' - entity_id = device_tracker.ENTITY_ID_FORMAT.format(dev_id) + entity_id = ENTITY_ID_FORMAT.format(dev_id) subscription = 'location/#' topic = 'somewhere/zanzito' location = json.dumps(LOCATION_MESSAGE) - assert await async_setup_component(hass, device_tracker.DOMAIN, { - device_tracker.DOMAIN: { + assert await async_setup_component(hass, DT_DOMAIN, { + DT_DOMAIN: { CONF_PLATFORM: 'mqtt_json', 'devices': {dev_id: subscription} } diff --git a/tests/components/tplink/test_device_tracker.py b/tests/components/tplink/test_device_tracker.py index f1d60d46762..d7676b51d72 100644 --- a/tests/components/tplink/test_device_tracker.py +++ b/tests/components/tplink/test_device_tracker.py @@ -3,7 +3,7 @@ import os import pytest -from homeassistant.components import device_tracker +from homeassistant.components.device_tracker.legacy import YAML_DEVICES from homeassistant.components.tplink.device_tracker import Tplink4DeviceScanner from homeassistant.const import (CONF_PLATFORM, CONF_PASSWORD, CONF_USERNAME, CONF_HOST) @@ -13,7 +13,7 @@ import requests_mock @pytest.fixture(autouse=True) def setup_comp(hass): """Initialize components.""" - yaml_devices = hass.config.path(device_tracker.YAML_DEVICES) + yaml_devices = hass.config.path(YAML_DEVICES) yield if os.path.isfile(yaml_devices): os.remove(yaml_devices) diff --git a/tests/components/unifi_direct/test_device_tracker.py b/tests/components/unifi_direct/test_device_tracker.py index ba40a09aa59..9407642b162 100644 --- a/tests/components/unifi_direct/test_device_tracker.py +++ b/tests/components/unifi_direct/test_device_tracker.py @@ -7,7 +7,7 @@ import pytest import voluptuous as vol from homeassistant.setup import async_setup_component -from homeassistant.components import device_tracker +from homeassistant.components.device_tracker.legacy import YAML_DEVICES from homeassistant.components.device_tracker import ( CONF_CONSIDER_HOME, CONF_TRACK_NEW, CONF_AWAY_HIDE, CONF_NEW_DEVICE_DEFAULTS) @@ -27,7 +27,7 @@ scanner_path = 'homeassistant.components.unifi_direct.device_tracker.' + \ def setup_comp(hass): """Initialize components.""" mock_component(hass, 'zone') - yaml_devices = hass.config.path(device_tracker.YAML_DEVICES) + yaml_devices = hass.config.path(YAML_DEVICES) yield if os.path.isfile(yaml_devices): os.remove(yaml_devices) diff --git a/tests/conftest.py b/tests/conftest.py index 4e567886ef0..fdac037bfa9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -102,11 +102,11 @@ def mock_device_tracker_conf(): devices.append(entity) with patch( - 'homeassistant.components.device_tracker' + 'homeassistant.components.device_tracker.legacy' '.DeviceTracker.async_update_config', side_effect=mock_update_config ), patch( - 'homeassistant.components.device_tracker.async_load_config', + 'homeassistant.components.device_tracker.legacy.async_load_config', side_effect=lambda *args: mock_coro(devices) ): yield devices From 1fec64a1b38b463c9591d9dd3185c428528bc010 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Wed, 15 May 2019 23:53:02 +0200 Subject: [PATCH 055/232] Update Pynetgear to v0.6.1 (#23886) * Update Pynetgear to v0.6.1 * update pynetgear to v0.6.1 --- homeassistant/components/netgear/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/netgear/manifest.json b/homeassistant/components/netgear/manifest.json index 8fbf185c6af..3ee3b189939 100644 --- a/homeassistant/components/netgear/manifest.json +++ b/homeassistant/components/netgear/manifest.json @@ -3,7 +3,7 @@ "name": "Netgear", "documentation": "https://www.home-assistant.io/components/netgear", "requirements": [ - "pynetgear==0.5.2" + "pynetgear==0.6.1" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index 4bec104e36f..dff4bcd7028 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1211,7 +1211,7 @@ pynanoleaf==0.0.5 pynello==2.0.2 # homeassistant.components.netgear -pynetgear==0.5.2 +pynetgear==0.6.1 # homeassistant.components.netio pynetio==0.1.9.1 From 6b359c95daaa50876d574c5168ddaf57afb37a08 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 16 May 2019 05:43:19 +0200 Subject: [PATCH 056/232] Fix PS4 blocking startup (#23893) --- homeassistant/components/ps4/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/ps4/__init__.py b/homeassistant/components/ps4/__init__.py index dca1142db7a..b91e6b239e7 100644 --- a/homeassistant/components/ps4/__init__.py +++ b/homeassistant/components/ps4/__init__.py @@ -73,7 +73,6 @@ async def async_migrate_entry(hass, entry): # Remove old entity entry. registry.async_remove(entity_id) - await hass.async_block_till_done() # Format old unique_id. unique_id = format_unique_id(entry.data[CONF_TOKEN], unique_id) From 36b1a89f93873d08b4b875661a53b934ed467911 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 16 May 2019 08:57:43 +0200 Subject: [PATCH 057/232] Fix Hassio-version for Azure Pipelines (#23895) --- azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index fd45c334cf3..ceccb39d5f0 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -159,7 +159,7 @@ jobs: git config --global user.email "pvizeli@syshack.ch" git config --global credential.helper store - echo "https://$(githubToken):x-oauth-basic@github.com > $HOME\.git-credentials + echo "https://$(githubToken):x-oauth-basic@github.com" > $HOME\.git-credentials displayName: 'Install requirements' - script: | set -e From 213c91ae73a895bbc6a774e0b7c7b92086f4821a Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 16 May 2019 09:28:08 +0200 Subject: [PATCH 058/232] Update azure-pipelines.yml for Azure Pipelines --- azure-pipelines.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index ceccb39d5f0..e7231918ab1 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -40,6 +40,7 @@ jobs: buildArch: 'aarch64' steps: - script: | + sudo apt-get update sudo apt-get install -y --no-install-recommends \ qemu-user-static \ binfmt-support From 692eeb3687bd053abb74dbbcba7b8d1f273d9041 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Thu, 16 May 2019 13:32:13 +0100 Subject: [PATCH 059/232] Fix ecobee 3 homekit pairing (#23882) --- .../homekit_controller/config_flow.py | 81 ++++- .../homekit_controller/test_config_flow.py | 306 ++++++++---------- 2 files changed, 209 insertions(+), 178 deletions(-) diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index 6c534bb0c64..deefb596310 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -66,14 +66,16 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow): def __init__(self): """Initialize the homekit_controller flow.""" + import homekit # pylint: disable=import-error + self.model = None self.hkid = None self.devices = {} + self.controller = homekit.Controller() + self.finish_pairing = None async def async_step_user(self, user_input=None): """Handle a flow start.""" - import homekit - errors = {} if user_input is not None: @@ -82,9 +84,8 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow): self.model = self.devices[key]['md'] return await self.async_step_pair() - controller = homekit.Controller() all_hosts = await self.hass.async_add_executor_job( - controller.discover, 5 + self.controller.discover, 5 ) self.devices = {} @@ -189,7 +190,11 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow): self.model = model self.hkid = hkid - return await self.async_step_pair() + + # We want to show the pairing form - but don't call async_step_pair + # directly as it has side effects (will ask the device to show a + # pairing code) + return self._async_step_pair_show_form() async def async_import_legacy_pairing(self, discovery_props, pairing_data): """Migrate a legacy pairing to config entries.""" @@ -216,45 +221,91 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow): """Pair with a new HomeKit accessory.""" import homekit # pylint: disable=import-error + # If async_step_pair is called with no pairing code then we do the M1 + # phase of pairing. If this is successful the device enters pairing + # mode. + + # If it doesn't have a screen then the pin is static. + + # If it has a display it will display a pin on that display. In + # this case the code is random. So we have to call the start_pairing + # API before the user can enter a pin. But equally we don't want to + # call start_pairing when the device is discovered, only when they + # click on 'Configure' in the UI. + + # start_pairing will make the device show its pin and return a + # callable. We call the callable with the pin that the user has typed + # in. + errors = {} if pair_info: code = pair_info['pairing_code'] - controller = homekit.Controller() try: await self.hass.async_add_executor_job( - controller.perform_pairing, self.hkid, self.hkid, code + self.finish_pairing, code ) - pairing = controller.pairings.get(self.hkid) + pairing = self.controller.pairings.get(self.hkid) if pairing: return await self._entry_from_accessory( pairing) errors['pairing_code'] = 'unable_to_pair' except homekit.AuthenticationError: + # PairSetup M4 - SRP proof failed + # PairSetup M6 - Ed25519 signature verification failed + # PairVerify M4 - Decryption failed + # PairVerify M4 - Device not recognised + # PairVerify M4 - Ed25519 signature verification failed errors['pairing_code'] = 'authentication_error' except homekit.UnknownError: + # An error occured on the device whilst performing this + # operation. errors['pairing_code'] = 'unknown_error' - except homekit.MaxTriesError: - errors['pairing_code'] = 'max_tries_error' - except homekit.BusyError: - errors['pairing_code'] = 'busy_error' except homekit.MaxPeersError: + # The device can't pair with any more accessories. errors['pairing_code'] = 'max_peers_error' except homekit.AccessoryNotFoundError: + # Can no longer find the device on the network return self.async_abort(reason='accessory_not_found_error') - except homekit.UnavailableError: - return self.async_abort(reason='already_paired') except Exception: # pylint: disable=broad-except _LOGGER.exception( "Pairing attempt failed with an unhandled exception" ) errors['pairing_code'] = 'pairing_failed' + start_pairing = self.controller.start_pairing + try: + self.finish_pairing = await self.hass.async_add_executor_job( + start_pairing, self.hkid, self.hkid + ) + except homekit.BusyError: + # Already performing a pair setup operation with a different + # controller + errors['pairing_code'] = 'busy_error' + except homekit.MaxTriesError: + # The accessory has received more than 100 unsuccessful auth + # attempts. + errors['pairing_code'] = 'max_tries_error' + except homekit.UnavailableError: + # The accessory is already paired - cannot try to pair again. + return self.async_abort(reason='already_paired') + except homekit.AccessoryNotFoundError: + # Can no longer find the device on the network + return self.async_abort(reason='accessory_not_found_error') + except Exception: # pylint: disable=broad-except + _LOGGER.exception( + "Pairing attempt failed with an unhandled exception" + ) + errors['pairing_code'] = 'pairing_failed' + + return self._async_step_pair_show_form(errors) + + def _async_step_pair_show_form(self, errors=None): return self.async_show_form( step_id='pair', - errors=errors, + errors=errors or {}, data_schema=vol.Schema({ vol.Required('pairing_code'): vol.All(str, vol.Strip), }) diff --git a/tests/components/homekit_controller/test_config_flow.py b/tests/components/homekit_controller/test_config_flow.py index 33160abaa55..9c869809544 100644 --- a/tests/components/homekit_controller/test_config_flow.py +++ b/tests/components/homekit_controller/test_config_flow.py @@ -13,14 +13,25 @@ from tests.components.homekit_controller.common import ( ) -ERROR_MAPPING_FORM_FIXTURE = [ - (homekit.MaxPeersError, 'max_peers_error'), +PAIRING_START_FORM_ERRORS = [ (homekit.BusyError, 'busy_error'), (homekit.MaxTriesError, 'max_tries_error'), (KeyError, 'pairing_failed'), ] -ERROR_MAPPING_ABORT_FIXTURE = [ +PAIRING_START_ABORT_ERRORS = [ + (homekit.AccessoryNotFoundError, 'accessory_not_found_error'), + (homekit.UnavailableError, 'already_paired'), +] + +PAIRING_FINISH_FORM_ERRORS = [ + (homekit.MaxPeersError, 'max_peers_error'), + (homekit.AuthenticationError, 'authentication_error'), + (homekit.UnknownError, 'unknown_error'), + (KeyError, 'pairing_failed'), +] + +PAIRING_FINISH_ABORT_ERRORS = [ (homekit.AccessoryNotFoundError, 'accessory_not_found_error'), ] @@ -29,6 +40,10 @@ def _setup_flow_handler(hass): flow = config_flow.HomekitControllerFlowHandler() flow.hass = hass flow.context = {} + + flow.controller = mock.Mock() + flow.controller.pairings = {} + return flow @@ -48,11 +63,18 @@ async def test_discovery_works(hass): flow = _setup_flow_handler(hass) + # Device is discovered result = await flow.async_step_discovery(discovery_info) assert result['type'] == 'form' assert result['step_id'] == 'pair' assert flow.context == {'title_placeholders': {'name': 'TestDevice'}} + # User initiates pairing - device enters pairing mode and displays code + result = await flow.async_step_pair({}) + assert result['type'] == 'form' + assert result['step_id'] == 'pair' + assert flow.controller.start_pairing.call_count == 1 + pairing = mock.Mock(pairing_data={ 'AccessoryPairingID': '00:00:00:00:00:00', }) @@ -68,17 +90,13 @@ async def test_discovery_works(hass): }] }] - controller = mock.Mock() - controller.pairings = { + # Pairing doesn't error error and pairing results + flow.controller.pairings = { '00:00:00:00:00:00': pairing, } - - with mock.patch('homekit.Controller') as controller_cls: - controller_cls.return_value = controller - result = await flow.async_step_pair({ - 'pairing_code': '111-22-33', - }) - + result = await flow.async_step_pair({ + 'pairing_code': '111-22-33', + }) assert result['type'] == 'create_entry' assert result['title'] == 'Koogeek-LS1-20833F' assert result['data'] == pairing.pairing_data @@ -100,11 +118,18 @@ async def test_discovery_works_upper_case(hass): flow = _setup_flow_handler(hass) + # Device is discovered result = await flow.async_step_discovery(discovery_info) assert result['type'] == 'form' assert result['step_id'] == 'pair' assert flow.context == {'title_placeholders': {'name': 'TestDevice'}} + # User initiates pairing - device enters pairing mode and displays code + result = await flow.async_step_pair({}) + assert result['type'] == 'form' + assert result['step_id'] == 'pair' + assert flow.controller.start_pairing.call_count == 1 + pairing = mock.Mock(pairing_data={ 'AccessoryPairingID': '00:00:00:00:00:00', }) @@ -120,17 +145,12 @@ async def test_discovery_works_upper_case(hass): }] }] - controller = mock.Mock() - controller.pairings = { + flow.controller.pairings = { '00:00:00:00:00:00': pairing, } - - with mock.patch('homekit.Controller') as controller_cls: - controller_cls.return_value = controller - result = await flow.async_step_pair({ - 'pairing_code': '111-22-33', - }) - + result = await flow.async_step_pair({ + 'pairing_code': '111-22-33', + }) assert result['type'] == 'create_entry' assert result['title'] == 'Koogeek-LS1-20833F' assert result['data'] == pairing.pairing_data @@ -151,11 +171,18 @@ async def test_discovery_works_missing_csharp(hass): flow = _setup_flow_handler(hass) + # Device is discovered result = await flow.async_step_discovery(discovery_info) assert result['type'] == 'form' assert result['step_id'] == 'pair' assert flow.context == {'title_placeholders': {'name': 'TestDevice'}} + # User initiates pairing - device enters pairing mode and displays code + result = await flow.async_step_pair({}) + assert result['type'] == 'form' + assert result['step_id'] == 'pair' + assert flow.controller.start_pairing.call_count == 1 + pairing = mock.Mock(pairing_data={ 'AccessoryPairingID': '00:00:00:00:00:00', }) @@ -171,17 +198,13 @@ async def test_discovery_works_missing_csharp(hass): }] }] - controller = mock.Mock() - controller.pairings = { + flow.controller.pairings = { '00:00:00:00:00:00': pairing, } - with mock.patch('homekit.Controller') as controller_cls: - controller_cls.return_value = controller - result = await flow.async_step_pair({ - 'pairing_code': '111-22-33', - }) - + result = await flow.async_step_pair({ + 'pairing_code': '111-22-33', + }) assert result['type'] == 'create_entry' assert result['title'] == 'Koogeek-LS1-20833F' assert result['data'] == pairing.pairing_data @@ -342,26 +365,28 @@ async def test_pair_unable_to_pair(hass): flow = _setup_flow_handler(hass) + # Device is discovered result = await flow.async_step_discovery(discovery_info) assert result['type'] == 'form' assert result['step_id'] == 'pair' assert flow.context == {'title_placeholders': {'name': 'TestDevice'}} - controller = mock.Mock() - controller.pairings = {} - - with mock.patch('homekit.Controller') as controller_cls: - controller_cls.return_value = controller - result = await flow.async_step_pair({ - 'pairing_code': '111-22-33', - }) + # User initiates pairing - device enters pairing mode and displays code + result = await flow.async_step_pair({}) + assert result['type'] == 'form' + assert result['step_id'] == 'pair' + assert flow.controller.start_pairing.call_count == 1 + # Pairing doesn't error but no pairing object is generated + result = await flow.async_step_pair({ + 'pairing_code': '111-22-33', + }) assert result['type'] == 'form' assert result['errors']['pairing_code'] == 'unable_to_pair' -@pytest.mark.parametrize("exception,expected", ERROR_MAPPING_ABORT_FIXTURE) -async def test_pair_abort_errors(hass, exception, expected): +@pytest.mark.parametrize("exception,expected", PAIRING_START_ABORT_ERRORS) +async def test_pair_abort_errors_on_start(hass, exception, expected): """Test various pairing errors.""" discovery_info = { 'name': 'TestDevice', @@ -377,28 +402,24 @@ async def test_pair_abort_errors(hass, exception, expected): flow = _setup_flow_handler(hass) + # Device is discovered result = await flow.async_step_discovery(discovery_info) assert result['type'] == 'form' assert result['step_id'] == 'pair' assert flow.context == {'title_placeholders': {'name': 'TestDevice'}} - controller = mock.Mock() - controller.pairings = {} - - with mock.patch('homekit.Controller') as controller_cls: - controller_cls.return_value = controller - controller.perform_pairing.side_effect = exception('error') - result = await flow.async_step_pair({ - 'pairing_code': '111-22-33', - }) + # User initiates pairing - device refuses to enter pairing mode + with mock.patch.object(flow.controller, 'start_pairing') as start_pairing: + start_pairing.side_effect = exception('error') + result = await flow.async_step_pair({}) assert result['type'] == 'abort' assert result['reason'] == expected assert flow.context == {'title_placeholders': {'name': 'TestDevice'}} -@pytest.mark.parametrize("exception,expected", ERROR_MAPPING_FORM_FIXTURE) -async def test_pair_form_errors(hass, exception, expected): +@pytest.mark.parametrize("exception,expected", PAIRING_START_FORM_ERRORS) +async def test_pair_form_errors_on_start(hass, exception, expected): """Test various pairing errors.""" discovery_info = { 'name': 'TestDevice', @@ -414,28 +435,25 @@ async def test_pair_form_errors(hass, exception, expected): flow = _setup_flow_handler(hass) + # Device is discovered result = await flow.async_step_discovery(discovery_info) assert result['type'] == 'form' assert result['step_id'] == 'pair' assert flow.context == {'title_placeholders': {'name': 'TestDevice'}} - controller = mock.Mock() - controller.pairings = {} - - with mock.patch('homekit.Controller') as controller_cls: - controller_cls.return_value = controller - controller.perform_pairing.side_effect = exception('error') - result = await flow.async_step_pair({ - 'pairing_code': '111-22-33', - }) + # User initiates pairing - device refuses to enter pairing mode + with mock.patch.object(flow.controller, 'start_pairing') as start_pairing: + start_pairing.side_effect = exception('error') + result = await flow.async_step_pair({}) assert result['type'] == 'form' assert result['errors']['pairing_code'] == expected assert flow.context == {'title_placeholders': {'name': 'TestDevice'}} -async def test_pair_authentication_error(hass): - """Pairing code is incorrect.""" +@pytest.mark.parametrize("exception,expected", PAIRING_FINISH_ABORT_ERRORS) +async def test_pair_abort_errors_on_finish(hass, exception, expected): + """Test various pairing errors.""" discovery_info = { 'name': 'TestDevice', 'host': '127.0.0.1', @@ -450,96 +468,65 @@ async def test_pair_authentication_error(hass): flow = _setup_flow_handler(hass) + # Device is discovered result = await flow.async_step_discovery(discovery_info) assert result['type'] == 'form' assert result['step_id'] == 'pair' assert flow.context == {'title_placeholders': {'name': 'TestDevice'}} - controller = mock.Mock() - controller.pairings = {} - - with mock.patch('homekit.Controller') as controller_cls: - controller_cls.return_value = controller - exc = homekit.AuthenticationError('Invalid pairing code') - controller.perform_pairing.side_effect = exc - result = await flow.async_step_pair({ - 'pairing_code': '111-22-33', - }) - - assert result['type'] == 'form' - assert result['errors']['pairing_code'] == 'authentication_error' - - -async def test_pair_unknown_error(hass): - """Pairing failed for an unknown rason.""" - discovery_info = { - 'name': 'TestDevice', - 'host': '127.0.0.1', - 'port': 8080, - 'properties': { - 'md': 'TestDevice', - 'id': '00:00:00:00:00:00', - 'c#': 1, - 'sf': 1, - } - } - - flow = _setup_flow_handler(hass) - - result = await flow.async_step_discovery(discovery_info) + # User initiates pairing - device enters pairing mode and displays code + result = await flow.async_step_pair({}) assert result['type'] == 'form' assert result['step_id'] == 'pair' - assert flow.context == {'title_placeholders': {'name': 'TestDevice'}} - - controller = mock.Mock() - controller.pairings = {} - - with mock.patch('homekit.Controller') as controller_cls: - controller_cls.return_value = controller - exc = homekit.UnknownError('Unknown error') - controller.perform_pairing.side_effect = exc - result = await flow.async_step_pair({ - 'pairing_code': '111-22-33', - }) - - assert result['type'] == 'form' - assert result['errors']['pairing_code'] == 'unknown_error' - - -async def test_pair_already_paired(hass): - """Device is already paired.""" - discovery_info = { - 'name': 'TestDevice', - 'host': '127.0.0.1', - 'port': 8080, - 'properties': { - 'md': 'TestDevice', - 'id': '00:00:00:00:00:00', - 'c#': 1, - 'sf': 1, - } - } - - flow = _setup_flow_handler(hass) - - result = await flow.async_step_discovery(discovery_info) - assert result['type'] == 'form' - assert result['step_id'] == 'pair' - assert flow.context == {'title_placeholders': {'name': 'TestDevice'}} - - controller = mock.Mock() - controller.pairings = {} - - with mock.patch('homekit.Controller') as controller_cls: - controller_cls.return_value = controller - exc = homekit.UnavailableError('Unavailable error') - controller.perform_pairing.side_effect = exc - result = await flow.async_step_pair({ - 'pairing_code': '111-22-33', - }) + assert flow.controller.start_pairing.call_count == 1 + # User submits code - pairing fails but can be retried + flow.finish_pairing.side_effect = exception('error') + result = await flow.async_step_pair({ + 'pairing_code': '111-22-33', + }) assert result['type'] == 'abort' - assert result['reason'] == 'already_paired' + assert result['reason'] == expected + assert flow.context == {'title_placeholders': {'name': 'TestDevice'}} + + +@pytest.mark.parametrize("exception,expected", PAIRING_FINISH_FORM_ERRORS) +async def test_pair_form_errors_on_finish(hass, exception, expected): + """Test various pairing errors.""" + discovery_info = { + 'name': 'TestDevice', + 'host': '127.0.0.1', + 'port': 8080, + 'properties': { + 'md': 'TestDevice', + 'id': '00:00:00:00:00:00', + 'c#': 1, + 'sf': 1, + } + } + + flow = _setup_flow_handler(hass) + + # Device is discovered + result = await flow.async_step_discovery(discovery_info) + assert result['type'] == 'form' + assert result['step_id'] == 'pair' + assert flow.context == {'title_placeholders': {'name': 'TestDevice'}} + + # User initiates pairing - device enters pairing mode and displays code + result = await flow.async_step_pair({}) + assert result['type'] == 'form' + assert result['step_id'] == 'pair' + assert flow.controller.start_pairing.call_count == 1 + + # User submits code - pairing fails but can be retried + flow.finish_pairing.side_effect = exception('error') + result = await flow.async_step_pair({ + 'pairing_code': '111-22-33', + }) + assert result['type'] == 'form' + assert result['errors']['pairing_code'] == expected + assert flow.context == {'title_placeholders': {'name': 'TestDevice'}} async def test_import_works(hass): @@ -647,19 +634,16 @@ async def test_user_works(hass): }] }] - controller = mock.Mock() - controller.pairings = { + flow = _setup_flow_handler(hass) + + flow.controller.pairings = { '00:00:00:00:00:00': pairing, } - controller.discover.return_value = [ + flow.controller.discover.return_value = [ discovery_info, ] - flow = _setup_flow_handler(hass) - - with mock.patch('homekit.Controller') as controller_cls: - controller_cls.return_value = controller - result = await flow.async_step_user() + result = await flow.async_step_user() assert result['type'] == 'form' assert result['step_id'] == 'user' @@ -669,11 +653,9 @@ async def test_user_works(hass): assert result['type'] == 'form' assert result['step_id'] == 'pair' - with mock.patch('homekit.Controller') as controller_cls: - controller_cls.return_value = controller - result = await flow.async_step_pair({ - 'pairing_code': '111-22-33', - }) + result = await flow.async_step_pair({ + 'pairing_code': '111-22-33', + }) assert result['type'] == 'create_entry' assert result['title'] == 'Koogeek-LS1-20833F' assert result['data'] == pairing.pairing_data @@ -683,9 +665,8 @@ async def test_user_no_devices(hass): """Test user initiated pairing where no devices discovered.""" flow = _setup_flow_handler(hass) - with mock.patch('homekit.Controller') as controller_cls: - controller_cls.return_value.discover.return_value = [] - result = await flow.async_step_user() + flow.controller.discover.return_value = [] + result = await flow.async_step_user() assert result['type'] == 'abort' assert result['reason'] == 'no_devices' @@ -705,11 +686,10 @@ async def test_user_no_unpaired_devices(hass): 'sf': 0, } - with mock.patch('homekit.Controller') as controller_cls: - controller_cls.return_value.discover.return_value = [ - discovery_info, - ] - result = await flow.async_step_user() + flow.controller.discover.return_value = [ + discovery_info, + ] + result = await flow.async_step_user() assert result['type'] == 'abort' assert result['reason'] == 'no_devices' From 9be384690ad5cc00ab5f98718b111e178d54da81 Mon Sep 17 00:00:00 2001 From: Markus Jankowski Date: Thu, 16 May 2019 15:10:30 +0200 Subject: [PATCH 060/232] Enable Homematic IP cloud climate device with HeatingThermostat only (#23776) * Enable climate device with HeatingThermostat only * Fix after review --- .../components/homematicip_cloud/climate.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/homeassistant/components/homematicip_cloud/climate.py b/homeassistant/components/homematicip_cloud/climate.py index 3170fc149d5..66695bb01c7 100644 --- a/homeassistant/components/homematicip_cloud/climate.py +++ b/homeassistant/components/homematicip_cloud/climate.py @@ -1,6 +1,8 @@ """Support for HomematicIP Cloud climate devices.""" import logging +from homematicip.aio.device import ( + AsyncHeatingThermostat, AsyncHeatingThermostatCompact) from homematicip.aio.group import AsyncHeatingGroup from homematicip.aio.home import AsyncHome @@ -48,6 +50,9 @@ class HomematicipHeatingGroup(HomematicipGenericDevice, ClimateDevice): def __init__(self, home: AsyncHome, device) -> None: """Initialize heating group.""" device.modelType = 'Group-Heating' + self._simple_heating = None + if device.actualTemperature is None: + self._simple_heating = _get_first_heating_thermostat(device) super().__init__(home, device) @property @@ -68,6 +73,8 @@ class HomematicipHeatingGroup(HomematicipGenericDevice, ClimateDevice): @property def current_temperature(self) -> float: """Return the current temperature.""" + if self._simple_heating: + return self._simple_heating.valveActualTemperature return self._device.actualTemperature @property @@ -96,3 +103,12 @@ class HomematicipHeatingGroup(HomematicipGenericDevice, ClimateDevice): if temperature is None: return await self._device.set_point_temperature(temperature) + + +def _get_first_heating_thermostat(heating_group: AsyncHeatingGroup): + """Return the first HeatingThermostat from a HeatingGroup.""" + for device in heating_group.devices: + if isinstance(device, (AsyncHeatingThermostat, + AsyncHeatingThermostatCompact)): + return device + return None From c2fc8a0d611eef5676acfc80a1ddb81fd003c146 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 16 May 2019 16:27:53 +0200 Subject: [PATCH 061/232] Load HA core config from storage (#23872) * Load HA core config from storage * Tweak * Lint, review comments * Fix test * Add tests * Lint * Address comments --- homeassistant/config.py | 84 ++++++++++++++++++++++++++++++++++------- homeassistant/core.py | 5 ++- tests/test_config.py | 62 ++++++++++++++++++++++++++++++ tests/test_core.py | 1 + 4 files changed, 137 insertions(+), 15 deletions(-) diff --git a/homeassistant/config.py b/homeassistant/config.py index 9e3f1d80663..88abf2ac791 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -50,6 +50,13 @@ FILE_MIGRATION = ( ('ios.conf', '.ios.conf'), ) +CORE_STORAGE_KEY = 'homeassistant.core_config' +CORE_STORAGE_VERSION = 1 + +SOURCE_DISCOVERED = 'discovered' +SOURCE_STORAGE = 'storage' +SOURCE_YAML = 'yaml' + DEFAULT_CORE_CONFIG = ( # Tuples (attribute, default, auto detect property, description) (CONF_NAME, 'Home', None, 'Name of the location where Home Assistant is ' @@ -473,6 +480,42 @@ def _format_config_error(ex: vol.Invalid, domain: str, config: Dict) -> str: return message +def _set_time_zone(hass: HomeAssistant, time_zone_str: Optional[str]) -> None: + """Help to set the time zone.""" + if time_zone_str is None: + return + + time_zone = date_util.get_time_zone(time_zone_str) + + if time_zone: + hass.config.time_zone = time_zone + date_util.set_default_time_zone(time_zone) + else: + _LOGGER.error("Received invalid time zone %s", time_zone_str) + + +async def async_load_ha_core_config(hass: HomeAssistant) -> None: + """Store [homeassistant] core config.""" + store = hass.helpers.storage.Store(CORE_STORAGE_VERSION, CORE_STORAGE_KEY, + private=True) + data = await store.async_load() + if not data: + return + + hac = hass.config + hac.config_source = SOURCE_STORAGE + hac.latitude = data['latitude'] + hac.longitude = data['longitude'] + hac.elevation = data['elevation'] + unit_system = data['unit_system'] + if unit_system == CONF_UNIT_SYSTEM_IMPERIAL: + hac.units = IMPERIAL_SYSTEM + else: + hac.units = METRIC_SYSTEM + hac.location_name = data['location_name'] + _set_time_zone(hass, data['time_zone']) + + async def async_process_ha_core_config( hass: HomeAssistant, config: Dict, api_password: Optional[str] = None, @@ -511,20 +554,14 @@ async def async_process_ha_core_config( auth_conf, mfa_conf)) + await async_load_ha_core_config(hass) + hac = hass.config - def set_time_zone(time_zone_str: Optional[str]) -> None: - """Help to set the time zone.""" - if time_zone_str is None: - return - - time_zone = date_util.get_time_zone(time_zone_str) - - if time_zone: - hac.time_zone = time_zone - date_util.set_default_time_zone(time_zone) - else: - _LOGGER.error("Received invalid time zone %s", time_zone_str) + if any([k in config for k in [ + CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, CONF_ELEVATION, + CONF_TIME_ZONE, CONF_UNIT_SYSTEM]]): + hac.config_source = SOURCE_YAML for key, attr in ((CONF_LATITUDE, 'latitude'), (CONF_LONGITUDE, 'longitude'), @@ -533,7 +570,7 @@ async def async_process_ha_core_config( if key in config: setattr(hac, attr, config[key]) - set_time_zone(config.get(CONF_TIME_ZONE)) + _set_time_zone(hass, config.get(CONF_TIME_ZONE)) # Init whitelist external dir hac.whitelist_external_dirs = {hass.config.path('www')} @@ -591,6 +628,7 @@ async def async_process_ha_core_config( # If we miss some of the needed values, auto detect them if None in (hac.latitude, hac.longitude, hac.units, hac.time_zone): + hac.config_source = SOURCE_DISCOVERED info = await loc_util.async_detect_location_info( hass.helpers.aiohttp_client.async_get_clientsession() ) @@ -613,7 +651,7 @@ async def async_process_ha_core_config( discovered.append(('name', info.city)) if hac.time_zone is None: - set_time_zone(info.time_zone) + _set_time_zone(hass, info.time_zone) discovered.append(('time_zone', info.time_zone)) if hac.elevation is None and hac.latitude is not None and \ @@ -630,6 +668,24 @@ async def async_process_ha_core_config( ", ".join('{}: {}'.format(key, val) for key, val in discovered)) +async def async_store_ha_core_config(hass: HomeAssistant) -> None: + """Store [homeassistant] core config.""" + config = hass.config.as_dict() + + data = { + 'latitude': config['latitude'], + 'longitude': config['longitude'], + 'elevation': config['elevation'], + 'unit_system': hass.config.units.name, + 'location_name': config['location_name'], + 'time_zone': config['time_zone'], + } + + store = hass.helpers.storage.Store(CORE_STORAGE_VERSION, CORE_STORAGE_KEY, + private=True) + await store.async_save(data) + + def _log_pkg_error( package: str, component: str, config: Dict, message: str) -> None: """Log an error while merging packages.""" diff --git a/homeassistant/core.py b/homeassistant/core.py index c127e100f11..a02c1b687ab 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -1177,6 +1177,8 @@ class Config: self.time_zone = None # type: Optional[datetime.tzinfo] self.units = METRIC_SYSTEM # type: UnitSystem + self.config_source = None # type: Optional[str] + # If True, pip install is skipped for requirements on startup self.skip_pip = False # type: bool @@ -1251,7 +1253,8 @@ class Config: 'components': self.components, 'config_dir': self.config_dir, 'whitelist_external_dirs': self.whitelist_external_dirs, - 'version': __version__ + 'version': __version__, + 'config_source': self.config_source } diff --git a/tests/test_config.py b/tests/test_config.py index 9090e229248..c081d97ed7c 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -414,6 +414,64 @@ def test_migrate_no_file_on_upgrade(mock_os, mock_shutil, hass): assert mock_os.rename.call_count == 0 +async def test_loading_configuration_from_storage(hass, hass_storage): + """Test loading core config onto hass object.""" + hass_storage["homeassistant.core_config"] = { + 'data': { + 'elevation': 10, + 'latitude': 55, + 'location_name': 'Home', + 'longitude': 13, + 'time_zone': 'Europe/Copenhagen', + 'unit_system': 'metric' + }, + 'key': 'homeassistant.core_config', + 'version': 1 + } + await config_util.async_process_ha_core_config( + hass, {'whitelist_external_dirs': '/tmp'}) + + assert hass.config.latitude == 55 + assert hass.config.longitude == 13 + assert hass.config.elevation == 10 + assert hass.config.location_name == 'Home' + assert hass.config.units.name == CONF_UNIT_SYSTEM_METRIC + assert hass.config.time_zone.zone == 'Europe/Copenhagen' + assert len(hass.config.whitelist_external_dirs) == 2 + assert '/tmp' in hass.config.whitelist_external_dirs + assert hass.config.config_source == config_util.SOURCE_STORAGE + + +async def test_override_stored_configuration(hass, hass_storage): + """Test loading core and YAML config onto hass object.""" + hass_storage["homeassistant.core_config"] = { + 'data': { + 'elevation': 10, + 'latitude': 55, + 'location_name': 'Home', + 'longitude': 13, + 'time_zone': 'Europe/Copenhagen', + 'unit_system': 'metric' + }, + 'key': 'homeassistant.core_config', + 'version': 1 + } + await config_util.async_process_ha_core_config(hass, { + 'latitude': 60, + 'whitelist_external_dirs': '/tmp', + }) + + assert hass.config.latitude == 60 + assert hass.config.longitude == 13 + assert hass.config.elevation == 10 + assert hass.config.location_name == 'Home' + assert hass.config.units.name == CONF_UNIT_SYSTEM_METRIC + assert hass.config.time_zone.zone == 'Europe/Copenhagen' + assert len(hass.config.whitelist_external_dirs) == 2 + assert '/tmp' in hass.config.whitelist_external_dirs + assert hass.config.config_source == config_util.SOURCE_YAML + + async def test_loading_configuration(hass): """Test loading core config onto hass object.""" hass.config = mock.Mock() @@ -436,6 +494,7 @@ async def test_loading_configuration(hass): assert hass.config.time_zone.zone == 'America/New_York' assert len(hass.config.whitelist_external_dirs) == 2 assert '/tmp' in hass.config.whitelist_external_dirs + assert hass.config.config_source == config_util.SOURCE_YAML async def test_loading_configuration_temperature_unit(hass): @@ -457,6 +516,7 @@ async def test_loading_configuration_temperature_unit(hass): assert hass.config.location_name == 'Huis' assert hass.config.units.name == CONF_UNIT_SYSTEM_METRIC assert hass.config.time_zone.zone == 'America/New_York' + assert hass.config.config_source == config_util.SOURCE_YAML async def test_loading_configuration_from_packages(hass): @@ -515,6 +575,7 @@ async def test_discovering_configuration(mock_detect, mock_elevation, hass): assert hass.config.units.name == CONF_UNIT_SYSTEM_METRIC assert hass.config.units.is_metric assert hass.config.time_zone.zone == 'America/Los_Angeles' + assert hass.config.config_source == config_util.SOURCE_DISCOVERED @asynctest.mock.patch('homeassistant.util.location.async_detect_location_info', @@ -539,6 +600,7 @@ async def test_discovering_configuration_auto_detect_fails(mock_detect, assert hass.config.time_zone == blankConfig.time_zone assert len(hass.config.whitelist_external_dirs) == 1 assert "/test/config/www" in hass.config.whitelist_external_dirs + assert hass.config.config_source == config_util.SOURCE_DISCOVERED @asynctest.mock.patch( diff --git a/tests/test_core.py b/tests/test_core.py index 1e709ed3a8a..afbbe3e33b2 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -900,6 +900,7 @@ class TestConfig(unittest.TestCase): 'config_dir': '/tmp/ha-config', 'whitelist_external_dirs': set(), 'version': __version__, + 'config_source': None, } assert expected == self.config.as_dict() From 7716e8fb68f22fdae44fd32e6bf055d354cf8fe2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Thu, 16 May 2019 18:07:37 +0200 Subject: [PATCH 062/232] Netatmo, handle offline device (#23907) * Netatmo, handle offline device * style --- homeassistant/components/netatmo/sensor.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index 7b71eaf659c..22ca9e696f3 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -523,9 +523,9 @@ class NetatmoData: _LOGGER.debug("%s detected!", str(self.data_class.__name__)) return station_data except NoDevice: - _LOGGER.error("No Weather or HomeCoach devices found for %s", str( - self.station - )) + _LOGGER.warning("No Weather or HomeCoach devices found for %s", + str(self.station) + ) raise def update(self): @@ -547,10 +547,14 @@ class NetatmoData: try: if self.station is not None: - self.data = self.station_data.lastData( + data = self.station_data.lastData( station=self.station, exclude=3600) else: - self.data = self.station_data.lastData(exclude=3600) + data = self.station_data.lastData(exclude=3600) + if not data: + self._next_update = time() + NETATMO_UPDATE_INTERVAL + return + self.data = data newinterval = 0 try: From 2f5d7d4522474e45dce4fac4b24c9cbd415009c0 Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Thu, 16 May 2019 12:04:20 -0700 Subject: [PATCH 063/232] [WIP] Simplify zeroconf (#23890) * Simplify zeroconf * Remove unused imports --- homeassistant/components/zeroconf/__init__.py | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index a14efb70411..e745cb53f6b 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -1,10 +1,8 @@ """Support for exposing Home Assistant via Zeroconf.""" import logging -import socket import voluptuous as vol -from homeassistant import util from homeassistant.const import (EVENT_HOMEASSISTANT_STOP, __version__) _LOGGER = logging.getLogger(__name__) @@ -23,8 +21,6 @@ async def async_setup(hass, config): """Set up Zeroconf and make Home Assistant discoverable.""" from aiozeroconf import Zeroconf, ServiceInfo - zeroconf = Zeroconf(hass.loop) - zeroconf_name = '{}.{}'.format(hass.config.location_name, ZEROCONF_TYPE) params = { @@ -34,18 +30,10 @@ async def async_setup(hass, config): 'requires_api_password': True, } - host_ip = util.get_local_ip() + info = ServiceInfo(ZEROCONF_TYPE, zeroconf_name, + port=hass.http.server_port, properties=params) - try: - host_ip_pton = socket.inet_pton(socket.AF_INET, host_ip) - info = ServiceInfo(ZEROCONF_TYPE, zeroconf_name, address=host_ip_pton, - port=hass.http.server_port, weight=0, priority=0, - properties=params) - except socket.error: - host_ip_pton = socket.inet_pton(socket.AF_INET6, host_ip) - info = ServiceInfo(ZEROCONF_TYPE, zeroconf_name, address6=host_ip_pton, - port=hass.http.server_port, weight=0, priority=0, - properties=params) + zeroconf = Zeroconf(hass.loop) await zeroconf.register_service(info) From c9703872e2b494d614be5009c3eeb7e77fb1a672 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 16 May 2019 21:21:38 +0200 Subject: [PATCH 064/232] Update Honeywell warning (#23913) --- homeassistant/components/honeywell/climate.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/honeywell/climate.py b/homeassistant/components/honeywell/climate.py index 5a07b094e24..75bbb2ca5d8 100644 --- a/homeassistant/components/honeywell/climate.py +++ b/homeassistant/components/honeywell/climate.py @@ -55,8 +55,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): _LOGGER.warning( "The honeywell component is deprecated for EU (i.e. non-US) systems, " - "this functionality will be removed in version 0.96.") - _LOGGER.warning( + "this functionality will be removed in version 0.96. " "Please switch to the evohome component, " "see: https://home-assistant.io/components/evohome") From 96fd8740903724b09ebc7bcf033826f18e9240bf Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Thu, 16 May 2019 12:28:24 -0700 Subject: [PATCH 065/232] Add @Kane610 to zeroconf CODEOWNERS --- CODEOWNERS | 2 +- homeassistant/components/zeroconf/manifest.json | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 29d4adc8933..ea0840a2519 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -269,7 +269,7 @@ homeassistant/components/yeelight/* @rytilahti @zewelor homeassistant/components/yeelightsunflower/* @lindsaymarkward homeassistant/components/yessssms/* @flowolf homeassistant/components/yi/* @bachya -homeassistant/components/zeroconf/* @robbiet480 +homeassistant/components/zeroconf/* @robbiet480 @Kane610 homeassistant/components/zha/* @dmulcahey @adminiuga homeassistant/components/zone/* @home-assistant/core homeassistant/components/zoneminder/* @rohankapoorcom diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index 5979ea12a58..07e620381e4 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -9,6 +9,7 @@ "api" ], "codeowners": [ - "@robbiet480" + "@robbiet480", + "@Kane610" ] } From 49d6d7c6568542a58d57635d734bfc667b9e6523 Mon Sep 17 00:00:00 2001 From: David McNett Date: Thu, 16 May 2019 13:34:06 -0700 Subject: [PATCH 066/232] Version bump insteonplm to 0.15.4 (#23918) * Version bump insteonplm to 0.15.4 * Package-level version change --- homeassistant/components/insteon/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/insteon/manifest.json b/homeassistant/components/insteon/manifest.json index 7ba27cbe625..a8c5b553943 100644 --- a/homeassistant/components/insteon/manifest.json +++ b/homeassistant/components/insteon/manifest.json @@ -3,7 +3,7 @@ "name": "Insteon", "documentation": "https://www.home-assistant.io/components/insteon", "requirements": [ - "insteonplm==0.15.2" + "insteonplm==0.15.4" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index dff4bcd7028..90c66d4b7dc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -611,7 +611,7 @@ incomfort-client==0.2.9 influxdb==5.2.0 # homeassistant.components.insteon -insteonplm==0.15.2 +insteonplm==0.15.4 # homeassistant.components.iperf3 iperf3==0.1.10 From c8cf06b8b716f4bfd24c0f169a0641dd52031373 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Thu, 16 May 2019 22:34:40 +0200 Subject: [PATCH 067/232] Switch media player to SWITCH type (#23914) MEDIA device type is being rejected by google now. --- homeassistant/components/google_assistant/const.py | 3 +-- tests/components/google_assistant/__init__.py | 8 ++++---- tests/components/google_assistant/test_smart_home.py | 2 +- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/google_assistant/const.py b/homeassistant/components/google_assistant/const.py index 0f15d10f181..92afe90a5ac 100644 --- a/homeassistant/components/google_assistant/const.py +++ b/homeassistant/components/google_assistant/const.py @@ -53,7 +53,6 @@ TYPE_SENSOR = PREFIX_TYPES + 'SENSOR' TYPE_DOOR = PREFIX_TYPES + 'DOOR' TYPE_TV = PREFIX_TYPES + 'TV' TYPE_SPEAKER = PREFIX_TYPES + 'SPEAKER' -TYPE_MEDIA = PREFIX_TYPES + 'MEDIA' SERVICE_REQUEST_SYNC = 'request_sync' HOMEGRAPH_URL = 'https://homegraph.googleapis.com/' @@ -89,7 +88,7 @@ DOMAIN_TO_GOOGLE_TYPES = { input_boolean.DOMAIN: TYPE_SWITCH, light.DOMAIN: TYPE_LIGHT, lock.DOMAIN: TYPE_LOCK, - media_player.DOMAIN: TYPE_MEDIA, + media_player.DOMAIN: TYPE_SWITCH, scene.DOMAIN: TYPE_SCENE, script.DOMAIN: TYPE_SCENE, switch.DOMAIN: TYPE_SWITCH, diff --git a/tests/components/google_assistant/__init__.py b/tests/components/google_assistant/__init__.py index 76ccc79a378..f3732c12213 100644 --- a/tests/components/google_assistant/__init__.py +++ b/tests/components/google_assistant/__init__.py @@ -147,7 +147,7 @@ DEMO_DEVICES = [{ 'action.devices.traits.Modes' ], 'type': - 'action.devices.types.MEDIA', + 'action.devices.types.SWITCH', 'willReportState': False }, { @@ -162,7 +162,7 @@ DEMO_DEVICES = [{ 'action.devices.traits.Modes' ], 'type': - 'action.devices.types.MEDIA', + 'action.devices.types.SWITCH', 'willReportState': False }, { @@ -171,7 +171,7 @@ DEMO_DEVICES = [{ 'name': 'Lounge room' }, 'traits': ['action.devices.traits.OnOff', 'action.devices.traits.Modes'], - 'type': 'action.devices.types.MEDIA', + 'type': 'action.devices.types.SWITCH', 'willReportState': False }, { 'id': @@ -182,7 +182,7 @@ DEMO_DEVICES = [{ 'traits': ['action.devices.traits.OnOff', 'action.devices.traits.Volume'], 'type': - 'action.devices.types.MEDIA', + 'action.devices.types.SWITCH', 'willReportState': False }, { diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index ea5291f28f7..519a55fbc00 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -686,7 +686,7 @@ async def test_device_class_cover(hass, device_class, google_type): @pytest.mark.parametrize("device_class,google_type", [ - ('non_existing_class', 'action.devices.types.MEDIA'), + ('non_existing_class', 'action.devices.types.SWITCH'), ('speaker', 'action.devices.types.SPEAKER'), ('tv', 'action.devices.types.TV'), ]) From 297d24c5b04a60abceacf0c6f16039f696cbbd65 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 16 May 2019 15:19:53 -0600 Subject: [PATCH 068/232] Fix bug when IQVIA API fails to return data (#23916) * Fix bug when IQVIA API fails to return data * Updated requirements * Fixed tests * Linting * Removed impossible case * Removed extraneous comment --- homeassistant/components/iqvia/__init__.py | 14 +++----------- homeassistant/components/iqvia/config_flow.py | 7 +++---- homeassistant/components/iqvia/manifest.json | 2 +- homeassistant/components/iqvia/sensor.py | 4 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/iqvia/test_config_flow.py | 15 ++------------- 7 files changed, 13 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/iqvia/__init__.py b/homeassistant/components/iqvia/__init__.py index cf8f92c1bd2..c58a7508e81 100644 --- a/homeassistant/components/iqvia/__init__.py +++ b/homeassistant/components/iqvia/__init__.py @@ -82,8 +82,9 @@ async def async_setup_entry(hass, config_entry): Client(config_entry.data[CONF_ZIP_CODE], websession), config_entry.data.get(CONF_MONITORED_CONDITIONS, list(SENSORS))) await iqvia.async_update() - except IQVIAError as err: - _LOGGER.error('Unable to set up IQVIA: %s', err) + except InvalidZipError: + _LOGGER.error( + 'Invalid ZIP code provided: %s', config_entry.data[CONF_ZIP_CODE]) return False hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = iqvia @@ -157,16 +158,7 @@ class IQVIAData: results = await asyncio.gather(*tasks.values(), return_exceptions=True) - # IQVIA sites require a bit more complicated error handling, given that - # they sometimes have parts (but not the whole thing) go down: - # 1. If `InvalidZipError` is thrown, quit everything immediately. - # 2. If a single request throws any other error, try the others. for key, result in zip(tasks, results): - if isinstance(result, InvalidZipError): - _LOGGER.error("No data for ZIP: %s", self._client.zip_code) - self.data = {} - return - if isinstance(result, IQVIAError): _LOGGER.error('Unable to get %s data: %s', key, result) self.data[key] = {} diff --git a/homeassistant/components/iqvia/config_flow.py b/homeassistant/components/iqvia/config_flow.py index d9a8c693670..fadecc8f3a7 100644 --- a/homeassistant/components/iqvia/config_flow.py +++ b/homeassistant/components/iqvia/config_flow.py @@ -4,7 +4,7 @@ from collections import OrderedDict import voluptuous as vol from pyiqvia import Client -from pyiqvia.errors import IQVIAError +from pyiqvia.errors import InvalidZipError from homeassistant import config_entries from homeassistant.core import callback @@ -54,11 +54,10 @@ class IQVIAFlowHandler(config_entries.ConfigFlow): return await self._show_form({CONF_ZIP_CODE: 'identifier_exists'}) websession = aiohttp_client.async_get_clientsession(self.hass) - client = Client(user_input[CONF_ZIP_CODE], websession) try: - await client.allergens.current() - except IQVIAError: + Client(user_input[CONF_ZIP_CODE], websession) + except InvalidZipError: return await self._show_form({CONF_ZIP_CODE: 'invalid_zip_code'}) return self.async_create_entry( diff --git a/homeassistant/components/iqvia/manifest.json b/homeassistant/components/iqvia/manifest.json index a59caa1654c..381165847ef 100644 --- a/homeassistant/components/iqvia/manifest.json +++ b/homeassistant/components/iqvia/manifest.json @@ -5,7 +5,7 @@ "documentation": "https://www.home-assistant.io/components/iqvia", "requirements": [ "numpy==1.16.3", - "pyiqvia==0.2.0" + "pyiqvia==0.2.1" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/iqvia/sensor.py b/homeassistant/components/iqvia/sensor.py index 5128b997b35..acd612d658b 100644 --- a/homeassistant/components/iqvia/sensor.py +++ b/homeassistant/components/iqvia/sensor.py @@ -106,8 +106,8 @@ class ForecastSensor(IQVIAEntity): if not self._iqvia.data: return - data = self._iqvia.data[self._type].get('Location') - if not data: + data = self._iqvia.data[self._type]['Location'] + if not data.get('periods'): return indices = [p['Index'] for p in data['periods']] diff --git a/requirements_all.txt b/requirements_all.txt index 90c66d4b7dc..9dc37a821cc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1130,7 +1130,7 @@ pyicloud==0.9.1 pyipma==1.2.1 # homeassistant.components.iqvia -pyiqvia==0.2.0 +pyiqvia==0.2.1 # homeassistant.components.irish_rail_transport pyirishrail==0.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 99c2e9e09dc..c932096825b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -235,7 +235,7 @@ pyheos==0.5.2 pyhomematic==0.1.58 # homeassistant.components.iqvia -pyiqvia==0.2.0 +pyiqvia==0.2.1 # homeassistant.components.litejet pylitejet==0.1 diff --git a/tests/components/iqvia/test_config_flow.py b/tests/components/iqvia/test_config_flow.py index 97ab4014291..48edc36629e 100644 --- a/tests/components/iqvia/test_config_flow.py +++ b/tests/components/iqvia/test_config_flow.py @@ -1,25 +1,16 @@ """Define tests for the IQVIA config flow.""" -from pyiqvia.errors import IQVIAError import pytest from homeassistant import data_entry_flow from homeassistant.components.iqvia import CONF_ZIP_CODE, DOMAIN, config_flow -from tests.common import MockConfigEntry, MockDependency, mock_coro +from tests.common import MockConfigEntry, MockDependency @pytest.fixture -def allergens_current_response(): - """Define a fixture for a successful allergens.current response.""" - return mock_coro() - - -@pytest.fixture -def mock_pyiqvia(allergens_current_response): +def mock_pyiqvia(): """Mock the pyiqvia library.""" with MockDependency('pyiqvia') as mock_pyiqvia_: - mock_pyiqvia_.Client().allergens.current.return_value = ( - allergens_current_response) yield mock_pyiqvia_ @@ -37,8 +28,6 @@ async def test_duplicate_error(hass): assert result['errors'] == {CONF_ZIP_CODE: 'identifier_exists'} -@pytest.mark.parametrize( - 'allergens_current_response', [mock_coro(exception=IQVIAError)]) async def test_invalid_zip_code(hass, mock_pyiqvia): """Test that an invalid ZIP code key throws an error.""" conf = { From 03a0a3572be9d972caad0eda6a336ee853a0c076 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Thu, 16 May 2019 22:30:48 +0100 Subject: [PATCH 069/232] Fix icons for homekit_controller sensors (#23921) --- homeassistant/components/homekit_controller/sensor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/sensor.py b/homeassistant/components/homekit_controller/sensor.py index f6f450b2b01..9ffa6c6b597 100644 --- a/homeassistant/components/homekit_controller/sensor.py +++ b/homeassistant/components/homekit_controller/sensor.py @@ -3,9 +3,9 @@ from homeassistant.const import TEMP_CELSIUS from . import KNOWN_DEVICES, HomeKitEntity -HUMIDITY_ICON = 'mdi-water-percent' -TEMP_C_ICON = "mdi-temperature-celsius" -BRIGHTNESS_ICON = "mdi-brightness-6" +HUMIDITY_ICON = 'mdi:water-percent' +TEMP_C_ICON = "mdi:thermometer" +BRIGHTNESS_ICON = "mdi:brightness-6" UNIT_PERCENT = "%" UNIT_LUX = "lux" From 1b5f526e097b49dc134ef9ab1890d00ffc3ef9db Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 16 May 2019 18:30:09 -0600 Subject: [PATCH 070/232] Fix additional IQVIA data bug (#23931) --- homeassistant/components/iqvia/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/iqvia/sensor.py b/homeassistant/components/iqvia/sensor.py index acd612d658b..b7f7519b543 100644 --- a/homeassistant/components/iqvia/sensor.py +++ b/homeassistant/components/iqvia/sensor.py @@ -106,8 +106,8 @@ class ForecastSensor(IQVIAEntity): if not self._iqvia.data: return - data = self._iqvia.data[self._type]['Location'] - if not data.get('periods'): + data = self._iqvia.data[self._type].get('Location') + if not data or not data.get('periods'): return indices = [p['Index'] for p in data['periods']] From edf34eea948f8ef09a39a7753e265e71e073800e Mon Sep 17 00:00:00 2001 From: karlkar Date: Fri, 17 May 2019 06:29:52 +0200 Subject: [PATCH 071/232] Fix problem with cameras that don't support time (#23924) Some onvif cameras don't support Date management. In that case None is returned and script crashes when trying to obtain date --- homeassistant/components/onvif/camera.py | 35 ++++++++++++------------ 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/onvif/camera.py b/homeassistant/components/onvif/camera.py index ea3d0277136..68c3c819567 100644 --- a/homeassistant/components/onvif/camera.py +++ b/homeassistant/components/onvif/camera.py @@ -164,27 +164,28 @@ class ONVIFHassCamera(Camera): system_date = dt_util.utcnow() device_time = await devicemgmt.GetSystemDateAndTime() - cdate = device_time.UTCDateTime - cam_date = dt.datetime(cdate.Date.Year, cdate.Date.Month, - cdate.Date.Day, cdate.Time.Hour, - cdate.Time.Minute, cdate.Time.Second, - 0, dt_util.UTC) + if device_time: + cdate = device_time.UTCDateTime + cam_date = dt.datetime(cdate.Date.Year, cdate.Date.Month, + cdate.Date.Day, cdate.Time.Hour, + cdate.Time.Minute, cdate.Time.Second, + 0, dt_util.UTC) - _LOGGER.debug("Camera date/time: %s", - cam_date) + _LOGGER.debug("Camera date/time: %s", + cam_date) - _LOGGER.debug("System date/time: %s", - system_date) + _LOGGER.debug("System date/time: %s", + system_date) - dt_diff = cam_date - system_date - dt_diff_seconds = dt_diff.total_seconds() + dt_diff = cam_date - system_date + dt_diff_seconds = dt_diff.total_seconds() - if dt_diff_seconds > 5: - _LOGGER.warning("The date/time on the camera is '%s', " - "which is different from the system '%s', " - "this could lead to authentication issues", - cam_date, - system_date) + if dt_diff_seconds > 5: + _LOGGER.warning("The date/time on the camera is '%s', " + "which is different from the system '%s', " + "this could lead to authentication issues", + cam_date, + system_date) _LOGGER.debug("Obtaining input uri") From 5b0d1415ad6e1073b53022d0620c8ff4b90ff75d Mon Sep 17 00:00:00 2001 From: Jc2k Date: Fri, 17 May 2019 07:41:21 +0100 Subject: [PATCH 072/232] Have homekit_controller use device registry (#23874) * Add device registry support * HK doesn't use mac as a connection id --- .../components/homekit_controller/__init__.py | 39 +++++++++++++++++++ .../homekit_controller/connection.py | 10 +++++ .../specific_devices/test_aqara_gateway.py | 13 +++++++ .../specific_devices/test_ecobee3.py | 18 +++++++++ .../specific_devices/test_koogeek_ls1.py | 9 +++++ .../specific_devices/test_lennox_e30.py | 12 ++++++ 6 files changed, 101 insertions(+) diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index f5e61c6060f..1fcbddbb400 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -3,6 +3,7 @@ import logging from homeassistant.helpers.entity import Entity from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import device_registry as dr # We need an import from .config_flow, without it .config_flow is never loaded. from .config_flow import HomekitControllerFlowHandler # noqa: F401 @@ -139,6 +140,29 @@ class HomeKitEntity(Entity): """Return True if entity is available.""" return self._available + @property + def device_info(self): + """Return the device info.""" + accessory_serial = self._accessory_info['serial-number'] + + device_info = { + 'identifiers': { + (DOMAIN, 'serial-number', accessory_serial), + }, + 'name': self._accessory_info['name'], + 'manufacturer': self._accessory_info.get('manufacturer', ''), + 'model': self._accessory_info.get('model', ''), + 'sw_version': self._accessory_info.get('firmware.revision', ''), + } + + # Some devices only have a single accessory - we don't add a via_hub + # otherwise it would be self referential. + bridge_serial = self._accessory.connection_info['serial-number'] + if accessory_serial != bridge_serial: + device_info['via_hub'] = (DOMAIN, 'serial-number', bridge_serial) + + return device_info + def get_characteristic_types(self): """Define the homekit characteristics the entity cares about.""" raise NotImplementedError @@ -153,6 +177,21 @@ async def async_setup_entry(hass, entry): del hass.data[KNOWN_DEVICES][conn.unique_id] raise ConfigEntryNotReady + conn_info = conn.connection_info + + device_registry = await dr.async_get_registry(hass) + device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + identifiers={ + (DOMAIN, 'serial-number', conn_info['serial-number']), + (DOMAIN, 'accessory-id', conn.unique_id), + }, + name=conn.name, + manufacturer=conn_info.get('manufacturer'), + model=conn_info.get('model'), + sw_version=conn_info.get('firmware.revision'), + ) + return True diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py index 080d6034237..3819bef680d 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -218,3 +218,13 @@ class HKDevice(): This id is random and will change if a device undergoes a hard reset. """ return self.pairing_data['AccessoryPairingID'] + + @property + def connection_info(self): + """Return accessory information for the main accessory.""" + return get_bridge_information(self.accessories) + + @property + def name(self): + """Name of the bridge accessory.""" + return get_accessory_name(self.connection_info) or self.unique_id diff --git a/tests/components/homekit_controller/specific_devices/test_aqara_gateway.py b/tests/components/homekit_controller/specific_devices/test_aqara_gateway.py index e0738d67083..0c77aa37196 100644 --- a/tests/components/homekit_controller/specific_devices/test_aqara_gateway.py +++ b/tests/components/homekit_controller/specific_devices/test_aqara_gateway.py @@ -39,3 +39,16 @@ async def test_aqara_gateway_setup(hass): assert light_state.attributes['supported_features'] == ( SUPPORT_BRIGHTNESS | SUPPORT_COLOR ) + + device_registry = await hass.helpers.device_registry.async_get_registry() + + # All the entities are services of the same accessory + # So it looks at the protocol like a single physical device + assert alarm.device_id == light.device_id + + device = device_registry.async_get(light.device_id) + assert device.manufacturer == 'Aqara' + assert device.name == 'Aqara Hub-1563' + assert device.model == 'ZHWA11LM' + assert device.sw_version == '1.4.7' + assert device.hub_device_id is None diff --git a/tests/components/homekit_controller/specific_devices/test_ecobee3.py b/tests/components/homekit_controller/specific_devices/test_ecobee3.py index 166ef32784b..10e01437cda 100644 --- a/tests/components/homekit_controller/specific_devices/test_ecobee3.py +++ b/tests/components/homekit_controller/specific_devices/test_ecobee3.py @@ -67,6 +67,24 @@ async def test_ecobee3_setup(hass): occ3 = entity_registry.async_get('binary_sensor.basement') assert occ3.unique_id == 'homekit-AB3C-56' + device_registry = await hass.helpers.device_registry.async_get_registry() + + climate_device = device_registry.async_get(climate.device_id) + assert climate_device.manufacturer == 'ecobee Inc.' + assert climate_device.name == 'HomeW' + assert climate_device.model == 'ecobee3' + assert climate_device.sw_version == '4.2.394' + assert climate_device.hub_device_id is None + + # Check that an attached sensor has its own device entity that + # is linked to the bridge + sensor_device = device_registry.async_get(occ1.device_id) + assert sensor_device.manufacturer == 'ecobee Inc.' + assert sensor_device.name == 'Kitchen' + assert sensor_device.model == 'REMOTE SENSOR' + assert sensor_device.sw_version == '1.0.0' + assert sensor_device.hub_device_id == climate_device.id + async def test_ecobee3_setup_from_cache(hass, hass_storage): """Test that Ecbobee can be correctly setup from its cached entity map.""" diff --git a/tests/components/homekit_controller/specific_devices/test_koogeek_ls1.py b/tests/components/homekit_controller/specific_devices/test_koogeek_ls1.py index a741885c6d5..8de3d1587b6 100644 --- a/tests/components/homekit_controller/specific_devices/test_koogeek_ls1.py +++ b/tests/components/homekit_controller/specific_devices/test_koogeek_ls1.py @@ -38,6 +38,15 @@ async def test_koogeek_ls1_setup(hass): SUPPORT_BRIGHTNESS | SUPPORT_COLOR ) + device_registry = await hass.helpers.device_registry.async_get_registry() + + device = device_registry.async_get(entry.device_id) + assert device.manufacturer == 'Koogeek' + assert device.name == 'Koogeek-LS1-20833F' + assert device.model == 'LS1' + assert device.sw_version == '2.2.15' + assert device.hub_device_id is None + @pytest.mark.parametrize('failure_cls', [ AccessoryDisconnectedError, EncryptionError diff --git a/tests/components/homekit_controller/specific_devices/test_lennox_e30.py b/tests/components/homekit_controller/specific_devices/test_lennox_e30.py index 1869161b1f8..9825e1ab4ab 100644 --- a/tests/components/homekit_controller/specific_devices/test_lennox_e30.py +++ b/tests/components/homekit_controller/specific_devices/test_lennox_e30.py @@ -27,3 +27,15 @@ async def test_lennox_e30_setup(hass): assert climate_state.attributes['supported_features'] == ( SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE ) + + device_registry = await hass.helpers.device_registry.async_get_registry() + + device = device_registry.async_get(climate.device_id) + assert device.manufacturer == 'Lennox' + assert device.name == 'Lennox' + assert device.model == 'E30 2B' + assert device.sw_version == '3.40.XX' + + # The fixture contains a single accessory - so its a single device + # and no bridge + assert device.hub_device_id is None From 1b4fc2ae8d59abdb781aee7c89343e3ac32676e5 Mon Sep 17 00:00:00 2001 From: Fredrik Erlandsson Date: Fri, 17 May 2019 09:25:07 +0200 Subject: [PATCH 073/232] Fix for non existing Daikin zones (#23792) --- homeassistant/components/daikin/switch.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/daikin/switch.py b/homeassistant/components/daikin/switch.py index 3106a7e8013..8521153c398 100644 --- a/homeassistant/components/daikin/switch.py +++ b/homeassistant/components/daikin/switch.py @@ -27,8 +27,7 @@ async def async_setup_entry(hass, entry, async_add_entities): if zones: async_add_entities([ DaikinZoneSwitch(daikin_api, zone_id) - for zone_id, name in enumerate(zones) - if name != '-' + for zone_id, zone in enumerate(zones) if zone != ('-', 0) ]) From 727f667cbc0a69181a37840da4b13d103413b0da Mon Sep 17 00:00:00 2001 From: Fredrik Erlandsson Date: Fri, 17 May 2019 09:36:47 +0200 Subject: [PATCH 074/232] Fix fan rates for Daikin (#23860) --- homeassistant/components/daikin/climate.py | 4 ++-- homeassistant/components/daikin/manifest.json | 2 +- requirements_all.txt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/daikin/climate.py b/homeassistant/components/daikin/climate.py index 239aa78bfbf..7b1d09827fe 100644 --- a/homeassistant/components/daikin/climate.py +++ b/homeassistant/components/daikin/climate.py @@ -81,7 +81,7 @@ class DaikinClimate(ClimateDevice): self._api = api self._list = { ATTR_OPERATION_MODE: list(HA_STATE_TO_DAIKIN), - ATTR_FAN_MODE: self._api.device.fan_modes, + ATTR_FAN_MODE: self._api.device.fan_rate, ATTR_SWING_MODE: list( map( str.title, @@ -97,7 +97,7 @@ class DaikinClimate(ClimateDevice): if self._api.device.support_away_mode: self._supported_features |= SUPPORT_AWAY_MODE - if self._api.device.support_fan_mode: + if self._api.device.support_fan_rate: self._supported_features |= SUPPORT_FAN_MODE if self._api.device.support_swing_mode: diff --git a/homeassistant/components/daikin/manifest.json b/homeassistant/components/daikin/manifest.json index a0141dd1f99..485004bb615 100644 --- a/homeassistant/components/daikin/manifest.json +++ b/homeassistant/components/daikin/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/components/daikin", "requirements": [ - "pydaikin==1.4.3" + "pydaikin==1.4.4" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 9dc37a821cc..1bbb6d8d308 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1024,7 +1024,7 @@ pycsspeechtts==1.0.2 # pycups==1.9.73 # homeassistant.components.daikin -pydaikin==1.4.3 +pydaikin==1.4.4 # homeassistant.components.danfoss_air pydanfossair==0.1.0 From 5d983d0b61151a34e4369b220a9c90f9893e63f0 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 17 May 2019 09:39:36 +0200 Subject: [PATCH 075/232] Fix auto version update Hass.io (#23935) --- azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 3cb262daa5e..02211dca902 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -160,7 +160,7 @@ jobs: git config --global user.email "pvizeli@syshack.ch" git config --global credential.helper store - echo "https://$(githubToken):x-oauth-basic@github.com" > $HOME\.git-credentials + echo "https://$(githubToken):x-oauth-basic@github.com" > $HOME/.git-credentials displayName: 'Install requirements' - script: | set -e From e072981295a12ae63f1e7df83ae2cb96393dcf9b Mon Sep 17 00:00:00 2001 From: bouni Date: Fri, 17 May 2019 09:47:10 +0200 Subject: [PATCH 076/232] Added support for sensor other than temperature and humidity (#23863) * Added support for sensor other than temperature and humidity * fixed lint errors * fixed minor issues pointed out by @fabaff --- homeassistant/components/spaceapi/__init__.py | 39 +++++++++++++------ 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/spaceapi/__init__.py b/homeassistant/components/spaceapi/__init__.py index 5431cd6260c..975706dd06a 100644 --- a/homeassistant/components/spaceapi/__init__.py +++ b/homeassistant/components/spaceapi/__init__.py @@ -28,6 +28,7 @@ ATTR_SPACE = 'space' ATTR_UNIT = 'unit' ATTR_URL = 'url' ATTR_VALUE = 'value' +ATTR_SENSOR_LOCATION = 'location' CONF_CONTACT = 'contact' CONF_HUMIDITY = 'humidity' @@ -72,9 +73,10 @@ STATE_SCHEMA = vol.Schema({ vol.Inclusive(CONF_ICON_OPEN, CONF_ICONS): cv.url, }, required=False) -SENSOR_SCHEMA = vol.Schema( - {vol.In(SENSOR_TYPES): [cv.entity_id]} -) +SENSOR_SCHEMA = vol.Schema({ + vol.In(SENSOR_TYPES): [cv.entity_id], + cv.string: [cv.entity_id] +}) CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ @@ -105,6 +107,27 @@ class APISpaceApiView(HomeAssistantView): url = URL_API_SPACEAPI name = 'api:spaceapi' + @staticmethod + def get_sensor_data(hass, spaceapi, sensor): + """Get data from a sensor.""" + sensor_state = hass.states.get(sensor) + if not sensor_state: + return None + sensor_data = { + ATTR_NAME: sensor_state.name, + ATTR_VALUE: sensor_state.state + } + if ATTR_SENSOR_LOCATION in sensor_state.attributes: + sensor_data[ATTR_LOCATION] = \ + sensor_state.attributes[ATTR_SENSOR_LOCATION] + else: + sensor_data[ATTR_LOCATION] = spaceapi[CONF_SPACE] + # Some sensors don't have a unit of measurement + if ATTR_UNIT_OF_MEASUREMENT in sensor_state.attributes: + sensor_data[ATTR_UNIT] = \ + sensor_state.attributes[ATTR_UNIT_OF_MEASUREMENT] + return sensor_data + @ha.callback def get(self, request): """Get SpaceAPI data.""" @@ -154,15 +177,7 @@ class APISpaceApiView(HomeAssistantView): for sensor_type in is_sensors: sensors[sensor_type] = [] for sensor in spaceapi['sensors'][sensor_type]: - sensor_state = hass.states.get(sensor) - unit = sensor_state.attributes[ATTR_UNIT_OF_MEASUREMENT] - value = sensor_state.state - sensor_data = { - ATTR_LOCATION: spaceapi[CONF_SPACE], - ATTR_NAME: sensor_state.name, - ATTR_UNIT: unit, - ATTR_VALUE: value, - } + sensor_data = self.get_sensor_data(hass, spaceapi, sensor) sensors[sensor_type].append(sensor_data) data[ATTR_SENSORS] = sensors From fffc4dd3fd5f5aef399eedc17ae404ea5438b719 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Fri, 17 May 2019 15:56:04 +0200 Subject: [PATCH 077/232] Bump pytraccar (#23939) --- homeassistant/components/traccar/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/traccar/manifest.json b/homeassistant/components/traccar/manifest.json index 0f9aa6e8464..15b78d0ec7b 100644 --- a/homeassistant/components/traccar/manifest.json +++ b/homeassistant/components/traccar/manifest.json @@ -3,7 +3,7 @@ "name": "Traccar", "documentation": "https://www.home-assistant.io/components/traccar", "requirements": [ - "pytraccar==0.8.0", + "pytraccar==0.9.0", "stringcase==1.2.0" ], "dependencies": [], diff --git a/requirements_all.txt b/requirements_all.txt index 1bbb6d8d308..a2554f55c35 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1453,7 +1453,7 @@ pytile==2.0.6 pytouchline==0.7 # homeassistant.components.traccar -pytraccar==0.8.0 +pytraccar==0.9.0 # homeassistant.components.trackr pytrackr==0.0.5 From 33ed017851830dffbaba66a98341b7076ed87b70 Mon Sep 17 00:00:00 2001 From: SiliconAvatar <43505983+SiliconAvatar@users.noreply.github.com> Date: Fri, 17 May 2019 17:02:56 -0400 Subject: [PATCH 078/232] Add unit of measurement to Tautulli sensor (#23873) Adds unit of measurement ("Watching") to sensor, so it can be graphed properly. This is the same unit of measurement as the Plex sensor. --- homeassistant/components/tautulli/sensor.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/tautulli/sensor.py b/homeassistant/components/tautulli/sensor.py index ca1651eca68..d30dafd8da4 100644 --- a/homeassistant/components/tautulli/sensor.py +++ b/homeassistant/components/tautulli/sensor.py @@ -119,6 +119,11 @@ class TautulliSensor(Entity): """Return the icon of the sensor.""" return 'mdi:plex' + @property + def unit_of_measurement(self): + """Return the unit this state is expressed in.""" + return "Watching" + @property def device_state_attributes(self): """Return attributes for the sensor.""" From 4a70c725b46331cb6010d68227136e43e43a6613 Mon Sep 17 00:00:00 2001 From: Julien Brochet Date: Sat, 18 May 2019 02:17:26 +0200 Subject: [PATCH 079/232] Setup integration dependencies before loading it (#23957) --- homeassistant/scripts/check_config.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/scripts/check_config.py b/homeassistant/scripts/check_config.py index 27b2738871c..c06a5da4207 100644 --- a/homeassistant/scripts/check_config.py +++ b/homeassistant/scripts/check_config.py @@ -343,12 +343,6 @@ async def check_ha_config_file(hass): result.add_error("Integration not found: {}".format(domain)) continue - try: - component = integration.get_component() - except ImportError: - result.add_error("Component not found: {}".format(domain)) - continue - if (not hass.config.skip_pip and integration.requirements and not await requirements.async_process_requirements( hass, integration.domain, integration.requirements)): @@ -356,6 +350,12 @@ async def check_ha_config_file(hass): ', '.join(integration.requirements))) continue + try: + component = integration.get_component() + except ImportError: + result.add_error("Component not found: {}".format(domain)) + continue + if hasattr(component, 'CONFIG_SCHEMA'): try: config = component.CONFIG_SCHEMA(config) From c483e4479e9407a7666685d92581ba611ff7e1db Mon Sep 17 00:00:00 2001 From: Josef Schlehofer Date: Sat, 18 May 2019 03:41:22 +0200 Subject: [PATCH 080/232] Update requests to 2.22.0 (#23958) --- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a4a08af1236..b81f7423f30 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ pip>=8.0.3 python-slugify==3.0.2 pytz>=2019.01 pyyaml>=3.13,<4 -requests==2.21.0 +requests==2.22.0 ruamel.yaml==0.15.94 voluptuous==0.11.5 voluptuous-serialize==2.1.0 diff --git a/requirements_all.txt b/requirements_all.txt index a2554f55c35..c8141147514 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -12,7 +12,7 @@ pip>=8.0.3 python-slugify==3.0.2 pytz>=2019.01 pyyaml>=3.13,<4 -requests==2.21.0 +requests==2.22.0 ruamel.yaml==0.15.94 voluptuous==0.11.5 voluptuous-serialize==2.1.0 diff --git a/setup.py b/setup.py index 4de6fa2f042..b1b66e0ca01 100755 --- a/setup.py +++ b/setup.py @@ -46,7 +46,7 @@ REQUIRES = [ 'python-slugify==3.0.2', 'pytz>=2019.01', 'pyyaml>=3.13,<4', - 'requests==2.21.0', + 'requests==2.22.0', 'ruamel.yaml==0.15.94', 'voluptuous==0.11.5', 'voluptuous-serialize==2.1.0', From 05146badf1d918194bcde65f9580b193cc78ccd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20Schr=C3=B6ter?= Date: Sat, 18 May 2019 10:27:05 +0200 Subject: [PATCH 081/232] show battery level also when vacuum has no map support (#23947) --- homeassistant/components/neato/vacuum.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/neato/vacuum.py b/homeassistant/components/neato/vacuum.py index 061d8fd04c8..8bbf07f2091 100644 --- a/homeassistant/components/neato/vacuum.py +++ b/homeassistant/components/neato/vacuum.py @@ -159,6 +159,8 @@ class NeatoConnectedVacuum(StateVacuumDevice): self._clean_state = STATE_ERROR self._status_state = ERRORS.get(self._state['error']) + self._battery_level = self._state['details']['charge'] + if not self._mapdata.get(self._robot_serial, {}).get('maps', []): return self.clean_time_start = ( @@ -182,8 +184,6 @@ class NeatoConnectedVacuum(StateVacuumDevice): self.clean_battery_end = ( self._mapdata[self._robot_serial]['maps'][0]['run_charge_at_end']) - self._battery_level = self._state['details']['charge'] - if self._robot_has_map: if self._state['availableServices']['maps'] != "basic-1": if self._robot_maps[self._robot_serial]: From a8286535ebddad9ef16befd9384773ff4a3b70d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Sat, 18 May 2019 11:01:31 +0200 Subject: [PATCH 082/232] Upate xiaomi voltage parser, fix #23898 (#23962) --- homeassistant/components/xiaomi_aqara/__init__.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/xiaomi_aqara/__init__.py b/homeassistant/components/xiaomi_aqara/__init__.py index 22a8ec95c33..2ae69e3b58c 100644 --- a/homeassistant/components/xiaomi_aqara/__init__.py +++ b/homeassistant/components/xiaomi_aqara/__init__.py @@ -294,11 +294,16 @@ class XiaomiDevice(Entity): def parse_voltage(self, data): """Parse battery level data sent by gateway.""" - if 'voltage' not in data: + if 'voltage' in data: + voltage_key = 'voltage' + elif 'battery_voltage' in data: + voltage_key = 'battery_voltage' + else: return False + max_volt = 3300 min_volt = 2800 - voltage = data['voltage'] + voltage = data[voltage_key] voltage = min(voltage, max_volt) voltage = max(voltage, min_volt) percent = ((voltage - min_volt) / (max_volt - min_volt)) * 100 From 10a1b156e3878d6223642666ad8b1e0ba69d0514 Mon Sep 17 00:00:00 2001 From: Matt Snyder Date: Sat, 18 May 2019 14:46:00 -0500 Subject: [PATCH 083/232] Doorbird Refactor (#23892) * Remove schedule management. Allow custom HTTP events defined in the configuration * Consolidate doorbird request handling. Make token a per device configuration item. * Lint fixes * Do not register dummy listener * Remove punctuation --- homeassistant/components/doorbird/__init__.py | 267 +++++------------- 1 file changed, 66 insertions(+), 201 deletions(-) diff --git a/homeassistant/components/doorbird/__init__.py b/homeassistant/components/doorbird/__init__.py index 477d96770bc..62d3584603a 100644 --- a/homeassistant/components/doorbird/__init__.py +++ b/homeassistant/components/doorbird/__init__.py @@ -6,8 +6,8 @@ import voluptuous as vol from homeassistant.components.http import HomeAssistantView from homeassistant.const import ( - CONF_DEVICES, CONF_HOST, CONF_MONITORED_CONDITIONS, CONF_NAME, - CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME) + CONF_DEVICES, CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_TOKEN, + CONF_USERNAME) import homeassistant.helpers.config_validation as cv from homeassistant.util import dt as dt_util, slugify @@ -18,25 +18,7 @@ DOMAIN = 'doorbird' API_URL = '/api/{}'.format(DOMAIN) CONF_CUSTOM_URL = 'hass_url_override' -CONF_DOORBELL_EVENTS = 'doorbell_events' -CONF_DOORBELL_NUMS = 'doorbell_numbers' -CONF_RELAY_NUMS = 'relay_numbers' -CONF_MOTION_EVENTS = 'motion_events' - -SENSOR_TYPES = { - 'doorbell': { - 'name': 'Button', - 'device_class': 'occupancy', - }, - 'motion': { - 'name': 'Motion', - 'device_class': 'motion', - }, - 'relay': { - 'name': 'Relay', - 'device_class': 'relay', - } -} +CONF_EVENTS = 'events' RESET_DEVICE_FAVORITES = 'doorbird_reset_favorites' @@ -44,19 +26,15 @@ DEVICE_SCHEMA = vol.Schema({ vol.Required(CONF_HOST): cv.string, vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_DOORBELL_NUMS, default=[1]): vol.All( - cv.ensure_list, [cv.positive_int]), - vol.Optional(CONF_RELAY_NUMS, default=[1]): vol.All( - cv.ensure_list, [cv.positive_int]), + vol.Required(CONF_TOKEN): cv.string, + vol.Optional(CONF_EVENTS, default=[]): vol.All( + cv.ensure_list, [cv.string]), vol.Optional(CONF_CUSTOM_URL): cv.string, - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_MONITORED_CONDITIONS, default=[]): - vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), + vol.Optional(CONF_NAME): cv.string }) CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ - vol.Required(CONF_TOKEN): cv.string, vol.Required(CONF_DEVICES): vol.All(cv.ensure_list, [DEVICE_SCHEMA]) }), }, extra=vol.ALLOW_EXTRA) @@ -66,13 +44,8 @@ def setup(hass, config): """Set up the DoorBird component.""" from doorbirdpy import DoorBird - token = config[DOMAIN].get(CONF_TOKEN) - # Provide an endpoint for the doorstations to call to trigger events - hass.http.register_view(DoorBirdRequestView(token)) - - # Provide an endpoint for the user to call to clear device changes - hass.http.register_view(DoorBirdCleanupView(token)) + hass.http.register_view(DoorBirdRequestView) doorstations = [] @@ -80,10 +53,9 @@ def setup(hass, config): device_ip = doorstation_config.get(CONF_HOST) username = doorstation_config.get(CONF_USERNAME) password = doorstation_config.get(CONF_PASSWORD) - doorbell_nums = doorstation_config.get(CONF_DOORBELL_NUMS) - relay_nums = doorstation_config.get(CONF_RELAY_NUMS) custom_url = doorstation_config.get(CONF_CUSTOM_URL) - events = doorstation_config.get(CONF_MONITORED_CONDITIONS) + events = doorstation_config.get(CONF_EVENTS) + token = doorstation_config.get(CONF_TOKEN) name = (doorstation_config.get(CONF_NAME) or 'DoorBird {}'.format(index + 1)) @@ -92,7 +64,7 @@ def setup(hass, config): if status[0]: doorstation = ConfiguredDoorBird(device, name, events, custom_url, - doorbell_nums, relay_nums, token) + token) doorstations.append(doorstation) _LOGGER.info('Connected to DoorBird "%s" as %s@%s', doorstation.name, username, device_ip) @@ -108,7 +80,7 @@ def setup(hass, config): # Subscribe to doorbell or motion events if events: try: - doorstation.update_schedule(hass) + doorstation.register_events(hass) except HTTPError: hass.components.persistent_notification.create( 'Doorbird configuration failed. Please verify that API ' @@ -124,15 +96,15 @@ def setup(hass, config): def _reset_device_favorites_handler(event): """Handle clearing favorites on device.""" - slug = event.data.get('slug') + token = event.data.get('token') - if slug is None: + if token is None: return - doorstation = get_doorstation_by_slug(hass, slug) + doorstation = get_doorstation_by_token(hass, token) if doorstation is None: - _LOGGER.error('Device not found %s', format(slug)) + _LOGGER.error('Device not found for provided token.') # Clear webhooks favorites = doorstation.device.favorites() @@ -146,30 +118,22 @@ def setup(hass, config): return True -def get_doorstation_by_slug(hass, slug): +def get_doorstation_by_token(hass, token): """Get doorstation by slug.""" for doorstation in hass.data[DOMAIN]: - if slugify(doorstation.name) in slug: + if token == doorstation.token: return doorstation -def handle_event(event): - """Handle dummy events.""" - return None - - class ConfiguredDoorBird(): """Attach additional information to pass along with configured device.""" - def __init__(self, device, name, events, custom_url, doorbell_nums, - relay_nums, token): + def __init__(self, device, name, events, custom_url, token): """Initialize configured device.""" self._name = name self._device = device self._custom_url = custom_url - self._monitored_events = events - self._doorbell_nums = doorbell_nums - self._relay_nums = relay_nums + self._events = events self._token = token @property @@ -187,14 +151,13 @@ class ConfiguredDoorBird(): """Get custom url for device.""" return self._custom_url - def update_schedule(self, hass): - """Register monitored sensors and deregister others.""" - from doorbirdpy import DoorBirdScheduleEntrySchedule - - # Create a new schedule (24/7) - schedule = DoorBirdScheduleEntrySchedule() - schedule.add_weekday(0, 604800) # seconds in a week + @property + def token(self): + """Get token for device.""" + return self._token + def register_events(self, hass): + """Register events on device.""" # Get the URL of this server hass_url = hass.config.api.base_url @@ -202,98 +165,39 @@ class ConfiguredDoorBird(): if self.custom_url is not None: hass_url = self.custom_url - # For all sensor types (enabled + disabled) - for sensor_type in SENSOR_TYPES: - name = '{} {}'.format(self.name, SENSOR_TYPES[sensor_type]['name']) - slug = slugify(name) + for event in self._events: + event = self._get_event_name(event) - url = '{}{}/{}?token={}'.format(hass_url, API_URL, slug, - self._token) - if sensor_type in self._monitored_events: - # Enabled -> register - self._register_event(url, sensor_type, schedule) - _LOGGER.info('Registered for %s pushes from DoorBird "%s". ' - 'Use the "%s_%s" event for automations.', - sensor_type, self.name, DOMAIN, slug) + self._register_event(hass_url, event) - # Register a dummy listener so event is listed in GUI - hass.bus.listen('{}_{}'.format(DOMAIN, slug), handle_event) - else: - # Disabled -> deregister - self._deregister_event(url, sensor_type) - _LOGGER.info('Deregistered %s pushes from DoorBird "%s". ' - 'If any old favorites or schedules remain, ' - 'follow the instructions in the component ' - 'documentation to clear device registrations.', - sensor_type, self.name) + _LOGGER.info('Successfully registered URL for %s on %s', + event, self.name) - def _register_event(self, hass_url, event, schedule): + @property + def slug(self): + """Get device slug.""" + return slugify(self._name) + + def _get_event_name(self, event): + return '{}_{}'.format(self.slug, event) + + def _register_event(self, hass_url, event): """Add a schedule entry in the device for a sensor.""" - from doorbirdpy import DoorBirdScheduleEntryOutput + url = '{}{}/{}?token={}'.format(hass_url, API_URL, event, self._token) # Register HA URL as webhook if not already, then get the ID - if not self.webhook_is_registered(hass_url): - self.device.change_favorite('http', 'Home Assistant ({} events)' - .format(event), hass_url) + if not self.webhook_is_registered(url): + self.device.change_favorite('http', 'Home Assistant ({})' + .format(event), url) - fav_id = self.get_webhook_id(hass_url) + fav_id = self.get_webhook_id(url) if not fav_id: _LOGGER.warning('Could not find favorite for URL "%s". ' - 'Skipping sensor "%s".', hass_url, event) + 'Skipping sensor "%s"', url, event) return - # Add event handling to device schedule - output = DoorBirdScheduleEntryOutput(event='http', - param=fav_id, - schedule=schedule) - - if event == 'doorbell': - # Repeat edit for each monitored doorbell number - for doorbell in self._doorbell_nums: - entry = self.device.get_schedule_entry(event, str(doorbell)) - entry.output.append(output) - self.device.change_schedule(entry) - elif event == 'relay': - # Repeat edit for each monitored doorbell number - for relay in self._relay_nums: - entry = self.device.get_schedule_entry(event, str(relay)) - entry.output.append(output) - else: - entry = self.device.get_schedule_entry(event) - entry.output.append(output) - self.device.change_schedule(entry) - - def _deregister_event(self, hass_url, event): - """Remove the schedule entry in the device for a sensor.""" - # Find the right favorite and delete it - fav_id = self.get_webhook_id(hass_url) - if not fav_id: - return - - self._device.delete_favorite('http', fav_id) - - if event == 'doorbell': - # Delete the matching schedule for each doorbell number - for doorbell in self._doorbell_nums: - self._delete_schedule_action(event, fav_id, str(doorbell)) - else: - self._delete_schedule_action(event, fav_id) - - def _delete_schedule_action(self, sensor, fav_id, param=""): - """Remove the HA output from a schedule.""" - entries = self._device.schedule() - for entry in entries: - if entry.input != sensor or entry.param != param: - continue - - for action in entry.output: - if action.event == 'http' and action.param == fav_id: - entry.output.remove(action) - - self._device.change_schedule(entry) - - def webhook_is_registered(self, ha_url, favs=None) -> bool: + def webhook_is_registered(self, url, favs=None) -> bool: """Return whether the given URL is registered as a device favorite.""" favs = favs if favs else self.device.favorites() @@ -301,12 +205,12 @@ class ConfiguredDoorBird(): return False for fav in favs['http'].values(): - if fav['value'] == ha_url: + if fav['value'] == url: return True return False - def get_webhook_id(self, ha_url, favs=None) -> str or None: + def get_webhook_id(self, url, favs=None) -> str or None: """ Return the device favorite ID for the given URL. @@ -318,7 +222,7 @@ class ConfiguredDoorBird(): return None for fav_id in favs['http']: - if favs['http'][fav_id]['value'] == ha_url: + if favs['http'][fav_id]['value'] == url: return fav_id return None @@ -340,72 +244,33 @@ class DoorBirdRequestView(HomeAssistantView): requires_auth = False url = API_URL name = API_URL[1:].replace('/', ':') - extra_urls = [API_URL + '/{sensor}'] - - def __init__(self, token): - """Initialize view.""" - HomeAssistantView.__init__(self) - self._token = token + extra_urls = [API_URL + '/{event}'] # pylint: disable=no-self-use - async def get(self, request, sensor): + async def get(self, request, event): """Respond to requests from the device.""" from aiohttp import web hass = request.app['hass'] - request_token = request.query.get('token') + token = request.query.get('token') - authenticated = request_token == self._token + device = get_doorstation_by_token(hass, token) - if request_token == '' or not authenticated: - return web.Response(status=401, text='Unauthorized') + if device is None: + return web.Response(status=401, text='Invalid token provided.') - doorstation = get_doorstation_by_slug(hass, sensor) - - if doorstation: - event_data = doorstation.get_event_data() + if device: + event_data = device.get_event_data() else: event_data = {} - hass.bus.async_fire('{}_{}'.format(DOMAIN, sensor), event_data) + if event == 'clear': + hass.bus.async_fire(RESET_DEVICE_FAVORITES, + {'token': token}) + + message = 'HTTP Favorites cleared for {}'.format(device.slug) + return web.Response(status=200, text=message) + + hass.bus.async_fire('{}_{}'.format(DOMAIN, event), event_data) return web.Response(status=200, text='OK') - - -class DoorBirdCleanupView(HomeAssistantView): - """Provide a URL to call to delete ALL webhooks/schedules.""" - - requires_auth = False - url = API_URL + '/clear/{slug}' - name = 'DoorBird Cleanup' - - def __init__(self, token): - """Initialize view.""" - HomeAssistantView.__init__(self) - self._token = token - - # pylint: disable=no-self-use - async def get(self, request, slug): - """Act on requests.""" - from aiohttp import web - hass = request.app['hass'] - - request_token = request.query.get('token') - - authenticated = request_token == self._token - - if request_token == '' or not authenticated: - return web.Response(status=401, text='Unauthorized') - - device = get_doorstation_by_slug(hass, slug) - - # No matching device - if device is None: - return web.Response(status=404, - text='Device slug {} not found'.format(slug)) - - hass.bus.async_fire(RESET_DEVICE_FAVORITES, - {'slug': slug}) - - message = 'Clearing schedule for {}'.format(slug) - return web.Response(status=200, text=message) From 4583638b924fd9d2c0a982987f2c65f964a5b2c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Sat, 18 May 2019 22:14:12 +0200 Subject: [PATCH 084/232] upgrade broadlink library (#23966) --- homeassistant/components/broadlink/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/broadlink/manifest.json b/homeassistant/components/broadlink/manifest.json index a2c565c3dd5..125a3a83d21 100644 --- a/homeassistant/components/broadlink/manifest.json +++ b/homeassistant/components/broadlink/manifest.json @@ -3,7 +3,7 @@ "name": "Broadlink", "documentation": "https://www.home-assistant.io/components/broadlink", "requirements": [ - "broadlink==0.9.0" + "broadlink==0.10.0" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index c8141147514..7bb322e6071 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -264,7 +264,7 @@ boto3==1.9.16 braviarc-homeassistant==0.3.7.dev0 # homeassistant.components.broadlink -broadlink==0.9.0 +broadlink==0.10.0 # homeassistant.components.brottsplatskartan brottsplatskartan==0.0.1 From a99e15343cce88131741df1c8f5af160192530c2 Mon Sep 17 00:00:00 2001 From: Cyro Date: Sat, 18 May 2019 22:14:54 +0200 Subject: [PATCH 085/232] Make Discord payload data key not required (#23964) --- homeassistant/components/discord/notify.py | 23 +++++++++++----------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/discord/notify.py b/homeassistant/components/discord/notify.py index 5a9cb77877d..2e3d2eee9e9 100644 --- a/homeassistant/components/discord/notify.py +++ b/homeassistant/components/discord/notify.py @@ -53,21 +53,20 @@ class DiscordNotificationService(BaseNotificationService): _LOGGER.error("No target specified") return None - if ATTR_DATA in kwargs: - data = kwargs.get(ATTR_DATA) + data = kwargs.get(ATTR_DATA) or {} - if ATTR_IMAGES in data: - images = list() + if ATTR_IMAGES in data: + images = list() - for image in data.get(ATTR_IMAGES): - image_exists = await self.hass.async_add_executor_job( - self.file_exists, - image) + for image in data.get(ATTR_IMAGES): + image_exists = await self.hass.async_add_executor_job( + self.file_exists, + image) - if image_exists: - images.append(image) - else: - _LOGGER.warning("Image not found: %s", image) + if image_exists: + images.append(image) + else: + _LOGGER.warning("Image not found: %s", image) # pylint: disable=unused-variable @discord_bot.event From 91ba35c68edafac8d5e15d18da1c4427afd003d5 Mon Sep 17 00:00:00 2001 From: Martin Donlon Date: Sat, 18 May 2019 13:56:34 -0700 Subject: [PATCH 086/232] Update russound_rio dependency to version 0.1.7 (#23973) v0.1.7 fixes async import issues in python 3.7+ --- homeassistant/components/russound_rio/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/russound_rio/manifest.json b/homeassistant/components/russound_rio/manifest.json index af81d9c031a..4667e9b8314 100644 --- a/homeassistant/components/russound_rio/manifest.json +++ b/homeassistant/components/russound_rio/manifest.json @@ -3,7 +3,7 @@ "name": "Russound rio", "documentation": "https://www.home-assistant.io/components/russound_rio", "requirements": [ - "russound_rio==0.1.4" + "russound_rio==0.1.7" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index 7bb322e6071..4cb94a4aeb7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1564,7 +1564,7 @@ rova==0.1.0 russound==0.1.9 # homeassistant.components.russound_rio -russound_rio==0.1.4 +russound_rio==0.1.7 # homeassistant.components.yamaha rxv==0.6.0 From 57bd4185d42f9df0eef701926c7749dfb6096bf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Sat, 18 May 2019 22:59:33 +0200 Subject: [PATCH 087/232] Fixes issue with multiple alerts (#23945) * Fixes issue with multiple alerts * Adds missing new line * Remove whitespace --- homeassistant/components/alert/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/alert/__init__.py b/homeassistant/components/alert/__init__.py index 4c990d62d4b..88217b026fd 100644 --- a/homeassistant/components/alert/__init__.py +++ b/homeassistant/components/alert/__init__.py @@ -1,7 +1,7 @@ """Support for repeating alerts when conditions are met.""" import asyncio import logging -from datetime import datetime, timedelta +from datetime import timedelta import voluptuous as vol @@ -13,6 +13,7 @@ from homeassistant.const import ( SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE, ATTR_ENTITY_ID) from homeassistant.helpers import service, event from homeassistant.helpers.entity import ToggleEntity +from homeassistant.util.dt import now _LOGGER = logging.getLogger(__name__) @@ -222,7 +223,7 @@ class Alert(ToggleEntity): async def _schedule_notify(self): """Schedule a notification.""" delay = self._delay[self._next_delay] - next_msg = datetime.now() + delay + next_msg = now() + delay self._cancel = \ event.async_track_point_in_time(self.hass, self._notify, next_msg) self._next_delay = min(self._next_delay + 1, len(self._delay) - 1) From eebd094423f6c0554a7eb9f39de7442f3e225655 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A1s=20Rutkai?= Date: Sat, 18 May 2019 23:05:59 +0200 Subject: [PATCH 088/232] Adding Watson TTS (IBM Cloud) (#23299) * Adding Watson TTS (IBM Cloud) * Code review changes --- .coveragerc | 1 + CODEOWNERS | 1 + .../components/watson_tts/__init__.py | 1 + .../components/watson_tts/manifest.json | 12 ++ homeassistant/components/watson_tts/tts.py | 137 ++++++++++++++++++ requirements_all.txt | 3 + 6 files changed, 155 insertions(+) create mode 100644 homeassistant/components/watson_tts/__init__.py create mode 100644 homeassistant/components/watson_tts/manifest.json create mode 100644 homeassistant/components/watson_tts/tts.py diff --git a/.coveragerc b/.coveragerc index 353aeaf5684..20bbc0474eb 100644 --- a/.coveragerc +++ b/.coveragerc @@ -651,6 +651,7 @@ omit = homeassistant/components/waqi/sensor.py homeassistant/components/waterfurnace/* homeassistant/components/watson_iot/* + homeassistant/components/watson_tts/tts.py homeassistant/components/waze_travel_time/sensor.py homeassistant/components/webostv/* homeassistant/components/wemo/* diff --git a/CODEOWNERS b/CODEOWNERS index ea0840a2519..cf71e61ead6 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -254,6 +254,7 @@ homeassistant/components/velux/* @Julius2342 homeassistant/components/version/* @fabaff homeassistant/components/vizio/* @raman325 homeassistant/components/waqi/* @andrey-git +homeassistant/components/watson_tts/* @rutkai homeassistant/components/weather/* @fabaff homeassistant/components/weblink/* @home-assistant/core homeassistant/components/websocket_api/* @home-assistant/core diff --git a/homeassistant/components/watson_tts/__init__.py b/homeassistant/components/watson_tts/__init__.py new file mode 100644 index 00000000000..abdc9308ca3 --- /dev/null +++ b/homeassistant/components/watson_tts/__init__.py @@ -0,0 +1 @@ +"""Support for IBM Watson TTS integration.""" diff --git a/homeassistant/components/watson_tts/manifest.json b/homeassistant/components/watson_tts/manifest.json new file mode 100644 index 00000000000..d40baaca132 --- /dev/null +++ b/homeassistant/components/watson_tts/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "watson_tts", + "name": "IBM Watson TTS", + "documentation": "https://www.home-assistant.io/components/watson_tts", + "requirements": [ + "ibm-watson==3.0.3" + ], + "dependencies": [], + "codeowners": [ + "@rutkai" + ] +} diff --git a/homeassistant/components/watson_tts/tts.py b/homeassistant/components/watson_tts/tts.py new file mode 100644 index 00000000000..be60908d096 --- /dev/null +++ b/homeassistant/components/watson_tts/tts.py @@ -0,0 +1,137 @@ +"""Support for IBM Watson TTS integration.""" +import logging + +import voluptuous as vol + +from homeassistant.components.tts import PLATFORM_SCHEMA, Provider +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +CONF_URL = 'watson_url' +CONF_APIKEY = 'watson_apikey' +ATTR_CREDENTIALS = 'credentials' + +DEFAULT_URL = 'https://stream.watsonplatform.net/text-to-speech/api' + +CONF_VOICE = 'voice' +CONF_OUTPUT_FORMAT = 'output_format' +CONF_TEXT_TYPE = 'text' + +# List from https://tinyurl.com/watson-tts-docs +SUPPORTED_VOICES = [ + "de-DE_BirgitVoice", + "de-DE_BirgitV2Voice", + "de-DE_DieterVoice", + "de-DE_DieterV2Voice" + "en-GB_KateVoice", + "en-US_AllisonVoice", + "en-US_AllisonV2Voice", + "en-US_LisaVoice", + "en-US_LisaV2Voice", + "en-US_MichaelVoice", + "en-US_MichaelV2Voice", + "es-ES_EnriqueVoice", + "es-ES_LauraVoice", + "es-LA_SofiaVoice", + "es-US_SofiaVoice", + "fr-FR_ReneeVoice", + "it-IT_FrancescaVoice", + "it-IT_FrancescaV2Voice", + "ja-JP_EmiVoice", + "pt-BR_IsabelaVoice" +] + +SUPPORTED_OUTPUT_FORMATS = [ + 'audio/flac', + 'audio/mp3', + 'audio/mpeg', + 'audio/ogg', + 'audio/ogg;codecs=opus', + 'audio/ogg;codecs=vorbis', + 'audio/wav' +] + +CONTENT_TYPE_EXTENSIONS = { + 'audio/flac': 'flac', + 'audio/mp3': 'mp3', + 'audio/mpeg': 'mp3', + 'audio/ogg': 'ogg', + 'audio/ogg;codecs=opus': 'ogg', + 'audio/ogg;codecs=vorbis': 'ogg', + 'audio/wav': 'wav', +} + +DEFAULT_VOICE = 'en-US_AllisonVoice' +DEFAULT_OUTPUT_FORMAT = 'audio/mp3' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_URL, default=DEFAULT_URL): cv.string, + vol.Required(CONF_APIKEY): cv.string, + vol.Optional(CONF_VOICE, default=DEFAULT_VOICE): vol.In(SUPPORTED_VOICES), + vol.Optional(CONF_OUTPUT_FORMAT, default=DEFAULT_OUTPUT_FORMAT): + vol.In(SUPPORTED_OUTPUT_FORMATS), +}) + + +def get_engine(hass, config): + """Set up IBM Watson TTS component.""" + from ibm_watson import TextToSpeechV1 + + service = TextToSpeechV1( + url=config[CONF_URL], + iam_apikey=config[CONF_APIKEY] + ) + + supported_languages = list({s[:5] for s in SUPPORTED_VOICES}) + default_voice = config[CONF_VOICE] + output_format = config[CONF_OUTPUT_FORMAT] + + return WatsonTTSProvider( + service, supported_languages, default_voice, output_format) + + +class WatsonTTSProvider(Provider): + """IBM Watson TTS api provider.""" + + def __init__(self, + service, + supported_languages, + default_voice, + output_format): + """Initialize Watson TTS provider.""" + self.service = service + self.supported_langs = supported_languages + self.default_lang = default_voice[:5] + self.default_voice = default_voice + self.output_format = output_format + self.name = 'Watson TTS' + + @property + def supported_languages(self): + """Return a list of supported languages.""" + return self.supported_langs + + @property + def default_language(self): + """Return the default language.""" + return self.default_lang + + @property + def default_options(self): + """Return dict include default options.""" + return {CONF_VOICE: self.default_voice} + + @property + def supported_options(self): + """Return a list of supported options.""" + return [CONF_VOICE] + + def get_tts_audio(self, message, language=None, options=None): + """Request TTS file from Watson TTS.""" + response = self.service.synthesize( + message, accept=self.output_format, + voice=self.default_voice).get_result() + + return (CONTENT_TYPE_EXTENSIONS[self.output_format], + response.content) diff --git a/requirements_all.txt b/requirements_all.txt index 4cb94a4aeb7..2730695cf96 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -595,6 +595,9 @@ hydrawiser==0.1.1 # homeassistant.components.htu21d # i2csense==0.0.4 +# homeassistant.components.watson_tts +ibm-watson==3.0.3 + # homeassistant.components.watson_iot ibmiotf==0.3.4 From 1282370ccbe10b3fe18f5d47090cf3a830985ddf Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Sun, 19 May 2019 05:14:11 -0400 Subject: [PATCH 089/232] Entity Cleanup on Z-Wave node removal (#23633) * Initial groundwork for entity cleanup on node removal * Connect node_removed to dispatcher * update docstring * Add node_removal test * Address review comments * Use hass.add_job instead of run_coroutine_threadsafe --- homeassistant/components/zwave/__init__.py | 21 ++++++++++ homeassistant/components/zwave/node_entity.py | 11 +++++ tests/components/zwave/test_init.py | 42 +++++++++++++++++++ 3 files changed, 74 insertions(+) diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index 741c6f852a8..10046825ad3 100644 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -376,6 +376,25 @@ async def async_setup_entry(hass, config_entry): hass.add_job(check_has_unique_id, entity, _on_ready, _on_timeout, hass.loop) + def node_removed(node): + node_id = node.node_id + node_key = 'node-{}'.format(node_id) + _LOGGER.info("Node Removed: %s", + hass.data[DATA_DEVICES][node_key]) + for key in list(hass.data[DATA_DEVICES]): + if not key.startswith('{}-'.format(node_id)): + continue + + entity = hass.data[DATA_DEVICES][key] + _LOGGER.info('Removing Entity - value: %s - entity_id: %s', + key, entity.entity_id) + hass.add_job(entity.node_removed()) + del hass.data[DATA_DEVICES][key] + + entity = hass.data[DATA_DEVICES][node_key] + hass.add_job(entity.node_removed()) + del hass.data[DATA_DEVICES][node_key] + def network_ready(): """Handle the query of all awake nodes.""" _LOGGER.info("Z-Wave network is ready for use. All awake nodes " @@ -399,6 +418,8 @@ async def async_setup_entry(hass, config_entry): value_added, ZWaveNetwork.SIGNAL_VALUE_ADDED, weak=False) dispatcher.connect( node_added, ZWaveNetwork.SIGNAL_NODE_ADDED, weak=False) + dispatcher.connect( + node_removed, ZWaveNetwork.SIGNAL_NODE_REMOVED, weak=False) dispatcher.connect( network_ready, ZWaveNetwork.SIGNAL_AWAKE_NODES_QUERIED, weak=False) dispatcher.connect( diff --git a/homeassistant/components/zwave/node_entity.py b/homeassistant/components/zwave/node_entity.py index 2339b8aba36..0a24f888c20 100644 --- a/homeassistant/components/zwave/node_entity.py +++ b/homeassistant/components/zwave/node_entity.py @@ -3,6 +3,7 @@ import logging from homeassistant.core import callback from homeassistant.const import ATTR_BATTERY_LEVEL, ATTR_WAKEUP, ATTR_ENTITY_ID +from homeassistant.helpers.entity_registry import async_get_registry from homeassistant.helpers.entity import Entity from .const import ( @@ -74,6 +75,16 @@ class ZWaveBaseEntity(Entity): if self.hass and self.platform: self.hass.add_job(_async_remove_and_add) + async def node_removed(self): + """Call when a node is removed from the Z-Wave network.""" + await self.async_remove() + + registry = await async_get_registry(self.hass) + if self.entity_id not in registry.entities: + return + + registry.async_remove(self.entity_id) + class ZWaveNodeEntity(ZWaveBaseEntity): """Representation of a Z-Wave node.""" diff --git a/tests/components/zwave/test_init.py b/tests/components/zwave/test_init.py index 7fc9f55cf03..69ee7c45a9b 100644 --- a/tests/components/zwave/test_init.py +++ b/tests/components/zwave/test_init.py @@ -226,6 +226,48 @@ async def test_device_entity(hass, mock_openzwave): assert device.device_state_attributes[zwave.ATTR_POWER] == 50.123 +async def test_node_removed(hass, mock_openzwave): + """Test node removed in base class.""" + # Create a mock node & node entity + node = MockNode(node_id='10', name='Mock Node') + value = MockValue(data=False, node=node, instance=2, object_id='11', + label='Sensor', + command_class=const.COMMAND_CLASS_SENSOR_BINARY) + power_value = MockValue(data=50.123456, node=node, precision=3, + command_class=const.COMMAND_CLASS_METER) + values = MockEntityValues(primary=value, power=power_value) + device = zwave.ZWaveDeviceEntity(values, 'zwave') + device.hass = hass + device.entity_id = 'zwave.mock_node' + device.value_added() + device.update_properties() + await hass.async_block_till_done() + + # Save it to the entity registry + registry = mock_registry(hass) + registry.async_get_or_create('zwave', 'zwave', device.unique_id) + device.entity_id = registry.async_get_entity_id( + 'zwave', 'zwave', device.unique_id) + + # Create dummy entity registry entries for other integrations + hue_entity = registry.async_get_or_create('light', 'hue', 1234) + zha_entity = registry.async_get_or_create('sensor', 'zha', 5678) + + # Verify our Z-Wave entity is registered + assert registry.async_is_registered(device.entity_id) + + # Remove it + entity_id = device.entity_id + await device.node_removed() + + # Verify registry entry for our Z-Wave node is gone + assert not registry.async_is_registered(entity_id) + + # Verify registry entries for our other entities remain + assert registry.async_is_registered(hue_entity.entity_id) + assert registry.async_is_registered(zha_entity.entity_id) + + async def test_node_discovery(hass, mock_openzwave): """Test discovery of a node.""" mock_receivers = [] From ff867a7d5760c5c833ec79a96230aa90703880f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Sun, 19 May 2019 11:23:55 +0200 Subject: [PATCH 090/232] Use the timezone defined in Home Assistant when making the API call (#23284) * Use HA defined timezone * Cleanup * Use homeassistant.util.dt.now to get the correct time. * Update homeassistant/components/vasttrafik/sensor.py Co-Authored-By: Paulus Schoutsen --- homeassistant/components/vasttrafik/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/vasttrafik/sensor.py b/homeassistant/components/vasttrafik/sensor.py index 45279fa8933..174395f5f3f 100644 --- a/homeassistant/components/vasttrafik/sensor.py +++ b/homeassistant/components/vasttrafik/sensor.py @@ -1,5 +1,4 @@ """Support for Västtrafik public transport.""" -from datetime import datetime from datetime import timedelta import logging @@ -10,6 +9,7 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_NAME, ATTR_ATTRIBUTION from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle +from homeassistant.util.dt import now _LOGGER = logging.getLogger(__name__) @@ -107,7 +107,7 @@ class VasttrafikDepartureSensor(Entity): self._departureboard = self._planner.departureboard( self._departure['id'], direction=self._heading['id'] if self._heading else None, - date=datetime.now()+self._delay) + date=now()+self._delay) except self._vasttrafik.Error: _LOGGER.debug("Unable to read departure board, updating token") self._planner.update_token() From d7d83c683d30b2336f85636c21af60ec8e95d77c Mon Sep 17 00:00:00 2001 From: Tomer Figenblat Date: Sun, 19 May 2019 12:24:59 +0300 Subject: [PATCH 091/232] Updated non-blocking timout to 10 seconds for fixing timeout issues. (#23930) * Updated non-blocking timout to 10 seconds for fixing timeout issues. * Added failed bridge fixture for faster unit tests. --- .../components/switcher_kis/__init__.py | 2 +- tests/components/switcher_kis/conftest.py | 22 +++++++++++++++++++ tests/components/switcher_kis/test_init.py | 4 +++- 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/switcher_kis/__init__.py b/homeassistant/components/switcher_kis/__init__.py index 43ca0abc2a0..9fb51992cd2 100644 --- a/homeassistant/components/switcher_kis/__init__.py +++ b/homeassistant/components/switcher_kis/__init__.py @@ -63,7 +63,7 @@ async def async_setup(hass: HomeAssistantType, config: Dict) -> bool: try: device_data = await wait_for( - v2bridge.queue.get(), timeout=5.0, loop=hass.loop) + v2bridge.queue.get(), timeout=10.0, loop=hass.loop) except (Asyncio_TimeoutError, RuntimeError): _LOGGER.exception("failed to get response from device") await v2bridge.stop() diff --git a/tests/components/switcher_kis/conftest.py b/tests/components/switcher_kis/conftest.py index d0398d448e9..9f961f72401 100644 --- a/tests/components/switcher_kis/conftest.py +++ b/tests/components/switcher_kis/conftest.py @@ -108,3 +108,25 @@ def mock_bridge_fixture() -> Generator[None, Any, None]: for patcher in patchers: patcher.stop() + + +@fixture(name='mock_failed_bridge') +def mock_failed_bridge_fixture() -> Generator[None, Any, None]: + """Fixture for mocking aioswitcher.bridge.SwitcherV2Bridge.""" + async def mock_queue(): + """Mock asyncio's Queue.""" + raise RuntimeError + + patchers = [ + patch('aioswitcher.bridge.SwitcherV2Bridge.start', return_value=None), + patch('aioswitcher.bridge.SwitcherV2Bridge.stop', return_value=None), + patch('aioswitcher.bridge.SwitcherV2Bridge.queue', get=mock_queue) + ] + + for patcher in patchers: + patcher.start() + + yield + + for patcher in patchers: + patcher.stop() diff --git a/tests/components/switcher_kis/test_init.py b/tests/components/switcher_kis/test_init.py index 0defb113747..33d24903f94 100644 --- a/tests/components/switcher_kis/test_init.py +++ b/tests/components/switcher_kis/test_init.py @@ -13,7 +13,9 @@ from .consts import ( DUMMY_REMAINING_TIME, MANDATORY_CONFIGURATION) -async def test_failed_config(hass: HomeAssistantType) -> None: +async def test_failed_config( + hass: HomeAssistantType, + mock_failed_bridge: Generator[None, Any, None]) -> None: """Test failed configuration.""" assert await async_setup_component( hass, DOMAIN, MANDATORY_CONFIGURATION) is False From f991ec15f2ab8998d220ceb648c25e2913e5aeb3 Mon Sep 17 00:00:00 2001 From: Penny Wood Date: Sun, 19 May 2019 17:41:39 +0800 Subject: [PATCH 092/232] Delete devices / entities when we remove a config entry. (#23983) * Remove device when last config entry removed * Remove entities when config entry removed * Update tests to use new behaviour --- homeassistant/helpers/device_registry.py | 15 ++++++++++++++- homeassistant/helpers/entity_registry.py | 8 +++++--- tests/helpers/test_device_registry.py | 6 +++--- tests/helpers/test_entity_registry.py | 5 ++--- tests/test_config_entries.py | 6 +++--- 5 files changed, 27 insertions(+), 13 deletions(-) diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 5c066967437..d090e571a8b 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -219,6 +219,14 @@ class DeviceRegistry: return new + def _async_remove_device(self, device_id): + del self.devices[device_id] + self.hass.bus.async_fire(EVENT_DEVICE_REGISTRY_UPDATED, { + 'action': 'remove', + 'device_id': device_id, + }) + self.async_schedule_save() + async def async_load(self): """Load the device registry.""" data = await self._store.async_load() @@ -278,10 +286,15 @@ class DeviceRegistry: @callback def async_clear_config_entry(self, config_entry_id): """Clear config entry from registry entries.""" + remove = [] for dev_id, device in self.devices.items(): - if config_entry_id in device.config_entries: + if device.config_entries == {config_entry_id}: + remove.append(dev_id) + else: self._async_update_device( dev_id, remove_config_entry_id=config_entry_id) + for dev_id in remove: + self._async_remove_device(dev_id) @callback def async_clear_area_id(self, area_id: str) -> None: diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 0a0c441b9cf..2fb32d5214e 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -302,9 +302,11 @@ class EntityRegistry: @callback def async_clear_config_entry(self, config_entry): """Clear config entry from registry entries.""" - for entity_id, entry in self.entities.items(): - if config_entry == entry.config_entry_id: - self._async_update_entity(entity_id, config_entry_id=None) + for entity_id in [ + entity_id + for entity_id, entry in self.entities.items() + if config_entry == entry.config_entry_id]: + self.async_remove(entity_id) @bind_hass diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index 4b08bf960bf..444bd44133b 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -211,10 +211,10 @@ async def test_removing_config_entries(hass, registry, update_events): registry.async_clear_config_entry('123') entry = registry.async_get_device({('bridgeid', '0123')}, set()) - entry3 = registry.async_get_device({('bridgeid', '4567')}, set()) + entry3_removed = registry.async_get_device({('bridgeid', '4567')}, set()) assert entry.config_entries == {'456'} - assert entry3.config_entries == set() + assert entry3_removed is None await hass.async_block_till_done() @@ -227,7 +227,7 @@ async def test_removing_config_entries(hass, registry, update_events): assert update_events[2]['device_id'] == entry3.id assert update_events[3]['action'] == 'update' assert update_events[3]['device_id'] == entry.id - assert update_events[4]['action'] == 'update' + assert update_events[4]['action'] == 'remove' assert update_events[4]['device_id'] == entry3.id diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index 3af9394a202..61d3af6e6f2 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -213,15 +213,14 @@ async def test_removing_config_entry_id(hass, registry, update_events): assert entry.config_entry_id == 'mock-id-1' registry.async_clear_config_entry('mock-id-1') - entry = registry.entities[entry.entity_id] - assert entry.config_entry_id is None + assert not registry.entities await hass.async_block_till_done() assert len(update_events) == 2 assert update_events[0]['action'] == 'create' assert update_events[0]['entity_id'] == entry.entity_id - assert update_events[1]['action'] == 'update' + assert update_events[1]['action'] == 'remove' assert update_events[1]['entity_id'] == entry.entity_id diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 9de31a6d5ca..752cb5eb277 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -261,9 +261,9 @@ async def test_remove_entry(hass, manager): # Just Group all_lights assert len(hass.states.async_all()) == 1 - # Check that entity registry entry no longer references config_entry_id - entity_entry = list(ent_reg.entities.values())[0] - assert entity_entry.config_entry_id is None + # Check that entity registry entry has been removed + entity_entry_list = list(ent_reg.entities.values()) + assert not entity_entry_list async def test_remove_entry_handles_callback_error(hass, manager): From e356d0bcda962ed4074f3f15a78d68d8c471bef6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 19 May 2019 12:01:29 +0200 Subject: [PATCH 093/232] Better handle file not found when loading YAML (#23908) * Better handle file not found * Lint --- homeassistant/components/apns/notify.py | 6 ++++-- homeassistant/components/http/ban.py | 6 ++---- homeassistant/config.py | 8 +++----- homeassistant/scripts/check_config.py | 2 ++ 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/apns/notify.py b/homeassistant/components/apns/notify.py index 365bdbcb4f5..ccf7c495f39 100644 --- a/homeassistant/components/apns/notify.py +++ b/homeassistant/components/apns/notify.py @@ -1,6 +1,5 @@ """APNS Notification platform.""" import logging -import os import voluptuous as vol @@ -149,7 +148,8 @@ class ApnsNotificationService(BaseNotificationService): self.devices = {} self.device_states = {} self.topic = topic - if os.path.isfile(self.yaml_path): + + try: self.devices = { str(key): ApnsDevice( str(key), @@ -160,6 +160,8 @@ class ApnsNotificationService(BaseNotificationService): for (key, value) in load_yaml_config_file(self.yaml_path).items() } + except FileNotFoundError: + pass tracking_ids = [ device.full_tracking_device_id diff --git a/homeassistant/components/http/ban.py b/homeassistant/components/http/ban.py index 92c41157a33..1cb610e71a6 100644 --- a/homeassistant/components/http/ban.py +++ b/homeassistant/components/http/ban.py @@ -3,7 +3,6 @@ from collections import defaultdict from datetime import datetime from ipaddress import ip_address import logging -import os from aiohttp.web import middleware from aiohttp.web_exceptions import HTTPForbidden, HTTPUnauthorized @@ -155,11 +154,10 @@ async def async_load_ip_bans_config(hass: HomeAssistant, path: str): """Load list of banned IPs from config file.""" ip_list = [] - if not os.path.isfile(path): - return ip_list - try: list_ = await hass.async_add_executor_job(load_yaml_config_file, path) + except FileNotFoundError: + return ip_list except HomeAssistantError as err: _LOGGER.error('Unable to load %s: %s', path, str(err)) return ip_list diff --git a/homeassistant/config.py b/homeassistant/config.py index 88abf2ac791..b084ed358f3 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -363,13 +363,11 @@ def find_config_file(config_dir: Optional[str]) -> Optional[str]: def load_yaml_config_file(config_path: str) -> Dict[Any, Any]: """Parse a YAML configuration file. + Raises FileNotFoundError or HomeAssistantError. + This method needs to run in an executor. """ - try: - conf_dict = load_yaml(config_path) - except FileNotFoundError as err: - raise HomeAssistantError("Config file not found: {}".format( - getattr(err, 'filename', err))) + conf_dict = load_yaml(config_path) if not isinstance(conf_dict, dict): msg = "The configuration file {} does not contain a dictionary".format( diff --git a/homeassistant/scripts/check_config.py b/homeassistant/scripts/check_config.py index c06a5da4207..991a45b6498 100644 --- a/homeassistant/scripts/check_config.py +++ b/homeassistant/scripts/check_config.py @@ -312,6 +312,8 @@ async def check_ha_config_file(hass): return result.add_error("File configuration.yaml not found.") config = await hass.async_add_executor_job( load_yaml_config_file, config_path) + except FileNotFoundError: + return result.add_error("File not found: {}".format(config_path)) except HomeAssistantError as err: return result.add_error( "Error loading {}: {}".format(config_path, err)) From 314574fc84ec26602226b4a313e114c69d89d2f5 Mon Sep 17 00:00:00 2001 From: Fredrik Erlandsson Date: Sun, 19 May 2019 18:49:03 +0200 Subject: [PATCH 094/232] daikin version bump (#23991) --- homeassistant/components/daikin/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/daikin/manifest.json b/homeassistant/components/daikin/manifest.json index 485004bb615..ccc791764a9 100644 --- a/homeassistant/components/daikin/manifest.json +++ b/homeassistant/components/daikin/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/components/daikin", "requirements": [ - "pydaikin==1.4.4" + "pydaikin==1.4.5" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 2730695cf96..78b6e870afa 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1027,7 +1027,7 @@ pycsspeechtts==1.0.2 # pycups==1.9.73 # homeassistant.components.daikin -pydaikin==1.4.4 +pydaikin==1.4.5 # homeassistant.components.danfoss_air pydanfossair==0.1.0 From 8d388c5e796272f3f82fc7f66dbc54b79b8425c4 Mon Sep 17 00:00:00 2001 From: Greg Dowling Date: Sun, 19 May 2019 17:51:10 +0100 Subject: [PATCH 095/232] Bump loopenergy library version - catches runtime exception. (#23989) * Bump loopenergy library version - catches runtime exception. * Update requirements_all. --- homeassistant/components/loopenergy/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/loopenergy/manifest.json b/homeassistant/components/loopenergy/manifest.json index b282755b1a0..20fe6fac2aa 100644 --- a/homeassistant/components/loopenergy/manifest.json +++ b/homeassistant/components/loopenergy/manifest.json @@ -3,7 +3,7 @@ "name": "Loopenergy", "documentation": "https://www.home-assistant.io/components/loopenergy", "requirements": [ - "pyloopenergy==0.1.2" + "pyloopenergy==0.1.3" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index 78b6e870afa..ca262da4179 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1172,7 +1172,7 @@ pylinky==0.3.3 pylitejet==0.1 # homeassistant.components.loopenergy -pyloopenergy==0.1.2 +pyloopenergy==0.1.3 # homeassistant.components.lutron_caseta pylutron-caseta==0.5.0 From 5c346e8fb634a789ff8ed0e1e05ecb66e0cc54fc Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 20 May 2019 05:01:02 +0200 Subject: [PATCH 096/232] Update owner frontend integrations [skip ci] (#24001) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/components/lovelace/manifest.json | 2 +- homeassistant/components/panel_custom/manifest.json | 2 +- homeassistant/components/panel_iframe/manifest.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 45b1f0ff351..c2f5e89dc51 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -15,6 +15,6 @@ "websocket_api" ], "codeowners": [ - "@home-assistant/core" + "@home-assistant/frontend" ] } diff --git a/homeassistant/components/lovelace/manifest.json b/homeassistant/components/lovelace/manifest.json index 1c1a7a107e4..dd8da40efe4 100644 --- a/homeassistant/components/lovelace/manifest.json +++ b/homeassistant/components/lovelace/manifest.json @@ -5,6 +5,6 @@ "requirements": [], "dependencies": [], "codeowners": [ - "@home-assistant/core" + "@home-assistant/frontend" ] } diff --git a/homeassistant/components/panel_custom/manifest.json b/homeassistant/components/panel_custom/manifest.json index 5fb7adb2a4a..06c9338742c 100644 --- a/homeassistant/components/panel_custom/manifest.json +++ b/homeassistant/components/panel_custom/manifest.json @@ -7,6 +7,6 @@ "frontend" ], "codeowners": [ - "@home-assistant/core" + "@home-assistant/frontend" ] } diff --git a/homeassistant/components/panel_iframe/manifest.json b/homeassistant/components/panel_iframe/manifest.json index 127ff3caa4d..e66f94bdcc2 100644 --- a/homeassistant/components/panel_iframe/manifest.json +++ b/homeassistant/components/panel_iframe/manifest.json @@ -7,6 +7,6 @@ "frontend" ], "codeowners": [ - "@home-assistant/core" + "@home-assistant/frontend" ] } From eb912be47a2c9d032dbf9a84751c77d4896fa8b3 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Mon, 20 May 2019 07:45:31 +0200 Subject: [PATCH 097/232] Axis IO-port support (#23312) Support digital inputs and supervised inputs, digital outputs and relays --- homeassistant/components/axis/axis_base.py | 86 +++++++++++++ .../components/axis/binary_sensor.py | 61 +++------ homeassistant/components/axis/camera.py | 40 ++---- homeassistant/components/axis/device.py | 52 +++++--- homeassistant/components/axis/manifest.json | 2 +- homeassistant/components/axis/switch.py | 59 +++++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/axis/test_device.py | 9 +- tests/components/axis/test_switch.py | 120 ++++++++++++++++++ 10 files changed, 334 insertions(+), 99 deletions(-) create mode 100644 homeassistant/components/axis/axis_base.py create mode 100644 homeassistant/components/axis/switch.py create mode 100644 tests/components/axis/test_switch.py diff --git a/homeassistant/components/axis/axis_base.py b/homeassistant/components/axis/axis_base.py new file mode 100644 index 00000000000..9a8f53c8bde --- /dev/null +++ b/homeassistant/components/axis/axis_base.py @@ -0,0 +1,86 @@ +"""Base classes for Axis entities.""" + +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import Entity + +from .const import DOMAIN as AXIS_DOMAIN + + +class AxisEntityBase(Entity): + """Base common to all Axis entities.""" + + def __init__(self, device): + """Initialize the Axis event.""" + self.device = device + self.unsub_dispatcher = [] + + async def async_added_to_hass(self): + """Subscribe device events.""" + self.unsub_dispatcher.append(async_dispatcher_connect( + self.hass, self.device.event_reachable, self.update_callback)) + + async def async_will_remove_from_hass(self) -> None: + """Unsubscribe device events when removed.""" + for unsub_dispatcher in self.unsub_dispatcher: + unsub_dispatcher() + + @property + def available(self): + """Return True if device is available.""" + return self.device.available + + @property + def device_info(self): + """Return a device description for device registry.""" + return { + 'identifiers': {(AXIS_DOMAIN, self.device.serial)} + } + + @callback + def update_callback(self, no_delay=None): + """Update the entities state.""" + self.async_schedule_update_ha_state() + + +class AxisEventBase(AxisEntityBase): + """Base common to all Axis entities from event stream.""" + + def __init__(self, event, device): + """Initialize the Axis event.""" + super().__init__(device) + self.event = event + + async def async_added_to_hass(self) -> None: + """Subscribe sensors events.""" + self.event.register_callback(self.update_callback) + + await super().async_added_to_hass() + + async def async_will_remove_from_hass(self) -> None: + """Disconnect device object when removed.""" + self.event.remove_callback(self.update_callback) + + await super().async_will_remove_from_hass() + + @property + def device_class(self): + """Return the class of the event.""" + return self.event.CLASS + + @property + def name(self): + """Return the name of the event.""" + return '{} {} {}'.format( + self.device.name, self.event.TYPE, self.event.id) + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def unique_id(self): + """Return a unique identifier for this device.""" + return '{}-{}-{}'.format( + self.device.serial, self.event.topic, self.event.id) diff --git a/homeassistant/components/axis/binary_sensor.py b/homeassistant/components/axis/binary_sensor.py index e9ef9f63710..86a2a738b70 100644 --- a/homeassistant/components/axis/binary_sensor.py +++ b/homeassistant/components/axis/binary_sensor.py @@ -2,6 +2,8 @@ from datetime import timedelta +from axis.event_stream import CLASS_INPUT, CLASS_OUTPUT + from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.const import CONF_MAC, CONF_TRIGGER_TIME from homeassistant.core import callback @@ -9,7 +11,8 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.util.dt import utcnow -from .const import DOMAIN as AXIS_DOMAIN, LOGGER +from .axis_base import AxisEventBase +from .const import DOMAIN as AXIS_DOMAIN async def async_setup_entry(hass, config_entry, async_add_entities): @@ -21,32 +24,21 @@ async def async_setup_entry(hass, config_entry, async_add_entities): def async_add_sensor(event_id): """Add binary sensor from Axis device.""" event = device.api.event.events[event_id] - async_add_entities([AxisBinarySensor(event, device)], True) + + if event.CLASS != CLASS_OUTPUT: + async_add_entities([AxisBinarySensor(event, device)], True) device.listeners.append(async_dispatcher_connect( hass, device.event_new_sensor, async_add_sensor)) -class AxisBinarySensor(BinarySensorDevice): +class AxisBinarySensor(AxisEventBase, BinarySensorDevice): """Representation of a binary Axis event.""" def __init__(self, event, device): """Initialize the Axis binary sensor.""" - self.event = event - self.device = device + super().__init__(event, device) self.remove_timer = None - self.unsub_dispatcher = None - - async def async_added_to_hass(self): - """Subscribe sensors events.""" - self.event.register_callback(self.update_callback) - self.unsub_dispatcher = async_dispatcher_connect( - self.hass, self.device.event_reachable, self.update_callback) - - async def async_will_remove_from_hass(self) -> None: - """Disconnect device object when removed.""" - self.event.remove_callback(self.update_callback) - self.unsub_dispatcher() @callback def update_callback(self, no_delay=False): @@ -67,7 +59,6 @@ class AxisBinarySensor(BinarySensorDevice): @callback def _delay_update(now): """Timer callback for sensor update.""" - LOGGER.debug("%s called delayed (%s sec) update", self.name, delay) self.async_schedule_update_ha_state() self.remove_timer = None @@ -83,32 +74,10 @@ class AxisBinarySensor(BinarySensorDevice): @property def name(self): """Return the name of the event.""" - return '{} {} {}'.format( - self.device.name, self.event.TYPE, self.event.id) + if self.event.CLASS == CLASS_INPUT and self.event.id and \ + self.device.api.vapix.ports[self.event.id].name: + return '{} {}'.format( + self.device.name, + self.device.api.vapix.ports[self.event.id].name) - @property - def device_class(self): - """Return the class of the event.""" - return self.event.CLASS - - @property - def unique_id(self): - """Return a unique identifier for this device.""" - return '{}-{}-{}'.format( - self.device.serial, self.event.topic, self.event.id) - - def available(self): - """Return True if device is available.""" - return self.device.available - - @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def device_info(self): - """Return a device description for device registry.""" - return { - 'identifiers': {(AXIS_DOMAIN, self.device.serial)} - } + return super().name diff --git a/homeassistant/components/axis/camera.py b/homeassistant/components/axis/camera.py index 457cc23e73d..08e40f4999a 100644 --- a/homeassistant/components/axis/camera.py +++ b/homeassistant/components/axis/camera.py @@ -6,9 +6,9 @@ from homeassistant.components.mjpeg.camera import ( from homeassistant.const import ( CONF_AUTHENTICATION, CONF_DEVICE, CONF_HOST, CONF_MAC, CONF_NAME, CONF_PASSWORD, CONF_PORT, CONF_USERNAME, HTTP_DIGEST_AUTHENTICATION) -from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from .axis_base import AxisEntityBase from .const import DOMAIN as AXIS_DOMAIN AXIS_IMAGE = 'http://{}:{}/axis-cgi/jpg/image.cgi' @@ -38,28 +38,20 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities([AxisCamera(config, device)]) -class AxisCamera(MjpegCamera): +class AxisCamera(AxisEntityBase, MjpegCamera): """Representation of a Axis camera.""" def __init__(self, config, device): """Initialize Axis Communications camera component.""" - super().__init__(config) - self.device_config = config - self.device = device - self.port = device.config_entry.data[CONF_DEVICE][CONF_PORT] - self.unsub_dispatcher = [] + AxisEntityBase.__init__(self, device) + MjpegCamera.__init__(self, config) async def async_added_to_hass(self): """Subscribe camera events.""" self.unsub_dispatcher.append(async_dispatcher_connect( self.hass, self.device.event_new_address, self._new_address)) - self.unsub_dispatcher.append(async_dispatcher_connect( - self.hass, self.device.event_reachable, self.update_callback)) - async def async_will_remove_from_hass(self) -> None: - """Disconnect device object when removed.""" - for unsub_dispatcher in self.unsub_dispatcher: - unsub_dispatcher() + await super().async_added_to_hass() @property def supported_features(self): @@ -74,29 +66,13 @@ class AxisCamera(MjpegCamera): self.device.config_entry.data[CONF_DEVICE][CONF_PASSWORD], self.device.host) - @callback - def update_callback(self, no_delay=None): - """Update the cameras state.""" - self.async_schedule_update_ha_state() - - @property - def available(self): - """Return True if device is available.""" - return self.device.available - def _new_address(self): """Set new device address for video stream.""" - self._mjpeg_url = AXIS_VIDEO.format(self.device.host, self.port) - self._still_image_url = AXIS_IMAGE.format(self.device.host, self.port) + port = self.device.config_entry.data[CONF_DEVICE][CONF_PORT] + self._mjpeg_url = AXIS_VIDEO.format(self.device.host, port) + self._still_image_url = AXIS_IMAGE.format(self.device.host, port) @property def unique_id(self): """Return a unique identifier for this device.""" return '{}-camera'.format(self.device.serial) - - @property - def device_info(self): - """Return a device description for device registry.""" - return { - 'identifiers': {(AXIS_DOMAIN, self.device.serial)} - } diff --git a/homeassistant/components/axis/device.py b/homeassistant/components/axis/device.py index 1595dde4cba..32c5ac090e9 100644 --- a/homeassistant/components/axis/device.py +++ b/homeassistant/components/axis/device.py @@ -83,19 +83,23 @@ class AxisNetworkDevice: self.product_type = self.api.vapix.params.prodtype if self.config_entry.options[CONF_CAMERA]: + self.hass.async_create_task( self.hass.config_entries.async_forward_entry_setup( self.config_entry, 'camera')) if self.config_entry.options[CONF_EVENTS]: - task = self.hass.async_create_task( - self.hass.config_entries.async_forward_entry_setup( - self.config_entry, 'binary_sensor')) self.api.stream.connection_status_callback = \ self.async_connection_status_callback self.api.enable_events(event_callback=self.async_event_callback) - task.add_done_callback(self.start) + + platform_tasks = [ + self.hass.config_entries.async_forward_entry_setup( + self.config_entry, platform) + for platform in ['binary_sensor', 'switch'] + ] + self.hass.async_create_task(self.start(platform_tasks)) self.config_entry.add_update_listener(self.async_new_address_callback) @@ -145,9 +149,9 @@ class AxisNetworkDevice: if action == 'add': async_dispatcher_send(self.hass, self.event_new_sensor, event_id) - @callback - def start(self, fut): - """Start the event stream.""" + async def start(self, platform_tasks): + """Start the event stream when all platforms are loaded.""" + await asyncio.gather(*platform_tasks) self.api.start() @callback @@ -157,15 +161,22 @@ class AxisNetworkDevice: async def async_reset(self): """Reset this device to default state.""" - self.api.stop() + platform_tasks = [] if self.config_entry.options[CONF_CAMERA]: - await self.hass.config_entries.async_forward_entry_unload( - self.config_entry, 'camera') + platform_tasks.append( + self.hass.config_entries.async_forward_entry_unload( + self.config_entry, 'camera')) if self.config_entry.options[CONF_EVENTS]: - await self.hass.config_entries.async_forward_entry_unload( - self.config_entry, 'binary_sensor') + self.api.stop() + platform_tasks += [ + self.hass.config_entries.async_forward_entry_unload( + self.config_entry, platform) + for platform in ['binary_sensor', 'switch'] + ] + + await asyncio.gather(*platform_tasks) for unsub_dispatcher in self.listeners: unsub_dispatcher() @@ -185,13 +196,22 @@ async def get_device(hass, config): port=config[CONF_PORT], web_proto='http') device.vapix.initialize_params(preload_data=False) + device.vapix.initialize_ports() try: with async_timeout.timeout(15): - await hass.async_add_executor_job( - device.vapix.params.update_brand) - await hass.async_add_executor_job( - device.vapix.params.update_properties) + + await asyncio.gather( + hass.async_add_executor_job( + device.vapix.params.update_brand), + + hass.async_add_executor_job( + device.vapix.params.update_properties), + + hass.async_add_executor_job( + device.vapix.ports.update) + ) + return device except axis.Unauthorized: diff --git a/homeassistant/components/axis/manifest.json b/homeassistant/components/axis/manifest.json index 0379ee3b03c..507f63c12b5 100644 --- a/homeassistant/components/axis/manifest.json +++ b/homeassistant/components/axis/manifest.json @@ -3,7 +3,7 @@ "name": "Axis", "config_flow": true, "documentation": "https://www.home-assistant.io/components/axis", - "requirements": ["axis==22"], + "requirements": ["axis==23"], "dependencies": [], "codeowners": ["@kane610"] } diff --git a/homeassistant/components/axis/switch.py b/homeassistant/components/axis/switch.py new file mode 100644 index 00000000000..852528120a5 --- /dev/null +++ b/homeassistant/components/axis/switch.py @@ -0,0 +1,59 @@ +"""Support for Axis switches.""" + +from axis.event_stream import CLASS_OUTPUT + +from homeassistant.components.switch import SwitchDevice +from homeassistant.const import CONF_MAC +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from .axis_base import AxisEventBase +from .const import DOMAIN as AXIS_DOMAIN + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up a Axis switch.""" + serial_number = config_entry.data[CONF_MAC] + device = hass.data[AXIS_DOMAIN][serial_number] + + @callback + def async_add_switch(event_id): + """Add switch from Axis device.""" + event = device.api.event.events[event_id] + + if event.CLASS == CLASS_OUTPUT: + async_add_entities([AxisSwitch(event, device)], True) + + device.listeners.append(async_dispatcher_connect( + hass, device.event_new_sensor, async_add_switch)) + + +class AxisSwitch(AxisEventBase, SwitchDevice): + """Representation of a Axis switch.""" + + @property + def is_on(self): + """Return true if event is active.""" + return self.event.is_tripped + + async def async_turn_on(self, **kwargs): + """Turn on switch.""" + action = '/' + await self.hass.async_add_executor_job( + self.device.api.vapix.ports[self.event.id].action, action) + + async def async_turn_off(self, **kwargs): + """Turn off switch.""" + action = '\\' + await self.hass.async_add_executor_job( + self.device.api.vapix.ports[self.event.id].action, action) + + @property + def name(self): + """Return the name of the event.""" + if self.event.id and self.device.api.vapix.ports[self.event.id].name: + return '{} {}'.format( + self.device.name, + self.device.api.vapix.ports[self.event.id].name) + + return super().name diff --git a/requirements_all.txt b/requirements_all.txt index ca262da4179..9b0eea4ce47 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -207,7 +207,7 @@ av==6.1.2 # avion==0.10 # homeassistant.components.axis -axis==22 +axis==23 # homeassistant.components.baidu baidu-aip==1.6.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c932096825b..826544c4e8d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -67,7 +67,7 @@ apns2==0.3.0 av==6.1.2 # homeassistant.components.axis -axis==22 +axis==23 # homeassistant.components.zha bellows-homeassistant==0.7.3 diff --git a/tests/components/axis/test_device.py b/tests/components/axis/test_device.py index 23714e51c88..ac2da3ddedc 100644 --- a/tests/components/axis/test_device.py +++ b/tests/components/axis/test_device.py @@ -37,6 +37,7 @@ async def test_device_setup(): api = Mock() axis_device = device.AxisNetworkDevice(hass, entry) + axis_device.start = Mock() assert axis_device.host == DEVICE_DATA[device.CONF_HOST] assert axis_device.model == ENTRY_CONFIG[device.CONF_MODEL] @@ -47,11 +48,13 @@ async def test_device_setup(): assert await axis_device.async_setup() is True assert axis_device.api is api - assert len(hass.config_entries.async_forward_entry_setup.mock_calls) == 2 + assert len(hass.config_entries.async_forward_entry_setup.mock_calls) == 3 assert hass.config_entries.async_forward_entry_setup.mock_calls[0][1] == \ (entry, 'camera') assert hass.config_entries.async_forward_entry_setup.mock_calls[1][1] == \ (entry, 'binary_sensor') + assert hass.config_entries.async_forward_entry_setup.mock_calls[2][1] == \ + (entry, 'switch') async def test_device_signal_new_address(hass): @@ -71,7 +74,7 @@ async def test_device_signal_new_address(hass): await hass.async_block_till_done() assert len(hass.states.async_all()) == 1 - assert len(axis_device.listeners) == 1 + assert len(axis_device.listeners) == 2 entry.data[device.CONF_DEVICE][device.CONF_HOST] = '2.3.4.5' hass.config_entries.async_update_entry(entry, data=entry.data) @@ -193,6 +196,8 @@ async def test_get_device(hass): with patch('axis.param_cgi.Params.update_brand', return_value=mock_coro()), \ patch('axis.param_cgi.Params.update_properties', + return_value=mock_coro()), \ + patch('axis.port_cgi.Ports.update', return_value=mock_coro()): assert await device.get_device(hass, DEVICE_DATA) diff --git a/tests/components/axis/test_switch.py b/tests/components/axis/test_switch.py new file mode 100644 index 00000000000..1acb81ee0a2 --- /dev/null +++ b/tests/components/axis/test_switch.py @@ -0,0 +1,120 @@ +"""Axis switch platform tests.""" + +from unittest.mock import call as mock_call, Mock + +from homeassistant import config_entries +from homeassistant.components import axis +from homeassistant.setup import async_setup_component + +import homeassistant.components.switch as switch + +EVENTS = [ + { + 'operation': 'Initialized', + 'topic': 'tns1:Device/Trigger/Relay', + 'source': 'RelayToken', + 'source_idx': '0', + 'type': 'LogicalState', + 'value': 'inactive' + }, + { + 'operation': 'Initialized', + 'topic': 'tns1:Device/Trigger/Relay', + 'source': 'RelayToken', + 'source_idx': '1', + 'type': 'LogicalState', + 'value': 'active' + } +] + +ENTRY_CONFIG = { + axis.CONF_DEVICE: { + axis.config_flow.CONF_HOST: '1.2.3.4', + axis.config_flow.CONF_USERNAME: 'user', + axis.config_flow.CONF_PASSWORD: 'pass', + axis.config_flow.CONF_PORT: 80 + }, + axis.config_flow.CONF_MAC: '1234ABCD', + axis.config_flow.CONF_MODEL: 'model', + axis.config_flow.CONF_NAME: 'model 0' +} + +ENTRY_OPTIONS = { + axis.CONF_CAMERA: False, + axis.CONF_EVENTS: True, + axis.CONF_TRIGGER_TIME: 0 +} + + +async def setup_device(hass): + """Load the Axis switch platform.""" + from axis import AxisDevice + loop = Mock() + + config_entry = config_entries.ConfigEntry( + 1, axis.DOMAIN, 'Mock Title', ENTRY_CONFIG, 'test', + config_entries.CONN_CLASS_LOCAL_PUSH, options=ENTRY_OPTIONS) + device = axis.AxisNetworkDevice(hass, config_entry) + device.api = AxisDevice(loop=loop, **config_entry.data[axis.CONF_DEVICE]) + hass.data[axis.DOMAIN] = {device.serial: device} + device.api.enable_events(event_callback=device.async_event_callback) + + await hass.config_entries.async_forward_entry_setup( + config_entry, 'switch') + # To flush out the service call to update the group + await hass.async_block_till_done() + + return device + + +async def test_platform_manually_configured(hass): + """Test that nothing happens when platform is manually configured.""" + assert await async_setup_component(hass, switch.DOMAIN, { + 'switch': { + 'platform': axis.DOMAIN + } + }) + + assert axis.DOMAIN not in hass.data + + +async def test_no_switches(hass): + """Test that no output events in Axis results in no switch entities.""" + await setup_device(hass) + + assert not hass.states.async_entity_ids('switch') + + +async def test_switches(hass): + """Test that switches are loaded properly.""" + device = await setup_device(hass) + device.api.vapix.ports = {'0': Mock(), '1': Mock()} + device.api.vapix.ports['0'].name = 'Doorbell' + device.api.vapix.ports['1'].name = '' + + for event in EVENTS: + device.api.stream.event.manage_event(event) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 3 + + relay_0 = hass.states.get('switch.model_0_doorbell') + assert relay_0.state == 'off' + assert relay_0.name == 'model 0 Doorbell' + + relay_1 = hass.states.get('switch.model_0_relay_1') + assert relay_1.state == 'on' + assert relay_1.name == 'model 0 Relay 1' + + device.api.vapix.ports['0'].action = Mock() + + await hass.services.async_call('switch', 'turn_on', { + 'entity_id': 'switch.model_0_doorbell' + }, blocking=True) + + await hass.services.async_call('switch', 'turn_off', { + 'entity_id': 'switch.model_0_doorbell' + }, blocking=True) + + assert device.api.vapix.ports['0'].action.call_args_list == \ + [mock_call('/'), mock_call('\\')] From afe9fc221ec2c0987bceb28ce84612485a85583d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 20 May 2019 20:02:36 +0200 Subject: [PATCH 098/232] Fire event when core config is updated (#23922) * Fire event when core config is updated --- homeassistant/config.py | 74 +++----------------------- homeassistant/const.py | 18 ++++--- homeassistant/core.py | 112 ++++++++++++++++++++++++++++++++++++---- tests/common.py | 1 - tests/test_config.py | 41 +++++++++++---- tests/test_core.py | 31 ++++++++++- 6 files changed, 180 insertions(+), 97 deletions(-) diff --git a/homeassistant/config.py b/homeassistant/config.py index b084ed358f3..ec83818d903 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -23,14 +23,16 @@ from homeassistant.const import ( __version__, CONF_CUSTOMIZE, CONF_CUSTOMIZE_DOMAIN, CONF_CUSTOMIZE_GLOB, CONF_WHITELIST_EXTERNAL_DIRS, CONF_AUTH_PROVIDERS, CONF_AUTH_MFA_MODULES, CONF_TYPE, CONF_ID) -from homeassistant.core import callback, DOMAIN as CONF_CORE, HomeAssistant +from homeassistant.core import ( + DOMAIN as CONF_CORE, SOURCE_DISCOVERED, SOURCE_YAML, HomeAssistant, + callback) from homeassistant.exceptions import HomeAssistantError from homeassistant.loader import ( Integration, async_get_integration, IntegrationNotFound ) from homeassistant.util.yaml import load_yaml, SECRET_YAML import homeassistant.helpers.config_validation as cv -from homeassistant.util import dt as date_util, location as loc_util +from homeassistant.util import location as loc_util from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM from homeassistant.helpers.entity_values import EntityValues from homeassistant.helpers import config_per_platform, extract_domain_configs @@ -50,13 +52,6 @@ FILE_MIGRATION = ( ('ios.conf', '.ios.conf'), ) -CORE_STORAGE_KEY = 'homeassistant.core_config' -CORE_STORAGE_VERSION = 1 - -SOURCE_DISCOVERED = 'discovered' -SOURCE_STORAGE = 'storage' -SOURCE_YAML = 'yaml' - DEFAULT_CORE_CONFIG = ( # Tuples (attribute, default, auto detect property, description) (CONF_NAME, 'Home', None, 'Name of the location where Home Assistant is ' @@ -478,42 +473,6 @@ def _format_config_error(ex: vol.Invalid, domain: str, config: Dict) -> str: return message -def _set_time_zone(hass: HomeAssistant, time_zone_str: Optional[str]) -> None: - """Help to set the time zone.""" - if time_zone_str is None: - return - - time_zone = date_util.get_time_zone(time_zone_str) - - if time_zone: - hass.config.time_zone = time_zone - date_util.set_default_time_zone(time_zone) - else: - _LOGGER.error("Received invalid time zone %s", time_zone_str) - - -async def async_load_ha_core_config(hass: HomeAssistant) -> None: - """Store [homeassistant] core config.""" - store = hass.helpers.storage.Store(CORE_STORAGE_VERSION, CORE_STORAGE_KEY, - private=True) - data = await store.async_load() - if not data: - return - - hac = hass.config - hac.config_source = SOURCE_STORAGE - hac.latitude = data['latitude'] - hac.longitude = data['longitude'] - hac.elevation = data['elevation'] - unit_system = data['unit_system'] - if unit_system == CONF_UNIT_SYSTEM_IMPERIAL: - hac.units = IMPERIAL_SYSTEM - else: - hac.units = METRIC_SYSTEM - hac.location_name = data['location_name'] - _set_time_zone(hass, data['time_zone']) - - async def async_process_ha_core_config( hass: HomeAssistant, config: Dict, api_password: Optional[str] = None, @@ -552,7 +511,7 @@ async def async_process_ha_core_config( auth_conf, mfa_conf)) - await async_load_ha_core_config(hass) + await hass.config.async_load() hac = hass.config @@ -568,7 +527,8 @@ async def async_process_ha_core_config( if key in config: setattr(hac, attr, config[key]) - _set_time_zone(hass, config.get(CONF_TIME_ZONE)) + if CONF_TIME_ZONE in config: + hac.set_time_zone(config[CONF_TIME_ZONE]) # Init whitelist external dir hac.whitelist_external_dirs = {hass.config.path('www')} @@ -649,7 +609,7 @@ async def async_process_ha_core_config( discovered.append(('name', info.city)) if hac.time_zone is None: - _set_time_zone(hass, info.time_zone) + hac.set_time_zone(info.time_zone) discovered.append(('time_zone', info.time_zone)) if hac.elevation is None and hac.latitude is not None and \ @@ -666,24 +626,6 @@ async def async_process_ha_core_config( ", ".join('{}: {}'.format(key, val) for key, val in discovered)) -async def async_store_ha_core_config(hass: HomeAssistant) -> None: - """Store [homeassistant] core config.""" - config = hass.config.as_dict() - - data = { - 'latitude': config['latitude'], - 'longitude': config['longitude'], - 'elevation': config['elevation'], - 'unit_system': hass.config.units.name, - 'location_name': config['location_name'], - 'time_zone': config['time_zone'], - } - - store = hass.helpers.storage.Store(CORE_STORAGE_VERSION, CORE_STORAGE_KEY, - private=True) - await store.async_save(data) - - def _log_pkg_error( package: str, component: str, config: Dict, message: str) -> None: """Log an error while merging packages.""" diff --git a/homeassistant/const.py b/homeassistant/const.py index 8d21c9d191e..1dcea3e2daf 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -160,21 +160,23 @@ CONF_XY = 'xy' CONF_ZONE = 'zone' # #### EVENTS #### +EVENT_AUTOMATION_TRIGGERED = 'automation_triggered' +EVENT_CALL_SERVICE = 'call_service' +EVENT_COMPONENT_LOADED = 'component_loaded' +EVENT_CORE_CONFIG_UPDATE = 'core_config_updated' +EVENT_HOMEASSISTANT_CLOSE = 'homeassistant_close' EVENT_HOMEASSISTANT_START = 'homeassistant_start' EVENT_HOMEASSISTANT_STOP = 'homeassistant_stop' -EVENT_HOMEASSISTANT_CLOSE = 'homeassistant_close' -EVENT_STATE_CHANGED = 'state_changed' -EVENT_TIME_CHANGED = 'time_changed' -EVENT_CALL_SERVICE = 'call_service' +EVENT_LOGBOOK_ENTRY = 'logbook_entry' EVENT_PLATFORM_DISCOVERED = 'platform_discovered' -EVENT_COMPONENT_LOADED = 'component_loaded' +EVENT_SCRIPT_STARTED = 'script_started' EVENT_SERVICE_REGISTERED = 'service_registered' EVENT_SERVICE_REMOVED = 'service_removed' -EVENT_LOGBOOK_ENTRY = 'logbook_entry' +EVENT_STATE_CHANGED = 'state_changed' EVENT_THEMES_UPDATED = 'themes_updated' EVENT_TIMER_OUT_OF_SYNC = 'timer_out_of_sync' -EVENT_AUTOMATION_TRIGGERED = 'automation_triggered' -EVENT_SCRIPT_STARTED = 'script_started' +EVENT_TIME_CHANGED = 'time_changed' + # #### DEVICE CLASSES #### DEVICE_CLASS_BATTERY = 'battery' diff --git a/homeassistant/core.py b/homeassistant/core.py index a02c1b687ab..4dd84cc1a46 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -27,12 +27,12 @@ import attr import voluptuous as vol from homeassistant.const import ( - ATTR_DOMAIN, ATTR_FRIENDLY_NAME, ATTR_NOW, ATTR_SERVICE, - ATTR_SERVICE_DATA, ATTR_SECONDS, EVENT_CALL_SERVICE, - EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, - EVENT_HOMEASSISTANT_CLOSE, EVENT_SERVICE_REMOVED, - EVENT_SERVICE_REGISTERED, EVENT_STATE_CHANGED, - EVENT_TIME_CHANGED, EVENT_TIMER_OUT_OF_SYNC, MATCH_ALL, __version__) + ATTR_DOMAIN, ATTR_FRIENDLY_NAME, ATTR_NOW, ATTR_SERVICE, ATTR_SERVICE_DATA, + ATTR_SECONDS, CONF_UNIT_SYSTEM_IMPERIAL, EVENT_CALL_SERVICE, + EVENT_CORE_CONFIG_UPDATE, EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_CLOSE, EVENT_SERVICE_REMOVED, + EVENT_SERVICE_REGISTERED, EVENT_STATE_CHANGED, EVENT_TIME_CHANGED, + EVENT_TIMER_OUT_OF_SYNC, MATCH_ALL, __version__) from homeassistant import loader from homeassistant.exceptions import ( HomeAssistantError, InvalidEntityFormatError, InvalidStateError, @@ -43,7 +43,8 @@ from homeassistant.util.async_ import ( from homeassistant import util import homeassistant.util.dt as dt_util from homeassistant.util import location, slugify -from homeassistant.util.unit_system import UnitSystem, METRIC_SYSTEM # NOQA +from homeassistant.util.unit_system import ( # NOQA + UnitSystem, IMPERIAL_SYSTEM, METRIC_SYSTEM) # Typing imports that create a circular dependency # pylint: disable=using-constant-test @@ -56,11 +57,19 @@ CALLABLE_T = TypeVar('CALLABLE_T', bound=Callable) CALLBACK_TYPE = Callable[[], None] # pylint: enable=invalid-name +CORE_STORAGE_KEY = 'homeassistant.core_config' +CORE_STORAGE_VERSION = 1 + DOMAIN = 'homeassistant' # How long we wait for the result of a service call SERVICE_CALL_LIMIT = 10 # seconds +# Source of core configuration +SOURCE_DISCOVERED = 'discovered' +SOURCE_STORAGE = 'storage' +SOURCE_YAML = 'yaml' + # How long to wait till things that run on startup have to finish. TIMEOUT_EVENT_START = 15 @@ -144,7 +153,7 @@ class HomeAssistant: self.bus = EventBus(self) self.services = ServiceRegistry(self) self.states = StateMachine(self.bus, self.loop) - self.config = Config() # type: Config + self.config = Config(self) # type: Config self.components = loader.Components(self) self.helpers = loader.Helpers(self) # This is a dictionary that any component can store any data on. @@ -1168,8 +1177,10 @@ class ServiceRegistry: class Config: """Configuration settings for Home Assistant.""" - def __init__(self) -> None: + def __init__(self, hass: HomeAssistant) -> None: """Initialize a new config object.""" + self.hass = hass + self.latitude = None # type: Optional[float] self.longitude = None # type: Optional[float] self.elevation = None # type: Optional[int] @@ -1235,7 +1246,7 @@ class Config: return False def as_dict(self) -> Dict: - """Create a dictionary representation of this dict. + """Create a dictionary representation of the configuration. Async friendly. """ @@ -1257,6 +1268,87 @@ class Config: 'config_source': self.config_source } + def set_time_zone(self, time_zone_str: str) -> None: + """Help to set the time zone.""" + time_zone = dt_util.get_time_zone(time_zone_str) + + if time_zone: + self.time_zone = time_zone + dt_util.set_default_time_zone(time_zone) + else: + raise ValueError( + "Received invalid time zone {}".format(time_zone_str)) + + @callback + def _update(self, *, + source: str, + latitude: Optional[float] = None, + longitude: Optional[float] = None, + elevation: Optional[int] = None, + unit_system: Optional[str] = None, + location_name: Optional[str] = None, + time_zone: Optional[str] = None) -> None: + """Update the configuration from a dictionary. + + Async friendly. + """ + self.config_source = source + if latitude is not None: + self.latitude = latitude + if longitude is not None: + self.longitude = longitude + if elevation is not None: + self.elevation = elevation + if unit_system is not None: + if unit_system == CONF_UNIT_SYSTEM_IMPERIAL: + self.units = IMPERIAL_SYSTEM + else: + self.units = METRIC_SYSTEM + if location_name is not None: + self.location_name = location_name + if time_zone is not None: + self.set_time_zone(time_zone) + + async def update(self, **kwargs: Any) -> None: + """Update the configuration from a dictionary. + + Async friendly. + """ + self._update(source=SOURCE_STORAGE, **kwargs) + await self.async_store() + self.hass.bus.async_fire( + EVENT_CORE_CONFIG_UPDATE, kwargs + ) + + async def async_load(self) -> None: + """Load [homeassistant] core config.""" + store = self.hass.helpers.storage.Store( + CORE_STORAGE_VERSION, CORE_STORAGE_KEY, private=True) + data = await store.async_load() + if not data: + return + + self._update(source=SOURCE_STORAGE, **data) + + async def async_store(self) -> None: + """Store [homeassistant] core config.""" + time_zone = dt_util.UTC.zone + if self.time_zone and getattr(self.time_zone, 'zone'): + time_zone = getattr(self.time_zone, 'zone') + + data = { + 'latitude': self.latitude, + 'longitude': self.longitude, + 'elevation': self.elevation, + 'unit_system': self.units.name, + 'location_name': self.location_name, + 'time_zone': time_zone, + } + + store = self.hass.helpers.storage.Store( + CORE_STORAGE_VERSION, CORE_STORAGE_KEY, private=True) + await store.async_save(data) + def _async_create_timer(hass: HomeAssistant) -> None: """Create a timer that will start on HOMEASSISTANT_START.""" diff --git a/tests/common.py b/tests/common.py index 572cd19a006..f7b3bc46bbd 100644 --- a/tests/common.py +++ b/tests/common.py @@ -122,7 +122,6 @@ def get_test_home_assistant(): async def async_test_home_assistant(loop): """Return a Home Assistant object pointing at test config dir.""" hass = ha.HomeAssistant(loop) - hass.config.async_load = Mock() store = auth_store.AuthStore(hass) hass.auth = auth.AuthManager(hass, store, {}, {}) ensure_auth_manager_loaded(hass.auth) diff --git a/tests/test_config.py b/tests/test_config.py index c081d97ed7c..42386cc1f4c 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,6 +1,7 @@ """Test config utils.""" # pylint: disable=protected-access import asyncio +import copy import os import unittest.mock as mock from collections import OrderedDict @@ -11,7 +12,8 @@ import pytest from voluptuous import MultipleInvalid, Invalid import yaml -from homeassistant.core import DOMAIN, HomeAssistantError, Config +from homeassistant.core import ( + DOMAIN, SOURCE_STORAGE, Config, HomeAssistantError) import homeassistant.config as config_util from homeassistant.loader import async_get_integration from homeassistant.const import ( @@ -439,7 +441,32 @@ async def test_loading_configuration_from_storage(hass, hass_storage): assert hass.config.time_zone.zone == 'Europe/Copenhagen' assert len(hass.config.whitelist_external_dirs) == 2 assert '/tmp' in hass.config.whitelist_external_dirs - assert hass.config.config_source == config_util.SOURCE_STORAGE + assert hass.config.config_source == SOURCE_STORAGE + + +async def test_updating_configuration(hass, hass_storage): + """Test updating configuration stores the new configuration.""" + core_data = { + 'data': { + 'elevation': 10, + 'latitude': 55, + 'location_name': 'Home', + 'longitude': 13, + 'time_zone': 'Europe/Copenhagen', + 'unit_system': 'metric' + }, + 'key': 'homeassistant.core_config', + 'version': 1 + } + hass_storage["homeassistant.core_config"] = dict(core_data) + await config_util.async_process_ha_core_config( + hass, {'whitelist_external_dirs': '/tmp'}) + await hass.config.update(latitude=50) + + new_core_data = copy.deepcopy(core_data) + new_core_data['data']['latitude'] = 50 + assert hass_storage["homeassistant.core_config"] == new_core_data + assert hass.config.latitude == 50 async def test_override_stored_configuration(hass, hass_storage): @@ -474,8 +501,6 @@ async def test_override_stored_configuration(hass, hass_storage): async def test_loading_configuration(hass): """Test loading core config onto hass object.""" - hass.config = mock.Mock() - await config_util.async_process_ha_core_config(hass, { 'latitude': 60, 'longitude': 50, @@ -499,8 +524,6 @@ async def test_loading_configuration(hass): async def test_loading_configuration_temperature_unit(hass): """Test backward compatibility when loading core config.""" - hass.config = mock.Mock() - await config_util.async_process_ha_core_config(hass, { 'latitude': 60, 'longitude': 50, @@ -521,8 +544,6 @@ async def test_loading_configuration_temperature_unit(hass): async def test_loading_configuration_from_packages(hass): """Test loading packages config onto hass object config.""" - hass.config = mock.Mock() - await config_util.async_process_ha_core_config(hass, { 'latitude': 39, 'longitude': -1, @@ -586,12 +607,12 @@ async def test_discovering_configuration_auto_detect_fails(mock_detect, mock_elevation, hass): """Test config remains unchanged if discovery fails.""" - hass.config = Config() + hass.config = Config(hass) hass.config.config_dir = "/test/config" await config_util.async_process_ha_core_config(hass, {}) - blankConfig = Config() + blankConfig = Config(hass) assert hass.config.latitude == blankConfig.latitude assert hass.config.longitude == blankConfig.longitude assert hass.config.elevation == blankConfig.elevation diff --git a/tests/test_core.py b/tests/test_core.py index afbbe3e33b2..101396dd05e 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -23,7 +23,8 @@ from homeassistant.const import ( __version__, EVENT_STATE_CHANGED, ATTR_FRIENDLY_NAME, CONF_UNIT_SYSTEM, ATTR_NOW, EVENT_TIME_CHANGED, EVENT_TIMER_OUT_OF_SYNC, ATTR_SECONDS, EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_CLOSE, - EVENT_SERVICE_REGISTERED, EVENT_SERVICE_REMOVED, EVENT_CALL_SERVICE) + EVENT_SERVICE_REGISTERED, EVENT_SERVICE_REMOVED, EVENT_CALL_SERVICE, + EVENT_CORE_CONFIG_UPDATE) from tests.common import get_test_home_assistant, async_mock_service @@ -871,7 +872,7 @@ class TestConfig(unittest.TestCase): # pylint: disable=invalid-name def setUp(self): """Set up things to be run when tests are started.""" - self.config = ha.Config() + self.config = ha.Config(None) assert self.config.config_dir is None def test_path_with_file(self): @@ -942,6 +943,32 @@ class TestConfig(unittest.TestCase): self.config.is_allowed_path(None) +async def test_event_on_update(hass, hass_storage): + """Test that event is fired on update.""" + events = [] + + @ha.callback + def callback(event): + events.append(event) + + hass.bus.async_listen(EVENT_CORE_CONFIG_UPDATE, callback) + + assert hass.config.latitude != 12 + + await hass.config.update(latitude=12) + await hass.async_block_till_done() + + assert hass.config.latitude == 12 + assert len(events) == 1 + assert events[0].data == {'latitude': 12} + + +def test_bad_timezone_raises_value_error(hass): + """Test bad timezone raises ValueError.""" + with pytest.raises(ValueError): + hass.config.set_time_zone('not_a_timezone') + + @patch('homeassistant.core.monotonic') def test_create_timer(mock_monotonic, loop): """Test create timer.""" From 9ae878d8f2809e6ae23a2264593e6dd02719edeb Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 21 May 2019 07:20:23 +0200 Subject: [PATCH 099/232] Update CODEOWNERS (#24015) --- CODEOWNERS | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index cf71e61ead6..cadf6a2e841 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -83,7 +83,7 @@ homeassistant/components/flock/* @fabaff homeassistant/components/flunearyou/* @bachya homeassistant/components/foursquare/* @robbiet480 homeassistant/components/freebox/* @snoof85 -homeassistant/components/frontend/* @home-assistant/core +homeassistant/components/frontend/* @home-assistant/frontend homeassistant/components/gearbest/* @HerrHofrat homeassistant/components/geniushub/* @zxdavb homeassistant/components/gitter/* @fabaff @@ -138,7 +138,7 @@ homeassistant/components/linux_battery/* @fabaff homeassistant/components/liveboxplaytv/* @pschmitt homeassistant/components/logger/* @home-assistant/core homeassistant/components/logi_circle/* @evanjd -homeassistant/components/lovelace/* @home-assistant/core +homeassistant/components/lovelace/* @home-assistant/frontend homeassistant/components/luci/* @fbradyirl homeassistant/components/luftdaten/* @fabaff homeassistant/components/mastodon/* @fabaff @@ -173,8 +173,8 @@ homeassistant/components/openuv/* @bachya homeassistant/components/openweathermap/* @fabaff homeassistant/components/orangepi_gpio/* @pascallj homeassistant/components/owlet/* @oblogic7 -homeassistant/components/panel_custom/* @home-assistant/core -homeassistant/components/panel_iframe/* @home-assistant/core +homeassistant/components/panel_custom/* @home-assistant/frontend +homeassistant/components/panel_iframe/* @home-assistant/frontend homeassistant/components/persistent_notification/* @home-assistant/core homeassistant/components/philips_js/* @elupus homeassistant/components/pi_hole/* @fabaff From fc58746bc3aaf8dec9188af98de00702dece402d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 21 May 2019 07:21:31 +0200 Subject: [PATCH 100/232] Add websocket API for updating core config (#24009) * Add websocket API for updating core config --- homeassistant/components/config/core.py | 24 ++++++ tests/components/config/test_core.py | 103 +++++++++++++++++++++--- 2 files changed, 118 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/config/core.py b/homeassistant/components/config/core.py index ce7675c41f4..7e9008eb115 100644 --- a/homeassistant/components/config/core.py +++ b/homeassistant/components/config/core.py @@ -1,12 +1,16 @@ """Component to interact with Hassbian tools.""" +import voluptuous as vol + from homeassistant.components.http import HomeAssistantView from homeassistant.config import async_check_ha_config_file +from homeassistant.components import websocket_api async def async_setup(hass): """Set up the Hassbian config.""" hass.http.register_view(CheckConfigView) + hass.components.websocket_api.async_register_command(websocket_core_update) return True @@ -26,3 +30,23 @@ class CheckConfigView(HomeAssistantView): "result": state, "errors": errors, }) + + +@websocket_api.require_admin +@websocket_api.async_response +@websocket_api.websocket_command({ + vol.Required('type'): 'config/core/update', + vol.Optional('latitude'): vol.Coerce(float), + vol.Optional('longitude'): vol.Coerce(float), + vol.Optional('elevation'): vol.Coerce(int), + vol.Optional('unit_system'): vol.Coerce(str), + vol.Optional('location_name'): vol.Coerce(str), + vol.Optional('time_zone'): vol.Coerce(str), +}) +async def websocket_core_update(hass, connection, msg): + """Handle request for account info.""" + data = dict(msg) + data.pop('id') + data.pop('type') + await hass.config.update(**data) + connection.send_result(msg['id']) diff --git a/tests/components/config/test_core.py b/tests/components/config/test_core.py index 4d9063d774b..81b3818a815 100644 --- a/tests/components/config/test_core.py +++ b/tests/components/config/test_core.py @@ -4,35 +4,120 @@ from unittest.mock import patch from homeassistant.bootstrap import async_setup_component from homeassistant.components import config +from homeassistant.components.websocket_api.const import TYPE_RESULT +from homeassistant.const import CONF_UNIT_SYSTEM, CONF_UNIT_SYSTEM_IMPERIAL +import homeassistant.util.dt as dt_util from tests.common import mock_coro +ORIG_TIME_ZONE = dt_util.DEFAULT_TIME_ZONE -@asyncio.coroutine -def test_validate_config_ok(hass, hass_client): + +async def test_validate_config_ok(hass, hass_client): """Test checking config.""" with patch.object(config, 'SECTIONS', ['core']): - yield from async_setup_component(hass, 'config', {}) + await async_setup_component(hass, 'config', {}) - yield from asyncio.sleep(0.1, loop=hass.loop) + await asyncio.sleep(0.1, loop=hass.loop) - client = yield from hass_client() + client = await hass_client() with patch( 'homeassistant.components.config.core.async_check_ha_config_file', return_value=mock_coro()): - resp = yield from client.post('/api/config/core/check_config') + resp = await client.post('/api/config/core/check_config') assert resp.status == 200 - result = yield from resp.json() + result = await resp.json() assert result['result'] == 'valid' assert result['errors'] is None with patch( 'homeassistant.components.config.core.async_check_ha_config_file', return_value=mock_coro('beer')): - resp = yield from client.post('/api/config/core/check_config') + resp = await client.post('/api/config/core/check_config') assert resp.status == 200 - result = yield from resp.json() + result = await resp.json() assert result['result'] == 'invalid' assert result['errors'] == 'beer' + + +async def test_websocket_core_update(hass, hass_ws_client): + """Test core config update websocket command.""" + with patch.object(config, 'SECTIONS', ['core']): + await async_setup_component(hass, 'config', {}) + + assert hass.config.latitude != 60 + assert hass.config.longitude != 50 + assert hass.config.elevation != 25 + assert hass.config.location_name != 'Huis' + assert hass.config.units.name != CONF_UNIT_SYSTEM_IMPERIAL + assert hass.config.time_zone.zone != 'America/New_York' + + client = await hass_ws_client(hass) + await client.send_json({ + 'id': 5, + 'type': 'config/core/update', + 'latitude': 60, + 'longitude': 50, + 'elevation': 25, + 'location_name': 'Huis', + CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_IMPERIAL, + 'time_zone': 'America/New_York', + }) + + msg = await client.receive_json() + + assert msg['id'] == 5 + assert msg['type'] == TYPE_RESULT + assert msg['success'] + assert hass.config.latitude == 60 + assert hass.config.longitude == 50 + assert hass.config.elevation == 25 + assert hass.config.location_name == 'Huis' + assert hass.config.units.name == CONF_UNIT_SYSTEM_IMPERIAL + assert hass.config.time_zone.zone == 'America/New_York' + + dt_util.set_default_time_zone(ORIG_TIME_ZONE) + + +async def test_websocket_core_update_not_admin( + hass, hass_ws_client, hass_admin_user): + """Test core config fails for non admin.""" + hass_admin_user.groups = [] + with patch.object(config, 'SECTIONS', ['core']): + await async_setup_component(hass, 'config', {}) + + client = await hass_ws_client(hass) + await client.send_json({ + 'id': 6, + 'type': 'config/core/update', + 'latitude': 123, + }) + + msg = await client.receive_json() + + assert msg['id'] == 6 + assert msg['type'] == TYPE_RESULT + assert not msg['success'] + assert msg['error']['code'] == 'unauthorized' + + +async def test_websocket_bad_core_update(hass, hass_ws_client): + """Test core config update fails with bad parameters.""" + with patch.object(config, 'SECTIONS', ['core']): + await async_setup_component(hass, 'config', {}) + + client = await hass_ws_client(hass) + await client.send_json({ + 'id': 7, + 'type': 'config/core/update', + 'latituude': 123, + }) + + msg = await client.receive_json() + + assert msg['id'] == 7 + assert msg['type'] == TYPE_RESULT + assert not msg['success'] + assert msg['error']['code'] == 'invalid_format' From fbd7c72283bdb01a968fe8be634bd1ea9dd12440 Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Tue, 21 May 2019 13:23:38 +0100 Subject: [PATCH 101/232] Add geniushub sensors for issues (#23976) * Inital commit * delint - use new string formatting --- homeassistant/components/geniushub/sensor.py | 55 +++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/geniushub/sensor.py b/homeassistant/components/geniushub/sensor.py index fc00c0db79e..744906b8f2d 100644 --- a/homeassistant/components/geniushub/sensor.py +++ b/homeassistant/components/geniushub/sensor.py @@ -14,6 +14,12 @@ _LOGGER = logging.getLogger(__name__) GH_HAS_BATTERY = [ 'Room Thermostat', 'Genius Valve', 'Room Sensor', 'Radiator Valve'] +GH_LEVEL_MAPPING = { + 'error': 'Errors', + 'warning': 'Warnings', + 'information': 'Information' +} + async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): @@ -23,7 +29,10 @@ async def async_setup_platform(hass, config, async_add_entities, sensors = [GeniusDevice(client, d) for d in client.hub.device_objs if d.type in GH_HAS_BATTERY] - async_add_entities(sensors) + issues = [GeniusIssue(client, i) + for i in list(GH_LEVEL_MAPPING)] + + async_add_entities(sensors + issues, update_before_add=True) class GeniusDevice(Entity): @@ -80,3 +89,47 @@ class GeniusDevice(Entity): last_comms).isoformat() return {**attrs} + + +class GeniusIssue(Entity): + """Representation of a Genius Hub sensor.""" + + def __init__(self, client, level): + """Initialize the sensor.""" + self._hub = client.hub + self._name = GH_LEVEL_MAPPING[level] + self._level = level + self._issues = [] + + async def async_added_to_hass(self): + """Set up a listener when this entity is added to HA.""" + async_dispatcher_connect(self.hass, DOMAIN, self._refresh) + + @callback + def _refresh(self): + self.async_schedule_update_ha_state(force_refresh=True) + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def should_poll(self) -> bool: + """Return False as the geniushub devices should not be polled.""" + return False + + @property + def state(self): + """Return the number of issues.""" + return len(self._issues) + + @property + def device_state_attributes(self): + """Return the device state attributes.""" + return {'{}_list'.format(self._level): self._issues} + + async def async_update(self): + """Process the sensor's state data.""" + self._issues = [i['description'] + for i in self._hub.issues if i['level'] == self._level] From eae306c3f1c254f444ff66b9ced80ea2aaf1f4bf Mon Sep 17 00:00:00 2001 From: Tyler Page Date: Tue, 21 May 2019 12:26:11 +0000 Subject: [PATCH 102/232] Fix iterating over NoneType exception (#23648) * Fix iterating over NoneType exception When self._dark_sky is None, don't try to return self._dark_sky.units * Fix wrong check --- homeassistant/components/darksky/weather.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/darksky/weather.py b/homeassistant/components/darksky/weather.py index dd945e7b01c..84de690504e 100644 --- a/homeassistant/components/darksky/weather.py +++ b/homeassistant/components/darksky/weather.py @@ -103,6 +103,8 @@ class DarkSkyWeather(WeatherEntity): @property def temperature_unit(self): """Return the unit of measurement.""" + if self._dark_sky.units is None: + return None return TEMP_FAHRENHEIT if 'us' in self._dark_sky.units \ else TEMP_CELSIUS From e047e4dcffec96aca64ebb7aa165d9b7f8c3a478 Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Tue, 21 May 2019 20:57:24 +0100 Subject: [PATCH 103/232] bump geniushub-client to 0.4.9 (#24022) --- homeassistant/components/geniushub/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/geniushub/manifest.json b/homeassistant/components/geniushub/manifest.json index 06ba4644062..e1708d0e836 100644 --- a/homeassistant/components/geniushub/manifest.json +++ b/homeassistant/components/geniushub/manifest.json @@ -3,7 +3,7 @@ "name": "Genius Hub", "documentation": "https://www.home-assistant.io/components/geniushub", "requirements": [ - "geniushub-client==0.4.7" + "geniushub-client==0.4.9" ], "dependencies": [], "codeowners": ["@zxdavb"] diff --git a/requirements_all.txt b/requirements_all.txt index 9b0eea4ce47..d28d6e71a94 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -477,7 +477,7 @@ gearbest_parser==1.0.7 geizhals==0.0.9 # homeassistant.components.geniushub -geniushub-client==0.4.7 +geniushub-client==0.4.9 # homeassistant.components.geo_json_events # homeassistant.components.nsw_rural_fire_service_feed From 636077c74d643a4939a559405a2ac1ef693469ba Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 22 May 2019 00:36:26 +0200 Subject: [PATCH 104/232] Zeroconf discovery for config entries (#23919) * Proof of concept * Follow comments * Fix line length and bad imports * Move imports to top * Exception handling for unicode decoding Create debug print for new service types Add empty test files * First try at a test * Add type and name to service info Fix static check * Add aiozeroconf to test dependencies --- homeassistant/components/axis/config_flow.py | 2 +- homeassistant/components/axis/manifest.json | 1 + .../components/discovery/__init__.py | 2 - homeassistant/components/zeroconf/__init__.py | 62 +++++++++++++++++- homeassistant/generated/zeroconf.py | 11 ++++ requirements_test_all.txt | 3 + script/gen_requirements_all.py | 1 + script/hassfest/__main__.py | 4 +- script/hassfest/manifest.py | 1 + script/hassfest/zeroconf.py | 63 +++++++++++++++++++ tests/components/axis/test_config_flow.py | 30 ++++----- tests/components/zeroconf/__init__.py | 1 + tests/components/zeroconf/test_init.py | 40 ++++++++++++ 13 files changed, 199 insertions(+), 22 deletions(-) create mode 100644 homeassistant/generated/zeroconf.py create mode 100644 script/hassfest/zeroconf.py create mode 100644 tests/components/zeroconf/__init__.py create mode 100644 tests/components/zeroconf/test_init.py diff --git a/homeassistant/components/axis/config_flow.py b/homeassistant/components/axis/config_flow.py index 0c175de20c7..fc2051e4925 100644 --- a/homeassistant/components/axis/config_flow.py +++ b/homeassistant/components/axis/config_flow.py @@ -146,7 +146,7 @@ class AxisFlowHandler(config_entries.ConfigFlow): entry.data[CONF_DEVICE][CONF_HOST] = host self.hass.config_entries.async_update_entry(entry) - async def async_step_discovery(self, discovery_info): + async def async_step_zeroconf(self, discovery_info): """Prepare configuration for a discovered Axis device. This flow is triggered by the discovery component. diff --git a/homeassistant/components/axis/manifest.json b/homeassistant/components/axis/manifest.json index 507f63c12b5..27c108b334c 100644 --- a/homeassistant/components/axis/manifest.json +++ b/homeassistant/components/axis/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/components/axis", "requirements": ["axis==23"], "dependencies": [], + "zeroconf": ["_axis-video._tcp.local."], "codeowners": ["@kane610"] } diff --git a/homeassistant/components/discovery/__init__.py b/homeassistant/components/discovery/__init__.py index 99879b60e66..246a66eb9a1 100644 --- a/homeassistant/components/discovery/__init__.py +++ b/homeassistant/components/discovery/__init__.py @@ -24,7 +24,6 @@ DOMAIN = 'discovery' SCAN_INTERVAL = timedelta(seconds=300) SERVICE_APPLE_TV = 'apple_tv' -SERVICE_AXIS = 'axis' SERVICE_DAIKIN = 'daikin' SERVICE_DECONZ = 'deconz' SERVICE_DLNA_DMR = 'dlna_dmr' @@ -51,7 +50,6 @@ SERVICE_WINK = 'wink' SERVICE_XIAOMI_GW = 'xiaomi_gw' CONFIG_ENTRY_HANDLERS = { - SERVICE_AXIS: 'axis', SERVICE_DAIKIN: 'daikin', SERVICE_DECONZ: 'deconz', 'esphome': 'esphome', diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index e745cb53f6b..161321d1e88 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -1,14 +1,25 @@ """Support for exposing Home Assistant via Zeroconf.""" import logging +import ipaddress import voluptuous as vol +from aiozeroconf import ( + ServiceBrowser, ServiceInfo, ServiceStateChange, Zeroconf) + from homeassistant.const import (EVENT_HOMEASSISTANT_STOP, __version__) +from homeassistant.generated import zeroconf as zeroconf_manifest _LOGGER = logging.getLogger(__name__) DOMAIN = 'zeroconf' +ATTR_HOST = 'host' +ATTR_PORT = 'port' +ATTR_HOSTNAME = 'hostname' +ATTR_TYPE = 'type' +ATTR_NAME = 'name' +ATTR_PROPERTIES = 'properties' ZEROCONF_TYPE = '_home-assistant._tcp.local.' @@ -19,8 +30,6 @@ CONFIG_SCHEMA = vol.Schema({ async def async_setup(hass, config): """Set up Zeroconf and make Home Assistant discoverable.""" - from aiozeroconf import Zeroconf, ServiceInfo - zeroconf_name = '{}.{}'.format(hass.config.location_name, ZEROCONF_TYPE) params = { @@ -37,7 +46,28 @@ async def async_setup(hass, config): await zeroconf.register_service(info) - async def stop_zeroconf(event): + async def new_service(service_type, name): + """Signal new service discovered.""" + service_info = await zeroconf.get_service_info(service_type, name) + info = info_from_service(service_info) + _LOGGER.debug("Discovered new device %s %s", name, info) + + for domain in zeroconf_manifest.SERVICE_TYPES[service_type]: + hass.async_create_task( + hass.config_entries.flow.async_init( + domain, context={'source': DOMAIN}, data=info + ) + ) + + def service_update(_, service_type, name, state_change): + """Service state changed.""" + if state_change is ServiceStateChange.Added: + hass.async_create_task(new_service(service_type, name)) + + for service in zeroconf_manifest.SERVICE_TYPES: + ServiceBrowser(zeroconf, service, handlers=[service_update]) + + async def stop_zeroconf(_): """Stop Zeroconf.""" await zeroconf.unregister_service(info) await zeroconf.close() @@ -45,3 +75,29 @@ async def async_setup(hass, config): hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_zeroconf) return True + + +def info_from_service(service): + """Return prepared info from mDNS entries.""" + properties = {} + + for key, value in service.properties.items(): + try: + if isinstance(value, bytes): + value = value.decode('utf-8') + properties[key.decode('utf-8')] = value + except UnicodeDecodeError: + _LOGGER.warning("Unicode decode error on %s: %s", key, value) + + address = service.address or service.address6 + + info = { + ATTR_HOST: str(ipaddress.ip_address(address)), + ATTR_PORT: service.port, + ATTR_HOSTNAME: service.server, + ATTR_TYPE: service.type, + ATTR_NAME: service.name, + ATTR_PROPERTIES: properties, + } + + return info diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py new file mode 100644 index 00000000000..08c520b3816 --- /dev/null +++ b/homeassistant/generated/zeroconf.py @@ -0,0 +1,11 @@ +"""Automatically generated by hassfest. + +To update, run python3 -m hassfest +""" + + +SERVICE_TYPES = { + "_axis-video._tcp.local.": [ + "axis" + ] +} diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 826544c4e8d..f03364c7b0a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -57,6 +57,9 @@ aioswitcher==2019.3.21 # homeassistant.components.unifi aiounifi==4 +# homeassistant.components.zeroconf +aiozeroconf==0.1.8 + # homeassistant.components.ambiclimate ambiclimate==0.1.1 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 057f5c9fd24..108d0bcab07 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -50,6 +50,7 @@ TEST_REQUIREMENTS = ( 'aiohue', 'aiounifi', 'aioswitcher', + 'aiozeroconf', 'apns2', 'av', 'axis', diff --git a/script/hassfest/__main__.py b/script/hassfest/__main__.py index 9e7797201ea..6a6b19aada7 100644 --- a/script/hassfest/__main__.py +++ b/script/hassfest/__main__.py @@ -3,7 +3,8 @@ import pathlib import sys from .model import Integration, Config -from . import dependencies, manifest, codeowners, services, config_flow +from . import ( + dependencies, manifest, codeowners, services, config_flow, zeroconf) PLUGINS = [ manifest, @@ -11,6 +12,7 @@ PLUGINS = [ codeowners, services, config_flow, + zeroconf ] diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index 789b5fc0b41..cfb2fdc006a 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -11,6 +11,7 @@ MANIFEST_SCHEMA = vol.Schema({ vol.Required('domain'): str, vol.Required('name'): str, vol.Optional('config_flow'): bool, + vol.Optional('zeroconf'): [str], vol.Required('documentation'): str, vol.Required('requirements'): [str], vol.Required('dependencies'): [str], diff --git a/script/hassfest/zeroconf.py b/script/hassfest/zeroconf.py new file mode 100644 index 00000000000..468d2741dbd --- /dev/null +++ b/script/hassfest/zeroconf.py @@ -0,0 +1,63 @@ +"""Generate zeroconf file.""" +import json +from typing import Dict + +from .model import Integration, Config + +BASE = """ +\"\"\"Automatically generated by hassfest. + +To update, run python3 -m hassfest +\"\"\" + + +SERVICE_TYPES = {} +""".strip() + + +def generate_and_validate(integrations: Dict[str, Integration]): + """Validate and generate zeroconf data.""" + service_type_dict = {} + + for domain in sorted(integrations): + integration = integrations[domain] + + if not integration.manifest: + continue + + service_types = integration.manifest.get('zeroconf') + + if not service_types: + continue + + for service_type in service_types: + + if service_type not in service_type_dict: + service_type_dict[service_type] = [] + + service_type_dict[service_type].append(domain) + + return BASE.format(json.dumps(service_type_dict, indent=4)) + + +def validate(integrations: Dict[str, Integration], config: Config): + """Validate zeroconf file.""" + zeroconf_path = config.root / 'homeassistant/generated/zeroconf.py' + config.cache['zeroconf'] = content = generate_and_validate(integrations) + + with open(str(zeroconf_path), 'r') as fp: + if fp.read().strip() != content: + config.add_error( + "zeroconf", + "File zeroconf.py is not up to date. " + "Run python3 -m script.hassfest", + fixable=True + ) + return + + +def generate(integrations: Dict[str, Integration], config: Config): + """Generate zeroconf file.""" + zeroconf_path = config.root / 'homeassistant/generated/zeroconf.py' + with open(str(zeroconf_path), 'w') as fp: + fp.write(config.cache['zeroconf'] + '\n') diff --git a/tests/components/axis/test_config_flow.py b/tests/components/axis/test_config_flow.py index 1a83e9be8b5..ebd2062ee0f 100644 --- a/tests/components/axis/test_config_flow.py +++ b/tests/components/axis/test_config_flow.py @@ -161,8 +161,8 @@ async def test_flow_create_entry_more_entries(hass): assert result['data'][config_flow.CONF_NAME] == 'model 2' -async def test_discovery_flow(hass): - """Test that discovery for new devices work.""" +async def test_zeroconf_flow(hass): + """Test that zeroconf discovery for new devices work.""" with patch.object(axis, 'get_device', return_value=mock_coro(Mock())): result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, @@ -171,15 +171,15 @@ async def test_discovery_flow(hass): config_flow.CONF_PORT: 80, 'properties': {'macaddress': '1234'} }, - context={'source': 'discovery'} + context={'source': 'zeroconf'} ) assert result['type'] == 'form' assert result['step_id'] == 'user' -async def test_discovery_flow_known_device(hass): - """Test that discovery for known devices work. +async def test_zeroconf_flow_known_device(hass): + """Test that zeroconf discovery for known devices work. This is legacy support from devices registered with configurator. """ @@ -210,14 +210,14 @@ async def test_discovery_flow_known_device(hass): 'hostname': 'name', 'properties': {'macaddress': '1234ABCD'} }, - context={'source': 'discovery'} + context={'source': 'zeroconf'} ) assert result['type'] == 'create_entry' -async def test_discovery_flow_already_configured(hass): - """Test that discovery doesn't setup already configured devices.""" +async def test_zeroconf_flow_already_configured(hass): + """Test that zeroconf doesn't setup already configured devices.""" entry = MockConfigEntry( domain=axis.DOMAIN, data={axis.CONF_DEVICE: {axis.config_flow.CONF_HOST: '1.2.3.4'}, @@ -235,27 +235,27 @@ async def test_discovery_flow_already_configured(hass): 'hostname': 'name', 'properties': {'macaddress': '1234ABCD'} }, - context={'source': 'discovery'} + context={'source': 'zeroconf'} ) assert result['type'] == 'abort' assert result['reason'] == 'already_configured' -async def test_discovery_flow_ignore_link_local_address(hass): - """Test that discovery doesn't setup devices with link local addresses.""" +async def test_zeroconf_flow_ignore_link_local_address(hass): + """Test that zeroconf doesn't setup devices with link local addresses.""" result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, data={config_flow.CONF_HOST: '169.254.3.4'}, - context={'source': 'discovery'} + context={'source': 'zeroconf'} ) assert result['type'] == 'abort' assert result['reason'] == 'link_local_address' -async def test_discovery_flow_bad_config_file(hass): - """Test that discovery with bad config files abort.""" +async def test_zeroconf_flow_bad_config_file(hass): + """Test that zeroconf discovery with bad config files abort.""" with patch('homeassistant.components.axis.config_flow.load_json', return_value={'1234ABCD': { config_flow.CONF_HOST: '2.3.4.5', @@ -270,7 +270,7 @@ async def test_discovery_flow_bad_config_file(hass): config_flow.CONF_HOST: '1.2.3.4', 'properties': {'macaddress': '1234ABCD'} }, - context={'source': 'discovery'} + context={'source': 'zeroconf'} ) assert result['type'] == 'abort' diff --git a/tests/components/zeroconf/__init__.py b/tests/components/zeroconf/__init__.py new file mode 100644 index 00000000000..d702ef482d6 --- /dev/null +++ b/tests/components/zeroconf/__init__.py @@ -0,0 +1 @@ +"""Tests for the Zeroconf component.""" diff --git a/tests/components/zeroconf/test_init.py b/tests/components/zeroconf/test_init.py new file mode 100644 index 00000000000..b3257f57714 --- /dev/null +++ b/tests/components/zeroconf/test_init.py @@ -0,0 +1,40 @@ +"""Test Zeroconf component setup process.""" +from unittest.mock import patch + +from aiozeroconf import ServiceInfo, ServiceStateChange + +from homeassistant.setup import async_setup_component +from homeassistant.components import zeroconf + + +def service_update_mock(zeroconf, service, handlers): + """Call service update handler.""" + handlers[0]( + None, service, '{}.{}'.format('name', service), + ServiceStateChange.Added) + + +async def get_service_info_mock(service_type, name): + """Return service info for get_service_info.""" + return ServiceInfo( + service_type, name, address=b'\n\x00\x00\x14', port=80, weight=0, + priority=0, server='name.local.', + properties={b'macaddress': b'ABCDEF012345'}) + + +async def test_setup(hass): + """Test configured options for a device are loaded via config entry.""" + with patch.object(hass.config_entries, 'flow') as mock_config_flow, \ + patch.object(zeroconf, 'ServiceBrowser') as MockServiceBrowser, \ + patch.object(zeroconf.Zeroconf, 'get_service_info') as \ + mock_get_service_info: + + MockServiceBrowser.side_effect = service_update_mock + mock_get_service_info.side_effect = get_service_info_mock + + assert await async_setup_component( + hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) + await hass.async_block_till_done() + + assert len(MockServiceBrowser.mock_calls) == 1 + assert len(mock_config_flow.mock_calls) == 1 From fdf1fa48e385c8270fef8e89ceef50ba339b5fa7 Mon Sep 17 00:00:00 2001 From: zewelor Date: Wed, 22 May 2019 04:47:10 +0200 Subject: [PATCH 105/232] Improve yeelight imports (#24020) * Improve yeelight imports * Move import on top * Fix lint --- homeassistant/components/yeelight/__init__.py | 8 +--- homeassistant/components/yeelight/light.py | 38 ++++++------------- 2 files changed, 12 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/yeelight/__init__.py b/homeassistant/components/yeelight/__init__.py index dd89ed27f53..dabd66751fd 100644 --- a/homeassistant/components/yeelight/__init__.py +++ b/homeassistant/components/yeelight/__init__.py @@ -4,6 +4,7 @@ import logging from datetime import timedelta import voluptuous as vol +from yeelight import Bulb, BulbException from homeassistant.components.discovery import SERVICE_YEELIGHT from homeassistant.const import CONF_DEVICES, CONF_NAME, CONF_SCAN_INTERVAL, \ CONF_HOST, ATTR_ENTITY_ID @@ -184,7 +185,6 @@ class YeelightDevice: def bulb(self): """Return bulb device.""" if self._bulb_device is None: - from yeelight import Bulb, BulbException try: self._bulb_device = Bulb(self._ipaddr, model=self._model) # force init for type @@ -238,8 +238,6 @@ class YeelightDevice: def turn_on(self, duration=DEFAULT_TRANSITION, light_type=None): """Turn on device.""" - from yeelight import BulbException - try: self.bulb.turn_on(duration=duration, light_type=light_type) except BulbException as ex: @@ -248,8 +246,6 @@ class YeelightDevice: def turn_off(self, duration=DEFAULT_TRANSITION, light_type=None): """Turn off device.""" - from yeelight import BulbException - try: self.bulb.turn_off(duration=duration, light_type=light_type) except BulbException as ex: @@ -258,8 +254,6 @@ class YeelightDevice: def update(self): """Read new properties from the device.""" - from yeelight import BulbException - if not self.bulb: return diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index 8d48e695b31..33116d973e9 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -2,6 +2,8 @@ import logging import voluptuous as vol +from yeelight import (RGBTransition, SleepTransition, Flow, BulbException) +from yeelight.enums import PowerMode, LightType, BulbType from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.service import extract_entity_ids from homeassistant.util.color import ( @@ -92,8 +94,6 @@ def _transitions_config_parser(transitions): def _parse_custom_effects(effects_config): - from yeelight import Flow - effects = {} for config in effects_config: params = config[CONF_FLOW_PARAMS] @@ -113,7 +113,6 @@ def _parse_custom_effects(effects_config): def _cmd(func): """Define a wrapper to catch exceptions from the bulb.""" def _wrap(self, *args, **kwargs): - from yeelight import BulbException try: _LOGGER.debug("Calling %s with %s %s", func, args, kwargs) return func(self, *args, **kwargs) @@ -125,8 +124,6 @@ def _cmd(func): def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Yeelight bulbs.""" - from yeelight.enums import PowerMode - data_key = '{}_lights'.format(DATA_YEELIGHT) if not discovery_info: @@ -187,8 +184,6 @@ 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 @@ -347,12 +342,11 @@ class YeelightLight(Light): def update(self) -> None: """Update properties from the bulb.""" - from yeelight import BulbType, enums bulb_type = self._bulb.bulb_type if bulb_type == BulbType.Color: self._supported_features = SUPPORT_YEELIGHT_RGB - elif self.light_type == enums.LightType.Ambient: + elif self.light_type == LightType.Ambient: self._supported_features = SUPPORT_YEELIGHT_RGB elif bulb_type in (BulbType.WhiteTemp, BulbType.WhiteTempMood): if self._is_nightlight_enabled: @@ -423,8 +417,6 @@ class YeelightLight(Light): def set_flash(self, flash) -> None: """Activate flash.""" if flash: - from yeelight import (RGBTransition, SleepTransition, Flow, - BulbException) if self._bulb.last_properties["color_mode"] != 1: _LOGGER.error("Flash supported currently only in RGB mode.") return @@ -458,7 +450,6 @@ class YeelightLight(Light): def set_effect(self, effect) -> None: """Activate effect.""" if effect: - from yeelight import (Flow, BulbException) from yeelight.transitions import (disco, temp, strobe, pulse, strobe_color, alarm, police, police2, christmas, rgb, @@ -502,7 +493,6 @@ class YeelightLight(Light): def turn_on(self, **kwargs) -> None: """Turn the bulb on.""" - import yeelight brightness = kwargs.get(ATTR_BRIGHTNESS) colortemp = kwargs.get(ATTR_COLOR_TEMP) hs_color = kwargs.get(ATTR_HS_COLOR) @@ -519,7 +509,7 @@ class YeelightLight(Light): if self.config[CONF_MODE_MUSIC] and not self._bulb.music_mode: try: self.set_music_mode(self.config[CONF_MODE_MUSIC]) - except yeelight.BulbException as ex: + except BulbException as ex: _LOGGER.error("Unable to turn on music mode," "consider disabling it: %s", ex) @@ -530,7 +520,7 @@ class YeelightLight(Light): self.set_brightness(brightness, duration) self.set_flash(flash) self.set_effect(effect) - except yeelight.BulbException as ex: + except BulbException as ex: _LOGGER.error("Unable to set bulb properties: %s", ex) return @@ -540,7 +530,7 @@ class YeelightLight(Light): or rgb): try: self.set_default() - except yeelight.BulbException as ex: + except BulbException as ex: _LOGGER.error("Unable to set the defaults: %s", ex) return self.device.update() @@ -556,27 +546,23 @@ class YeelightLight(Light): def set_mode(self, mode: str): """Set a power mode.""" - import yeelight - try: - self._bulb.set_power_mode(yeelight.enums.PowerMode[mode.upper()]) + self._bulb.set_power_mode(PowerMode[mode.upper()]) self.device.update() - except yeelight.BulbException as ex: + except BulbException as ex: _LOGGER.error("Unable to set the power mode: %s", ex) def start_flow(self, transitions, count=0, action=ACTION_RECOVER): """Start flow.""" - import yeelight - try: - flow = yeelight.Flow( + flow = Flow( count=count, - action=yeelight.Flow.actions[action], + action=Flow.actions[action], transitions=transitions) self._bulb.start_flow(flow, light_type=self.light_type) self.device.update() - except yeelight.BulbException as ex: + except BulbException as ex: _LOGGER.error("Unable to set effect: %s", ex) @@ -590,8 +576,6 @@ 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) From 806903ffe06bd5e227fd8515f82bb82d1170fce5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 22 May 2019 05:59:16 -0700 Subject: [PATCH 106/232] Downgrade Hue warning (#24033) --- homeassistant/components/hue/bridge.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py index 25db031e6bf..0610278de7b 100644 --- a/homeassistant/components/hue/bridge.py +++ b/homeassistant/components/hue/bridge.py @@ -60,6 +60,7 @@ class HueBridge: return False except CannotConnect: + LOGGER.error("Error connecting to the Hue bridge at %s", host) raise ConfigEntryNotReady except Exception: # pylint: disable=broad-except @@ -161,10 +162,8 @@ async def get_bridge(hass, host, username=None): return bridge except (aiohue.LinkButtonNotPressed, aiohue.Unauthorized): - LOGGER.warning("Connected to Hue at %s but not registered.", host) raise AuthenticationRequired except (asyncio.TimeoutError, aiohue.RequestError): - LOGGER.error("Error connecting to the Hue bridge at %s", host) raise CannotConnect except aiohue.AiohueException: LOGGER.exception('Unknown Hue linking error occurred') From 6b3bb3347be6e35fc0b60a64da4594c5ab432e61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Wed, 22 May 2019 15:00:05 +0200 Subject: [PATCH 107/232] Ambiclimate test, mock (#24034) --- tests/components/ambiclimate/test_config_flow.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/components/ambiclimate/test_config_flow.py b/tests/components/ambiclimate/test_config_flow.py index f61665b9e5b..64c3644aaa2 100644 --- a/tests/components/ambiclimate/test_config_flow.py +++ b/tests/components/ambiclimate/test_config_flow.py @@ -86,12 +86,14 @@ async def test_full_flow_implementation(hass): assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT -async def test_abort_no_code(hass): +async def test_abort_invalid_code(hass): """Test if no code is given to step_code.""" config_flow.register_flow_implementation(hass, None, None) flow = await init_config_flow(hass) - result = await flow.async_step_code('invalid') + with patch('ambiclimate.AmbiclimateOAuth.get_access_token', + return_value=mock_coro(None)): + result = await flow.async_step_code('invalid') assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT assert result['reason'] == 'access_token' From f207e015109a3d102619684863798ffb3fe1a2b2 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Wed, 22 May 2019 23:05:03 +0200 Subject: [PATCH 108/232] Upgrade Mastodon.py to 1.4.2 (#24004) * Upgrade Mastodon.py to 1.4.2 * Update --- homeassistant/components/mastodon/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mastodon/manifest.json b/homeassistant/components/mastodon/manifest.json index 730fe866a5d..6db3791c519 100644 --- a/homeassistant/components/mastodon/manifest.json +++ b/homeassistant/components/mastodon/manifest.json @@ -3,7 +3,7 @@ "name": "Mastodon", "documentation": "https://www.home-assistant.io/components/mastodon", "requirements": [ - "Mastodon.py==1.4.0" + "Mastodon.py==1.4.2" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index d28d6e71a94..6e82a9022a5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -36,7 +36,7 @@ Adafruit-SHT31==1.0.2 HAP-python==2.5.0 # homeassistant.components.mastodon -Mastodon.py==1.4.0 +Mastodon.py==1.4.2 # homeassistant.components.orangepi_gpio OPi.GPIO==0.3.6 From 9e96397e6a64774bcb5a7ea39e30609d19277d06 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 22 May 2019 17:24:46 -0700 Subject: [PATCH 109/232] Require core config detection to be triggerd manually (#24019) * Detect core config * Remove elevation * Lint * Lint * Fix type --- homeassistant/components/config/core.py | 69 ++++++++-- .../components/onboarding/__init__.py | 10 +- homeassistant/components/onboarding/const.py | 2 + homeassistant/components/onboarding/views.py | 27 +++- homeassistant/config.py | 125 ++---------------- homeassistant/core.py | 14 +- homeassistant/util/location.py | 24 ---- tests/components/config/test_core.py | 76 ++++++++--- tests/conftest.py | 2 - tests/test_config.py | 123 +++-------------- tests/test_core.py | 10 +- tests/util/test_location.py | 29 +--- 12 files changed, 188 insertions(+), 323 deletions(-) diff --git a/homeassistant/components/config/core.py b/homeassistant/components/config/core.py index 7e9008eb115..31abb832f23 100644 --- a/homeassistant/components/config/core.py +++ b/homeassistant/components/config/core.py @@ -5,12 +5,18 @@ import voluptuous as vol from homeassistant.components.http import HomeAssistantView from homeassistant.config import async_check_ha_config_file from homeassistant.components import websocket_api +from homeassistant.const import ( + CONF_UNIT_SYSTEM_METRIC, CONF_UNIT_SYSTEM_IMPERIAL +) +from homeassistant.helpers import config_validation as cv +from homeassistant.util import location async def async_setup(hass): """Set up the Hassbian config.""" hass.http.register_view(CheckConfigView) - hass.components.websocket_api.async_register_command(websocket_core_update) + websocket_api.async_register_command(hass, websocket_update_config) + websocket_api.async_register_command(hass, websocket_detect_config) return True @@ -35,18 +41,57 @@ class CheckConfigView(HomeAssistantView): @websocket_api.require_admin @websocket_api.async_response @websocket_api.websocket_command({ - vol.Required('type'): 'config/core/update', - vol.Optional('latitude'): vol.Coerce(float), - vol.Optional('longitude'): vol.Coerce(float), - vol.Optional('elevation'): vol.Coerce(int), - vol.Optional('unit_system'): vol.Coerce(str), - vol.Optional('location_name'): vol.Coerce(str), - vol.Optional('time_zone'): vol.Coerce(str), + 'type': 'config/core/update', + vol.Optional('latitude'): cv.latitude, + vol.Optional('longitude'): cv.longitude, + vol.Optional('elevation'): int, + vol.Optional('unit_system'): cv.unit_system, + vol.Optional('location_name'): str, + vol.Optional('time_zone'): cv.time_zone, }) -async def websocket_core_update(hass, connection, msg): - """Handle request for account info.""" +async def websocket_update_config(hass, connection, msg): + """Handle update core config command.""" data = dict(msg) data.pop('id') data.pop('type') - await hass.config.update(**data) - connection.send_result(msg['id']) + + try: + await hass.config.update(**data) + connection.send_result(msg['id']) + except ValueError as err: + connection.send_error( + msg['id'], 'invalid_info', str(err) + ) + + +@websocket_api.require_admin +@websocket_api.async_response +@websocket_api.websocket_command({ + 'type': 'config/core/detect', +}) +async def websocket_detect_config(hass, connection, msg): + """Detect core config.""" + session = hass.helpers.aiohttp_client.async_get_clientsession() + location_info = await location.async_detect_location_info(session) + + info = {} + + if location_info is None: + connection.send_result(msg['id'], info) + return + + if location_info.use_metric: + info['unit_system'] = CONF_UNIT_SYSTEM_METRIC + else: + info['unit_system'] = CONF_UNIT_SYSTEM_IMPERIAL + + if location_info.latitude: + info['latitude'] = location_info.latitude + + if location_info.longitude: + info['longitude'] = location_info.longitude + + if location_info.time_zone: + info['time_zone'] = location_info.time_zone + + connection.send_result(msg['id'], info) diff --git a/homeassistant/components/onboarding/__init__.py b/homeassistant/components/onboarding/__init__.py index 55bba8f4efe..f5ed1a9b271 100644 --- a/homeassistant/components/onboarding/__init__.py +++ b/homeassistant/components/onboarding/__init__.py @@ -3,10 +3,11 @@ from homeassistant.core import callback from homeassistant.loader import bind_hass from homeassistant.helpers.storage import Store -from .const import DOMAIN, STEP_USER, STEPS, STEP_INTEGRATION +from .const import ( + DOMAIN, STEP_USER, STEPS, STEP_INTEGRATION, STEP_CORE_CONFIG) STORAGE_KEY = DOMAIN -STORAGE_VERSION = 2 +STORAGE_VERSION = 3 class OnboadingStorage(Store): @@ -15,7 +16,10 @@ class OnboadingStorage(Store): async def _async_migrate_func(self, old_version, old_data): """Migrate to the new version.""" # From version 1 -> 2, we automatically mark the integration step done - old_data['done'].append(STEP_INTEGRATION) + if old_version < 2: + old_data['done'].append(STEP_INTEGRATION) + if old_version < 3: + old_data['done'].append(STEP_CORE_CONFIG) return old_data diff --git a/homeassistant/components/onboarding/const.py b/homeassistant/components/onboarding/const.py index fe1b28fc316..bdc573efcb4 100644 --- a/homeassistant/components/onboarding/const.py +++ b/homeassistant/components/onboarding/const.py @@ -1,10 +1,12 @@ """Constants for the onboarding component.""" DOMAIN = 'onboarding' STEP_USER = 'user' +STEP_CORE_CONFIG = 'core_config' STEP_INTEGRATION = 'integration' STEPS = [ STEP_USER, + STEP_CORE_CONFIG, STEP_INTEGRATION, ] diff --git a/homeassistant/components/onboarding/views.py b/homeassistant/components/onboarding/views.py index a156fe4676f..c8060891fd4 100644 --- a/homeassistant/components/onboarding/views.py +++ b/homeassistant/components/onboarding/views.py @@ -7,13 +7,16 @@ from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.components.http.view import HomeAssistantView from homeassistant.core import callback -from .const import DOMAIN, STEP_USER, STEPS, DEFAULT_AREAS, STEP_INTEGRATION +from .const import ( + DOMAIN, STEP_USER, STEPS, DEFAULT_AREAS, STEP_INTEGRATION, + STEP_CORE_CONFIG) async def async_setup(hass, data, store): """Set up the onboarding view.""" hass.http.register_view(OnboardingView(data, store)) hass.http.register_view(UserOnboardingView(data, store)) + hass.http.register_view(CoreConfigOnboardingView(data, store)) hass.http.register_view(IntegrationOnboardingView(data, store)) @@ -128,6 +131,26 @@ class UserOnboardingView(_BaseOnboardingView): }) +class CoreConfigOnboardingView(_BaseOnboardingView): + """View to finish core config onboarding step.""" + + url = '/api/onboarding/core_config' + name = 'api:onboarding:core_config' + step = STEP_CORE_CONFIG + + async def post(self, request): + """Handle finishing core config step.""" + hass = request.app['hass'] + + async with self._lock: + if self._async_is_done(): + return self.json_message('Core config step already done', 403) + + await self._async_mark_done(hass) + + return self.json({}) + + class IntegrationOnboardingView(_BaseOnboardingView): """View to finish integration onboarding step.""" @@ -139,7 +162,7 @@ class IntegrationOnboardingView(_BaseOnboardingView): vol.Required('client_id'): str, })) async def post(self, request, data): - """Handle user creation, area creation.""" + """Handle token creation.""" hass = request.app['hass'] user = request['hass_user'] diff --git a/homeassistant/config.py b/homeassistant/config.py index ec83818d903..cffaffd8985 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -18,13 +18,13 @@ from homeassistant.auth import providers as auth_providers,\ from homeassistant.const import ( ATTR_FRIENDLY_NAME, ATTR_HIDDEN, ATTR_ASSUMED_STATE, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, CONF_PACKAGES, CONF_UNIT_SYSTEM, - CONF_TIME_ZONE, CONF_ELEVATION, CONF_UNIT_SYSTEM_METRIC, + CONF_TIME_ZONE, CONF_ELEVATION, CONF_UNIT_SYSTEM_IMPERIAL, CONF_TEMPERATURE_UNIT, TEMP_CELSIUS, __version__, CONF_CUSTOMIZE, CONF_CUSTOMIZE_DOMAIN, CONF_CUSTOMIZE_GLOB, CONF_WHITELIST_EXTERNAL_DIRS, CONF_AUTH_PROVIDERS, CONF_AUTH_MFA_MODULES, CONF_TYPE, CONF_ID) from homeassistant.core import ( - DOMAIN as CONF_CORE, SOURCE_DISCOVERED, SOURCE_YAML, HomeAssistant, + DOMAIN as CONF_CORE, SOURCE_YAML, HomeAssistant, callback) from homeassistant.exceptions import HomeAssistantError from homeassistant.loader import ( @@ -32,7 +32,6 @@ from homeassistant.loader import ( ) from homeassistant.util.yaml import load_yaml, SECRET_YAML import homeassistant.helpers.config_validation as cv -from homeassistant.util import location as loc_util from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM from homeassistant.helpers.entity_values import EntityValues from homeassistant.helpers import config_per_platform, extract_domain_configs @@ -52,22 +51,6 @@ FILE_MIGRATION = ( ('ios.conf', '.ios.conf'), ) -DEFAULT_CORE_CONFIG = ( - # Tuples (attribute, default, auto detect property, description) - (CONF_NAME, 'Home', None, 'Name of the location where Home Assistant is ' - 'running'), - (CONF_LATITUDE, 0, 'latitude', 'Location required to calculate the time' - ' the sun rises and sets'), - (CONF_LONGITUDE, 0, 'longitude', None), - (CONF_ELEVATION, 0, None, 'Impacts weather/sunrise data' - ' (altitude above sea level in meters)'), - (CONF_UNIT_SYSTEM, CONF_UNIT_SYSTEM_METRIC, None, - '{} for Metric, {} for Imperial'.format(CONF_UNIT_SYSTEM_METRIC, - CONF_UNIT_SYSTEM_IMPERIAL)), - (CONF_TIME_ZONE, 'UTC', 'time_zone', 'Pick yours from here: http://en.wiki' - 'pedia.org/wiki/List_of_tz_database_time_zones'), - (CONF_CUSTOMIZE, '!include customize.yaml', None, 'Customization file'), -) # type: Tuple[Tuple[str, Any, Any, Optional[str]], ...] DEFAULT_CONFIG = """ # Configure a default setup of Home Assistant (frontend, api, etc) default_config: @@ -207,8 +190,7 @@ def get_default_config_dir() -> str: return os.path.join(data_dir, CONFIG_DIR_NAME) # type: ignore -async def async_ensure_config_exists(hass: HomeAssistant, config_dir: str, - detect_location: bool = True)\ +async def async_ensure_config_exists(hass: HomeAssistant, config_dir: str) \ -> Optional[str]: """Ensure a configuration file exists in given configuration directory. @@ -220,49 +202,22 @@ async def async_ensure_config_exists(hass: HomeAssistant, config_dir: str, if config_path is None: print("Unable to find configuration. Creating default one in", config_dir) - config_path = await async_create_default_config( - hass, config_dir, detect_location) + config_path = await async_create_default_config(hass, config_dir) return config_path -async def async_create_default_config( - hass: HomeAssistant, config_dir: str, detect_location: bool = True - ) -> Optional[str]: +async def async_create_default_config(hass: HomeAssistant, config_dir: str) \ + -> Optional[str]: """Create a default configuration file in given configuration directory. Return path to new config file if success, None if failed. This method needs to run in an executor. """ - info = {attr: default for attr, default, _, _ in DEFAULT_CORE_CONFIG} - - if detect_location: - session = hass.helpers.aiohttp_client.async_get_clientsession() - location_info = await loc_util.async_detect_location_info(session) - else: - location_info = None - - if location_info: - if location_info.use_metric: - info[CONF_UNIT_SYSTEM] = CONF_UNIT_SYSTEM_METRIC - else: - info[CONF_UNIT_SYSTEM] = CONF_UNIT_SYSTEM_IMPERIAL - - for attr, default, prop, _ in DEFAULT_CORE_CONFIG: - if prop is None: - continue - info[attr] = getattr(location_info, prop) or default - - if location_info.latitude and location_info.longitude: - info[CONF_ELEVATION] = await loc_util.async_get_elevation( - session, location_info.latitude, location_info.longitude) - - return await hass.async_add_executor_job( - _write_default_config, config_dir, info - ) + return await hass.async_add_executor_job(_write_default_config, config_dir) -def _write_default_config(config_dir: str, info: Dict)\ +def _write_default_config(config_dir: str)\ -> Optional[str]: """Write the default config.""" from homeassistant.components.config.group import ( @@ -271,8 +226,6 @@ def _write_default_config(config_dir: str, info: Dict)\ CONFIG_PATH as AUTOMATION_CONFIG_PATH) from homeassistant.components.config.script import ( CONFIG_PATH as SCRIPT_CONFIG_PATH) - from homeassistant.components.config.customize import ( - CONFIG_PATH as CUSTOMIZE_CONFIG_PATH) config_path = os.path.join(config_dir, YAML_CONFIG_FILE) secret_path = os.path.join(config_dir, SECRET_YAML) @@ -280,21 +233,11 @@ def _write_default_config(config_dir: str, info: Dict)\ group_yaml_path = os.path.join(config_dir, GROUP_CONFIG_PATH) automation_yaml_path = os.path.join(config_dir, AUTOMATION_CONFIG_PATH) script_yaml_path = os.path.join(config_dir, SCRIPT_CONFIG_PATH) - customize_yaml_path = os.path.join(config_dir, CUSTOMIZE_CONFIG_PATH) # Writing files with YAML does not create the most human readable results # So we're hard coding a YAML template. try: with open(config_path, 'wt') as config_file: - config_file.write("homeassistant:\n") - - for attr, _, _, description in DEFAULT_CORE_CONFIG: - if info[attr] is None: - continue - elif description: - config_file.write(" # {}\n".format(description)) - config_file.write(" {}: {}\n".format(attr, info[attr])) - config_file.write(DEFAULT_CONFIG) with open(secret_path, 'wt') as secret_file: @@ -312,9 +255,6 @@ def _write_default_config(config_dir: str, info: Dict)\ with open(script_yaml_path, 'wt'): pass - with open(customize_yaml_path, 'wt'): - pass - return config_path except IOError: @@ -576,55 +516,6 @@ async def async_process_ha_core_config( "with '%s: %s'", CONF_TEMPERATURE_UNIT, unit, CONF_UNIT_SYSTEM, hac.units.name) - # Shortcut if no auto-detection necessary - if None not in (hac.latitude, hac.longitude, hac.units, - hac.time_zone, hac.elevation): - return - - discovered = [] # type: List[Tuple[str, Any]] - - # If we miss some of the needed values, auto detect them - if None in (hac.latitude, hac.longitude, hac.units, - hac.time_zone): - hac.config_source = SOURCE_DISCOVERED - info = await loc_util.async_detect_location_info( - hass.helpers.aiohttp_client.async_get_clientsession() - ) - - if info is None: - _LOGGER.error("Could not detect location information") - return - - if hac.latitude is None and hac.longitude is None: - hac.latitude, hac.longitude = (info.latitude, info.longitude) - discovered.append(('latitude', hac.latitude)) - discovered.append(('longitude', hac.longitude)) - - if hac.units is None: - hac.units = METRIC_SYSTEM if info.use_metric else IMPERIAL_SYSTEM - discovered.append((CONF_UNIT_SYSTEM, hac.units.name)) - - if hac.location_name is None: - hac.location_name = info.city - discovered.append(('name', info.city)) - - if hac.time_zone is None: - hac.set_time_zone(info.time_zone) - discovered.append(('time_zone', info.time_zone)) - - if hac.elevation is None and hac.latitude is not None and \ - hac.longitude is not None: - elevation = await loc_util.async_get_elevation( - hass.helpers.aiohttp_client.async_get_clientsession(), - hac.latitude, hac.longitude) - hac.elevation = elevation - discovered.append(('elevation', elevation)) - - if discovered: - _LOGGER.warning( - "Incomplete core configuration. Auto detected %s", - ", ".join('{}: {}'.format(key, val) for key, val in discovered)) - def _log_pkg_error( package: str, component: str, config: Dict, message: str) -> None: diff --git a/homeassistant/core.py b/homeassistant/core.py index 4dd84cc1a46..5941739fcc5 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -57,7 +57,7 @@ CALLABLE_T = TypeVar('CALLABLE_T', bound=Callable) CALLBACK_TYPE = Callable[[], None] # pylint: enable=invalid-name -CORE_STORAGE_KEY = 'homeassistant.core_config' +CORE_STORAGE_KEY = 'core.config' CORE_STORAGE_VERSION = 1 DOMAIN = 'homeassistant' @@ -1181,14 +1181,14 @@ class Config: """Initialize a new config object.""" self.hass = hass - self.latitude = None # type: Optional[float] - self.longitude = None # type: Optional[float] - self.elevation = None # type: Optional[int] - self.location_name = None # type: Optional[str] - self.time_zone = None # type: Optional[datetime.tzinfo] + self.latitude = 0 # type: float + self.longitude = 0 # type: float + self.elevation = 0 # type: int + self.location_name = "Home" # type: str + self.time_zone = dt_util.UTC # type: datetime.tzinfo self.units = METRIC_SYSTEM # type: UnitSystem - self.config_source = None # type: Optional[str] + self.config_source = "default" # type: str # If True, pip install is skipped for requirements on startup self.skip_pip = False # type: bool diff --git a/homeassistant/util/location.py b/homeassistant/util/location.py index 1d13bcf0ce5..bacffa9da42 100644 --- a/homeassistant/util/location.py +++ b/homeassistant/util/location.py @@ -65,30 +65,6 @@ def distance(lat1: Optional[float], lon1: Optional[float], return result * 1000 -async def async_get_elevation(session: aiohttp.ClientSession, latitude: float, - longitude: float) -> int: - """Return elevation for given latitude and longitude.""" - try: - resp = await session.get(ELEVATION_URL, params={ - 'locations': '{},{}'.format(latitude, longitude), - }, timeout=5) - except (aiohttp.ClientError, asyncio.TimeoutError): - return 0 - - if resp.status != 200: - return 0 - - try: - raw_info = await resp.json() - except (aiohttp.ClientError, ValueError): - return 0 - - try: - return int(float(raw_info['results'][0]['elevation'])) - except (ValueError, KeyError, IndexError): - return 0 - - # Author: https://github.com/maurycyp # Source: https://github.com/maurycyp/vincenty # License: https://github.com/maurycyp/vincenty/blob/master/LICENSE diff --git a/tests/components/config/test_core.py b/tests/components/config/test_core.py index 81b3818a815..e58971a4cd8 100644 --- a/tests/components/config/test_core.py +++ b/tests/components/config/test_core.py @@ -1,24 +1,31 @@ """Test hassbian config.""" -import asyncio from unittest.mock import patch +import pytest + from homeassistant.bootstrap import async_setup_component from homeassistant.components import config from homeassistant.components.websocket_api.const import TYPE_RESULT from homeassistant.const import CONF_UNIT_SYSTEM, CONF_UNIT_SYSTEM_IMPERIAL -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util, location from tests.common import mock_coro ORIG_TIME_ZONE = dt_util.DEFAULT_TIME_ZONE +@pytest.fixture +async def client(hass, hass_ws_client): + """Fixture that can interact with the config manager API.""" + with patch.object(config, 'SECTIONS', ['core']): + assert await async_setup_component(hass, 'config', {}) + return await hass_ws_client(hass) + + async def test_validate_config_ok(hass, hass_client): """Test checking config.""" with patch.object(config, 'SECTIONS', ['core']): await async_setup_component(hass, 'config', {}) - await asyncio.sleep(0.1, loop=hass.loop) - client = await hass_client() with patch( @@ -42,11 +49,8 @@ async def test_validate_config_ok(hass, hass_client): assert result['errors'] == 'beer' -async def test_websocket_core_update(hass, hass_ws_client): +async def test_websocket_core_update(hass, client): """Test core config update websocket command.""" - with patch.object(config, 'SECTIONS', ['core']): - await async_setup_component(hass, 'config', {}) - assert hass.config.latitude != 60 assert hass.config.longitude != 50 assert hass.config.elevation != 25 @@ -54,7 +58,6 @@ async def test_websocket_core_update(hass, hass_ws_client): assert hass.config.units.name != CONF_UNIT_SYSTEM_IMPERIAL assert hass.config.time_zone.zone != 'America/New_York' - client = await hass_ws_client(hass) await client.send_json({ 'id': 5, 'type': 'config/core/update', @@ -92,7 +95,7 @@ async def test_websocket_core_update_not_admin( await client.send_json({ 'id': 6, 'type': 'config/core/update', - 'latitude': 123, + 'latitude': 23, }) msg = await client.receive_json() @@ -103,16 +106,12 @@ async def test_websocket_core_update_not_admin( assert msg['error']['code'] == 'unauthorized' -async def test_websocket_bad_core_update(hass, hass_ws_client): +async def test_websocket_bad_core_update(hass, client): """Test core config update fails with bad parameters.""" - with patch.object(config, 'SECTIONS', ['core']): - await async_setup_component(hass, 'config', {}) - - client = await hass_ws_client(hass) await client.send_json({ 'id': 7, 'type': 'config/core/update', - 'latituude': 123, + 'latituude': 23, }) msg = await client.receive_json() @@ -121,3 +120,48 @@ async def test_websocket_bad_core_update(hass, hass_ws_client): assert msg['type'] == TYPE_RESULT assert not msg['success'] assert msg['error']['code'] == 'invalid_format' + + +async def test_detect_config(hass, client): + """Test detect config.""" + with patch('homeassistant.util.location.async_detect_location_info', + return_value=mock_coro(None)): + await client.send_json({ + 'id': 1, + 'type': 'config/core/detect', + }) + + msg = await client.receive_json() + + assert msg['success'] is True + assert msg['result'] == {} + + +async def test_detect_config_fail(hass, client): + """Test detect config.""" + with patch('homeassistant.util.location.async_detect_location_info', + return_value=mock_coro(location.LocationInfo( + ip=None, + country_code=None, + country_name=None, + region_code=None, + region_name=None, + city=None, + zip_code=None, + latitude=None, + longitude=None, + use_metric=True, + time_zone='Europe/Amsterdam', + ))): + await client.send_json({ + 'id': 1, + 'type': 'config/core/detect', + }) + + msg = await client.receive_json() + + assert msg['success'] is True + assert msg['result'] == { + 'unit_system': 'metric', + 'time_zone': 'Europe/Amsterdam', + } diff --git a/tests/conftest.py b/tests/conftest.py index fdac037bfa9..83a175656d7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -44,8 +44,6 @@ def check_real(func): # Guard a few functions that would make network connections location.async_detect_location_info = \ check_real(location.async_detect_location_info) -location.async_get_elevation = \ - check_real(location.async_get_elevation) util.get_local_ip = lambda: '127.0.0.1' diff --git a/tests/test_config.py b/tests/test_config.py index 42386cc1f4c..5579679937b 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -12,17 +12,16 @@ import pytest from voluptuous import MultipleInvalid, Invalid import yaml -from homeassistant.core import ( - DOMAIN, SOURCE_STORAGE, Config, HomeAssistantError) +from homeassistant.core import SOURCE_STORAGE, HomeAssistantError import homeassistant.config as config_util from homeassistant.loader import async_get_integration from homeassistant.const import ( ATTR_FRIENDLY_NAME, ATTR_HIDDEN, ATTR_ASSUMED_STATE, CONF_LATITUDE, CONF_LONGITUDE, CONF_UNIT_SYSTEM, CONF_NAME, - CONF_TIME_ZONE, CONF_ELEVATION, CONF_CUSTOMIZE, __version__, + CONF_CUSTOMIZE, __version__, CONF_UNIT_SYSTEM_METRIC, CONF_UNIT_SYSTEM_IMPERIAL, CONF_TEMPERATURE_UNIT, CONF_AUTH_PROVIDERS, CONF_AUTH_MFA_MODULES) -from homeassistant.util import location as location_util, dt as dt_util +from homeassistant.util import dt as dt_util from homeassistant.util.yaml import SECRET_YAML from homeassistant.helpers.entity import Entity from homeassistant.components.config.group import ( @@ -31,12 +30,10 @@ from homeassistant.components.config.automation import ( CONFIG_PATH as AUTOMATIONS_CONFIG_PATH) from homeassistant.components.config.script import ( CONFIG_PATH as SCRIPTS_CONFIG_PATH) -from homeassistant.components.config.customize import ( - CONFIG_PATH as CUSTOMIZE_CONFIG_PATH) import homeassistant.scripts.check_config as check_config from tests.common import ( - get_test_config_dir, patch_yaml_files, mock_coro) + get_test_config_dir, patch_yaml_files) CONFIG_DIR = get_test_config_dir() YAML_PATH = os.path.join(CONFIG_DIR, config_util.YAML_CONFIG_FILE) @@ -45,7 +42,6 @@ VERSION_PATH = os.path.join(CONFIG_DIR, config_util.VERSION_FILE) GROUP_PATH = os.path.join(CONFIG_DIR, GROUP_CONFIG_PATH) AUTOMATIONS_PATH = os.path.join(CONFIG_DIR, AUTOMATIONS_CONFIG_PATH) SCRIPTS_PATH = os.path.join(CONFIG_DIR, SCRIPTS_CONFIG_PATH) -CUSTOMIZE_PATH = os.path.join(CONFIG_DIR, CUSTOMIZE_CONFIG_PATH) ORIG_TIMEZONE = dt_util.DEFAULT_TIME_ZONE @@ -77,20 +73,16 @@ def teardown(): if os.path.isfile(SCRIPTS_PATH): os.remove(SCRIPTS_PATH) - if os.path.isfile(CUSTOMIZE_PATH): - os.remove(CUSTOMIZE_PATH) - async def test_create_default_config(hass): """Test creation of default config.""" - await config_util.async_create_default_config(hass, CONFIG_DIR, False) + await config_util.async_create_default_config(hass, CONFIG_DIR) assert os.path.isfile(YAML_PATH) assert os.path.isfile(SECRET_PATH) assert os.path.isfile(VERSION_PATH) assert os.path.isfile(GROUP_PATH) assert os.path.isfile(AUTOMATIONS_PATH) - assert os.path.isfile(CUSTOMIZE_PATH) def test_find_config_file_yaml(): @@ -106,7 +98,7 @@ async def test_ensure_config_exists_creates_config(hass): If not creates a new config file. """ with mock.patch('builtins.print') as mock_print: - await config_util.async_ensure_config_exists(hass, CONFIG_DIR, False) + await config_util.async_ensure_config_exists(hass, CONFIG_DIR) assert os.path.isfile(YAML_PATH) assert mock_print.called @@ -115,7 +107,7 @@ async def test_ensure_config_exists_creates_config(hass): async def test_ensure_config_exists_uses_existing_config(hass): """Test that calling ensure_config_exists uses existing config.""" create_file(YAML_PATH) - await config_util.async_ensure_config_exists(hass, CONFIG_DIR, False) + await config_util.async_ensure_config_exists(hass, CONFIG_DIR) with open(YAML_PATH) as f: content = f.read() @@ -168,38 +160,6 @@ def test_load_yaml_config_preserves_key_order(): list(config_util.load_yaml_config_file(YAML_PATH).items()) -async def test_create_default_config_detect_location(hass): - """Test that detect location sets the correct config keys.""" - with mock.patch('homeassistant.util.location.async_detect_location_info', - return_value=mock_coro(location_util.LocationInfo( - '0.0.0.0', 'US', 'United States', 'CA', 'California', - 'San Diego', '92122', 'America/Los_Angeles', 32.8594, - -117.2073, True))), \ - mock.patch('homeassistant.util.location.async_get_elevation', - return_value=mock_coro(101)), \ - mock.patch('builtins.print') as mock_print: - await config_util.async_ensure_config_exists(hass, CONFIG_DIR) - - config = config_util.load_yaml_config_file(YAML_PATH) - - assert DOMAIN in config - - ha_conf = config[DOMAIN] - - expected_values = { - CONF_LATITUDE: 32.8594, - CONF_LONGITUDE: -117.2073, - CONF_ELEVATION: 101, - CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC, - CONF_NAME: 'Home', - CONF_TIME_ZONE: 'America/Los_Angeles', - CONF_CUSTOMIZE: OrderedDict(), - } - - assert expected_values == ha_conf - assert mock_print.called - - async def test_create_default_config_returns_none_if_write_error(hass): """Test the writing of a default configuration. @@ -207,7 +167,7 @@ async def test_create_default_config_returns_none_if_write_error(hass): """ with mock.patch('builtins.print') as mock_print: assert await config_util.async_create_default_config( - hass, os.path.join(CONFIG_DIR, 'non_existing_dir/'), False) is None + hass, os.path.join(CONFIG_DIR, 'non_existing_dir/')) is None assert mock_print.called @@ -418,7 +378,7 @@ def test_migrate_no_file_on_upgrade(mock_os, mock_shutil, hass): async def test_loading_configuration_from_storage(hass, hass_storage): """Test loading core config onto hass object.""" - hass_storage["homeassistant.core_config"] = { + hass_storage["core.config"] = { 'data': { 'elevation': 10, 'latitude': 55, @@ -427,7 +387,7 @@ async def test_loading_configuration_from_storage(hass, hass_storage): 'time_zone': 'Europe/Copenhagen', 'unit_system': 'metric' }, - 'key': 'homeassistant.core_config', + 'key': 'core.config', 'version': 1 } await config_util.async_process_ha_core_config( @@ -455,23 +415,23 @@ async def test_updating_configuration(hass, hass_storage): 'time_zone': 'Europe/Copenhagen', 'unit_system': 'metric' }, - 'key': 'homeassistant.core_config', + 'key': 'core.config', 'version': 1 } - hass_storage["homeassistant.core_config"] = dict(core_data) + hass_storage["core.config"] = dict(core_data) await config_util.async_process_ha_core_config( hass, {'whitelist_external_dirs': '/tmp'}) await hass.config.update(latitude=50) new_core_data = copy.deepcopy(core_data) new_core_data['data']['latitude'] = 50 - assert hass_storage["homeassistant.core_config"] == new_core_data + assert hass_storage["core.config"] == new_core_data assert hass.config.latitude == 50 async def test_override_stored_configuration(hass, hass_storage): """Test loading core and YAML config onto hass object.""" - hass_storage["homeassistant.core_config"] = { + hass_storage["core.config"] = { 'data': { 'elevation': 10, 'latitude': 55, @@ -480,7 +440,7 @@ async def test_override_stored_configuration(hass, hass_storage): 'time_zone': 'Europe/Copenhagen', 'unit_system': 'metric' }, - 'key': 'homeassistant.core_config', + 'key': 'core.config', 'version': 1 } await config_util.async_process_ha_core_config(hass, { @@ -571,59 +531,6 @@ async def test_loading_configuration_from_packages(hass): }) -@asynctest.mock.patch( - 'homeassistant.util.location.async_detect_location_info', - autospec=True, return_value=mock_coro(location_util.LocationInfo( - '0.0.0.0', 'US', 'United States', 'CA', - 'California', 'San Diego', '92122', - 'America/Los_Angeles', 32.8594, -117.2073, True))) -@asynctest.mock.patch('homeassistant.util.location.async_get_elevation', - autospec=True, return_value=mock_coro(101)) -async def test_discovering_configuration(mock_detect, mock_elevation, hass): - """Test auto discovery for missing core configs.""" - hass.config.latitude = None - hass.config.longitude = None - hass.config.elevation = None - hass.config.location_name = None - hass.config.time_zone = None - - await config_util.async_process_ha_core_config(hass, {}) - - assert hass.config.latitude == 32.8594 - assert hass.config.longitude == -117.2073 - assert hass.config.elevation == 101 - assert hass.config.location_name == 'San Diego' - assert hass.config.units.name == CONF_UNIT_SYSTEM_METRIC - assert hass.config.units.is_metric - assert hass.config.time_zone.zone == 'America/Los_Angeles' - assert hass.config.config_source == config_util.SOURCE_DISCOVERED - - -@asynctest.mock.patch('homeassistant.util.location.async_detect_location_info', - autospec=True, return_value=mock_coro(None)) -@asynctest.mock.patch('homeassistant.util.location.async_get_elevation', - return_value=mock_coro(0)) -async def test_discovering_configuration_auto_detect_fails(mock_detect, - mock_elevation, - hass): - """Test config remains unchanged if discovery fails.""" - hass.config = Config(hass) - hass.config.config_dir = "/test/config" - - await config_util.async_process_ha_core_config(hass, {}) - - blankConfig = Config(hass) - assert hass.config.latitude == blankConfig.latitude - assert hass.config.longitude == blankConfig.longitude - assert hass.config.elevation == blankConfig.elevation - assert hass.config.location_name == blankConfig.location_name - assert hass.config.units == blankConfig.units - assert hass.config.time_zone == blankConfig.time_zone - assert len(hass.config.whitelist_external_dirs) == 1 - assert "/test/config/www" in hass.config.whitelist_external_dirs - assert hass.config.config_source == config_util.SOURCE_DISCOVERED - - @asynctest.mock.patch( 'homeassistant.scripts.check_config.check_ha_config_file') async def test_check_ha_config_file_correct(mock_check, hass): diff --git a/tests/test_core.py b/tests/test_core.py index 101396dd05e..2e9e14ed97a 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -891,17 +891,17 @@ class TestConfig(unittest.TestCase): """Test as dict.""" self.config.config_dir = '/tmp/ha-config' expected = { - 'latitude': None, - 'longitude': None, - 'elevation': None, + 'latitude': 0, + 'longitude': 0, + 'elevation': 0, CONF_UNIT_SYSTEM: METRIC_SYSTEM.as_dict(), - 'location_name': None, + 'location_name': "Home", 'time_zone': 'UTC', 'components': set(), 'config_dir': '/tmp/ha-config', 'whitelist_external_dirs': set(), 'version': __version__, - 'config_source': None, + 'config_source': "default", } assert expected == self.config.as_dict() diff --git a/tests/util/test_location.py b/tests/util/test_location.py index 2db37c46730..3fb7d07c2bb 100644 --- a/tests/util/test_location.py +++ b/tests/util/test_location.py @@ -116,10 +116,8 @@ async def test_detect_location_info_ip_api(aioclient_mock, session): async def test_detect_location_info_both_queries_fail(session): """Ensure we return None if both queries fail.""" - with patch('homeassistant.util.location.async_get_elevation', - return_value=mock_coro(0)), \ - patch('homeassistant.util.location._get_ipapi', - return_value=mock_coro(None)), \ + with patch('homeassistant.util.location._get_ipapi', + return_value=mock_coro(None)), \ patch('homeassistant.util.location._get_ip_api', return_value=mock_coro(None)): info = await location_util.async_detect_location_info( @@ -137,26 +135,3 @@ async def test_ip_api_query_raises(raising_session): """Test ip api query when the request to API fails.""" info = await location_util._get_ip_api(raising_session) assert info is None - - -async def test_elevation_query_raises(raising_session): - """Test elevation when the request to API fails.""" - elevation = await location_util.async_get_elevation( - raising_session, 10, 10, _test_real=True) - assert elevation == 0 - - -async def test_elevation_query_fails(aioclient_mock, session): - """Test elevation when the request to API fails.""" - aioclient_mock.get(location_util.ELEVATION_URL, text='{}', status=401) - elevation = await location_util.async_get_elevation( - session, 10, 10, _test_real=True) - assert elevation == 0 - - -async def test_elevation_query_nonjson(aioclient_mock, session): - """Test if elevation API returns a non JSON value.""" - aioclient_mock.get(location_util.ELEVATION_URL, text='{ I am not JSON }') - elevation = await location_util.async_get_elevation( - session, 10, 10, _test_real=True) - assert elevation == 0 From f995ab9d5430bf932bd7f780271196a43c531a28 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 22 May 2019 21:09:59 -0700 Subject: [PATCH 110/232] Don't pass in loop (#23984) * Don't pass in loop * Revert some changes * Lint + Axis revert * reinstate loop * Fix a test * Set loop * Update camera.py * Lint --- homeassistant/components/alert/__init__.py | 2 +- homeassistant/components/alexa/auth.py | 4 ++-- homeassistant/components/alexa/smart_home.py | 2 +- .../components/android_ip_webcam/__init__.py | 2 +- .../components/anthemav/media_player.py | 2 +- homeassistant/components/api/__init__.py | 5 ++--- homeassistant/components/apple_tv/__init__.py | 2 +- homeassistant/components/automation/__init__.py | 6 +++--- homeassistant/components/aws/__init__.py | 4 ++-- homeassistant/components/aws/notify.py | 4 ++-- .../components/bluesound/media_player.py | 8 ++++---- homeassistant/components/buienradar/sensor.py | 4 ++-- homeassistant/components/camera/__init__.py | 4 ++-- homeassistant/components/canary/camera.py | 2 +- homeassistant/components/citybikes/sensor.py | 6 +++--- homeassistant/components/cloud/http_api.py | 16 ++++++++-------- .../components/comed_hourly_pricing/sensor.py | 2 +- homeassistant/components/config/__init__.py | 2 +- .../components/device_tracker/__init__.py | 2 +- .../components/device_tracker/legacy.py | 4 ++-- homeassistant/components/device_tracker/setup.py | 2 +- homeassistant/components/discord/notify.py | 2 +- .../components/dlna_dmr/media_player.py | 1 - homeassistant/components/dnsip/sensor.py | 2 +- homeassistant/components/doorbird/camera.py | 2 +- homeassistant/components/dsmr/sensor.py | 8 +++----- homeassistant/components/envisalink/__init__.py | 2 +- homeassistant/components/ffmpeg/camera.py | 2 +- homeassistant/components/flock/notify.py | 7 +++---- homeassistant/components/freedns/__init__.py | 2 +- homeassistant/components/frontend/__init__.py | 4 ++-- homeassistant/components/generic/camera.py | 2 +- .../components/google_assistant/__init__.py | 2 +- .../components/google_domains/__init__.py | 2 +- homeassistant/components/google_translate/tts.py | 2 +- homeassistant/components/group/__init__.py | 2 +- homeassistant/components/group/notify.py | 2 +- homeassistant/components/hassio/handler.py | 2 +- homeassistant/components/hassio/http.py | 4 +--- .../components/homeassistant/__init__.py | 2 +- .../components/homekit_controller/connection.py | 2 +- .../components/homematicip_cloud/hap.py | 2 +- homeassistant/components/hook/switch.py | 6 +++--- .../components/image_processing/__init__.py | 2 +- homeassistant/components/ipma/weather.py | 4 ++-- homeassistant/components/kodi/media_player.py | 2 +- homeassistant/components/lifx/light.py | 4 ++-- homeassistant/components/lifx_cloud/scene.py | 4 ++-- homeassistant/components/light/__init__.py | 2 +- homeassistant/components/logi_circle/__init__.py | 2 +- .../components/logi_circle/config_flow.py | 2 +- homeassistant/components/mailbox/__init__.py | 5 ++--- homeassistant/components/marytts/tts.py | 2 +- .../components/media_player/__init__.py | 4 ++-- .../components/microsoft_face/__init__.py | 4 ++-- homeassistant/components/mjpeg/camera.py | 2 +- homeassistant/components/mobile_app/notify.py | 2 +- homeassistant/components/mqtt/__init__.py | 2 +- homeassistant/components/mysensors/gateway.py | 8 ++++---- homeassistant/components/no_ip/__init__.py | 2 +- homeassistant/components/notify/__init__.py | 2 +- homeassistant/components/onvif/camera.py | 2 +- .../openalpr_cloud/image_processing.py | 2 +- homeassistant/components/prowl/notify.py | 2 +- homeassistant/components/push/camera.py | 2 +- homeassistant/components/recorder/__init__.py | 2 +- homeassistant/components/recswitch/switch.py | 2 +- homeassistant/components/rest/switch.py | 4 ++-- .../components/rest_command/__init__.py | 2 +- homeassistant/components/ring/camera.py | 2 +- homeassistant/components/scene/__init__.py | 2 +- homeassistant/components/script/__init__.py | 2 +- homeassistant/components/sensibo/climate.py | 2 +- homeassistant/components/sma/sensor.py | 2 +- homeassistant/components/smhi/weather.py | 2 +- homeassistant/components/sonos/media_player.py | 2 +- .../components/squeezebox/media_player.py | 4 ++-- homeassistant/components/startca/sensor.py | 2 +- .../components/switcher_kis/__init__.py | 3 +-- homeassistant/components/tado/device_tracker.py | 2 +- homeassistant/components/teksavvy/sensor.py | 2 +- .../components/thethingsnetwork/sensor.py | 2 +- homeassistant/components/tradfri/config_flow.py | 2 +- homeassistant/components/tts/__init__.py | 2 +- .../components/upc_connect/device_tracker.py | 4 ++-- homeassistant/components/updater/__init__.py | 2 +- homeassistant/components/viaggiatreno/sensor.py | 2 +- homeassistant/components/voicerss/tts.py | 2 +- homeassistant/components/websocket_api/http.py | 4 ++-- homeassistant/components/worxlandroid/sensor.py | 2 +- homeassistant/components/wunderground/sensor.py | 2 +- homeassistant/components/xiaomi/camera.py | 2 +- homeassistant/components/xiaomi_miio/fan.py | 2 +- homeassistant/components/xiaomi_miio/light.py | 2 +- homeassistant/components/xiaomi_miio/remote.py | 2 +- homeassistant/components/xiaomi_miio/switch.py | 2 +- homeassistant/components/xiaomi_miio/vacuum.py | 2 +- homeassistant/components/yandextts/tts.py | 2 +- homeassistant/components/yi/camera.py | 2 +- homeassistant/components/yr/sensor.py | 4 ++-- homeassistant/components/zwave/__init__.py | 2 +- homeassistant/core.py | 2 +- homeassistant/helpers/aiohttp_client.py | 4 ++-- homeassistant/helpers/entity_component.py | 4 ++-- homeassistant/helpers/entity_platform.py | 14 +++++++------- homeassistant/helpers/state.py | 2 +- homeassistant/helpers/storage.py | 2 +- homeassistant/requirements.py | 2 +- homeassistant/scripts/benchmark/__init__.py | 6 +++--- homeassistant/setup.py | 4 ++-- homeassistant/util/logging.py | 2 +- tests/common.py | 3 ++- .../components/automatic/test_device_tracker.py | 2 +- tests/components/dsmr/test_sensor.py | 6 +++--- tests/components/websocket_api/test_commands.py | 4 ++-- tests/helpers/test_entity_platform.py | 2 +- tests/test_core.py | 14 +++++++------- 117 files changed, 179 insertions(+), 187 deletions(-) diff --git a/homeassistant/components/alert/__init__.py b/homeassistant/components/alert/__init__.py index 88217b026fd..a5b6d26d4fd 100644 --- a/homeassistant/components/alert/__init__.py +++ b/homeassistant/components/alert/__init__.py @@ -118,7 +118,7 @@ async def async_setup(hass, config): tasks = [alert.async_update_ha_state() for alert in entities] if tasks: - await asyncio.wait(tasks, loop=hass.loop) + await asyncio.wait(tasks) return True diff --git a/homeassistant/components/alexa/auth.py b/homeassistant/components/alexa/auth.py index 6918ec1e54f..0717532f64d 100644 --- a/homeassistant/components/alexa/auth.py +++ b/homeassistant/components/alexa/auth.py @@ -39,7 +39,7 @@ class Auth: self._prefs = None self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) - self._get_token_lock = asyncio.Lock(loop=hass.loop) + self._get_token_lock = asyncio.Lock() async def async_do_auth(self, accept_grant_code): """Do authentication with an AcceptGrant code.""" @@ -97,7 +97,7 @@ class Auth: try: session = aiohttp_client.async_get_clientsession(self.hass) - with async_timeout.timeout(DEFAULT_TIMEOUT, loop=self.hass.loop): + with async_timeout.timeout(DEFAULT_TIMEOUT): response = await session.post(LWA_TOKEN_URI, headers=LWA_HEADERS, data=lwa_params, diff --git a/homeassistant/components/alexa/smart_home.py b/homeassistant/components/alexa/smart_home.py index 184aee9a440..a69a0cf6ec7 100644 --- a/homeassistant/components/alexa/smart_home.py +++ b/homeassistant/components/alexa/smart_home.py @@ -1432,7 +1432,7 @@ async def async_send_changereport_message(hass, config, alexa_entity): try: session = aiohttp_client.async_get_clientsession(hass) - with async_timeout.timeout(DEFAULT_TIMEOUT, loop=hass.loop): + with async_timeout.timeout(DEFAULT_TIMEOUT): response = await session.post(config.endpoint, headers=headers, json=message_serialized, diff --git a/homeassistant/components/android_ip_webcam/__init__.py b/homeassistant/components/android_ip_webcam/__init__.py index dfb6d143e9a..c9357c4cce0 100644 --- a/homeassistant/components/android_ip_webcam/__init__.py +++ b/homeassistant/components/android_ip_webcam/__init__.py @@ -233,7 +233,7 @@ async def async_setup(hass, config): tasks = [async_setup_ipcamera(conf) for conf in config[DOMAIN]] if tasks: - await asyncio.wait(tasks, loop=hass.loop) + await asyncio.wait(tasks) return True diff --git a/homeassistant/components/anthemav/media_player.py b/homeassistant/components/anthemav/media_player.py index 1a335fc2ce6..13c0ef338bc 100644 --- a/homeassistant/components/anthemav/media_player.py +++ b/homeassistant/components/anthemav/media_player.py @@ -47,7 +47,7 @@ async def async_setup_platform(hass, config, async_add_entities, hass.async_create_task(device.async_update_ha_state()) avr = await anthemav.Connection.create( - host=host, port=port, loop=hass.loop, + host=host, port=port, update_callback=async_anthemav_update_callback) device = AnthemAVR(avr, name) diff --git a/homeassistant/components/api/__init__.py b/homeassistant/components/api/__init__.py index 0e860854af4..feea4f21c9c 100644 --- a/homeassistant/components/api/__init__.py +++ b/homeassistant/components/api/__init__.py @@ -82,7 +82,7 @@ class APIEventStream(HomeAssistantView): raise Unauthorized() hass = request.app['hass'] stop_obj = object() - to_write = asyncio.Queue(loop=hass.loop) + to_write = asyncio.Queue() restrict = request.query.get('restrict') if restrict: @@ -119,8 +119,7 @@ class APIEventStream(HomeAssistantView): while True: try: - with async_timeout.timeout(STREAM_PING_INTERVAL, - loop=hass.loop): + with async_timeout.timeout(STREAM_PING_INTERVAL): payload = await to_write.get() if payload is stop_obj: diff --git a/homeassistant/components/apple_tv/__init__.py b/homeassistant/components/apple_tv/__init__.py index 0ebe29ed47c..80da26195ee 100644 --- a/homeassistant/components/apple_tv/__init__.py +++ b/homeassistant/components/apple_tv/__init__.py @@ -167,7 +167,7 @@ async def async_setup(hass, config): tasks = [_setup_atv(hass, config, conf) for conf in config.get(DOMAIN, [])] if tasks: - await asyncio.wait(tasks, loop=hass.loop) + await asyncio.wait(tasks) hass.services.async_register( DOMAIN, SERVICE_SCAN, async_service_handler, diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index fa8b77da768..beca5cd236c 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -124,7 +124,7 @@ async def async_setup(hass, config): context=service_call.context)) if tasks: - await asyncio.wait(tasks, loop=hass.loop) + await asyncio.wait(tasks) async def turn_onoff_service_handler(service_call): """Handle automation turn on/off service calls.""" @@ -134,7 +134,7 @@ async def async_setup(hass, config): tasks.append(getattr(entity, method)()) if tasks: - await asyncio.wait(tasks, loop=hass.loop) + await asyncio.wait(tasks) async def toggle_service_handler(service_call): """Handle automation toggle service calls.""" @@ -146,7 +146,7 @@ async def async_setup(hass, config): tasks.append(entity.async_turn_on()) if tasks: - await asyncio.wait(tasks, loop=hass.loop) + await asyncio.wait(tasks) async def reload_service_handler(service_call): """Remove all automations and load new ones from config.""" diff --git a/homeassistant/components/aws/__init__.py b/homeassistant/components/aws/__init__.py index e25af68d550..5b9978fb3e6 100644 --- a/homeassistant/components/aws/__init__.py +++ b/homeassistant/components/aws/__init__.py @@ -166,14 +166,14 @@ async def _validate_aws_credentials(hass, credential): profile = aws_config.get(CONF_PROFILE_NAME) if profile is not None: - session = aiobotocore.AioSession(profile=profile, loop=hass.loop) + session = aiobotocore.AioSession(profile=profile) del aws_config[CONF_PROFILE_NAME] if CONF_ACCESS_KEY_ID in aws_config: del aws_config[CONF_ACCESS_KEY_ID] if CONF_SECRET_ACCESS_KEY in aws_config: del aws_config[CONF_SECRET_ACCESS_KEY] else: - session = aiobotocore.AioSession(loop=hass.loop) + session = aiobotocore.AioSession() if credential[CONF_VALIDATE]: async with session.create_client("iam", **aws_config) as client: diff --git a/homeassistant/components/aws/notify.py b/homeassistant/components/aws/notify.py index 3a6193f403d..4b71ae425cb 100644 --- a/homeassistant/components/aws/notify.py +++ b/homeassistant/components/aws/notify.py @@ -94,10 +94,10 @@ async def async_get_service(hass, config, discovery_info=None): if session is None: profile = aws_config.get(CONF_PROFILE_NAME) if profile is not None: - session = aiobotocore.AioSession(profile=profile, loop=hass.loop) + session = aiobotocore.AioSession(profile=profile) del aws_config[CONF_PROFILE_NAME] else: - session = aiobotocore.AioSession(loop=hass.loop) + session = aiobotocore.AioSession() aws_config[CONF_REGION] = region_name diff --git a/homeassistant/components/bluesound/media_player.py b/homeassistant/components/bluesound/media_player.py index 080afeea280..2a3b3e35125 100644 --- a/homeassistant/components/bluesound/media_player.py +++ b/homeassistant/components/bluesound/media_player.py @@ -255,7 +255,7 @@ class BluesoundPlayer(MediaPlayerDevice): BluesoundPlayer._TimeoutException): _LOGGER.info("Node %s is offline, retrying later", self._name) await asyncio.sleep( - NODE_OFFLINE_CHECK_TIMEOUT, loop=self._hass.loop) + NODE_OFFLINE_CHECK_TIMEOUT) self.start_polling() except CancelledError: @@ -318,7 +318,7 @@ class BluesoundPlayer(MediaPlayerDevice): try: websession = async_get_clientsession(self._hass) - with async_timeout.timeout(10, loop=self._hass.loop): + with async_timeout.timeout(10): response = await websession.get(url) if response.status == 200: @@ -361,7 +361,7 @@ class BluesoundPlayer(MediaPlayerDevice): try: - with async_timeout.timeout(125, loop=self._hass.loop): + with async_timeout.timeout(125): response = await self._polling_session.get( url, headers={CONNECTION: KEEP_ALIVE}) @@ -378,7 +378,7 @@ class BluesoundPlayer(MediaPlayerDevice): self._group_name = group_name # the sleep is needed to make sure that the # devices is synced - await asyncio.sleep(1, loop=self._hass.loop) + await asyncio.sleep(1) await self.async_trigger_sync_on_all() elif self.is_grouped: # when player is grouped we need to fetch volume from diff --git a/homeassistant/components/buienradar/sensor.py b/homeassistant/components/buienradar/sensor.py index f3aaa9b7537..71ad6abb914 100644 --- a/homeassistant/components/buienradar/sensor.py +++ b/homeassistant/components/buienradar/sensor.py @@ -388,7 +388,7 @@ class BrData: tasks.append(dev.async_update_ha_state()) if tasks: - await asyncio.wait(tasks, loop=self.hass.loop) + await asyncio.wait(tasks) async def schedule_update(self, minute=1): """Schedule an update after minute minutes.""" @@ -407,7 +407,7 @@ class BrData: resp = None try: websession = async_get_clientsession(self.hass) - with async_timeout.timeout(10, loop=self.hass.loop): + with async_timeout.timeout(10): resp = await websession.get(url) result[STATUS_CODE] = resp.status diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 7098d8bcb75..352a9dd5060 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -121,7 +121,7 @@ async def async_get_image(hass, entity_id, timeout=10): camera = _get_camera_from_entity_id(hass, entity_id) with suppress(asyncio.CancelledError, asyncio.TimeoutError): - with async_timeout.timeout(timeout, loop=hass.loop): + with async_timeout.timeout(timeout): image = await camera.async_camera_image() if image: @@ -481,7 +481,7 @@ class CameraImageView(CameraView): async def handle(self, request, camera): """Serve camera image.""" with suppress(asyncio.CancelledError, asyncio.TimeoutError): - with async_timeout.timeout(10, loop=request.app['hass'].loop): + with async_timeout.timeout(10): image = await camera.async_camera_image() if image: diff --git a/homeassistant/components/canary/camera.py b/homeassistant/components/canary/camera.py index 33e1265921f..9411ab2a41c 100644 --- a/homeassistant/components/canary/camera.py +++ b/homeassistant/components/canary/camera.py @@ -79,7 +79,7 @@ class CanaryCamera(Camera): image = await asyncio.shield(ffmpeg.get_image( self._live_stream_session.live_stream_url, output_format=IMAGE_JPEG, - extra_cmd=self._ffmpeg_arguments), loop=self.hass.loop) + extra_cmd=self._ffmpeg_arguments)) return image async def handle_async_mjpeg_stream(self, request): diff --git a/homeassistant/components/citybikes/sensor.py b/homeassistant/components/citybikes/sensor.py index 344311aa231..fc751d96602 100644 --- a/homeassistant/components/citybikes/sensor.py +++ b/homeassistant/components/citybikes/sensor.py @@ -106,7 +106,7 @@ async def async_citybikes_request(hass, uri, schema): try: session = async_get_clientsession(hass) - with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop): + with async_timeout.timeout(REQUEST_TIMEOUT): req = await session.get(DEFAULT_ENDPOINT.format(uri=uri)) json_response = await req.json() @@ -181,7 +181,7 @@ class CityBikesNetworks: """Initialize the networks instance.""" self.hass = hass self.networks = None - self.networks_loading = asyncio.Condition(loop=hass.loop) + self.networks_loading = asyncio.Condition() async def get_closest_network_id(self, latitude, longitude): """Return the id of the network closest to provided location.""" @@ -217,7 +217,7 @@ class CityBikesNetwork: self.hass = hass self.network_id = network_id self.stations = [] - self.ready = asyncio.Event(loop=hass.loop) + self.ready = asyncio.Event() async def async_refresh(self, now=None): """Refresh the state of the network.""" diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index bf9b7833527..40d19c198be 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -164,10 +164,10 @@ class GoogleActionsSyncView(HomeAssistantView): cloud = hass.data[DOMAIN] websession = hass.helpers.aiohttp_client.async_get_clientsession() - with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop): + with async_timeout.timeout(REQUEST_TIMEOUT): await hass.async_add_job(cloud.auth.check_token) - with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop): + with async_timeout.timeout(REQUEST_TIMEOUT): req = await websession.post( cloud.google_actions_sync_url, headers={ 'authorization': cloud.id_token @@ -192,7 +192,7 @@ class CloudLoginView(HomeAssistantView): hass = request.app['hass'] cloud = hass.data[DOMAIN] - with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop): + with async_timeout.timeout(REQUEST_TIMEOUT): await hass.async_add_job(cloud.auth.login, data['email'], data['password']) @@ -212,7 +212,7 @@ class CloudLogoutView(HomeAssistantView): hass = request.app['hass'] cloud = hass.data[DOMAIN] - with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop): + with async_timeout.timeout(REQUEST_TIMEOUT): await cloud.logout() return self.json_message('ok') @@ -234,7 +234,7 @@ class CloudRegisterView(HomeAssistantView): hass = request.app['hass'] cloud = hass.data[DOMAIN] - with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop): + with async_timeout.timeout(REQUEST_TIMEOUT): await hass.async_add_job( cloud.auth.register, data['email'], data['password']) @@ -256,7 +256,7 @@ class CloudResendConfirmView(HomeAssistantView): hass = request.app['hass'] cloud = hass.data[DOMAIN] - with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop): + with async_timeout.timeout(REQUEST_TIMEOUT): await hass.async_add_job( cloud.auth.resend_email_confirm, data['email']) @@ -278,7 +278,7 @@ class CloudForgotPasswordView(HomeAssistantView): hass = request.app['hass'] cloud = hass.data[DOMAIN] - with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop): + with async_timeout.timeout(REQUEST_TIMEOUT): await hass.async_add_job( cloud.auth.forgot_password, data['email']) @@ -320,7 +320,7 @@ async def websocket_subscription(hass, connection, msg): from hass_nabucasa.const import STATE_DISCONNECTED cloud = hass.data[DOMAIN] - with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop): + with async_timeout.timeout(REQUEST_TIMEOUT): response = await cloud.fetch_subscription_info() if response.status != 200: diff --git a/homeassistant/components/comed_hourly_pricing/sensor.py b/homeassistant/components/comed_hourly_pricing/sensor.py index 384aadd8bf4..3c06bc0c2d7 100644 --- a/homeassistant/components/comed_hourly_pricing/sensor.py +++ b/homeassistant/components/comed_hourly_pricing/sensor.py @@ -106,7 +106,7 @@ class ComedHourlyPricingSensor(Entity): else: url_string += '?type=currenthouraverage' - with async_timeout.timeout(60, loop=self.loop): + with async_timeout.timeout(60): response = await self.websession.get(url_string) # The API responds with MIME type 'text/html' text = await response.text() diff --git a/homeassistant/components/config/__init__.py b/homeassistant/components/config/__init__.py index 70c72e899c0..8cd8856c1ec 100644 --- a/homeassistant/components/config/__init__.py +++ b/homeassistant/components/config/__init__.py @@ -62,7 +62,7 @@ async def async_setup(hass, config): tasks.append(setup_panel(panel_name)) if tasks: - await asyncio.wait(tasks, loop=hass.loop) + await asyncio.wait(tasks) return True diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index d7947fd5123..618ed163b9d 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -144,7 +144,7 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType): })) if setup_tasks: - await asyncio.wait(setup_tasks, loop=hass.loop) + await asyncio.wait(setup_tasks) tracker.async_setup_group() diff --git a/homeassistant/components/device_tracker/legacy.py b/homeassistant/components/device_tracker/legacy.py index 73846480655..bc1c6013ac0 100644 --- a/homeassistant/components/device_tracker/legacy.py +++ b/homeassistant/components/device_tracker/legacy.py @@ -82,7 +82,7 @@ class DeviceTracker: else defaults.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW) self.defaults = defaults self.group = None - self._is_updating = asyncio.Lock(loop=hass.loop) + self._is_updating = asyncio.Lock() for dev in devices: if self.devices[dev.dev_id] is not dev: @@ -229,7 +229,7 @@ class DeviceTracker: async_init_single_device(device))) if tasks: - await asyncio.wait(tasks, loop=self.hass.loop) + await asyncio.wait(tasks) class Device(RestoreEntity): diff --git a/homeassistant/components/device_tracker/setup.py b/homeassistant/components/device_tracker/setup.py index e336821c758..b2a3b66a27c 100644 --- a/homeassistant/components/device_tracker/setup.py +++ b/homeassistant/components/device_tracker/setup.py @@ -147,7 +147,7 @@ def async_setup_scanner_platform(hass: HomeAssistantType, config: ConfigType, This method must be run in the event loop. """ interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) - update_lock = asyncio.Lock(loop=hass.loop) + update_lock = asyncio.Lock() scanner.hass = hass # Initial scan of each mac we also tell about host name for config diff --git a/homeassistant/components/discord/notify.py b/homeassistant/components/discord/notify.py index 2e3d2eee9e9..75a434a3739 100644 --- a/homeassistant/components/discord/notify.py +++ b/homeassistant/components/discord/notify.py @@ -46,7 +46,7 @@ class DiscordNotificationService(BaseNotificationService): import discord discord.VoiceClient.warn_nacl = False - discord_bot = discord.Client(loop=self.hass.loop) + discord_bot = discord.Client() images = None if ATTR_TARGET not in kwargs: diff --git a/homeassistant/components/dlna_dmr/media_player.py b/homeassistant/components/dlna_dmr/media_player.py index 6f29bd65d56..dd348d1fbbc 100644 --- a/homeassistant/components/dlna_dmr/media_player.py +++ b/homeassistant/components/dlna_dmr/media_player.py @@ -103,7 +103,6 @@ async def async_start_event_handler( requester, listen_port=server_port, listen_host=server_host, - loop=hass.loop, callback_url=callback_url_override) await server.start_server() _LOGGER.info( diff --git a/homeassistant/components/dnsip/sensor.py b/homeassistant/components/dnsip/sensor.py index a29a0513cee..337a68a77ce 100644 --- a/homeassistant/components/dnsip/sensor.py +++ b/homeassistant/components/dnsip/sensor.py @@ -63,7 +63,7 @@ class WanIpSensor(Entity): self.hass = hass self._name = name self.hostname = hostname - self.resolver = aiodns.DNSResolver(loop=self.hass.loop) + self.resolver = aiodns.DNSResolver() self.resolver.nameservers = [resolver] self.querytype = 'AAAA' if ipv6 else 'A' self._state = None diff --git a/homeassistant/components/doorbird/camera.py b/homeassistant/components/doorbird/camera.py index 9a20a91c758..6da2cd1447d 100644 --- a/homeassistant/components/doorbird/camera.py +++ b/homeassistant/components/doorbird/camera.py @@ -81,7 +81,7 @@ class DoorBirdCamera(Camera): try: websession = async_get_clientsession(self.hass) - with async_timeout.timeout(_TIMEOUT, loop=self.hass.loop): + with async_timeout.timeout(_TIMEOUT): response = await websession.get(self._url) self._last_image = await response.read() diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index d7acc5c28bf..e19d910ad83 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -183,12 +183,11 @@ async def async_setup_platform(hass, config, async_add_entities, if CONF_HOST in config: reader_factory = partial( create_tcp_dsmr_reader, config[CONF_HOST], config[CONF_PORT], - config[CONF_DSMR_VERSION], update_entities_telegram, - loop=hass.loop) + config[CONF_DSMR_VERSION], update_entities_telegram) else: reader_factory = partial( create_dsmr_reader, config[CONF_PORT], config[CONF_DSMR_VERSION], - update_entities_telegram, loop=hass.loop) + update_entities_telegram) async def connect_and_reconnect(): """Connect to DSMR and keep reconnecting until Home Assistant stops.""" @@ -223,8 +222,7 @@ async def async_setup_platform(hass, config, async_add_entities, update_entities_telegram({}) # throttle reconnect attempts - await asyncio.sleep(config[CONF_RECONNECT_INTERVAL], - loop=hass.loop) + await asyncio.sleep(config[CONF_RECONNECT_INTERVAL]) # Can't be hass.async_add_job because job runs forever hass.loop.create_task(connect_and_reconnect()) diff --git a/homeassistant/components/envisalink/__init__.py b/homeassistant/components/envisalink/__init__.py index d7a015e8e45..84b98846c2a 100644 --- a/homeassistant/components/envisalink/__init__.py +++ b/homeassistant/components/envisalink/__init__.py @@ -106,7 +106,7 @@ async def async_setup(hass, config): zones = conf.get(CONF_ZONES) partitions = conf.get(CONF_PARTITIONS) connection_timeout = conf.get(CONF_TIMEOUT) - sync_connect = asyncio.Future(loop=hass.loop) + sync_connect = asyncio.Future() controller = EnvisalinkAlarmPanel( host, port, panel_type, version, user, password, zone_dump, diff --git a/homeassistant/components/ffmpeg/camera.py b/homeassistant/components/ffmpeg/camera.py index 0e8a69e0bcf..e803155d254 100644 --- a/homeassistant/components/ffmpeg/camera.py +++ b/homeassistant/components/ffmpeg/camera.py @@ -59,7 +59,7 @@ class FFmpegCamera(Camera): image = await asyncio.shield(ffmpeg.get_image( self._input, output_format=IMAGE_JPEG, - extra_cmd=self._extra_arguments), loop=self.hass.loop) + extra_cmd=self._extra_arguments)) return image async def handle_async_mjpeg_stream(self, request): diff --git a/homeassistant/components/flock/notify.py b/homeassistant/components/flock/notify.py index 384bf26599a..93a478611db 100644 --- a/homeassistant/components/flock/notify.py +++ b/homeassistant/components/flock/notify.py @@ -26,15 +26,14 @@ async def get_service(hass, config, discovery_info=None): url = '{}{}'.format(_RESOURCE, access_token) session = async_get_clientsession(hass) - return FlockNotificationService(url, session, hass.loop) + return FlockNotificationService(url, session) class FlockNotificationService(BaseNotificationService): """Implement the notification service for Flock.""" - def __init__(self, url, session, loop): + def __init__(self, url, session): """Initialize the Flock notification service.""" - self._loop = loop self._url = url self._session = session @@ -45,7 +44,7 @@ class FlockNotificationService(BaseNotificationService): _LOGGER.debug("Attempting to call Flock at %s", self._url) try: - with async_timeout.timeout(10, loop=self._loop): + with async_timeout.timeout(10): response = await self._session.post(self._url, json=payload) result = await response.json() diff --git a/homeassistant/components/freedns/__init__.py b/homeassistant/components/freedns/__init__.py index 1986c932e22..6125057ca33 100644 --- a/homeassistant/components/freedns/__init__.py +++ b/homeassistant/components/freedns/__init__.py @@ -68,7 +68,7 @@ async def _update_freedns(hass, session, url, auth_token): params[auth_token] = "" try: - with async_timeout.timeout(TIMEOUT, loop=hass.loop): + with async_timeout.timeout(TIMEOUT): resp = await session.get(url, params=params) body = await resp.text() diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 7ef031a90cb..8d7f7213787 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -243,11 +243,11 @@ async def async_setup(hass, config): await asyncio.wait( [async_register_built_in_panel(hass, panel) for panel in ( - 'kiosk', 'states', 'profile')], loop=hass.loop) + 'kiosk', 'states', 'profile')]) await asyncio.wait( [async_register_built_in_panel(hass, panel, require_admin=True) for panel in ('dev-event', 'dev-info', 'dev-service', 'dev-state', - 'dev-template', 'dev-mqtt')], loop=hass.loop) + 'dev-template', 'dev-mqtt')]) hass.data[DATA_FINALIZE_PANEL] = async_finalize_panel diff --git a/homeassistant/components/generic/camera.py b/homeassistant/components/generic/camera.py index bfe42a5b080..7f63a832779 100644 --- a/homeassistant/components/generic/camera.py +++ b/homeassistant/components/generic/camera.py @@ -127,7 +127,7 @@ class GenericCamera(Camera): try: websession = async_get_clientsession( self.hass, verify_ssl=self.verify_ssl) - with async_timeout.timeout(10, loop=self.hass.loop): + with async_timeout.timeout(10): response = await websession.get( url, auth=self._auth) self._last_image = await response.read() diff --git a/homeassistant/components/google_assistant/__init__.py b/homeassistant/components/google_assistant/__init__.py index c8078b7d9d2..1e0ac6d9363 100644 --- a/homeassistant/components/google_assistant/__init__.py +++ b/homeassistant/components/google_assistant/__init__.py @@ -65,7 +65,7 @@ async def async_setup(hass: HomeAssistant, yaml_config: Dict[str, Any]): """Handle request sync service calls.""" websession = async_get_clientsession(hass) try: - with async_timeout.timeout(15, loop=hass.loop): + with async_timeout.timeout(15): agent_user_id = call.data.get('agent_user_id') or \ call.context.user_id res = await websession.post( diff --git a/homeassistant/components/google_domains/__init__.py b/homeassistant/components/google_domains/__init__.py index f884e46cc4c..2d3736d2ec3 100644 --- a/homeassistant/components/google_domains/__init__.py +++ b/homeassistant/components/google_domains/__init__.py @@ -67,7 +67,7 @@ async def _update_google_domains( } try: - with async_timeout.timeout(timeout, loop=hass.loop): + with async_timeout.timeout(timeout): resp = await session.get(url, params=params) body = await resp.text() diff --git a/homeassistant/components/google_translate/tts.py b/homeassistant/components/google_translate/tts.py index 4d988bed21c..0f067cf13b9 100644 --- a/homeassistant/components/google_translate/tts.py +++ b/homeassistant/components/google_translate/tts.py @@ -87,7 +87,7 @@ class GoogleProvider(Provider): } try: - with async_timeout.timeout(10, loop=self.hass.loop): + with async_timeout.timeout(10): request = await websession.get( GOOGLE_SPEECH_URL, params=url_param, headers=self.headers diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index 80ac01a78ac..d13580ec42a 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -306,7 +306,7 @@ async def async_setup(hass, config): tasks.append(group.async_update_ha_state()) if tasks: - await asyncio.wait(tasks, loop=hass.loop) + await asyncio.wait(tasks) hass.services.async_register( DOMAIN, SERVICE_SET_VISIBILITY, visibility_service_handler, diff --git a/homeassistant/components/group/notify.py b/homeassistant/components/group/notify.py index b59c49563e2..e13499878e9 100644 --- a/homeassistant/components/group/notify.py +++ b/homeassistant/components/group/notify.py @@ -65,4 +65,4 @@ class GroupNotifyPlatform(BaseNotificationService): DOMAIN, entity.get(ATTR_SERVICE), sending_payload)) if tasks: - await asyncio.wait(tasks, loop=self.hass.loop) + await asyncio.wait(tasks) diff --git a/homeassistant/components/hassio/handler.py b/homeassistant/components/hassio/handler.py index aae1f31d486..1e6e1c2fffe 100644 --- a/homeassistant/components/hassio/handler.py +++ b/homeassistant/components/hassio/handler.py @@ -156,7 +156,7 @@ class HassIO: This method is a coroutine. """ try: - with async_timeout.timeout(timeout, loop=self.loop): + with async_timeout.timeout(timeout): request = await self.websession.request( method, "http://{}{}".format(self._ip, command), json=payload, headers={ diff --git a/homeassistant/components/hassio/http.py b/homeassistant/components/hassio/http.py index a798d312c25..a9c5deda9f9 100644 --- a/homeassistant/components/hassio/http.py +++ b/homeassistant/components/hassio/http.py @@ -71,13 +71,11 @@ class HassIOView(HomeAssistantView): This method is a coroutine. """ read_timeout = _get_timeout(path) - hass = request.app['hass'] - data = None headers = _init_header(request) try: - with async_timeout.timeout(10, loop=hass.loop): + with async_timeout.timeout(10): data = await request.read() method = getattr(self._websession, request.method.lower()) diff --git a/homeassistant/components/homeassistant/__init__.py b/homeassistant/components/homeassistant/__init__.py index ef01d133cff..93a197969ca 100644 --- a/homeassistant/components/homeassistant/__init__.py +++ b/homeassistant/components/homeassistant/__init__.py @@ -64,7 +64,7 @@ async def async_setup(hass: ha.HomeAssistant, config: dict) -> Awaitable[bool]: tasks.append(hass.services.async_call( domain, service.service, data, blocking)) - await asyncio.wait(tasks, loop=hass.loop) + await asyncio.wait(tasks) hass.services.async_register( ha.DOMAIN, SERVICE_TURN_OFF, async_handle_turn_service) diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py index 3819bef680d..d0fc99de0d7 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -79,7 +79,7 @@ class HKDevice(): # There are multiple entities sharing a single connection - only # allow one entity to use pairing at once. - self.pairing_lock = asyncio.Lock(loop=hass.loop) + self.pairing_lock = asyncio.Lock() async def async_setup(self): """Prepare to use a paired HomeKit device in homeassistant.""" diff --git a/homeassistant/components/homematicip_cloud/hap.py b/homeassistant/components/homematicip_cloud/hap.py index b3731bc9f1a..8bbbb8f41b6 100644 --- a/homeassistant/components/homematicip_cloud/hap.py +++ b/homeassistant/components/homematicip_cloud/hap.py @@ -180,7 +180,7 @@ class HomematicipHAP: try: self._retry_task = self.hass.async_create_task(asyncio.sleep( - retry_delay, loop=self.hass.loop)) + retry_delay)) await self._retry_task except asyncio.CancelledError: break diff --git a/homeassistant/components/hook/switch.py b/homeassistant/components/hook/switch.py index 7a11c1dd8b7..abe2040b091 100644 --- a/homeassistant/components/hook/switch.py +++ b/homeassistant/components/hook/switch.py @@ -36,7 +36,7 @@ async def async_setup_platform(hass, config, async_add_entities, # If password is set in config, prefer it over token if username is not None and password is not None: try: - with async_timeout.timeout(TIMEOUT, loop=hass.loop): + with async_timeout.timeout(TIMEOUT): response = await websession.post( '{}{}'.format(HOOK_ENDPOINT, 'user/login'), data={ @@ -56,7 +56,7 @@ async def async_setup_platform(hass, config, async_add_entities, return False try: - with async_timeout.timeout(TIMEOUT, loop=hass.loop): + with async_timeout.timeout(TIMEOUT): response = await websession.get( '{}{}'.format(HOOK_ENDPOINT, 'device'), params={"token": token}) @@ -103,7 +103,7 @@ class HookSmartHome(SwitchDevice): try: _LOGGER.debug("Sending: %s", url) websession = async_get_clientsession(self.hass) - with async_timeout.timeout(TIMEOUT, loop=self.hass.loop): + with async_timeout.timeout(TIMEOUT): response = await websession.get( url, params={"token": self._token}) data = await response.json(content_type=None) diff --git a/homeassistant/components/image_processing/__init__.py b/homeassistant/components/image_processing/__init__.py index ce49ebf932e..95ab0245dbb 100644 --- a/homeassistant/components/image_processing/__init__.py +++ b/homeassistant/components/image_processing/__init__.py @@ -77,7 +77,7 @@ async def async_setup(hass, config): entity.async_update_ha_state(True)) if update_tasks: - await asyncio.wait(update_tasks, loop=hass.loop) + await asyncio.wait(update_tasks) hass.services.async_register( DOMAIN, SERVICE_SCAN, async_scan_service, diff --git a/homeassistant/components/ipma/weather.py b/homeassistant/components/ipma/weather.py index e976bcb9896..a5c1d3e26f5 100644 --- a/homeassistant/components/ipma/weather.py +++ b/homeassistant/components/ipma/weather.py @@ -82,7 +82,7 @@ async def async_get_station(hass, latitude, longitude): from pyipma import Station websession = async_get_clientsession(hass) - with async_timeout.timeout(10, loop=hass.loop): + with async_timeout.timeout(10): station = await Station.get(websession, float(latitude), float(longitude)) @@ -106,7 +106,7 @@ class IPMAWeather(WeatherEntity): @Throttle(MIN_TIME_BETWEEN_UPDATES) async def async_update(self): """Update Condition and Forecast.""" - with async_timeout.timeout(10, loop=self.hass.loop): + with async_timeout.timeout(10): _new_condition = await self._station.observation() if _new_condition is None: _LOGGER.warning("Could not update weather conditions") diff --git a/homeassistant/components/kodi/media_player.py b/homeassistant/components/kodi/media_player.py index 96fb02a08a0..661ebd86187 100644 --- a/homeassistant/components/kodi/media_player.py +++ b/homeassistant/components/kodi/media_player.py @@ -231,7 +231,7 @@ async def async_setup_platform(hass, config, async_add_entities, update_tasks.append(update_coro) if update_tasks: - await asyncio.wait(update_tasks, loop=hass.loop) + await asyncio.wait(update_tasks) if hass.services.has_service(DOMAIN, SERVICE_ADD_MEDIA): return diff --git a/homeassistant/components/lifx/light.py b/homeassistant/components/lifx/light.py index 04f756e6ded..5f462941062 100644 --- a/homeassistant/components/lifx/light.py +++ b/homeassistant/components/lifx/light.py @@ -202,7 +202,7 @@ class LIFXManager: self.entities = {} self.hass = hass self.async_add_entities = async_add_entities - self.effects_conductor = aiolifx_effects().Conductor(loop=hass.loop) + self.effects_conductor = aiolifx_effects().Conductor(hass.loop) self.discoveries = [] self.cleanup_unsub = self.hass.bus.async_listen( EVENT_HOMEASSISTANT_STOP, @@ -253,7 +253,7 @@ class LIFXManager: task = light.set_state(**service.data) tasks.append(self.hass.async_create_task(task)) if tasks: - await asyncio.wait(tasks, loop=self.hass.loop) + await asyncio.wait(tasks) self.hass.services.async_register( DOMAIN, SERVICE_LIFX_SET_STATE, service_handler, diff --git a/homeassistant/components/lifx_cloud/scene.py b/homeassistant/components/lifx_cloud/scene.py index c877bddbe53..fd6f548c0b1 100644 --- a/homeassistant/components/lifx_cloud/scene.py +++ b/homeassistant/components/lifx_cloud/scene.py @@ -38,7 +38,7 @@ async def async_setup_platform(hass, config, async_add_entities, try: httpsession = async_get_clientsession(hass) - with async_timeout.timeout(timeout, loop=hass.loop): + with async_timeout.timeout(timeout): scenes_resp = await httpsession.get(url, headers=headers) except (asyncio.TimeoutError, aiohttp.ClientError): @@ -83,7 +83,7 @@ class LifxCloudScene(Scene): try: httpsession = async_get_clientsession(self.hass) - with async_timeout.timeout(self._timeout, loop=self.hass.loop): + with async_timeout.timeout(self._timeout): await httpsession.put(url, headers=self._headers) except (asyncio.TimeoutError, aiohttp.ClientError): diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index f9ce6eb05d4..d5fc087888e 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -303,7 +303,7 @@ async def async_setup(hass, config): light.async_update_ha_state(True)) if update_tasks: - await asyncio.wait(update_tasks, loop=hass.loop) + await asyncio.wait(update_tasks) # Listen for light on and light off service calls. hass.services.async_register( diff --git a/homeassistant/components/logi_circle/__init__.py b/homeassistant/components/logi_circle/__init__.py index 1b74a9df03b..4e5ad0c5aeb 100644 --- a/homeassistant/components/logi_circle/__init__.py +++ b/homeassistant/components/logi_circle/__init__.py @@ -118,7 +118,7 @@ async def async_setup_entry(hass, entry): return False try: - with async_timeout.timeout(_TIMEOUT, loop=hass.loop): + with async_timeout.timeout(_TIMEOUT): # Ensure the cameras property returns the same Camera objects for # all devices. Performs implicit login and session validation. await logi_circle.synchronize_cameras() diff --git a/homeassistant/components/logi_circle/config_flow.py b/homeassistant/components/logi_circle/config_flow.py index ba772fb0fed..728ca27ba51 100644 --- a/homeassistant/components/logi_circle/config_flow.py +++ b/homeassistant/components/logi_circle/config_flow.py @@ -160,7 +160,7 @@ class LogiCircleFlowHandler(config_entries.ConfigFlow): cache_file=DEFAULT_CACHEDB) try: - with async_timeout.timeout(_TIMEOUT, loop=self.hass.loop): + with async_timeout.timeout(_TIMEOUT): await logi_session.authorize(code) except AuthorizationFailed: (self.hass.data[DATA_FLOW_IMPL][DOMAIN] diff --git a/homeassistant/components/mailbox/__init__.py b/homeassistant/components/mailbox/__init__.py index 8f851146464..939cc4a2aa2 100644 --- a/homeassistant/components/mailbox/__init__.py +++ b/homeassistant/components/mailbox/__init__.py @@ -82,7 +82,7 @@ async def async_setup(hass, config): in config_per_platform(config, DOMAIN)] if setup_tasks: - await asyncio.wait(setup_tasks, loop=hass.loop) + await asyncio.wait(setup_tasks) async def async_platform_discovered(platform, info): """Handle for discovered platform.""" @@ -241,9 +241,8 @@ class MailboxMediaView(MailboxView): """Retrieve media.""" mailbox = self.get_mailbox(platform) - hass = request.app['hass'] with suppress(asyncio.CancelledError, asyncio.TimeoutError): - with async_timeout.timeout(10, loop=hass.loop): + with async_timeout.timeout(10): try: stream = await mailbox.async_get_media(msgid) except StreamError as err: diff --git a/homeassistant/components/marytts/tts.py b/homeassistant/components/marytts/tts.py index 294383cb4dd..a17b95d1711 100644 --- a/homeassistant/components/marytts/tts.py +++ b/homeassistant/components/marytts/tts.py @@ -75,7 +75,7 @@ class MaryTTSProvider(Provider): actual_language = re.sub('-', '_', language) try: - with async_timeout.timeout(10, loop=self.hass.loop): + with async_timeout.timeout(10): url = 'http://{}:{}/process?'.format(self._host, self._port) audio = self._codec.upper() diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index b433a90f329..63e2a127fd7 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -777,7 +777,7 @@ async def _async_fetch_image(hass, url): url = hass.config.api.base_url + url if url not in cache_images: - cache_images[url] = {CACHE_LOCK: asyncio.Lock(loop=hass.loop)} + cache_images[url] = {CACHE_LOCK: asyncio.Lock()} async with cache_images[url][CACHE_LOCK]: if CACHE_CONTENT in cache_images[url]: @@ -786,7 +786,7 @@ async def _async_fetch_image(hass, url): content, content_type = (None, None) websession = async_get_clientsession(hass) try: - with async_timeout.timeout(10, loop=hass.loop): + with async_timeout.timeout(10): response = await websession.get(url) if response.status == 200: diff --git a/homeassistant/components/microsoft_face/__init__.py b/homeassistant/components/microsoft_face/__init__.py index 25b74698da6..157ebe9d3aa 100644 --- a/homeassistant/components/microsoft_face/__init__.py +++ b/homeassistant/components/microsoft_face/__init__.py @@ -277,7 +277,7 @@ class MicrosoftFace: tasks.append(self._entities[g_id].async_update_ha_state()) if tasks: - await asyncio.wait(tasks, loop=self.hass.loop) + await asyncio.wait(tasks) async def call_api(self, method, function, data=None, binary=False, params=None): @@ -297,7 +297,7 @@ class MicrosoftFace: payload = None try: - with async_timeout.timeout(self.timeout, loop=self.hass.loop): + with async_timeout.timeout(self.timeout): response = await getattr(self.websession, method)( url, data=payload, headers=headers, params=params) diff --git a/homeassistant/components/mjpeg/camera.py b/homeassistant/components/mjpeg/camera.py index b9aa9c3e186..697ed8c52a8 100644 --- a/homeassistant/components/mjpeg/camera.py +++ b/homeassistant/components/mjpeg/camera.py @@ -112,7 +112,7 @@ class MjpegCamera(Camera): verify_ssl=self._verify_ssl ) try: - with async_timeout.timeout(10, loop=self.hass.loop): + with async_timeout.timeout(10): response = await websession.get( self._still_image_url, auth=self._auth) diff --git a/homeassistant/components/mobile_app/notify.py b/homeassistant/components/mobile_app/notify.py index a69c020cfc8..721751e69a8 100644 --- a/homeassistant/components/mobile_app/notify.py +++ b/homeassistant/components/mobile_app/notify.py @@ -115,7 +115,7 @@ class MobileAppNotificationService(BaseNotificationService): data['registration_info'] = reg_info try: - with async_timeout.timeout(10, loop=self.hass.loop): + with async_timeout.timeout(10): response = await self._session.post(push_url, json=data) result = await response.json() diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 3de53145cfc..4ba8f1a5cc5 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -651,7 +651,7 @@ class MQTT: self.birth_message = birth_message self.connected = False self._mqttc = None # type: mqtt.Client - self._paho_lock = asyncio.Lock(loop=hass.loop) + self._paho_lock = asyncio.Lock() if protocol == PROTOCOL_31: proto = mqtt.MQTTv31 # type: int diff --git a/homeassistant/components/mysensors/gateway.py b/homeassistant/components/mysensors/gateway.py index 19f8b82a669..b0d8c4dfb3e 100644 --- a/homeassistant/components/mysensors/gateway.py +++ b/homeassistant/components/mysensors/gateway.py @@ -151,9 +151,9 @@ async def finish_setup(hass, hass_config, gateways): start_tasks.append(_gw_start(hass, gateway)) if discover_tasks: # Make sure all devices and platforms are loaded before gateway start. - await asyncio.wait(discover_tasks, loop=hass.loop) + await asyncio.wait(discover_tasks) if start_tasks: - await asyncio.wait(start_tasks, loop=hass.loop) + await asyncio.wait(start_tasks) async def _discover_persistent_devices(hass, hass_config, gateway): @@ -172,7 +172,7 @@ async def _discover_persistent_devices(hass, hass_config, gateway): tasks.append(discover_mysensors_platform( hass, hass_config, platform, dev_ids)) if tasks: - await asyncio.wait(tasks, loop=hass.loop) + await asyncio.wait(tasks) async def _gw_start(hass, gateway): @@ -196,7 +196,7 @@ async def _gw_start(hass, gateway): hass.data[gateway_ready_key] = gateway_ready try: - with async_timeout.timeout(GATEWAY_READY_TIMEOUT, loop=hass.loop): + with async_timeout.timeout(GATEWAY_READY_TIMEOUT): await gateway_ready except asyncio.TimeoutError: _LOGGER.warning( diff --git a/homeassistant/components/no_ip/__init__.py b/homeassistant/components/no_ip/__init__.py index 6a714747484..13bc4c2aa4b 100644 --- a/homeassistant/components/no_ip/__init__.py +++ b/homeassistant/components/no_ip/__init__.py @@ -89,7 +89,7 @@ async def _update_no_ip(hass, session, domain, auth_str, timeout): } try: - with async_timeout.timeout(timeout, loop=hass.loop): + with async_timeout.timeout(timeout): resp = await session.get(url, params=params, headers=headers) body = await resp.text() diff --git a/homeassistant/components/notify/__init__.py b/homeassistant/components/notify/__init__.py index 8bb3384aebd..42beb7a65b6 100644 --- a/homeassistant/components/notify/__init__.py +++ b/homeassistant/components/notify/__init__.py @@ -139,7 +139,7 @@ async def async_setup(hass, config): in config_per_platform(config, DOMAIN)] if setup_tasks: - await asyncio.wait(setup_tasks, loop=hass.loop) + await asyncio.wait(setup_tasks) async def async_platform_discovered(platform, info): """Handle for discovered platform.""" diff --git a/homeassistant/components/onvif/camera.py b/homeassistant/components/onvif/camera.py index 68c3c819567..c308ba2c4d2 100644 --- a/homeassistant/components/onvif/camera.py +++ b/homeassistant/components/onvif/camera.py @@ -308,7 +308,7 @@ class ONVIFHassCamera(Camera): image = await asyncio.shield(ffmpeg.get_image( self._input, output_format=IMAGE_JPEG, - extra_cmd=self._ffmpeg_arguments), loop=self.hass.loop) + extra_cmd=self._ffmpeg_arguments)) return image async def handle_async_mjpeg_stream(self, request): diff --git a/homeassistant/components/openalpr_cloud/image_processing.py b/homeassistant/components/openalpr_cloud/image_processing.py index 12146009fac..78707d2f0a2 100644 --- a/homeassistant/components/openalpr_cloud/image_processing.py +++ b/homeassistant/components/openalpr_cloud/image_processing.py @@ -108,7 +108,7 @@ class OpenAlprCloudEntity(ImageProcessingAlprEntity): } try: - with async_timeout.timeout(self.timeout, loop=self.hass.loop): + with async_timeout.timeout(self.timeout): request = await websession.post( OPENALPR_API_URL, params=params, data=body ) diff --git a/homeassistant/components/prowl/notify.py b/homeassistant/components/prowl/notify.py index 1f2067cc660..7511a89370c 100644 --- a/homeassistant/components/prowl/notify.py +++ b/homeassistant/components/prowl/notify.py @@ -52,7 +52,7 @@ class ProwlNotificationService(BaseNotificationService): session = async_get_clientsession(self._hass) try: - with async_timeout.timeout(10, loop=self._hass.loop): + with async_timeout.timeout(10): response = await session.post(url, data=payload) result = await response.text() diff --git a/homeassistant/components/push/camera.py b/homeassistant/components/push/camera.py index c962aee91ca..ccef0e72cda 100644 --- a/homeassistant/components/push/camera.py +++ b/homeassistant/components/push/camera.py @@ -60,7 +60,7 @@ async def async_setup_platform(hass, config, async_add_entities, async def handle_webhook(hass, webhook_id, request): """Handle incoming webhook POST with image files.""" try: - with async_timeout.timeout(5, loop=hass.loop): + with async_timeout.timeout(5): data = dict(await request.post()) except (asyncio.TimeoutError, aiohttp.web.HTTPException) as error: _LOGGER.error("Could not get information from POST <%s>", error) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 97654b21c6d..528f6f4a8a3 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -141,7 +141,7 @@ class Recorder(threading.Thread): self.queue = queue.Queue() # type: Any self.recording_start = dt_util.utcnow() self.db_url = uri - self.async_db_ready = asyncio.Future(loop=hass.loop) + self.async_db_ready = asyncio.Future() self.engine = None # type: Any self.run_info = None # type: Any diff --git a/homeassistant/components/recswitch/switch.py b/homeassistant/components/recswitch/switch.py index c43064c5674..90b0bd0b218 100644 --- a/homeassistant/components/recswitch/switch.py +++ b/homeassistant/components/recswitch/switch.py @@ -33,7 +33,7 @@ async def async_setup_platform(hass, config, async_add_entities, if not hass.data.get(DATA_RSN): hass.data[DATA_RSN] = RSNetwork() - job = hass.data[DATA_RSN].create_datagram_endpoint(loop=hass.loop) + job = hass.data[DATA_RSN].create_datagram_endpoint() hass.async_create_task(job) device = hass.data[DATA_RSN].register_device(mac_address, host) diff --git a/homeassistant/components/rest/switch.py b/homeassistant/components/rest/switch.py index 2ef45b226fe..65a06908881 100644 --- a/homeassistant/components/rest/switch.py +++ b/homeassistant/components/rest/switch.py @@ -149,7 +149,7 @@ class RestSwitch(SwitchDevice): """Send a state update to the device.""" websession = async_get_clientsession(self.hass, self._verify_ssl) - with async_timeout.timeout(self._timeout, loop=self.hass.loop): + with async_timeout.timeout(self._timeout): req = await getattr(websession, self._method)( self._resource, auth=self._auth, data=bytes(body, 'utf-8'), headers=self._headers) @@ -168,7 +168,7 @@ class RestSwitch(SwitchDevice): """Get the latest data from REST API and update the state.""" websession = async_get_clientsession(hass, self._verify_ssl) - with async_timeout.timeout(self._timeout, loop=hass.loop): + with async_timeout.timeout(self._timeout): req = await websession.get(self._resource, auth=self._auth, headers=self._headers) text = await req.text() diff --git a/homeassistant/components/rest_command/__init__.py b/homeassistant/components/rest_command/__init__.py index 01c5d837ca9..a37e7f3e8ba 100644 --- a/homeassistant/components/rest_command/__init__.py +++ b/homeassistant/components/rest_command/__init__.py @@ -92,7 +92,7 @@ async def async_setup(hass, config): 'utf-8') try: - with async_timeout.timeout(timeout, loop=hass.loop): + with async_timeout.timeout(timeout): request = await getattr(websession, method)( template_url.async_render(variables=service.data), data=payload, diff --git a/homeassistant/components/ring/camera.py b/homeassistant/components/ring/camera.py index 2a680a63b78..ddab2861539 100644 --- a/homeassistant/components/ring/camera.py +++ b/homeassistant/components/ring/camera.py @@ -116,7 +116,7 @@ class RingCam(Camera): image = await asyncio.shield(ffmpeg.get_image( self._video_url, output_format=IMAGE_JPEG, - extra_cmd=self._ffmpeg_arguments), loop=self.hass.loop) + extra_cmd=self._ffmpeg_arguments)) return image async def handle_async_mjpeg_stream(self, request): diff --git a/homeassistant/components/scene/__init__.py b/homeassistant/components/scene/__init__.py index 86392c0902d..f15e3ec61f0 100644 --- a/homeassistant/components/scene/__init__.py +++ b/homeassistant/components/scene/__init__.py @@ -70,7 +70,7 @@ async def async_setup(hass, config): tasks = [scene.async_activate() for scene in target_scenes] if tasks: - await asyncio.wait(tasks, loop=hass.loop) + await asyncio.wait(tasks) hass.services.async_register( DOMAIN, SERVICE_TURN_ON, async_handle_scene_service, diff --git a/homeassistant/components/script/__init__.py b/homeassistant/components/script/__init__.py index 90aefcc7aaa..36cb144fada 100644 --- a/homeassistant/components/script/__init__.py +++ b/homeassistant/components/script/__init__.py @@ -82,7 +82,7 @@ async def async_setup(hass, config): await asyncio.wait([ script.async_turn_off() for script in await component.async_extract_from_service(service) - ], loop=hass.loop) + ]) async def toggle_service(service): """Toggle a script.""" diff --git a/homeassistant/components/sensibo/climate.py b/homeassistant/components/sensibo/climate.py index d2f95dcee79..0becbce5bca 100644 --- a/homeassistant/components/sensibo/climate.py +++ b/homeassistant/components/sensibo/climate.py @@ -101,7 +101,7 @@ async def async_setup_platform(hass, config, async_add_entities, update_tasks.append(climate.async_update_ha_state(True)) if update_tasks: - await asyncio.wait(update_tasks, loop=hass.loop) + await asyncio.wait(update_tasks) hass.services.async_register( DOMAIN, SERVICE_ASSUME_STATE, async_assume_state, schema=ASSUME_STATE_SCHEMA) diff --git a/homeassistant/components/sma/sensor.py b/homeassistant/components/sma/sensor.py index 9b6406484df..9f33e236186 100644 --- a/homeassistant/components/sma/sensor.py +++ b/homeassistant/components/sma/sensor.py @@ -141,7 +141,7 @@ async def async_setup_platform( if task: tasks.append(task) if tasks: - await asyncio.wait(tasks, loop=hass.loop) + await asyncio.wait(tasks) interval = config.get(CONF_SCAN_INTERVAL) or timedelta(seconds=5) async_track_time_interval(hass, async_sma, interval) diff --git a/homeassistant/components/smhi/weather.py b/homeassistant/components/smhi/weather.py index ab5d08e770b..feeb22608a6 100644 --- a/homeassistant/components/smhi/weather.py +++ b/homeassistant/components/smhi/weather.py @@ -107,7 +107,7 @@ class SmhiWeather(WeatherEntity): RETRY_TIMEOUT, self.retry_update()) try: - with async_timeout.timeout(10, loop=self.hass.loop): + with async_timeout.timeout(10): self._forecasts = await self.get_weather_forecast() self._fail_count = 0 diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 5d1cd138260..40369597646 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -63,7 +63,7 @@ class SonosData: def __init__(self, hass): """Initialize the data.""" self.entities = [] - self.topology_condition = asyncio.Condition(loop=hass.loop) + self.topology_condition = asyncio.Condition() async def async_setup_platform(hass, diff --git a/homeassistant/components/squeezebox/media_player.py b/homeassistant/components/squeezebox/media_player.py index d25d2f03fce..c6b995963d9 100644 --- a/homeassistant/components/squeezebox/media_player.py +++ b/homeassistant/components/squeezebox/media_player.py @@ -128,7 +128,7 @@ async def async_setup_platform(hass, config, async_add_entities, update_tasks.append(player.async_update_ha_state(True)) if update_tasks: - await asyncio.wait(update_tasks, loop=hass.loop) + await asyncio.wait(update_tasks) for service in SERVICE_TO_METHOD: schema = SERVICE_TO_METHOD[service]['schema'] @@ -179,7 +179,7 @@ class LogitechMediaServer: try: websession = async_get_clientsession(self.hass) - with async_timeout.timeout(TIMEOUT, loop=self.hass.loop): + with async_timeout.timeout(TIMEOUT): response = await websession.post( url, data=data, diff --git a/homeassistant/components/startca/sensor.py b/homeassistant/components/startca/sensor.py index fe2c35c39b7..f384bae005b 100644 --- a/homeassistant/components/startca/sensor.py +++ b/homeassistant/components/startca/sensor.py @@ -138,7 +138,7 @@ class StartcaData: _LOGGER.debug("Updating Start.ca usage data") url = 'https://www.start.ca/support/usage/api?key=' + \ self.api_key - with async_timeout.timeout(REQUEST_TIMEOUT, loop=self.loop): + with async_timeout.timeout(REQUEST_TIMEOUT): req = await self.websession.get(url) if req.status != 200: _LOGGER.error("Request failed with status: %u", req.status) diff --git a/homeassistant/components/switcher_kis/__init__.py b/homeassistant/components/switcher_kis/__init__.py index 9fb51992cd2..8f959369b7b 100644 --- a/homeassistant/components/switcher_kis/__init__.py +++ b/homeassistant/components/switcher_kis/__init__.py @@ -62,8 +62,7 @@ async def async_setup(hass: HomeAssistantType, config: Dict) -> bool: EVENT_HOMEASSISTANT_STOP, async_stop_bridge)) try: - device_data = await wait_for( - v2bridge.queue.get(), timeout=10.0, loop=hass.loop) + device_data = await wait_for(v2bridge.queue.get(), timeout=10.0) except (Asyncio_TimeoutError, RuntimeError): _LOGGER.exception("failed to get response from device") await v2bridge.stop() diff --git a/homeassistant/components/tado/device_tracker.py b/homeassistant/components/tado/device_tracker.py index 7812bbd812b..3bb62f328b9 100644 --- a/homeassistant/components/tado/device_tracker.py +++ b/homeassistant/components/tado/device_tracker.py @@ -61,7 +61,7 @@ class TadoDeviceScanner(DeviceScanner): self.tadoapiurl += '?username={username}&password={password}' self.websession = async_create_clientsession( - hass, cookie_jar=aiohttp.CookieJar(unsafe=True, loop=hass.loop)) + hass, cookie_jar=aiohttp.CookieJar(unsafe=True)) self.success_init = asyncio.run_coroutine_threadsafe( self._async_update_info(), hass.loop diff --git a/homeassistant/components/teksavvy/sensor.py b/homeassistant/components/teksavvy/sensor.py index de74ceda9f5..78b6e26b1ef 100644 --- a/homeassistant/components/teksavvy/sensor.py +++ b/homeassistant/components/teksavvy/sensor.py @@ -132,7 +132,7 @@ class TekSavvyData: _LOGGER.debug("Updating TekSavvy data") url = "https://api.teksavvy.com/"\ "web/Usage/UsageSummaryRecords?$filter=IsCurrent%20eq%20true" - with async_timeout.timeout(REQUEST_TIMEOUT, loop=self.loop): + with async_timeout.timeout(REQUEST_TIMEOUT): req = await self.websession.get(url, headers=headers) if req.status != 200: _LOGGER.error("Request failed with status: %u", req.status) diff --git a/homeassistant/components/thethingsnetwork/sensor.py b/homeassistant/components/thethingsnetwork/sensor.py index e9073c4f98c..abfe07747d5 100644 --- a/homeassistant/components/thethingsnetwork/sensor.py +++ b/homeassistant/components/thethingsnetwork/sensor.py @@ -124,7 +124,7 @@ class TtnDataStorage: """Get the current state from The Things Network Data Storage.""" try: session = async_get_clientsession(self._hass) - with async_timeout.timeout(DEFAULT_TIMEOUT, loop=self._hass.loop): + with async_timeout.timeout(DEFAULT_TIMEOUT): response = await session.get(self._url, headers=self._headers) except (asyncio.TimeoutError, aiohttp.ClientError): diff --git a/homeassistant/components/tradfri/config_flow.py b/homeassistant/components/tradfri/config_flow.py index 0ad269b8780..bbbe104f296 100644 --- a/homeassistant/components/tradfri/config_flow.py +++ b/homeassistant/components/tradfri/config_flow.py @@ -143,7 +143,7 @@ async def authenticate(hass, host, security_code): identity = uuid4().hex - api_factory = APIFactory(host, psk_id=identity, loop=hass.loop) + api_factory = APIFactory(host, psk_id=identity) try: with async_timeout.timeout(5): diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index 8af22fbb460..559cc4a16e6 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -165,7 +165,7 @@ async def async_setup(hass, config): in config_per_platform(config, DOMAIN)] if setup_tasks: - await asyncio.wait(setup_tasks, loop=hass.loop) + await asyncio.wait(setup_tasks) async def async_clear_cache_handle(service): """Handle clear cache service call.""" diff --git a/homeassistant/components/upc_connect/device_tracker.py b/homeassistant/components/upc_connect/device_tracker.py index 4b4c32182bd..cf8e548ac61 100644 --- a/homeassistant/components/upc_connect/device_tracker.py +++ b/homeassistant/components/upc_connect/device_tracker.py @@ -81,7 +81,7 @@ class UPCDeviceScanner(DeviceScanner): """Get first token.""" try: # get first token - with async_timeout.timeout(10, loop=self.hass.loop): + with async_timeout.timeout(10): response = await self.websession.get( "http://{}/common_page/login.html".format(self.host), headers=self.headers) @@ -99,7 +99,7 @@ class UPCDeviceScanner(DeviceScanner): async def _async_ws_function(self, function): """Execute a command on UPC firmware webservice.""" try: - with async_timeout.timeout(10, loop=self.hass.loop): + with async_timeout.timeout(10): # The 'token' parameter has to be first, and 'fun' second # or the UPC firmware will return an error response = await self.websession.post( diff --git a/homeassistant/components/updater/__init__.py b/homeassistant/components/updater/__init__.py index 95b1372418c..b7e8e47e8c2 100644 --- a/homeassistant/components/updater/__init__.py +++ b/homeassistant/components/updater/__init__.py @@ -136,7 +136,7 @@ async def get_newest_version(hass, huuid, include_components): session = async_get_clientsession(hass) try: - with async_timeout.timeout(5, loop=hass.loop): + with async_timeout.timeout(5): req = await session.post(UPDATER_URL, json=info_object) _LOGGER.info(("Submitted analytics to Home Assistant servers. " "Information submitted includes %s"), info_object) diff --git a/homeassistant/components/viaggiatreno/sensor.py b/homeassistant/components/viaggiatreno/sensor.py index ee939d4a594..704cb77f5c8 100644 --- a/homeassistant/components/viaggiatreno/sensor.py +++ b/homeassistant/components/viaggiatreno/sensor.py @@ -67,7 +67,7 @@ async def async_http_request(hass, uri): """Perform actual request.""" try: session = hass.helpers.aiohttp_client.async_get_clientsession(hass) - with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop): + with async_timeout.timeout(REQUEST_TIMEOUT): req = await session.get(uri) if req.status != 200: return {'error': req.status} diff --git a/homeassistant/components/voicerss/tts.py b/homeassistant/components/voicerss/tts.py index d5340e45b5c..eaa605ee265 100644 --- a/homeassistant/components/voicerss/tts.py +++ b/homeassistant/components/voicerss/tts.py @@ -116,7 +116,7 @@ class VoiceRSSProvider(Provider): form_data['hl'] = language try: - with async_timeout.timeout(10, loop=self.hass.loop): + with async_timeout.timeout(10): request = await websession.post( VOICERSS_API_URL, data=form_data ) diff --git a/homeassistant/components/websocket_api/http.py b/homeassistant/components/websocket_api/http.py index 80592cc7151..b652f38ee2f 100644 --- a/homeassistant/components/websocket_api/http.py +++ b/homeassistant/components/websocket_api/http.py @@ -40,7 +40,7 @@ class WebSocketHandler: self.hass = hass self.request = request self.wsock = None - self._to_write = asyncio.Queue(maxsize=MAX_PENDING_MSG, loop=hass.loop) + self._to_write = asyncio.Queue(maxsize=MAX_PENDING_MSG) self._handle_task = None self._writer_task = None self._logger = logging.getLogger( @@ -101,7 +101,7 @@ class WebSocketHandler: # pylint: disable=no-member self._handle_task = asyncio.current_task() else: - self._handle_task = asyncio.Task.current_task(loop=self.hass.loop) + self._handle_task = asyncio.Task.current_task() @callback def handle_hass_stop(event): diff --git a/homeassistant/components/worxlandroid/sensor.py b/homeassistant/components/worxlandroid/sensor.py index fa4fcc96c12..668e7240372 100644 --- a/homeassistant/components/worxlandroid/sensor.py +++ b/homeassistant/components/worxlandroid/sensor.py @@ -88,7 +88,7 @@ class WorxLandroidSensor(Entity): try: session = async_get_clientsession(self.hass) - with async_timeout.timeout(self.timeout, loop=self.hass.loop): + with async_timeout.timeout(self.timeout): auth = aiohttp.helpers.BasicAuth('admin', self.pin) mower_response = await session.get(self.url, auth=auth) except (asyncio.TimeoutError, aiohttp.ClientError): diff --git a/homeassistant/components/wunderground/sensor.py b/homeassistant/components/wunderground/sensor.py index 7ad1a6fd75a..23fc02288c4 100644 --- a/homeassistant/components/wunderground/sensor.py +++ b/homeassistant/components/wunderground/sensor.py @@ -807,7 +807,7 @@ class WUndergroundData: async def async_update(self): """Get the latest data from WUnderground.""" try: - with async_timeout.timeout(10, loop=self._hass.loop): + with async_timeout.timeout(10): response = await self._session.get(self._build_url()) result = await response.json() if "error" in result['response']: diff --git a/homeassistant/components/xiaomi/camera.py b/homeassistant/components/xiaomi/camera.py index e541936ef0e..224c620e8ed 100644 --- a/homeassistant/components/xiaomi/camera.py +++ b/homeassistant/components/xiaomi/camera.py @@ -140,7 +140,7 @@ class XiaomiCamera(Camera): ffmpeg = ImageFrame(self._manager.binary, loop=self.hass.loop) self._last_image = await asyncio.shield(ffmpeg.get_image( url, output_format=IMAGE_JPEG, - extra_cmd=self._extra_arguments), loop=self.hass.loop) + extra_cmd=self._extra_arguments)) self._last_url = url return self._last_image diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index ea00cd6d95e..01c896c1f75 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -467,7 +467,7 @@ async def async_setup_platform(hass, config, async_add_entities, update_tasks.append(device.async_update_ha_state(True)) if update_tasks: - await asyncio.wait(update_tasks, loop=hass.loop) + await asyncio.wait(update_tasks) for air_purifier_service in SERVICE_TO_METHOD: schema = SERVICE_TO_METHOD[air_purifier_service].get( diff --git a/homeassistant/components/xiaomi_miio/light.py b/homeassistant/components/xiaomi_miio/light.py index fa853d1f83d..951e3db511f 100644 --- a/homeassistant/components/xiaomi_miio/light.py +++ b/homeassistant/components/xiaomi_miio/light.py @@ -203,7 +203,7 @@ async def async_setup_platform(hass, config, async_add_entities, update_tasks.append(target_device.async_update_ha_state(True)) if update_tasks: - await asyncio.wait(update_tasks, loop=hass.loop) + await asyncio.wait(update_tasks) for xiaomi_miio_service in SERVICE_TO_METHOD: schema = SERVICE_TO_METHOD[xiaomi_miio_service].get( diff --git a/homeassistant/components/xiaomi_miio/remote.py b/homeassistant/components/xiaomi_miio/remote.py index 7cb0cd68439..6a78766801a 100644 --- a/homeassistant/components/xiaomi_miio/remote.py +++ b/homeassistant/components/xiaomi_miio/remote.py @@ -142,7 +142,7 @@ async def async_setup_platform(hass, config, async_add_entities, message['error']['message'] == "learn timeout"): await hass.async_add_executor_job(device.learn, slot) - await asyncio.sleep(1, loop=hass.loop) + await asyncio.sleep(1) _LOGGER.error("Timeout. No infrared command captured") hass.components.persistent_notification.async_create( diff --git a/homeassistant/components/xiaomi_miio/switch.py b/homeassistant/components/xiaomi_miio/switch.py index 91924c82821..1c3752c54c8 100644 --- a/homeassistant/components/xiaomi_miio/switch.py +++ b/homeassistant/components/xiaomi_miio/switch.py @@ -185,7 +185,7 @@ async def async_setup_platform(hass, config, async_add_entities, update_tasks.append(device.async_update_ha_state(True)) if update_tasks: - await asyncio.wait(update_tasks, loop=hass.loop) + await asyncio.wait(update_tasks) for plug_service in SERVICE_TO_METHOD: schema = SERVICE_TO_METHOD[plug_service].get('schema', SERVICE_SCHEMA) diff --git a/homeassistant/components/xiaomi_miio/vacuum.py b/homeassistant/components/xiaomi_miio/vacuum.py index ce527d41e25..c44a9e3fba3 100644 --- a/homeassistant/components/xiaomi_miio/vacuum.py +++ b/homeassistant/components/xiaomi_miio/vacuum.py @@ -165,7 +165,7 @@ async def async_setup_platform(hass, config, async_add_entities, update_tasks.append(update_coro) if update_tasks: - await asyncio.wait(update_tasks, loop=hass.loop) + await asyncio.wait(update_tasks) for vacuum_service in SERVICE_TO_METHOD: schema = SERVICE_TO_METHOD[vacuum_service].get( diff --git a/homeassistant/components/yandextts/tts.py b/homeassistant/components/yandextts/tts.py index e08f44d1974..89bf4e98c52 100644 --- a/homeassistant/components/yandextts/tts.py +++ b/homeassistant/components/yandextts/tts.py @@ -111,7 +111,7 @@ class YandexSpeechKitProvider(Provider): options = options or {} try: - with async_timeout.timeout(10, loop=self.hass.loop): + with async_timeout.timeout(10): url_param = { 'text': message, 'lang': actual_language, diff --git a/homeassistant/components/yi/camera.py b/homeassistant/components/yi/camera.py index 0dbb42c384e..5ee2f2d9b58 100644 --- a/homeassistant/components/yi/camera.py +++ b/homeassistant/components/yi/camera.py @@ -77,7 +77,7 @@ class YiCamera(Camera): """Retrieve the latest video file from the customized Yi FTP server.""" from aioftp import Client, StatusCodeError - ftp = Client(loop=self.hass.loop) + ftp = Client() try: await ftp.connect(self.host) await ftp.login(self.user, self.passwd) diff --git a/homeassistant/components/yr/sensor.py b/homeassistant/components/yr/sensor.py index c9f57abf5d9..8a28fe42f89 100644 --- a/homeassistant/components/yr/sensor.py +++ b/homeassistant/components/yr/sensor.py @@ -160,7 +160,7 @@ class YrData: async_call_later(self.hass, minutes*60, self.fetching_data) try: websession = async_get_clientsession(self.hass) - with async_timeout.timeout(10, loop=self.hass.loop): + with async_timeout.timeout(10): resp = await websession.get( self._url, params=self._urlparams) if resp.status != 200: @@ -247,4 +247,4 @@ class YrData: tasks.append(dev.async_update_ha_state()) if tasks: - await asyncio.wait(tasks, loop=self.hass.loop) + await asyncio.wait(tasks) diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index 10046825ad3..51e956e3314 100644 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -715,7 +715,7 @@ async def async_setup_entry(hass, config_entry): network.state_str) break else: - await asyncio.sleep(1, loop=hass.loop) + await asyncio.sleep(1) hass.async_add_job(_finalize_start) diff --git a/homeassistant/core.py b/homeassistant/core.py index 5941739fcc5..b732eb0d4b3 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -346,7 +346,7 @@ class HomeAssistant: def block_till_done(self) -> None: """Block till all pending work is done.""" run_coroutine_threadsafe( - self.async_block_till_done(), loop=self.loop).result() + self.async_block_till_done(), self.loop).result() async def async_block_till_done(self) -> None: """Block till all pending work is done.""" diff --git a/homeassistant/helpers/aiohttp_client.py b/homeassistant/helpers/aiohttp_client.py index f5b3e443d3a..ff2ce8b7d98 100644 --- a/homeassistant/helpers/aiohttp_client.py +++ b/homeassistant/helpers/aiohttp_client.py @@ -80,7 +80,7 @@ async def async_aiohttp_proxy_web( timeout: int = 10) -> Optional[web.StreamResponse]: """Stream websession request to aiohttp web response.""" try: - with async_timeout.timeout(timeout, loop=hass.loop): + with async_timeout.timeout(timeout): req = await web_coro except asyncio.CancelledError: @@ -120,7 +120,7 @@ async def async_aiohttp_proxy_stream(hass: HomeAssistantType, try: while True: - with async_timeout.timeout(timeout, loop=hass.loop): + with async_timeout.timeout(timeout): data = await stream.read(buffer_size) if not data: diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index 5a5f3dc8177..fb31e664605 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -109,7 +109,7 @@ class EntityComponent: tasks.append(self._async_setup_platform(p_type, p_config)) if tasks: - await asyncio.wait(tasks, loop=self.hass.loop) + await asyncio.wait(tasks) # Generic discovery listener for loading platform dynamically # Refer to: homeassistant.components.discovery.load_platform() @@ -250,7 +250,7 @@ class EntityComponent: in self._platforms.values()] if tasks: - await asyncio.wait(tasks, loop=self.hass.loop) + await asyncio.wait(tasks) self._platforms = { self.domain: self._platforms[self.domain] diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index a092c89405e..7908440e92b 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -45,7 +45,7 @@ class EntityPlatform: self._async_unsub_polling = None # Method to cancel the retry of setup self._async_cancel_retry_setup = None - self._process_updates = asyncio.Lock(loop=hass.loop) + self._process_updates = asyncio.Lock() # Platform is None for the EntityComponent "catch-all" EntityPlatform # which powers entity_component.add_entities @@ -122,8 +122,8 @@ class EntityPlatform: task = async_create_setup_task() await asyncio.wait_for( - asyncio.shield(task, loop=hass.loop), - SLOW_SETUP_MAX_WAIT, loop=hass.loop) + asyncio.shield(task), + SLOW_SETUP_MAX_WAIT) # Block till all entities are done if self._tasks: @@ -132,7 +132,7 @@ class EntityPlatform: if pending: await asyncio.wait( - pending, loop=self.hass.loop) + pending) hass.config.components.add(full_name) return True @@ -218,7 +218,7 @@ class EntityPlatform: if not tasks: return - await asyncio.wait(tasks, loop=self.hass.loop) + await asyncio.wait(tasks) self.async_entities_added_callback() if self._async_unsub_polling is not None or \ @@ -379,7 +379,7 @@ class EntityPlatform: tasks = [self.async_remove_entity(entity_id) for entity_id in self.entities] - await asyncio.wait(tasks, loop=self.hass.loop) + await asyncio.wait(tasks) if self._async_unsub_polling is not None: self._async_unsub_polling() @@ -419,4 +419,4 @@ class EntityPlatform: tasks.append(entity.async_update_ha_state(True)) if tasks: - await asyncio.wait(tasks, loop=self.hass.loop) + await asyncio.wait(tasks) diff --git a/homeassistant/helpers/state.py b/homeassistant/helpers/state.py index bbed1ffbbcd..992ba6c10cc 100644 --- a/homeassistant/helpers/state.py +++ b/homeassistant/helpers/state.py @@ -209,7 +209,7 @@ async def async_reproduce_state_legacy( ) if domain_tasks: - await asyncio.wait(domain_tasks, loop=hass.loop) + await asyncio.wait(domain_tasks) def state_as_number(state: State) -> float: diff --git a/homeassistant/helpers/storage.py b/homeassistant/helpers/storage.py index 5fbb7700458..67ce2f7a923 100644 --- a/homeassistant/helpers/storage.py +++ b/homeassistant/helpers/storage.py @@ -57,7 +57,7 @@ class Store: self._data = None self._unsub_delay_listener = None self._unsub_stop_listener = None - self._write_lock = asyncio.Lock(loop=hass.loop) + self._write_lock = asyncio.Lock() self._load_task = None self._encoder = encoder diff --git a/homeassistant/requirements.py b/homeassistant/requirements.py index a3d168d22e7..5039fbbd41e 100644 --- a/homeassistant/requirements.py +++ b/homeassistant/requirements.py @@ -26,7 +26,7 @@ async def async_process_requirements(hass: HomeAssistant, name: str, """ pip_lock = hass.data.get(DATA_PIP_LOCK) if pip_lock is None: - pip_lock = hass.data[DATA_PIP_LOCK] = asyncio.Lock(loop=hass.loop) + pip_lock = hass.data[DATA_PIP_LOCK] = asyncio.Lock() pkg_cache = hass.data.get(DATA_PKG_CACHE) if pkg_cache is None: diff --git a/homeassistant/scripts/benchmark/__init__.py b/homeassistant/scripts/benchmark/__init__.py index e231d7602cd..d159f7dcedd 100644 --- a/homeassistant/scripts/benchmark/__init__.py +++ b/homeassistant/scripts/benchmark/__init__.py @@ -54,7 +54,7 @@ async def async_million_events(hass): """Run a million events.""" count = 0 event_name = 'benchmark_event' - event = asyncio.Event(loop=hass.loop) + event = asyncio.Event() @core.callback def listener(_): @@ -81,7 +81,7 @@ async def async_million_events(hass): async def async_million_time_changed_helper(hass): """Run a million events through time changed helper.""" count = 0 - event = asyncio.Event(loop=hass.loop) + event = asyncio.Event() @core.callback def listener(_): @@ -112,7 +112,7 @@ async def async_million_state_changed_helper(hass): """Run a million events through state changed helper.""" count = 0 entity_id = 'light.kitchen' - event = asyncio.Event(loop=hass.loop) + event = asyncio.Event() @core.callback def listener(*args): diff --git a/homeassistant/setup.py b/homeassistant/setup.py index ee362ad130f..ec88a48ae7b 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -27,7 +27,7 @@ def setup_component(hass: core.HomeAssistant, domain: str, config: Dict) -> bool: """Set up a component and all its dependencies.""" return run_coroutine_threadsafe( # type: ignore - async_setup_component(hass, domain, config), loop=hass.loop).result() + async_setup_component(hass, domain, config), hass.loop).result() async def async_setup_component(hass: core.HomeAssistant, domain: str, @@ -69,7 +69,7 @@ async def _async_process_dependencies( if not tasks: return True - results = await asyncio.gather(*tasks, loop=hass.loop) + results = await asyncio.gather(*tasks) failed = [dependencies[idx] for idx, res in enumerate(results) if not res] diff --git a/homeassistant/util/logging.py b/homeassistant/util/logging.py index 317a30d9d56..a821c9b6fb8 100644 --- a/homeassistant/util/logging.py +++ b/homeassistant/util/logging.py @@ -64,7 +64,7 @@ class AsyncHandler: if blocking: while self._thread.is_alive(): - await asyncio.sleep(0, loop=self.loop) + await asyncio.sleep(0) def emit(self, record: Optional[logging.LogRecord]) -> None: """Process a record.""" diff --git a/tests/common.py b/tests/common.py index f7b3bc46bbd..f934d2990d3 100644 --- a/tests/common.py +++ b/tests/common.py @@ -87,6 +87,7 @@ def get_test_home_assistant(): else: loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) hass = loop.run_until_complete(async_test_home_assistant(loop)) stop_event = threading.Event() @@ -102,7 +103,7 @@ def get_test_home_assistant(): def start_hass(*mocks): """Start hass.""" - run_coroutine_threadsafe(hass.async_start(), loop=hass.loop).result() + run_coroutine_threadsafe(hass.async_start(), loop).result() def stop_hass(): """Stop hass.""" diff --git a/tests/components/automatic/test_device_tracker.py b/tests/components/automatic/test_device_tracker.py index 03b631b4689..317198f59c7 100644 --- a/tests/components/automatic/test_device_tracker.py +++ b/tests/components/automatic/test_device_tracker.py @@ -99,7 +99,7 @@ def test_valid_credentials( @asyncio.coroutine def ws_connect(): - return asyncio.Future(loop=hass.loop) + return asyncio.Future() mock_ws_connect.side_effect = ws_connect diff --git a/tests/components/dsmr/test_sensor.py b/tests/components/dsmr/test_sensor.py index 366d2163818..04bc4414aa7 100644 --- a/tests/components/dsmr/test_sensor.py +++ b/tests/components/dsmr/test_sensor.py @@ -78,7 +78,7 @@ def test_default_setup(hass, mock_connection_factory): telegram_callback(telegram) # after receiving telegram entities need to have the chance to update - yield from asyncio.sleep(0, loop=hass.loop) + yield from asyncio.sleep(0) # ensure entities have new state value after incoming telegram power_consumption = hass.states.get('sensor.power_consumption') @@ -183,9 +183,9 @@ def test_reconnect(hass, monkeypatch, mock_connection_factory): } # mock waiting coroutine while connection lasts - closed = asyncio.Event(loop=hass.loop) + closed = asyncio.Event() # Handshake so that `hass.async_block_till_done()` doesn't cycle forever - closed2 = asyncio.Event(loop=hass.loop) + closed2 = asyncio.Event() @asyncio.coroutine def wait_closed(): diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index d50501897d7..1487b6b8869 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -158,7 +158,7 @@ async def test_subscribe_unsubscribe_events(hass, websocket_client): hass.bus.async_fire('test_event', {'hello': 'world'}) hass.bus.async_fire('ignore_event') - with timeout(3, loop=hass.loop): + with timeout(3): msg = await websocket_client.receive_json() assert msg['id'] == 5 @@ -389,7 +389,7 @@ async def test_subscribe_unsubscribe_events_whitelist( hass.bus.async_fire('themes_updated') - with timeout(3, loop=hass.loop): + with timeout(3): msg = await websocket_client.receive_json() assert msg['id'] == 6 diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 65c22aa176f..95e1af403d4 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -209,7 +209,7 @@ async def test_platform_error_slow_setup(hass, caplog): async def setup_platform(*args): called.append(1) - await asyncio.sleep(1, loop=hass.loop) + await asyncio.sleep(1) platform = MockPlatform(async_setup_platform=setup_platform) component = EntityComponent(_LOGGER, DOMAIN, hass) diff --git a/tests/test_core.py b/tests/test_core.py index 2e9e14ed97a..15ab2baf3a9 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -184,7 +184,7 @@ class TestHomeAssistant(unittest.TestCase): self.hass.add_job(test_coro()) run_coroutine_threadsafe( - asyncio.wait(self.hass._pending_tasks, loop=self.hass.loop), + asyncio.wait(self.hass._pending_tasks), loop=self.hass.loop ).result() @@ -206,8 +206,8 @@ class TestHomeAssistant(unittest.TestCase): @asyncio.coroutine def wait_finish_callback(): """Wait until all stuff is scheduled.""" - yield from asyncio.sleep(0, loop=self.hass.loop) - yield from asyncio.sleep(0, loop=self.hass.loop) + yield from asyncio.sleep(0) + yield from asyncio.sleep(0) run_coroutine_threadsafe( wait_finish_callback(), self.hass.loop).result() @@ -227,8 +227,8 @@ class TestHomeAssistant(unittest.TestCase): @asyncio.coroutine def wait_finish_callback(): """Wait until all stuff is scheduled.""" - yield from asyncio.sleep(0, loop=self.hass.loop) - yield from asyncio.sleep(0, loop=self.hass.loop) + yield from asyncio.sleep(0) + yield from asyncio.sleep(0) for _ in range(2): self.hass.add_job(test_executor) @@ -252,8 +252,8 @@ class TestHomeAssistant(unittest.TestCase): @asyncio.coroutine def wait_finish_callback(): """Wait until all stuff is scheduled.""" - yield from asyncio.sleep(0, loop=self.hass.loop) - yield from asyncio.sleep(0, loop=self.hass.loop) + yield from asyncio.sleep(0) + yield from asyncio.sleep(0) for _ in range(2): self.hass.add_job(test_callback) From 9ac6f906ff298aee73708bd0c98b8911ef45025f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Thu, 23 May 2019 08:53:38 +0200 Subject: [PATCH 111/232] Update ambiclimate library (#24049) --- homeassistant/components/ambiclimate/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ambiclimate/manifest.json b/homeassistant/components/ambiclimate/manifest.json index bd1117d86bc..70c05704873 100644 --- a/homeassistant/components/ambiclimate/manifest.json +++ b/homeassistant/components/ambiclimate/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/components/ambiclimate", "requirements": [ - "ambiclimate==0.1.1" + "ambiclimate==0.1.2" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 6e82a9022a5..25fc72b514a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -167,7 +167,7 @@ alarmdecoder==1.13.2 alpha_vantage==2.1.0 # homeassistant.components.ambiclimate -ambiclimate==0.1.1 +ambiclimate==0.1.2 # homeassistant.components.amcrest amcrest==1.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f03364c7b0a..e56a34c10ed 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -61,7 +61,7 @@ aiounifi==4 aiozeroconf==0.1.8 # homeassistant.components.ambiclimate -ambiclimate==0.1.1 +ambiclimate==0.1.2 # homeassistant.components.apns apns2==0.3.0 From 085303c3490c4203d0aa8868eef70b6294ece548 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Thu, 23 May 2019 08:55:08 +0200 Subject: [PATCH 112/232] ESPHome component to use zeroconf discovery (#24043) * Move ESPHome component to use zeroconf discovery * Remove esphome from discovery component --- homeassistant/components/discovery/__init__.py | 1 - homeassistant/components/esphome/config_flow.py | 4 ++-- homeassistant/components/esphome/manifest.json | 1 + homeassistant/generated/zeroconf.py | 3 +++ tests/components/esphome/test_config_flow.py | 6 +++--- tests/components/zeroconf/test_init.py | 4 ++-- 6 files changed, 11 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/discovery/__init__.py b/homeassistant/components/discovery/__init__.py index 246a66eb9a1..7f05c70653c 100644 --- a/homeassistant/components/discovery/__init__.py +++ b/homeassistant/components/discovery/__init__.py @@ -52,7 +52,6 @@ SERVICE_XIAOMI_GW = 'xiaomi_gw' CONFIG_ENTRY_HANDLERS = { SERVICE_DAIKIN: 'daikin', SERVICE_DECONZ: 'deconz', - 'esphome': 'esphome', 'google_cast': 'cast', SERVICE_HEOS: 'heos', SERVICE_HUE: 'hue', diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py index f6b8bb9abd7..96ea8012bd4 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -69,8 +69,8 @@ class EsphomeFlowHandler(config_entries.ConfigFlow): description_placeholders={'name': self._name}, ) - async def async_step_discovery(self, user_input: ConfigType): - """Handle discovery.""" + async def async_step_zeroconf(self, user_input: ConfigType): + """Handle zeroconf discovery.""" address = user_input['properties'].get( 'address', user_input['hostname'][:-1]) for entry in self._async_current_entries(): diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index b50d11dbd12..71d233fee2e 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -7,6 +7,7 @@ "aioesphomeapi==2.0.1" ], "dependencies": [], + "zeroconf": ["_esphomelib._tcp.local."], "codeowners": [ "@OttoWinter" ] diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index 08c520b3816..f009132228c 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -7,5 +7,8 @@ To update, run python3 -m hassfest SERVICE_TYPES = { "_axis-video._tcp.local.": [ "axis" + ], + "_esphomelib._tcp.local.": [ + "esphome" ] } diff --git a/tests/components/esphome/test_config_flow.py b/tests/components/esphome/test_config_flow.py index 076ec0066a6..d27d449eb50 100644 --- a/tests/components/esphome/test_config_flow.py +++ b/tests/components/esphome/test_config_flow.py @@ -202,7 +202,7 @@ async def test_discovery_initiation(hass, mock_client): mock_client.device_info.return_value = mock_coro( MockDeviceInfo(False, "test8266")) - result = await flow.async_step_discovery(user_input=service_info) + result = await flow.async_step_zeroconf(user_input=service_info) assert result['type'] == 'form' assert result['step_id'] == 'discovery_confirm' assert result['description_placeholders']['name'] == 'test8266' @@ -229,7 +229,7 @@ async def test_discovery_already_configured_hostname(hass, mock_client): 'hostname': 'test8266.local.', 'properties': {} } - result = await flow.async_step_discovery(user_input=service_info) + result = await flow.async_step_zeroconf(user_input=service_info) assert result['type'] == 'abort' assert result['reason'] == 'already_configured' @@ -251,6 +251,6 @@ async def test_discovery_already_configured_ip(hass, mock_client): "address": "192.168.43.183" } } - result = await flow.async_step_discovery(user_input=service_info) + result = await flow.async_step_zeroconf(user_input=service_info) assert result['type'] == 'abort' assert result['reason'] == 'already_configured' diff --git a/tests/components/zeroconf/test_init.py b/tests/components/zeroconf/test_init.py index b3257f57714..0596d5e0ed5 100644 --- a/tests/components/zeroconf/test_init.py +++ b/tests/components/zeroconf/test_init.py @@ -36,5 +36,5 @@ async def test_setup(hass): hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) await hass.async_block_till_done() - assert len(MockServiceBrowser.mock_calls) == 1 - assert len(mock_config_flow.mock_calls) == 1 + assert len(MockServiceBrowser.mock_calls) == 2 + assert len(mock_config_flow.mock_calls) == 2 From d2eb5bb0f39e5a7ad2ceb615ad103f7ea6f254fc Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 23 May 2019 09:46:40 +0200 Subject: [PATCH 113/232] [skip ci] Update azure-pipelines.yml for Azure Pipelines --- azure-pipelines.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 02211dca902..7a1e6e550d7 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -185,3 +185,4 @@ jobs: git commit -am "Bump Home Assistant $version" git push + displayName: 'Update version files' From 7f7435f00302add4053b8894059262c9bdd405ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Thu, 23 May 2019 09:52:30 +0200 Subject: [PATCH 114/232] Add support for available property for broadlink (#23981) * Add support for available property for broadlink * Broadlink, except oserror * Broadlink, except oserror --- homeassistant/components/broadlink/sensor.py | 15 ++++++++++++--- homeassistant/components/broadlink/switch.py | 18 ++++++++++++++---- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/broadlink/sensor.py b/homeassistant/components/broadlink/sensor.py index c542d8f5549..d9a8121e635 100644 --- a/homeassistant/components/broadlink/sensor.py +++ b/homeassistant/components/broadlink/sensor.py @@ -1,7 +1,6 @@ """Support for the Broadlink RM2 Pro (only temperature) and A1 devices.""" import binascii import logging -import socket from datetime import timedelta import voluptuous as vol @@ -60,6 +59,7 @@ class BroadlinkSensor(Entity): """Initialize the sensor.""" self._name = '{} {}'.format(name, SENSOR_TYPES[sensor_type][0]) self._state = None + self._is_available = False self._type = sensor_type self._broadlink_data = broadlink_data self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] @@ -74,6 +74,11 @@ class BroadlinkSensor(Entity): """Return the state of the sensor.""" return self._state + @property + def available(self): + """Return True if entity is available.""" + return self._is_available + @property def unit_of_measurement(self): """Return the unit this state is expressed in.""" @@ -83,8 +88,11 @@ class BroadlinkSensor(Entity): """Get the latest data from the sensor.""" self._broadlink_data.update() if self._broadlink_data.data is None: + self._state = None + self._is_available = False return self._state = self._broadlink_data.data[self._type] + self._is_available = True class BroadlinkData: @@ -119,8 +127,9 @@ class BroadlinkData: if data is not None: self.data = self._schema(data) return - except socket.timeout as error: + except OSError as error: if retry < 1: + self.data = None _LOGGER.error(error) return except (vol.Invalid, vol.MultipleInvalid): @@ -131,7 +140,7 @@ class BroadlinkData: def _auth(self, retry=3): try: auth = self._device.auth() - except socket.timeout: + except OSError: auth = False if not auth and retry > 0: self._connect() diff --git a/homeassistant/components/broadlink/switch.py b/homeassistant/components/broadlink/switch.py index 8474584ab37..96a45322114 100644 --- a/homeassistant/components/broadlink/switch.py +++ b/homeassistant/components/broadlink/switch.py @@ -110,7 +110,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): broadlink_device.timeout = config.get(CONF_TIMEOUT) try: broadlink_device.auth() - except socket.timeout: + except OSError: _LOGGER.error("Failed to connect to device") add_entities(switches) @@ -127,6 +127,7 @@ class BroadlinkRMSwitch(SwitchDevice, RestoreEntity): self._command_on = command_on self._command_off = command_off self._device = device + self._is_available = False async def async_added_to_hass(self): """Call when entity about to be added to hass.""" @@ -145,6 +146,11 @@ class BroadlinkRMSwitch(SwitchDevice, RestoreEntity): """Return true if unable to access real state of entity.""" return True + @property + def available(self): + """Return True if entity is available.""" + return not self.should_poll or self._is_available + @property def should_poll(self): """Return the polling state.""" @@ -174,7 +180,7 @@ class BroadlinkRMSwitch(SwitchDevice, RestoreEntity): return True try: self._device.send_data(packet) - except (socket.timeout, ValueError) as error: + except (ValueError, OSError) as error: if retry < 1: _LOGGER.error("Error during sending a packet: %s", error) return False @@ -186,7 +192,7 @@ class BroadlinkRMSwitch(SwitchDevice, RestoreEntity): def _auth(self, retry=2): try: auth = self._device.auth() - except socket.timeout: + except OSError: auth = False if retry < 1: _LOGGER.error("Timeout during authorization") @@ -252,6 +258,7 @@ class BroadlinkSP2Switch(BroadlinkSP1Switch): except (socket.timeout, ValueError) as error: if retry < 1: _LOGGER.error("Error during updating the state: %s", error) + self._is_available = False return if not self._auth(): return @@ -260,6 +267,7 @@ class BroadlinkSP2Switch(BroadlinkSP1Switch): return self._update(retry-1) self._state = state self._load_power = load_power + self._is_available = True class BroadlinkMP1Slot(BroadlinkRMSwitch): @@ -285,10 +293,12 @@ class BroadlinkMP1Slot(BroadlinkRMSwitch): except (socket.timeout, ValueError) as error: if retry < 1: _LOGGER.error("Error during sending a packet: %s", error) + self._is_available = False return False if not self._auth(): return False return self._sendpacket(packet, max(0, retry-1)) + self._is_available = True return True @property @@ -338,7 +348,7 @@ class BroadlinkMP1Switch: """Authenticate the device.""" try: auth = self._device.auth() - except socket.timeout: + except OSError: auth = False if not auth and retry > 0: return self._auth(retry-1) From 8d22479d24d6f63f7f5a415e8b2fcda72bec1b2b Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 23 May 2019 07:00:41 -0500 Subject: [PATCH 115/232] Always update all Plex client types (#24038) --- homeassistant/components/plex/media_player.py | 79 ++++++++----------- 1 file changed, 33 insertions(+), 46 deletions(-) diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index 4a65808e049..4d5f70cf36e 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -29,7 +29,6 @@ MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1) PLEX_CONFIG_FILE = 'plex.conf' PLEX_DATA = 'plex' -CONF_INCLUDE_NON_CLIENTS = 'include_non_clients' CONF_USE_EPISODE_ART = 'use_episode_art' CONF_USE_CUSTOM_ENTITY_IDS = 'use_custom_entity_ids' CONF_SHOW_ALL_CONTROLS = 'show_all_controls' @@ -37,7 +36,6 @@ CONF_REMOVE_UNAVAILABLE_CLIENTS = 'remove_unavailable_clients' CONF_CLIENT_REMOVE_INTERVAL = 'client_remove_interval' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_INCLUDE_NON_CLIENTS, default=False): cv.boolean, vol.Optional(CONF_USE_EPISODE_ART, default=False): cv.boolean, vol.Optional(CONF_USE_CUSTOM_ENTITY_IDS, default=False): cv.boolean, vol.Optional(CONF_SHOW_ALL_CONTROLS, default=False): cv.boolean, @@ -160,8 +158,7 @@ def setup_plexserver( if device.machineIdentifier not in plex_clients: new_client = PlexClient( - config, device, None, plex_sessions, update_devices, - update_sessions) + config, device, None, plex_sessions, update_devices) plex_clients[device.machineIdentifier] = new_client _LOGGER.debug("New device: %s", device.machineIdentifier) new_plex_clients.append(new_client) @@ -171,27 +168,38 @@ def setup_plexserver( plex_clients[device.machineIdentifier].refresh(device, None) # add devices with a session and no client (ex. PlexConnect Apple TV's) - if config.get(CONF_INCLUDE_NON_CLIENTS): - # To avoid errors when plex sessions created during iteration - sessions = list(plex_sessions.items()) - for machine_identifier, (session, player) in sessions: - if machine_identifier in available_client_ids: - # Avoid using session if already added as a device. - _LOGGER.debug("Skipping session, device exists: %s", - machine_identifier) - continue + try: + sessions = plexserver.sessions() + except plexapi.exceptions.BadRequest: + _LOGGER.exception("Error listing plex sessions") + return + except requests.exceptions.RequestException as ex: + _LOGGER.warning( + "Could not connect to plex server at http://%s (%s)", host, ex) + return - if (machine_identifier not in plex_clients - and machine_identifier is not None): - new_client = PlexClient( - config, player, session, plex_sessions, update_devices, - update_sessions) - plex_clients[machine_identifier] = new_client - _LOGGER.debug("New session: %s", machine_identifier) - new_plex_clients.append(new_client) - else: - _LOGGER.debug("Refreshing session: %s", machine_identifier) - plex_clients[machine_identifier].refresh(None, session) + plex_sessions.clear() + for session in sessions: + for player in session.players: + plex_sessions[player.machineIdentifier] = session, player + + for machine_identifier, (session, player) in plex_sessions.items(): + if machine_identifier in available_client_ids: + # Avoid using session if already added as a device. + _LOGGER.debug("Skipping session, device exists: %s", + machine_identifier) + continue + + if (machine_identifier not in plex_clients + and machine_identifier is not None): + new_client = PlexClient( + config, player, session, plex_sessions, update_devices) + plex_clients[machine_identifier] = new_client + _LOGGER.debug("New session: %s", machine_identifier) + new_plex_clients.append(new_client) + else: + _LOGGER.debug("Refreshing session: %s", machine_identifier) + plex_clients[machine_identifier].refresh(None, session) clients_to_remove = [] for client in plex_clients.values(): @@ -219,25 +227,6 @@ def setup_plexserver( if new_plex_clients: add_entities_callback(new_plex_clients) - @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS) - def update_sessions(): - """Update the sessions objects.""" - try: - sessions = plexserver.sessions() - except plexapi.exceptions.BadRequest: - _LOGGER.exception("Error listing plex sessions") - return - except requests.exceptions.RequestException as ex: - _LOGGER.warning( - "Could not connect to plex server at http://%s (%s)", host, ex) - return - - plex_sessions.clear() - for session in sessions: - for player in session.players: - plex_sessions[player.machineIdentifier] = session, player - - update_sessions() update_devices() @@ -285,7 +274,7 @@ class PlexClient(MediaPlayerDevice): """Representation of a Plex device.""" def __init__(self, config, device, session, plex_sessions, - update_devices, update_sessions): + update_devices): """Initialize the Plex device.""" self._app_name = '' self._device = None @@ -309,7 +298,6 @@ class PlexClient(MediaPlayerDevice): self.config = config self.plex_sessions = plex_sessions self.update_devices = update_devices - self.update_sessions = update_sessions # General self._media_content_id = None self._media_content_rating = None @@ -575,7 +563,6 @@ class PlexClient(MediaPlayerDevice): def update(self): """Get the latest details.""" self.update_devices(no_throttle=True) - self.update_sessions(no_throttle=True) @property def _active_media_plexapi_type(self): From 1de0a0bbb95e177f7f109099bb63d785b070f922 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 23 May 2019 09:45:30 -0700 Subject: [PATCH 116/232] Convert stream source to method (#23905) * Convert stream source to method * Use async with --- homeassistant/components/amcrest/camera.py | 3 +- homeassistant/components/axis/camera.py | 3 +- homeassistant/components/camera/__init__.py | 53 +++++++++++++++------ homeassistant/components/doorbird/camera.py | 3 +- homeassistant/components/ffmpeg/camera.py | 3 +- homeassistant/components/foscam/camera.py | 3 +- homeassistant/components/generic/camera.py | 3 +- homeassistant/components/netatmo/camera.py | 3 +- homeassistant/components/onvif/camera.py | 3 +- tests/components/camera/test_init.py | 12 ++--- 10 files changed, 50 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/amcrest/camera.py b/homeassistant/components/amcrest/camera.py index e646c11f2e9..d75475dbb26 100644 --- a/homeassistant/components/amcrest/camera.py +++ b/homeassistant/components/amcrest/camera.py @@ -203,8 +203,7 @@ class AmcrestCam(Camera): """Return the camera model.""" return self._model - @property - def stream_source(self): + async def stream_source(self): """Return the source of the stream.""" return self._api.rtsp_url(typeno=self._resolution) diff --git a/homeassistant/components/axis/camera.py b/homeassistant/components/axis/camera.py index 08e40f4999a..c993e9d9f64 100644 --- a/homeassistant/components/axis/camera.py +++ b/homeassistant/components/axis/camera.py @@ -58,8 +58,7 @@ class AxisCamera(AxisEntityBase, MjpegCamera): """Return supported features.""" return SUPPORT_STREAM - @property - def stream_source(self): + async def stream_source(self): """Return the stream source.""" return AXIS_STREAM.format( self.device.config_entry.data[CONF_DEVICE][CONF_USERNAME], diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 352a9dd5060..b6e41e2cf11 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -107,11 +107,14 @@ async def async_request_stream(hass, entity_id, fmt): camera = _get_camera_from_entity_id(hass, entity_id) camera_prefs = hass.data[DATA_CAMERA_PREFS].get(entity_id) - if not camera.stream_source: + async with async_timeout.timeout(10): + source = await camera.stream_source() + + if not source: raise HomeAssistantError("{} does not support play stream service" .format(camera.entity_id)) - return request_stream(hass, camera.stream_source, fmt=fmt, + return request_stream(hass, source, fmt=fmt, keepalive=camera_prefs.preload_stream) @@ -121,7 +124,7 @@ async def async_get_image(hass, entity_id, timeout=10): camera = _get_camera_from_entity_id(hass, entity_id) with suppress(asyncio.CancelledError, asyncio.TimeoutError): - with async_timeout.timeout(timeout): + async with async_timeout.timeout(timeout): image = await camera.async_camera_image() if image: @@ -221,8 +224,16 @@ async def async_setup(hass, config): async def preload_stream(hass, _): for camera in component.entities: camera_prefs = prefs.get(camera.entity_id) - if camera.stream_source and camera_prefs.preload_stream: - request_stream(hass, camera.stream_source, keepalive=True) + if not camera_prefs.preload_stream: + continue + + async with async_timeout.timeout(10): + source = await camera.stream_source() + + if not source: + continue + + request_stream(hass, source, keepalive=True) async_when_setup(hass, DOMAIN_STREAM, preload_stream) @@ -328,8 +339,7 @@ class Camera(Entity): """Return the interval between frames of the mjpeg stream.""" return 0.5 - @property - def stream_source(self): + async def stream_source(self): """Return the source of the stream.""" return None @@ -481,7 +491,7 @@ class CameraImageView(CameraView): async def handle(self, request, camera): """Serve camera image.""" with suppress(asyncio.CancelledError, asyncio.TimeoutError): - with async_timeout.timeout(10): + async with async_timeout.timeout(10): image = await camera.async_camera_image() if image: @@ -547,18 +557,25 @@ async def ws_camera_stream(hass, connection, msg): camera = _get_camera_from_entity_id(hass, entity_id) camera_prefs = hass.data[DATA_CAMERA_PREFS].get(entity_id) - if not camera.stream_source: + async with async_timeout.timeout(10): + source = await camera.stream_source() + + if not source: raise HomeAssistantError("{} does not support play stream service" .format(camera.entity_id)) fmt = msg['format'] - url = request_stream(hass, camera.stream_source, fmt=fmt, + url = request_stream(hass, source, fmt=fmt, keepalive=camera_prefs.preload_stream) connection.send_result(msg['id'], {'url': url}) except HomeAssistantError as ex: - _LOGGER.error(ex) + _LOGGER.error("Error requesting stream: %s", ex) connection.send_error( msg['id'], 'start_stream_failed', str(ex)) + except asyncio.TimeoutError: + _LOGGER.error("Timeout getting stream source") + connection.send_error( + msg['id'], 'start_stream_failed', "Timeout getting stream source") @websocket_api.async_response @@ -622,7 +639,10 @@ async def async_handle_snapshot_service(camera, service): async def async_handle_play_stream_service(camera, service_call): """Handle play stream services calls.""" - if not camera.stream_source: + async with async_timeout.timeout(10): + source = await camera.stream_source() + + if not source: raise HomeAssistantError("{} does not support play stream service" .format(camera.entity_id)) @@ -631,7 +651,7 @@ async def async_handle_play_stream_service(camera, service_call): fmt = service_call.data[ATTR_FORMAT] entity_ids = service_call.data[ATTR_MEDIA_PLAYER] - url = request_stream(hass, camera.stream_source, fmt=fmt, + url = request_stream(hass, source, fmt=fmt, keepalive=camera_prefs.preload_stream) data = { ATTR_ENTITY_ID: entity_ids, @@ -646,7 +666,10 @@ async def async_handle_play_stream_service(camera, service_call): async def async_handle_record_service(camera, call): """Handle stream recording service calls.""" - if not camera.stream_source: + async with async_timeout.timeout(10): + source = await camera.stream_source() + + if not source: raise HomeAssistantError("{} does not support record service" .format(camera.entity_id)) @@ -657,7 +680,7 @@ async def async_handle_record_service(camera, call): variables={ATTR_ENTITY_ID: camera}) data = { - CONF_STREAM_SOURCE: camera.stream_source, + CONF_STREAM_SOURCE: source, CONF_FILENAME: video_path, CONF_DURATION: call.data[CONF_DURATION], CONF_LOOKBACK: call.data[CONF_LOOKBACK], diff --git a/homeassistant/components/doorbird/camera.py b/homeassistant/components/doorbird/camera.py index 6da2cd1447d..b4bd40c442c 100644 --- a/homeassistant/components/doorbird/camera.py +++ b/homeassistant/components/doorbird/camera.py @@ -57,8 +57,7 @@ class DoorBirdCamera(Camera): self._last_update = datetime.datetime.min super().__init__() - @property - def stream_source(self): + async def stream_source(self): """Return the stream source.""" return self._stream_url diff --git a/homeassistant/components/ffmpeg/camera.py b/homeassistant/components/ffmpeg/camera.py index e803155d254..20b4e538085 100644 --- a/homeassistant/components/ffmpeg/camera.py +++ b/homeassistant/components/ffmpeg/camera.py @@ -47,8 +47,7 @@ class FFmpegCamera(Camera): """Return supported features.""" return SUPPORT_STREAM - @property - def stream_source(self): + async def stream_source(self): """Return the stream source.""" return self._input.split(' ')[-1] diff --git a/homeassistant/components/foscam/camera.py b/homeassistant/components/foscam/camera.py index f83c3f1966a..3bb000380d7 100644 --- a/homeassistant/components/foscam/camera.py +++ b/homeassistant/components/foscam/camera.py @@ -77,8 +77,7 @@ class FoscamCam(Camera): return SUPPORT_STREAM return 0 - @property - def stream_source(self): + async def stream_source(self): """Return the stream source.""" if self._rtsp_port: return 'rtsp://{}:{}@{}:{}/videoMain'.format( diff --git a/homeassistant/components/generic/camera.py b/homeassistant/components/generic/camera.py index 7f63a832779..8b98d84c06d 100644 --- a/homeassistant/components/generic/camera.py +++ b/homeassistant/components/generic/camera.py @@ -146,7 +146,6 @@ class GenericCamera(Camera): """Return the name of this device.""" return self._name - @property - def stream_source(self): + async def stream_source(self): """Return the source of the stream.""" return self._stream_source diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index 976e0794938..7a0c1b0e513 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -123,8 +123,7 @@ class NetatmoCamera(Camera): """Return supported features.""" return SUPPORT_STREAM - @property - def stream_source(self): + async def stream_source(self): """Return the stream source.""" url = '{0}/live/files/{1}/index.m3u8' if self._localurl: diff --git a/homeassistant/components/onvif/camera.py b/homeassistant/components/onvif/camera.py index c308ba2c4d2..230aa913791 100644 --- a/homeassistant/components/onvif/camera.py +++ b/homeassistant/components/onvif/camera.py @@ -339,8 +339,7 @@ class ONVIFHassCamera(Camera): return SUPPORT_STREAM return 0 - @property - def stream_source(self): + async def stream_source(self): """Return the stream source.""" return self._input diff --git a/tests/components/camera/test_init.py b/tests/components/camera/test_init.py index e12cca75c61..75ee8f6c665 100644 --- a/tests/components/camera/test_init.py +++ b/tests/components/camera/test_init.py @@ -209,8 +209,7 @@ async def test_websocket_camera_stream(hass, hass_ws_client, return_value='http://home.assistant/playlist.m3u8' ) as mock_request_stream, \ patch('homeassistant.components.demo.camera.DemoCamera.stream_source', - new_callable=PropertyMock) as mock_stream_source: - mock_stream_source.return_value = io.BytesIO() + return_value=mock_coro('http://example.com')): # Request playlist through WebSocket client = await hass_ws_client(hass) await client.send_json({ @@ -289,8 +288,7 @@ async def test_handle_play_stream_service(hass, mock_camera, mock_stream): with patch('homeassistant.components.camera.request_stream' ) as mock_request_stream, \ patch('homeassistant.components.demo.camera.DemoCamera.stream_source', - new_callable=PropertyMock) as mock_stream_source: - mock_stream_source.return_value = io.BytesIO() + return_value=mock_coro('http://example.com')): # Call service await hass.services.async_call( camera.DOMAIN, camera.SERVICE_PLAY_STREAM, data, blocking=True) @@ -331,8 +329,7 @@ async def test_preload_stream(hass, mock_stream): patch('homeassistant.components.camera.prefs.CameraPreferences.get', return_value=demo_prefs), \ patch('homeassistant.components.demo.camera.DemoCamera.stream_source', - new_callable=PropertyMock) as mock_stream_source: - mock_stream_source.return_value = io.BytesIO() + return_value=mock_coro("http://example.com")): await async_setup_component(hass, 'camera', { DOMAIN: { 'platform': 'demo' @@ -364,12 +361,11 @@ async def test_record_service(hass, mock_camera, mock_stream): } with patch('homeassistant.components.demo.camera.DemoCamera.stream_source', - new_callable=PropertyMock) as mock_stream_source, \ + return_value=mock_coro("http://example.com")), \ patch( 'homeassistant.components.stream.async_handle_record_service', return_value=mock_coro()) as mock_record_service, \ patch.object(hass.config, 'is_allowed_path', return_value=True): - mock_stream_source.return_value = io.BytesIO() # Call service await hass.services.async_call( camera.DOMAIN, camera.SERVICE_RECORD, data, blocking=True) From 3d79bf2bfe78b7b925ee26e4ba18e9e59c0fb514 Mon Sep 17 00:00:00 2001 From: Troels Agergaard Jacobsen Date: Thu, 23 May 2019 19:27:42 +0200 Subject: [PATCH 117/232] Fix entity id naming when not using first install (#23606) * Fix entity id naming when not using first install Currently, the verisure component will use the alias of the first installation to decide entity id of the alarm_control_panel even though a different installation is configured through a specified giid. This fixes that * Fixed pulled request review comments * Remove trailing whitespace * Fix remaining pylint errors --- .../components/verisure/alarm_control_panel.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/verisure/alarm_control_panel.py b/homeassistant/components/verisure/alarm_control_panel.py index dc73be056db..53c79098782 100644 --- a/homeassistant/components/verisure/alarm_control_panel.py +++ b/homeassistant/components/verisure/alarm_control_panel.py @@ -6,7 +6,7 @@ import homeassistant.components.alarm_control_panel as alarm from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED) -from . import CONF_ALARM, CONF_CODE_DIGITS, HUB as hub +from . import CONF_ALARM, CONF_CODE_DIGITS, CONF_GIID, HUB as hub _LOGGER = logging.getLogger(__name__) @@ -45,6 +45,14 @@ class VerisureAlarm(alarm.AlarmControlPanel): @property def name(self): """Return the name of the device.""" + giid = hub.config.get(CONF_GIID) + if giid is not None: + aliass = {i['giid']: i['alias'] for i in hub.session.installations} + if giid in aliass.keys(): + return '{} alarm'.format(aliass[giid]) + + _LOGGER.error('Verisure installation giid not found: %s', giid) + return '{} alarm'.format(hub.session.installations[0]['alias']) @property From 958c5ecbfec941e5eed84e2379f0b495386fd545 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 23 May 2019 13:32:16 -0700 Subject: [PATCH 118/232] Updated frontend to 20190523.0 --- homeassistant/components/frontend/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index c2f5e89dc51..1150c70d0d8 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/components/frontend", "requirements": [ - "home-assistant-frontend==20190514.0" + "home-assistant-frontend==20190523.0" ], "dependencies": [ "api", diff --git a/requirements_all.txt b/requirements_all.txt index 25fc72b514a..a70837565fd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -566,7 +566,7 @@ hole==0.3.0 holidays==0.9.10 # homeassistant.components.frontend -home-assistant-frontend==20190514.0 +home-assistant-frontend==20190523.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e56a34c10ed..14c074b46d8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -148,7 +148,7 @@ hdate==0.8.7 holidays==0.9.10 # homeassistant.components.frontend -home-assistant-frontend==20190514.0 +home-assistant-frontend==20190523.0 # homeassistant.components.homekit_controller homekit[IP]==0.14.0 From 206029eadcd36e4255552f564588ab62449bfdb2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 23 May 2019 13:32:35 -0700 Subject: [PATCH 119/232] Update translations --- .../ambiclimate/.translations/es.json | 23 +++++++++++++++++++ .../ambiclimate/.translations/no.json | 23 +++++++++++++++++++ .../ambiclimate/.translations/pl.json | 23 +++++++++++++++++++ .../ambiclimate/.translations/sv.json | 23 +++++++++++++++++++ .../components/axis/.translations/nl.json | 18 +++++++++++++++ .../components/axis/.translations/sv.json | 5 ++-- .../components/deconz/.translations/fr.json | 8 +++++++ .../components/deconz/.translations/sv.json | 7 ++++-- .../emulated_roku/.translations/es.json | 5 +++- .../components/hangouts/.translations/fr.json | 1 + .../components/heos/.translations/nl.json | 13 +++++++++++ .../components/heos/.translations/no.json | 2 +- .../components/heos/.translations/sv.json | 5 +++- .../homekit_controller/.translations/es.json | 6 +++++ .../homekit_controller/.translations/fr.json | 1 + .../homekit_controller/.translations/ko.json | 2 +- .../homekit_controller/.translations/nl.json | 1 + .../homekit_controller/.translations/no.json | 5 ++++ .../homekit_controller/.translations/pl.json | 5 ++++ .../homekit_controller/.translations/sv.json | 10 +++++++- .../.translations/zh-Hans.json | 6 +++++ .../homematicip_cloud/.translations/no.json | 2 +- .../components/iqvia/.translations/ca.json | 18 +++++++++++++++ .../components/iqvia/.translations/de.json | 18 +++++++++++++++ .../components/iqvia/.translations/es.json | 18 +++++++++++++++ .../components/iqvia/.translations/fr.json | 18 +++++++++++++++ .../components/iqvia/.translations/ko.json | 18 +++++++++++++++ .../components/iqvia/.translations/lb.json | 18 +++++++++++++++ .../components/iqvia/.translations/nl.json | 17 ++++++++++++++ .../components/iqvia/.translations/no.json | 18 +++++++++++++++ .../components/iqvia/.translations/pl.json | 18 +++++++++++++++ .../components/iqvia/.translations/ru.json | 18 +++++++++++++++ .../components/iqvia/.translations/sv.json | 18 +++++++++++++++ .../iqvia/.translations/zh-Hans.json | 16 +++++++++++++ .../iqvia/.translations/zh-Hant.json | 18 +++++++++++++++ .../logi_circle/.translations/fr.json | 18 +++++++++++++++ .../logi_circle/.translations/nl.json | 9 ++++++++ .../logi_circle/.translations/pl.json | 2 +- .../logi_circle/.translations/sv.json | 6 ++++- .../luftdaten/.translations/ko.json | 2 +- .../mobile_app/.translations/sv.json | 9 ++++++-- .../moon/.translations/sensor.sv.json | 8 +++---- .../components/nest/.translations/pl.json | 2 +- .../onboarding/.translations/ca.json | 7 ++++++ .../onboarding/.translations/de.json | 7 ++++++ .../onboarding/.translations/es.json | 7 ++++++ .../onboarding/.translations/fr.json | 7 ++++++ .../onboarding/.translations/ko.json | 7 ++++++ .../onboarding/.translations/nl.json | 7 ++++++ .../onboarding/.translations/no.json | 7 ++++++ .../onboarding/.translations/pl.json | 7 ++++++ .../onboarding/.translations/sv.json | 7 ++++++ .../onboarding/.translations/zh-Hans.json | 7 ++++++ .../components/point/.translations/no.json | 2 +- .../components/point/.translations/pl.json | 2 +- .../components/ps4/.translations/es.json | 1 + .../components/ps4/.translations/ko.json | 2 +- .../components/ps4/.translations/nl.json | 7 ++++++ .../components/ps4/.translations/sv.json | 2 ++ 59 files changed, 544 insertions(+), 23 deletions(-) create mode 100644 homeassistant/components/ambiclimate/.translations/es.json create mode 100644 homeassistant/components/ambiclimate/.translations/no.json create mode 100644 homeassistant/components/ambiclimate/.translations/pl.json create mode 100644 homeassistant/components/ambiclimate/.translations/sv.json create mode 100644 homeassistant/components/axis/.translations/nl.json create mode 100644 homeassistant/components/heos/.translations/nl.json create mode 100644 homeassistant/components/iqvia/.translations/ca.json create mode 100644 homeassistant/components/iqvia/.translations/de.json create mode 100644 homeassistant/components/iqvia/.translations/es.json create mode 100644 homeassistant/components/iqvia/.translations/fr.json create mode 100644 homeassistant/components/iqvia/.translations/ko.json create mode 100644 homeassistant/components/iqvia/.translations/lb.json create mode 100644 homeassistant/components/iqvia/.translations/nl.json create mode 100644 homeassistant/components/iqvia/.translations/no.json create mode 100644 homeassistant/components/iqvia/.translations/pl.json create mode 100644 homeassistant/components/iqvia/.translations/ru.json create mode 100644 homeassistant/components/iqvia/.translations/sv.json create mode 100644 homeassistant/components/iqvia/.translations/zh-Hans.json create mode 100644 homeassistant/components/iqvia/.translations/zh-Hant.json create mode 100644 homeassistant/components/logi_circle/.translations/fr.json create mode 100644 homeassistant/components/onboarding/.translations/ca.json create mode 100644 homeassistant/components/onboarding/.translations/de.json create mode 100644 homeassistant/components/onboarding/.translations/es.json create mode 100644 homeassistant/components/onboarding/.translations/fr.json create mode 100644 homeassistant/components/onboarding/.translations/ko.json create mode 100644 homeassistant/components/onboarding/.translations/nl.json create mode 100644 homeassistant/components/onboarding/.translations/no.json create mode 100644 homeassistant/components/onboarding/.translations/pl.json create mode 100644 homeassistant/components/onboarding/.translations/sv.json create mode 100644 homeassistant/components/onboarding/.translations/zh-Hans.json diff --git a/homeassistant/components/ambiclimate/.translations/es.json b/homeassistant/components/ambiclimate/.translations/es.json new file mode 100644 index 00000000000..6447926f64e --- /dev/null +++ b/homeassistant/components/ambiclimate/.translations/es.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "access_token": "Error desconocido al generar un token de acceso.", + "already_setup": "La cuenta de Ambiclimate est\u00e1 configurada.", + "no_config": "Es necesario configurar Ambiclimate antes de poder autenticarse con \u00e9l. [Por favor, lee las instrucciones](https://www.home-assistant.io/components/ambiclimate/)." + }, + "create_entry": { + "default": "Autenticado correctamente con Ambiclimate" + }, + "error": { + "follow_link": "Accede al enlace e identif\u00edcate antes de pulsar Enviar.", + "no_token": "No autenticado con Ambiclimate" + }, + "step": { + "auth": { + "description": "Accede al siguiente [enlace]({authorization_url}) y permite el acceso a tu cuenta de Ambiclimate, despu\u00e9s vuelve y pulsa en enviar a continuaci\u00f3n.\n(Aseg\u00farate que la url de devoluci\u00f3n de llamada es {cb_url})", + "title": "Autenticaci\u00f3n de Ambiclimate" + } + }, + "title": "Ambiclimate" + } +} \ No newline at end of file diff --git a/homeassistant/components/ambiclimate/.translations/no.json b/homeassistant/components/ambiclimate/.translations/no.json new file mode 100644 index 00000000000..567d0b95ff3 --- /dev/null +++ b/homeassistant/components/ambiclimate/.translations/no.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "access_token": "Ukjent feil ved oppretting av tilgangstoken.", + "already_setup": "Ambiclimate-kontoen er konfigurert.", + "no_config": "Du m\u00e5 konfigurere Ambiclimate f\u00f8r du kan autentisere med den. [Vennligst les instruksjonene](https://www.home-assistant.io/components/ambiclimate/)." + }, + "create_entry": { + "default": "Vellykket autentisering med Ambiclimate" + }, + "error": { + "follow_link": "Vennligst f\u00f8lg lenken og godkjen f\u00f8r du trykker p\u00e5 Send", + "no_token": "Ikke autentisert med Ambiclimate" + }, + "step": { + "auth": { + "description": "Vennligst f\u00f8lg denne [linken]({authorization_url}) og Tillat tilgang til din Ambiclimate konto, og kom s\u00e5 tilbake og trykk Send nedenfor.\n(Kontroller at den angitte URL-adressen for tilbakeringing er {cb_url})", + "title": "Autensiere Ambiclimate" + } + }, + "title": "Ambiclimate" + } +} \ No newline at end of file diff --git a/homeassistant/components/ambiclimate/.translations/pl.json b/homeassistant/components/ambiclimate/.translations/pl.json new file mode 100644 index 00000000000..dac6e52dda2 --- /dev/null +++ b/homeassistant/components/ambiclimate/.translations/pl.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "access_token": "Nieznany b\u0142\u0105d podczas generowania tokena dost\u0119pu.", + "already_setup": "Konto Ambiclimate jest skonfigurowane.", + "no_config": "Musisz skonfigurowa\u0107 Ambiclimate, zanim b\u0119dziesz m\u00f3g\u0142 si\u0119 z nim uwierzytelni\u0107. [Przeczytaj instrukcj\u0119](https://www.home-assistant.io/components/ambiclimate/)." + }, + "create_entry": { + "default": "Pomy\u015blnie uwierzytelniono z Ambiclimate" + }, + "error": { + "follow_link": "Prosz\u0119 klikn\u0105\u0107 link i uwierzytelni\u0107 przed naci\u015bni\u0119ciem przycisku Prze\u015blij", + "no_token": "Nie uwierzytelniony z Ambiclimate" + }, + "step": { + "auth": { + "description": "Kliknij poni\u017cszy [link]({authorization_url}) i Zezw\u00f3l na dost\u0119p do swojego konta Ambiclimate, a nast\u0119pnie wr\u00f3\u0107 i naci\u015bnij Prze\u015blij poni\u017cej. \n(Upewnij si\u0119, \u017ce podany adres URL to {cb_url})", + "title": "Uwierzytelnienie Ambiclimate" + } + }, + "title": "Ambiclimate" + } +} \ No newline at end of file diff --git a/homeassistant/components/ambiclimate/.translations/sv.json b/homeassistant/components/ambiclimate/.translations/sv.json new file mode 100644 index 00000000000..f52bb6697f9 --- /dev/null +++ b/homeassistant/components/ambiclimate/.translations/sv.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "access_token": "Ok\u00e4nt fel vid generering av \u00e5tkomsttoken.", + "already_setup": "Ambiclientkontot \u00e4r konfigurerat", + "no_config": "Du m\u00e5ste konfigurera Ambiclimate innan du kan autentisera med den. [V\u00e4nligen l\u00e4s instruktionerna] (https://www.home-assistant.io/components/ambiclimate/)." + }, + "create_entry": { + "default": "Lyckad autentisering med Ambiclimate" + }, + "error": { + "follow_link": "V\u00e4nligen f\u00f6lj l\u00e4nken och autentisera dig innan du trycker p\u00e5 Skicka", + "no_token": "Inte autentiserad med Ambiclimate" + }, + "step": { + "auth": { + "description": "V\u00e4nligen f\u00f6lj denna [l\u00e4nk] ({authorization_url}) och till\u00e5ta till g\u00e5ng till ditt Ambiclimate konto, kom sedan tillbaka och tryck p\u00e5 Skicka nedan.\n(Kontrollera att den angivna callback url \u00e4r {cb_url})", + "title": "Autentisera Ambiclimate" + } + }, + "title": "Ambiclimate" + } +} \ No newline at end of file diff --git a/homeassistant/components/axis/.translations/nl.json b/homeassistant/components/axis/.translations/nl.json new file mode 100644 index 00000000000..e46f35aa1f9 --- /dev/null +++ b/homeassistant/components/axis/.translations/nl.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "device_unavailable": "Apparaat is niet beschikbaar", + "faulty_credentials": "Ongeldige gebruikersreferenties" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Wachtwoord", + "port": "Poort", + "username": "Gebruikersnaam" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/axis/.translations/sv.json b/homeassistant/components/axis/.translations/sv.json index 435a56632e8..2f75a9dcfff 100644 --- a/homeassistant/components/axis/.translations/sv.json +++ b/homeassistant/components/axis/.translations/sv.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "Enheten \u00e4r redan konfigurerad", - "bad_config_file": "Felaktig data fr\u00e5n config fil" + "bad_config_file": "Felaktig data fr\u00e5n config fil", + "link_local_address": "Link local addresses are not supported" }, "error": { "already_configured": "Enheten \u00e4r redan konfigurerad", @@ -17,7 +18,7 @@ "port": "Port", "username": "Anv\u00e4ndarnamn" }, - "title": "Konfigurera Axis enhet" + "title": "Konfigurera Axis-enhet" } }, "title": "Axis enhet" diff --git a/homeassistant/components/deconz/.translations/fr.json b/homeassistant/components/deconz/.translations/fr.json index d18df13701e..23036c19aec 100644 --- a/homeassistant/components/deconz/.translations/fr.json +++ b/homeassistant/components/deconz/.translations/fr.json @@ -9,6 +9,14 @@ "no_key": "Impossible d'obtenir une cl\u00e9 d'API" }, "step": { + "hassio_confirm": { + "data": { + "allow_clip_sensor": "Autoriser l'importation de capteurs virtuels", + "allow_deconz_groups": "Autoriser l'importation des groupes deCONZ" + }, + "description": "Voulez-vous configurer Home Assistant pour qu'il se connecte \u00e0 la passerelle deCONZ fournie par l'add-on hass.io {addon} ?", + "title": "Passerelle deCONZ Zigbee via l'add-on Hass.io" + }, "init": { "data": { "host": "H\u00f4te", diff --git a/homeassistant/components/deconz/.translations/sv.json b/homeassistant/components/deconz/.translations/sv.json index a5efd2a36d9..17367c49f5b 100644 --- a/homeassistant/components/deconz/.translations/sv.json +++ b/homeassistant/components/deconz/.translations/sv.json @@ -3,7 +3,8 @@ "abort": { "already_configured": "Bryggan \u00e4r redan konfigurerad", "no_bridges": "Inga deCONZ-bryggor uppt\u00e4cktes", - "one_instance_only": "Komponenten st\u00f6djer endast en deCONZ-instans" + "one_instance_only": "Komponenten st\u00f6djer endast en deCONZ-instans", + "updated_instance": "Uppdaterad deCONZ-instans med ny v\u00e4rdadress" }, "error": { "no_key": "Det gick inte att ta emot en API-nyckel" @@ -11,8 +12,10 @@ "step": { "hassio_confirm": { "data": { - "allow_clip_sensor": "Till\u00e5t import av virtuella sensorer" + "allow_clip_sensor": "Till\u00e5t import av virtuella sensorer", + "allow_deconz_groups": "Till\u00e5t import av deCONZ-grupper" }, + "description": "Vill du konfigurera Home Assistant f\u00f6r att ansluta till deCONZ gateway som tillhandah\u00e5lls av hass.io till\u00e4gg {addon}?", "title": "deCONZ Zigbee gateway via Hass.io till\u00e4gg" }, "init": { diff --git a/homeassistant/components/emulated_roku/.translations/es.json b/homeassistant/components/emulated_roku/.translations/es.json index a4c8503b3f3..f727c8bf522 100644 --- a/homeassistant/components/emulated_roku/.translations/es.json +++ b/homeassistant/components/emulated_roku/.translations/es.json @@ -6,9 +6,12 @@ "step": { "user": { "data": { + "advertise_ip": "IP para anunciar", + "advertise_port": "Puerto para anunciar", "host_ip": "IP del host", "listen_port": "Puerto de escucha", - "name": "Nombre" + "name": "Nombre", + "upnp_bind_multicast": "Enlazar multicast (verdadero/falso)" }, "title": "Definir la configuraci\u00f3n del servidor" } diff --git a/homeassistant/components/hangouts/.translations/fr.json b/homeassistant/components/hangouts/.translations/fr.json index 00a7d5fd80d..0b6dbfcbe44 100644 --- a/homeassistant/components/hangouts/.translations/fr.json +++ b/homeassistant/components/hangouts/.translations/fr.json @@ -18,6 +18,7 @@ }, "user": { "data": { + "authorization_code": "Code d'autorisation (requis pour l'authentification manuelle)", "email": "Adresse e-mail", "password": "Mot de passe" }, diff --git a/homeassistant/components/heos/.translations/nl.json b/homeassistant/components/heos/.translations/nl.json new file mode 100644 index 00000000000..d3c91af2c16 --- /dev/null +++ b/homeassistant/components/heos/.translations/nl.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "access_token": "Host" + }, + "title": "Verbinding maken met Heos" + } + }, + "title": "HEOS" + } +} \ No newline at end of file diff --git a/homeassistant/components/heos/.translations/no.json b/homeassistant/components/heos/.translations/no.json index 144b08c0663..dd4cb48a090 100644 --- a/homeassistant/components/heos/.translations/no.json +++ b/homeassistant/components/heos/.translations/no.json @@ -16,6 +16,6 @@ "title": "Koble til Heos" } }, - "title": "Heos" + "title": "HEOS" } } \ No newline at end of file diff --git a/homeassistant/components/heos/.translations/sv.json b/homeassistant/components/heos/.translations/sv.json index d36ad203438..96d4991a5b8 100644 --- a/homeassistant/components/heos/.translations/sv.json +++ b/homeassistant/components/heos/.translations/sv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_setup": "Du kan bara konfigurera en enda Heos-anslutning eftersom den kommer att st\u00f6dja alla enheter i n\u00e4tverket." + }, "error": { "connection_failure": "Det gick inte att ansluta till den angivna v\u00e4rden." }, @@ -13,6 +16,6 @@ "title": "Anslut till Heos" } }, - "title": "Heos" + "title": "HEOS" } } \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/.translations/es.json b/homeassistant/components/homekit_controller/.translations/es.json index f22b4158698..642e76fd1dd 100644 --- a/homeassistant/components/homekit_controller/.translations/es.json +++ b/homeassistant/components/homekit_controller/.translations/es.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "accessory_not_found_error": "No se puede a\u00f1adir el emparejamiento porque ya no se puede encontrar el dispositivo.", "already_configured": "El accesorio ya est\u00e1 configurado con este controlador.", "already_paired": "Este accesorio ya est\u00e1 emparejado con otro dispositivo. Por favor, reinicia el accesorio e int\u00e9ntalo de nuevo.", "ignored_model": "El soporte de HomeKit para este modelo est\u00e1 bloqueado ya que est\u00e1 disponible una integraci\u00f3n nativa m\u00e1s completa.", @@ -9,9 +10,14 @@ }, "error": { "authentication_error": "C\u00f3digo HomeKit incorrecto. Por favor, compru\u00e9belo e int\u00e9ntelo de nuevo.", + "busy_error": "El dispositivo rechaz\u00f3 el emparejamiento porque ya est\u00e1 emparejado con otro controlador.", + "max_peers_error": "El dispositivo rechaz\u00f3 el emparejamiento ya que no tiene almacenamiento de emparejamientos libres.", + "max_tries_error": "El dispositivo rechaz\u00f3 el emparejamiento ya que ha recibido m\u00e1s de 100 intentos de autenticaci\u00f3n fallidos.", + "pairing_failed": "Se ha producido un error no controlado al intentar emparejarse con este dispositivo. Esto puede ser un fallo temporal o que tu dispositivo no est\u00e9 admitido en este momento.", "unable_to_pair": "No se ha podido emparejar, por favor int\u00e9ntelo de nuevo.", "unknown_error": "El dispositivo report\u00f3 un error desconocido. La vinculaci\u00f3n ha fallado." }, + "flow_title": "Accesorio HomeKit: {name}", "step": { "pair": { "data": { diff --git a/homeassistant/components/homekit_controller/.translations/fr.json b/homeassistant/components/homekit_controller/.translations/fr.json index 73cbbdf046a..5e1bea42bdc 100644 --- a/homeassistant/components/homekit_controller/.translations/fr.json +++ b/homeassistant/components/homekit_controller/.translations/fr.json @@ -9,6 +9,7 @@ }, "error": { "authentication_error": "Code HomeKit incorrect. S'il vous pla\u00eet v\u00e9rifier et essayez \u00e0 nouveau.", + "pairing_failed": "Une erreur non g\u00e9r\u00e9e s'est produite lors de la tentative d'appairage avec cet appareil. Il se peut qu'il s'agisse d'une panne temporaire ou que votre appareil ne soit pas pris en charge actuellement.", "unable_to_pair": "Impossible d'appairer, veuillez r\u00e9essayer.", "unknown_error": "L'appareil a signal\u00e9 une erreur inconnue. L'appairage a \u00e9chou\u00e9." }, diff --git a/homeassistant/components/homekit_controller/.translations/ko.json b/homeassistant/components/homekit_controller/.translations/ko.json index c780f07e96e..5ee62ad62b4 100644 --- a/homeassistant/components/homekit_controller/.translations/ko.json +++ b/homeassistant/components/homekit_controller/.translations/ko.json @@ -13,7 +13,7 @@ "busy_error": "\uae30\uae30\uac00 \uc774\ubbf8 \ub2e4\ub978 \ucee8\ud2b8\ub864\ub7ec\uc640 \ud398\uc5b4\ub9c1 \uc911\uc774\ubbc0\ub85c \ud398\uc5b4\ub9c1 \ucd94\uac00\ub97c \ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.", "max_peers_error": "\uae30\uae30\uc5d0 \ube44\uc5b4\uc788\ub294 \ud398\uc5b4\ub9c1 \uc7a5\uc18c\uac00 \uc5c6\uc5b4 \ud398\uc5b4\ub9c1 \ucd94\uac00\ub97c \ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.", "max_tries_error": "\uae30\uae30\uac00 \uc2e4\ud328\ud55c \uc778\uc99d \uc2dc\ub3c4 \ud69f\uc218\uac00 100 \ud68c\ub97c \ucd08\uacfc\ud558\uc5ec \ud398\uc5b4\ub9c1\uc744 \ucd94\uac00\ub97c \ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.", - "pairing_failed": "\uc774 \uae30\uae30\uc640 \ud398\uc5b4\ub9c1\uc744 \uc2dc\ub3c4\ud558\ub294 \uc911 \ucc98\ub9ac\ub418\uc9c0 \uc54a\uc740 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4. \uc77c\uc2dc\uc801\uc778 \uc624\ub958\uc774\uac70\ub098 \ud604\uc7ac \uc9c0\uc6d0\ub418\uc9c0 \uc54a\ub294 \uc7a5\uce58 \uc77c \uc218 \uc788\uc2b5\ub2c8\ub2e4.", + "pairing_failed": "\uc774 \uae30\uae30\uc640 \ud398\uc5b4\ub9c1\uc744 \uc2dc\ub3c4\ud558\ub294 \uc911 \ucc98\ub9ac\ub418\uc9c0 \uc54a\uc740 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4. \uc77c\uc2dc\uc801\uc778 \uc624\ub958\uc774\uac70\ub098 \ud604\uc7ac \uc9c0\uc6d0\ub418\uc9c0 \uc54a\ub294 \uae30\uae30 \uc77c \uc218 \uc788\uc2b5\ub2c8\ub2e4.", "unable_to_pair": "\ud398\uc5b4\ub9c1 \ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", "unknown_error": "\uae30\uae30\uc5d0\uc11c \uc54c \uc218\uc5c6\ub294 \uc624\ub958\ub97c \ubcf4\uace0\ud588\uc2b5\ub2c8\ub2e4. \ud398\uc5b4\ub9c1\uc5d0 \uc2e4\ud328\ud588\uc2b5\ub2c8\ub2e4." }, diff --git a/homeassistant/components/homekit_controller/.translations/nl.json b/homeassistant/components/homekit_controller/.translations/nl.json index 30380344d9b..a714934372b 100644 --- a/homeassistant/components/homekit_controller/.translations/nl.json +++ b/homeassistant/components/homekit_controller/.translations/nl.json @@ -6,6 +6,7 @@ }, "error": { "authentication_error": "Onjuiste HomeKit-code. Controleer het en probeer het opnieuw.", + "pairing_failed": "Er deed zich een fout voor tijdens het koppelen met dit apparaat. Dit kan een tijdelijke storing zijn of uw apparaat wordt mogelijk momenteel niet ondersteund.", "unable_to_pair": "Kan niet koppelen, probeer het opnieuw.", "unknown_error": "Apparaat meldde een onbekende fout. Koppelen mislukt." }, diff --git a/homeassistant/components/homekit_controller/.translations/no.json b/homeassistant/components/homekit_controller/.translations/no.json index 555faef1061..e7ec6c279fa 100644 --- a/homeassistant/components/homekit_controller/.translations/no.json +++ b/homeassistant/components/homekit_controller/.translations/no.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "accessory_not_found_error": "Kan ikke legge til sammenkobling da enheten ikke lenger kan bli funnet.", "already_configured": "Tilbeh\u00f8r er allerede konfigurert med denne kontrolleren.", "already_paired": "Dette tilbeh\u00f8ret er allerede sammenkoblet med en annen enhet. Vennligst tilbakestill tilbeh\u00f8ret og pr\u00f8v igjen.", "ignored_model": "HomeKit st\u00f8tte for denne modellen er blokkert da en mer funksjonsrik standard integrering er tilgjengelig.", @@ -9,6 +10,10 @@ }, "error": { "authentication_error": "Ugyldig HomeKit kode. Vennligst sjekk den og pr\u00f8v igjen.", + "busy_error": "Enheten nekter \u00e5 sammenkoble da den allerede er sammenkoblet med en annen kontroller.", + "max_peers_error": "Enheten nekter \u00e5 sammenkoble da den ikke har ledig sammenkoblingslagring.", + "max_tries_error": "Enheten nekter \u00e5 sammenkoble da den har mottatt mer enn 100 mislykkede godkjenningsfors\u00f8k.", + "pairing_failed": "En uh\u00e5ndtert feil oppstod under fors\u00f8k p\u00e5 \u00e5 koble til denne enheten. Dette kan v\u00e6re en midlertidig feil, eller at enheten din kan ikke st\u00f8ttes for \u00f8yeblikket.", "unable_to_pair": "Kunne ikke koble til, vennligst pr\u00f8v igjen.", "unknown_error": "Enheten rapporterte en ukjent feil. Sammenkobling mislyktes." }, diff --git a/homeassistant/components/homekit_controller/.translations/pl.json b/homeassistant/components/homekit_controller/.translations/pl.json index acbc6ee81f7..a0489aa083a 100644 --- a/homeassistant/components/homekit_controller/.translations/pl.json +++ b/homeassistant/components/homekit_controller/.translations/pl.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "accessory_not_found_error": "Nie mo\u017cna rozpocz\u0105\u0107 parowania, poniewa\u017c nie znaleziono urz\u0105dzenia.", "already_configured": "Akcesorium jest ju\u017c skonfigurowane z tym kontrolerem.", "already_paired": "To akcesorium jest ju\u017c sparowane z innym urz\u0105dzeniem. Zresetuj akcesorium i spr\u00f3buj ponownie.", "ignored_model": "Obs\u0142uga HomeKit dla tego modelu jest zablokowana, poniewa\u017c dost\u0119pna jest pe\u0142niejsza integracja natywna.", @@ -9,6 +10,10 @@ }, "error": { "authentication_error": "Niepoprawny kod parowania HomeKit. Sprawd\u017a go i spr\u00f3buj ponownie.", + "busy_error": "Urz\u0105dzenie odm\u00f3wi\u0142o parowania, poniewa\u017c jest ju\u017c powi\u0105zane z innym kontrolerem.", + "max_peers_error": "Urz\u0105dzenie odm\u00f3wi\u0142o parowania, poniewa\u017c nie ma wolnej pami\u0119ci parowania.", + "max_tries_error": "Urz\u0105dzenie odm\u00f3wi\u0142o parowania, poniewa\u017c otrzyma\u0142o ponad 100 nieudanych pr\u00f3b uwierzytelnienia.", + "pairing_failed": "Wyst\u0105pi\u0142 nieobs\u0142ugiwany b\u0142\u0105d podczas pr\u00f3by sparowania z tym urz\u0105dzeniem. Mo\u017ce to by\u0107 tymczasowa awaria lub Twoje urz\u0105dzenie mo\u017ce nie by\u0107 obecnie obs\u0142ugiwane.", "unable_to_pair": "Nie mo\u017cna sparowa\u0107, spr\u00f3buj ponownie.", "unknown_error": "Urz\u0105dzenie zg\u0142osi\u0142o nieznany b\u0142\u0105d. Parowanie nie powiod\u0142o si\u0119." }, diff --git a/homeassistant/components/homekit_controller/.translations/sv.json b/homeassistant/components/homekit_controller/.translations/sv.json index 32372840031..264fca2de50 100644 --- a/homeassistant/components/homekit_controller/.translations/sv.json +++ b/homeassistant/components/homekit_controller/.translations/sv.json @@ -1,11 +1,19 @@ { "config": { "abort": { + "accessory_not_found_error": "Kan inte genomf\u00f6ra parningsf\u00f6rs\u00f6ket eftersom enheten inte l\u00e4ngre kan hittas.", + "already_configured": "Tillbeh\u00f6ret \u00e4r redan konfigurerat med denna kontroller.", "already_paired": "Det h\u00e4r tillbeh\u00f6ret \u00e4r redan kopplat till en annan enhet. \u00c5terst\u00e4ll tillbeh\u00f6ret och f\u00f6rs\u00f6k igen.", + "ignored_model": "HomeKit-st\u00f6d f\u00f6r den h\u00e4r modellen blockeras eftersom en mer komplett inbyggd integration \u00e4r tillg\u00e4nglig.", + "invalid_config_entry": "Den h\u00e4r enheten visas som redo att paras ihop, men det finns redan en motstridig konfigurations-post f\u00f6r den i Home Assistant som f\u00f6rst m\u00e5ste tas bort.", "no_devices": "Inga oparade enheter kunde hittas" }, "error": { "authentication_error": "Felaktig HomeKit-kod. V\u00e4nligen kontrollera och f\u00f6rs\u00f6k igen.", + "busy_error": "Enheten nekade parning d\u00e5 den redan \u00e4r parad med annan controller.", + "max_peers_error": "Enheten nekade parningsf\u00f6rs\u00f6ket d\u00e5 det inte finns n\u00e5got parningsminnesutrymme kvar", + "max_tries_error": "Enheten nekade parningen d\u00e5 den har emottagit mer \u00e4n 100 misslyckade autentiseringsf\u00f6rs\u00f6k", + "pairing_failed": "Ett ok\u00e4nt fel uppstod n\u00e4r parningsf\u00f6rs\u00f6ket gjordes med den h\u00e4r enheten. Det h\u00e4r kan vara ett tillf\u00e4lligt fel, eller s\u00e5 st\u00f6ds inte din enhet i nul\u00e4get.", "unable_to_pair": "Det g\u00e5r inte att para ihop, f\u00f6rs\u00f6k igen.", "unknown_error": "Enheten rapporterade ett ok\u00e4nt fel. Parning misslyckades." }, @@ -15,7 +23,7 @@ "data": { "pairing_code": "Parningskod" }, - "description": "Ange din HomeKit-parningskod f\u00f6r att anv\u00e4nda det h\u00e4r tillbeh\u00f6ret", + "description": "Ange din HomeKit-parningskod (i formatet XXX-XX-XXX) f\u00f6r att anv\u00e4nda det h\u00e4r tillbeh\u00f6ret", "title": "Para HomeKit-tillbeh\u00f6r" }, "user": { diff --git a/homeassistant/components/homekit_controller/.translations/zh-Hans.json b/homeassistant/components/homekit_controller/.translations/zh-Hans.json index d8c7ba8c4da..aae5b68ceb2 100644 --- a/homeassistant/components/homekit_controller/.translations/zh-Hans.json +++ b/homeassistant/components/homekit_controller/.translations/zh-Hans.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "accessory_not_found_error": "\u65e0\u6cd5\u6dfb\u52a0\u914d\u5bf9\uff0c\u56e0\u4e3a\u65e0\u6cd5\u518d\u627e\u5230\u8bbe\u5907\u3002", "already_configured": "\u914d\u4ef6\u5df2\u901a\u8fc7\u6b64\u63a7\u5236\u5668\u914d\u7f6e\u5b8c\u6210\u3002", "already_paired": "\u6b64\u914d\u4ef6\u5df2\u4e0e\u53e6\u4e00\u53f0\u8bbe\u5907\u914d\u5bf9\u3002\u8bf7\u91cd\u7f6e\u914d\u4ef6\uff0c\u7136\u540e\u91cd\u8bd5\u3002", "ignored_model": "HomeKit \u5bf9\u6b64\u8bbe\u5907\u7684\u652f\u6301\u5df2\u88ab\u963b\u6b62\uff0c\u56e0\u4e3a\u6709\u529f\u80fd\u66f4\u5b8c\u6574\u7684\u539f\u751f\u96c6\u6210\u53ef\u4ee5\u4f7f\u7528\u3002", @@ -9,9 +10,14 @@ }, "error": { "authentication_error": "HomeKit \u4ee3\u7801\u4e0d\u6b63\u786e\u3002\u8bf7\u68c0\u67e5\u540e\u91cd\u8bd5\u3002", + "busy_error": "\u8bbe\u5907\u62d2\u7edd\u914d\u5bf9\uff0c\u56e0\u4e3a\u5b83\u5df2\u7ecf\u4e0e\u53e6\u4e00\u4e2a\u63a7\u5236\u5668\u914d\u5bf9\u3002", + "max_peers_error": "\u8bbe\u5907\u62d2\u7edd\u914d\u5bf9\uff0c\u56e0\u4e3a\u5b83\u6ca1\u6709\u7a7a\u95f2\u7684\u914d\u5bf9\u5b58\u50a8\u7a7a\u95f4\u3002", + "max_tries_error": "\u8bbe\u5907\u62d2\u7edd\u914d\u5bf9\uff0c\u56e0\u4e3a\u5b83\u5df2\u6536\u5230\u8d85\u8fc7 100 \u6b21\u5931\u8d25\u7684\u8eab\u4efd\u8ba4\u8bc1\u3002", + "pairing_failed": "\u5c1d\u8bd5\u4e0e\u6b64\u8bbe\u5907\u914d\u5bf9\u65f6\u53d1\u751f\u672a\u5904\u7406\u7684\u9519\u8bef\u3002\u8fd9\u53ef\u80fd\u662f\u6682\u65f6\u6027\u6545\u969c\uff0c\u4e5f\u53ef\u80fd\u662f\u60a8\u7684\u8bbe\u5907\u76ee\u524d\u4e0d\u88ab\u652f\u6301\u3002", "unable_to_pair": "\u65e0\u6cd5\u914d\u5bf9\uff0c\u8bf7\u518d\u8bd5\u4e00\u6b21\u3002", "unknown_error": "\u8bbe\u5907\u62a5\u544a\u4e86\u672a\u77e5\u9519\u8bef\u3002\u914d\u5bf9\u5931\u8d25\u3002" }, + "flow_title": "HomeKit \u914d\u4ef6", "step": { "pair": { "data": { diff --git a/homeassistant/components/homematicip_cloud/.translations/no.json b/homeassistant/components/homematicip_cloud/.translations/no.json index 28cfc502aba..9a4dd424bee 100644 --- a/homeassistant/components/homematicip_cloud/.translations/no.json +++ b/homeassistant/components/homematicip_cloud/.translations/no.json @@ -14,7 +14,7 @@ "step": { "init": { "data": { - "hapid": "Tilgangspunkt ID (SGTIN)", + "hapid": "Tilgangspunkt-ID (SGTIN)", "name": "Navn (valgfritt, brukes som prefiks for alle enheter)", "pin": "PIN kode (valgfritt)" }, diff --git a/homeassistant/components/iqvia/.translations/ca.json b/homeassistant/components/iqvia/.translations/ca.json new file mode 100644 index 00000000000..249fd6d0ae2 --- /dev/null +++ b/homeassistant/components/iqvia/.translations/ca.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "identifier_exists": "Codi postal ja registrat", + "invalid_zip_code": "Codi postal incorrecte" + }, + "step": { + "user": { + "data": { + "zip_code": "Codi postal" + }, + "description": "Introdueix el teu codi postal d'Estats Units o Canad\u00e0.", + "title": "IQVIA" + } + }, + "title": "IQVIA" + } +} \ No newline at end of file diff --git a/homeassistant/components/iqvia/.translations/de.json b/homeassistant/components/iqvia/.translations/de.json new file mode 100644 index 00000000000..3a66a1e11a0 --- /dev/null +++ b/homeassistant/components/iqvia/.translations/de.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "identifier_exists": "Postleitzahl bereits registriert", + "invalid_zip_code": "Postleitzahl ist ung\u00fcltig" + }, + "step": { + "user": { + "data": { + "zip_code": "Postleitzahl" + }, + "description": "Trage eine US-amerikanische oder kanadische Postleitzahl ein.", + "title": "IQVIA" + } + }, + "title": "IQVIA" + } +} \ No newline at end of file diff --git a/homeassistant/components/iqvia/.translations/es.json b/homeassistant/components/iqvia/.translations/es.json new file mode 100644 index 00000000000..91e34e82903 --- /dev/null +++ b/homeassistant/components/iqvia/.translations/es.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "identifier_exists": "C\u00f3digo postal ya registrado", + "invalid_zip_code": "El c\u00f3digo postal no es v\u00e1lido" + }, + "step": { + "user": { + "data": { + "zip_code": "C\u00f3digo postal" + }, + "description": "Indica tu c\u00f3digo postal de Estados Unidos o Canad\u00e1.", + "title": "IQVIA" + } + }, + "title": "IQVIA" + } +} \ No newline at end of file diff --git a/homeassistant/components/iqvia/.translations/fr.json b/homeassistant/components/iqvia/.translations/fr.json new file mode 100644 index 00000000000..f5e5907f2c4 --- /dev/null +++ b/homeassistant/components/iqvia/.translations/fr.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "identifier_exists": "Code postal d\u00e9j\u00e0 enregistr\u00e9", + "invalid_zip_code": "Code postal invalide" + }, + "step": { + "user": { + "data": { + "zip_code": "Code postal" + }, + "description": "Entrez votre code postal am\u00e9ricain ou canadien.", + "title": "IQVIA" + } + }, + "title": "IQVIA" + } +} \ No newline at end of file diff --git a/homeassistant/components/iqvia/.translations/ko.json b/homeassistant/components/iqvia/.translations/ko.json new file mode 100644 index 00000000000..a163891c042 --- /dev/null +++ b/homeassistant/components/iqvia/.translations/ko.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "identifier_exists": "\uc6b0\ud3b8\ubc88\ud638\uac00 \uc774\ubbf8 \ub4f1\ub85d\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "invalid_zip_code": "\uc6b0\ud3b8\ubc88\ud638\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "zip_code": "\uc6b0\ud3b8\ubc88\ud638" + }, + "description": "\ubbf8\uad6d \ub610\ub294 \uce90\ub098\ub2e4\uc758 \uc6b0\ud3b8\ubc88\ud638\ub97c \uae30\uc785\ud574\uc8fc\uc138\uc694.", + "title": "IQVIA" + } + }, + "title": "IQVIA" + } +} \ No newline at end of file diff --git a/homeassistant/components/iqvia/.translations/lb.json b/homeassistant/components/iqvia/.translations/lb.json new file mode 100644 index 00000000000..8dc7c3bc20e --- /dev/null +++ b/homeassistant/components/iqvia/.translations/lb.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "identifier_exists": "Postleitzuel ass scho registr\u00e9iert", + "invalid_zip_code": "Postleitzuel ass ong\u00eblteg" + }, + "step": { + "user": { + "data": { + "zip_code": "Postleitzuel" + }, + "description": "Gitt \u00e4r U.S. oder Kanadesch Postleitzuel un.", + "title": "IQVIA" + } + }, + "title": "IQVIA" + } +} \ No newline at end of file diff --git a/homeassistant/components/iqvia/.translations/nl.json b/homeassistant/components/iqvia/.translations/nl.json new file mode 100644 index 00000000000..dccb7348a01 --- /dev/null +++ b/homeassistant/components/iqvia/.translations/nl.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "identifier_exists": "Postcode reeds geregistreerd", + "invalid_zip_code": "Postcode is ongeldig" + }, + "step": { + "user": { + "data": { + "zip_code": "Postcode" + }, + "title": "IQVIA" + } + }, + "title": "IQVIA" + } +} \ No newline at end of file diff --git a/homeassistant/components/iqvia/.translations/no.json b/homeassistant/components/iqvia/.translations/no.json new file mode 100644 index 00000000000..f04caf5bc8b --- /dev/null +++ b/homeassistant/components/iqvia/.translations/no.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "identifier_exists": "Postnummer er allerede registrert", + "invalid_zip_code": "Postnummeret er ugyldig" + }, + "step": { + "user": { + "data": { + "zip_code": "Postnummer" + }, + "description": "Fyll ut ditt amerikanske eller kanadiske postnummer.", + "title": "IQVIA" + } + }, + "title": "IQVIA" + } +} \ No newline at end of file diff --git a/homeassistant/components/iqvia/.translations/pl.json b/homeassistant/components/iqvia/.translations/pl.json new file mode 100644 index 00000000000..7a6e9a8a915 --- /dev/null +++ b/homeassistant/components/iqvia/.translations/pl.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "identifier_exists": "Kod pocztowy ju\u017c zarejestrowany", + "invalid_zip_code": "Kod pocztowy jest nieprawid\u0142owy" + }, + "step": { + "user": { + "data": { + "zip_code": "Kod pocztowy" + }, + "description": "Wprowad\u017a sw\u00f3j ameryka\u0144ski lub kanadyjski kod pocztowy.", + "title": "IQVIA" + } + }, + "title": "IQVIA" + } +} \ No newline at end of file diff --git a/homeassistant/components/iqvia/.translations/ru.json b/homeassistant/components/iqvia/.translations/ru.json new file mode 100644 index 00000000000..06a5b7e69dd --- /dev/null +++ b/homeassistant/components/iqvia/.translations/ru.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "identifier_exists": "\u041f\u043e\u0447\u0442\u043e\u0432\u044b\u0439 \u0438\u043d\u0434\u0435\u043a\u0441 \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d", + "invalid_zip_code": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043f\u043e\u0447\u0442\u043e\u0432\u044b\u0439 \u0438\u043d\u0434\u0435\u043a\u0441" + }, + "step": { + "user": { + "data": { + "zip_code": "\u041f\u043e\u0447\u0442\u043e\u0432\u044b\u0439 \u0438\u043d\u0434\u0435\u043a\u0441" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0441\u0432\u043e\u0439 \u043f\u043e\u0447\u0442\u043e\u0432\u044b\u0439 \u0438\u043d\u0434\u0435\u043a\u0441 (\u0434\u043b\u044f \u0421\u0428\u0410 \u0438\u043b\u0438 \u041a\u0430\u043d\u0430\u0434\u044b).", + "title": "IQVIA" + } + }, + "title": "IQVIA" + } +} \ No newline at end of file diff --git a/homeassistant/components/iqvia/.translations/sv.json b/homeassistant/components/iqvia/.translations/sv.json new file mode 100644 index 00000000000..5bb4029dfcc --- /dev/null +++ b/homeassistant/components/iqvia/.translations/sv.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "identifier_exists": "Postnummer redan registrerat", + "invalid_zip_code": "Ogiltigt postnummer" + }, + "step": { + "user": { + "data": { + "zip_code": "Postnummer" + }, + "description": "Fyll i ditt Amerikanska eller Kanadensiska postnummer", + "title": "IQVIA" + } + }, + "title": "IQVIA" + } +} \ No newline at end of file diff --git a/homeassistant/components/iqvia/.translations/zh-Hans.json b/homeassistant/components/iqvia/.translations/zh-Hans.json new file mode 100644 index 00000000000..91d7a26d6c6 --- /dev/null +++ b/homeassistant/components/iqvia/.translations/zh-Hans.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "identifier_exists": "\u90ae\u653f\u7f16\u7801\u5df2\u88ab\u6ce8\u518c", + "invalid_zip_code": "\u90ae\u653f\u7f16\u7801\u65e0\u6548" + }, + "step": { + "user": { + "data": { + "zip_code": "\u90ae\u653f\u7f16\u7801" + }, + "description": "\u586b\u5199\u60a8\u7684\u7f8e\u56fd\u6216\u52a0\u62ff\u5927\u90ae\u653f\u7f16\u7801\u3002" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iqvia/.translations/zh-Hant.json b/homeassistant/components/iqvia/.translations/zh-Hant.json new file mode 100644 index 00000000000..a09db3b02c3 --- /dev/null +++ b/homeassistant/components/iqvia/.translations/zh-Hant.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "identifier_exists": "\u90f5\u905e\u5340\u865f\u5df2\u8a3b\u518a", + "invalid_zip_code": "\u90f5\u905e\u5340\u865f\u7121\u6548" + }, + "step": { + "user": { + "data": { + "zip_code": "\u90f5\u905e\u5340\u865f" + }, + "description": "\u586b\u5beb\u7f8e\u570b\u6216\u52a0\u62ff\u5927\u90f5\u905e\u5340\u865f\u3002", + "title": "IQVIA" + } + }, + "title": "IQVIA" + } +} \ No newline at end of file diff --git a/homeassistant/components/logi_circle/.translations/fr.json b/homeassistant/components/logi_circle/.translations/fr.json new file mode 100644 index 00000000000..85e8edc6bb1 --- /dev/null +++ b/homeassistant/components/logi_circle/.translations/fr.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_setup": "Vous ne pouvez configurer qu'un seul compte Logi Circle.", + "external_error": "Une exception est survenue \u00e0 partir d'un autre flux.", + "external_setup": "Logi Circle a \u00e9t\u00e9 configur\u00e9 avec succ\u00e8s \u00e0 partir d'un autre flux.", + "no_flows": "Vous devez configurer Logi Circle avant de pouvoir vous authentifier aupr\u00e8s de celui-ci. [Veuillez lire les instructions] (https://www.home-assistant.io/components/logi_circle/)." + }, + "create_entry": { + "default": "Authentifi\u00e9 avec succ\u00e8s avec Logi Circle." + }, + "error": { + "auth_error": "L'autorisation de l'API a \u00e9chou\u00e9.", + "auth_timeout": "L'autorisation a expir\u00e9 lors de la demande du jeton d'acc\u00e8s.", + "follow_link": "Veuillez suivre le lien et vous authentifier avant d'appuyer sur Soumettre." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/logi_circle/.translations/nl.json b/homeassistant/components/logi_circle/.translations/nl.json index fe1708568bd..84af68e1384 100644 --- a/homeassistant/components/logi_circle/.translations/nl.json +++ b/homeassistant/components/logi_circle/.translations/nl.json @@ -1,5 +1,14 @@ { "config": { + "abort": { + "external_error": "Uitzondering opgetreden uit een andere stroom." + }, + "create_entry": { + "default": "Succesvol geverifieerd met Logi Circle." + }, + "error": { + "auth_error": "API-autorisatie mislukt." + }, "title": "Logi Circle" } } \ No newline at end of file diff --git a/homeassistant/components/logi_circle/.translations/pl.json b/homeassistant/components/logi_circle/.translations/pl.json index ab46b72fdac..2c155ffde61 100644 --- a/homeassistant/components/logi_circle/.translations/pl.json +++ b/homeassistant/components/logi_circle/.translations/pl.json @@ -4,7 +4,7 @@ "already_setup": "Mo\u017cesz skonfigurowa\u0107 tylko jedno konto Logi Circle.", "external_error": "Wyst\u0105pi\u0142 wyj\u0105tek zewn\u0119trzny.", "external_setup": "Logi Circle pomy\u015blnie skonfigurowano.", - "no_flows": "Musisz skonfigurowa\u0107 Logi Circle, zanim b\u0119dziesz m\u00f3g\u0142 si\u0119 z nim uwierzytelni\u0107. [Przeczytaj instrukcje](https://www.home-assistant.io/components/logi_circle/)." + "no_flows": "Musisz skonfigurowa\u0107 Logi Circle, zanim b\u0119dziesz m\u00f3g\u0142 si\u0119 z nim uwierzytelni\u0107. [Przeczytaj instrukcj\u0119](https://www.home-assistant.io/components/logi_circle/)." }, "create_entry": { "default": "Pomy\u015blnie uwierzytelniono z Logi Circle." diff --git a/homeassistant/components/logi_circle/.translations/sv.json b/homeassistant/components/logi_circle/.translations/sv.json index d7e1e1e251c..221d2a7a86b 100644 --- a/homeassistant/components/logi_circle/.translations/sv.json +++ b/homeassistant/components/logi_circle/.translations/sv.json @@ -1,13 +1,17 @@ { "config": { "abort": { - "already_setup": "Du kan endast konfigurera ett Logi Circle-konto." + "already_setup": "Du kan endast konfigurera ett Logi Circle-konto.", + "external_error": "Undantag intr\u00e4ffade fr\u00e5n ett annat fl\u00f6de.", + "external_setup": "Logi Circle har konfigurerats fr\u00e5n ett annat fl\u00f6de.", + "no_flows": "Du m\u00e5ste konfigurera Logi Circle innan du kan autentisera med den. [V\u00e4nligen l\u00e4s instruktionerna] (https://www.home-assistant.io/components/logi_circle/)." }, "create_entry": { "default": "Autentiserad med Logi Circle." }, "error": { "auth_error": "API autentiseringen misslyckades.", + "auth_timeout": "Godk\u00e4nnandet tog f\u00f6r l\u00e5ng tid vid beg\u00e4ran om \u00e5tkomsttoken.", "follow_link": "V\u00e4nligen f\u00f6lj l\u00e4nken och autentisera innan du trycker p\u00e5 Skicka." }, "step": { diff --git a/homeassistant/components/luftdaten/.translations/ko.json b/homeassistant/components/luftdaten/.translations/ko.json index 7d182cc1a0e..97af0e8ed9b 100644 --- a/homeassistant/components/luftdaten/.translations/ko.json +++ b/homeassistant/components/luftdaten/.translations/ko.json @@ -3,7 +3,7 @@ "error": { "communication_error": "Luftdaten API \uc640 \ud1b5\uc2e0 \ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", "invalid_sensor": "\uc13c\uc11c\ub97c \uc0ac\uc6a9\ud560 \uc218 \uc5c6\uac70\ub098 \uc720\ud6a8\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4", - "sensor_exists": "\uc13c\uc11c\uac00 \uc774\ubbf8 \ub4f1\ub85d\ub41c \uc0c1\ud0dc\uc785\ub2c8\ub2e4" + "sensor_exists": "\uc13c\uc11c\uac00 \uc774\ubbf8 \ub4f1\ub85d\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "step": { "user": { diff --git a/homeassistant/components/mobile_app/.translations/sv.json b/homeassistant/components/mobile_app/.translations/sv.json index bdd94b84a1a..4f9570146f2 100644 --- a/homeassistant/components/mobile_app/.translations/sv.json +++ b/homeassistant/components/mobile_app/.translations/sv.json @@ -1,9 +1,14 @@ { "config": { + "abort": { + "install_app": "\u00d6ppna mobilappen f\u00f6r att konfigurera integrationen med Home Assistant. Se [docs] ({apps_url}) f\u00f6r en lista \u00f6ver kompatibla appar." + }, "step": { "confirm": { - "description": "Vill du st\u00e4lla in mobilappkomponenten?" + "description": "Vill du konfigurera komponenten Mobile App?", + "title": "Mobilapp" } - } + }, + "title": "Mobilapp" } } \ No newline at end of file diff --git a/homeassistant/components/moon/.translations/sensor.sv.json b/homeassistant/components/moon/.translations/sensor.sv.json index ae69c1c9654..1cd7596ba0f 100644 --- a/homeassistant/components/moon/.translations/sensor.sv.json +++ b/homeassistant/components/moon/.translations/sensor.sv.json @@ -1,12 +1,12 @@ { "state": { - "first_quarter": "F\u00f6rsta kvartalet", + "first_quarter": "F\u00f6rsta halvm\u00e5ne", "full_moon": "Fullm\u00e5ne", - "last_quarter": "Sista kvartalet", + "last_quarter": "Sista halvm\u00e5ne", "new_moon": "Nym\u00e5ne", - "waning_crescent": "Avtagande halvm\u00e5ne", + "waning_crescent": "Avtagande m\u00e5nsk\u00e4ra", "waning_gibbous": "Avtagande halvm\u00e5ne", - "waxing_crescent": "Tilltagande halvm\u00e5ne", + "waxing_crescent": "Tilltagande m\u00e5nsk\u00e4ra", "waxing_gibbous": "Tilltagande halvm\u00e5ne" } } \ No newline at end of file diff --git a/homeassistant/components/nest/.translations/pl.json b/homeassistant/components/nest/.translations/pl.json index c03b2eff0fa..ec33346cdf8 100644 --- a/homeassistant/components/nest/.translations/pl.json +++ b/homeassistant/components/nest/.translations/pl.json @@ -4,7 +4,7 @@ "already_setup": "Mo\u017cesz skonfigurowa\u0107 tylko jedno konto Nest.", "authorize_url_fail": "Nieznany b\u0142\u0105d podczas generowania url autoryzacji.", "authorize_url_timeout": "Min\u0105\u0142 limit czasu generowania url autoryzacji.", - "no_flows": "Musisz skonfigurowa\u0107 Nest, zanim b\u0119dziesz m\u00f3g\u0142 wykona\u0107 uwierzytelnienie. [Przeczytaj instrukcje](https://www.home-assistant.io/components/nest/)." + "no_flows": "Musisz skonfigurowa\u0107 Nest, zanim b\u0119dziesz m\u00f3g\u0142 wykona\u0107 uwierzytelnienie. [Przeczytaj instrukcj\u0119](https://www.home-assistant.io/components/nest/)." }, "error": { "internal_error": "Wewn\u0119trzny b\u0142\u0105d sprawdzania poprawno\u015bci kodu", diff --git a/homeassistant/components/onboarding/.translations/ca.json b/homeassistant/components/onboarding/.translations/ca.json new file mode 100644 index 00000000000..894bfe51674 --- /dev/null +++ b/homeassistant/components/onboarding/.translations/ca.json @@ -0,0 +1,7 @@ +{ + "area": { + "bedroom": "Dormitori", + "kitchen": "Cuina", + "living_room": "Sala d'estar" + } +} \ No newline at end of file diff --git a/homeassistant/components/onboarding/.translations/de.json b/homeassistant/components/onboarding/.translations/de.json new file mode 100644 index 00000000000..e44387f8008 --- /dev/null +++ b/homeassistant/components/onboarding/.translations/de.json @@ -0,0 +1,7 @@ +{ + "area": { + "bedroom": "Schlafzimmer", + "kitchen": "K\u00fcche", + "living_room": "Wohnzimmer" + } +} \ No newline at end of file diff --git a/homeassistant/components/onboarding/.translations/es.json b/homeassistant/components/onboarding/.translations/es.json new file mode 100644 index 00000000000..4c67fe20910 --- /dev/null +++ b/homeassistant/components/onboarding/.translations/es.json @@ -0,0 +1,7 @@ +{ + "area": { + "bedroom": "Dormitorio", + "kitchen": "Cocina", + "living_room": "Sal\u00f3n" + } +} \ No newline at end of file diff --git a/homeassistant/components/onboarding/.translations/fr.json b/homeassistant/components/onboarding/.translations/fr.json new file mode 100644 index 00000000000..8a8ff47a48a --- /dev/null +++ b/homeassistant/components/onboarding/.translations/fr.json @@ -0,0 +1,7 @@ +{ + "area": { + "bedroom": "Chambre", + "kitchen": "Cuisine", + "living_room": "Salle De S\u00e9jour" + } +} \ No newline at end of file diff --git a/homeassistant/components/onboarding/.translations/ko.json b/homeassistant/components/onboarding/.translations/ko.json new file mode 100644 index 00000000000..54d8ad6a7b7 --- /dev/null +++ b/homeassistant/components/onboarding/.translations/ko.json @@ -0,0 +1,7 @@ +{ + "area": { + "bedroom": "\uce68\uc2e4", + "kitchen": "\uc8fc\ubc29", + "living_room": "\uac70\uc2e4" + } +} \ No newline at end of file diff --git a/homeassistant/components/onboarding/.translations/nl.json b/homeassistant/components/onboarding/.translations/nl.json new file mode 100644 index 00000000000..ed9314973fb --- /dev/null +++ b/homeassistant/components/onboarding/.translations/nl.json @@ -0,0 +1,7 @@ +{ + "area": { + "bedroom": "Slaapkamer", + "kitchen": "Keuken", + "living_room": "Woonkamer" + } +} \ No newline at end of file diff --git a/homeassistant/components/onboarding/.translations/no.json b/homeassistant/components/onboarding/.translations/no.json new file mode 100644 index 00000000000..04f8359d026 --- /dev/null +++ b/homeassistant/components/onboarding/.translations/no.json @@ -0,0 +1,7 @@ +{ + "area": { + "bedroom": "Soverom", + "kitchen": "Kj\u00f8kken", + "living_room": "Stue" + } +} \ No newline at end of file diff --git a/homeassistant/components/onboarding/.translations/pl.json b/homeassistant/components/onboarding/.translations/pl.json new file mode 100644 index 00000000000..446ce7115aa --- /dev/null +++ b/homeassistant/components/onboarding/.translations/pl.json @@ -0,0 +1,7 @@ +{ + "area": { + "bedroom": "Sypialnia", + "kitchen": "Kuchnia", + "living_room": "Salon" + } +} \ No newline at end of file diff --git a/homeassistant/components/onboarding/.translations/sv.json b/homeassistant/components/onboarding/.translations/sv.json new file mode 100644 index 00000000000..4aec4ab353e --- /dev/null +++ b/homeassistant/components/onboarding/.translations/sv.json @@ -0,0 +1,7 @@ +{ + "area": { + "bedroom": "Sovrum", + "kitchen": "K\u00f6k", + "living_room": "Vardagsrum" + } +} \ No newline at end of file diff --git a/homeassistant/components/onboarding/.translations/zh-Hans.json b/homeassistant/components/onboarding/.translations/zh-Hans.json new file mode 100644 index 00000000000..3c38aa22985 --- /dev/null +++ b/homeassistant/components/onboarding/.translations/zh-Hans.json @@ -0,0 +1,7 @@ +{ + "area": { + "bedroom": "\u5367\u5ba4", + "kitchen": "\u53a8\u623f", + "living_room": "\u5ba2\u5385" + } +} \ No newline at end of file diff --git a/homeassistant/components/point/.translations/no.json b/homeassistant/components/point/.translations/no.json index c5e4a7b2e86..58b6e1e63fd 100644 --- a/homeassistant/components/point/.translations/no.json +++ b/homeassistant/components/point/.translations/no.json @@ -4,7 +4,7 @@ "already_setup": "Du kan kun konfigurere \u00e9n Point-konto.", "authorize_url_fail": "Ukjent feil ved generering en autoriseringsadresse.", "authorize_url_timeout": "Tidsavbrudd ved generering av autoriseringsadresse.", - "external_setup": "Point vellykket konfigurasjon fra en annen flow.", + "external_setup": "Punktet er konfigurert fra en annen flyt.", "no_flows": "Du m\u00e5 konfigurere Point f\u00f8r du kan autentisere med den. [Vennligst les instruksjonene](https://www.home-assistant.io/components/point/)." }, "create_entry": { diff --git a/homeassistant/components/point/.translations/pl.json b/homeassistant/components/point/.translations/pl.json index 98fa79573b0..66b454e47ff 100644 --- a/homeassistant/components/point/.translations/pl.json +++ b/homeassistant/components/point/.translations/pl.json @@ -5,7 +5,7 @@ "authorize_url_fail": "Nieznany b\u0142\u0105d podczas generowania url autoryzacji.", "authorize_url_timeout": "Min\u0105\u0142 limit czasu generowania url autoryzacji.", "external_setup": "Punkt pomy\u015blnie skonfigurowany.", - "no_flows": "Musisz skonfigurowa\u0107 Point, zanim b\u0119dziesz m\u00f3g\u0142 si\u0119 z nim uwierzytelni\u0107. [Przeczytaj instrukcje](https://www.home-assistant.io/components/point/)." + "no_flows": "Musisz skonfigurowa\u0107 Point, zanim b\u0119dziesz m\u00f3g\u0142 si\u0119 z nim uwierzytelni\u0107. [Przeczytaj instrukcj\u0119](https://www.home-assistant.io/components/point/)." }, "create_entry": { "default": "Pomy\u015blnie uwierzytelniono przy u\u017cyciu Minut dla urz\u0105dze\u0144 Point" diff --git a/homeassistant/components/ps4/.translations/es.json b/homeassistant/components/ps4/.translations/es.json index fd68e06a552..d2d749e4deb 100644 --- a/homeassistant/components/ps4/.translations/es.json +++ b/homeassistant/components/ps4/.translations/es.json @@ -8,6 +8,7 @@ "port_997_bind_error": "No se ha podido unir al puerto 997. Consulta la [documentaci\u00f3n](https://www.home-assistant.io/components/ps4/) para m\u00e1s informaci\u00f3n." }, "error": { + "credential_timeout": "Se agot\u00f3 el tiempo para el servicio de credenciales. Pulsa enviar para reiniciar.", "login_failed": "No se ha podido emparejar con PlayStation 4. Verifique que el PIN sea correcto.", "no_ipaddress": "Introduce la direcci\u00f3n IP de la PlayStation 4 que quieres configurar.", "not_ready": "PlayStation 4 no est\u00e1 encendido o conectado a la red." diff --git a/homeassistant/components/ps4/.translations/ko.json b/homeassistant/components/ps4/.translations/ko.json index 51454eeb135..f13a66d5e8a 100644 --- a/homeassistant/components/ps4/.translations/ko.json +++ b/homeassistant/components/ps4/.translations/ko.json @@ -15,7 +15,7 @@ }, "step": { "creds": { - "description": "\uc790\uaca9 \uc99d\uba85\uc774 \ud544\uc694\ud569\ub2c8\ub2e4. 'Submit' \uc744 \ub204\ub978 \ub2e4\uc74c PS4 Second Screen \uc571\uc5d0\uc11c \uae30\uae30\ub97c \uc0c8\ub85c \uace0\uce68\ud558\uace0 'Home-Assistant' \uae30\uae30\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694.", + "description": "\uc790\uaca9 \uc99d\uba85\uc774 \ud544\uc694\ud569\ub2c8\ub2e4. 'Submit' \uc744 \ub204\ub978 \ub2e4\uc74c PS4 \uc138\ucee8\ub4dc \uc2a4\ud06c\ub9b0 \uc571\uc5d0\uc11c \uae30\uae30\ub97c \uc0c8\ub85c \uace0\uce68\ud558\uace0 'Home-Assistant' \uae30\uae30\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694.", "title": "PlayStation 4" }, "link": { diff --git a/homeassistant/components/ps4/.translations/nl.json b/homeassistant/components/ps4/.translations/nl.json index 3dcadef20eb..c3cdf03355f 100644 --- a/homeassistant/components/ps4/.translations/nl.json +++ b/homeassistant/components/ps4/.translations/nl.json @@ -9,6 +9,7 @@ }, "error": { "login_failed": "Kan niet koppelen met PlayStation 4. Controleer of de pincode juist is.", + "no_ipaddress": "Voer het IP-adres in van de PlayStation 4 die je wilt configureren.", "not_ready": "PlayStation 4 staat niet aan of is niet verbonden met een netwerk." }, "step": { @@ -25,6 +26,12 @@ }, "description": "Voer je PlayStation 4 informatie in. Voor 'PIN', blader naar 'Instellingen' op je PlayStation 4. Blader dan naar 'Mobiele App verbindingsinstellingen' en kies 'Apparaat toevoegen'. Voer de weergegeven PIN-code in.", "title": "PlayStation 4" + }, + "mode": { + "data": { + "ip_address": "IP-adres (leeg laten als u Auto Discovery gebruikt)." + }, + "title": "PlayStation 4" } }, "title": "PlayStation 4" diff --git a/homeassistant/components/ps4/.translations/sv.json b/homeassistant/components/ps4/.translations/sv.json index 81f24179e54..a36c8e28d9e 100644 --- a/homeassistant/components/ps4/.translations/sv.json +++ b/homeassistant/components/ps4/.translations/sv.json @@ -8,6 +8,7 @@ "port_997_bind_error": "Kunde inte binda till port 997." }, "error": { + "credential_timeout": "Autentiseringstj\u00e4nsten orsakade timeout. Tryck p\u00e5 Skicka f\u00f6r att starta om.", "login_failed": "Misslyckades med att para till PlayStation 4. Verifiera PIN-koden \u00e4r korrekt.", "no_ipaddress": "Ange IP-adressen f\u00f6r PlayStation 4 du vill konfigurera.", "not_ready": "PlayStation 4 \u00e4r inte p\u00e5slagen eller ansluten till n\u00e4tverket." @@ -32,6 +33,7 @@ "ip_address": "IP-adress (l\u00e4mna tom om du anv\u00e4nder automatisk uppt\u00e4ckt).", "mode": "Konfigureringsl\u00e4ge" }, + "description": "V\u00e4lj l\u00e4ge f\u00f6r konfigurering. F\u00e4ltet IP-adress kan l\u00e4mnas tomt om du v\u00e4ljer Automatisk uppt\u00e4ckt, eftersom enheter d\u00e5 kommer att identifieras automatiskt.", "title": "PlayStation 4" } }, From aa5d8e5a817b864ac60746f67967d44196555e6d Mon Sep 17 00:00:00 2001 From: Fredrik Erlandsson Date: Thu, 23 May 2019 22:55:23 +0200 Subject: [PATCH 120/232] Daikin airbase beta fixes (#24050) * values are strings not integers * pydaikin version bump --- homeassistant/components/daikin/manifest.json | 2 +- homeassistant/components/daikin/switch.py | 2 +- requirements_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/daikin/manifest.json b/homeassistant/components/daikin/manifest.json index ccc791764a9..bb6db101314 100644 --- a/homeassistant/components/daikin/manifest.json +++ b/homeassistant/components/daikin/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/components/daikin", "requirements": [ - "pydaikin==1.4.5" + "pydaikin==1.4.6" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/daikin/switch.py b/homeassistant/components/daikin/switch.py index 8521153c398..f1a058957fa 100644 --- a/homeassistant/components/daikin/switch.py +++ b/homeassistant/components/daikin/switch.py @@ -27,7 +27,7 @@ async def async_setup_entry(hass, entry, async_add_entities): if zones: async_add_entities([ DaikinZoneSwitch(daikin_api, zone_id) - for zone_id, zone in enumerate(zones) if zone != ('-', 0) + for zone_id, zone in enumerate(zones) if zone != ('-', '0') ]) diff --git a/requirements_all.txt b/requirements_all.txt index a70837565fd..b1ab4f872a3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1027,7 +1027,7 @@ pycsspeechtts==1.0.2 # pycups==1.9.73 # homeassistant.components.daikin -pydaikin==1.4.5 +pydaikin==1.4.6 # homeassistant.components.danfoss_air pydanfossair==0.1.0 From 03253f4598946d01fb56ff922673790de696d53d Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Thu, 23 May 2019 13:57:00 -0700 Subject: [PATCH 121/232] Better logging of method used for ADB connection (#24037) --- homeassistant/components/androidtv/media_player.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py index 030f0425df0..e91cb1c02c4 100644 --- a/homeassistant/components/androidtv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -90,20 +90,21 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if CONF_ADB_SERVER_IP not in config: # Use "python-adb" (Python ADB implementation) + adb_log = "using Python ADB implementation " if CONF_ADBKEY in config: aftv = setup(host, config[CONF_ADBKEY], device_class=config[CONF_DEVICE_CLASS]) - adb_log = " using adbkey='{0}'".format(config[CONF_ADBKEY]) + adb_log += "with adbkey='{0}'".format(config[CONF_ADBKEY]) else: aftv = setup(host, device_class=config[CONF_DEVICE_CLASS]) - adb_log = "" + adb_log += "without adbkey authentication" else: # Use "pure-python-adb" (communicate with ADB server) aftv = setup(host, adb_server_ip=config[CONF_ADB_SERVER_IP], adb_server_port=config[CONF_ADB_SERVER_PORT], device_class=config[CONF_DEVICE_CLASS]) - adb_log = " using ADB server at {0}:{1}".format( + adb_log = "using ADB server at {0}:{1}".format( config[CONF_ADB_SERVER_IP], config[CONF_ADB_SERVER_PORT]) if not aftv.available: @@ -117,7 +118,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): else: device_name = 'Android TV / Fire TV device' - _LOGGER.warning("Could not connect to %s at %s%s", + _LOGGER.warning("Could not connect to %s at %s %s", device_name, host, adb_log) raise PlatformNotReady From 66af4bd0113957891293faf06000f7bcba553883 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 23 May 2019 14:41:57 -0700 Subject: [PATCH 122/232] Fix zeroconf sorting (#24068) --- script/hassfest/zeroconf.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/script/hassfest/zeroconf.py b/script/hassfest/zeroconf.py index 468d2741dbd..26e302c864c 100644 --- a/script/hassfest/zeroconf.py +++ b/script/hassfest/zeroconf.py @@ -1,4 +1,5 @@ """Generate zeroconf file.""" +from collections import OrderedDict import json from typing import Dict @@ -37,7 +38,10 @@ def generate_and_validate(integrations: Dict[str, Integration]): service_type_dict[service_type].append(domain) - return BASE.format(json.dumps(service_type_dict, indent=4)) + data = OrderedDict((key, service_type_dict[key]) + for key in sorted(service_type_dict)) + + return BASE.format(json.dumps(data, indent=4)) def validate(integrations: Dict[str, Integration], config: Config): From 6cef85049710044e98a7cac724283f396571b615 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Fri, 24 May 2019 09:46:59 +0200 Subject: [PATCH 123/232] Rfxtrx, add data types (#24066) * Rfxtrx, add data types * fix style --- homeassistant/components/rfxtrx/__init__.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py index f7e42ce4357..3545d16ebbd 100644 --- a/homeassistant/components/rfxtrx/__init__.py +++ b/homeassistant/components/rfxtrx/__init__.py @@ -44,7 +44,24 @@ DATA_TYPES = OrderedDict([ ('Sound', ''), ('Sensor Status', ''), ('Counter value', ''), - ('UV', 'uv')]) + ('UV', 'uv'), + ('Humidity status', ''), + ('Forecast', ''), + ('Forecast numeric', ''), + ('Rain total', ''), + ('Wind average speed', ''), + ('Wind gust', ''), + ('Chill', ''), + ('Total usage', ''), + ('Count', ''), + ('Current Ch. 1', ''), + ('Current Ch. 2', ''), + ('Current Ch. 3', ''), + ('Energy usage', ''), + ('Voltage', ''), + ('Current', ''), + ('Battery numeric', ''), + ('Rssi numeric', '')]) RECEIVED_EVT_SUBSCRIBERS = [] RFX_DEVICES = {} From 0a9a8ecc4e30beef313faa5c7bc3ecb6cedd300c Mon Sep 17 00:00:00 2001 From: dreed47 Date: Fri, 24 May 2019 10:01:55 -0400 Subject: [PATCH 124/232] Update the name of Zestimate sensors (#23770) * Zestimate: fix for issue #23757 Changed name property to return Zestimate and the property address. This will make it easier distinguish multiple Zestimate sensor entities in the UI. Also removed MIN_TIME_BETWEEN_UPDATES in favor of SCAN_INTERVAL per suggestion from amelchio#9580 on Discord * Zestimate fix for issue #23757 Changed name property to return Zestimate and the property address. This will make it easier distinguish multiple Zestimate sensor entities in the UI. * Changed name property to return Zestimate and the property address. This will make it easier distinguish multiple Zestimate sensor entities in the UI. * moved code fix to the correct function * removed code change from unique_id function --- homeassistant/components/zestimate/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/zestimate/sensor.py b/homeassistant/components/zestimate/sensor.py index 10467b20cfa..d48ecd8467c 100644 --- a/homeassistant/components/zestimate/sensor.py +++ b/homeassistant/components/zestimate/sensor.py @@ -75,7 +75,7 @@ class ZestimateDataSensor(Entity): @property def name(self): """Return the name of the sensor.""" - return self._name + return '{} {}'.format(self._name, self.address) @property def state(self): From ca2a68217da48634bf7216a985c777454ab9921f Mon Sep 17 00:00:00 2001 From: P0L0 <1452110+p0l0@users.noreply.github.com> Date: Fri, 24 May 2019 17:28:45 +0200 Subject: [PATCH 125/232] Added possibility to define the data type of Homematic (#24078) * Homematic: Added possibility to define the data type for set_device_value * Fixed coding style * Fixed variable name --- .../components/homematic/__init__.py | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index b301e22597e..013f1eab679 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -1,5 +1,5 @@ """Support for HomeMatic devices.""" -from datetime import timedelta +from datetime import timedelta, datetime from functools import partial import logging @@ -34,6 +34,7 @@ ATTR_PARAM = 'param' ATTR_CHANNEL = 'channel' ATTR_ADDRESS = 'address' ATTR_VALUE = 'value' +ATTR_VALUE_TYPE = 'value_type' ATTR_INTERFACE = 'interface' ATTR_ERRORCODE = 'error' ATTR_MESSAGE = 'message' @@ -235,6 +236,10 @@ SCHEMA_SERVICE_SET_DEVICE_VALUE = vol.Schema({ vol.Required(ATTR_CHANNEL): vol.Coerce(int), vol.Required(ATTR_PARAM): vol.All(cv.string, vol.Upper), vol.Required(ATTR_VALUE): cv.match_all, + vol.Optional(ATTR_VALUE_TYPE): vol.In([ + 'boolean', 'dateTime.iso8601', + 'double', 'int', 'string' + ]), vol.Optional(ATTR_INTERFACE): cv.string, }) @@ -379,6 +384,22 @@ def setup(hass, config): channel = service.data.get(ATTR_CHANNEL) param = service.data.get(ATTR_PARAM) value = service.data.get(ATTR_VALUE) + value_type = service.data.get(ATTR_VALUE_TYPE) + + # Convert value into correct XML-RPC Type. + # https://docs.python.org/3/library/xmlrpc.client.html#xmlrpc.client.ServerProxy + if value_type: + if value_type == 'int': + value = int(value) + elif value_type == 'double': + value = float(value) + elif value_type == 'boolean': + value = bool(value) + elif value_type == 'dateTime.iso8601': + value = datetime.strptime(value, '%Y%m%dT%H:%M:%S') + else: + # Default is 'string' + value = str(value) # Device not found hmdevice = _device_from_servicecall(hass, service) From 14d169558f4667588c47c94d241c7e479c69902b Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Fri, 24 May 2019 15:43:35 -0700 Subject: [PATCH 126/232] Add 'adb_response' attribute to Android TV / Fire TV (#23960) * Add 'adb_response' attribute to Android TV / Fire TV * Use None instead of empty string for empty ADB responses * Initialize self._adb_response as None, not empty string * Update the state after sending an ADB command This ensures that the `'adb_response'` attribute contains the response to the latest command --- .../components/androidtv/media_player.py | 30 +++++++++++++++---- 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py index e91cb1c02c4..efdd32ecbc5 100644 --- a/homeassistant/components/androidtv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -157,10 +157,10 @@ def setup_platform(hass, config, add_entities, discovery_info=None): for target_device in target_devices: output = target_device.adb_command(cmd) - # log the output if there is any - if output and (not isinstance(output, str) or output.strip()): + # log the output, if there is any + if output: _LOGGER.info("Output of command '%s' from '%s': %s", - cmd, target_device.entity_id, repr(output)) + cmd, target_device.entity_id, output) hass.services.register(ANDROIDTV_DOMAIN, SERVICE_ADB_COMMAND, service_adb_command, @@ -225,6 +225,7 @@ class ADBDevice(MediaPlayerDevice): self.exceptions = (ConnectionResetError, RuntimeError) # Property attributes + self._adb_response = None self._available = self.aftv.available self._current_app = None self._state = None @@ -244,6 +245,11 @@ class ADBDevice(MediaPlayerDevice): """Return whether or not the ADB connection is valid.""" return self._available + @property + def device_state_attributes(self): + """Provide the last ADB command's response as an attribute.""" + return {'adb_response': self._adb_response} + @property def name(self): """Return the device name.""" @@ -305,12 +311,24 @@ class ADBDevice(MediaPlayerDevice): """Send an ADB command to an Android TV / Fire TV device.""" key = self._keys.get(cmd) if key: - return self.aftv.adb_shell('input keyevent {}'.format(key)) + self.aftv.adb_shell('input keyevent {}'.format(key)) + self._adb_response = None + self.schedule_update_ha_state() + return if cmd == 'GET_PROPERTIES': - return self.aftv.get_properties_dict() + self._adb_response = str(self.aftv.get_properties_dict()) + self.schedule_update_ha_state() + return self._adb_response - return self.aftv.adb_shell(cmd) + response = self.aftv.adb_shell(cmd) + if isinstance(response, str) and response.strip(): + self._adb_response = response.strip() + else: + self._adb_response = None + + self.schedule_update_ha_state() + return self._adb_response class AndroidTVDevice(ADBDevice): From e9f561e7abf8a99c291e69d6fa000dd416635203 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sat, 25 May 2019 00:54:04 +0200 Subject: [PATCH 127/232] Adjust logging (#24082) * Make sure we log full path to debug log * Make sure we log the exception to debug log --- homeassistant/setup.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/setup.py b/homeassistant/setup.py index ec88a48ae7b..86a188bea01 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -207,7 +207,7 @@ async def async_prepare_setup_platform(hass: core.HomeAssistant, def log_error(msg: str) -> None: """Log helper.""" _LOGGER.error("Unable to prepare setup for platform %s: %s", - platform_name, msg) + platform_path, msg) async_notify_setup_error(hass, platform_path) try: @@ -226,8 +226,8 @@ async def async_prepare_setup_platform(hass: core.HomeAssistant, try: platform = integration.get_platform(domain) - except ImportError: - log_error("Platform not found.") + except ImportError as exc: + log_error("Platform not found ({}).".format(exc)) return None # Already loaded @@ -239,8 +239,8 @@ async def async_prepare_setup_platform(hass: core.HomeAssistant, if integration.domain not in hass.config.components: try: component = integration.get_component() - except ImportError: - log_error("Unable to import the component") + except ImportError as exc: + log_error("Unable to import the component ({}).".format(exc)) return None if (hasattr(component, 'setup') From bad9ac539557d9332596c91843a8f63f210cf7fa Mon Sep 17 00:00:00 2001 From: terual Date: Sat, 25 May 2019 00:55:13 +0200 Subject: [PATCH 128/232] Fix Hue bridge timeout (#24084) * Change timeout from 5 seconds to 10 seconds Underpowered platforms timeout during configuration/discovery of a Hue bridge on a new install. Increasing this timeout fixes this. --- homeassistant/components/hue/bridge.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py index 0610278de7b..6fa6bad2f47 100644 --- a/homeassistant/components/hue/bridge.py +++ b/homeassistant/components/hue/bridge.py @@ -153,7 +153,7 @@ async def get_bridge(hass, host, username=None): ) try: - with async_timeout.timeout(5): + with async_timeout.timeout(10): # Create username if we don't have one if not username: await bridge.create_user('home-assistant') From 1d022522cd7fbb67856ea18b158f97d9d5b059fe Mon Sep 17 00:00:00 2001 From: Jardi Martinez <1088732+jardiamj@users.noreply.github.com> Date: Fri, 24 May 2019 23:09:53 -0700 Subject: [PATCH 129/232] MCP23017 (#23127) * Added support for MCP23017 I2C GPIO extender. * Updated .coveragerc to exclude mcp23017 component from tests. * Generated CODEOWNERS for mcp23017 usign script. * Removed .svn folder that had been accidentally uploaded. * Added link to www.home-assistant.io docs in manifest.json * Fixed logic error in switch platform. * Cleaned up code and removed unnecessary should_poll() function. * Limited the options for pull mode to UP and DOWN * Fixed line too long in binary sensor. * Fixed line too long on switch.py * Changed to setup_platform. * Reorder constants --- .coveragerc | 1 + CODEOWNERS | 1 + homeassistant/components/mcp23017/__init__.py | 3 + .../components/mcp23017/binary_sensor.py | 89 ++++++++++++++++++ .../components/mcp23017/manifest.json | 12 +++ homeassistant/components/mcp23017/switch.py | 92 +++++++++++++++++++ requirements_all.txt | 7 ++ 7 files changed, 205 insertions(+) create mode 100644 homeassistant/components/mcp23017/__init__.py create mode 100644 homeassistant/components/mcp23017/binary_sensor.py create mode 100644 homeassistant/components/mcp23017/manifest.json create mode 100644 homeassistant/components/mcp23017/switch.py diff --git a/.coveragerc b/.coveragerc index 20bbc0474eb..8f32fcdbe37 100644 --- a/.coveragerc +++ b/.coveragerc @@ -343,6 +343,7 @@ omit = homeassistant/components/mastodon/notify.py homeassistant/components/matrix/* homeassistant/components/maxcube/* + homeassistant/components/mcp23017/* homeassistant/components/media_extractor/* homeassistant/components/mediaroom/media_player.py homeassistant/components/message_bird/notify.py diff --git a/CODEOWNERS b/CODEOWNERS index cadf6a2e841..00e05b98778 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -143,6 +143,7 @@ homeassistant/components/luci/* @fbradyirl homeassistant/components/luftdaten/* @fabaff homeassistant/components/mastodon/* @fabaff homeassistant/components/matrix/* @tinloaf +homeassistant/components/mcp23017/* @jardiamj homeassistant/components/mediaroom/* @dgomes homeassistant/components/melissa/* @kennedyshead homeassistant/components/met/* @danielhiversen diff --git a/homeassistant/components/mcp23017/__init__.py b/homeassistant/components/mcp23017/__init__.py new file mode 100644 index 00000000000..350ebc7f71d --- /dev/null +++ b/homeassistant/components/mcp23017/__init__.py @@ -0,0 +1,3 @@ +"""Support for I2C MCP23017 chip.""" + +DOMAIN = 'mcp23017' diff --git a/homeassistant/components/mcp23017/binary_sensor.py b/homeassistant/components/mcp23017/binary_sensor.py new file mode 100644 index 00000000000..6934468ec1c --- /dev/null +++ b/homeassistant/components/mcp23017/binary_sensor.py @@ -0,0 +1,89 @@ +"""Support for binary sensor using I2C MCP23017 chip.""" +import logging + +import voluptuous as vol + +from homeassistant.components.binary_sensor import ( + BinarySensorDevice, PLATFORM_SCHEMA) +from homeassistant.const import DEVICE_DEFAULT_NAME +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +CONF_INVERT_LOGIC = 'invert_logic' +CONF_I2C_ADDRESS = 'i2c_address' +CONF_PINS = 'pins' +CONF_PULL_MODE = 'pull_mode' + +MODE_UP = 'UP' +MODE_DOWN = 'DOWN' + +DEFAULT_INVERT_LOGIC = False +DEFAULT_I2C_ADDRESS = 0x20 +DEFAULT_PULL_MODE = MODE_UP + +_SENSORS_SCHEMA = vol.Schema({ + cv.positive_int: cv.string, +}) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_PINS): _SENSORS_SCHEMA, + vol.Optional(CONF_INVERT_LOGIC, default=DEFAULT_INVERT_LOGIC): cv.boolean, + vol.Optional(CONF_PULL_MODE, default=DEFAULT_PULL_MODE): + vol.All(vol.Upper, vol.In([MODE_UP, MODE_DOWN])), + vol.Optional(CONF_I2C_ADDRESS, default=DEFAULT_I2C_ADDRESS): + vol.Coerce(int), +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the MCP23017 binary sensors.""" + import board + import busio + import adafruit_mcp230xx + + pull_mode = config[CONF_PULL_MODE] + invert_logic = config[CONF_INVERT_LOGIC] + i2c_address = config[CONF_I2C_ADDRESS] + + i2c = busio.I2C(board.SCL, board.SDA) + mcp = adafruit_mcp230xx.MCP23017(i2c, address=i2c_address) + + binary_sensors = [] + pins = config[CONF_PINS] + + for pin_num, pin_name in pins.items(): + pin = mcp.get_pin(pin_num) + binary_sensors.append(MCP23017BinarySensor( + pin_name, pin, pull_mode, invert_logic)) + + add_devices(binary_sensors, True) + + +class MCP23017BinarySensor(BinarySensorDevice): + """Represent a binary sensor that uses MCP23017.""" + + def __init__(self, name, pin, pull_mode, invert_logic): + """Initialize the MCP23017 binary sensor.""" + import digitalio + self._name = name or DEVICE_DEFAULT_NAME + self._pin = pin + self._pull_mode = pull_mode + self._invert_logic = invert_logic + self._state = None + self._pin.direction = digitalio.Direction.INPUT + self._pin.pull = digitalio.Pull.UP + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def is_on(self): + """Return the state of the entity.""" + return self._state != self._invert_logic + + def update(self): + """Update the GPIO state.""" + self._state = self._pin.value diff --git a/homeassistant/components/mcp23017/manifest.json b/homeassistant/components/mcp23017/manifest.json new file mode 100644 index 00000000000..41048683c92 --- /dev/null +++ b/homeassistant/components/mcp23017/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "mcp23017", + "name": "MCP23017 I/O Expander", + "documentation": "https://www.home-assistant.io/components/mcp23017", + "requirements": [ + "RPi.GPIO==0.6.5", + "adafruit-blinka==1.2.1", + "adafruit-circuitpython-mcp230xx==1.1.2" + ], + "dependencies": [], + "codeowners": ["@jardiamj"] +} diff --git a/homeassistant/components/mcp23017/switch.py b/homeassistant/components/mcp23017/switch.py new file mode 100644 index 00000000000..caa183543ac --- /dev/null +++ b/homeassistant/components/mcp23017/switch.py @@ -0,0 +1,92 @@ +"""Support for switch sensor using I2C MCP23017 chip.""" +import logging + +import voluptuous as vol + +from homeassistant.components.switch import PLATFORM_SCHEMA +from homeassistant.const import DEVICE_DEFAULT_NAME +from homeassistant.helpers.entity import ToggleEntity +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +CONF_INVERT_LOGIC = 'invert_logic' +CONF_I2C_ADDRESS = 'i2c_address' +CONF_PINS = 'pins' +CONF_PULL_MODE = 'pull_mode' + +DEFAULT_INVERT_LOGIC = False +DEFAULT_I2C_ADDRESS = 0x20 + +_SWITCHES_SCHEMA = vol.Schema({ + cv.positive_int: cv.string, +}) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_PINS): _SWITCHES_SCHEMA, + vol.Optional(CONF_INVERT_LOGIC, default=DEFAULT_INVERT_LOGIC): cv.boolean, + vol.Optional(CONF_I2C_ADDRESS, default=DEFAULT_I2C_ADDRESS): + vol.Coerce(int), +}) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the MCP23017 devices.""" + import board + import busio + import adafruit_mcp230xx + + invert_logic = config.get(CONF_INVERT_LOGIC) + i2c_address = config.get(CONF_I2C_ADDRESS) + + i2c = busio.I2C(board.SCL, board.SDA) + mcp = adafruit_mcp230xx.MCP23017(i2c, address=i2c_address) + + switches = [] + pins = config.get(CONF_PINS) + for pin_num, pin_name in pins.items(): + pin = mcp.get_pin(pin_num) + switches.append(MCP23017Switch(pin_name, pin, invert_logic)) + add_entities(switches) + + +class MCP23017Switch(ToggleEntity): + """Representation of a MCP23017 output pin.""" + + def __init__(self, name, pin, invert_logic): + """Initialize the pin.""" + import digitalio + self._name = name or DEVICE_DEFAULT_NAME + self._pin = pin + self._invert_logic = invert_logic + self._state = False + + self._pin.direction = digitalio.Direction.OUTPUT + self._pin.value = self._invert_logic + + @property + def name(self): + """Return the name of the switch.""" + return self._name + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def is_on(self): + """Return true if device is on.""" + return self._state + + def turn_on(self, **kwargs): + """Turn the device on.""" + self._pin.value = not self._invert_logic + self._state = True + self.schedule_update_ha_state() + + def turn_off(self, **kwargs): + """Turn the device off.""" + self._pin.value = self._invert_logic + self._state = False + self.schedule_update_ha_state() diff --git a/requirements_all.txt b/requirements_all.txt index b1ab4f872a3..dbaa52d6baf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -75,6 +75,7 @@ PyTransportNSW==0.1.1 # homeassistant.components.xiaomi_aqara PyXiaomiGateway==0.12.3 +# homeassistant.components.mcp23017 # homeassistant.components.rpi_gpio # RPi.GPIO==0.6.5 @@ -99,6 +100,12 @@ YesssSMS==0.2.3 # homeassistant.components.abode abodepy==0.15.0 +# homeassistant.components.mcp23017 +adafruit-blinka==1.2.1 + +# homeassistant.components.mcp23017 +adafruit-circuitpython-mcp230xx==1.1.2 + # homeassistant.components.frontier_silicon afsapi==0.0.4 From 02f927ae2dfc1ed8de305a3cb7a7ee2b955b97cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Sat, 25 May 2019 09:26:46 +0200 Subject: [PATCH 130/232] typo for ambiclimate (#24083) --- homeassistant/components/ambiclimate/climate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/ambiclimate/climate.py b/homeassistant/components/ambiclimate/climate.py index d326a943761..ae61163ab05 100644 --- a/homeassistant/components/ambiclimate/climate.py +++ b/homeassistant/components/ambiclimate/climate.py @@ -62,7 +62,7 @@ async def async_setup_entry(hass, entry, async_add_entities): return if _token_info: - await store.async_save(token_info) + await store.async_save(_token_info) token_info = _token_info data_connection = ambiclimate.AmbiclimateConnection(oauth, From 9d7aa8f05d82a5012fe9cfb6c5fb1930ce8bfc93 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sat, 25 May 2019 11:29:20 +0200 Subject: [PATCH 131/232] Remove device tracker unnecessary separate except clause (#24081) Handle exception where it can be thrown. --- .../components/device_tracker/legacy.py | 40 +++++++++---------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/device_tracker/legacy.py b/homeassistant/components/device_tracker/legacy.py index bc1c6013ac0..b27b5e20acf 100644 --- a/homeassistant/components/device_tracker/legacy.py +++ b/homeassistant/components/device_tracker/legacy.py @@ -478,29 +478,27 @@ async def async_load_config(path: str, hass: HomeAssistantType, vol.Optional(CONF_CONSIDER_HOME, default=consider_home): vol.All( cv.time_period, cv.positive_timedelta), }) + result = [] try: - result = [] - try: - devices = await hass.async_add_job( - load_yaml_config_file, path) - except HomeAssistantError as err: - LOGGER.error("Unable to load %s: %s", path, str(err)) - return [] - - for dev_id, device in devices.items(): - # Deprecated option. We just ignore it to avoid breaking change - device.pop('vendor', None) - try: - device = dev_schema(device) - device['dev_id'] = cv.slugify(dev_id) - except vol.Invalid as exp: - async_log_exception(exp, dev_id, devices, hass) - else: - result.append(Device(hass, **device)) - return result - except (HomeAssistantError, FileNotFoundError): - # When YAML file could not be loaded/did not contain a dict + devices = await hass.async_add_job( + load_yaml_config_file, path) + except HomeAssistantError as err: + LOGGER.error("Unable to load %s: %s", path, str(err)) return [] + except FileNotFoundError: + return [] + + for dev_id, device in devices.items(): + # Deprecated option. We just ignore it to avoid breaking change + device.pop('vendor', None) + try: + device = dev_schema(device) + device['dev_id'] = cv.slugify(dev_id) + except vol.Invalid as exp: + async_log_exception(exp, dev_id, devices, hass) + else: + result.append(Device(hass, **device)) + return result def update_config(path: str, dev_id: str, device: Device): From c928f82cbf8bb5956e8cb9d1d4ef210036c07476 Mon Sep 17 00:00:00 2001 From: Andre Lengwenus Date: Sat, 25 May 2019 11:40:44 +0200 Subject: [PATCH 132/232] Refactoring of LCN component (#23824) * Moved helper functions to const.py * Removed pypck attribute from LcnDevice * Bump to pypck==0.6.0 * Added myself as a codeowner * Moved helper functions to helpers.py --- CODEOWNERS | 1 + homeassistant/components/lcn/__init__.py | 80 +++---------------- homeassistant/components/lcn/binary_sensor.py | 17 ++-- homeassistant/components/lcn/climate.py | 13 +-- homeassistant/components/lcn/const.py | 5 -- homeassistant/components/lcn/cover.py | 19 ++--- homeassistant/components/lcn/helpers.py | 67 ++++++++++++++++ homeassistant/components/lcn/light.py | 25 +++--- homeassistant/components/lcn/manifest.json | 6 +- homeassistant/components/lcn/sensor.py | 19 ++--- homeassistant/components/lcn/switch.py | 19 ++--- requirements_all.txt | 2 +- 12 files changed, 144 insertions(+), 129 deletions(-) create mode 100644 homeassistant/components/lcn/helpers.py diff --git a/CODEOWNERS b/CODEOWNERS index 00e05b98778..a4e82847523 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -131,6 +131,7 @@ homeassistant/components/kodi/* @armills homeassistant/components/konnected/* @heythisisnate homeassistant/components/lametric/* @robbiet480 homeassistant/components/launch_library/* @ludeeus +homeassistant/components/lcn/* @alengwenus homeassistant/components/lifx/* @amelchio homeassistant/components/lifx_cloud/* @amelchio homeassistant/components/lifx_legacy/* @amelchio diff --git a/homeassistant/components/lcn/__init__.py b/homeassistant/components/lcn/__init__.py index b2dbccbce7b..77ba00e451d 100644 --- a/homeassistant/components/lcn/__init__.py +++ b/homeassistant/components/lcn/__init__.py @@ -2,7 +2,6 @@ import logging import pypck -from pypck.connection import PchkConnectionManager import voluptuous as vol from homeassistant.components.climate import DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP @@ -18,55 +17,13 @@ from .const import ( BINSENSOR_PORTS, CONF_CLIMATES, CONF_CONNECTIONS, CONF_DIM_MODE, CONF_DIMMABLE, CONF_LOCKABLE, CONF_MAX_TEMP, CONF_MIN_TEMP, CONF_MOTOR, CONF_OUTPUT, CONF_SETPOINT, CONF_SK_NUM_TRIES, CONF_SOURCE, - CONF_TRANSITION, DATA_LCN, DEFAULT_NAME, DIM_MODES, DOMAIN, KEYS, - LED_PORTS, LOGICOP_PORTS, MOTOR_PORTS, OUTPUT_PORTS, PATTERN_ADDRESS, - RELAY_PORTS, S0_INPUTS, SETPOINTS, THRESHOLDS, VAR_UNITS, VARIABLES) + CONF_TRANSITION, DATA_LCN, DIM_MODES, DOMAIN, KEYS, LED_PORTS, + LOGICOP_PORTS, MOTOR_PORTS, OUTPUT_PORTS, RELAY_PORTS, S0_INPUTS, + SETPOINTS, THRESHOLDS, VAR_UNITS, VARIABLES) +from .helpers import has_unique_connection_names, is_address _LOGGER = logging.getLogger(__name__) - -def has_unique_connection_names(connections): - """Validate that all connection names are unique. - - Use 'pchk' as default connection_name (or add a numeric suffix if - pchk' is already in use. - """ - for suffix, connection in enumerate(connections): - connection_name = connection.get(CONF_NAME) - if connection_name is None: - if suffix == 0: - connection[CONF_NAME] = DEFAULT_NAME - else: - connection[CONF_NAME] = '{}{:d}'.format(DEFAULT_NAME, suffix) - - schema = vol.Schema(vol.Unique()) - schema([connection.get(CONF_NAME) for connection in connections]) - return connections - - -def is_address(value): - """Validate the given address string. - - Examples for S000M005 at myhome: - myhome.s000.m005 - myhome.s0.m5 - myhome.0.5 ("m" is implicit if missing) - - Examples for s000g011 - myhome.0.g11 - myhome.s0.g11 - """ - matcher = PATTERN_ADDRESS.match(value) - if matcher: - is_group = (matcher.group('type') == 'g') - addr = (int(matcher.group('seg_id')), - int(matcher.group('id')), - is_group) - conn_id = matcher.group('conn_id') - return addr, conn_id - raise vol.error.Invalid('Not a valid address string.') - - BINARY_SENSORS_SCHEMA = vol.Schema({ vol.Required(CONF_NAME): cv.string, vol.Required(CONF_ADDRESS): is_address, @@ -153,19 +110,6 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -def get_connection(connections, connection_id=None): - """Return the connection object from list.""" - if connection_id is None: - connection = connections[0] - else: - for connection in connections: - if connection.connection_id == connection_id: - break - else: - raise ValueError('Unknown connection_id.') - return connection - - async def async_setup(hass, config): """Set up the LCN component.""" hass.data[DATA_LCN] = {} @@ -179,13 +123,14 @@ async def async_setup(hass, config): 'DIM_MODE': pypck.lcn_defs.OutputPortDimMode[ conf_connection[CONF_DIM_MODE]]} - connection = PchkConnectionManager(hass.loop, - conf_connection[CONF_HOST], - conf_connection[CONF_PORT], - conf_connection[CONF_USERNAME], - conf_connection[CONF_PASSWORD], - settings=settings, - connection_id=connection_name) + connection = pypck.connection.PchkConnectionManager( + hass.loop, + conf_connection[CONF_HOST], + conf_connection[CONF_PORT], + conf_connection[CONF_USERNAME], + conf_connection[CONF_PASSWORD], + settings=settings, + connection_id=connection_name) try: # establish connection to PCHK server @@ -218,7 +163,6 @@ class LcnDevice(Entity): def __init__(self, config, address_connection): """Initialize the LCN device.""" - self.pypck = pypck self.config = config self.address_connection = address_connection self._name = config[CONF_NAME] diff --git a/homeassistant/components/lcn/binary_sensor.py b/homeassistant/components/lcn/binary_sensor.py index a59494023bb..7f034b3e1ed 100755 --- a/homeassistant/components/lcn/binary_sensor.py +++ b/homeassistant/components/lcn/binary_sensor.py @@ -4,9 +4,10 @@ import pypck from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.const import CONF_ADDRESS -from . import LcnDevice, get_connection +from . import LcnDevice from .const import ( BINSENSOR_PORTS, CONF_CONNECTIONS, CONF_SOURCE, DATA_LCN, SETPOINTS) +from .helpers import get_connection async def async_setup_platform(hass, hass_config, async_add_entities, @@ -43,7 +44,7 @@ class LcnRegulatorLockSensor(LcnDevice, BinarySensorDevice): super().__init__(config, address_connection) self.setpoint_variable = \ - self.pypck.lcn_defs.Var[config[CONF_SOURCE]] + pypck.lcn_defs.Var[config[CONF_SOURCE]] self._value = None @@ -60,7 +61,7 @@ class LcnRegulatorLockSensor(LcnDevice, BinarySensorDevice): def input_received(self, input_obj): """Set sensor value when LCN input object (command) is received.""" - if not isinstance(input_obj, self.pypck.inputs.ModStatusVar) or \ + if not isinstance(input_obj, pypck.inputs.ModStatusVar) or \ input_obj.get_var() != self.setpoint_variable: return @@ -76,7 +77,7 @@ class LcnBinarySensor(LcnDevice, BinarySensorDevice): super().__init__(config, address_connection) self.bin_sensor_port = \ - self.pypck.lcn_defs.BinSensorPort[config[CONF_SOURCE]] + pypck.lcn_defs.BinSensorPort[config[CONF_SOURCE]] self._value = None @@ -93,7 +94,7 @@ class LcnBinarySensor(LcnDevice, BinarySensorDevice): def input_received(self, input_obj): """Set sensor value when LCN input object (command) is received.""" - if not isinstance(input_obj, self.pypck.inputs.ModStatusBinSensors): + if not isinstance(input_obj, pypck.inputs.ModStatusBinSensors): return self._value = input_obj.get_state(self.bin_sensor_port.value) @@ -107,7 +108,7 @@ class LcnLockKeysSensor(LcnDevice, BinarySensorDevice): """Initialize the LCN sensor.""" super().__init__(config, address_connection) - self.source = self.pypck.lcn_defs.Key[config[CONF_SOURCE]] + self.source = pypck.lcn_defs.Key[config[CONF_SOURCE]] self._value = None async def async_added_to_hass(self): @@ -123,8 +124,8 @@ class LcnLockKeysSensor(LcnDevice, BinarySensorDevice): def input_received(self, input_obj): """Set sensor value when LCN input object (command) is received.""" - if not isinstance(input_obj, self.pypck.inputs.ModStatusKeyLocks) or \ - self.source not in self.pypck.lcn_defs.Key: + if not isinstance(input_obj, pypck.inputs.ModStatusKeyLocks) or \ + self.source not in pypck.lcn_defs.Key: return table_id = ord(self.source.name[0]) - 65 diff --git a/homeassistant/components/lcn/climate.py b/homeassistant/components/lcn/climate.py index 355a9e84c79..67ba6d90c53 100644 --- a/homeassistant/components/lcn/climate.py +++ b/homeassistant/components/lcn/climate.py @@ -5,10 +5,11 @@ from homeassistant.components.climate import ClimateDevice, const from homeassistant.const import ( ATTR_TEMPERATURE, CONF_ADDRESS, CONF_UNIT_OF_MEASUREMENT) -from . import LcnDevice, get_connection +from . import LcnDevice from .const import ( CONF_CONNECTIONS, CONF_LOCKABLE, CONF_MAX_TEMP, CONF_MIN_TEMP, CONF_SETPOINT, CONF_SOURCE, DATA_LCN) +from .helpers import get_connection async def async_setup_platform(hass, hass_config, async_add_entities, @@ -37,13 +38,13 @@ class LcnClimate(LcnDevice, ClimateDevice): """Initialize of a LCN climate device.""" super().__init__(config, address_connection) - self.variable = self.pypck.lcn_defs.Var[config[CONF_SOURCE]] - self.setpoint = self.pypck.lcn_defs.Var[config[CONF_SETPOINT]] - self.unit = self.pypck.lcn_defs.VarUnit.parse( + self.variable = pypck.lcn_defs.Var[config[CONF_SOURCE]] + self.setpoint = pypck.lcn_defs.Var[config[CONF_SETPOINT]] + self.unit = pypck.lcn_defs.VarUnit.parse( config[CONF_UNIT_OF_MEASUREMENT]) self.regulator_id = \ - self.pypck.lcn_defs.Var.to_set_point_id(self.setpoint) + pypck.lcn_defs.Var.to_set_point_id(self.setpoint) self.is_lockable = config[CONF_LOCKABLE] self._max_temp = config[CONF_MAX_TEMP] self._min_temp = config[CONF_MIN_TEMP] @@ -125,7 +126,7 @@ class LcnClimate(LcnDevice, ClimateDevice): def input_received(self, input_obj): """Set temperature value when LCN input object is received.""" - if not isinstance(input_obj, self.pypck.inputs.ModStatusVar): + if not isinstance(input_obj, pypck.inputs.ModStatusVar): return if input_obj.get_var() == self.variable: diff --git a/homeassistant/components/lcn/const.py b/homeassistant/components/lcn/const.py index 68818984788..45dc04a491e 100644 --- a/homeassistant/components/lcn/const.py +++ b/homeassistant/components/lcn/const.py @@ -1,7 +1,6 @@ # coding: utf-8 """Constants for the LCN component.""" from itertools import product -import re from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT @@ -9,10 +8,6 @@ DOMAIN = 'lcn' DATA_LCN = 'lcn' DEFAULT_NAME = 'pchk' -# Regex for address validation -PATTERN_ADDRESS = re.compile('^((?P\\w+)\\.)?s?(?P\\d+)' - '\\.(?Pm|g)?(?P\\d+)$') - CONF_CONNECTIONS = 'connections' CONF_SK_NUM_TRIES = 'sk_num_tries' CONF_OUTPUT = 'output' diff --git a/homeassistant/components/lcn/cover.py b/homeassistant/components/lcn/cover.py index d07fa09c189..8b268aa617e 100755 --- a/homeassistant/components/lcn/cover.py +++ b/homeassistant/components/lcn/cover.py @@ -4,8 +4,9 @@ import pypck from homeassistant.components.cover import CoverDevice from homeassistant.const import CONF_ADDRESS -from . import LcnDevice, get_connection +from . import LcnDevice from .const import CONF_CONNECTIONS, CONF_MOTOR, DATA_LCN +from .helpers import get_connection async def async_setup_platform(hass, hass_config, async_add_entities, @@ -34,7 +35,7 @@ class LcnCover(LcnDevice, CoverDevice): """Initialize the LCN cover.""" super().__init__(config, address_connection) - self.motor = self.pypck.lcn_defs.MotorPort[config[CONF_MOTOR]] + self.motor = pypck.lcn_defs.MotorPort[config[CONF_MOTOR]] self.motor_port_onoff = self.motor.value * 2 self.motor_port_updown = self.motor_port_onoff + 1 @@ -54,30 +55,30 @@ class LcnCover(LcnDevice, CoverDevice): async def async_close_cover(self, **kwargs): """Close the cover.""" self._closed = True - states = [self.pypck.lcn_defs.MotorStateModifier.NOCHANGE] * 4 - states[self.motor.value] = self.pypck.lcn_defs.MotorStateModifier.DOWN + states = [pypck.lcn_defs.MotorStateModifier.NOCHANGE] * 4 + states[self.motor.value] = pypck.lcn_defs.MotorStateModifier.DOWN self.address_connection.control_motors(states) await self.async_update_ha_state() async def async_open_cover(self, **kwargs): """Open the cover.""" self._closed = False - states = [self.pypck.lcn_defs.MotorStateModifier.NOCHANGE] * 4 - states[self.motor.value] = self.pypck.lcn_defs.MotorStateModifier.UP + states = [pypck.lcn_defs.MotorStateModifier.NOCHANGE] * 4 + states[self.motor.value] = pypck.lcn_defs.MotorStateModifier.UP self.address_connection.control_motors(states) await self.async_update_ha_state() async def async_stop_cover(self, **kwargs): """Stop the cover.""" self._closed = None - states = [self.pypck.lcn_defs.MotorStateModifier.NOCHANGE] * 4 - states[self.motor.value] = self.pypck.lcn_defs.MotorStateModifier.STOP + states = [pypck.lcn_defs.MotorStateModifier.NOCHANGE] * 4 + states[self.motor.value] = pypck.lcn_defs.MotorStateModifier.STOP self.address_connection.control_motors(states) await self.async_update_ha_state() def input_received(self, input_obj): """Set cover states when LCN input object (command) is received.""" - if not isinstance(input_obj, self.pypck.inputs.ModStatusRelays): + if not isinstance(input_obj, pypck.inputs.ModStatusRelays): return states = input_obj.states # list of boolean values (relay on/off) diff --git a/homeassistant/components/lcn/helpers.py b/homeassistant/components/lcn/helpers.py new file mode 100644 index 00000000000..701b6e2436e --- /dev/null +++ b/homeassistant/components/lcn/helpers.py @@ -0,0 +1,67 @@ +"""Helpers for LCN component.""" +import re + +import voluptuous as vol + +from homeassistant.const import CONF_NAME + +from .const import DEFAULT_NAME + +# Regex for address validation +PATTERN_ADDRESS = re.compile('^((?P\\w+)\\.)?s?(?P\\d+)' + '\\.(?Pm|g)?(?P\\d+)$') + + +def get_connection(connections, connection_id=None): + """Return the connection object from list.""" + if connection_id is None: + connection = connections[0] + else: + for connection in connections: + if connection.connection_id == connection_id: + break + else: + raise ValueError('Unknown connection_id.') + return connection + + +def has_unique_connection_names(connections): + """Validate that all connection names are unique. + + Use 'pchk' as default connection_name (or add a numeric suffix if + pchk' is already in use. + """ + for suffix, connection in enumerate(connections): + connection_name = connection.get(CONF_NAME) + if connection_name is None: + if suffix == 0: + connection[CONF_NAME] = DEFAULT_NAME + else: + connection[CONF_NAME] = '{}{:d}'.format(DEFAULT_NAME, suffix) + + schema = vol.Schema(vol.Unique()) + schema([connection.get(CONF_NAME) for connection in connections]) + return connections + + +def is_address(value): + """Validate the given address string. + + Examples for S000M005 at myhome: + myhome.s000.m005 + myhome.s0.m5 + myhome.0.5 ("m" is implicit if missing) + + Examples for s000g011 + myhome.0.g11 + myhome.s0.g11 + """ + matcher = PATTERN_ADDRESS.match(value) + if matcher: + is_group = (matcher.group('type') == 'g') + addr = (int(matcher.group('seg_id')), + int(matcher.group('id')), + is_group) + conn_id = matcher.group('conn_id') + return addr, conn_id + raise vol.error.Invalid('Not a valid address string.') diff --git a/homeassistant/components/lcn/light.py b/homeassistant/components/lcn/light.py index 49cdff5de49..28d85d6d45a 100644 --- a/homeassistant/components/lcn/light.py +++ b/homeassistant/components/lcn/light.py @@ -6,10 +6,11 @@ from homeassistant.components.light import ( Light) from homeassistant.const import CONF_ADDRESS -from . import LcnDevice, get_connection +from . import LcnDevice from .const import ( CONF_CONNECTIONS, CONF_DIMMABLE, CONF_OUTPUT, CONF_TRANSITION, DATA_LCN, OUTPUT_PORTS) +from .helpers import get_connection async def async_setup_platform( @@ -43,9 +44,9 @@ class LcnOutputLight(LcnDevice, Light): """Initialize the LCN light.""" super().__init__(config, address_connection) - self.output = self.pypck.lcn_defs.OutputPort[config[CONF_OUTPUT]] + self.output = pypck.lcn_defs.OutputPort[config[CONF_OUTPUT]] - self._transition = self.pypck.lcn_defs.time_to_ramp_value( + self._transition = pypck.lcn_defs.time_to_ramp_value( config[CONF_TRANSITION]) self.dimmable = config[CONF_DIMMABLE] @@ -86,7 +87,7 @@ class LcnOutputLight(LcnDevice, Light): else: percent = 100 if ATTR_TRANSITION in kwargs: - transition = self.pypck.lcn_defs.time_to_ramp_value( + transition = pypck.lcn_defs.time_to_ramp_value( kwargs[ATTR_TRANSITION] * 1000) else: transition = self._transition @@ -99,7 +100,7 @@ class LcnOutputLight(LcnDevice, Light): """Turn the entity off.""" self._is_on = False if ATTR_TRANSITION in kwargs: - transition = self.pypck.lcn_defs.time_to_ramp_value( + transition = pypck.lcn_defs.time_to_ramp_value( kwargs[ATTR_TRANSITION] * 1000) else: transition = self._transition @@ -111,7 +112,7 @@ class LcnOutputLight(LcnDevice, Light): def input_received(self, input_obj): """Set light state when LCN input object (command) is received.""" - if not isinstance(input_obj, self.pypck.inputs.ModStatusOutput) or \ + if not isinstance(input_obj, pypck.inputs.ModStatusOutput) or \ input_obj.get_output_id() != self.output.value: return @@ -130,7 +131,7 @@ class LcnRelayLight(LcnDevice, Light): """Initialize the LCN light.""" super().__init__(config, address_connection) - self.output = self.pypck.lcn_defs.RelayPort[config[CONF_OUTPUT]] + self.output = pypck.lcn_defs.RelayPort[config[CONF_OUTPUT]] self._is_on = None @@ -149,8 +150,8 @@ class LcnRelayLight(LcnDevice, Light): """Turn the entity on.""" self._is_on = True - states = [self.pypck.lcn_defs.RelayStateModifier.NOCHANGE] * 8 - states[self.output.value] = self.pypck.lcn_defs.RelayStateModifier.ON + states = [pypck.lcn_defs.RelayStateModifier.NOCHANGE] * 8 + states[self.output.value] = pypck.lcn_defs.RelayStateModifier.ON self.address_connection.control_relays(states) await self.async_update_ha_state() @@ -159,15 +160,15 @@ class LcnRelayLight(LcnDevice, Light): """Turn the entity off.""" self._is_on = False - states = [self.pypck.lcn_defs.RelayStateModifier.NOCHANGE] * 8 - states[self.output.value] = self.pypck.lcn_defs.RelayStateModifier.OFF + states = [pypck.lcn_defs.RelayStateModifier.NOCHANGE] * 8 + states[self.output.value] = pypck.lcn_defs.RelayStateModifier.OFF self.address_connection.control_relays(states) await self.async_update_ha_state() def input_received(self, input_obj): """Set light state when LCN input object (command) is received.""" - if not isinstance(input_obj, self.pypck.inputs.ModStatusRelays): + if not isinstance(input_obj, pypck.inputs.ModStatusRelays): return self._is_on = input_obj.get_state(self.output.value) diff --git a/homeassistant/components/lcn/manifest.json b/homeassistant/components/lcn/manifest.json index bbf2746c067..5f0d1052741 100644 --- a/homeassistant/components/lcn/manifest.json +++ b/homeassistant/components/lcn/manifest.json @@ -3,8 +3,10 @@ "name": "Lcn", "documentation": "https://www.home-assistant.io/components/lcn", "requirements": [ - "pypck==0.5.9" + "pypck==0.6.0" ], "dependencies": [], - "codeowners": [] + "codeowners": [ + "@alengwenus" + ] } diff --git a/homeassistant/components/lcn/sensor.py b/homeassistant/components/lcn/sensor.py index 38b17c80793..91d2b916cca 100755 --- a/homeassistant/components/lcn/sensor.py +++ b/homeassistant/components/lcn/sensor.py @@ -3,10 +3,11 @@ import pypck from homeassistant.const import CONF_ADDRESS, CONF_UNIT_OF_MEASUREMENT -from . import LcnDevice, get_connection +from . import LcnDevice from .const import ( CONF_CONNECTIONS, CONF_SOURCE, DATA_LCN, LED_PORTS, S0_INPUTS, SETPOINTS, THRESHOLDS, VARIABLES) +from .helpers import get_connection async def async_setup_platform(hass, hass_config, async_add_entities, @@ -41,8 +42,8 @@ class LcnVariableSensor(LcnDevice): """Initialize the LCN sensor.""" super().__init__(config, address_connection) - self.variable = self.pypck.lcn_defs.Var[config[CONF_SOURCE]] - self.unit = self.pypck.lcn_defs.VarUnit.parse( + self.variable = pypck.lcn_defs.Var[config[CONF_SOURCE]] + self.unit = pypck.lcn_defs.VarUnit.parse( config[CONF_UNIT_OF_MEASUREMENT]) self._value = None @@ -65,7 +66,7 @@ class LcnVariableSensor(LcnDevice): def input_received(self, input_obj): """Set sensor value when LCN input object (command) is received.""" - if not isinstance(input_obj, self.pypck.inputs.ModStatusVar) or \ + if not isinstance(input_obj, pypck.inputs.ModStatusVar) or \ input_obj.get_var() != self.variable: return @@ -81,9 +82,9 @@ class LcnLedLogicSensor(LcnDevice): super().__init__(config, address_connection) if config[CONF_SOURCE] in LED_PORTS: - self.source = self.pypck.lcn_defs.LedPort[config[CONF_SOURCE]] + self.source = pypck.lcn_defs.LedPort[config[CONF_SOURCE]] else: - self.source = self.pypck.lcn_defs.LogicOpPort[config[CONF_SOURCE]] + self.source = pypck.lcn_defs.LogicOpPort[config[CONF_SOURCE]] self._value = None @@ -101,13 +102,13 @@ class LcnLedLogicSensor(LcnDevice): def input_received(self, input_obj): """Set sensor value when LCN input object (command) is received.""" if not isinstance(input_obj, - self.pypck.inputs.ModStatusLedsAndLogicOps): + pypck.inputs.ModStatusLedsAndLogicOps): return - if self.source in self.pypck.lcn_defs.LedPort: + if self.source in pypck.lcn_defs.LedPort: self._value = input_obj.get_led_state( self.source.value).name.lower() - elif self.source in self.pypck.lcn_defs.LogicOpPort: + elif self.source in pypck.lcn_defs.LogicOpPort: self._value = input_obj.get_logic_op_state( self.source.value).name.lower() diff --git a/homeassistant/components/lcn/switch.py b/homeassistant/components/lcn/switch.py index e5a8484e271..1e86609c38c 100755 --- a/homeassistant/components/lcn/switch.py +++ b/homeassistant/components/lcn/switch.py @@ -4,8 +4,9 @@ import pypck from homeassistant.components.switch import SwitchDevice from homeassistant.const import CONF_ADDRESS -from . import LcnDevice, get_connection +from . import LcnDevice from .const import CONF_CONNECTIONS, CONF_OUTPUT, DATA_LCN, OUTPUT_PORTS +from .helpers import get_connection async def async_setup_platform(hass, hass_config, async_add_entities, @@ -39,7 +40,7 @@ class LcnOutputSwitch(LcnDevice, SwitchDevice): """Initialize the LCN switch.""" super().__init__(config, address_connection) - self.output = self.pypck.lcn_defs.OutputPort[config[CONF_OUTPUT]] + self.output = pypck.lcn_defs.OutputPort[config[CONF_OUTPUT]] self._is_on = None @@ -68,7 +69,7 @@ class LcnOutputSwitch(LcnDevice, SwitchDevice): def input_received(self, input_obj): """Set switch state when LCN input object (command) is received.""" - if not isinstance(input_obj, self.pypck.inputs.ModStatusOutput) or \ + if not isinstance(input_obj, pypck.inputs.ModStatusOutput) or \ input_obj.get_output_id() != self.output.value: return @@ -83,7 +84,7 @@ class LcnRelaySwitch(LcnDevice, SwitchDevice): """Initialize the LCN switch.""" super().__init__(config, address_connection) - self.output = self.pypck.lcn_defs.RelayPort[config[CONF_OUTPUT]] + self.output = pypck.lcn_defs.RelayPort[config[CONF_OUTPUT]] self._is_on = None @@ -102,8 +103,8 @@ class LcnRelaySwitch(LcnDevice, SwitchDevice): """Turn the entity on.""" self._is_on = True - states = [self.pypck.lcn_defs.RelayStateModifier.NOCHANGE] * 8 - states[self.output.value] = self.pypck.lcn_defs.RelayStateModifier.ON + states = [pypck.lcn_defs.RelayStateModifier.NOCHANGE] * 8 + states[self.output.value] = pypck.lcn_defs.RelayStateModifier.ON self.address_connection.control_relays(states) await self.async_update_ha_state() @@ -111,14 +112,14 @@ class LcnRelaySwitch(LcnDevice, SwitchDevice): """Turn the entity off.""" self._is_on = False - states = [self.pypck.lcn_defs.RelayStateModifier.NOCHANGE] * 8 - states[self.output.value] = self.pypck.lcn_defs.RelayStateModifier.OFF + states = [pypck.lcn_defs.RelayStateModifier.NOCHANGE] * 8 + states[self.output.value] = pypck.lcn_defs.RelayStateModifier.OFF self.address_connection.control_relays(states) await self.async_update_ha_state() def input_received(self, input_obj): """Set switch state when LCN input object (command) is received.""" - if not isinstance(input_obj, self.pypck.inputs.ModStatusRelays): + if not isinstance(input_obj, pypck.inputs.ModStatusRelays): return self._is_on = input_obj.get_state(self.output.value) diff --git a/requirements_all.txt b/requirements_all.txt index dbaa52d6baf..3eebc5530a7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1259,7 +1259,7 @@ pyowlet==1.0.2 pyowm==2.10.0 # homeassistant.components.lcn -pypck==0.5.9 +pypck==0.6.0 # homeassistant.components.pjlink pypjlink2==1.2.0 From f867b025e5f7aa245b01ffd6eda11bd880682fa7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20N=C3=B8rager=20S=C3=B8rensen?= <6843486+simse@users.noreply.github.com> Date: Sat, 25 May 2019 17:57:16 +0200 Subject: [PATCH 133/232] Update code owner for Xiaomi TV (#24102) * Update code owner * Update CODEOWNERS --- CODEOWNERS | 2 +- homeassistant/components/xiaomi_tv/manifest.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index a4e82847523..0207363102b 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -265,7 +265,7 @@ homeassistant/components/worldclock/* @fabaff homeassistant/components/xfinity/* @cisasteelersfan homeassistant/components/xiaomi_aqara/* @danielhiversen @syssi homeassistant/components/xiaomi_miio/* @rytilahti @syssi -homeassistant/components/xiaomi_tv/* @fattdev +homeassistant/components/xiaomi_tv/* @simse homeassistant/components/xmpp/* @fabaff @flowolf homeassistant/components/yamaha_musiccast/* @jalmeroth homeassistant/components/yeelight/* @rytilahti @zewelor diff --git a/homeassistant/components/xiaomi_tv/manifest.json b/homeassistant/components/xiaomi_tv/manifest.json index 221532c1c8d..26940a57c78 100644 --- a/homeassistant/components/xiaomi_tv/manifest.json +++ b/homeassistant/components/xiaomi_tv/manifest.json @@ -7,6 +7,6 @@ ], "dependencies": [], "codeowners": [ - "@fattdev" + "@simse" ] } From 39ba99005ab74c2dbf9e04631b15a1a5504d4764 Mon Sep 17 00:00:00 2001 From: Kevin Fronczak Date: Sat, 25 May 2019 11:58:44 -0400 Subject: [PATCH 134/232] Fix broken blink motion detection (#24097) --- homeassistant/components/blink/__init__.py | 12 +++++++++--- homeassistant/components/blink/manifest.json | 2 +- requirements_all.txt | 2 +- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/blink/__init__.py b/homeassistant/components/blink/__init__.py index 397ee097cae..74057c7b6bc 100644 --- a/homeassistant/components/blink/__init__.py +++ b/homeassistant/components/blink/__init__.py @@ -8,7 +8,7 @@ from homeassistant.helpers import ( from homeassistant.const import ( CONF_USERNAME, CONF_PASSWORD, CONF_NAME, CONF_SCAN_INTERVAL, CONF_BINARY_SENSORS, CONF_SENSORS, CONF_FILENAME, - CONF_MONITORED_CONDITIONS, TEMP_FAHRENHEIT) + CONF_MONITORED_CONDITIONS, CONF_MODE, CONF_OFFSET, TEMP_FAHRENHEIT) _LOGGER = logging.getLogger(__name__) @@ -41,7 +41,7 @@ BINARY_SENSORS = { SENSORS = { TYPE_TEMPERATURE: ['Temperature', TEMP_FAHRENHEIT, 'mdi:thermometer'], - TYPE_BATTERY: ['Battery', '%', 'mdi:battery-80'], + TYPE_BATTERY: ['Battery', '', 'mdi:battery-80'], TYPE_WIFI_STRENGTH: ['Wifi Signal', 'dBm', 'mdi:wifi-strength-2'], } @@ -75,6 +75,8 @@ CONFIG_SCHEMA = vol.Schema( vol.Optional(CONF_BINARY_SENSORS, default={}): BINARY_SENSOR_SCHEMA, vol.Optional(CONF_SENSORS, default={}): SENSOR_SCHEMA, + vol.Optional(CONF_OFFSET, default=1): int, + vol.Optional(CONF_MODE, default=''): cv.string, }) }, extra=vol.ALLOW_EXTRA) @@ -87,8 +89,12 @@ def setup(hass, config): username = conf[CONF_USERNAME] password = conf[CONF_PASSWORD] scan_interval = conf[CONF_SCAN_INTERVAL] + is_legacy = bool(conf[CONF_MODE] == 'legacy') + motion_interval = conf[CONF_OFFSET] hass.data[BLINK_DATA] = blinkpy.Blink(username=username, - password=password) + password=password, + motion_interval=motion_interval, + legacy_subdomain=is_legacy) hass.data[BLINK_DATA].refresh_rate = scan_interval.total_seconds() hass.data[BLINK_DATA].start() diff --git a/homeassistant/components/blink/manifest.json b/homeassistant/components/blink/manifest.json index 7be44f95a53..abce8a4a0d1 100644 --- a/homeassistant/components/blink/manifest.json +++ b/homeassistant/components/blink/manifest.json @@ -3,7 +3,7 @@ "name": "Blink", "documentation": "https://www.home-assistant.io/components/blink", "requirements": [ - "blinkpy==0.13.1" + "blinkpy==0.14.0" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 3eebc5530a7..5288c6c9001 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -243,7 +243,7 @@ bimmer_connected==0.5.3 bizkaibus==0.1.1 # homeassistant.components.blink -blinkpy==0.13.1 +blinkpy==0.14.0 # homeassistant.components.blinksticklight blinkstick==1.1.8 From 144b5300457b8845a20deec93f8a378e628d2553 Mon Sep 17 00:00:00 2001 From: Alex Bahm Date: Sat, 25 May 2019 13:07:23 -0700 Subject: [PATCH 135/232] Issue #23514 - fix invalid hue response (#23909) Based on the discoveries in issue #23514, the periodic lack of response from emulated hue was due to an invalid value (null) being returned. --- homeassistant/components/emulated_hue/hue_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index 44a9c6e53ef..632fdab12a4 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -453,7 +453,7 @@ def get_entity_state(config, entity): if cached_state is None: data[STATE_ON] = entity.state != STATE_OFF if data[STATE_ON]: - data[STATE_BRIGHTNESS] = entity.attributes.get(ATTR_BRIGHTNESS) + data[STATE_BRIGHTNESS] = entity.attributes.get(ATTR_BRIGHTNESS, 0) hue_sat = entity.attributes.get(ATTR_HS_COLOR, None) if hue_sat is not None: hue = hue_sat[0] From e6d7f6ed712f6c286e44ca5466e974652c86027b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 25 May 2019 13:34:53 -0700 Subject: [PATCH 136/232] Config entry device tracker (#24040) * Move zone helpers to zone root * Add config entry support to device tracker * Convert Geofency * Convert GPSLogger * Track unsub per entry * Convert locative * Migrate OwnTracks * Lint * location -> latitude, longitude props * Lint * lint * Fix test --- .../components/device_tracker/__init__.py | 37 +- .../components/device_tracker/config_entry.py | 114 +++++ .../components/device_tracker/legacy.py | 2 +- .../components/device_tracker/setup.py | 19 +- homeassistant/components/geofency/__init__.py | 10 +- .../components/geofency/device_tracker.py | 107 ++++- .../components/gpslogger/__init__.py | 6 +- .../components/gpslogger/device_tracker.py | 118 ++++- .../components/icloud/device_tracker.py | 15 +- homeassistant/components/locative/__init__.py | 5 + .../components/locative/device_tracker.py | 97 +++- .../components/owntracks/__init__.py | 28 +- .../components/owntracks/device_tracker.py | 433 +++++------------- .../components/owntracks/messages.py | 348 ++++++++++++++ homeassistant/components/zone/__init__.py | 40 +- homeassistant/components/zone/const.py | 2 + homeassistant/components/zone/zone.py | 49 +- tests/components/gpslogger/test_init.py | 2 +- .../owntracks/test_device_tracker.py | 23 +- tests/components/zone/test_init.py | 8 +- 20 files changed, 946 insertions(+), 517 deletions(-) create mode 100644 homeassistant/components/device_tracker/config_entry.py create mode 100644 homeassistant/components/owntracks/messages.py diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index 618ed163b9d..4c67e6fa65d 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -3,10 +3,8 @@ import asyncio import voluptuous as vol -from homeassistant.helpers.entity_component import EntityComponent from homeassistant.loader import bind_hass from homeassistant.components import group -from homeassistant.config import config_without_domain from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import GPSType, ConfigType, HomeAssistantType @@ -15,6 +13,9 @@ from homeassistant.helpers.event import async_track_utc_time_change from homeassistant.const import ATTR_GPS_ACCURACY, STATE_HOME from . import legacy, setup +from .config_entry import ( # noqa # pylint: disable=unused-import + async_setup_entry, async_unload_entry +) from .legacy import DeviceScanner # noqa # pylint: disable=unused-import from .const import ( ATTR_ATTRIBUTES, @@ -35,9 +36,7 @@ from .const import ( DEFAULT_CONSIDER_HOME, DEFAULT_TRACK_NEW, DOMAIN, - LOGGER, PLATFORM_TYPE_LEGACY, - SCAN_INTERVAL, SOURCE_TYPE_BLUETOOTH_LE, SOURCE_TYPE_BLUETOOTH, SOURCE_TYPE_GPS, @@ -113,36 +112,13 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType): """Set up the device tracker.""" tracker = await legacy.get_tracker(hass, config) - async def setup_entry_helper(entry): - """Set up a config entry.""" - platform = await setup.async_create_platform_type( - hass, config, entry.domain, entry) - - if platform is None: - return False - - await platform.async_setup_legacy(hass, tracker) - - return True - - hass.data[DOMAIN] = setup_entry_helper - component = EntityComponent( - LOGGER, DOMAIN, hass, SCAN_INTERVAL) - - legacy_platforms, entity_platforms = \ - await setup.async_extract_config(hass, config) + legacy_platforms = await setup.async_extract_config(hass, config) setup_tasks = [ legacy_platform.async_setup_legacy(hass, tracker) for legacy_platform in legacy_platforms ] - if entity_platforms: - setup_tasks.append(component.async_setup({ - **config_without_domain(config, DOMAIN), - DOMAIN: [platform.config for platform in entity_platforms] - })) - if setup_tasks: await asyncio.wait(setup_tasks) @@ -178,8 +154,3 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType): # restore await tracker.async_setup_tracked_device() return True - - -async def async_setup_entry(hass, entry): - """Set up an entry.""" - return await hass.data[DOMAIN](entry) diff --git a/homeassistant/components/device_tracker/config_entry.py b/homeassistant/components/device_tracker/config_entry.py new file mode 100644 index 00000000000..59f6c0c49c1 --- /dev/null +++ b/homeassistant/components/device_tracker/config_entry.py @@ -0,0 +1,114 @@ +"""Code to set up a device tracker platform using a config entry.""" +from typing import Optional + +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.const import ( + STATE_NOT_HOME, + STATE_HOME, + ATTR_GPS_ACCURACY, + ATTR_LATITUDE, + ATTR_LONGITUDE, + ATTR_BATTERY_LEVEL, +) +from homeassistant.components import zone + +from .const import ( + ATTR_SOURCE_TYPE, + DOMAIN, + LOGGER, +) + + +async def async_setup_entry(hass, entry): + """Set up an entry.""" + component = hass.data.get(DOMAIN) # type: Optional[EntityComponent] + + if component is None: + component = hass.data[DOMAIN] = EntityComponent( + LOGGER, DOMAIN, hass + ) + + return await component.async_setup_entry(entry) + + +async def async_unload_entry(hass, entry): + """Unload an entry.""" + return await hass.data[DOMAIN].async_unload_entry(entry) + + +class DeviceTrackerEntity(Entity): + """Represent a tracked device.""" + + @property + def battery_level(self): + """Return the battery level of the device. + + Percentage from 0-100. + """ + return None + + @property + def location_accuracy(self): + """Return the location accuracy of the device. + + Value in meters. + """ + return 0 + + @property + def location_name(self) -> str: + """Return a location name for the current location of the device.""" + return None + + @property + def latitude(self) -> float: + """Return latitude value of the device.""" + return NotImplementedError + + @property + def longitude(self) -> float: + """Return longitude value of the device.""" + return NotImplementedError + + @property + def source_type(self): + """Return the source type, eg gps or router, of the device.""" + raise NotImplementedError + + @property + def state(self): + """Return the state of the device.""" + if self.location_name: + return self.location_name + + if self.latitude is not None: + zone_state = zone.async_active_zone( + self.hass, self.latitude, self.longitude, + self.location_accuracy) + if zone_state is None: + state = STATE_NOT_HOME + elif zone_state.entity_id == zone.ENTITY_ID_HOME: + state = STATE_HOME + else: + state = zone_state.name + return state + + return None + + @property + def state_attributes(self): + """Return the device state attributes.""" + attr = { + ATTR_SOURCE_TYPE: self.source_type + } + + if self.latitude is not None: + attr[ATTR_LATITUDE] = self.latitude + attr[ATTR_LONGITUDE] = self.longitude + attr[ATTR_GPS_ACCURACY] = self.location_accuracy + + if self.battery_level: + attr[ATTR_BATTERY_LEVEL] = self.battery_level + + return attr diff --git a/homeassistant/components/device_tracker/legacy.py b/homeassistant/components/device_tracker/legacy.py index b27b5e20acf..1fdd8077728 100644 --- a/homeassistant/components/device_tracker/legacy.py +++ b/homeassistant/components/device_tracker/legacy.py @@ -10,7 +10,7 @@ from homeassistant.components import zone from homeassistant.components.group import ( ATTR_ADD_ENTITIES, ATTR_ENTITIES, ATTR_OBJECT_ID, ATTR_VISIBLE, DOMAIN as DOMAIN_GROUP, SERVICE_SET) -from homeassistant.components.zone.zone import async_active_zone +from homeassistant.components.zone import async_active_zone from homeassistant.config import load_yaml_config_file, async_log_exception from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/device_tracker/setup.py b/homeassistant/components/device_tracker/setup.py index b2a3b66a27c..a74f51c6638 100644 --- a/homeassistant/components/device_tracker/setup.py +++ b/homeassistant/components/device_tracker/setup.py @@ -20,7 +20,6 @@ from homeassistant.const import ( from .const import ( DOMAIN, - PLATFORM_TYPE_ENTITY, PLATFORM_TYPE_LEGACY, CONF_SCAN_INTERVAL, SCAN_INTERVAL, @@ -38,14 +37,7 @@ class DeviceTrackerPlatform: 'get_scanner', 'async_setup_scanner', 'setup_scanner', - # Small steps, initially just legacy setup supported. - 'async_setup_entry' ) - # ENTITY_PLATFORM_SETUP = ( - # 'setup_platform', - # 'async_setup_platform', - # 'async_setup_entry' - # ) name = attr.ib(type=str) platform = attr.ib(type=ModuleType) @@ -56,7 +48,6 @@ class DeviceTrackerPlatform: """Return platform type.""" for methods, platform_type in ( (self.LEGACY_SETUP, PLATFORM_TYPE_LEGACY), - # (self.ENTITY_PLATFORM_SETUP, PLATFORM_TYPE_ENTITY), ): for meth in methods: if hasattr(self.platform, meth): @@ -83,9 +74,6 @@ class DeviceTrackerPlatform: setup = await hass.async_add_job( self.platform.setup_scanner, hass, self.config, tracker.see, discovery_info) - elif hasattr(self.platform, 'async_setup_entry'): - setup = await self.platform.async_setup_entry( - hass, self.config, tracker.async_see) else: raise HomeAssistantError( "Invalid legacy device_tracker platform.") @@ -106,7 +94,6 @@ class DeviceTrackerPlatform: async def async_extract_config(hass, config): """Extract device tracker config and split between legacy and modern.""" legacy = [] - entity_platform = [] for platform in await asyncio.gather(*[ async_create_platform_type(hass, config, p_type, p_config) @@ -115,15 +102,13 @@ async def async_extract_config(hass, config): if platform is None: continue - if platform.type == PLATFORM_TYPE_ENTITY: - entity_platform.append(platform) - elif platform.type == PLATFORM_TYPE_LEGACY: + if platform.type == PLATFORM_TYPE_LEGACY: legacy.append(platform) else: raise ValueError("Unable to determine type for {}: {}".format( platform.name, platform.type)) - return (legacy, entity_platform) + return legacy async def async_create_platform_type(hass, config, p_type, p_config) \ diff --git a/homeassistant/components/geofency/__init__.py b/homeassistant/components/geofency/__init__.py index e5698b997a4..944879788de 100644 --- a/homeassistant/components/geofency/__init__.py +++ b/homeassistant/components/geofency/__init__.py @@ -63,7 +63,11 @@ async def async_setup(hass, hass_config): """Set up the Geofency component.""" config = hass_config.get(DOMAIN, {}) mobile_beacons = config.get(CONF_MOBILE_BEACONS, []) - hass.data[DOMAIN] = [slugify(beacon) for beacon in mobile_beacons] + hass.data[DOMAIN] = { + 'beacons': [slugify(beacon) for beacon in mobile_beacons], + 'devices': set(), + 'unsub_device_tracker': {} + } return True @@ -77,7 +81,7 @@ async def handle_webhook(hass, webhook_id, request): status=HTTP_UNPROCESSABLE_ENTITY ) - if _is_mobile_beacon(data, hass.data[DOMAIN]): + if _is_mobile_beacon(data, hass.data[DOMAIN]['beacons']): return _set_location(hass, data, None) if data['entry'] == LOCATION_ENTRY: location_name = data['name'] @@ -128,7 +132,7 @@ async def async_setup_entry(hass, entry): async def async_unload_entry(hass, entry): """Unload a config entry.""" hass.components.webhook.async_unregister(entry.data[CONF_WEBHOOK_ID]) - + hass.data[DOMAIN]['unsub_device_tracker'].pop(entry.entry_id)() await hass.config_entries.async_forward_entry_unload(entry, DEVICE_TRACKER) return True diff --git a/homeassistant/components/geofency/device_tracker.py b/homeassistant/components/geofency/device_tracker.py index abccf610f5e..e340272c966 100644 --- a/homeassistant/components/geofency/device_tracker.py +++ b/homeassistant/components/geofency/device_tracker.py @@ -1,35 +1,100 @@ """Support for the Geofency device tracker platform.""" import logging -from homeassistant.components.device_tracker import ( - DOMAIN as DEVICE_TRACKER_DOMAIN) +from homeassistant.core import callback +from homeassistant.components.device_tracker import SOURCE_TYPE_GPS +from homeassistant.components.device_tracker.config_entry import ( + DeviceTrackerEntity +) from homeassistant.helpers.dispatcher import async_dispatcher_connect -from . import DOMAIN as GEOFENCY_DOMAIN, TRACKER_UPDATE +from . import DOMAIN as GF_DOMAIN, TRACKER_UPDATE _LOGGER = logging.getLogger(__name__) -DATA_KEY = '{}.{}'.format(GEOFENCY_DOMAIN, DEVICE_TRACKER_DOMAIN) - -async def async_setup_entry(hass, entry, async_see): - """Configure a dispatcher connection based on a config entry.""" - async def _set_location(device, gps, location_name, attributes): +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up Geofency config entry.""" + @callback + def _receive_data(device, gps, location_name, attributes): """Fire HA event to set location.""" - await async_see( - dev_id=device, - gps=gps, - location_name=location_name, - attributes=attributes - ) + if device in hass.data[GF_DOMAIN]['devices']: + return + + hass.data[GF_DOMAIN]['devices'].add(device) + + async_add_entities([GeofencyEntity( + device, gps, location_name, attributes + )]) + + hass.data[GF_DOMAIN]['unsub_device_tracker'][config_entry.entry_id] = \ + async_dispatcher_connect(hass, TRACKER_UPDATE, _receive_data) - hass.data[DATA_KEY] = async_dispatcher_connect( - hass, TRACKER_UPDATE, _set_location - ) return True -async def async_unload_entry(hass, entry): - """Unload the config entry and remove the dispatcher connection.""" - hass.data[DATA_KEY]() - return True +class GeofencyEntity(DeviceTrackerEntity): + """Represent a tracked device.""" + + def __init__(self, device, gps, location_name, attributes): + """Set up Geofency entity.""" + self._attributes = attributes + self._name = device + self._location_name = location_name + self._gps = gps + self._unsub_dispatcher = None + + @property + def device_state_attributes(self): + """Return device specific attributes.""" + return self._attributes + + @property + def latitude(self): + """Return latitude value of the device.""" + return self._gps[0] + + @property + def longitude(self): + """Return longitude value of the device.""" + return self._gps[1] + + @property + def location_name(self): + """Return a location name for the current location of the device.""" + return self._location_name + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def source_type(self): + """Return the source type, eg gps or router, of the device.""" + return SOURCE_TYPE_GPS + + async def async_added_to_hass(self): + """Register state update callback.""" + self._unsub_dispatcher = async_dispatcher_connect( + self.hass, TRACKER_UPDATE, self._async_receive_data) + + async def async_will_remove_from_hass(self): + """Clean up after entity before removal.""" + self._unsub_dispatcher() + + @callback + def _async_receive_data(self, device, gps, location_name, attributes): + """Mark the device as seen.""" + if device != self.name: + return + + self._attributes.update(attributes) + self._location_name = location_name + self._gps = gps + self.async_write_ha_state() diff --git a/homeassistant/components/gpslogger/__init__.py b/homeassistant/components/gpslogger/__init__.py index 016de66e9fd..2123421334a 100644 --- a/homeassistant/components/gpslogger/__init__.py +++ b/homeassistant/components/gpslogger/__init__.py @@ -50,6 +50,10 @@ WEBHOOK_SCHEMA = vol.Schema({ async def async_setup(hass, hass_config): """Set up the GPSLogger component.""" + hass.data[DOMAIN] = { + 'devices': set(), + 'unsub_device_tracker': {}, + } return True @@ -98,7 +102,7 @@ async def async_setup_entry(hass, entry): async def async_unload_entry(hass, entry): """Unload a config entry.""" hass.components.webhook.async_unregister(entry.data[CONF_WEBHOOK_ID]) - + hass.data[DOMAIN]['unsub_device_tracker'].pop(entry.entry_id)() await hass.config_entries.async_forward_entry_unload(entry, DEVICE_TRACKER) return True diff --git a/homeassistant/components/gpslogger/device_tracker.py b/homeassistant/components/gpslogger/device_tracker.py index 67967821083..81a4fb3e7f8 100644 --- a/homeassistant/components/gpslogger/device_tracker.py +++ b/homeassistant/components/gpslogger/device_tracker.py @@ -1,37 +1,109 @@ """Support for the GPSLogger device tracking.""" import logging -from homeassistant.components.device_tracker import ( - DOMAIN as DEVICE_TRACKER_DOMAIN) +from homeassistant.core import callback +from homeassistant.components.device_tracker import SOURCE_TYPE_GPS +from homeassistant.components.device_tracker.config_entry import ( + DeviceTrackerEntity +) from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.typing import HomeAssistantType -from . import DOMAIN as GPSLOGGER_DOMAIN, TRACKER_UPDATE +from . import DOMAIN as GPL_DOMAIN, TRACKER_UPDATE _LOGGER = logging.getLogger(__name__) -DATA_KEY = '{}.{}'.format(GPSLOGGER_DOMAIN, DEVICE_TRACKER_DOMAIN) - -async def async_setup_entry(hass: HomeAssistantType, entry, async_see): +async def async_setup_entry(hass: HomeAssistantType, entry, + async_add_entities): """Configure a dispatcher connection based on a config entry.""" - async def _set_location(device, gps_location, battery, accuracy, attrs): - """Fire HA event to set location.""" - await async_see( - dev_id=device, - gps=gps_location, - battery=battery, - gps_accuracy=accuracy, - attributes=attrs - ) + @callback + def _receive_data(device, gps, battery, accuracy, attrs): + """Receive set location.""" + if device in hass.data[GPL_DOMAIN]['devices']: + return - hass.data[DATA_KEY] = async_dispatcher_connect( - hass, TRACKER_UPDATE, _set_location - ) - return True + hass.data[GPL_DOMAIN]['devices'].add(device) + + async_add_entities([GPSLoggerEntity( + device, gps, battery, accuracy, attrs + )]) + + hass.data[GPL_DOMAIN]['unsub_device_tracker'][entry.entry_id] = \ + async_dispatcher_connect(hass, TRACKER_UPDATE, _receive_data) -async def async_unload_entry(hass: HomeAssistantType, entry): - """Unload the config entry and remove the dispatcher connection.""" - hass.data[DATA_KEY]() - return True +class GPSLoggerEntity(DeviceTrackerEntity): + """Represent a tracked device.""" + + def __init__( + self, device, location, battery, accuracy, attributes): + """Set up Geofency entity.""" + self._accuracy = accuracy + self._attributes = attributes + self._name = device + self._battery = battery + self._location = location + self._unsub_dispatcher = None + + @property + def battery_level(self): + """Return battery value of the device.""" + return self._battery + + @property + def device_state_attributes(self): + """Return device specific attributes.""" + return self._attributes + + @property + def latitude(self): + """Return latitude value of the device.""" + return self._location[0] + + @property + def longitude(self): + """Return longitude value of the device.""" + return self._location[1] + + @property + def location_accuracy(self): + """Return the gps accuracy of the device.""" + return self._accuracy + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def source_type(self): + """Return the source type, eg gps or router, of the device.""" + return SOURCE_TYPE_GPS + + async def async_added_to_hass(self): + """Register state update callback.""" + self._unsub_dispatcher = async_dispatcher_connect( + self.hass, TRACKER_UPDATE, self._async_receive_data) + + async def async_will_remove_from_hass(self): + """Clean up after entity before removal.""" + self._unsub_dispatcher() + + @callback + def _async_receive_data(self, device, location, battery, accuracy, + attributes): + """Mark the device as seen.""" + if device != self.name: + return + + self._location = location + self._battery = battery + self._accuracy = accuracy + self._attributes.update(attributes) + self.async_write_ha_state() diff --git a/homeassistant/components/icloud/device_tracker.py b/homeassistant/components/icloud/device_tracker.py index 573da5fce63..89de6e57f6e 100644 --- a/homeassistant/components/icloud/device_tracker.py +++ b/homeassistant/components/icloud/device_tracker.py @@ -10,12 +10,13 @@ from homeassistant.components.device_tracker import PLATFORM_SCHEMA from homeassistant.components.device_tracker.const import ( DOMAIN, ATTR_ATTRIBUTES, ENTITY_ID_FORMAT) from homeassistant.components.device_tracker.legacy import DeviceScanner -from homeassistant.components.zone.zone import active_zone +from homeassistant.components.zone import async_active_zone from homeassistant.helpers.event import track_utc_time_change import homeassistant.helpers.config_validation as cv from homeassistant.util import slugify import homeassistant.util.dt as dt_util from homeassistant.util.location import distance +from homeassistant.util.async_ import run_callback_threadsafe _LOGGER = logging.getLogger(__name__) @@ -330,7 +331,10 @@ class Icloud(DeviceScanner): def determine_interval(self, devicename, latitude, longitude, battery): """Calculate new interval.""" - currentzone = active_zone(self.hass, latitude, longitude) + currentzone = run_callback_threadsafe( + self.hass.loop, + async_active_zone, self.hass, latitude, longitude + ).result() if ((currentzone is not None and currentzone == self._overridestates.get(devicename)) or @@ -472,10 +476,13 @@ class Icloud(DeviceScanner): devicestate = self.hass.states.get(devid) if interval is not None: if devicestate is not None: - self._overridestates[device] = active_zone( + self._overridestates[device] = run_callback_threadsafe( + self.hass.loop, + async_active_zone, self.hass, float(devicestate.attributes.get('latitude', 0)), - float(devicestate.attributes.get('longitude', 0))) + float(devicestate.attributes.get('longitude', 0)) + ).result() if self._overridestates[device] is None: self._overridestates[device] = 'away' self._intervals[device] = interval diff --git a/homeassistant/components/locative/__init__.py b/homeassistant/components/locative/__init__.py index 66f917e5729..49502186d8e 100644 --- a/homeassistant/components/locative/__init__.py +++ b/homeassistant/components/locative/__init__.py @@ -49,6 +49,10 @@ WEBHOOK_SCHEMA = vol.All( async def async_setup(hass, hass_config): """Set up the Locative component.""" + hass.data[DOMAIN] = { + 'devices': set(), + 'unsub_device_tracker': {}, + } return True @@ -139,6 +143,7 @@ async def async_setup_entry(hass, entry): async def async_unload_entry(hass, entry): """Unload a config entry.""" hass.components.webhook.async_unregister(entry.data[CONF_WEBHOOK_ID]) + hass.data[DOMAIN]['unsub_device_tracker'].pop(entry.entry_id)() await hass.config_entries.async_forward_entry_unload(entry, DEVICE_TRACKER) return True diff --git a/homeassistant/components/locative/device_tracker.py b/homeassistant/components/locative/device_tracker.py index 1e16bde58ad..6f86519c47c 100644 --- a/homeassistant/components/locative/device_tracker.py +++ b/homeassistant/components/locative/device_tracker.py @@ -1,35 +1,90 @@ """Support for the Locative platform.""" import logging -from homeassistant.components.device_tracker import ( - DOMAIN as DEVICE_TRACKER_DOMAIN) +from homeassistant.core import callback +from homeassistant.components.device_tracker import SOURCE_TYPE_GPS +from homeassistant.components.device_tracker.config_entry import ( + DeviceTrackerEntity +) from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.util import slugify -from . import DOMAIN as LOCATIVE_DOMAIN, TRACKER_UPDATE +from . import DOMAIN as LT_DOMAIN, TRACKER_UPDATE _LOGGER = logging.getLogger(__name__) -DATA_KEY = '{}.{}'.format(LOCATIVE_DOMAIN, DEVICE_TRACKER_DOMAIN) - -async def async_setup_entry(hass, entry, async_see): +async def async_setup_entry(hass, entry, async_add_entities): """Configure a dispatcher connection based on a config entry.""" - async def _set_location(device, gps_location, location_name): - """Fire HA event to set location.""" - await async_see( - dev_id=slugify(device), - gps=gps_location, - location_name=location_name - ) + @callback + def _receive_data(device, location, location_name): + """Receive set location.""" + if device in hass.data[LT_DOMAIN]['devices']: + return + + hass.data[LT_DOMAIN]['devices'].add(device) + + async_add_entities([LocativeEntity( + device, location, location_name + )]) + + hass.data[LT_DOMAIN]['unsub_device_tracker'][entry.entry_id] = \ + async_dispatcher_connect(hass, TRACKER_UPDATE, _receive_data) - hass.data[DATA_KEY] = async_dispatcher_connect( - hass, TRACKER_UPDATE, _set_location - ) return True -async def async_unload_entry(hass, entry): - """Unload the config entry and remove the dispatcher connection.""" - hass.data[DATA_KEY]() - return True +class LocativeEntity(DeviceTrackerEntity): + """Represent a tracked device.""" + + def __init__(self, device, location, location_name): + """Set up Locative entity.""" + self._name = device + self._location = location + self._location_name = location_name + self._unsub_dispatcher = None + + @property + def latitude(self): + """Return latitude value of the device.""" + return self._location[0] + + @property + def longitude(self): + """Return longitude value of the device.""" + return self._location[1] + + @property + def location_name(self): + """Return a location name for the current location of the device.""" + return self._location_name + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def source_type(self): + """Return the source type, eg gps or router, of the device.""" + return SOURCE_TYPE_GPS + + async def async_added_to_hass(self): + """Register state update callback.""" + self._unsub_dispatcher = async_dispatcher_connect( + self.hass, TRACKER_UPDATE, self._async_receive_data) + + async def async_will_remove_from_hass(self): + """Clean up after entity before removal.""" + self._unsub_dispatcher() + + @callback + def _async_receive_data(self, device, location, location_name): + """Update device data.""" + self._location_name = location_name + self._location = location + self.async_write_ha_state() diff --git a/homeassistant/components/owntracks/__init__.py b/homeassistant/components/owntracks/__init__.py index 979f3829454..a4df4303fa8 100644 --- a/homeassistant/components/owntracks/__init__.py +++ b/homeassistant/components/owntracks/__init__.py @@ -15,6 +15,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.setup import async_when_setup from .config_flow import CONF_SECRET +from .messages import async_handle_message _LOGGER = logging.getLogger(__name__) @@ -50,7 +51,9 @@ CONFIG_SCHEMA = vol.Schema({ async def async_setup(hass, config): """Initialize OwnTracks component.""" hass.data[DOMAIN] = { - 'config': config[DOMAIN] + 'config': config[DOMAIN], + 'devices': {}, + 'unsub': None, } if not hass.config_entries.async_entries(DOMAIN): hass.async_create_task(hass.config_entries.flow.async_init( @@ -88,6 +91,10 @@ async def async_setup_entry(hass, entry): hass.async_create_task(hass.config_entries.async_forward_entry_setup( entry, 'device_tracker')) + hass.data[DOMAIN]['unsub'] = \ + hass.helpers.dispatcher.async_dispatcher_connect( + DOMAIN, async_handle_message) + return True @@ -96,6 +103,8 @@ async def async_unload_entry(hass, entry): hass.components.webhook.async_unregister(entry.data[CONF_WEBHOOK_ID]) await hass.config_entries.async_forward_entry_unload( entry, 'device_tracker') + hass.data[DOMAIN]['unsub']() + return True @@ -213,11 +222,13 @@ class OwnTracksContext: return True - async def async_see(self, **data): + @callback + def async_see(self, **data): """Send a see message to the device tracker.""" raise NotImplementedError - async def async_see_beacons(self, hass, dev_id, kwargs_param): + @callback + def async_see_beacons(self, hass, dev_id, kwargs_param): """Set active beacons to the current location.""" kwargs = kwargs_param.copy() @@ -231,8 +242,13 @@ class OwnTracksContext: acc = device_tracker_state.attributes.get("gps_accuracy") lat = device_tracker_state.attributes.get("latitude") lon = device_tracker_state.attributes.get("longitude") - kwargs['gps_accuracy'] = acc - kwargs['gps'] = (lat, lon) + + if lat is not None and lon is not None: + kwargs['gps'] = (lat, lon) + kwargs['gps_accuracy'] = acc + else: + kwargs['gps'] = None + kwargs['gps_accuracy'] = None # the battery state applies to the tracking device, not the beacon # kwargs location is the beacon's configured lat/lon @@ -240,4 +256,4 @@ class OwnTracksContext: for beacon in self.mobile_beacons_active[dev_id]: kwargs['dev_id'] = "{}_{}".format(BEACON_DEV_ID, beacon) kwargs['host_name'] = beacon - await self.async_see(**kwargs) + self.async_see(**kwargs) diff --git a/homeassistant/components/owntracks/device_tracker.py b/homeassistant/components/owntracks/device_tracker.py index 999e883be19..fb9fedf26fa 100644 --- a/homeassistant/components/owntracks/device_tracker.py +++ b/homeassistant/components/owntracks/device_tracker.py @@ -1,351 +1,142 @@ """Device tracker platform that adds support for OwnTracks over MQTT.""" -import json import logging -from homeassistant.components import zone as zone_comp -from homeassistant.components.device_tracker import ( - ATTR_SOURCE_TYPE, SOURCE_TYPE_BLUETOOTH_LE, SOURCE_TYPE_GPS) -from homeassistant.const import STATE_HOME -from homeassistant.util import decorator, slugify - +from homeassistant.core import callback +from homeassistant.components.device_tracker.const import ENTITY_ID_FORMAT +from homeassistant.components.device_tracker.config_entry import ( + DeviceTrackerEntity +) from . import DOMAIN as OT_DOMAIN _LOGGER = logging.getLogger(__name__) -HANDLERS = decorator.Registry() - -async def async_setup_entry(hass, entry, async_see): +async def async_setup_entry(hass, entry, async_add_entities): """Set up OwnTracks based off an entry.""" - hass.data[OT_DOMAIN]['context'].async_see = async_see - hass.helpers.dispatcher.async_dispatcher_connect( - OT_DOMAIN, async_handle_message) + @callback + def _receive_data(dev_id, host_name, gps, attributes, gps_accuracy=None, + battery=None, source_type=None, location_name=None): + """Receive set location.""" + device = hass.data[OT_DOMAIN]['devices'].get(dev_id) + + if device is not None: + device.update_data( + host_name=host_name, + gps=gps, + attributes=attributes, + gps_accuracy=gps_accuracy, + battery=battery, + source_type=source_type, + location_name=location_name, + ) + return + + device = hass.data[OT_DOMAIN]['devices'][dev_id] = OwnTracksEntity( + dev_id=dev_id, + host_name=host_name, + gps=gps, + attributes=attributes, + gps_accuracy=gps_accuracy, + battery=battery, + source_type=source_type, + location_name=location_name, + ) + async_add_entities([device]) + + hass.data[OT_DOMAIN]['context'].async_see = _receive_data return True -def get_cipher(): - """Return decryption function and length of key. +class OwnTracksEntity(DeviceTrackerEntity): + """Represent a tracked device.""" - Async friendly. - """ - from nacl.secret import SecretBox - from nacl.encoding import Base64Encoder + def __init__(self, dev_id, host_name, gps, attributes, gps_accuracy, + battery, source_type, location_name): + """Set up OwnTracks entity.""" + self._dev_id = dev_id + self._host_name = host_name + self._gps = gps + self._gps_accuracy = gps_accuracy + self._location_name = location_name + self._attributes = attributes + self._battery = battery + self._source_type = source_type + self.entity_id = ENTITY_ID_FORMAT.format(dev_id) - def decrypt(ciphertext, key): - """Decrypt ciphertext using key.""" - return SecretBox(key).decrypt(ciphertext, encoder=Base64Encoder) - return (SecretBox.KEY_SIZE, decrypt) + @property + def unique_id(self): + """Return the unique ID.""" + return self._dev_id + @property + def battery_level(self): + """Return the battery level of the device.""" + return self._battery -def _parse_topic(topic, subscribe_topic): - """Parse an MQTT topic {sub_topic}/user/dev, return (user, dev) tuple. + @property + def device_state_attributes(self): + """Return device specific attributes.""" + return self._attributes - Async friendly. - """ - subscription = subscribe_topic.split('/') - try: - user_index = subscription.index('#') - except ValueError: - _LOGGER.error("Can't parse subscription topic: '%s'", subscribe_topic) - raise + @property + def location_accuracy(self): + """Return the gps accuracy of the device.""" + return self._gps_accuracy - topic_list = topic.split('/') - try: - user, device = topic_list[user_index], topic_list[user_index + 1] - except IndexError: - _LOGGER.error("Can't parse topic: '%s'", topic) - raise + @property + def latitude(self): + """Return latitude value of the device.""" + if self._gps is not None: + return self._gps[0] - return user, device - - -def _parse_see_args(message, subscribe_topic): - """Parse the OwnTracks location parameters, into the format see expects. - - Async friendly. - """ - user, device = _parse_topic(message['topic'], subscribe_topic) - dev_id = slugify('{}_{}'.format(user, device)) - kwargs = { - 'dev_id': dev_id, - 'host_name': user, - 'gps': (message['lat'], message['lon']), - 'attributes': {} - } - if 'acc' in message: - kwargs['gps_accuracy'] = message['acc'] - if 'batt' in message: - kwargs['battery'] = message['batt'] - if 'vel' in message: - kwargs['attributes']['velocity'] = message['vel'] - if 'tid' in message: - kwargs['attributes']['tid'] = message['tid'] - if 'addr' in message: - kwargs['attributes']['address'] = message['addr'] - if 'cog' in message: - kwargs['attributes']['course'] = message['cog'] - if 't' in message: - if message['t'] == 'c': - kwargs['attributes'][ATTR_SOURCE_TYPE] = SOURCE_TYPE_GPS - if message['t'] == 'b': - kwargs['attributes'][ATTR_SOURCE_TYPE] = SOURCE_TYPE_BLUETOOTH_LE - - return dev_id, kwargs - - -def _set_gps_from_zone(kwargs, location, zone): - """Set the see parameters from the zone parameters. - - Async friendly. - """ - if zone is not None: - kwargs['gps'] = ( - zone.attributes['latitude'], - zone.attributes['longitude']) - kwargs['gps_accuracy'] = zone.attributes['radius'] - kwargs['location_name'] = location - return kwargs - - -def _decrypt_payload(secret, topic, ciphertext): - """Decrypt encrypted payload.""" - try: - keylen, decrypt = get_cipher() - except OSError: - _LOGGER.warning( - "Ignoring encrypted payload because libsodium not installed") return None - if isinstance(secret, dict): - key = secret.get(topic) - else: - key = secret + @property + def longitude(self): + """Return longitude value of the device.""" + if self._gps is not None: + return self._gps[1] - if key is None: - _LOGGER.warning( - "Ignoring encrypted payload because no decryption key known " - "for topic %s", topic) return None - key = key.encode("utf-8") - key = key[:keylen] - key = key.ljust(keylen, b'\0') + @property + def location_name(self): + """Return a location name for the current location of the device.""" + return self._location_name - try: - message = decrypt(ciphertext, key) - message = message.decode("utf-8") - _LOGGER.debug("Decrypted payload: %s", message) - return message - except ValueError: - _LOGGER.warning( - "Ignoring encrypted payload because unable to decrypt using " - "key for topic %s", topic) - return None + @property + def name(self): + """Return the name of the device.""" + return self._host_name + @property + def should_poll(self): + """No polling needed.""" + return False -@HANDLERS.register('location') -async def async_handle_location_message(hass, context, message): - """Handle a location message.""" - if not context.async_valid_accuracy(message): - return + @property + def source_type(self): + """Return the source type, eg gps or router, of the device.""" + return self._source_type - if context.events_only: - _LOGGER.debug("Location update ignored due to events_only setting") - return + @property + def device_info(self): + """Return the device info.""" + return { + 'name': self._host_name, + 'identifiers': {(OT_DOMAIN, self._dev_id)}, + } - dev_id, kwargs = _parse_see_args(message, context.mqtt_topic) + @callback + def update_data(self, host_name, gps, attributes, gps_accuracy, + battery, source_type, location_name): + """Mark the device as seen.""" + self._host_name = host_name + self._gps = gps + self._gps_accuracy = gps_accuracy + self._location_name = location_name + self._attributes = attributes + self._battery = battery + self._source_type = source_type - if context.regions_entered[dev_id]: - _LOGGER.debug( - "Location update ignored, inside region %s", - context.regions_entered[-1]) - return - - await context.async_see(**kwargs) - await context.async_see_beacons(hass, dev_id, kwargs) - - -async def _async_transition_message_enter(hass, context, message, location): - """Execute enter event.""" - zone = hass.states.get("zone.{}".format(slugify(location))) - dev_id, kwargs = _parse_see_args(message, context.mqtt_topic) - - if zone is None and message.get('t') == 'b': - # Not a HA zone, and a beacon so mobile beacon. - # kwargs will contain the lat/lon of the beacon - # which is not where the beacon actually is - # and is probably set to 0/0 - beacons = context.mobile_beacons_active[dev_id] - if location not in beacons: - beacons.add(location) - _LOGGER.info("Added beacon %s", location) - await context.async_see_beacons(hass, dev_id, kwargs) - else: - # Normal region - regions = context.regions_entered[dev_id] - if location not in regions: - regions.append(location) - _LOGGER.info("Enter region %s", location) - _set_gps_from_zone(kwargs, location, zone) - await context.async_see(**kwargs) - await context.async_see_beacons(hass, dev_id, kwargs) - - -async def _async_transition_message_leave(hass, context, message, location): - """Execute leave event.""" - dev_id, kwargs = _parse_see_args(message, context.mqtt_topic) - regions = context.regions_entered[dev_id] - - if location in regions: - regions.remove(location) - - beacons = context.mobile_beacons_active[dev_id] - if location in beacons: - beacons.remove(location) - _LOGGER.info("Remove beacon %s", location) - await context.async_see_beacons(hass, dev_id, kwargs) - else: - new_region = regions[-1] if regions else None - if new_region: - # Exit to previous region - zone = hass.states.get( - "zone.{}".format(slugify(new_region))) - _set_gps_from_zone(kwargs, new_region, zone) - _LOGGER.info("Exit to %s", new_region) - await context.async_see(**kwargs) - await context.async_see_beacons(hass, dev_id, kwargs) - return - - _LOGGER.info("Exit to GPS") - - # Check for GPS accuracy - if context.async_valid_accuracy(message): - await context.async_see(**kwargs) - await context.async_see_beacons(hass, dev_id, kwargs) - - -@HANDLERS.register('transition') -async def async_handle_transition_message(hass, context, message): - """Handle a transition message.""" - if message.get('desc') is None: - _LOGGER.error( - "Location missing from `Entering/Leaving` message - " - "please turn `Share` on in OwnTracks app") - return - # OwnTracks uses - at the start of a beacon zone - # to switch on 'hold mode' - ignore this - location = message['desc'].lstrip("-") - - # Create a layer of indirection for Owntracks instances that may name - # regions differently than their HA names - if location in context.region_mapping: - location = context.region_mapping[location] - - if location.lower() == 'home': - location = STATE_HOME - - if message['event'] == 'enter': - await _async_transition_message_enter( - hass, context, message, location) - elif message['event'] == 'leave': - await _async_transition_message_leave( - hass, context, message, location) - else: - _LOGGER.error( - "Misformatted mqtt msgs, _type=transition, event=%s", - message['event']) - - -async def async_handle_waypoint(hass, name_base, waypoint): - """Handle a waypoint.""" - name = waypoint['desc'] - pretty_name = '{} - {}'.format(name_base, name) - lat = waypoint['lat'] - lon = waypoint['lon'] - rad = waypoint['rad'] - - # check zone exists - entity_id = zone_comp.ENTITY_ID_FORMAT.format(slugify(pretty_name)) - - # Check if state already exists - if hass.states.get(entity_id) is not None: - return - - zone = zone_comp.Zone(hass, pretty_name, lat, lon, rad, - zone_comp.ICON_IMPORT, False) - zone.entity_id = entity_id - await zone.async_update_ha_state() - - -@HANDLERS.register('waypoint') -@HANDLERS.register('waypoints') -async def async_handle_waypoints_message(hass, context, message): - """Handle a waypoints message.""" - if not context.import_waypoints: - return - - if context.waypoint_whitelist is not None: - user = _parse_topic(message['topic'], context.mqtt_topic)[0] - - if user not in context.waypoint_whitelist: - return - - if 'waypoints' in message: - wayps = message['waypoints'] - else: - wayps = [message] - - _LOGGER.info("Got %d waypoints from %s", len(wayps), message['topic']) - - name_base = ' '.join(_parse_topic(message['topic'], context.mqtt_topic)) - - for wayp in wayps: - await async_handle_waypoint(hass, name_base, wayp) - - -@HANDLERS.register('encrypted') -async def async_handle_encrypted_message(hass, context, message): - """Handle an encrypted message.""" - if 'topic' not in message and isinstance(context.secret, dict): - _LOGGER.error("You cannot set per topic secrets when using HTTP") - return - - plaintext_payload = _decrypt_payload(context.secret, message.get('topic'), - message['data']) - - if plaintext_payload is None: - return - - decrypted = json.loads(plaintext_payload) - if 'topic' in message and 'topic' not in decrypted: - decrypted['topic'] = message['topic'] - - await async_handle_message(hass, context, decrypted) - - -@HANDLERS.register('lwt') -@HANDLERS.register('configuration') -@HANDLERS.register('beacon') -@HANDLERS.register('cmd') -@HANDLERS.register('steps') -@HANDLERS.register('card') -async def async_handle_not_impl_msg(hass, context, message): - """Handle valid but not implemented message types.""" - _LOGGER.debug('Not handling %s message: %s', message.get("_type"), message) - - -async def async_handle_unsupported_msg(hass, context, message): - """Handle an unsupported or invalid message type.""" - _LOGGER.warning('Received unsupported message type: %s.', - message.get('_type')) - - -async def async_handle_message(hass, context, message): - """Handle an OwnTracks message.""" - msgtype = message.get('_type') - - _LOGGER.debug("Received %s", message) - - handler = HANDLERS.get(msgtype, async_handle_unsupported_msg) - - await handler(hass, context, message) + self.async_write_ha_state() diff --git a/homeassistant/components/owntracks/messages.py b/homeassistant/components/owntracks/messages.py new file mode 100644 index 00000000000..7eac2148013 --- /dev/null +++ b/homeassistant/components/owntracks/messages.py @@ -0,0 +1,348 @@ +"""OwnTracks Message handlers.""" +import json +import logging + +from homeassistant.components import zone as zone_comp +from homeassistant.components.device_tracker import ( + SOURCE_TYPE_GPS, SOURCE_TYPE_BLUETOOTH_LE +) + +from homeassistant.const import STATE_HOME +from homeassistant.util import decorator, slugify + + +_LOGGER = logging.getLogger(__name__) + +HANDLERS = decorator.Registry() + + +def get_cipher(): + """Return decryption function and length of key. + + Async friendly. + """ + from nacl.secret import SecretBox + from nacl.encoding import Base64Encoder + + def decrypt(ciphertext, key): + """Decrypt ciphertext using key.""" + return SecretBox(key).decrypt(ciphertext, encoder=Base64Encoder) + return (SecretBox.KEY_SIZE, decrypt) + + +def _parse_topic(topic, subscribe_topic): + """Parse an MQTT topic {sub_topic}/user/dev, return (user, dev) tuple. + + Async friendly. + """ + subscription = subscribe_topic.split('/') + try: + user_index = subscription.index('#') + except ValueError: + _LOGGER.error("Can't parse subscription topic: '%s'", subscribe_topic) + raise + + topic_list = topic.split('/') + try: + user, device = topic_list[user_index], topic_list[user_index + 1] + except IndexError: + _LOGGER.error("Can't parse topic: '%s'", topic) + raise + + return user, device + + +def _parse_see_args(message, subscribe_topic): + """Parse the OwnTracks location parameters, into the format see expects. + + Async friendly. + """ + user, device = _parse_topic(message['topic'], subscribe_topic) + dev_id = slugify('{}_{}'.format(user, device)) + kwargs = { + 'dev_id': dev_id, + 'host_name': user, + 'attributes': {} + } + if message['lat'] is not None and message['lon'] is not None: + kwargs['gps'] = (message['lat'], message['lon']) + else: + kwargs['gps'] = None + + if 'acc' in message: + kwargs['gps_accuracy'] = message['acc'] + if 'batt' in message: + kwargs['battery'] = message['batt'] + if 'vel' in message: + kwargs['attributes']['velocity'] = message['vel'] + if 'tid' in message: + kwargs['attributes']['tid'] = message['tid'] + if 'addr' in message: + kwargs['attributes']['address'] = message['addr'] + if 'cog' in message: + kwargs['attributes']['course'] = message['cog'] + if 't' in message: + if message['t'] in ('c', 'u'): + kwargs['source_type'] = SOURCE_TYPE_GPS + if message['t'] == 'b': + kwargs['source_type'] = SOURCE_TYPE_BLUETOOTH_LE + + return dev_id, kwargs + + +def _set_gps_from_zone(kwargs, location, zone): + """Set the see parameters from the zone parameters. + + Async friendly. + """ + if zone is not None: + kwargs['gps'] = ( + zone.attributes['latitude'], + zone.attributes['longitude']) + kwargs['gps_accuracy'] = zone.attributes['radius'] + kwargs['location_name'] = location + return kwargs + + +def _decrypt_payload(secret, topic, ciphertext): + """Decrypt encrypted payload.""" + try: + keylen, decrypt = get_cipher() + except OSError: + _LOGGER.warning( + "Ignoring encrypted payload because libsodium not installed") + return None + + if isinstance(secret, dict): + key = secret.get(topic) + else: + key = secret + + if key is None: + _LOGGER.warning( + "Ignoring encrypted payload because no decryption key known " + "for topic %s", topic) + return None + + key = key.encode("utf-8") + key = key[:keylen] + key = key.ljust(keylen, b'\0') + + try: + message = decrypt(ciphertext, key) + message = message.decode("utf-8") + _LOGGER.debug("Decrypted payload: %s", message) + return message + except ValueError: + _LOGGER.warning( + "Ignoring encrypted payload because unable to decrypt using " + "key for topic %s", topic) + return None + + +@HANDLERS.register('location') +async def async_handle_location_message(hass, context, message): + """Handle a location message.""" + if not context.async_valid_accuracy(message): + return + + if context.events_only: + _LOGGER.debug("Location update ignored due to events_only setting") + return + + dev_id, kwargs = _parse_see_args(message, context.mqtt_topic) + + if context.regions_entered[dev_id]: + _LOGGER.debug( + "Location update ignored, inside region %s", + context.regions_entered[-1]) + return + + context.async_see(**kwargs) + context.async_see_beacons(hass, dev_id, kwargs) + + +async def _async_transition_message_enter(hass, context, message, location): + """Execute enter event.""" + zone = hass.states.get("zone.{}".format(slugify(location))) + dev_id, kwargs = _parse_see_args(message, context.mqtt_topic) + + if zone is None and message.get('t') == 'b': + # Not a HA zone, and a beacon so mobile beacon. + # kwargs will contain the lat/lon of the beacon + # which is not where the beacon actually is + # and is probably set to 0/0 + beacons = context.mobile_beacons_active[dev_id] + if location not in beacons: + beacons.add(location) + _LOGGER.info("Added beacon %s", location) + context.async_see_beacons(hass, dev_id, kwargs) + else: + # Normal region + regions = context.regions_entered[dev_id] + if location not in regions: + regions.append(location) + _LOGGER.info("Enter region %s", location) + _set_gps_from_zone(kwargs, location, zone) + context.async_see(**kwargs) + context.async_see_beacons(hass, dev_id, kwargs) + + +async def _async_transition_message_leave(hass, context, message, location): + """Execute leave event.""" + dev_id, kwargs = _parse_see_args(message, context.mqtt_topic) + regions = context.regions_entered[dev_id] + + if location in regions: + regions.remove(location) + + beacons = context.mobile_beacons_active[dev_id] + if location in beacons: + beacons.remove(location) + _LOGGER.info("Remove beacon %s", location) + context.async_see_beacons(hass, dev_id, kwargs) + else: + new_region = regions[-1] if regions else None + if new_region: + # Exit to previous region + zone = hass.states.get( + "zone.{}".format(slugify(new_region))) + _set_gps_from_zone(kwargs, new_region, zone) + _LOGGER.info("Exit to %s", new_region) + context.async_see(**kwargs) + context.async_see_beacons(hass, dev_id, kwargs) + return + + _LOGGER.info("Exit to GPS") + + # Check for GPS accuracy + if context.async_valid_accuracy(message): + context.async_see(**kwargs) + context.async_see_beacons(hass, dev_id, kwargs) + + +@HANDLERS.register('transition') +async def async_handle_transition_message(hass, context, message): + """Handle a transition message.""" + if message.get('desc') is None: + _LOGGER.error( + "Location missing from `Entering/Leaving` message - " + "please turn `Share` on in OwnTracks app") + return + # OwnTracks uses - at the start of a beacon zone + # to switch on 'hold mode' - ignore this + location = message['desc'].lstrip("-") + + # Create a layer of indirection for Owntracks instances that may name + # regions differently than their HA names + if location in context.region_mapping: + location = context.region_mapping[location] + + if location.lower() == 'home': + location = STATE_HOME + + if message['event'] == 'enter': + await _async_transition_message_enter( + hass, context, message, location) + elif message['event'] == 'leave': + await _async_transition_message_leave( + hass, context, message, location) + else: + _LOGGER.error( + "Misformatted mqtt msgs, _type=transition, event=%s", + message['event']) + + +async def async_handle_waypoint(hass, name_base, waypoint): + """Handle a waypoint.""" + name = waypoint['desc'] + pretty_name = '{} - {}'.format(name_base, name) + lat = waypoint['lat'] + lon = waypoint['lon'] + rad = waypoint['rad'] + + # check zone exists + entity_id = zone_comp.ENTITY_ID_FORMAT.format(slugify(pretty_name)) + + # Check if state already exists + if hass.states.get(entity_id) is not None: + return + + zone = zone_comp.Zone(hass, pretty_name, lat, lon, rad, + zone_comp.ICON_IMPORT, False) + zone.entity_id = entity_id + await zone.async_update_ha_state() + + +@HANDLERS.register('waypoint') +@HANDLERS.register('waypoints') +async def async_handle_waypoints_message(hass, context, message): + """Handle a waypoints message.""" + if not context.import_waypoints: + return + + if context.waypoint_whitelist is not None: + user = _parse_topic(message['topic'], context.mqtt_topic)[0] + + if user not in context.waypoint_whitelist: + return + + if 'waypoints' in message: + wayps = message['waypoints'] + else: + wayps = [message] + + _LOGGER.info("Got %d waypoints from %s", len(wayps), message['topic']) + + name_base = ' '.join(_parse_topic(message['topic'], context.mqtt_topic)) + + for wayp in wayps: + await async_handle_waypoint(hass, name_base, wayp) + + +@HANDLERS.register('encrypted') +async def async_handle_encrypted_message(hass, context, message): + """Handle an encrypted message.""" + if 'topic' not in message and isinstance(context.secret, dict): + _LOGGER.error("You cannot set per topic secrets when using HTTP") + return + + plaintext_payload = _decrypt_payload(context.secret, message.get('topic'), + message['data']) + + if plaintext_payload is None: + return + + decrypted = json.loads(plaintext_payload) + if 'topic' in message and 'topic' not in decrypted: + decrypted['topic'] = message['topic'] + + await async_handle_message(hass, context, decrypted) + + +@HANDLERS.register('lwt') +@HANDLERS.register('configuration') +@HANDLERS.register('beacon') +@HANDLERS.register('cmd') +@HANDLERS.register('steps') +@HANDLERS.register('card') +async def async_handle_not_impl_msg(hass, context, message): + """Handle valid but not implemented message types.""" + _LOGGER.debug('Not handling %s message: %s', message.get("_type"), message) + + +async def async_handle_unsupported_msg(hass, context, message): + """Handle an unsupported or invalid message type.""" + _LOGGER.warning('Received unsupported message type: %s.', + message.get('_type')) + + +async def async_handle_message(hass, context, message): + """Handle an OwnTracks message.""" + msgtype = message.get('_type') + + _LOGGER.debug("Received %s", message) + + handler = HANDLERS.get(msgtype, async_handle_unsupported_msg) + + await handler(hass, context, message) diff --git a/homeassistant/components/zone/__init__.py b/homeassistant/components/zone/__init__.py index 242f0362088..0340964561c 100644 --- a/homeassistant/components/zone/__init__.py +++ b/homeassistant/components/zone/__init__.py @@ -3,15 +3,19 @@ import logging import voluptuous as vol +from homeassistant.loader import bind_hass import homeassistant.helpers.config_validation as cv from homeassistant.const import ( CONF_NAME, CONF_LATITUDE, CONF_LONGITUDE, CONF_ICON, CONF_RADIUS) from homeassistant.helpers import config_per_platform from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.util import slugify +from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE +from homeassistant.util.location import distance + from .config_flow import configured_zones -from .const import CONF_PASSIVE, DOMAIN, HOME_ZONE +from .const import CONF_PASSIVE, DOMAIN, HOME_ZONE, ATTR_PASSIVE, ATTR_RADIUS from .zone import Zone _LOGGER = logging.getLogger(__name__) @@ -37,6 +41,40 @@ PLATFORM_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) +@bind_hass +def async_active_zone(hass, latitude, longitude, radius=0): + """Find the active zone for given latitude, longitude. + + This method must be run in the event loop. + """ + # Sort entity IDs so that we are deterministic if equal distance to 2 zones + zones = (hass.states.get(entity_id) for entity_id + in sorted(hass.states.async_entity_ids(DOMAIN))) + + min_dist = None + closest = None + + for zone in zones: + if zone.attributes.get(ATTR_PASSIVE): + continue + + zone_dist = distance( + latitude, longitude, + zone.attributes[ATTR_LATITUDE], zone.attributes[ATTR_LONGITUDE]) + + within_zone = zone_dist - radius < zone.attributes[ATTR_RADIUS] + closer_zone = closest is None or zone_dist < min_dist + smaller_zone = (zone_dist == min_dist and + zone.attributes[ATTR_RADIUS] < + closest.attributes[ATTR_RADIUS]) + + if within_zone and (closer_zone or smaller_zone): + min_dist = zone_dist + closest = zone + + return closest + + async def async_setup(hass, config): """Set up configured zones as well as home assistant zone if necessary.""" hass.data[DOMAIN] = {} diff --git a/homeassistant/components/zone/const.py b/homeassistant/components/zone/const.py index b69ba67302a..676104b6943 100644 --- a/homeassistant/components/zone/const.py +++ b/homeassistant/components/zone/const.py @@ -3,3 +3,5 @@ CONF_PASSIVE = 'passive' DOMAIN = 'zone' HOME_ZONE = 'home' +ATTR_PASSIVE = 'passive' +ATTR_RADIUS = 'radius' diff --git a/homeassistant/components/zone/zone.py b/homeassistant/components/zone/zone.py index 21084e18f06..20155e06311 100644 --- a/homeassistant/components/zone/zone.py +++ b/homeassistant/components/zone/zone.py @@ -1,60 +1,13 @@ """Zone entity and functionality.""" from homeassistant.const import ATTR_HIDDEN, ATTR_LATITUDE, ATTR_LONGITUDE from homeassistant.helpers.entity import Entity -from homeassistant.loader import bind_hass -from homeassistant.util.async_ import run_callback_threadsafe from homeassistant.util.location import distance -from .const import DOMAIN - -ATTR_PASSIVE = 'passive' -ATTR_RADIUS = 'radius' +from .const import ATTR_PASSIVE, ATTR_RADIUS STATE = 'zoning' -@bind_hass -def active_zone(hass, latitude, longitude, radius=0): - """Find the active zone for given latitude, longitude.""" - return run_callback_threadsafe( - hass.loop, async_active_zone, hass, latitude, longitude, radius - ).result() - - -@bind_hass -def async_active_zone(hass, latitude, longitude, radius=0): - """Find the active zone for given latitude, longitude. - - This method must be run in the event loop. - """ - # Sort entity IDs so that we are deterministic if equal distance to 2 zones - zones = (hass.states.get(entity_id) for entity_id - in sorted(hass.states.async_entity_ids(DOMAIN))) - - min_dist = None - closest = None - - for zone in zones: - if zone.attributes.get(ATTR_PASSIVE): - continue - - zone_dist = distance( - latitude, longitude, - zone.attributes[ATTR_LATITUDE], zone.attributes[ATTR_LONGITUDE]) - - within_zone = zone_dist - radius < zone.attributes[ATTR_RADIUS] - closer_zone = closest is None or zone_dist < min_dist - smaller_zone = (zone_dist == min_dist and - zone.attributes[ATTR_RADIUS] < - closest.attributes[ATTR_RADIUS]) - - if within_zone and (closer_zone or smaller_zone): - min_dist = zone_dist - closest = zone - - return closest - - def in_zone(zone, latitude, longitude, radius=0) -> bool: """Test if given latitude, longitude is in given zone. diff --git a/tests/components/gpslogger/test_init.py b/tests/components/gpslogger/test_init.py index 608456d44db..2cffa86f393 100644 --- a/tests/components/gpslogger/test_init.py +++ b/tests/components/gpslogger/test_init.py @@ -165,7 +165,7 @@ async def test_enter_with_attrs(hass, gpslogger_client, webhook_id): data['device'])) assert state.state == STATE_NOT_HOME assert state.attributes['gps_accuracy'] == 10.5 - assert state.attributes['battery'] == 10.0 + assert state.attributes['battery_level'] == 10.0 assert state.attributes['speed'] == 100.0 assert state.attributes['direction'] == 105.32 assert state.attributes['altitude'] == 102.0 diff --git a/tests/components/owntracks/test_device_tracker.py b/tests/components/owntracks/test_device_tracker.py index 8e868296703..b81f434a2c1 100644 --- a/tests/components/owntracks/test_device_tracker.py +++ b/tests/components/owntracks/test_device_tracker.py @@ -861,10 +861,9 @@ async def test_event_beacon_unknown_zone_no_location(hass, context): # the Device during test case setup. assert_location_state(hass, 'None') - # home is the state of a Device constructed through - # the normal code path on it's first observation with - # the conditions I pass along. - assert_mobile_tracker_state(hass, 'home', 'unknown') + # We have had no location yet, so the beacon status + # set to unknown. + assert_mobile_tracker_state(hass, 'unknown', 'unknown') async def test_event_beacon_unknown_zone(hass, context): @@ -1276,7 +1275,7 @@ async def test_single_waypoint_import(hass, context): async def test_not_implemented_message(hass, context): """Handle not implemented message type.""" patch_handler = patch('homeassistant.components.owntracks.' - 'device_tracker.async_handle_not_impl_msg', + 'messages.async_handle_not_impl_msg', return_value=mock_coro(False)) patch_handler.start() assert not await send_message(hass, LWT_TOPIC, LWT_MESSAGE) @@ -1286,7 +1285,7 @@ async def test_not_implemented_message(hass, context): async def test_unsupported_message(hass, context): """Handle not implemented message type.""" patch_handler = patch('homeassistant.components.owntracks.' - 'device_tracker.async_handle_unsupported_msg', + 'messages.async_handle_unsupported_msg', return_value=mock_coro(False)) patch_handler.start() assert not await send_message(hass, BAD_TOPIC, BAD_MESSAGE) @@ -1374,7 +1373,7 @@ def config_context(hass, setup_comp): patch_save.stop() -@patch('homeassistant.components.owntracks.device_tracker.get_cipher', +@patch('homeassistant.components.owntracks.messages.get_cipher', mock_cipher) async def test_encrypted_payload(hass, setup_comp): """Test encrypted payload.""" @@ -1385,7 +1384,7 @@ async def test_encrypted_payload(hass, setup_comp): assert_location_latitude(hass, LOCATION_MESSAGE['lat']) -@patch('homeassistant.components.owntracks.device_tracker.get_cipher', +@patch('homeassistant.components.owntracks.messages.get_cipher', mock_cipher) async def test_encrypted_payload_topic_key(hass, setup_comp): """Test encrypted payload with a topic key.""" @@ -1398,7 +1397,7 @@ async def test_encrypted_payload_topic_key(hass, setup_comp): assert_location_latitude(hass, LOCATION_MESSAGE['lat']) -@patch('homeassistant.components.owntracks.device_tracker.get_cipher', +@patch('homeassistant.components.owntracks.messages.get_cipher', mock_cipher) async def test_encrypted_payload_no_key(hass, setup_comp): """Test encrypted payload with no key, .""" @@ -1411,7 +1410,7 @@ async def test_encrypted_payload_no_key(hass, setup_comp): assert hass.states.get(DEVICE_TRACKER_STATE) is None -@patch('homeassistant.components.owntracks.device_tracker.get_cipher', +@patch('homeassistant.components.owntracks.messages.get_cipher', mock_cipher) async def test_encrypted_payload_wrong_key(hass, setup_comp): """Test encrypted payload with wrong key.""" @@ -1422,7 +1421,7 @@ async def test_encrypted_payload_wrong_key(hass, setup_comp): assert hass.states.get(DEVICE_TRACKER_STATE) is None -@patch('homeassistant.components.owntracks.device_tracker.get_cipher', +@patch('homeassistant.components.owntracks.messages.get_cipher', mock_cipher) async def test_encrypted_payload_wrong_topic_key(hass, setup_comp): """Test encrypted payload with wrong topic key.""" @@ -1435,7 +1434,7 @@ async def test_encrypted_payload_wrong_topic_key(hass, setup_comp): assert hass.states.get(DEVICE_TRACKER_STATE) is None -@patch('homeassistant.components.owntracks.device_tracker.get_cipher', +@patch('homeassistant.components.owntracks.messages.get_cipher', mock_cipher) async def test_encrypted_payload_no_topic_key(hass, setup_comp): """Test encrypted payload with no topic key.""" diff --git a/tests/components/zone/test_init.py b/tests/components/zone/test_init.py index ba98915e777..576be0ce03c 100644 --- a/tests/components/zone/test_init.py +++ b/tests/components/zone/test_init.py @@ -142,7 +142,7 @@ class TestComponentZone(unittest.TestCase): ] }) self.hass.block_till_done() - active = zone.zone.active_zone(self.hass, 32.880600, -117.237561) + active = zone.async_active_zone(self.hass, 32.880600, -117.237561) assert active is None def test_active_zone_skips_passive_zones_2(self): @@ -158,7 +158,7 @@ class TestComponentZone(unittest.TestCase): ] }) self.hass.block_till_done() - active = zone.zone.active_zone(self.hass, 32.880700, -117.237561) + active = zone.async_active_zone(self.hass, 32.880700, -117.237561) assert 'zone.active_zone' == active.entity_id def test_active_zone_prefers_smaller_zone_if_same_distance(self): @@ -182,7 +182,7 @@ class TestComponentZone(unittest.TestCase): ] }) - active = zone.zone.active_zone(self.hass, latitude, longitude) + active = zone.async_active_zone(self.hass, latitude, longitude) assert 'zone.small_zone' == active.entity_id def test_active_zone_prefers_smaller_zone_if_same_distance_2(self): @@ -200,7 +200,7 @@ class TestComponentZone(unittest.TestCase): ] }) - active = zone.zone.active_zone(self.hass, latitude, longitude) + active = zone.async_active_zone(self.hass, latitude, longitude) assert 'zone.smallest_zone' == active.entity_id def test_in_zone_works_for_passive_zones(self): From 7959c04d1efa70a7e79b0d5602b4c1b89973d2f9 Mon Sep 17 00:00:00 2001 From: Robin Wohlers-Reichel Date: Sun, 26 May 2019 12:55:30 +1000 Subject: [PATCH 137/232] Solax Inverter Sensor Component (#22579) * Solax inverter direct API * Linter compliance * lint++ * move api communication to external lib * lint++ * requirements * Revert "requirements" This reverts commit 82a6c0c095ae5d454a1a16f72a77941626fa5ea2. * potentially? * Addressing review comments * Also update CODEOWNERS * Only update sensor state if data has changed --- .coveragerc | 1 + CODEOWNERS | 1 + homeassistant/components/solax/__init__.py | 1 + homeassistant/components/solax/manifest.json | 11 ++ homeassistant/components/solax/sensor.py | 106 +++++++++++++++++++ requirements_all.txt | 3 + 6 files changed, 123 insertions(+) create mode 100644 homeassistant/components/solax/__init__.py create mode 100644 homeassistant/components/solax/manifest.json create mode 100644 homeassistant/components/solax/sensor.py diff --git a/.coveragerc b/.coveragerc index 8f32fcdbe37..20111585c15 100644 --- a/.coveragerc +++ b/.coveragerc @@ -546,6 +546,7 @@ omit = homeassistant/components/sochain/sensor.py homeassistant/components/socialblade/sensor.py homeassistant/components/solaredge/sensor.py + homeassistant/components/solax/sensor.py homeassistant/components/somfy_mylink/* homeassistant/components/sonarr/sensor.py homeassistant/components/songpal/media_player.py diff --git a/CODEOWNERS b/CODEOWNERS index 0207363102b..a1581d4720c 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -210,6 +210,7 @@ homeassistant/components/sma/* @kellerza homeassistant/components/smarthab/* @outadoc homeassistant/components/smartthings/* @andrewsayre homeassistant/components/smtp/* @fabaff +homeassistant/components/solax/* @squishykid homeassistant/components/sonos/* @amelchio homeassistant/components/spaceapi/* @fabaff homeassistant/components/spider/* @peternijssen diff --git a/homeassistant/components/solax/__init__.py b/homeassistant/components/solax/__init__.py new file mode 100644 index 00000000000..3995ab10ac9 --- /dev/null +++ b/homeassistant/components/solax/__init__.py @@ -0,0 +1 @@ +"""The solax component.""" diff --git a/homeassistant/components/solax/manifest.json b/homeassistant/components/solax/manifest.json new file mode 100644 index 00000000000..8e5f9d960f0 --- /dev/null +++ b/homeassistant/components/solax/manifest.json @@ -0,0 +1,11 @@ +{ + "domain": "solax", + "name": "Solax Inverter", + "documentation": "https://www.home-assistant.io/components/solax", + "requirements": [ + "solax==0.0.3" + ], + "dependencies": [], + "codeowners": ["@squishykid"] + } + \ No newline at end of file diff --git a/homeassistant/components/solax/sensor.py b/homeassistant/components/solax/sensor.py new file mode 100644 index 00000000000..46d8722f831 --- /dev/null +++ b/homeassistant/components/solax/sensor.py @@ -0,0 +1,106 @@ +"""Support for Solax inverter via local API.""" +import asyncio + +from datetime import timedelta +import logging + +import voluptuous as vol + +from homeassistant.const import ( + TEMP_CELSIUS, + CONF_IP_ADDRESS +) +from homeassistant.helpers.entity import Entity +import homeassistant.helpers.config_validation as cv +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.exceptions import PlatformNotReady +from homeassistant.helpers.event import async_track_time_interval + +_LOGGER = logging.getLogger(__name__) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_IP_ADDRESS): cv.string, +}) + +SCAN_INTERVAL = timedelta(seconds=30) + + +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): + """Platform setup.""" + import solax + + api = solax.solax.RealTimeAPI(config[CONF_IP_ADDRESS]) + endpoint = RealTimeDataEndpoint(hass, api) + hass.async_add_job(endpoint.async_refresh) + async_track_time_interval(hass, endpoint.async_refresh, SCAN_INTERVAL) + devices = [] + for sensor in solax.INVERTER_SENSORS: + unit = solax.INVERTER_SENSORS[sensor][1] + if unit == 'C': + unit = TEMP_CELSIUS + devices.append(Inverter(sensor, unit)) + endpoint.sensors = devices + async_add_entities(devices) + + +class RealTimeDataEndpoint: + """Representation of a Sensor.""" + + def __init__(self, hass, api): + """Initialize the sensor.""" + self.hass = hass + self.api = api + self.data = {} + self.ready = asyncio.Event() + self.sensors = [] + + async def async_refresh(self, now=None): + """Fetch new state data for the sensor. + + This is the only method that should fetch new data for Home Assistant. + """ + from solax import SolaxRequestError + + try: + self.data = await self.api.get_data() + self.ready.set() + except SolaxRequestError: + if now is not None: + self.ready.clear() + else: + raise PlatformNotReady + for sensor in self.sensors: + if sensor.key in self.data: + sensor.value = self.data[sensor.key] + sensor.async_schedule_update_ha_state() + + +class Inverter(Entity): + """Class for a sensor.""" + + def __init__(self, key, unit): + """Initialize an inverter sensor.""" + self.key = key + self.value = None + self.unit = unit + + @property + def state(self): + """State of this inverter attribute.""" + return self.value + + @property + def name(self): + """Name of this inverter attribute.""" + return self.key + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return self.unit + + @property + def should_poll(self): + """No polling needed.""" + return False diff --git a/requirements_all.txt b/requirements_all.txt index 5288c6c9001..21bdcc6bf4c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1653,6 +1653,9 @@ socialbladeclient==0.2 # homeassistant.components.solaredge solaredge==0.0.2 +# homeassistant.components.solax +solax==0.0.3 + # homeassistant.components.honeywell somecomfort==0.5.2 From fa20957e011751bd9704fa51c412c0208760ab5a Mon Sep 17 00:00:00 2001 From: cgtobi Date: Sun, 26 May 2019 12:09:02 +0200 Subject: [PATCH 138/232] Bump pyatmo version to 1.12 (#24088) --- homeassistant/components/netatmo/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json index a5e4e8aa7a7..91e96e48b5c 100644 --- a/homeassistant/components/netatmo/manifest.json +++ b/homeassistant/components/netatmo/manifest.json @@ -3,7 +3,7 @@ "name": "Netatmo", "documentation": "https://www.home-assistant.io/components/netatmo", "requirements": [ - "pyatmo==1.11" + "pyatmo==1.12" ], "dependencies": [ "webhook" diff --git a/requirements_all.txt b/requirements_all.txt index 21bdcc6bf4c..c20b212be88 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -989,7 +989,7 @@ pyalarmdotcom==0.3.2 pyarlo==0.2.3 # homeassistant.components.netatmo -pyatmo==1.11 +pyatmo==1.12 # homeassistant.components.apple_tv pyatv==0.3.12 From ce219ac6c7ada8b0a80c06e920801cb7d42fe29c Mon Sep 17 00:00:00 2001 From: Jardi Martinez <1088732+jardiamj@users.noreply.github.com> Date: Sun, 26 May 2019 03:26:51 -0700 Subject: [PATCH 139/232] Set assumed_state property to True. (#24118) --- homeassistant/components/mcp23017/switch.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/mcp23017/switch.py b/homeassistant/components/mcp23017/switch.py index caa183543ac..8638b793a65 100644 --- a/homeassistant/components/mcp23017/switch.py +++ b/homeassistant/components/mcp23017/switch.py @@ -79,6 +79,11 @@ class MCP23017Switch(ToggleEntity): """Return true if device is on.""" return self._state + @property + def assumed_state(self): + """Return true if optimistic updates are used.""" + return True + def turn_on(self, **kwargs): """Turn the device on.""" self._pin.value = not self._invert_logic From 25505dc1d4fe96c1c25c15248f30a2106266ef5a Mon Sep 17 00:00:00 2001 From: jjlawren Date: Sun, 26 May 2019 05:28:29 -0500 Subject: [PATCH 140/232] Remove custom entity_id naming (#24072) * Remove custom entity_id naming * Set entity_ids with 'plex' * Set name instead of entity_id * Lint * Use a name template --- homeassistant/components/plex/media_player.py | 26 +++---------------- 1 file changed, 4 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index 4d5f70cf36e..1d71b59cde1 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -26,18 +26,17 @@ _LOGGER = logging.getLogger(__name__) MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1) +NAME_FORMAT = 'Plex {}' PLEX_CONFIG_FILE = 'plex.conf' PLEX_DATA = 'plex' CONF_USE_EPISODE_ART = 'use_episode_art' -CONF_USE_CUSTOM_ENTITY_IDS = 'use_custom_entity_ids' CONF_SHOW_ALL_CONTROLS = 'show_all_controls' CONF_REMOVE_UNAVAILABLE_CLIENTS = 'remove_unavailable_clients' CONF_CLIENT_REMOVE_INTERVAL = 'client_remove_interval' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_USE_EPISODE_ART, default=False): cv.boolean, - vol.Optional(CONF_USE_CUSTOM_ENTITY_IDS, default=False): cv.boolean, vol.Optional(CONF_SHOW_ALL_CONTROLS, default=False): cv.boolean, vol.Optional(CONF_REMOVE_UNAVAILABLE_CLIENTS, default=True): cv.boolean, vol.Optional(CONF_CLIENT_REMOVE_INTERVAL, default=timedelta(seconds=600)): @@ -319,24 +318,6 @@ class PlexClient(MediaPlayerDevice): self.refresh(device, session) - # Assign custom entity ID if desired - if self.config.get(CONF_USE_CUSTOM_ENTITY_IDS): - prefix = '' - # allow for namespace prefixing when using custom entity names - if config.get("entity_namespace"): - prefix = config.get("entity_namespace") + '_' - - # rename the entity id - if self.machine_identifier: - self.entity_id = "%s.%s%s" % ( - 'media_player', prefix, - self.machine_identifier.lower().replace('-', '_')) - else: - if self.name: - self.entity_id = "%s.%s%s" % ( - 'media_player', prefix, - self.name.lower().replace('-', '_')) - def _clear_media_details(self): """Set all Media Items to None.""" # General @@ -378,7 +359,8 @@ class PlexClient(MediaPlayerDevice): self._device.proxyThroughServer() self._session = None self._machine_identifier = self._device.machineIdentifier - self._name = self._device.title or DEVICE_DEFAULT_NAME + self._name = NAME_FORMAT.format(self._device.title or + DEVICE_DEFAULT_NAME) self._device_protocol_capabilities = ( self._device.protocolCapabilities) @@ -395,7 +377,7 @@ class PlexClient(MediaPlayerDevice): self._player = [p for p in self._session.players if p.machineIdentifier == self._device.machineIdentifier][0] - self._name = self._player.title + self._name = NAME_FORMAT.format(self._player.title) self._player_state = self._player.state self._session_username = self._session.usernames[0] self._make = self._player.device From 0194905e97aed71c9dc5c2a1a15905e12470a796 Mon Sep 17 00:00:00 2001 From: Andrew Sayre <6730289+andrewsayre@users.noreply.github.com> Date: Sun, 26 May 2019 06:47:11 -0500 Subject: [PATCH 141/232] Move imports to top (#24108) --- homeassistant/components/heos/__init__.py | 32 ++++++------- homeassistant/components/heos/config_flow.py | 2 +- homeassistant/components/heos/media_player.py | 45 +++++++++---------- tests/components/heos/conftest.py | 33 +++++++------- 4 files changed, 54 insertions(+), 58 deletions(-) diff --git a/homeassistant/components/heos/__init__.py b/homeassistant/components/heos/__init__.py index 6585393d12e..7a6cb36ab7b 100644 --- a/homeassistant/components/heos/__init__.py +++ b/homeassistant/components/heos/__init__.py @@ -4,6 +4,7 @@ from datetime import timedelta import logging from typing import Dict +from pyheos import CommandError, Heos, const as heos_const import voluptuous as vol from homeassistant.components.media_player.const import ( @@ -57,7 +58,6 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType): async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): """Initialize config entry which represents the HEOS controller.""" - from pyheos import Heos, CommandError host = entry.data[CONF_HOST] # Setting all_progress_events=False ensures that we only receive a # media position update upon start of playback or when media changes @@ -137,16 +137,15 @@ class ControllerManager: async def connect_listeners(self): """Subscribe to events of interest.""" - from pyheos import const self._device_registry, self._entity_registry = await asyncio.gather( self._hass.helpers.device_registry.async_get_registry(), self._hass.helpers.entity_registry.async_get_registry()) # Handle controller events self._signals.append(self.controller.dispatcher.connect( - const.SIGNAL_CONTROLLER_EVENT, self._controller_event)) + heos_const.SIGNAL_CONTROLLER_EVENT, self._controller_event)) # Handle connection-related events self._signals.append(self.controller.dispatcher.connect( - const.SIGNAL_HEOS_EVENT, self._heos_event)) + heos_const.SIGNAL_HEOS_EVENT, self._heos_event)) async def disconnect(self): """Disconnect subscriptions.""" @@ -158,21 +157,19 @@ class ControllerManager: async def _controller_event(self, event, data): """Handle controller event.""" - from pyheos import const - if event == const.EVENT_PLAYERS_CHANGED: - self.update_ids(data[const.DATA_MAPPED_IDS]) + if event == heos_const.EVENT_PLAYERS_CHANGED: + self.update_ids(data[heos_const.DATA_MAPPED_IDS]) # Update players self._hass.helpers.dispatcher.async_dispatcher_send( SIGNAL_HEOS_UPDATED) async def _heos_event(self, event): """Handle connection event.""" - from pyheos import CommandError, const - if event == const.EVENT_CONNECTED: + if event == heos_const.EVENT_CONNECTED: try: # Retrieve latest players and refresh status data = await self.controller.load_players() - self.update_ids(data[const.DATA_MAPPED_IDS]) + self.update_ids(data[heos_const.DATA_MAPPED_IDS]) except (CommandError, asyncio.TimeoutError, ConnectionError) as ex: _LOGGER.error("Unable to refresh players: %s", ex) # Update players @@ -241,9 +238,8 @@ class SourceManager: def get_current_source(self, now_playing_media): """Determine current source from now playing media.""" - from pyheos import const # Match input by input_name:media_id - if now_playing_media.source_id == const.MUSIC_SOURCE_AUX_INPUT: + if now_playing_media.source_id == heos_const.MUSIC_SOURCE_AUX_INPUT: return next((input_source.name for input_source in self.inputs if input_source.input_name == now_playing_media.media_id), None) @@ -260,8 +256,6 @@ class SourceManager: physical event therefore throttle it. Retrieving sources immediately after the event may fail so retry. """ - from pyheos import CommandError, const - @Throttle(MIN_UPDATE_SOURCES) async def get_sources(): retry_attempts = 0 @@ -286,9 +280,9 @@ class SourceManager: return async def update_sources(event, data=None): - if event in (const.EVENT_SOURCES_CHANGED, - const.EVENT_USER_CHANGED, - const.EVENT_CONNECTED): + if event in (heos_const.EVENT_SOURCES_CHANGED, + heos_const.EVENT_USER_CHANGED, + heos_const.EVENT_CONNECTED): sources = await get_sources() # If throttled, it will return None if sources: @@ -300,6 +294,6 @@ class SourceManager: SIGNAL_HEOS_UPDATED) controller.dispatcher.connect( - const.SIGNAL_CONTROLLER_EVENT, update_sources) + heos_const.SIGNAL_CONTROLLER_EVENT, update_sources) controller.dispatcher.connect( - const.SIGNAL_HEOS_EVENT, update_sources) + heos_const.SIGNAL_HEOS_EVENT, update_sources) diff --git a/homeassistant/components/heos/config_flow.py b/homeassistant/components/heos/config_flow.py index 656058877db..064813a86a7 100644 --- a/homeassistant/components/heos/config_flow.py +++ b/homeassistant/components/heos/config_flow.py @@ -1,6 +1,7 @@ """Config flow to configure Heos.""" import asyncio +from pyheos import Heos import voluptuous as vol from homeassistant import config_entries @@ -44,7 +45,6 @@ class HeosFlowHandler(config_entries.ConfigFlow): async def async_step_user(self, user_input=None): """Obtain host and validate connection.""" - from pyheos import Heos self.hass.data.setdefault(DATA_DISCOVERED_HOSTS, {}) # Only a single entry is needed for all devices if self._async_current_entries(): diff --git a/homeassistant/components/heos/media_player.py b/homeassistant/components/heos/media_player.py index 00a3b721efb..ff5c2d707f2 100644 --- a/homeassistant/components/heos/media_player.py +++ b/homeassistant/components/heos/media_player.py @@ -5,6 +5,8 @@ import logging from operator import ior from typing import Sequence +from pyheos import CommandError, const as heos_const + from homeassistant.components.media_player import MediaPlayerDevice from homeassistant.components.media_player.const import ( ATTR_MEDIA_ENQUEUE, DOMAIN, MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST, @@ -25,6 +27,20 @@ BASE_SUPPORTED_FEATURES = SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET | \ SUPPORT_SHUFFLE_SET | SUPPORT_SELECT_SOURCE | \ SUPPORT_PLAY_MEDIA +PLAY_STATE_TO_STATE = { + heos_const.PLAY_STATE_PLAY: STATE_PLAYING, + heos_const.PLAY_STATE_STOP: STATE_IDLE, + heos_const.PLAY_STATE_PAUSE: STATE_PAUSED +} + +CONTROL_TO_SUPPORT = { + heos_const.CONTROL_PLAY: SUPPORT_PLAY, + heos_const.CONTROL_PAUSE: SUPPORT_PAUSE, + heos_const.CONTROL_STOP: SUPPORT_STOP, + heos_const.CONTROL_PLAY_PREVIOUS: SUPPORT_PREVIOUS_TRACK, + heos_const.CONTROL_PLAY_NEXT: SUPPORT_NEXT_TRACK +} + _LOGGER = logging.getLogger(__name__) @@ -47,7 +63,6 @@ def log_command_error(command: str): def decorator(func): @wraps(func) async def wrapper(*args, **kwargs): - from pyheos import CommandError try: await func(*args, **kwargs) except (CommandError, asyncio.TimeoutError, ConnectionError, @@ -62,31 +77,17 @@ class HeosMediaPlayer(MediaPlayerDevice): def __init__(self, player): """Initialize.""" - from pyheos import const self._media_position_updated_at = None self._player = player self._signals = [] self._supported_features = BASE_SUPPORTED_FEATURES self._source_manager = None - self._play_state_to_state = { - const.PLAY_STATE_PLAY: STATE_PLAYING, - const.PLAY_STATE_STOP: STATE_IDLE, - const.PLAY_STATE_PAUSE: STATE_PAUSED - } - self._control_to_support = { - const.CONTROL_PLAY: SUPPORT_PLAY, - const.CONTROL_PAUSE: SUPPORT_PAUSE, - const.CONTROL_STOP: SUPPORT_STOP, - const.CONTROL_PLAY_PREVIOUS: SUPPORT_PREVIOUS_TRACK, - const.CONTROL_PLAY_NEXT: SUPPORT_NEXT_TRACK - } async def _player_update(self, player_id, event): """Handle player attribute updated.""" - from pyheos import const if self._player.player_id != player_id: return - if event == const.EVENT_PLAYER_NOW_PLAYING_PROGRESS: + if event == heos_const.EVENT_PLAYER_NOW_PLAYING_PROGRESS: self._media_position_updated_at = utcnow() await self.async_update_ha_state(True) @@ -96,11 +97,10 @@ class HeosMediaPlayer(MediaPlayerDevice): async def async_added_to_hass(self): """Device added to hass.""" - from pyheos import const self._source_manager = self.hass.data[HEOS_DOMAIN][DATA_SOURCE_MANAGER] # Update state when attributes of the player change self._signals.append(self._player.heos.dispatcher.connect( - const.SIGNAL_PLAYER_EVENT, self._player_update)) + heos_const.SIGNAL_PLAYER_EVENT, self._player_update)) # Update state when heos changes self._signals.append( self.hass.helpers.dispatcher.async_dispatcher_connect( @@ -163,14 +163,13 @@ class HeosMediaPlayer(MediaPlayerDevice): return if media_type == MEDIA_TYPE_PLAYLIST: - from pyheos import const playlists = await self._player.heos.get_playlists() playlist = next((p for p in playlists if p.name == media_id), None) if not playlist: raise ValueError("Invalid playlist '{}'".format(media_id)) - add_queue_option = const.ADD_QUEUE_ADD_TO_END \ + add_queue_option = heos_const.ADD_QUEUE_ADD_TO_END \ if kwargs.get(ATTR_MEDIA_ENQUEUE) \ - else const.ADD_QUEUE_REPLACE_AND_PLAY + else heos_const.ADD_QUEUE_REPLACE_AND_PLAY await self._player.add_to_queue(playlist, add_queue_option) return @@ -208,7 +207,7 @@ class HeosMediaPlayer(MediaPlayerDevice): async def async_update(self): """Update supported features of the player.""" controls = self._player.now_playing_media.supported_controls - current_support = [self._control_to_support[control] + current_support = [CONTROL_TO_SUPPORT[control] for control in controls] self._supported_features = reduce(ior, current_support, BASE_SUPPORTED_FEATURES) @@ -343,7 +342,7 @@ class HeosMediaPlayer(MediaPlayerDevice): @property def state(self) -> str: """State of the player.""" - return self._play_state_to_state[self._player.state] + return PLAY_STATE_TO_STATE[self._player.state] @property def supported_features(self) -> int: diff --git a/tests/components/heos/conftest.py b/tests/components/heos/conftest.py index 11a2ece3442..175a180e4a3 100644 --- a/tests/components/heos/conftest.py +++ b/tests/components/heos/conftest.py @@ -2,7 +2,7 @@ from typing import Dict, Sequence from asynctest.mock import Mock, patch as patch -from pyheos import Dispatcher, HeosPlayer, HeosSource, InputSource, const +from pyheos import Dispatcher, Heos, HeosPlayer, HeosSource, InputSource, const import pytest from homeassistant.components.heos import DOMAIN @@ -22,20 +22,23 @@ def config_entry_fixture(): def controller_fixture( players, favorites, input_sources, playlists, change_data, dispatcher): """Create a mock Heos controller fixture.""" - with patch("pyheos.Heos", autospec=True) as mock: - mock_heos = mock.return_value - for player in players.values(): - player.heos = mock_heos - mock_heos.dispatcher = dispatcher - mock_heos.get_players.return_value = players - mock_heos.players = players - mock_heos.get_favorites.return_value = favorites - mock_heos.get_input_sources.return_value = input_sources - mock_heos.get_playlists.return_value = playlists - mock_heos.load_players.return_value = change_data - mock_heos.is_signed_in = True - mock_heos.signed_in_username = "user@user.com" - mock_heos.connection_state = const.STATE_CONNECTED + mock_heos = Mock(Heos) + for player in players.values(): + player.heos = mock_heos + mock_heos.dispatcher = dispatcher + mock_heos.get_players.return_value = players + mock_heos.players = players + mock_heos.get_favorites.return_value = favorites + mock_heos.get_input_sources.return_value = input_sources + mock_heos.get_playlists.return_value = playlists + mock_heos.load_players.return_value = change_data + mock_heos.is_signed_in = True + mock_heos.signed_in_username = "user@user.com" + mock_heos.connection_state = const.STATE_CONNECTED + mock = Mock(return_value=mock_heos) + + with patch("homeassistant.components.heos.Heos", new=mock), \ + patch("homeassistant.components.heos.config_flow.Heos", new=mock): yield mock_heos From 9438dd1cbdab8e57a1dbdf232be4cff11f7cb8cd Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sun, 26 May 2019 13:48:05 +0200 Subject: [PATCH 142/232] Use name in ESPHome discovery title (#24100) * Use name in ESPHome discovery title * Add test * Lint --- .../components/esphome/config_flow.py | 5 +++ homeassistant/components/esphome/strings.json | 5 +-- tests/components/esphome/test_config_flow.py | 32 +++++++++---------- 3 files changed, 24 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py index 96ea8012bd4..f2344e40b2a 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -50,6 +50,11 @@ class EsphomeFlowHandler(config_entries.ConfigFlow): if error is not None: return await self.async_step_user(error=error) self._name = device_info.name + # pylint: disable=unsupported-assignment-operation + self.context['title_placeholders'] = { + 'name': self._name + } + # Only show authentication step if device uses password if device_info.uses_password: return await self.async_step_authenticate() diff --git a/homeassistant/components/esphome/strings.json b/homeassistant/components/esphome/strings.json index 8f691d9cb00..3b662441e13 100644 --- a/homeassistant/components/esphome/strings.json +++ b/homeassistant/components/esphome/strings.json @@ -26,9 +26,10 @@ }, "discovery_confirm": { "description": "Do you want to add the ESPHome node `{name}` to Home Assistant?", - "title": "Discovered ESPHome node" + "title": "Discovered ESPHome node" } }, - "title": "ESPHome" + "title": "ESPHome", + "flow_title": "ESPHome: {name}" } } diff --git a/tests/components/esphome/test_config_flow.py b/tests/components/esphome/test_config_flow.py index d27d449eb50..5aeb9d1c045 100644 --- a/tests/components/esphome/test_config_flow.py +++ b/tests/components/esphome/test_config_flow.py @@ -45,10 +45,16 @@ def mock_api_connection_error(): yield mock_error -async def test_user_connection_works(hass, mock_client): - """Test we can finish a config flow.""" +def _setup_flow_handler(hass): flow = config_flow.EsphomeFlowHandler() flow.hass = hass + flow.context = {} + return flow + + +async def test_user_connection_works(hass, mock_client): + """Test we can finish a config flow.""" + flow = _setup_flow_handler(hass) result = await flow.async_step_user(user_input=None) assert result['type'] == 'form' @@ -78,8 +84,7 @@ async def test_user_connection_works(hass, mock_client): async def test_user_resolve_error(hass, mock_api_connection_error, mock_client): """Test user step with IP resolve error.""" - flow = config_flow.EsphomeFlowHandler() - flow.hass = hass + flow = _setup_flow_handler(hass) await flow.async_step_user(user_input=None) class MockResolveError(mock_api_connection_error): @@ -111,8 +116,7 @@ async def test_user_resolve_error(hass, mock_api_connection_error, async def test_user_connection_error(hass, mock_api_connection_error, mock_client): """Test user step with connection error.""" - flow = config_flow.EsphomeFlowHandler() - flow.hass = hass + flow = _setup_flow_handler(hass) await flow.async_step_user(user_input=None) mock_client.device_info.side_effect = mock_api_connection_error @@ -134,8 +138,7 @@ async def test_user_connection_error(hass, mock_api_connection_error, async def test_user_with_password(hass, mock_client): """Test user step with password.""" - flow = config_flow.EsphomeFlowHandler() - flow.hass = hass + flow = _setup_flow_handler(hass) await flow.async_step_user(user_input=None) mock_client.device_info.return_value = mock_coro( @@ -165,8 +168,7 @@ async def test_user_with_password(hass, mock_client): async def test_user_invalid_password(hass, mock_api_connection_error, mock_client): """Test user step with invalid password.""" - flow = config_flow.EsphomeFlowHandler() - flow.hass = hass + flow = _setup_flow_handler(hass) await flow.async_step_user(user_input=None) mock_client.device_info.return_value = mock_coro( @@ -190,8 +192,7 @@ async def test_user_invalid_password(hass, mock_api_connection_error, async def test_discovery_initiation(hass, mock_client): """Test discovery importing works.""" - flow = config_flow.EsphomeFlowHandler() - flow.hass = hass + flow = _setup_flow_handler(hass) service_info = { 'host': '192.168.43.183', 'port': 6053, @@ -206,6 +207,7 @@ async def test_discovery_initiation(hass, mock_client): assert result['type'] == 'form' assert result['step_id'] == 'discovery_confirm' assert result['description_placeholders']['name'] == 'test8266' + assert flow.context['title_placeholders']['name'] == 'test8266' result = await flow.async_step_discovery_confirm(user_input={}) assert result['type'] == 'create_entry' @@ -221,8 +223,7 @@ async def test_discovery_already_configured_hostname(hass, mock_client): data={'host': 'test8266.local', 'port': 6053, 'password': ''} ).add_to_hass(hass) - flow = config_flow.EsphomeFlowHandler() - flow.hass = hass + flow = _setup_flow_handler(hass) service_info = { 'host': '192.168.43.183', 'port': 6053, @@ -241,8 +242,7 @@ async def test_discovery_already_configured_ip(hass, mock_client): data={'host': '192.168.43.183', 'port': 6053, 'password': ''} ).add_to_hass(hass) - flow = config_flow.EsphomeFlowHandler() - flow.hass = hass + flow = _setup_flow_handler(hass) service_info = { 'host': '192.168.43.183', 'port': 6053, From 6e1728542ef22ef48a7cb03542004e0ef3cee9b5 Mon Sep 17 00:00:00 2001 From: jgriff2 Date: Sun, 26 May 2019 04:52:06 -0700 Subject: [PATCH 143/232] Add Remote RPi Component (#23518) * Add Remote RPi Component * Add Remote RPi Component * fix imports * Added support for setup as switch and binary_sensor * remove pylint error handling * Changed to domain config * Changed to domain config * Changed to domain config * Changed to domain config * Update __init__.py * Update manifest.json * Update requirements_all.txt * Update switch.py * Update binary_sensor.py * Changed to domain config --- .coveragerc | 1 + .../components/remote_rpi_gpio/__init__.py | 63 +++++++++++ .../remote_rpi_gpio/binary_sensor.py | 106 ++++++++++++++++++ .../components/remote_rpi_gpio/manifest.json | 10 ++ .../components/remote_rpi_gpio/switch.py | 91 +++++++++++++++ requirements_all.txt | 3 + 6 files changed, 274 insertions(+) create mode 100644 homeassistant/components/remote_rpi_gpio/__init__.py create mode 100644 homeassistant/components/remote_rpi_gpio/binary_sensor.py create mode 100644 homeassistant/components/remote_rpi_gpio/manifest.json create mode 100644 homeassistant/components/remote_rpi_gpio/switch.py diff --git a/.coveragerc b/.coveragerc index 20111585c15..df371e1b683 100644 --- a/.coveragerc +++ b/.coveragerc @@ -487,6 +487,7 @@ omit = homeassistant/components/reddit/* homeassistant/components/rejseplanen/sensor.py homeassistant/components/remember_the_milk/__init__.py + homeassistant/components/remote_rpi_gpio/* homeassistant/components/rest/binary_sensor.py homeassistant/components/rest/notify.py homeassistant/components/rest/switch.py diff --git a/homeassistant/components/remote_rpi_gpio/__init__.py b/homeassistant/components/remote_rpi_gpio/__init__.py new file mode 100644 index 00000000000..82865b00cda --- /dev/null +++ b/homeassistant/components/remote_rpi_gpio/__init__.py @@ -0,0 +1,63 @@ +"""Support for controlling GPIO pins of a Raspberry Pi.""" +import logging + +_LOGGER = logging.getLogger(__name__) + +CONF_BOUNCETIME = 'bouncetime' +CONF_INVERT_LOGIC = 'invert_logic' +CONF_PULL_MODE = 'pull_mode' + +DEFAULT_BOUNCETIME = 50 +DEFAULT_INVERT_LOGIC = False +DEFAULT_PULL_MODE = "UP" + +DOMAIN = 'remote_rpi_gpio' + + +def setup(hass, config): + """Set up the Raspberry Pi Remote GPIO component.""" + return True + + +def setup_output(address, port, invert_logic): + """Set up a GPIO as output.""" + from gpiozero import LED + from gpiozero.pins.pigpio import PiGPIOFactory + + try: + return LED(port, active_high=invert_logic, + pin_factory=PiGPIOFactory(address)) + except (ValueError, IndexError, KeyError): + return None + + +def setup_input(address, port, pull_mode, bouncetime): + """Set up a GPIO as input.""" + from gpiozero import Button + from gpiozero.pins.pigpio import PiGPIOFactory + + if pull_mode == "UP": + pull_gpio_up = True + elif pull_mode == "DOWN": + pull_gpio_up = False + + try: + return Button(port, + pull_up=pull_gpio_up, + bounce_time=bouncetime, + pin_factory=PiGPIOFactory(address)) + except (ValueError, IndexError, KeyError, IOError): + return None + + +def write_output(switch, value): + """Write a value to a GPIO.""" + if value == 1: + switch.on() + if value == 0: + switch.off() + + +def read_input(button): + """Read a value from a GPIO.""" + return button.is_pressed diff --git a/homeassistant/components/remote_rpi_gpio/binary_sensor.py b/homeassistant/components/remote_rpi_gpio/binary_sensor.py new file mode 100644 index 00000000000..4c359163e56 --- /dev/null +++ b/homeassistant/components/remote_rpi_gpio/binary_sensor.py @@ -0,0 +1,106 @@ +"""Support for binary sensor using RPi GPIO.""" +import logging + +import voluptuous as vol + +import requests + +from homeassistant.const import CONF_HOST +from homeassistant.components.binary_sensor import ( + BinarySensorDevice, PLATFORM_SCHEMA) + +import homeassistant.helpers.config_validation as cv + +from . import (CONF_BOUNCETIME, CONF_PULL_MODE, CONF_INVERT_LOGIC, + DEFAULT_BOUNCETIME, DEFAULT_INVERT_LOGIC, DEFAULT_PULL_MODE) +from .. import remote_rpi_gpio + +_LOGGER = logging.getLogger(__name__) + +CONF_PORTS = 'ports' + +_SENSORS_SCHEMA = vol.Schema({ + cv.positive_int: cv.string, +}) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_PORTS): _SENSORS_SCHEMA, + vol.Optional(CONF_INVERT_LOGIC, + default=DEFAULT_INVERT_LOGIC): cv.boolean, + vol.Optional(CONF_BOUNCETIME, + default=DEFAULT_BOUNCETIME): cv.positive_int, + vol.Optional(CONF_PULL_MODE, + default=DEFAULT_PULL_MODE): cv.string, +}) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Raspberry PI GPIO devices.""" + address = config['host'] + invert_logic = config[CONF_INVERT_LOGIC] + pull_mode = config[CONF_PULL_MODE] + ports = config['ports'] + bouncetime = config[CONF_BOUNCETIME]/1000 + + devices = [] + for port_num, port_name in ports.items(): + try: + button = remote_rpi_gpio.setup_input(address, + port_num, + pull_mode, + bouncetime) + except (ValueError, IndexError, KeyError, IOError): + return + new_sensor = RemoteRPiGPIOBinarySensor(port_name, button, invert_logic) + devices.append(new_sensor) + + add_entities(devices, True) + + +class RemoteRPiGPIOBinarySensor(BinarySensorDevice): + """Represent a binary sensor that uses a Remote Raspberry Pi GPIO.""" + + def __init__(self, name, button, invert_logic): + """Initialize the RPi binary sensor.""" + self._name = name + self._invert_logic = invert_logic + self._state = False + self._button = button + + async def async_added_to_hass(self): + """Run when entity about to be added to hass.""" + def read_gpio(): + """Read state from GPIO.""" + self._state = remote_rpi_gpio.read_input(self._button) + self.schedule_update_ha_state() + + self._button.when_released = read_gpio + self._button.when_pressed = read_gpio + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def is_on(self): + """Return the state of the entity.""" + return self._state != self._invert_logic + + @property + def device_class(self): + """Return the class of this sensor, from DEVICE_CLASSES.""" + return + + def update(self): + """Update the GPIO state.""" + try: + self._state = remote_rpi_gpio.read_input(self._button) + except requests.exceptions.ConnectionError: + return diff --git a/homeassistant/components/remote_rpi_gpio/manifest.json b/homeassistant/components/remote_rpi_gpio/manifest.json new file mode 100644 index 00000000000..f15defd63dc --- /dev/null +++ b/homeassistant/components/remote_rpi_gpio/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "remote_rpi_gpio", + "name": "remote_rpi_gpio", + "documentation": "https://www.home-assistant.io/components/remote_rpi_gpio", + "requirements": [ + "gpiozero==1.4.1" + ], + "dependencies": [], + "codeowners": [] +} diff --git a/homeassistant/components/remote_rpi_gpio/switch.py b/homeassistant/components/remote_rpi_gpio/switch.py new file mode 100644 index 00000000000..493ccf03c32 --- /dev/null +++ b/homeassistant/components/remote_rpi_gpio/switch.py @@ -0,0 +1,91 @@ +"""Allows to configure a switch using RPi GPIO.""" +import logging + +import voluptuous as vol + +from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA +from homeassistant.const import DEVICE_DEFAULT_NAME, CONF_HOST + +import homeassistant.helpers.config_validation as cv + +from . import CONF_INVERT_LOGIC, DEFAULT_INVERT_LOGIC +from .. import remote_rpi_gpio + +_LOGGER = logging.getLogger(__name__) + +CONF_PORTS = 'ports' + +_SENSORS_SCHEMA = vol.Schema({ + cv.positive_int: cv.string, +}) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_PORTS): _SENSORS_SCHEMA, + vol.Optional(CONF_INVERT_LOGIC, + default=DEFAULT_INVERT_LOGIC): cv.boolean +}) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Remote Raspberry PI GPIO devices.""" + address = config[CONF_HOST] + invert_logic = config[CONF_INVERT_LOGIC] + ports = config[CONF_PORTS] + + devices = [] + for port, name in ports.items(): + try: + led = remote_rpi_gpio.setup_output( + address, port, invert_logic) + except (ValueError, IndexError, KeyError, IOError): + return + new_switch = RemoteRPiGPIOSwitch(name, led, invert_logic) + devices.append(new_switch) + + add_entities(devices) + + +class RemoteRPiGPIOSwitch(SwitchDevice): + """Representation of a Remtoe Raspberry Pi GPIO.""" + + def __init__(self, name, led, invert_logic): + """Initialize the pin.""" + self._name = name or DEVICE_DEFAULT_NAME + self._state = False + self._invert_logic = invert_logic + self._switch = led + + @property + def name(self): + """Return the name of the switch.""" + return self._name + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def assumed_state(self): + """If unable to access real state of the entity.""" + return True + + @property + def is_on(self): + """Return true if device is on.""" + return self._state + + def turn_on(self, **kwargs): + """Turn the device on.""" + remote_rpi_gpio.write_output(self._switch, + 0 if self._invert_logic else 1) + self._state = True + self.schedule_update_ha_state() + + def turn_off(self, **kwargs): + """Turn the device off.""" + remote_rpi_gpio.write_output(self._switch, + 1 if self._invert_logic else 0) + self._state = False + self.schedule_update_ha_state() diff --git a/requirements_all.txt b/requirements_all.txt index c20b212be88..545ff132bed 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -518,6 +518,9 @@ googledevices==1.0.2 # homeassistant.components.google_travel_time googlemaps==2.5.1 +# homeassistant.components.remote_rpi_gpio +gpiozero==1.4.1 + # homeassistant.components.gpsd gps3==0.33.3 From afeb13d980e339aa88f5d02a69466e992828edb3 Mon Sep 17 00:00:00 2001 From: Eduard van Valkenburg Date: Sun, 26 May 2019 13:55:40 +0200 Subject: [PATCH 144/232] Azure Event Hub history component (#23878) * v1 of Azure Event Hub History component * updates to EH * small fix * small updates and changed requirements_all * new version of Event Hub component * redid config to just ask names * small edit * latest version of EH component * updated codeowners * codeowner fix * typo in domain * updates based on reviews. * using built-in jsonencoder for DT * delete unused import --- .coveragerc | 1 + CODEOWNERS | 1 + .../components/azure_event_hub/__init__.py | 80 +++++++++++++++++++ .../components/azure_event_hub/manifest.json | 8 ++ requirements_all.txt | 3 + 5 files changed, 93 insertions(+) create mode 100644 homeassistant/components/azure_event_hub/__init__.py create mode 100644 homeassistant/components/azure_event_hub/manifest.json diff --git a/.coveragerc b/.coveragerc index df371e1b683..9f6bb0d1b95 100644 --- a/.coveragerc +++ b/.coveragerc @@ -47,6 +47,7 @@ omit = homeassistant/components/august/* homeassistant/components/automatic/device_tracker.py homeassistant/components/avion/light.py + homeassistant/components/azure_event_hub/* homeassistant/components/baidu/tts.py homeassistant/components/bbb_gpio/* homeassistant/components/bbox/device_tracker.py diff --git a/CODEOWNERS b/CODEOWNERS index a1581d4720c..1e7c3c87a07 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -32,6 +32,7 @@ homeassistant/components/automatic/* @armills homeassistant/components/automation/* @home-assistant/core homeassistant/components/aws/* @awarecan @robbiet480 homeassistant/components/axis/* @kane610 +homeassistant/components/azure_event_hub/* @eavanvalkenburg homeassistant/components/bitcoin/* @fabaff homeassistant/components/bizkaibus/* @UgaitzEtxebarria homeassistant/components/blink/* @fronzbot diff --git a/homeassistant/components/azure_event_hub/__init__.py b/homeassistant/components/azure_event_hub/__init__.py new file mode 100644 index 00000000000..c5362fe1821 --- /dev/null +++ b/homeassistant/components/azure_event_hub/__init__.py @@ -0,0 +1,80 @@ +"""Support for Azure Event Hubs.""" +import json +import logging +from typing import Any, Dict + +import voluptuous as vol +from azure.eventhub import EventData, EventHubClientAsync + +from homeassistant.const import ( + EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED, STATE_UNAVAILABLE, + STATE_UNKNOWN) +from homeassistant.core import Event, HomeAssistant +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entityfilter import FILTER_SCHEMA +from homeassistant.helpers.json import JSONEncoder + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'azure_event_hub' + +CONF_EVENT_HUB_NAMESPACE = 'event_hub_namespace' +CONF_EVENT_HUB_INSTANCE_NAME = 'event_hub_instance_name' +CONF_EVENT_HUB_SAS_POLICY = 'event_hub_sas_policy' +CONF_EVENT_HUB_SAS_KEY = 'event_hub_sas_key' +CONF_FILTER = 'filter' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_EVENT_HUB_NAMESPACE): cv.string, + vol.Required(CONF_EVENT_HUB_INSTANCE_NAME): cv.string, + vol.Required(CONF_EVENT_HUB_SAS_POLICY): cv.string, + vol.Required(CONF_EVENT_HUB_SAS_KEY): cv.string, + vol.Required(CONF_FILTER): FILTER_SCHEMA, + }), +}, extra=vol.ALLOW_EXTRA) + + +async def async_setup(hass: HomeAssistant, yaml_config: Dict[str, Any]): + """Activate Azure EH component.""" + config = yaml_config[DOMAIN] + + event_hub_address = "amqps://{}.servicebus.windows.net/{}".format( + config[CONF_EVENT_HUB_NAMESPACE], + config[CONF_EVENT_HUB_INSTANCE_NAME]) + entities_filter = config[CONF_FILTER] + + client = EventHubClientAsync( + event_hub_address, + debug=True, + username=config[CONF_EVENT_HUB_SAS_POLICY], + password=config[CONF_EVENT_HUB_SAS_KEY]) + async_sender = client.add_async_sender() + await client.run_async() + + encoder = JSONEncoder() + + async def async_send_to_event_hub(event: Event): + """Send states to Event Hub.""" + state = event.data.get('new_state') + if (state is None + or state.state in (STATE_UNKNOWN, '', STATE_UNAVAILABLE) + or not entities_filter(state.entity_id)): + return + + event_data = EventData( + json.dumps( + obj=state.as_dict(), + default=encoder.encode + ).encode('utf-8') + ) + await async_sender.send(event_data) + + async def async_shutdown(event: Event): + """Shut down the client.""" + await client.stop_async() + + hass.bus.async_listen(EVENT_STATE_CHANGED, async_send_to_event_hub) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_shutdown) + + return True diff --git a/homeassistant/components/azure_event_hub/manifest.json b/homeassistant/components/azure_event_hub/manifest.json new file mode 100644 index 00000000000..e2223fc97c3 --- /dev/null +++ b/homeassistant/components/azure_event_hub/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "azure_event_hub", + "name": "Azure Event Hub", + "documentation": "https://www.home-assistant.io/components/azure_event_hub", + "requirements": ["azure-eventhub==1.3.1"], + "dependencies": [], + "codeowners": ["@eavanvalkenburg"] + } \ No newline at end of file diff --git a/requirements_all.txt b/requirements_all.txt index 545ff132bed..3287c6ef46e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -216,6 +216,9 @@ av==6.1.2 # homeassistant.components.axis axis==23 +# homeassistant.components.azure_event_hub +azure-eventhub==1.3.1 + # homeassistant.components.baidu baidu-aip==1.6.6 From 96b7bb625d5e7dc1cbf9633b19aa4c4d4675e25f Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Sun, 26 May 2019 13:01:29 +0100 Subject: [PATCH 145/232] geniushub: fix sensor battery level, and bump client (#24123) * Initial commit * bump client --- homeassistant/components/geniushub/manifest.json | 2 +- homeassistant/components/geniushub/sensor.py | 3 ++- requirements_all.txt | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/geniushub/manifest.json b/homeassistant/components/geniushub/manifest.json index e1708d0e836..b2c7286a2d5 100644 --- a/homeassistant/components/geniushub/manifest.json +++ b/homeassistant/components/geniushub/manifest.json @@ -3,7 +3,7 @@ "name": "Genius Hub", "documentation": "https://www.home-assistant.io/components/geniushub", "requirements": [ - "geniushub-client==0.4.9" + "geniushub-client==0.4.11" ], "dependencies": [], "codeowners": ["@zxdavb"] diff --git a/homeassistant/components/geniushub/sensor.py b/homeassistant/components/geniushub/sensor.py index 744906b8f2d..ef148b48143 100644 --- a/homeassistant/components/geniushub/sensor.py +++ b/homeassistant/components/geniushub/sensor.py @@ -76,7 +76,8 @@ class GeniusDevice(Entity): @property def state(self): """Return the state of the sensor.""" - return self._device.state['batteryLevel'] + level = self._device.state['batteryLevel'] + return level if level != 255 else 0 @property def device_state_attributes(self): diff --git a/requirements_all.txt b/requirements_all.txt index 3287c6ef46e..81582926d95 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -487,7 +487,7 @@ gearbest_parser==1.0.7 geizhals==0.0.9 # homeassistant.components.geniushub -geniushub-client==0.4.9 +geniushub-client==0.4.11 # homeassistant.components.geo_json_events # homeassistant.components.nsw_rural_fire_service_feed From 179fb0f3b56b1f3cbd09170c3b01ba678290bebd Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 26 May 2019 11:58:42 -0700 Subject: [PATCH 146/232] Use importlib metadata to check installed packages (#24114) * Use importlib metadata * Fix script * Remove unused import * Update requirements" --- homeassistant/package_constraints.txt | 1 + homeassistant/requirements.py | 57 +-------------------------- homeassistant/scripts/__init__.py | 10 ++--- homeassistant/util/package.py | 24 +++++++++++ requirements_all.txt | 1 + setup.py | 1 + tests/test_requirements.py | 51 +----------------------- tests/util/test_package.py | 18 +++++++++ 8 files changed, 52 insertions(+), 111 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index b81f7423f30..8ae1023e1a9 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -4,6 +4,7 @@ async_timeout==3.0.1 attrs==19.1.0 bcrypt==3.1.6 certifi>=2018.04.16 +importlib-metadata==0.15 jinja2>=2.10 PyJWT==1.7.1 cryptography==2.6.1 diff --git a/homeassistant/requirements.py b/homeassistant/requirements.py index 5039fbbd41e..3a1081e4b87 100644 --- a/homeassistant/requirements.py +++ b/homeassistant/requirements.py @@ -3,11 +3,7 @@ import asyncio from functools import partial import logging import os -import sys from typing import Any, Dict, List, Optional -from urllib.parse import urlparse - -import pkg_resources import homeassistant.util.package as pkg_util from homeassistant.core import HomeAssistant @@ -28,16 +24,12 @@ async def async_process_requirements(hass: HomeAssistant, name: str, if pip_lock is None: pip_lock = hass.data[DATA_PIP_LOCK] = asyncio.Lock() - pkg_cache = hass.data.get(DATA_PKG_CACHE) - if pkg_cache is None: - pkg_cache = hass.data[DATA_PKG_CACHE] = PackageLoadable(hass) - pip_install = partial(pkg_util.install_package, **pip_kwargs(hass.config.config_dir)) async with pip_lock: for req in requirements: - if await pkg_cache.loadable(req): + if pkg_util.is_installed(req): continue ret = await hass.async_add_executor_job(pip_install, req) @@ -58,50 +50,3 @@ def pip_kwargs(config_dir: Optional[str]) -> Dict[str, Any]: if not (config_dir is None or pkg_util.is_virtual_env()): kwargs['target'] = os.path.join(config_dir, 'deps') return kwargs - - -class PackageLoadable: - """Class to check if a package is loadable, with built-in cache.""" - - def __init__(self, hass: HomeAssistant) -> None: - """Initialize the PackageLoadable class.""" - self.dist_cache = {} # type: Dict[str, pkg_resources.Distribution] - self.hass = hass - - async def loadable(self, package: str) -> bool: - """Check if a package is what will be loaded when we import it. - - Returns True when the requirement is met. - Returns False when the package is not installed or doesn't meet req. - """ - dist_cache = self.dist_cache - - try: - req = pkg_resources.Requirement.parse(package) - except ValueError: - # This is a zip file. We no longer use this in Home Assistant, - # leaving it in for custom components. - req = pkg_resources.Requirement.parse(urlparse(package).fragment) - - req_proj_name = req.project_name.lower() - dist = dist_cache.get(req_proj_name) - - if dist is not None: - return dist in req - - for path in sys.path: - # We read the whole mount point as we're already here - # Caching it on first call makes subsequent calls a lot faster. - await self.hass.async_add_executor_job(self._fill_cache, path) - - dist = dist_cache.get(req_proj_name) - if dist is not None: - return dist in req - - return False - - def _fill_cache(self, path: str) -> None: - """Add packages from a path to the cache.""" - dist_cache = self.dist_cache - for dist in pkg_resources.find_distributions(path): - dist_cache.setdefault(dist.project_name.lower(), dist) diff --git a/homeassistant/scripts/__init__.py b/homeassistant/scripts/__init__.py index 070d907a7d9..961ce5a9d13 100644 --- a/homeassistant/scripts/__init__.py +++ b/homeassistant/scripts/__init__.py @@ -9,9 +9,9 @@ from typing import List from homeassistant.bootstrap import async_mount_local_lib_path from homeassistant.config import get_default_config_dir -from homeassistant.core import HomeAssistant -from homeassistant.requirements import pip_kwargs, PackageLoadable -from homeassistant.util.package import install_package, is_virtual_env +from homeassistant.requirements import pip_kwargs +from homeassistant.util.package import ( + install_package, is_virtual_env, is_installed) def run(args: List) -> int: @@ -49,10 +49,8 @@ def run(args: List) -> int: logging.basicConfig(stream=sys.stdout, level=logging.INFO) - hass = HomeAssistant(loop) - pkgload = PackageLoadable(hass) for req in getattr(script, 'REQUIREMENTS', []): - if loop.run_until_complete(pkgload.loadable(req)): + if is_installed(req): continue if not install_package(req, **_pip_kwargs): diff --git a/homeassistant/util/package.py b/homeassistant/util/package.py index 925755eb741..3a34ab0a365 100644 --- a/homeassistant/util/package.py +++ b/homeassistant/util/package.py @@ -5,6 +5,11 @@ import os from subprocess import PIPE, Popen import sys from typing import Optional +from urllib.parse import urlparse + +import pkg_resources +from importlib_metadata import version, PackageNotFoundError + _LOGGER = logging.getLogger(__name__) @@ -16,6 +21,25 @@ def is_virtual_env() -> bool: hasattr(sys, 'real_prefix')) +def is_installed(package: str) -> bool: + """Check if a package is installed and will be loaded when we import it. + + Returns True when the requirement is met. + Returns False when the package is not installed or doesn't meet req. + """ + try: + req = pkg_resources.Requirement.parse(package) + except ValueError: + # This is a zip file. We no longer use this in Home Assistant, + # leaving it in for custom components. + req = pkg_resources.Requirement.parse(urlparse(package).fragment) + + try: + return version(req.project_name) in req + except PackageNotFoundError: + return False + + def install_package(package: str, upgrade: bool = True, target: Optional[str] = None, constraints: Optional[str] = None) -> bool: diff --git a/requirements_all.txt b/requirements_all.txt index 81582926d95..31569b62a47 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -5,6 +5,7 @@ async_timeout==3.0.1 attrs==19.1.0 bcrypt==3.1.6 certifi>=2018.04.16 +importlib-metadata==0.15 jinja2>=2.10 PyJWT==1.7.1 cryptography==2.6.1 diff --git a/setup.py b/setup.py index b1b66e0ca01..2ae5d8e8c3b 100755 --- a/setup.py +++ b/setup.py @@ -38,6 +38,7 @@ REQUIRES = [ 'attrs==19.1.0', 'bcrypt==3.1.6', 'certifi>=2018.04.16', + 'importlib-metadata==0.15', 'jinja2>=2.10', 'PyJWT==1.7.1', # PyJWT has loose dependency. We want the latest one. diff --git a/tests/test_requirements.py b/tests/test_requirements.py index dcc107ea07e..c061e37ca0a 100644 --- a/tests/test_requirements.py +++ b/tests/test_requirements.py @@ -4,21 +4,11 @@ from unittest.mock import patch, call from homeassistant import setup from homeassistant.requirements import ( - CONSTRAINT_FILE, PackageLoadable, async_process_requirements) - -import pkg_resources + CONSTRAINT_FILE, async_process_requirements) from tests.common import ( get_test_home_assistant, MockModule, mock_coro, mock_integration) -RESOURCE_DIR = os.path.abspath( - os.path.join(os.path.dirname(__file__), '..', 'resources')) - -TEST_NEW_REQ = 'pyhelloworld3==1.0.0' - -TEST_ZIP_REQ = 'file://{}#{}' \ - .format(os.path.join(RESOURCE_DIR, 'pyhelloworld3.zip'), TEST_NEW_REQ) - class TestRequirements: """Test the requirements module.""" @@ -80,47 +70,10 @@ async def test_install_existing_package(hass): assert len(mock_inst.mock_calls) == 1 - with patch('homeassistant.requirements.PackageLoadable.loadable', - return_value=mock_coro(True)), \ + with patch('homeassistant.util.package.is_installed', return_value=True), \ patch( 'homeassistant.util.package.install_package') as mock_inst: assert await async_process_requirements( hass, 'test_component', ['hello==1.0.0']) assert len(mock_inst.mock_calls) == 0 - - -async def test_check_package_global(hass): - """Test for an installed package.""" - installed_package = list(pkg_resources.working_set)[0].project_name - assert await PackageLoadable(hass).loadable(installed_package) - - -async def test_check_package_zip(hass): - """Test for an installed zip package.""" - assert not await PackageLoadable(hass).loadable(TEST_ZIP_REQ) - - -async def test_package_loadable_installed_twice(hass): - """Test that a package is loadable when installed twice. - - If a package is installed twice, only the first version will be imported. - Test that package_loadable will only compare with the first package. - """ - v1 = pkg_resources.Distribution(project_name='hello', version='1.0.0') - v2 = pkg_resources.Distribution(project_name='hello', version='2.0.0') - - with patch('pkg_resources.find_distributions', side_effect=[[v1]]): - assert not await PackageLoadable(hass).loadable('hello==2.0.0') - - with patch('pkg_resources.find_distributions', side_effect=[[v1], [v2]]): - assert not await PackageLoadable(hass).loadable('hello==2.0.0') - - with patch('pkg_resources.find_distributions', side_effect=[[v2], [v1]]): - assert await PackageLoadable(hass).loadable('hello==2.0.0') - - with patch('pkg_resources.find_distributions', side_effect=[[v2]]): - assert await PackageLoadable(hass).loadable('hello==2.0.0') - - with patch('pkg_resources.find_distributions', side_effect=[[v2]]): - assert await PackageLoadable(hass).loadable('Hello==2.0.0') diff --git a/tests/util/test_package.py b/tests/util/test_package.py index 5422140c232..41af56265d2 100644 --- a/tests/util/test_package.py +++ b/tests/util/test_package.py @@ -6,13 +6,20 @@ import sys from subprocess import PIPE from unittest.mock import MagicMock, call, patch +import pkg_resources import pytest import homeassistant.util.package as package +RESOURCE_DIR = os.path.abspath( + os.path.join(os.path.dirname(__file__), '..', 'resources')) + TEST_NEW_REQ = 'pyhelloworld3==1.0.0' +TEST_ZIP_REQ = 'file://{}#{}' \ + .format(os.path.join(RESOURCE_DIR, 'pyhelloworld3.zip'), TEST_NEW_REQ) + @pytest.fixture def mock_sys(): @@ -176,3 +183,14 @@ def test_async_get_user_site(mock_env_copy): stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.DEVNULL, env=env) assert ret == os.path.join(deps_dir, 'lib_dir') + + +def test_check_package_global(): + """Test for an installed package.""" + installed_package = list(pkg_resources.working_set)[0].project_name + assert package.is_installed(installed_package) + + +def test_check_package_zip(): + """Test for an installed zip package.""" + assert not package.is_installed(TEST_ZIP_REQ) From 97b671171b9589e95efabe97eb16f1c797c202dd Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Sun, 26 May 2019 21:49:26 +0200 Subject: [PATCH 147/232] Avoid useless Sonos state updates (#24135) --- homeassistant/components/sonos/media_player.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 40369597646..2a96308c587 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -433,6 +433,9 @@ class SonosEntity(MediaPlayerDevice): self._shuffle = self.soco.shuffle + update_position = (new_status != self._status) + self._status = new_status + if self.soco.is_playing_tv: self.update_media_linein(SOURCE_TV) elif self.soco.is_playing_line_in: @@ -444,11 +447,8 @@ class SonosEntity(MediaPlayerDevice): variables = event and event.variables self.update_media_radio(variables, track_info) else: - update_position = (new_status != self._status) self.update_media_music(update_position, track_info) - self._status = new_status - self.schedule_update_ha_state() # Also update slaves @@ -550,7 +550,9 @@ class SonosEntity(MediaPlayerDevice): self._media_position is None # position jumped? - if rel_time is not None and self._media_position is not None: + if (self.state == STATE_PLAYING + and rel_time is not None + and self._media_position is not None): time_diff = utcnow() - self._media_position_updated_at time_diff = time_diff.total_seconds() From 9debbfb1a8be2eae6a85163735623fc009fdd6dc Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 26 May 2019 19:48:27 -0700 Subject: [PATCH 148/232] Add SSDP integration (#24090) * Add SSDP integration * Fix tests * Sort all the things * Add netdisco to test requirements --- .../components/default_config/manifest.json | 1 + .../components/discovery/__init__.py | 2 - homeassistant/components/hue/config_flow.py | 11 +- homeassistant/components/hue/manifest.json | 5 + homeassistant/components/hue/strings.json | 3 +- homeassistant/components/ssdp/__init__.py | 170 ++++++++++++++++++ homeassistant/components/ssdp/manifest.json | 12 ++ homeassistant/components/zeroconf/__init__.py | 6 +- homeassistant/config_entries.py | 2 + homeassistant/data_entry_flow.py | 2 + homeassistant/generated/ssdp.py | 15 ++ requirements_all.txt | 1 + requirements_test_all.txt | 4 + script/gen_requirements_all.py | 1 + script/hassfest/__main__.py | 18 +- script/hassfest/manifest.py | 5 + script/hassfest/ssdp.py | 88 +++++++++ script/hassfest/zeroconf.py | 13 ++ tests/components/hue/test_config_flow.py | 15 +- tests/components/ssdp/__init__.py | 1 + tests/components/ssdp/test_init.py | 78 ++++++++ tests/test_data_entry_flow.py | 11 +- 22 files changed, 436 insertions(+), 28 deletions(-) create mode 100644 homeassistant/components/ssdp/__init__.py create mode 100644 homeassistant/components/ssdp/manifest.json create mode 100644 homeassistant/generated/ssdp.py create mode 100644 script/hassfest/ssdp.py create mode 100644 tests/components/ssdp/__init__.py create mode 100644 tests/components/ssdp/test_init.py diff --git a/homeassistant/components/default_config/manifest.json b/homeassistant/components/default_config/manifest.json index f52da35dc64..992cb71c07c 100644 --- a/homeassistant/components/default_config/manifest.json +++ b/homeassistant/components/default_config/manifest.json @@ -15,6 +15,7 @@ "mobile_app", "person", "script", + "ssdp", "sun", "system_health", "updater", diff --git a/homeassistant/components/discovery/__init__.py b/homeassistant/components/discovery/__init__.py index 7f05c70653c..130898018d5 100644 --- a/homeassistant/components/discovery/__init__.py +++ b/homeassistant/components/discovery/__init__.py @@ -33,7 +33,6 @@ SERVICE_HASS_IOS_APP = 'hass_ios' SERVICE_HASSIO = 'hassio' SERVICE_HOMEKIT = 'homekit' SERVICE_HEOS = 'heos' -SERVICE_HUE = 'philips_hue' SERVICE_IGD = 'igd' SERVICE_IKEA_TRADFRI = 'ikea_tradfri' SERVICE_KONNECTED = 'konnected' @@ -54,7 +53,6 @@ CONFIG_ENTRY_HANDLERS = { SERVICE_DECONZ: 'deconz', 'google_cast': 'cast', SERVICE_HEOS: 'heos', - SERVICE_HUE: 'hue', SERVICE_TELLDUSLIVE: 'tellduslive', SERVICE_IKEA_TRADFRI: 'tradfri', 'sonos': 'sonos', diff --git a/homeassistant/components/hue/config_flow.py b/homeassistant/components/hue/config_flow.py index 89dc0b9aa67..4167027bf89 100644 --- a/homeassistant/components/hue/config_flow.py +++ b/homeassistant/components/hue/config_flow.py @@ -137,17 +137,22 @@ class HueFlowHandler(config_entries.ConfigFlow): errors=errors, ) - async def async_step_discovery(self, discovery_info): + async def async_step_ssdp(self, discovery_info): """Handle a discovered Hue bridge. - This flow is triggered by the discovery component. It will check if the + This flow is triggered by the SSDP component. It will check if the host is already configured and delegate to the import step if not. """ # Filter out emulated Hue if "HASS Bridge" in discovery_info.get('name', ''): return self.async_abort(reason='already_configured') - host = discovery_info.get('host') + # pylint: disable=unsupported-assignment-operation + host = self.context['host'] = discovery_info.get('host') + + if any(host == flow['context']['host'] + for flow in self._async_in_progress()): + return self.async_abort(reason='already_in_progress') if host in configured_hosts(self.hass): return self.async_abort(reason='already_configured') diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json index d035e4468e4..d16988529b1 100644 --- a/homeassistant/components/hue/manifest.json +++ b/homeassistant/components/hue/manifest.json @@ -6,6 +6,11 @@ "requirements": [ "aiohue==1.9.1" ], + "ssdp": { + "manufacturer": [ + "Royal Philips Electronics" + ] + }, "dependencies": [], "codeowners": [ "@balloob" diff --git a/homeassistant/components/hue/strings.json b/homeassistant/components/hue/strings.json index f8873894a01..079ac1a2b8d 100644 --- a/homeassistant/components/hue/strings.json +++ b/homeassistant/components/hue/strings.json @@ -23,7 +23,8 @@ "all_configured": "All Philips Hue bridges are already configured", "unknown": "Unknown error occurred", "cannot_connect": "Unable to connect to the bridge", - "already_configured": "Bridge is already configured" + "already_configured": "Bridge is already configured", + "already_in_progress": "Config flow for bridge is already in progress." } } } diff --git a/homeassistant/components/ssdp/__init__.py b/homeassistant/components/ssdp/__init__.py new file mode 100644 index 00000000000..af24dd22a89 --- /dev/null +++ b/homeassistant/components/ssdp/__init__.py @@ -0,0 +1,170 @@ +"""The SSDP integration.""" +import asyncio +from datetime import timedelta +import logging +from urllib.parse import urlparse +from xml.etree import ElementTree + +import aiohttp +from netdisco import ssdp, util + +from homeassistant.helpers.event import async_track_time_interval +from homeassistant.generated.ssdp import SSDP + +DOMAIN = 'ssdp' +SCAN_INTERVAL = timedelta(seconds=60) + +ATTR_HOST = 'host' +ATTR_PORT = 'port' +ATTR_SSDP_DESCRIPTION = 'ssdp_description' +ATTR_ST = 'ssdp_st' +ATTR_NAME = 'name' +ATTR_MODEL_NAME = 'model_name' +ATTR_MODEL_NUMBER = 'model_number' +ATTR_SERIAL = 'serial_number' +ATTR_MANUFACTURER = 'manufacturer' +ATTR_UDN = 'udn' +ATTR_UPNP_DEVICE_TYPE = 'upnp_device_type' + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass, config): + """Set up the SSDP integration.""" + async def initialize(): + scanner = Scanner(hass) + await scanner.async_scan(None) + async_track_time_interval(hass, scanner.async_scan, SCAN_INTERVAL) + + hass.loop.create_task(initialize()) + + return True + + +class Scanner: + """Class to manage SSDP scanning.""" + + def __init__(self, hass): + """Initialize class.""" + self.hass = hass + self.seen = set() + self._description_cache = {} + + async def async_scan(self, _): + """Scan for new entries.""" + _LOGGER.debug("Scanning") + # Run 3 times as packets can get lost + for _ in range(3): + entries = await self.hass.async_add_executor_job(ssdp.scan) + await self._process_entries(entries) + + # We clear the cache after each run. We track discovered entries + # so will never need a description twice. + self._description_cache.clear() + + async def _process_entries(self, entries): + """Process SSDP entries.""" + tasks = [] + + for entry in entries: + key = (entry.st, entry.location) + + if key in self.seen: + continue + + self.seen.add(key) + + tasks.append(self._process_entry(entry)) + + if not tasks: + return + + to_load = [result for result in await asyncio.gather(*tasks) + if result is not None] + + if not to_load: + return + + for entry, info, domains in to_load: + + for domain in domains: + _LOGGER.debug("Discovered %s at %s", domain, entry.location) + await self.hass.config_entries.flow.async_init( + domain, context={'source': DOMAIN}, data=info + ) + + async def _process_entry(self, entry): + """Process a single entry.""" + domains = set(SSDP["st"].get(entry.st, [])) + + xml_location = entry.location + + if not xml_location: + if domains: + return (entry, info_from_entry(entry, None), domains) + return None + + # Multiple entries usally share same location. Make sure + # we fetch it only once. + info_req = self._description_cache.get(xml_location) + + if info_req is None: + info_req = self._description_cache[xml_location] = \ + self.hass.async_create_task( + self._fetch_description(xml_location)) + + info = await info_req + + domains.update(SSDP["manufacturer"].get(info.get('manufacturer'), [])) + domains.update(SSDP["device_type"].get(info.get('deviceType'), [])) + + if domains: + return (entry, info_from_entry(entry, info), domains) + + return None + + async def _fetch_description(self, xml_location): + """Fetch an XML description.""" + session = self.hass.helpers.aiohttp_client.async_get_clientsession() + try: + resp = await session.get(xml_location, timeout=5) + xml = await resp.text() + + # Samsung Smart TV sometimes returns an empty document the + # first time. Retry once. + if not xml: + resp = await session.get(xml_location, timeout=5) + xml = await resp.text() + except aiohttp.ClientError as err: + _LOGGER.debug("Error fetching %s: %s", xml_location, err) + return None + + try: + tree = ElementTree.fromstring(xml) + except ElementTree.ParseError as err: + _LOGGER.debug("Error parsing %s: %s", xml_location, err) + return None + + return util.etree_to_dict(tree).get('root', {}).get('device', {}) + + +def info_from_entry(entry, device_info): + """Get most important info from an entry.""" + url = urlparse(entry.location) + info = { + ATTR_HOST: url.hostname, + ATTR_PORT: url.port, + ATTR_SSDP_DESCRIPTION: entry.location, + ATTR_ST: entry.st, + } + + if device_info: + info[ATTR_NAME] = device_info.get('friendlyName') + info[ATTR_MODEL_NAME] = device_info.get('modelName') + info[ATTR_MODEL_NUMBER] = device_info.get('modelNumber') + info[ATTR_SERIAL] = device_info.get('serialNumber') + info[ATTR_MANUFACTURER] = device_info.get('manufacturer') + info[ATTR_UDN] = device_info.get('UDN') + info[ATTR_UPNP_DEVICE_TYPE] = device_info.get('deviceType') + + return info diff --git a/homeassistant/components/ssdp/manifest.json b/homeassistant/components/ssdp/manifest.json new file mode 100644 index 00000000000..ce00bcbc888 --- /dev/null +++ b/homeassistant/components/ssdp/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "ssdp", + "name": "SSDP", + "documentation": "https://www.home-assistant.io/components/ssdp", + "requirements": [ + "netdisco==2.6.0" + ], + "dependencies": [ + ], + "codeowners": [ + ] +} diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index 161321d1e88..a0185a3bbe2 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -53,10 +53,8 @@ async def async_setup(hass, config): _LOGGER.debug("Discovered new device %s %s", name, info) for domain in zeroconf_manifest.SERVICE_TYPES[service_type]: - hass.async_create_task( - hass.config_entries.flow.async_init( - domain, context={'source': DOMAIN}, data=info - ) + await hass.config_entries.flow.async_init( + domain, context={'source': DOMAIN}, data=info ) def service_update(_, service_type, name, state_change): diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index e96c10e17fa..299bfe9b407 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -169,6 +169,8 @@ UNRECOVERABLE_STATES = ( DISCOVERY_NOTIFICATION_ID = 'config_entry_discovery' DISCOVERY_SOURCES = ( + 'ssdp', + 'zeroconf', SOURCE_DISCOVERY, SOURCE_IMPORT, ) diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index aa1d21a66d3..389b8498421 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -58,6 +58,8 @@ class FlowManager: context: Optional[Dict] = None, data: Any = None) -> Any: """Start a configuration flow.""" + if context is None: + context = {} flow = await self._async_create_flow( handler, context=context, data=data) flow.hass = self.hass diff --git a/homeassistant/generated/ssdp.py b/homeassistant/generated/ssdp.py new file mode 100644 index 00000000000..897f68a6521 --- /dev/null +++ b/homeassistant/generated/ssdp.py @@ -0,0 +1,15 @@ +"""Automatically generated by hassfest. + +To update, run python3 -m hassfest +""" + + +SSDP = { + "device_type": {}, + "manufacturer": { + "Royal Philips Electronics": [ + "hue" + ] + }, + "st": {} +} diff --git a/requirements_all.txt b/requirements_all.txt index 31569b62a47..b6824d3d894 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -778,6 +778,7 @@ nessclient==0.9.15 netdata==0.1.2 # homeassistant.components.discovery +# homeassistant.components.ssdp netdisco==2.6.0 # homeassistant.components.neurio_energy diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 14c074b46d8..137a1662b00 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -181,6 +181,10 @@ mbddns==0.1.2 # homeassistant.components.mfi mficlient==0.3.0 +# homeassistant.components.discovery +# homeassistant.components.ssdp +netdisco==2.6.0 + # homeassistant.components.iqvia # homeassistant.components.opencv # homeassistant.components.tensorflow diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 108d0bcab07..a5f24be51de 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -89,6 +89,7 @@ TEST_REQUIREMENTS = ( 'luftdaten', 'mbddns', 'mficlient', + 'netdisco', 'numpy', 'oauth2client', 'paho-mqtt', diff --git a/script/hassfest/__main__.py b/script/hassfest/__main__.py index 6a6b19aada7..5ee52e72f7a 100644 --- a/script/hassfest/__main__.py +++ b/script/hassfest/__main__.py @@ -4,15 +4,23 @@ import sys from .model import Integration, Config from . import ( - dependencies, manifest, codeowners, services, config_flow, zeroconf) + codeowners, + config_flow, + dependencies, + manifest, + services, + ssdp, + zeroconf, +) PLUGINS = [ - manifest, - dependencies, codeowners, - services, config_flow, - zeroconf + dependencies, + manifest, + services, + ssdp, + zeroconf, ] diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index cfb2fdc006a..31181ed76bd 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -12,6 +12,11 @@ MANIFEST_SCHEMA = vol.Schema({ vol.Required('name'): str, vol.Optional('config_flow'): bool, vol.Optional('zeroconf'): [str], + vol.Optional('ssdp'): vol.Schema({ + vol.Optional('st'): [str], + vol.Optional('manufacturer'): [str], + vol.Optional('device_type'): [str], + }), vol.Required('documentation'): str, vol.Required('requirements'): [str], vol.Required('dependencies'): [str], diff --git a/script/hassfest/ssdp.py b/script/hassfest/ssdp.py new file mode 100644 index 00000000000..b5c4b9721c0 --- /dev/null +++ b/script/hassfest/ssdp.py @@ -0,0 +1,88 @@ +"""Generate ssdp file.""" +from collections import OrderedDict, defaultdict +import json +from typing import Dict + +from .model import Integration, Config + +BASE = """ +\"\"\"Automatically generated by hassfest. + +To update, run python3 -m hassfest +\"\"\" + + +SSDP = {} +""".strip() + + +def sort_dict(value): + """Sort a dictionary.""" + return OrderedDict((key, value[key]) + for key in sorted(value)) + + +def generate_and_validate(integrations: Dict[str, Integration]): + """Validate and generate ssdp data.""" + data = { + 'st': defaultdict(list), + 'manufacturer': defaultdict(list), + 'device_type': defaultdict(list), + } + + for domain in sorted(integrations): + integration = integrations[domain] + + if not integration.manifest: + continue + + ssdp = integration.manifest.get('ssdp') + + if not ssdp: + continue + + try: + with open(str(integration.path / "config_flow.py")) as fp: + if ' async_step_ssdp(' not in fp.read(): + integration.add_error( + 'ssdp', 'Config flow has no async_step_ssdp') + continue + except FileNotFoundError: + integration.add_error( + 'ssdp', + 'SSDP info in a manifest requires a config flow to exist' + ) + continue + + for key in 'st', 'manufacturer', 'device_type': + if key not in ssdp: + continue + + for value in ssdp[key]: + data[key][value].append(domain) + + data = sort_dict({key: sort_dict(value) for key, value in data.items()}) + return BASE.format(json.dumps(data, indent=4)) + + +def validate(integrations: Dict[str, Integration], config: Config): + """Validate ssdp file.""" + ssdp_path = config.root / 'homeassistant/generated/ssdp.py' + config.cache['ssdp'] = content = generate_and_validate(integrations) + + with open(str(ssdp_path), 'r') as fp: + if fp.read().strip() != content: + config.add_error( + "ssdp", + "File ssdp.py is not up to date. " + "Run python3 -m script.hassfest", + fixable=True + ) + return + + +def generate(integrations: Dict[str, Integration], config: Config): + """Generate ssdp file.""" + ssdp_path = config.root / 'homeassistant/generated/ssdp.py' + with open(str(ssdp_path), 'w') as fp: + fp.write(config.cache['ssdp'] + '\n') diff --git a/script/hassfest/zeroconf.py b/script/hassfest/zeroconf.py index 26e302c864c..9cec4a7211b 100644 --- a/script/hassfest/zeroconf.py +++ b/script/hassfest/zeroconf.py @@ -31,6 +31,19 @@ def generate_and_validate(integrations: Dict[str, Integration]): if not service_types: continue + try: + with open(str(integration.path / "config_flow.py")) as fp: + if ' async_step_zeroconf(' not in fp.read(): + integration.add_error( + 'zeroconf', 'Config flow has no async_step_zeroconf') + continue + except FileNotFoundError: + integration.add_error( + 'zeroconf', + 'Zeroconf info in a manifest requires a config flow to exist' + ) + continue + for service_type in service_types: if service_type not in service_type_dict: diff --git a/tests/components/hue/test_config_flow.py b/tests/components/hue/test_config_flow.py index 78b7ba0269c..37cece0bbd8 100644 --- a/tests/components/hue/test_config_flow.py +++ b/tests/components/hue/test_config_flow.py @@ -185,14 +185,15 @@ async def test_flow_link_unknown_host(hass): } -async def test_bridge_discovery(hass): +async def test_bridge_ssdp(hass): """Test a bridge being discovered.""" flow = config_flow.HueFlowHandler() flow.hass = hass + flow.context = {} with patch.object(config_flow, 'get_bridge', side_effect=errors.AuthenticationRequired): - result = await flow.async_step_discovery({ + result = await flow.async_step_ssdp({ 'host': '0.0.0.0', 'serial': '1234' }) @@ -201,12 +202,13 @@ async def test_bridge_discovery(hass): assert result['step_id'] == 'link' -async def test_bridge_discovery_emulated_hue(hass): +async def test_bridge_ssdp_emulated_hue(hass): """Test if discovery info is from an emulated hue instance.""" flow = config_flow.HueFlowHandler() flow.hass = hass + flow.context = {} - result = await flow.async_step_discovery({ + result = await flow.async_step_ssdp({ 'name': 'HASS Bridge', 'host': '0.0.0.0', 'serial': '1234' @@ -215,7 +217,7 @@ async def test_bridge_discovery_emulated_hue(hass): assert result['type'] == 'abort' -async def test_bridge_discovery_already_configured(hass): +async def test_bridge_ssdp_already_configured(hass): """Test if a discovered bridge has already been configured.""" MockConfigEntry(domain='hue', data={ 'host': '0.0.0.0' @@ -223,8 +225,9 @@ async def test_bridge_discovery_already_configured(hass): flow = config_flow.HueFlowHandler() flow.hass = hass + flow.context = {} - result = await flow.async_step_discovery({ + result = await flow.async_step_ssdp({ 'host': '0.0.0.0', 'serial': '1234' }) diff --git a/tests/components/ssdp/__init__.py b/tests/components/ssdp/__init__.py new file mode 100644 index 00000000000..b6dcb9d49b5 --- /dev/null +++ b/tests/components/ssdp/__init__.py @@ -0,0 +1 @@ +"""Tests for the SSDP integration.""" diff --git a/tests/components/ssdp/test_init.py b/tests/components/ssdp/test_init.py new file mode 100644 index 00000000000..7ded5f12329 --- /dev/null +++ b/tests/components/ssdp/test_init.py @@ -0,0 +1,78 @@ +"""Test the SSDP integration.""" +from unittest.mock import patch, Mock + +from homeassistant.generated import ssdp as gn_ssdp +from homeassistant.components import ssdp + +from tests.common import mock_coro + + +async def test_scan_match_st(hass): + """Test matching based on ST.""" + scanner = ssdp.Scanner(hass) + + with patch('netdisco.ssdp.scan', return_value=[ + Mock(st="mock-st", location=None) + ]), patch.dict( + gn_ssdp.SSDP['st'], {'mock-st': ['mock-domain']} + ), patch.object( + hass.config_entries.flow, 'async_init', + return_value=mock_coro() + ) as mock_init: + await scanner.async_scan(None) + + assert len(mock_init.mock_calls) == 1 + assert mock_init.mock_calls[0][1][0] == 'mock-domain' + assert mock_init.mock_calls[0][2]['context'] == {'source': 'ssdp'} + + +async def test_scan_match_manufacturer(hass, aioclient_mock): + """Test matching based on ST.""" + aioclient_mock.get('http://1.1.1.1', text=""" + + + Paulus + + + """) + scanner = ssdp.Scanner(hass) + + with patch('netdisco.ssdp.scan', return_value=[ + Mock(st="mock-st", location='http://1.1.1.1') + ]), patch.dict( + gn_ssdp.SSDP['manufacturer'], {'Paulus': ['mock-domain']} + ), patch.object( + hass.config_entries.flow, 'async_init', + return_value=mock_coro() + ) as mock_init: + await scanner.async_scan(None) + + assert len(mock_init.mock_calls) == 1 + assert mock_init.mock_calls[0][1][0] == 'mock-domain' + assert mock_init.mock_calls[0][2]['context'] == {'source': 'ssdp'} + + +async def test_scan_match_device_type(hass, aioclient_mock): + """Test matching based on ST.""" + aioclient_mock.get('http://1.1.1.1', text=""" + + + Paulus + + + """) + scanner = ssdp.Scanner(hass) + + with patch('netdisco.ssdp.scan', return_value=[ + Mock(st="mock-st", location='http://1.1.1.1') + ]), patch.dict( + gn_ssdp.SSDP['device_type'], {'Paulus': ['mock-domain']} + ), patch.object( + hass.config_entries.flow, 'async_init', + return_value=mock_coro() + ) as mock_init: + await scanner.async_scan(None) + + assert len(mock_init.mock_calls) == 1 + assert mock_init.mock_calls[0][1][0] == 'mock-domain' + assert mock_init.mock_calls[0][2]['context'] == {'source': 'ssdp'} diff --git a/tests/test_data_entry_flow.py b/tests/test_data_entry_flow.py index f6e33d264b6..379ab35cad2 100644 --- a/tests/test_data_entry_flow.py +++ b/tests/test_data_entry_flow.py @@ -21,16 +21,13 @@ def manager(): raise data_entry_flow.UnknownHandler flow = handler() - flow.init_step = context.get('init_step', 'init') \ - if context is not None else 'init' - flow.source = context.get('source') \ - if context is not None else 'user_input' + flow.init_step = context.get('init_step', 'init') + flow.source = context.get('source') return flow async def async_add_entry(flow, result): if result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY: - result['source'] = flow.context.get('source') \ - if flow.context is not None else 'user' + result['source'] = flow.context.get('source') entries.append(result) return result @@ -173,7 +170,7 @@ async def test_create_saves_data(manager): assert entry['handler'] == 'test' assert entry['title'] == 'Test Title' assert entry['data'] == 'Test Data' - assert entry['source'] == 'user' + assert entry['source'] is None async def test_discovery_init_flow(manager): From 5c86a51b45103fe9058e6ef633b1ae6f74725762 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 27 May 2019 05:27:07 +0200 Subject: [PATCH 149/232] Lovelace: Fire event on save (#24104) * Lovelace: Fire event on save * Add event to whitelist --- homeassistant/components/lovelace/__init__.py | 4 ++++ homeassistant/components/websocket_api/permissions.py | 2 ++ 2 files changed, 6 insertions(+) diff --git a/homeassistant/components/lovelace/__init__.py b/homeassistant/components/lovelace/__init__.py index 996e3f7b296..f550f85bcef 100644 --- a/homeassistant/components/lovelace/__init__.py +++ b/homeassistant/components/lovelace/__init__.py @@ -26,6 +26,7 @@ CONFIG_SCHEMA = vol.Schema({ }), }, extra=vol.ALLOW_EXTRA) +EVENT_LOVELACE_UPDATED = 'lovelace_updated' LOVELACE_CONFIG_FILE = 'ui-lovelace.yaml' @@ -83,6 +84,7 @@ class LovelaceStorage: """Initialize Lovelace config based on storage helper.""" self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) self._data = None + self._hass = hass async def async_get_info(self): """Return the YAML storage mode.""" @@ -115,6 +117,8 @@ class LovelaceStorage: self._data['config'] = config await self._store.async_save(self._data) + self._hass.bus.async_fire(EVENT_LOVELACE_UPDATED) + async def _load(self): """Load the config.""" data = await self._store.async_load() diff --git a/homeassistant/components/websocket_api/permissions.py b/homeassistant/components/websocket_api/permissions.py index f1849fda539..753c5688d18 100644 --- a/homeassistant/components/websocket_api/permissions.py +++ b/homeassistant/components/websocket_api/permissions.py @@ -10,6 +10,7 @@ from homeassistant.const import ( EVENT_THEMES_UPDATED) from homeassistant.components.persistent_notification import ( EVENT_PERSISTENT_NOTIFICATIONS_UPDATED) +from homeassistant.components.lovelace import EVENT_LOVELACE_UPDATED from homeassistant.helpers.area_registry import EVENT_AREA_REGISTRY_UPDATED from homeassistant.helpers.device_registry import EVENT_DEVICE_REGISTRY_UPDATED from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED @@ -26,4 +27,5 @@ SUBSCRIBE_WHITELIST = { EVENT_AREA_REGISTRY_UPDATED, EVENT_DEVICE_REGISTRY_UPDATED, EVENT_ENTITY_REGISTRY_UPDATED, + EVENT_LOVELACE_UPDATED, } From 0ba54ee9b798dc1f5615c7aeb83002277d5698fa Mon Sep 17 00:00:00 2001 From: jjlawren Date: Sun, 26 May 2019 23:39:50 -0500 Subject: [PATCH 150/232] Use central polling to update entities (#24059) * Use central polling to update entities * Fix for line length * Remove unnecessary import * Use interval * Trigger entity refreshes after commands * Lint --- homeassistant/components/plex/media_player.py | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index 1d71b59cde1..5ce375ffe03 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -6,7 +6,6 @@ import logging import requests import voluptuous as vol -from homeassistant import util from homeassistant.components.media_player import ( MediaPlayerDevice, PLATFORM_SCHEMA) from homeassistant.components.media_player.const import ( @@ -16,16 +15,13 @@ from homeassistant.components.media_player.const import ( from homeassistant.const import ( DEVICE_DEFAULT_NAME, STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING) from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.event import track_utc_time_change +from homeassistant.helpers.event import track_time_interval from homeassistant.util import dt as dt_util from homeassistant.util.json import load_json, save_json _CONFIGURING = {} _LOGGER = logging.getLogger(__name__) -MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) -MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1) - NAME_FORMAT = 'Plex {}' PLEX_CONFIG_FILE = 'plex.conf' PLEX_DATA = 'plex' @@ -131,9 +127,9 @@ def setup_plexserver( plex_clients = hass.data[PLEX_DATA] plex_sessions = {} - track_utc_time_change(hass, lambda now: update_devices(), second=30) + track_time_interval( + hass, lambda now: update_devices(), timedelta(seconds=10)) - @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS) def update_devices(): """Update the devices objects.""" try: @@ -211,6 +207,9 @@ def setup_plexserver( or client.machine_identifier in plex_sessions) + if client not in new_plex_clients: + client.schedule_update_ha_state() + if not config.get(CONF_REMOVE_UNAVAILABLE_CLIENTS) \ or client.available: continue @@ -226,8 +225,6 @@ def setup_plexserver( if new_plex_clients: add_entities_callback(new_plex_clients) - update_devices() - def request_configuration(host, hass, config, add_entities_callback): """Request configuration steps from the user.""" @@ -497,6 +494,11 @@ class PlexClient(MediaPlayerDevice): self._session = None self._clear_media_details() + @property + def should_poll(self): + """Return True if entity has to be polled for state.""" + return False + @property def unique_id(self): """Return the id of this plex client.""" @@ -542,10 +544,6 @@ class PlexClient(MediaPlayerDevice): """Return the state of the device.""" return self._state - def update(self): - """Get the latest details.""" - self.update_devices(no_throttle=True) - @property def _active_media_plexapi_type(self): """Get the active media type required by PlexAPI commands.""" @@ -688,6 +686,7 @@ class PlexClient(MediaPlayerDevice): self.device.setVolume( int(volume * 100), self._active_media_plexapi_type) self._volume_level = volume # store since we can't retrieve + self.update_devices() @property def volume_level(self): @@ -724,16 +723,19 @@ class PlexClient(MediaPlayerDevice): """Send play command.""" if self.device and 'playback' in self._device_protocol_capabilities: self.device.play(self._active_media_plexapi_type) + self.update_devices() def media_pause(self): """Send pause command.""" if self.device and 'playback' in self._device_protocol_capabilities: self.device.pause(self._active_media_plexapi_type) + self.update_devices() def media_stop(self): """Send stop command.""" if self.device and 'playback' in self._device_protocol_capabilities: self.device.stop(self._active_media_plexapi_type) + self.update_devices() def turn_off(self): """Turn the client off.""" @@ -744,11 +746,13 @@ class PlexClient(MediaPlayerDevice): """Send next track command.""" if self.device and 'playback' in self._device_protocol_capabilities: self.device.skipNext(self._active_media_plexapi_type) + self.update_devices() def media_previous_track(self): """Send previous track command.""" if self.device and 'playback' in self._device_protocol_capabilities: self.device.skipPrevious(self._active_media_plexapi_type) + self.update_devices() def play_media(self, media_type, media_id, **kwargs): """Play a piece of media.""" @@ -852,6 +856,7 @@ class PlexClient(MediaPlayerDevice): '/playQueues/{}?window=100&own=1'.format( playqueue.playQueueID), }, **params)) + self.update_devices() @property def device_state_attributes(self): From 31b2f331db07da6dea533af8edfa91fd7d858ee4 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Mon, 27 May 2019 06:56:00 +0200 Subject: [PATCH 151/232] Library refactorization of deCONZ (#23725) * Improved sensors * Lib update signalling * Replace reason with changed * Move imports to top of file * Add support for secondary temperature reported by some Xiaomi devices * Bump dependency to v59 --- homeassistant/components/deconz/__init__.py | 1 + .../components/deconz/binary_sensor.py | 41 +++++++------- homeassistant/components/deconz/climate.py | 13 +++-- .../components/deconz/config_flow.py | 10 ++-- homeassistant/components/deconz/cover.py | 2 +- .../components/deconz/deconz_device.py | 2 +- homeassistant/components/deconz/gateway.py | 15 +++--- homeassistant/components/deconz/light.py | 2 +- homeassistant/components/deconz/manifest.json | 2 +- homeassistant/components/deconz/scene.py | 2 +- homeassistant/components/deconz/sensor.py | 53 ++++++++++--------- homeassistant/components/deconz/switch.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/deconz/test_binary_sensor.py | 5 +- tests/components/deconz/test_climate.py | 13 +++-- tests/components/deconz/test_config_flow.py | 12 +++-- tests/components/deconz/test_gateway.py | 3 +- tests/components/deconz/test_sensor.py | 5 +- 19 files changed, 104 insertions(+), 83 deletions(-) diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index 153e654f3fb..71e03da70b7 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -164,6 +164,7 @@ async def async_unload_entry(hass, config_entry): if not hass.data[DOMAIN]: hass.services.async_remove(DOMAIN, SERVICE_DECONZ) hass.services.async_remove(DOMAIN, SERVICE_DEVICE_REFRESH) + elif gateway.master: await async_populate_options(hass, config_entry) new_master_gateway = next(iter(hass.data[DOMAIN].values())) diff --git a/homeassistant/components/deconz/binary_sensor.py b/homeassistant/components/deconz/binary_sensor.py index fbb15abc744..6fe8b4324b3 100644 --- a/homeassistant/components/deconz/binary_sensor.py +++ b/homeassistant/components/deconz/binary_sensor.py @@ -1,6 +1,8 @@ """Support for deCONZ binary sensors.""" +from pydeconz.sensor import Presence, Vibration + from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.const import ATTR_BATTERY_LEVEL +from homeassistant.const import ATTR_BATTERY_LEVEL, ATTR_TEMPERATURE from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -15,7 +17,7 @@ ATTR_VIBRATIONSTRENGTH = 'vibrationstrength' async def async_setup_platform( hass, config, async_add_entities, discovery_info=None): - """Old way of setting up deCONZ binary sensors.""" + """Old way of setting up deCONZ platforms.""" pass @@ -26,12 +28,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities): @callback def async_add_sensor(sensors): """Add binary sensor from deCONZ.""" - from pydeconz.sensor import DECONZ_BINARY_SENSOR entities = [] for sensor in sensors: - if sensor.type in DECONZ_BINARY_SENSOR and \ + if sensor.BINARY and \ not (not gateway.allow_clip_sensor and sensor.type.startswith('CLIP')): @@ -49,16 +50,11 @@ class DeconzBinarySensor(DeconzDevice, BinarySensorDevice): """Representation of a deCONZ binary sensor.""" @callback - def async_update_callback(self, reason): - """Update the sensor's state. - - If reason is that state is updated, - or reachable has changed or battery has changed. - """ - if reason['state'] or \ - 'reachable' in reason['attr'] or \ - 'battery' in reason['attr'] or \ - 'on' in reason['attr']: + def async_update_callback(self, force_update=False): + """Update the sensor's state.""" + changed = set(self._device.changed_keys) + keys = {'battery', 'on', 'reachable', 'state'} + if force_update or any(key in changed for key in keys): self.async_schedule_update_ha_state() @property @@ -69,26 +65,33 @@ class DeconzBinarySensor(DeconzDevice, BinarySensorDevice): @property def device_class(self): """Return the class of the sensor.""" - return self._device.sensor_class + return self._device.SENSOR_CLASS @property def icon(self): """Return the icon to use in the frontend.""" - return self._device.sensor_icon + return self._device.SENSOR_ICON @property def device_state_attributes(self): """Return the state attributes of the sensor.""" - from pydeconz.sensor import PRESENCE, VIBRATION attr = {} if self._device.battery: attr[ATTR_BATTERY_LEVEL] = self._device.battery + if self._device.on is not None: attr[ATTR_ON] = self._device.on - if self._device.type in PRESENCE and self._device.dark is not None: + + if self._device.secondary_temperature is not None: + attr[ATTR_TEMPERATURE] = self._device.secondary_temperature + + if self._device.type in Presence.ZHATYPE and \ + self._device.dark is not None: attr[ATTR_DARK] = self._device.dark - elif self._device.type in VIBRATION: + + elif self._device.type in Vibration.ZHATYPE: attr[ATTR_ORIENTATION] = self._device.orientation attr[ATTR_TILTANGLE] = self._device.tiltangle attr[ATTR_VIBRATIONSTRENGTH] = self._device.vibrationstrength + return attr diff --git a/homeassistant/components/deconz/climate.py b/homeassistant/components/deconz/climate.py index c4a021a80c2..cde123f7f08 100644 --- a/homeassistant/components/deconz/climate.py +++ b/homeassistant/components/deconz/climate.py @@ -1,4 +1,6 @@ """Support for deCONZ climate devices.""" +from pydeconz.sensor import Thermostat + from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( SUPPORT_ON_OFF, SUPPORT_TARGET_TEMPERATURE) @@ -12,6 +14,12 @@ from .deconz_device import DeconzDevice from .gateway import get_gateway_from_config_entry +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): + """Old way of setting up deCONZ platforms.""" + pass + + async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the deCONZ climate devices. @@ -22,12 +30,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities): @callback def async_add_climate(sensors): """Add climate devices from deCONZ.""" - from pydeconz.sensor import THERMOSTAT entities = [] for sensor in sensors: - if sensor.type in THERMOSTAT and \ + if sensor.type in Thermostat.ZHATYPE and \ not (not gateway.allow_clip_sensor and sensor.type.startswith('CLIP')): @@ -59,7 +66,7 @@ class DeconzThermostat(DeconzDevice, ClimateDevice): @property def is_on(self): """Return true if on.""" - return self._device.on + return self._device.state_on async def async_turn_on(self): """Turn on switch.""" diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index d9065ad2727..24eb3dd4d5d 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -4,6 +4,10 @@ import asyncio import async_timeout import voluptuous as vol +from pydeconz.errors import ResponseError, RequestError +from pydeconz.utils import ( + async_discovery, async_get_api_key, async_get_bridgeid) + from homeassistant import config_entries from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT from homeassistant.core import callback @@ -54,8 +58,6 @@ class DeconzFlowHandler(config_entries.ConfigFlow): If more than one bridge is found let user choose bridge to link. If no bridge is found allow user to manually input configuration. """ - from pydeconz.utils import async_discovery - if user_input is not None: for bridge in self.bridges: if bridge[CONF_HOST] == user_input[CONF_HOST]: @@ -101,8 +103,6 @@ class DeconzFlowHandler(config_entries.ConfigFlow): async def async_step_link(self, user_input=None): """Attempt to link with the deCONZ bridge.""" - from pydeconz.errors import ResponseError, RequestError - from pydeconz.utils import async_get_api_key errors = {} if user_input is not None: @@ -127,8 +127,6 @@ class DeconzFlowHandler(config_entries.ConfigFlow): async def _create_entry(self): """Create entry for gateway.""" - from pydeconz.utils import async_get_bridgeid - if CONF_BRIDGEID not in self.deconz_config: session = aiohttp_client.async_get_clientsession(self.hass) diff --git a/homeassistant/components/deconz/cover.py b/homeassistant/components/deconz/cover.py index aa29e8c6b58..a89e7fdd595 100644 --- a/homeassistant/components/deconz/cover.py +++ b/homeassistant/components/deconz/cover.py @@ -14,7 +14,7 @@ ZIGBEE_SPEC = ['lumi.curtain'] async def async_setup_platform( hass, config, async_add_entities, discovery_info=None): - """Unsupported way of setting up deCONZ covers.""" + """Old way of setting up deCONZ platforms.""" pass diff --git a/homeassistant/components/deconz/deconz_device.py b/homeassistant/components/deconz/deconz_device.py index 73ac2499cd3..90a5c8a3dde 100644 --- a/homeassistant/components/deconz/deconz_device.py +++ b/homeassistant/components/deconz/deconz_device.py @@ -31,7 +31,7 @@ class DeconzDevice(Entity): self.unsub_dispatcher() @callback - def async_update_callback(self, reason): + def async_update_callback(self, force_update=False): """Update the device's state.""" self.async_schedule_update_ha_state() diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py index 46078ea6648..f5d398fcd2f 100644 --- a/homeassistant/components/deconz/gateway.py +++ b/homeassistant/components/deconz/gateway.py @@ -2,6 +2,9 @@ import asyncio import async_timeout +from pydeconz import DeconzSession, errors +from pydeconz.sensor import Switch + from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.const import CONF_EVENT, CONF_HOST, CONF_ID from homeassistant.core import EventOrigin, callback @@ -126,8 +129,7 @@ class DeconzGateway: def async_connection_status_callback(self, available): """Handle signals of gateway connection status.""" self.available = available - async_dispatcher_send(self.hass, self.event_reachable, - {'state': True, 'attr': 'reachable'}) + async_dispatcher_send(self.hass, self.event_reachable, True) @callback def async_event_new_device(self, device_type): @@ -145,9 +147,8 @@ class DeconzGateway: @callback def async_add_remote(self, sensors): """Set up remote from deCONZ.""" - from pydeconz.sensor import SWITCH as DECONZ_REMOTE for sensor in sensors: - if sensor.type in DECONZ_REMOTE and \ + if sensor.type in Switch.ZHATYPE and \ not (not self.allow_clip_sensor and sensor.type.startswith('CLIP')): self.events.append(DeconzEvent(self.hass, sensor)) @@ -187,8 +188,6 @@ class DeconzGateway: async def get_gateway(hass, config, async_add_device_callback, async_connection_status_callback): """Create a gateway object and verify configuration.""" - from pydeconz import DeconzSession, errors - session = aiohttp_client.async_get_clientsession(hass) deconz = DeconzSession(hass.loop, session, **config, @@ -232,8 +231,8 @@ class DeconzEvent: self._device = None @callback - def async_update_callback(self, reason): + def async_update_callback(self, force_update=False): """Fire the event if reason is that state is updated.""" - if reason['state']: + if 'state' in self._device.changed_keys: data = {CONF_ID: self._id, CONF_EVENT: self._device.state} self._hass.bus.async_fire(self._event, data, EventOrigin.remote) diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py index c195703c36a..a3328ca8042 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -15,7 +15,7 @@ from .gateway import get_gateway_from_config_entry async def async_setup_platform( hass, config, async_add_entities, discovery_info=None): - """Old way of setting up deCONZ lights and group.""" + """Old way of setting up deCONZ platforms.""" pass diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json index 0692bd444b8..08a01cd1379 100644 --- a/homeassistant/components/deconz/manifest.json +++ b/homeassistant/components/deconz/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/components/deconz", "requirements": [ - "pydeconz==58" + "pydeconz==59" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/deconz/scene.py b/homeassistant/components/deconz/scene.py index d2e7f6719e9..c8cfa9674c5 100644 --- a/homeassistant/components/deconz/scene.py +++ b/homeassistant/components/deconz/scene.py @@ -9,7 +9,7 @@ from .gateway import get_gateway_from_config_entry async def async_setup_platform( hass, config, async_add_entities, discovery_info=None): - """Old way of setting up deCONZ scenes.""" + """Old way of setting up deCONZ platforms.""" pass diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index 9f1e87db4ba..efdb8ad8091 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -1,6 +1,8 @@ """Support for deCONZ sensors.""" +from pydeconz.sensor import LightLevel, Switch + from homeassistant.const import ( - ATTR_BATTERY_LEVEL, ATTR_VOLTAGE, DEVICE_CLASS_BATTERY) + ATTR_BATTERY_LEVEL, ATTR_TEMPERATURE, ATTR_VOLTAGE, DEVICE_CLASS_BATTERY) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.util import slugify @@ -16,7 +18,7 @@ ATTR_EVENT_ID = 'event_id' async def async_setup_platform( hass, config, async_add_entities, discovery_info=None): - """Old way of setting up deCONZ sensors.""" + """Old way of setting up deCONZ platforms.""" pass @@ -27,17 +29,15 @@ async def async_setup_entry(hass, config_entry, async_add_entities): @callback def async_add_sensor(sensors): """Add sensors from deCONZ.""" - from pydeconz.sensor import ( - DECONZ_SENSOR, SWITCH as DECONZ_REMOTE) entities = [] for sensor in sensors: - if sensor.type in DECONZ_SENSOR and \ + if not sensor.BINARY and \ not (not gateway.allow_clip_sensor and sensor.type.startswith('CLIP')): - if sensor.type in DECONZ_REMOTE: + if sensor.type in Switch.ZHATYPE: if sensor.battery: entities.append(DeconzBattery(sensor, gateway)) @@ -56,16 +56,11 @@ class DeconzSensor(DeconzDevice): """Representation of a deCONZ sensor.""" @callback - def async_update_callback(self, reason): - """Update the sensor's state. - - If reason is that state is updated, - or reachable has changed or battery has changed. - """ - if reason['state'] or \ - 'reachable' in reason['attr'] or \ - 'battery' in reason['attr'] or \ - 'on' in reason['attr']: + def async_update_callback(self, force_update=False): + """Update the sensor's state.""" + changed = set(self._device.changed_keys) + keys = {'battery', 'on', 'reachable', 'state'} + if force_update or any(key in changed for key in keys): self.async_schedule_update_ha_state() @property @@ -76,34 +71,42 @@ class DeconzSensor(DeconzDevice): @property def device_class(self): """Return the class of the sensor.""" - return self._device.sensor_class + return self._device.SENSOR_CLASS @property def icon(self): """Return the icon to use in the frontend.""" - return self._device.sensor_icon + return self._device.SENSOR_ICON @property def unit_of_measurement(self): """Return the unit of measurement of this sensor.""" - return self._device.sensor_unit + return self._device.SENSOR_UNIT @property def device_state_attributes(self): """Return the state attributes of the sensor.""" - from pydeconz.sensor import LIGHTLEVEL attr = {} if self._device.battery: attr[ATTR_BATTERY_LEVEL] = self._device.battery + if self._device.on is not None: attr[ATTR_ON] = self._device.on - if self._device.type in LIGHTLEVEL and self._device.dark is not None: + + if self._device.secondary_temperature is not None: + attr[ATTR_TEMPERATURE] = self._device.secondary_temperature + + if self._device.type in LightLevel.ZHATYPE and \ + self._device.dark is not None: attr[ATTR_DARK] = self._device.dark + if self.unit_of_measurement == 'Watts': attr[ATTR_CURRENT] = self._device.current attr[ATTR_VOLTAGE] = self._device.voltage - if self._device.sensor_class == 'daylight': + + if self._device.SENSOR_CLASS == 'daylight': attr[ATTR_DAYLIGHT] = self._device.daylight + return attr @@ -118,9 +121,11 @@ class DeconzBattery(DeconzDevice): self._unit_of_measurement = "%" @callback - def async_update_callback(self, reason): + def async_update_callback(self, force_update=False): """Update the battery's state, if needed.""" - if 'reachable' in reason['attr'] or 'battery' in reason['attr']: + changed = set(self._device.changed_keys) + keys = {'battery', 'reachable'} + if force_update or any(key in changed for key in keys): self.async_schedule_update_ha_state() @property diff --git a/homeassistant/components/deconz/switch.py b/homeassistant/components/deconz/switch.py index c399f5da128..dd06dba9583 100644 --- a/homeassistant/components/deconz/switch.py +++ b/homeassistant/components/deconz/switch.py @@ -10,7 +10,7 @@ from .gateway import get_gateway_from_config_entry async def async_setup_platform( hass, config, async_add_entities, discovery_info=None): - """Old way of setting up deCONZ switches.""" + """Old way of setting up deCONZ platforms.""" pass diff --git a/requirements_all.txt b/requirements_all.txt index b6824d3d894..d5cff9366b2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1048,7 +1048,7 @@ pydaikin==1.4.6 pydanfossair==0.1.0 # homeassistant.components.deconz -pydeconz==58 +pydeconz==59 # homeassistant.components.zwave pydispatcher==2.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 137a1662b00..f551e9ddc34 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -230,7 +230,7 @@ pyHS100==0.3.5 pyblackbird==0.5 # homeassistant.components.deconz -pydeconz==58 +pydeconz==59 # homeassistant.components.zwave pydispatcher==2.0.5 diff --git a/tests/components/deconz/test_binary_sensor.py b/tests/components/deconz/test_binary_sensor.py index 1aee53f43c2..89629a07cfa 100644 --- a/tests/components/deconz/test_binary_sensor.py +++ b/tests/components/deconz/test_binary_sensor.py @@ -1,6 +1,8 @@ """deCONZ binary sensor platform tests.""" from unittest.mock import Mock, patch +from tests.common import mock_coro + from homeassistant import config_entries from homeassistant.components import deconz from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -8,8 +10,6 @@ from homeassistant.setup import async_setup_component import homeassistant.components.binary_sensor as binary_sensor -from tests.common import mock_coro - SENSOR = { "1": { @@ -104,6 +104,7 @@ async def test_add_new_sensor(hass): sensor = Mock() sensor.name = 'name' sensor.type = 'ZHAPresence' + sensor.BINARY = True sensor.register_async_callback = Mock() async_dispatcher_send( hass, gateway.async_event_new_device('sensor'), [sensor]) diff --git a/tests/components/deconz/test_climate.py b/tests/components/deconz/test_climate.py index a5f4d2bb79b..407f5d92871 100644 --- a/tests/components/deconz/test_climate.py +++ b/tests/components/deconz/test_climate.py @@ -1,4 +1,5 @@ """deCONZ climate platform tests.""" +from copy import deepcopy from unittest.mock import Mock, patch import asynctest @@ -18,9 +19,9 @@ SENSOR = { "id": "Climate 1 id", "name": "Climate 1 name", "type": "ZHAThermostat", - "state": {"on": True, "temperature": 2260}, + "state": {"on": True, "temperature": 2260, "valve": 30}, "config": {"battery": 100, "heatsetpoint": 2200, "mode": "auto", - "offset": 10, "reachable": True, "valve": 30}, + "offset": 10, "reachable": True}, "uniqueid": "00:00:00:00:00:00:00:00-00" }, "2": { @@ -97,7 +98,7 @@ async def test_no_sensors(hass): async def test_climate_devices(hass): """Test successful creation of sensor entities.""" - gateway = await setup_gateway(hass, {"sensors": SENSOR}) + gateway = await setup_gateway(hass, {"sensors": deepcopy(SENSOR)}) assert "climate.climate_1_name" in gateway.deconz_ids assert "sensor.sensor_2_name" not in gateway.deconz_ids assert len(hass.states.async_all()) == 1 @@ -138,7 +139,7 @@ async def test_climate_devices(hass): async def test_verify_state_update(hass): """Test that state update properly.""" - gateway = await setup_gateway(hass, {"sensors": SENSOR}) + gateway = await setup_gateway(hass, {"sensors": deepcopy(SENSOR)}) assert "climate.climate_1_name" in gateway.deconz_ids thermostat = hass.states.get('climate.climate_1_name') @@ -149,7 +150,7 @@ async def test_verify_state_update(hass): "e": "changed", "r": "sensors", "id": "1", - "config": {"on": False} + "state": {"on": False} } gateway.api.async_event_handler(state_update) @@ -158,6 +159,8 @@ async def test_verify_state_update(hass): thermostat = hass.states.get('climate.climate_1_name') assert thermostat.state == 'off' + assert gateway.api.sensors['1'].changed_keys == \ + {'state', 'r', 't', 'on', 'e', 'id'} async def test_add_new_climate_device(hass): diff --git a/tests/components/deconz/test_config_flow.py b/tests/components/deconz/test_config_flow.py index ada506be428..46b0084b01b 100644 --- a/tests/components/deconz/test_config_flow.py +++ b/tests/components/deconz/test_config_flow.py @@ -43,7 +43,7 @@ async def test_flow_works(hass, aioclient_mock): async def test_user_step_bridge_discovery_fails(hass, aioclient_mock): """Test config flow works when discovery fails.""" - with patch('pydeconz.utils.async_discovery', + with patch('homeassistant.components.deconz.config_flow.async_discovery', side_effect=asyncio.TimeoutError): result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, @@ -158,8 +158,9 @@ async def test_link_no_api_key(hass): config_flow.CONF_PORT: 80 } - with patch('pydeconz.utils.async_get_api_key', - side_effect=pydeconz.errors.ResponseError): + with patch( + 'homeassistant.components.deconz.config_flow.async_get_api_key', + side_effect=pydeconz.errors.ResponseError): result = await flow.async_step_link(user_input={}) assert result['type'] == 'form' @@ -275,8 +276,9 @@ async def test_create_entry_timeout(hass, aioclient_mock): config_flow.CONF_API_KEY: '1234567890ABCDEF' } - with patch('pydeconz.utils.async_get_bridgeid', - side_effect=asyncio.TimeoutError): + with patch( + 'homeassistant.components.deconz.config_flow.async_get_bridgeid', + side_effect=asyncio.TimeoutError): result = await flow._create_entry() assert result['type'] == 'abort' diff --git a/tests/components/deconz/test_gateway.py b/tests/components/deconz/test_gateway.py index 6006ff66898..46107e1dd6c 100644 --- a/tests/components/deconz/test_gateway.py +++ b/tests/components/deconz/test_gateway.py @@ -223,7 +223,8 @@ async def test_update_event(): remote.name = 'Name' event = gateway.DeconzEvent(hass, remote) - event.async_update_callback({'state': True}) + remote.changed_keys = {'state': True} + event.async_update_callback() assert len(hass.bus.async_fire.mock_calls) == 1 diff --git a/tests/components/deconz/test_sensor.py b/tests/components/deconz/test_sensor.py index 41bb7b362f5..7ed8bef093e 100644 --- a/tests/components/deconz/test_sensor.py +++ b/tests/components/deconz/test_sensor.py @@ -1,6 +1,8 @@ """deCONZ sensor platform tests.""" from unittest.mock import Mock, patch +from tests.common import mock_coro + from homeassistant import config_entries from homeassistant.components import deconz from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -8,8 +10,6 @@ from homeassistant.setup import async_setup_component import homeassistant.components.sensor as sensor -from tests.common import mock_coro - SENSOR = { "1": { @@ -142,6 +142,7 @@ async def test_add_new_sensor(hass): sensor = Mock() sensor.name = 'name' sensor.type = 'ZHATemperature' + sensor.BINARY = False sensor.register_async_callback = Mock() async_dispatcher_send( hass, gateway.async_event_new_device('sensor'), [sensor]) From 9678752480be670f4ea3768b852422770b40aa25 Mon Sep 17 00:00:00 2001 From: Julien Brochet <556303+aerialls@users.noreply.github.com> Date: Mon, 27 May 2019 18:00:21 +0200 Subject: [PATCH 152/232] Retrieve wire and wireless devices with the SRM device tracker (#24117) --- homeassistant/components/synology_srm/device_tracker.py | 2 +- homeassistant/components/synology_srm/manifest.json | 4 ++-- requirements_all.txt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/synology_srm/device_tracker.py b/homeassistant/components/synology_srm/device_tracker.py index 57dbb7134e2..6330b14f7c4 100644 --- a/homeassistant/components/synology_srm/device_tracker.py +++ b/homeassistant/components/synology_srm/device_tracker.py @@ -80,7 +80,7 @@ class SynologySrmDeviceScanner(DeviceScanner): """Check the router for connected devices.""" _LOGGER.debug("Scanning for connected devices") - devices = self.client.mesh.network_wifidevice() + devices = self.client.core.network_nsm_device({'is_online': True}) last_results = [] for device in devices: diff --git a/homeassistant/components/synology_srm/manifest.json b/homeassistant/components/synology_srm/manifest.json index fa89577f26e..a790a6c453c 100644 --- a/homeassistant/components/synology_srm/manifest.json +++ b/homeassistant/components/synology_srm/manifest.json @@ -1,9 +1,9 @@ { "domain": "synology_srm", - "name": "Synology srm", + "name": "Synology SRM", "documentation": "https://www.home-assistant.io/components/synology_srm", "requirements": [ - "synology-srm==0.0.6" + "synology-srm==0.0.7" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index d5cff9366b2..c7a2906ae57 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1710,7 +1710,7 @@ sucks==0.9.4 swisshydrodata==0.0.3 # homeassistant.components.synology_srm -synology-srm==0.0.6 +synology-srm==0.0.7 # homeassistant.components.tahoma tahoma-api==0.0.14 From c840771c0af9257498bb1944ce89b846b62f5e52 Mon Sep 17 00:00:00 2001 From: maheus Date: Mon, 27 May 2019 18:07:59 +0200 Subject: [PATCH 153/232] Add station name for creating the unique_id in netatmo platform (#24141) --- homeassistant/components/netatmo/sensor.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index 22ca9e696f3..dabfb827aea 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -145,7 +145,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): # Only create sensors for monitored properties for condition in monitored_conditions: dev.append(NetatmoSensor( - data, module_name, condition.lower())) + data, module_name, condition.lower(), + config.get(CONF_STATION))) for module_name, _ in not_handled.items(): _LOGGER.error('Module name: "%s" not found', module_name) @@ -164,13 +165,14 @@ def all_product_classes(): class NetatmoSensor(Entity): """Implementation of a Netatmo sensor.""" - def __init__(self, netatmo_data, module_name, sensor_type): + def __init__(self, netatmo_data, module_name, sensor_type, station): """Initialize the sensor.""" self._name = 'Netatmo {} {}'.format(module_name, SENSOR_TYPES[sensor_type][0]) self.netatmo_data = netatmo_data self.module_name = module_name self.type = sensor_type + self.station_name = station self._state = None self._device_class = SENSOR_TYPES[self.type][3] self._icon = SENSOR_TYPES[self.type][2] @@ -178,7 +180,8 @@ class NetatmoSensor(Entity): self._module_type = self.netatmo_data. \ station_data.moduleByName(module=module_name)['type'] module_id = self.netatmo_data. \ - station_data.moduleByName(module=module_name)['_id'] + station_data.moduleByName(station=self.station_name, + module=module_name)['_id'] self._unique_id = '{}-{}'.format(module_id, self.type) @property From 5800b57791ac7767d19a50dcd07ea40353d925fc Mon Sep 17 00:00:00 2001 From: Jesse Rizzo <32472573+jesserizzo@users.noreply.github.com> Date: Mon, 27 May 2019 12:48:00 -0500 Subject: [PATCH 154/232] bump dependency envoy_reader to 0.4 (#24145) * bump envoy_reader version to 0.4 * bump dependency envoy_reader to 0.4 --- homeassistant/components/enphase_envoy/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/enphase_envoy/manifest.json b/homeassistant/components/enphase_envoy/manifest.json index 6fee88b39fc..1a816bc91d9 100644 --- a/homeassistant/components/enphase_envoy/manifest.json +++ b/homeassistant/components/enphase_envoy/manifest.json @@ -3,7 +3,7 @@ "name": "Enphase envoy", "documentation": "https://www.home-assistant.io/components/enphase_envoy", "requirements": [ - "envoy_reader==0.3" + "envoy_reader==0.4" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index c7a2906ae57..0e954db800e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -415,7 +415,7 @@ enturclient==0.2.0 # envirophat==0.0.6 # homeassistant.components.enphase_envoy -envoy_reader==0.3 +envoy_reader==0.4 # homeassistant.components.season ephem==3.7.6.0 From 9a5d783537688c6b11cda32bc0522fd1d63bde27 Mon Sep 17 00:00:00 2001 From: Sylvia van Os Date: Mon, 27 May 2019 22:36:15 +0200 Subject: [PATCH 155/232] Don't crash on first EAN without installations (#24137) * Don't crash on first EAN without installations * Remove duplicated values * Switch from Exception to persistent notification * Make pylint happy --- homeassistant/components/essent/manifest.json | 2 +- homeassistant/components/essent/sensor.py | 21 +++++++++++++------ requirements_all.txt | 2 +- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/essent/manifest.json b/homeassistant/components/essent/manifest.json index 49189f6bacb..41313cb44a9 100644 --- a/homeassistant/components/essent/manifest.json +++ b/homeassistant/components/essent/manifest.json @@ -2,7 +2,7 @@ "domain": "essent", "name": "Essent", "documentation": "https://www.home-assistant.io/components/essent", - "requirements": ["PyEssent==0.10"], + "requirements": ["PyEssent==0.12"], "dependencies": [], "codeowners": ["@TheLastProject"] } diff --git a/homeassistant/components/essent/sensor.py b/homeassistant/components/essent/sensor.py index 545ed3d5baf..e77b256abb7 100644 --- a/homeassistant/components/essent/sensor.py +++ b/homeassistant/components/essent/sensor.py @@ -36,6 +36,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None): tariff, data['values']['LVR'][tariff]['unit'])) + if not meters: + hass.components.persistent_notification.create( + 'Couldn\'t find any meter readings. ' + 'Please ensure Verbruiks Manager is enabled in Mijn Essent ' + 'and at least one reading has been logged to Meterstanden.', + title='Essent', notification_id='essent_notification') + return + add_devices(meters, True) @@ -46,14 +54,13 @@ class EssentBase(): """Initialize the Essent API.""" self._username = username self._password = password - self._meters = [] self._meter_data = {} self.update() def retrieve_meters(self): """Retrieve the list of meters.""" - return self._meters + return self._meter_data.keys() def retrieve_meter_data(self, meter): """Retrieve the data for this meter.""" @@ -63,10 +70,12 @@ class EssentBase(): def update(self): """Retrieve the latest meter data from Essent.""" essent = PyEssent(self._username, self._password) - self._meters = essent.get_EANs() - for meter in self._meters: - self._meter_data[meter] = essent.read_meter( - meter, only_last_meter_reading=True) + eans = essent.get_EANs() + for possible_meter in eans: + meter_data = essent.read_meter( + possible_meter, only_last_meter_reading=True) + if meter_data: + self._meter_data[possible_meter] = meter_data class EssentMeter(Entity): diff --git a/requirements_all.txt b/requirements_all.txt index 0e954db800e..a2cf5e37cd9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -43,7 +43,7 @@ Mastodon.py==1.4.2 OPi.GPIO==0.3.6 # homeassistant.components.essent -PyEssent==0.10 +PyEssent==0.12 # homeassistant.components.github PyGithub==1.43.5 From aa266cb63077826781765e8fffaad706f4898a86 Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Mon, 27 May 2019 22:09:05 -0400 Subject: [PATCH 156/232] pubnubsub-handler to 1.0.5 (#24154) --- homeassistant/components/wink/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/wink/manifest.json b/homeassistant/components/wink/manifest.json index c837a46e011..3536f69a7ca 100644 --- a/homeassistant/components/wink/manifest.json +++ b/homeassistant/components/wink/manifest.json @@ -3,7 +3,7 @@ "name": "Wink", "documentation": "https://www.home-assistant.io/components/wink", "requirements": [ - "pubnubsub-handler==1.0.4", + "pubnubsub-handler==1.0.5", "python-wink==1.10.5" ], "dependencies": ["configurator"], diff --git a/requirements_all.txt b/requirements_all.txt index a2cf5e37cd9..be565b3683e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -921,7 +921,7 @@ psutil==5.6.2 ptvsd==4.2.8 # homeassistant.components.wink -pubnubsub-handler==1.0.4 +pubnubsub-handler==1.0.5 # homeassistant.components.pushbullet pushbullet.py==0.11.0 From f2033c418f191f0d88c7640f34b2e29ab2504da1 Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Tue, 28 May 2019 12:09:30 -0400 Subject: [PATCH 157/232] Pubnub to 1.0.6 (#24159) --- homeassistant/components/wink/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/wink/manifest.json b/homeassistant/components/wink/manifest.json index 3536f69a7ca..118f7a19733 100644 --- a/homeassistant/components/wink/manifest.json +++ b/homeassistant/components/wink/manifest.json @@ -3,7 +3,7 @@ "name": "Wink", "documentation": "https://www.home-assistant.io/components/wink", "requirements": [ - "pubnubsub-handler==1.0.5", + "pubnubsub-handler==1.0.6", "python-wink==1.10.5" ], "dependencies": ["configurator"], diff --git a/requirements_all.txt b/requirements_all.txt index be565b3683e..233fc5f8459 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -921,7 +921,7 @@ psutil==5.6.2 ptvsd==4.2.8 # homeassistant.components.wink -pubnubsub-handler==1.0.5 +pubnubsub-handler==1.0.6 # homeassistant.components.pushbullet pushbullet.py==0.11.0 From 9fb814403189f5f24436efa7e27dfb836054d00f Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Tue, 28 May 2019 22:50:48 -0400 Subject: [PATCH 158/232] Debug log when polling ZHA light. (#24167) --- homeassistant/components/zha/light.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index 8395c2317e8..c3aa0e50f44 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -246,6 +246,7 @@ class Light(ZhaEntity, light.Light): async def async_get_state(self, from_cache=True): """Attempt to retrieve on off state from the light.""" + self.debug("polling current state") if self._on_off_channel: self._state = await self._on_off_channel.get_attribute_value( 'on_off', from_cache=from_cache) From 1b543cf5388f2956712199dd52529bdae0b8a083 Mon Sep 17 00:00:00 2001 From: chmielowiec Date: Wed, 29 May 2019 04:51:07 +0200 Subject: [PATCH 159/232] Upgrade huawei-lte-api to 1.2.0 (#24165) --- homeassistant/components/huawei_lte/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/huawei_lte/manifest.json b/homeassistant/components/huawei_lte/manifest.json index 2e096343b09..bfdc6f167aa 100644 --- a/homeassistant/components/huawei_lte/manifest.json +++ b/homeassistant/components/huawei_lte/manifest.json @@ -3,7 +3,7 @@ "name": "Huawei lte", "documentation": "https://www.home-assistant.io/components/huawei_lte", "requirements": [ - "huawei-lte-api==1.1.5" + "huawei-lte-api==1.2.0" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 233fc5f8459..f6367e30758 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -599,7 +599,7 @@ horimote==0.4.1 httplib2==0.10.3 # homeassistant.components.huawei_lte -huawei-lte-api==1.1.5 +huawei-lte-api==1.2.0 # homeassistant.components.hydrawise hydrawiser==0.1.1 From d9c78b77cb54bfd3c24d02370d41d9e7df6279d4 Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Tue, 28 May 2019 19:52:47 -0700 Subject: [PATCH 160/232] Use device name for device_tracker entry (#24155) --- homeassistant/components/mobile_app/webhook.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py index 1ef5f4ce531..4f867885d4f 100644 --- a/homeassistant/components/mobile_app/webhook.py +++ b/homeassistant/components/mobile_app/webhook.py @@ -24,6 +24,8 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.template import attach from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.util import slugify + from .const import (ATTR_ALTITUDE, ATTR_BATTERY, ATTR_COURSE, ATTR_DEVICE_ID, ATTR_DEVICE_NAME, ATTR_EVENT_DATA, ATTR_EVENT_TYPE, ATTR_GPS, ATTR_GPS_ACCURACY, ATTR_LOCATION_NAME, @@ -150,7 +152,7 @@ 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_DEV_ID: slugify(registration[ATTR_DEVICE_NAME]), ATTR_GPS: data[ATTR_GPS], ATTR_GPS_ACCURACY: data[ATTR_GPS_ACCURACY], } From 015c8811a50ba57cd5c95f48a69d4d90386d902c Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Wed, 29 May 2019 13:33:49 +0200 Subject: [PATCH 161/232] Use global imports for ESPHome (#24158) * Use global import for ESPHome * Add aioesphomeapi to test requirements aioesphomeapi is also shipped as a pure-python wheel, so this should not impact test install time --- homeassistant/components/esphome/__init__.py | 37 +++++++------------ .../components/esphome/binary_sensor.py | 15 +++----- homeassistant/components/esphome/camera.py | 16 +++----- homeassistant/components/esphome/climate.py | 22 ++++------- homeassistant/components/esphome/cover.py | 17 +++------ homeassistant/components/esphome/fan.py | 22 ++++------- homeassistant/components/esphome/light.py | 17 +++------ homeassistant/components/esphome/sensor.py | 20 ++++------ homeassistant/components/esphome/switch.py | 17 +++------ requirements_test_all.txt | 3 ++ script/gen_requirements_all.py | 1 + 11 files changed, 69 insertions(+), 118 deletions(-) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index e5feedd8421..d42bbb725dd 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -2,36 +2,35 @@ import asyncio import logging import math -from typing import Any, Dict, List, Optional, TYPE_CHECKING, Callable, Tuple +from typing import Any, Callable, Dict, List, Optional, Tuple +from aioesphomeapi import ( + COMPONENT_TYPE_TO_INFO, APIClient, APIConnectionError, DeviceInfo, + EntityInfo, EntityState, ServiceCall, UserService, UserServiceArgType) import attr import voluptuous as vol from homeassistant import const from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, \ - EVENT_HOMEASSISTANT_STOP -from homeassistant.core import callback, Event, State -import homeassistant.helpers.device_registry as dr +from homeassistant.const import ( + CONF_HOST, CONF_PASSWORD, CONF_PORT, EVENT_HOMEASSISTANT_STOP) +from homeassistant.core import Event, State, callback from homeassistant.exceptions import TemplateError -import homeassistant.helpers.config_validation as cv from homeassistant.helpers import template -from homeassistant.helpers.dispatcher import async_dispatcher_connect, \ - async_dispatcher_send +import homeassistant.helpers.config_validation as cv +import homeassistant.helpers.device_registry as dr +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, async_dispatcher_send) from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_state_change -from homeassistant.helpers.template import Template from homeassistant.helpers.json import JSONEncoder from homeassistant.helpers.storage import Store -from homeassistant.helpers.typing import HomeAssistantType, ConfigType +from homeassistant.helpers.template import Template +from homeassistant.helpers.typing import ConfigType, HomeAssistantType # Import config flow so that it's added to the registry from .config_flow import EsphomeFlowHandler # noqa -if TYPE_CHECKING: - from aioesphomeapi import APIClient, EntityInfo, EntityState, DeviceInfo, \ - ServiceCall, UserService - DOMAIN = 'esphome' _LOGGER = logging.getLogger(__name__) @@ -110,10 +109,6 @@ class RuntimeEntryData: async def async_load_from_store(self) -> Tuple[List['EntityInfo'], List['UserService']]: """Load the retained data from store and return de-serialized data.""" - # pylint: disable= redefined-outer-name - from aioesphomeapi import COMPONENT_TYPE_TO_INFO, DeviceInfo, \ - UserService - restored = await self.store.async_load() if restored is None: return [], [] @@ -164,9 +159,6 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: """Set up the esphome component.""" - # pylint: disable=redefined-outer-name - from aioesphomeapi import APIClient, APIConnectionError - hass.data.setdefault(DOMAIN, {}) host = entry.data[CONF_HOST] @@ -306,8 +298,6 @@ async def _setup_auto_reconnect_logic(hass: HomeAssistantType, cli: 'APIClient', entry: ConfigEntry, host: str, on_login): """Set up the re-connect logic for the API client.""" - from aioesphomeapi import APIConnectionError - async def try_connect(tries: int = 0, is_disconnect: bool = True) -> None: """Try connecting to the API client. Will retry if not successful.""" if entry.entry_id not in hass.data[DOMAIN]: @@ -382,7 +372,6 @@ async def _async_setup_device_registry(hass: HomeAssistantType, async def _register_service(hass: HomeAssistantType, entry_data: RuntimeEntryData, service: 'UserService'): - from aioesphomeapi import UserServiceArgType service_name = '{}_{}'.format(entry_data.device_info.name, service.name) schema = {} for arg in service.args: diff --git a/homeassistant/components/esphome/binary_sensor.py b/homeassistant/components/esphome/binary_sensor.py index 6a6f9bfac1c..75a7235c58f 100644 --- a/homeassistant/components/esphome/binary_sensor.py +++ b/homeassistant/components/esphome/binary_sensor.py @@ -1,23 +1,18 @@ """Support for ESPHome binary sensors.""" import logging -from typing import TYPE_CHECKING, Optional +from typing import Optional + +from aioesphomeapi import BinarySensorInfo, BinarySensorState from homeassistant.components.binary_sensor import BinarySensorDevice from . import EsphomeEntity, platform_async_setup_entry -if TYPE_CHECKING: - # pylint: disable=unused-import - from aioesphomeapi import BinarySensorInfo, BinarySensorState # noqa - _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass, entry, async_add_entities): """Set up ESPHome binary sensors based on a config entry.""" - # pylint: disable=redefined-outer-name - from aioesphomeapi import BinarySensorInfo, BinarySensorState # noqa - await platform_async_setup_entry( hass, entry, async_add_entities, component_key='binary_sensor', @@ -30,11 +25,11 @@ class EsphomeBinarySensor(EsphomeEntity, BinarySensorDevice): """A binary sensor implementation for ESPHome.""" @property - def _static_info(self) -> 'BinarySensorInfo': + def _static_info(self) -> BinarySensorInfo: return super()._static_info @property - def _state(self) -> Optional['BinarySensorState']: + def _state(self) -> Optional[BinarySensorState]: return super()._state @property diff --git a/homeassistant/components/esphome/camera.py b/homeassistant/components/esphome/camera.py index 64e73dc8784..54f774bc426 100644 --- a/homeassistant/components/esphome/camera.py +++ b/homeassistant/components/esphome/camera.py @@ -1,17 +1,16 @@ """Support for ESPHome cameras.""" import asyncio import logging -from typing import Optional, TYPE_CHECKING +from typing import Optional + +from aioesphomeapi import CameraInfo, CameraState from homeassistant.components import camera from homeassistant.components.camera import Camera from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.typing import HomeAssistantType -from . import EsphomeEntity, platform_async_setup_entry -if TYPE_CHECKING: - # pylint: disable=unused-import - from aioesphomeapi import CameraInfo, CameraState # noqa +from . import EsphomeEntity, platform_async_setup_entry _LOGGER = logging.getLogger(__name__) @@ -19,9 +18,6 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry, async_add_entities) -> None: """Set up esphome cameras based on a config entry.""" - # pylint: disable=redefined-outer-name - from aioesphomeapi import CameraInfo, CameraState # noqa - await platform_async_setup_entry( hass, entry, async_add_entities, component_key='camera', @@ -40,11 +36,11 @@ class EsphomeCamera(Camera, EsphomeEntity): self._image_cond = asyncio.Condition() @property - def _static_info(self) -> 'CameraInfo': + def _static_info(self) -> CameraInfo: return super()._static_info @property - def _state(self) -> Optional['CameraState']: + def _state(self) -> Optional[CameraState]: return super()._state async def _on_update(self) -> None: diff --git a/homeassistant/components/esphome/climate.py b/homeassistant/components/esphome/climate.py index 184eb4b6270..33ea5524787 100644 --- a/homeassistant/components/esphome/climate.py +++ b/homeassistant/components/esphome/climate.py @@ -1,6 +1,8 @@ """Support for ESPHome climate devices.""" import logging -from typing import TYPE_CHECKING, List, Optional +from typing import List, Optional + +from aioesphomeapi import ClimateInfo, ClimateMode, ClimateState from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( @@ -12,21 +14,15 @@ from homeassistant.const import ( ATTR_TEMPERATURE, PRECISION_HALVES, PRECISION_TENTHS, PRECISION_WHOLE, STATE_OFF, TEMP_CELSIUS) -from . import EsphomeEntity, platform_async_setup_entry, \ - esphome_state_property, esphome_map_enum - -if TYPE_CHECKING: - # pylint: disable=unused-import - from aioesphomeapi import ClimateInfo, ClimateState, ClimateMode # noqa +from . import ( + EsphomeEntity, esphome_map_enum, esphome_state_property, + platform_async_setup_entry) _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass, entry, async_add_entities): """Set up ESPHome climate devices based on a config entry.""" - # pylint: disable=redefined-outer-name - from aioesphomeapi import ClimateInfo, ClimateState # noqa - await platform_async_setup_entry( hass, entry, async_add_entities, component_key='climate', @@ -37,8 +33,6 @@ async def async_setup_entry(hass, entry, async_add_entities): @esphome_map_enum def _climate_modes(): - # pylint: disable=redefined-outer-name - from aioesphomeapi import ClimateMode # noqa return { ClimateMode.OFF: STATE_OFF, ClimateMode.AUTO: STATE_AUTO, @@ -51,11 +45,11 @@ class EsphomeClimateDevice(EsphomeEntity, ClimateDevice): """A climate implementation for ESPHome.""" @property - def _static_info(self) -> 'ClimateInfo': + def _static_info(self) -> ClimateInfo: return super()._static_info @property - def _state(self) -> Optional['ClimateState']: + def _state(self) -> Optional[ClimateState]: return super()._state @property diff --git a/homeassistant/components/esphome/cover.py b/homeassistant/components/esphome/cover.py index a3ef15fa4c7..b69b62075db 100644 --- a/homeassistant/components/esphome/cover.py +++ b/homeassistant/components/esphome/cover.py @@ -1,6 +1,8 @@ """Support for ESPHome covers.""" import logging -from typing import TYPE_CHECKING, Optional +from typing import Optional + +from aioesphomeapi import CoverInfo, CoverState from homeassistant.components.cover import ( ATTR_POSITION, ATTR_TILT_POSITION, SUPPORT_CLOSE, SUPPORT_CLOSE_TILT, @@ -9,11 +11,7 @@ from homeassistant.components.cover import ( from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.typing import HomeAssistantType -from . import EsphomeEntity, platform_async_setup_entry, esphome_state_property - -if TYPE_CHECKING: - # pylint: disable=unused-import - from aioesphomeapi import CoverInfo, CoverState # noqa +from . import EsphomeEntity, esphome_state_property, platform_async_setup_entry _LOGGER = logging.getLogger(__name__) @@ -21,9 +19,6 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry, async_add_entities) -> None: """Set up ESPHome covers based on a config entry.""" - # pylint: disable=redefined-outer-name - from aioesphomeapi import CoverInfo, CoverState # noqa - await platform_async_setup_entry( hass, entry, async_add_entities, component_key='cover', @@ -36,7 +31,7 @@ class EsphomeCover(EsphomeEntity, CoverDevice): """A cover implementation for ESPHome.""" @property - def _static_info(self) -> 'CoverInfo': + def _static_info(self) -> CoverInfo: return super()._static_info @property @@ -61,7 +56,7 @@ class EsphomeCover(EsphomeEntity, CoverDevice): return self._static_info.assumed_state @property - def _state(self) -> Optional['CoverState']: + def _state(self) -> Optional[CoverState]: return super()._state @esphome_state_property diff --git a/homeassistant/components/esphome/fan.py b/homeassistant/components/esphome/fan.py index 50cf04203f3..255bdaa8cb1 100644 --- a/homeassistant/components/esphome/fan.py +++ b/homeassistant/components/esphome/fan.py @@ -1,6 +1,8 @@ """Support for ESPHome fans.""" import logging -from typing import TYPE_CHECKING, List, Optional +from typing import List, Optional + +from aioesphomeapi import FanInfo, FanSpeed, FanState from homeassistant.components.fan import ( SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM, SPEED_OFF, SUPPORT_OSCILLATE, @@ -8,12 +10,9 @@ from homeassistant.components.fan import ( from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.typing import HomeAssistantType -from . import EsphomeEntity, platform_async_setup_entry, \ - esphome_state_property, esphome_map_enum - -if TYPE_CHECKING: - # pylint: disable=unused-import - from aioesphomeapi import FanInfo, FanState, FanSpeed # noqa +from . import ( + EsphomeEntity, esphome_map_enum, esphome_state_property, + platform_async_setup_entry) _LOGGER = logging.getLogger(__name__) @@ -21,9 +20,6 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry, async_add_entities) -> None: """Set up ESPHome fans based on a config entry.""" - # pylint: disable=redefined-outer-name - from aioesphomeapi import FanInfo, FanState # noqa - await platform_async_setup_entry( hass, entry, async_add_entities, component_key='fan', @@ -34,8 +30,6 @@ async def async_setup_entry(hass: HomeAssistantType, @esphome_map_enum def _fan_speeds(): - # pylint: disable=redefined-outer-name - from aioesphomeapi import FanSpeed # noqa return { FanSpeed.LOW: SPEED_LOW, FanSpeed.MEDIUM: SPEED_MEDIUM, @@ -47,11 +41,11 @@ class EsphomeFan(EsphomeEntity, FanEntity): """A fan implementation for ESPHome.""" @property - def _static_info(self) -> 'FanInfo': + def _static_info(self) -> FanInfo: return super()._static_info @property - def _state(self) -> Optional['FanState']: + def _state(self) -> Optional[FanState]: return super()._state async def async_set_speed(self, speed: str) -> None: diff --git a/homeassistant/components/esphome/light.py b/homeassistant/components/esphome/light.py index 6b4abafe62b..f94229d61cc 100644 --- a/homeassistant/components/esphome/light.py +++ b/homeassistant/components/esphome/light.py @@ -1,6 +1,8 @@ """Support for ESPHome lights.""" import logging -from typing import TYPE_CHECKING, List, Optional, Tuple +from typing import List, Optional, Tuple + +from aioesphomeapi import LightInfo, LightState from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, ATTR_HS_COLOR, @@ -11,11 +13,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.typing import HomeAssistantType import homeassistant.util.color as color_util -from . import EsphomeEntity, platform_async_setup_entry, esphome_state_property - -if TYPE_CHECKING: - # pylint: disable=unused-import - from aioesphomeapi import LightInfo, LightState # noqa +from . import EsphomeEntity, esphome_state_property, platform_async_setup_entry _LOGGER = logging.getLogger(__name__) @@ -29,9 +27,6 @@ FLASH_LENGTHS = { async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry, async_add_entities) -> None: """Set up ESPHome lights based on a config entry.""" - # pylint: disable=redefined-outer-name - from aioesphomeapi import LightInfo, LightState # noqa - await platform_async_setup_entry( hass, entry, async_add_entities, component_key='light', @@ -44,11 +39,11 @@ class EsphomeLight(EsphomeEntity, Light): """A switch implementation for ESPHome.""" @property - def _static_info(self) -> 'LightInfo': + def _static_info(self) -> LightInfo: return super()._static_info @property - def _state(self) -> Optional['LightState']: + def _state(self) -> Optional[LightState]: return super()._state @esphome_state_property diff --git a/homeassistant/components/esphome/sensor.py b/homeassistant/components/esphome/sensor.py index 8d8fb938c68..a5a530b49f1 100644 --- a/homeassistant/components/esphome/sensor.py +++ b/homeassistant/components/esphome/sensor.py @@ -1,17 +1,15 @@ """Support for esphome sensors.""" import logging import math -from typing import TYPE_CHECKING, Optional +from typing import Optional + +from aioesphomeapi import ( + SensorInfo, SensorState, TextSensorInfo, TextSensorState) from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.typing import HomeAssistantType -from . import EsphomeEntity, platform_async_setup_entry, esphome_state_property - -if TYPE_CHECKING: - # pylint: disable=unused-import - from aioesphomeapi import ( # noqa - SensorInfo, SensorState, TextSensorInfo, TextSensorState) +from . import EsphomeEntity, esphome_state_property, platform_async_setup_entry _LOGGER = logging.getLogger(__name__) @@ -19,10 +17,6 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry, async_add_entities) -> None: """Set up esphome sensors based on a config entry.""" - # pylint: disable=redefined-outer-name - from aioesphomeapi import ( # noqa - SensorInfo, SensorState, TextSensorInfo, TextSensorState) - await platform_async_setup_entry( hass, entry, async_add_entities, component_key='sensor', @@ -41,11 +35,11 @@ class EsphomeSensor(EsphomeEntity): """A sensor implementation for esphome.""" @property - def _static_info(self) -> 'SensorInfo': + def _static_info(self) -> SensorInfo: return super()._static_info @property - def _state(self) -> Optional['SensorState']: + def _state(self) -> Optional[SensorState]: return super()._state @property diff --git a/homeassistant/components/esphome/switch.py b/homeassistant/components/esphome/switch.py index 77994d0be58..d209df8cd83 100644 --- a/homeassistant/components/esphome/switch.py +++ b/homeassistant/components/esphome/switch.py @@ -1,16 +1,14 @@ """Support for ESPHome switches.""" import logging -from typing import TYPE_CHECKING, Optional +from typing import Optional + +from aioesphomeapi import SwitchInfo, SwitchState from homeassistant.components.switch import SwitchDevice from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.typing import HomeAssistantType -from . import EsphomeEntity, platform_async_setup_entry, esphome_state_property - -if TYPE_CHECKING: - # pylint: disable=unused-import - from aioesphomeapi import SwitchInfo, SwitchState # noqa +from . import EsphomeEntity, esphome_state_property, platform_async_setup_entry _LOGGER = logging.getLogger(__name__) @@ -18,9 +16,6 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry, async_add_entities) -> None: """Set up ESPHome switches based on a config entry.""" - # pylint: disable=redefined-outer-name - from aioesphomeapi import SwitchInfo, SwitchState # noqa - await platform_async_setup_entry( hass, entry, async_add_entities, component_key='switch', @@ -33,11 +28,11 @@ class EsphomeSwitch(EsphomeEntity, SwitchDevice): """A switch implementation for ESPHome.""" @property - def _static_info(self) -> 'SwitchInfo': + def _static_info(self) -> SwitchInfo: return super()._static_info @property - def _state(self) -> Optional['SwitchState']: + def _state(self) -> Optional[SwitchState]: return super()._state @property diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f551e9ddc34..39f0512cdeb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -44,6 +44,9 @@ aioautomatic==0.6.5 # homeassistant.components.aws aiobotocore==0.10.2 +# homeassistant.components.esphome +aioesphomeapi==2.0.1 + # homeassistant.components.emulated_hue # homeassistant.components.http aiohttp_cors==0.7.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index a5f24be51de..dad20279d5b 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -46,6 +46,7 @@ TEST_REQUIREMENTS = ( 'aioambient', 'aioautomatic', 'aiobotocore', + 'aioesphomeapi', 'aiohttp_cors', 'aiohue', 'aiounifi', From 85dfea16424d53d957cc26a4cdbe82a1b71b9d52 Mon Sep 17 00:00:00 2001 From: Morten Trab Date: Wed, 29 May 2019 14:31:04 +0200 Subject: [PATCH 162/232] Add Repetier-Server Component (#21658) * Bumped to version 2.0 * Updated requirements * Updated requirements and coveragerc * Removed long lines, changes to coveragerc and requirements * Fixed under-indented lines * Fixed invalid syntax * Updated .coveragerc to include repetier/__init__.py and sensor.py * Module update * Rebased to latest dev * Blank lines fix * Add missing manifest.json * Update requirements * Bumped component to new API module style * Removed whitespaces and line feeds * Added missing newline * Added missing heated chamber sensor * Fixed wrong indentation * Various fixes * Various build fixes * Clean up * Load platform only once * Sort imports * Add printer api * Clean up * Build out sensor classes * Clarify temperature sensor variable names * Move constants * Clean up name * Run script/gen_requirements_all.py * Working code, missing auto add of new sensors * Updated code to return proper device_class and timestamp * Removed unnessecary code * Renamed elapsed and remaining sensors * Dynamically adding sensors as they become available * Rebased .coveragerc due to conflicts * Code changes and cleanup * Removed whitespace and code simplification --- .coveragerc | 2 + CODEOWNERS | 1 + homeassistant/components/repetier/__init__.py | 248 ++++++++++++++++++ .../components/repetier/manifest.json | 10 + homeassistant/components/repetier/sensor.py | 215 +++++++++++++++ requirements_all.txt | 3 + 6 files changed, 479 insertions(+) create mode 100755 homeassistant/components/repetier/__init__.py create mode 100755 homeassistant/components/repetier/manifest.json create mode 100755 homeassistant/components/repetier/sensor.py diff --git a/.coveragerc b/.coveragerc index 9f6bb0d1b95..030c48cd10c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -488,6 +488,8 @@ omit = homeassistant/components/reddit/* homeassistant/components/rejseplanen/sensor.py homeassistant/components/remember_the_milk/__init__.py + homeassistant/components/repetier/__init__.py + homeassistant/components/repetier/sensor.py homeassistant/components/remote_rpi_gpio/* homeassistant/components/rest/binary_sensor.py homeassistant/components/rest/notify.py diff --git a/CODEOWNERS b/CODEOWNERS index 1e7c3c87a07..59bd8c31af1 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -193,6 +193,7 @@ homeassistant/components/qwikswitch/* @kellerza homeassistant/components/raincloud/* @vanstinator homeassistant/components/rainmachine/* @bachya homeassistant/components/random/* @fabaff +homeassistant/components/repetier/* @MTrab homeassistant/components/rfxtrx/* @danielhiversen homeassistant/components/rmvtransport/* @cgtobi homeassistant/components/roomba/* @pschmitt diff --git a/homeassistant/components/repetier/__init__.py b/homeassistant/components/repetier/__init__.py new file mode 100755 index 00000000000..24382b2f12d --- /dev/null +++ b/homeassistant/components/repetier/__init__.py @@ -0,0 +1,248 @@ +"""Support for Repetier-Server sensors.""" +import logging +from datetime import timedelta + +import voluptuous as vol + +from homeassistant.const import ( + CONF_API_KEY, CONF_HOST, CONF_MONITORED_CONDITIONS, CONF_NAME, CONF_PORT, + CONF_SENSORS, TEMP_CELSIUS) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.discovery import load_platform +from homeassistant.helpers.dispatcher import dispatcher_send +from homeassistant.helpers.event import track_time_interval +from homeassistant.util import slugify as util_slugify + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = 'RepetierServer' +DOMAIN = 'repetier' +REPETIER_API = 'repetier_api' +SCAN_INTERVAL = timedelta(seconds=10) +UPDATE_SIGNAL = 'repetier_update_signal' + +TEMP_DATA = { + 'tempset': 'temp_set', + 'tempread': 'state', + 'output': 'output', +} + + +API_PRINTER_METHODS = { + 'bed_temperature': { + 'offline': {'heatedbeds': None, 'state': 'off'}, + 'state': {'heatedbeds': 'temp_data'}, + 'temp_data': TEMP_DATA, + 'attribute': 'heatedbeds', + }, + 'extruder_temperature': { + 'offline': {'extruder': None, 'state': 'off'}, + 'state': {'extruder': 'temp_data'}, + 'temp_data': TEMP_DATA, + 'attribute': 'extruder', + }, + 'chamber_temperature': { + 'offline': {'heatedchambers': None, 'state': 'off'}, + 'state': {'heatedchambers': 'temp_data'}, + 'temp_data': TEMP_DATA, + 'attribute': 'heatedchambers', + }, + 'current_state': { + 'offline': {'state': None}, + 'state': { + 'state': 'state', + 'activeextruder': 'active_extruder', + 'hasxhome': 'x_homed', + 'hasyhome': 'y_homed', + 'haszhome': 'z_homed', + 'firmware': 'firmware', + 'firmwareurl': 'firmware_url', + }, + }, + 'current_job': { + 'offline': {'job': None, 'state': 'off'}, + 'state': { + 'done': 'state', + 'job': 'job_name', + 'jobid': 'job_id', + 'totallines': 'total_lines', + 'linessent': 'lines_sent', + 'oflayer': 'total_layers', + 'layer': 'current_layer', + 'speedmultiply': 'feed_rate', + 'flowmultiply': 'flow', + 'x': 'x', + 'y': 'y', + 'z': 'z', + }, + }, + 'job_end': { + 'offline': { + 'job': None, 'state': 'off', 'start': None, 'printtime': None}, + 'state': { + 'job': 'job_name', + 'start': 'start', + 'printtime': 'print_time', + 'printedtimecomp': 'from_start', + }, + }, + 'job_start': { + 'offline': { + 'job': None, + 'state': 'off', + 'start': None, + 'printedtimecomp': None + }, + 'state': { + 'job': 'job_name', + 'start': 'start', + 'printedtimecomp': 'from_start', + }, + }, +} + + +def has_all_unique_names(value): + """Validate that printers have an unique name.""" + names = [util_slugify(printer[CONF_NAME]) for printer in value] + vol.Schema(vol.Unique())(names) + return value + + +SENSOR_TYPES = { + # Type, Unit, Icon + 'bed_temperature': ['temperature', TEMP_CELSIUS, 'mdi:thermometer', + '_bed_'], + 'extruder_temperature': ['temperature', TEMP_CELSIUS, 'mdi:thermometer', + '_extruder_'], + 'chamber_temperature': ['temperature', TEMP_CELSIUS, 'mdi:thermometer', + '_chamber_'], + 'current_state': ['state', None, 'mdi:printer-3d', ''], + 'current_job': ['progress', '%', 'mdi:file-percent', '_current_job'], + 'job_end': ['progress', None, 'mdi:clock-end', '_job_end'], + 'job_start': ['progress', None, 'mdi:clock-start', '_job_start'], +} + +SENSOR_SCHEMA = vol.Schema({ + vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)): + vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, +}) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.All(cv.ensure_list, [vol.Schema({ + vol.Required(CONF_API_KEY): cv.string, + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_PORT, default=3344): cv.port, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_SENSORS, default={}): SENSOR_SCHEMA, + })], has_all_unique_names), +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up the Repetier Server component.""" + import pyrepetier + + hass.data[REPETIER_API] = {} + + for repetier in config[DOMAIN]: + _LOGGER.debug("Repetier server config %s", repetier[CONF_HOST]) + + url = "http://{}".format(repetier[CONF_HOST]) + port = repetier[CONF_PORT] + api_key = repetier[CONF_API_KEY] + + client = pyrepetier.Repetier( + url=url, + port=port, + apikey=api_key) + + printers = client.getprinters() + + if not printers: + return False + + sensors = repetier[CONF_SENSORS][CONF_MONITORED_CONDITIONS] + api = PrinterAPI(hass, client, printers, sensors, + repetier[CONF_NAME], config) + api.update() + track_time_interval(hass, api.update, SCAN_INTERVAL) + + hass.data[REPETIER_API][repetier[CONF_NAME]] = api + + return True + + +class PrinterAPI: + """Handle the printer API.""" + + def __init__(self, hass, client, printers, sensors, conf_name, config): + """Set up instance.""" + self._hass = hass + self._client = client + self.printers = printers + self.sensors = sensors + self.conf_name = conf_name + self.config = config + self._known_entities = set() + + def get_data(self, printer_id, sensor_type, temp_id): + """Get data from the state cache.""" + printer = self.printers[printer_id] + methods = API_PRINTER_METHODS[sensor_type] + for prop, offline in methods['offline'].items(): + state = getattr(printer, prop) + if state == offline: + # if state matches offline, sensor is offline + return None + + data = {} + for prop, attr in methods['state'].items(): + prop_data = getattr(printer, prop) + if attr == 'temp_data': + temp_methods = methods['temp_data'] + for temp_prop, temp_attr in temp_methods.items(): + data[temp_attr] = getattr(prop_data[temp_id], temp_prop) + else: + data[attr] = prop_data + return data + + def update(self, now=None): + """Update the state cache from the printer API.""" + for printer in self.printers: + printer.get_data() + self._load_entities() + dispatcher_send(self._hass, UPDATE_SIGNAL) + + def _load_entities(self): + sensor_info = [] + for pidx, printer in enumerate(self.printers): + for sensor_type in self.sensors: + info = {} + info['sensor_type'] = sensor_type + info['printer_id'] = pidx + info['name'] = printer.slug + info['printer_name'] = self.conf_name + + known = '{}-{}'.format(printer.slug, sensor_type) + if known in self._known_entities: + continue + + methods = API_PRINTER_METHODS[sensor_type] + if 'temp_data' in methods['state'].values(): + prop_data = getattr(printer, methods['attribute']) + if prop_data is None: + continue + for idx, _ in enumerate(prop_data): + info['temp_id'] = idx + sensor_info.append(info) + else: + info['temp_id'] = None + sensor_info.append(info) + + self._known_entities.add(known) + + if not sensor_info: + return + load_platform(self._hass, 'sensor', DOMAIN, sensor_info, self.config) diff --git a/homeassistant/components/repetier/manifest.json b/homeassistant/components/repetier/manifest.json new file mode 100755 index 00000000000..14af98cfb64 --- /dev/null +++ b/homeassistant/components/repetier/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "repetier", + "name": "Repetier Server", + "documentation": "https://www.home-assistant.io/components/repetier", + "requirements": [ + "pyrepetier==3.0.5" + ], + "dependencies": [], + "codeowners": ["@MTrab"] +} diff --git a/homeassistant/components/repetier/sensor.py b/homeassistant/components/repetier/sensor.py new file mode 100755 index 00000000000..17f999a95cf --- /dev/null +++ b/homeassistant/components/repetier/sensor.py @@ -0,0 +1,215 @@ +"""Support for monitoring Repetier Server Sensors.""" +from datetime import datetime +import logging +import time + +from homeassistant.const import DEVICE_CLASS_TIMESTAMP +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import Entity + +from . import REPETIER_API, SENSOR_TYPES, UPDATE_SIGNAL + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the available Repetier Server sensors.""" + if discovery_info is None: + return + + sensor_map = { + 'bed_temperature': RepetierTempSensor, + 'extruder_temperature': RepetierTempSensor, + 'chamber_temperature': RepetierTempSensor, + 'current_state': RepetierSensor, + 'current_job': RepetierJobSensor, + 'job_end': RepetierJobEndSensor, + 'job_start': RepetierJobStartSensor, + } + + entities = [] + for info in discovery_info: + printer_name = info['printer_name'] + api = hass.data[REPETIER_API][printer_name] + printer_id = info['printer_id'] + sensor_type = info['sensor_type'] + temp_id = info['temp_id'] + name = info['name'] + if temp_id is not None: + name = '{}{}{}'.format( + name, SENSOR_TYPES[sensor_type][3], temp_id) + else: + name = '{}{}'.format(name, SENSOR_TYPES[sensor_type][3]) + sensor_class = sensor_map[sensor_type] + entity = sensor_class(api, temp_id, name, printer_id, sensor_type) + entities.append(entity) + + add_entities(entities, True) + + +class RepetierSensor(Entity): + """Class to create and populate a Repetier Sensor.""" + + def __init__(self, api, temp_id, name, printer_id, sensor_type): + """Init new sensor.""" + self._api = api + self._attributes = {} + self._available = False + self._temp_id = temp_id + self._name = name + self._printer_id = printer_id + self._sensor_type = sensor_type + self._state = None + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self._available + + @property + def device_state_attributes(self): + """Return sensor attributes.""" + return self._attributes + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity, if any.""" + return SENSOR_TYPES[self._sensor_type][1] + + @property + def icon(self): + """Icon to use in the frontend.""" + return SENSOR_TYPES[self._sensor_type][2] + + @property + def should_poll(self): + """Return False as entity is updated from the component.""" + return False + + @property + def state(self): + """Return sensor state.""" + return self._state + + @callback + def update_callback(self): + """Get new data and update state.""" + self.async_schedule_update_ha_state(True) + + async def async_added_to_hass(self): + """Connect update callbacks.""" + async_dispatcher_connect( + self.hass, UPDATE_SIGNAL, self.update_callback) + + def _get_data(self): + """Return new data from the api cache.""" + data = self._api.get_data( + self._printer_id, self._sensor_type, self._temp_id) + if data is None: + _LOGGER.debug( + "Data not found for %s and %s", + self._sensor_type, self._temp_id) + self._available = False + return None + self._available = True + return data + + def update(self): + """Update the sensor.""" + data = self._get_data() + if data is None: + return + state = data.pop('state') + _LOGGER.debug("Printer %s State %s", self._name, state) + self._attributes.update(data) + self._state = state + + +class RepetierTempSensor(RepetierSensor): + """Represent a Repetier temp sensor.""" + + @property + def state(self): + """Return sensor state.""" + if self._state is None: + return None + return round(self._state, 2) + + def update(self): + """Update the sensor.""" + data = self._get_data() + if data is None: + return + state = data.pop('state') + temp_set = data['temp_set'] + _LOGGER.debug( + "Printer %s Setpoint: %s, Temp: %s", + self._name, temp_set, state) + self._attributes.update(data) + self._state = state + + +class RepetierJobSensor(RepetierSensor): + """Represent a Repetier job sensor.""" + + @property + def state(self): + """Return sensor state.""" + if self._state is None: + return None + return round(self._state, 2) + + +class RepetierJobEndSensor(RepetierSensor): + """Class to create and populate a Repetier Job End timestamp Sensor.""" + + @property + def device_class(self): + """Return the device class.""" + return DEVICE_CLASS_TIMESTAMP + + def update(self): + """Update the sensor.""" + data = self._get_data() + if data is None: + return + job_name = data['job_name'] + start = data['start'] + print_time = data['print_time'] + from_start = data['from_start'] + time_end = start + round(print_time, 0) + self._state = datetime.utcfromtimestamp(time_end).isoformat() + remaining = print_time - from_start + remaining_secs = int(round(remaining, 0)) + _LOGGER.debug( + "Job %s remaining %s", + job_name, time.strftime('%H:%M:%S', time.gmtime(remaining_secs))) + + +class RepetierJobStartSensor(RepetierSensor): + """Class to create and populate a Repetier Job Start timestamp Sensor.""" + + @property + def device_class(self): + """Return the device class.""" + return DEVICE_CLASS_TIMESTAMP + + def update(self): + """Update the sensor.""" + data = self._get_data() + if data is None: + return + job_name = data['job_name'] + start = data['start'] + from_start = data['from_start'] + self._state = datetime.utcfromtimestamp(start).isoformat() + elapsed_secs = int(round(from_start, 0)) + _LOGGER.debug( + "Job %s elapsed %s", + job_name, time.strftime('%H:%M:%S', time.gmtime(elapsed_secs))) diff --git a/requirements_all.txt b/requirements_all.txt index f6367e30758..f045a546798 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1290,6 +1290,9 @@ pyrainbird==0.1.6 # homeassistant.components.recswitch pyrecswitch==1.0.2 +# homeassistant.components.repetier +pyrepetier==3.0.5 + # homeassistant.components.ruter pyruter==1.1.0 From 6947f8cb2e732dd53971eab03372357240e0881c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 29 May 2019 08:39:12 -0700 Subject: [PATCH 163/232] Cloud: Websocket API to manage Google assistant entity config (#24153) * Extract exposed devices function * Add might_2fa info to trait * Do not filter with should_expose in Google helper func * Cloud: allow setting if Google entity is exposed * Allow disabling 2FA via config * Cloud: allow disabling 2FA * Lint * More changes * Fix typing --- homeassistant/components/cloud/client.py | 20 +++++- homeassistant/components/cloud/const.py | 7 ++ homeassistant/components/cloud/http_api.py | 62 ++++++++++++++++- homeassistant/components/cloud/prefs.py | 52 ++++++++++++++- .../components/google_assistant/helpers.py | 58 +++++++++++----- .../components/google_assistant/smart_home.py | 27 +++----- .../components/google_assistant/trait.py | 33 ++++++++-- homeassistant/helpers/entityfilter.py | 14 ++-- tests/components/cloud/test_client.py | 54 +++++++++++++++ tests/components/cloud/test_http_api.py | 66 ++++++++++++++++++- .../google_assistant/test_smart_home.py | 28 -------- .../components/google_assistant/test_trait.py | 11 ++++ 12 files changed, 346 insertions(+), 86 deletions(-) diff --git a/homeassistant/components/cloud/client.py b/homeassistant/components/cloud/client.py index f47eae74986..eadb1731bd0 100644 --- a/homeassistant/components/cloud/client.py +++ b/homeassistant/components/cloud/client.py @@ -17,7 +17,9 @@ from homeassistant.util.aiohttp import MockRequest from . import utils from .const import ( - CONF_ENTITY_CONFIG, CONF_FILTER, DOMAIN, DISPATCHER_REMOTE_UPDATE) + CONF_ENTITY_CONFIG, CONF_FILTER, DOMAIN, DISPATCHER_REMOTE_UPDATE, + PREF_SHOULD_EXPOSE, DEFAULT_SHOULD_EXPOSE, + PREF_DISABLE_2FA, DEFAULT_DISABLE_2FA) from .prefs import CloudPreferences @@ -98,12 +100,26 @@ class CloudClient(Interface): if entity.entity_id in CLOUD_NEVER_EXPOSED_ENTITIES: return False - return google_conf['filter'](entity.entity_id) + if not google_conf['filter'].empty_filter: + return google_conf['filter'](entity.entity_id) + + entity_configs = self.prefs.google_entity_configs + entity_config = entity_configs.get(entity.entity_id, {}) + return entity_config.get( + PREF_SHOULD_EXPOSE, DEFAULT_SHOULD_EXPOSE) + + def should_2fa(entity): + """If an entity should be checked for 2FA.""" + entity_configs = self.prefs.google_entity_configs + entity_config = entity_configs.get(entity.entity_id, {}) + return not entity_config.get( + PREF_DISABLE_2FA, DEFAULT_DISABLE_2FA) username = self._hass.data[DOMAIN].claims["cognito:username"] self._google_config = ga_h.Config( should_expose=should_expose, + should_2fa=should_2fa, secure_devices_pin=self._prefs.google_secure_devices_pin, entity_config=google_conf.get(CONF_ENTITY_CONFIG), agent_user_id=username, diff --git a/homeassistant/components/cloud/const.py b/homeassistant/components/cloud/const.py index 5002286edb9..e2f4b9c0785 100644 --- a/homeassistant/components/cloud/const.py +++ b/homeassistant/components/cloud/const.py @@ -8,6 +8,13 @@ PREF_ENABLE_REMOTE = 'remote_enabled' PREF_GOOGLE_SECURE_DEVICES_PIN = 'google_secure_devices_pin' PREF_CLOUDHOOKS = 'cloudhooks' PREF_CLOUD_USER = 'cloud_user' +PREF_GOOGLE_ENTITY_CONFIGS = 'google_entity_configs' +PREF_OVERRIDE_NAME = 'override_name' +PREF_DISABLE_2FA = 'disable_2fa' +PREF_ALIASES = 'aliases' +PREF_SHOULD_EXPOSE = 'should_expose' +DEFAULT_SHOULD_EXPOSE = True +DEFAULT_DISABLE_2FA = False CONF_ALEXA = 'alexa' CONF_ALIASES = 'aliases' diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index 40d19c198be..e6151a917af 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -14,8 +14,7 @@ from homeassistant.components.http.data_validator import ( RequestDataValidator) from homeassistant.components import websocket_api from homeassistant.components.alexa import smart_home as alexa_sh -from homeassistant.components.google_assistant import ( - const as google_const) +from homeassistant.components.google_assistant import helpers as google_helpers from .const import ( DOMAIN, REQUEST_TIMEOUT, PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE, @@ -81,6 +80,12 @@ async def async_setup(hass): websocket_remote_connect) hass.components.websocket_api.async_register_command( websocket_remote_disconnect) + + hass.components.websocket_api.async_register_command( + google_assistant_list) + hass.components.websocket_api.async_register_command( + google_assistant_update) + hass.http.register_view(GoogleActionsSyncView) hass.http.register_view(CloudLoginView) hass.http.register_view(CloudLogoutView) @@ -411,7 +416,6 @@ def _account_data(cloud): 'cloud': cloud.iot.state, 'prefs': client.prefs.as_dict(), 'google_entities': client.google_user_config['filter'].config, - 'google_domains': list(google_const.DOMAIN_TO_GOOGLE_TYPES), 'alexa_entities': client.alexa_config.should_expose.config, 'alexa_domains': list(alexa_sh.ENTITY_ADAPTERS), 'remote_domain': remote.instance_domain, @@ -448,3 +452,55 @@ async def websocket_remote_disconnect(hass, connection, msg): await cloud.client.prefs.async_update(remote_enabled=False) await cloud.remote.disconnect() connection.send_result(msg['id'], _account_data(cloud)) + + +@websocket_api.require_admin +@_require_cloud_login +@websocket_api.async_response +@_ws_handle_cloud_errors +@websocket_api.websocket_command({ + 'type': 'cloud/google_assistant/entities' +}) +async def google_assistant_list(hass, connection, msg): + """List all google assistant entities.""" + cloud = hass.data[DOMAIN] + entities = google_helpers.async_get_entities( + hass, cloud.client.google_config + ) + + result = [] + + for entity in entities: + result.append({ + 'entity_id': entity.entity_id, + 'traits': [trait.name for trait in entity.traits()], + 'might_2fa': entity.might_2fa(), + }) + + connection.send_result(msg['id'], result) + + +@websocket_api.require_admin +@_require_cloud_login +@websocket_api.async_response +@_ws_handle_cloud_errors +@websocket_api.websocket_command({ + 'type': 'cloud/google_assistant/entities/update', + 'entity_id': str, + vol.Optional('should_expose'): bool, + vol.Optional('override_name'): str, + vol.Optional('aliases'): [str], + vol.Optional('disable_2fa'): bool, +}) +async def google_assistant_update(hass, connection, msg): + """List all google assistant entities.""" + cloud = hass.data[DOMAIN] + changes = dict(msg) + changes.pop('type') + changes.pop('id') + + await cloud.client.prefs.async_update_google_entity_config(**changes) + + connection.send_result( + msg['id'], + cloud.client.prefs.google_entity_configs.get(msg['entity_id'])) diff --git a/homeassistant/components/cloud/prefs.py b/homeassistant/components/cloud/prefs.py index 0e2abae15b0..0f45f25c49b 100644 --- a/homeassistant/components/cloud/prefs.py +++ b/homeassistant/components/cloud/prefs.py @@ -4,6 +4,8 @@ from ipaddress import ip_address from .const import ( DOMAIN, PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE, PREF_ENABLE_REMOTE, PREF_GOOGLE_SECURE_DEVICES_PIN, PREF_CLOUDHOOKS, PREF_CLOUD_USER, + PREF_GOOGLE_ENTITY_CONFIGS, PREF_OVERRIDE_NAME, PREF_DISABLE_2FA, + PREF_ALIASES, PREF_SHOULD_EXPOSE, InvalidTrustedNetworks) STORAGE_KEY = DOMAIN @@ -30,6 +32,7 @@ class CloudPreferences: PREF_ENABLE_GOOGLE: True, PREF_ENABLE_REMOTE: False, PREF_GOOGLE_SECURE_DEVICES_PIN: None, + PREF_GOOGLE_ENTITY_CONFIGS: {}, PREF_CLOUDHOOKS: {}, PREF_CLOUD_USER: None, } @@ -39,7 +42,7 @@ class CloudPreferences: async def async_update(self, *, google_enabled=_UNDEF, alexa_enabled=_UNDEF, remote_enabled=_UNDEF, google_secure_devices_pin=_UNDEF, cloudhooks=_UNDEF, - cloud_user=_UNDEF): + cloud_user=_UNDEF, google_entity_configs=_UNDEF): """Update user preferences.""" for key, value in ( (PREF_ENABLE_GOOGLE, google_enabled), @@ -48,6 +51,7 @@ class CloudPreferences: (PREF_GOOGLE_SECURE_DEVICES_PIN, google_secure_devices_pin), (PREF_CLOUDHOOKS, cloudhooks), (PREF_CLOUD_USER, cloud_user), + (PREF_GOOGLE_ENTITY_CONFIGS, google_entity_configs), ): if value is not _UNDEF: self._prefs[key] = value @@ -57,9 +61,48 @@ class CloudPreferences: await self._store.async_save(self._prefs) + async def async_update_google_entity_config( + self, *, entity_id, override_name=_UNDEF, disable_2fa=_UNDEF, + aliases=_UNDEF, should_expose=_UNDEF): + """Update config for a Google entity.""" + entities = self.google_entity_configs + entity = entities.get(entity_id, {}) + + changes = {} + for key, value in ( + (PREF_OVERRIDE_NAME, override_name), + (PREF_DISABLE_2FA, disable_2fa), + (PREF_ALIASES, aliases), + (PREF_SHOULD_EXPOSE, should_expose), + ): + if value is not _UNDEF: + changes[key] = value + + if not changes: + return + + updated_entity = { + **entity, + **changes, + } + + updated_entities = { + **entities, + entity_id: updated_entity, + } + await self.async_update(google_entity_configs=updated_entities) + def as_dict(self): """Return dictionary version.""" - return self._prefs + return { + PREF_ENABLE_ALEXA: self.alexa_enabled, + PREF_ENABLE_GOOGLE: self.google_enabled, + PREF_ENABLE_REMOTE: self.remote_enabled, + PREF_GOOGLE_SECURE_DEVICES_PIN: self.google_secure_devices_pin, + PREF_GOOGLE_ENTITY_CONFIGS: self.google_entity_configs, + PREF_CLOUDHOOKS: self.cloudhooks, + PREF_CLOUD_USER: self.cloud_user, + } @property def remote_enabled(self): @@ -89,6 +132,11 @@ class CloudPreferences: """Return if Google is allowed to unlock locks.""" return self._prefs.get(PREF_GOOGLE_SECURE_DEVICES_PIN) + @property + def google_entity_configs(self): + """Return Google Entity configurations.""" + return self._prefs.get(PREF_GOOGLE_ENTITY_CONFIGS, {}) + @property def cloudhooks(self): """Return the published cloud webhooks.""" diff --git a/homeassistant/components/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py index 4d3f2855b31..770a502ad5d 100644 --- a/homeassistant/components/google_assistant/helpers.py +++ b/homeassistant/components/google_assistant/helpers.py @@ -1,17 +1,18 @@ """Helper classes for Google Assistant integration.""" from asyncio import gather from collections.abc import Mapping +from typing import List from homeassistant.core import Context, callback from homeassistant.const import ( CONF_NAME, STATE_UNAVAILABLE, ATTR_SUPPORTED_FEATURES, - ATTR_DEVICE_CLASS + ATTR_DEVICE_CLASS, CLOUD_NEVER_EXPOSED_ENTITIES ) from . import trait from .const import ( DOMAIN_TO_GOOGLE_TYPES, CONF_ALIASES, ERR_FUNCTION_NOT_SUPPORTED, - DEVICE_CLASS_TO_GOOGLE_TYPES, CONF_ROOM_HINT, + DEVICE_CLASS_TO_GOOGLE_TYPES, CONF_ROOM_HINT ) from .error import SmartHomeError @@ -21,15 +22,20 @@ class Config: def __init__(self, should_expose, entity_config=None, secure_devices_pin=None, - agent_user_id=None): + agent_user_id=None, should_2fa=None): """Initialize the configuration.""" self.should_expose = should_expose self.entity_config = entity_config or {} self.secure_devices_pin = secure_devices_pin + self._should_2fa = should_2fa # Agent User Id to use for query responses self.agent_user_id = agent_user_id + def should_2fa(self, state): + """If an entity should have 2FA checked.""" + return self._should_2fa is None or self._should_2fa(state) + class RequestData: """Hold data associated with a particular request.""" @@ -79,6 +85,22 @@ class GoogleEntity: if Trait.supported(domain, features, device_class)] return self._traits + @callback + def is_supported(self) -> bool: + """Return if the entity is supported by Google.""" + return self.state.state != STATE_UNAVAILABLE and bool(self.traits()) + + @callback + def might_2fa(self) -> bool: + """Return if the entity might encounter 2FA.""" + state = self.state + domain = state.domain + features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + device_class = state.attributes.get(ATTR_DEVICE_CLASS) + + return any(trait.might_2fa(domain, features, device_class) + for trait in self.traits()) + async def sync_serialize(self): """Serialize entity for a SYNC response. @@ -86,27 +108,13 @@ class GoogleEntity: """ state = self.state - # When a state is unavailable, the attributes that describe - # capabilities will be stripped. For example, a light entity will miss - # the min/max mireds. Therefore they will be excluded from a sync. - if state.state == STATE_UNAVAILABLE: - return None - entity_config = self.config.entity_config.get(state.entity_id, {}) name = (entity_config.get(CONF_NAME) or state.name).strip() domain = state.domain device_class = state.attributes.get(ATTR_DEVICE_CLASS) - # If an empty string - if not name: - return None - traits = self.traits() - # Found no supported traits for this entity - if not traits: - return None - device_type = get_google_type(domain, device_class) @@ -213,3 +221,19 @@ def deep_update(target, source): else: target[key] = value return target + + +@callback +def async_get_entities(hass, config) -> List[GoogleEntity]: + """Return all entities that are supported by Google.""" + entities = [] + for state in hass.states.async_all(): + if state.entity_id in CLOUD_NEVER_EXPOSED_ENTITIES: + continue + + entity = GoogleEntity(hass, config, state) + + if entity.is_supported(): + entities.append(entity) + + return entities diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index 1ec47bbedd6..07548ee95eb 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -1,17 +1,17 @@ """Support for Google Assistant Smart Home API.""" +import asyncio from itertools import product import logging from homeassistant.util.decorator import Registry -from homeassistant.const import ( - CLOUD_NEVER_EXPOSED_ENTITIES, ATTR_ENTITY_ID) +from homeassistant.const import ATTR_ENTITY_ID from .const import ( ERR_PROTOCOL_ERROR, ERR_DEVICE_OFFLINE, ERR_UNKNOWN_ERROR, EVENT_COMMAND_RECEIVED, EVENT_SYNC_RECEIVED, EVENT_QUERY_RECEIVED ) -from .helpers import RequestData, GoogleEntity +from .helpers import RequestData, GoogleEntity, async_get_entities from .error import SmartHomeError HANDLERS = Registry() @@ -81,22 +81,11 @@ async def async_devices_sync(hass, data, payload): {'request_id': data.request_id}, context=data.context) - devices = [] - for state in hass.states.async_all(): - if state.entity_id in CLOUD_NEVER_EXPOSED_ENTITIES: - continue - - if not data.config.should_expose(state): - continue - - entity = GoogleEntity(hass, data.config, state) - serialized = await entity.sync_serialize() - - if serialized is None: - _LOGGER.debug("No mapping for %s domain", entity.state) - continue - - devices.append(serialized) + devices = await asyncio.gather(*[ + entity.sync_serialize() for entity in + async_get_entities(hass, data.config) + if data.config.should_expose(entity.state) + ]) response = { 'agentUserId': data.config.agent_user_id or data.context.user_id, diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index cb2bf688ad0..f9590a07b95 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -104,6 +104,11 @@ class _Trait: commands = [] + @staticmethod + def might_2fa(domain, features, device_class): + """Return if the trait might ask for 2FA.""" + return False + def __init__(self, hass, state, config): """Initialize a trait for a state.""" self.hass = hass @@ -732,6 +737,11 @@ class LockUnlockTrait(_Trait): """Test if state is supported.""" return domain == lock.DOMAIN + @staticmethod + def might_2fa(domain, features, device_class): + """Return if the trait might ask for 2FA.""" + return True + def sync_attributes(self): """Return LockUnlock attributes for a sync request.""" return {} @@ -745,7 +755,7 @@ class LockUnlockTrait(_Trait): if params['lock']: service = lock.SERVICE_LOCK else: - _verify_pin_challenge(data, challenge) + _verify_pin_challenge(data, self.state, challenge) service = lock.SERVICE_UNLOCK await self.hass.services.async_call(lock.DOMAIN, service, { @@ -1021,6 +1031,9 @@ class OpenCloseTrait(_Trait): https://developers.google.com/actions/smarthome/traits/openclose """ + # Cover device classes that require 2FA + COVER_2FA = (cover.DEVICE_CLASS_DOOR, cover.DEVICE_CLASS_GARAGE) + name = TRAIT_OPENCLOSE commands = [ COMMAND_OPENCLOSE @@ -1042,6 +1055,12 @@ class OpenCloseTrait(_Trait): binary_sensor.DEVICE_CLASS_WINDOW, ) + @staticmethod + def might_2fa(domain, features, device_class): + """Return if the trait might ask for 2FA.""" + return (domain == cover.DOMAIN and + device_class in OpenCloseTrait.COVER_2FA) + def sync_attributes(self): """Return opening direction.""" response = {} @@ -1114,9 +1133,8 @@ class OpenCloseTrait(_Trait): if (should_verify and self.state.attributes.get(ATTR_DEVICE_CLASS) - in (cover.DEVICE_CLASS_DOOR, - cover.DEVICE_CLASS_GARAGE)): - _verify_pin_challenge(data, challenge) + in OpenCloseTrait.COVER_2FA): + _verify_pin_challenge(data, self.state, challenge) await self.hass.services.async_call( cover.DOMAIN, service, svc_params, @@ -1202,8 +1220,11 @@ class VolumeTrait(_Trait): ERR_NOT_SUPPORTED, 'Command not supported') -def _verify_pin_challenge(data, challenge): +def _verify_pin_challenge(data, state, challenge): """Verify a pin challenge.""" + if not data.config.should_2fa(state): + return + if not data.config.secure_devices_pin: raise SmartHomeError( ERR_CHALLENGE_NOT_SETUP, 'Challenge is not set up') @@ -1217,7 +1238,7 @@ def _verify_pin_challenge(data, challenge): raise ChallengeNeeded(CHALLENGE_FAILED_PIN_NEEDED) -def _verify_ack_challenge(data, challenge): +def _verify_ack_challenge(data, state, challenge): """Verify a pin challenge.""" if not challenge or not challenge.get('ack'): raise ChallengeNeeded(CHALLENGE_ACK_NEEDED) diff --git a/homeassistant/helpers/entityfilter.py b/homeassistant/helpers/entityfilter.py index 7db577dfdc6..590aba02670 100644 --- a/homeassistant/helpers/entityfilter.py +++ b/homeassistant/helpers/entityfilter.py @@ -1,5 +1,5 @@ """Helper class to implement include/exclude of entities and domains.""" -from typing import Callable, Dict, Iterable +from typing import Callable, Dict, List import voluptuous as vol @@ -12,7 +12,7 @@ CONF_EXCLUDE_DOMAINS = 'exclude_domains' CONF_EXCLUDE_ENTITIES = 'exclude_entities' -def _convert_filter(config: Dict[str, Iterable[str]]) -> Callable[[str], bool]: +def _convert_filter(config: Dict[str, List[str]]) -> Callable[[str], bool]: filt = generate_filter( config[CONF_INCLUDE_DOMAINS], config[CONF_INCLUDE_ENTITIES], @@ -20,6 +20,8 @@ def _convert_filter(config: Dict[str, Iterable[str]]) -> Callable[[str], bool]: config[CONF_EXCLUDE_ENTITIES], ) setattr(filt, 'config', config) + setattr( + filt, 'empty_filter', sum(len(val) for val in config.values()) == 0) return filt @@ -34,10 +36,10 @@ FILTER_SCHEMA = vol.All( }), _convert_filter) -def generate_filter(include_domains: Iterable[str], - include_entities: Iterable[str], - exclude_domains: Iterable[str], - exclude_entities: Iterable[str]) -> Callable[[str], bool]: +def generate_filter(include_domains: List[str], + include_entities: List[str], + exclude_domains: List[str], + exclude_entities: List[str]) -> Callable[[str], bool]: """Return a function that will filter entities based on the args.""" include_d = set(include_domains) include_e = set(include_entities) diff --git a/tests/components/cloud/test_client.py b/tests/components/cloud/test_client.py index 4440651d089..fa1d8cf8b9b 100644 --- a/tests/components/cloud/test_client.py +++ b/tests/components/cloud/test_client.py @@ -2,9 +2,12 @@ from unittest.mock import patch, MagicMock from aiohttp import web +import jwt import pytest +from homeassistant.core import State from homeassistant.setup import async_setup_component +from homeassistant.components.cloud import DOMAIN from homeassistant.components.cloud.const import ( PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE) from tests.components.alexa import test_smart_home as test_alexa @@ -19,6 +22,25 @@ def mock_cloud(): return MagicMock(subscription_expired=False) +@pytest.fixture +async def mock_cloud_setup(hass): + """Set up the cloud.""" + with patch('hass_nabucasa.Cloud.start', return_value=mock_coro()): + assert await async_setup_component(hass, 'cloud', { + 'cloud': {} + }) + + +@pytest.fixture +def mock_cloud_login(hass, mock_cloud_setup): + """Mock cloud is logged in.""" + hass.data[DOMAIN].id_token = jwt.encode({ + 'email': 'hello@home-assistant.io', + 'custom:sub-exp': '2018-01-03', + 'cognito:username': 'abcdefghjkl', + }, 'test') + + async def test_handler_alexa(hass): """Test handler Alexa.""" hass.states.async_set( @@ -197,3 +219,35 @@ async def test_webhook_msg(hass): assert await received[0].json() == { 'hello': 'world' } + + +async def test_google_config_expose_entity( + hass, mock_cloud_setup, mock_cloud_login): + """Test Google config exposing entity method uses latest config.""" + cloud_client = hass.data[DOMAIN].client + state = State('light.kitchen', 'on') + + assert cloud_client.google_config.should_expose(state) + + await cloud_client.prefs.async_update_google_entity_config( + entity_id='light.kitchen', + should_expose=False, + ) + + assert not cloud_client.google_config.should_expose(state) + + +async def test_google_config_should_2fa( + hass, mock_cloud_setup, mock_cloud_login): + """Test Google config disabling 2FA method uses latest config.""" + cloud_client = hass.data[DOMAIN].client + state = State('light.kitchen', 'on') + + assert cloud_client.google_config.should_2fa(state) + + await cloud_client.prefs.async_update_google_entity_config( + entity_id='light.kitchen', + disable_2fa=True, + ) + + assert not cloud_client.google_config.should_2fa(state) diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py index 4aebc5679a0..5ccaba14be6 100644 --- a/tests/components/cloud/test_http_api.py +++ b/tests/components/cloud/test_http_api.py @@ -7,10 +7,13 @@ from jose import jwt from hass_nabucasa.auth import Unauthenticated, UnknownError from hass_nabucasa.const import STATE_CONNECTED +from homeassistant.core import State from homeassistant.auth.providers import trusted_networks as tn_auth from homeassistant.components.cloud.const import ( PREF_ENABLE_GOOGLE, PREF_ENABLE_ALEXA, PREF_GOOGLE_SECURE_DEVICES_PIN, DOMAIN) +from homeassistant.components.google_assistant.helpers import ( + GoogleEntity, Config) from tests.common import mock_coro @@ -32,7 +35,8 @@ def mock_cloud_login(hass, setup_api): """Mock cloud is logged in.""" hass.data[DOMAIN].id_token = jwt.encode({ 'email': 'hello@home-assistant.io', - 'custom:sub-exp': '2018-01-03' + 'custom:sub-exp': '2018-01-03', + 'cognito:username': 'abcdefghjkl', }, 'test') @@ -349,7 +353,15 @@ async def test_websocket_status(hass, hass_ws_client, mock_cloud_fixture, 'logged_in': True, 'email': 'hello@home-assistant.io', 'cloud': 'connected', - 'prefs': mock_cloud_fixture, + 'prefs': { + 'alexa_enabled': True, + 'cloud_user': None, + 'cloudhooks': {}, + 'google_enabled': True, + 'google_entity_configs': {}, + 'google_secure_devices_pin': None, + 'remote_enabled': False, + }, 'alexa_entities': { 'include_domains': [], 'include_entities': ['light.kitchen', 'switch.ac'], @@ -363,7 +375,6 @@ async def test_websocket_status(hass, hass_ws_client, mock_cloud_fixture, 'exclude_domains': [], 'exclude_entities': [], }, - 'google_domains': ['light'], 'remote_domain': None, 'remote_connected': False, 'remote_certificate': None, @@ -689,3 +700,52 @@ async def test_enabling_remote_trusted_networks_other( assert cloud.client.remote_autostart assert len(mock_connect.mock_calls) == 1 + + +async def test_list_google_entities( + hass, hass_ws_client, setup_api, mock_cloud_login): + """Test that we can list Google entities.""" + client = await hass_ws_client(hass) + entity = GoogleEntity(hass, Config(lambda *_: False), State( + 'light.kitchen', 'on' + )) + with patch('homeassistant.components.google_assistant.helpers' + '.async_get_entities', return_value=[entity]): + await client.send_json({ + 'id': 5, + 'type': 'cloud/google_assistant/entities', + }) + response = await client.receive_json() + + assert response['success'] + assert len(response['result']) == 1 + assert response['result'][0] == { + 'entity_id': 'light.kitchen', + 'might_2fa': False, + 'traits': ['action.devices.traits.OnOff'], + } + + +async def test_update_google_entity( + hass, hass_ws_client, setup_api, mock_cloud_login): + """Test that we can update config of a Google entity.""" + client = await hass_ws_client(hass) + await client.send_json({ + 'id': 5, + 'type': 'cloud/google_assistant/entities/update', + 'entity_id': 'light.kitchen', + 'should_expose': False, + 'override_name': 'updated name', + 'aliases': ['lefty', 'righty'], + 'disable_2fa': False, + }) + response = await client.receive_json() + + assert response['success'] + prefs = hass.data[DOMAIN].client.prefs + assert prefs.google_entity_configs['light.kitchen'] == { + 'should_expose': False, + 'override_name': 'updated name', + 'aliases': ['lefty', 'righty'], + 'disable_2fa': False, + } diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index 519a55fbc00..a65387d48a2 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -530,34 +530,6 @@ async def test_unavailable_state_doesnt_sync(hass): } -async def test_empty_name_doesnt_sync(hass): - """Test that an entity with empty name does not sync over.""" - light = DemoLight( - None, ' ', - state=False, - ) - light.hass = hass - light.entity_id = 'light.demo_light' - await light.async_update_ha_state() - - result = await sh.async_handle_message( - hass, BASIC_CONFIG, 'test-agent', - { - "requestId": REQ_ID, - "inputs": [{ - "intent": "action.devices.SYNC" - }] - }) - - assert result == { - 'requestId': REQ_ID, - 'payload': { - 'agentUserId': 'test-agent', - 'devices': [] - } - } - - @pytest.mark.parametrize("device_class,google_type", [ ('non_existing_class', 'action.devices.types.SWITCH'), ('switch', 'action.devices.types.SWITCH'), diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index 5e6dadf14f4..28cab008201 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -843,6 +843,8 @@ async def test_lock_unlock_lock(hass): assert helpers.get_google_type(lock.DOMAIN, None) is not None assert trait.LockUnlockTrait.supported(lock.DOMAIN, lock.SUPPORT_OPEN, None) + assert trait.LockUnlockTrait.might_2fa(lock.DOMAIN, lock.SUPPORT_OPEN, + None) trt = trait.LockUnlockTrait(hass, State('lock.front_door', lock.STATE_LOCKED), @@ -922,6 +924,13 @@ async def test_lock_unlock_unlock(hass): assert len(calls) == 1 assert err.value.code == const.ERR_CHALLENGE_NOT_SETUP + # Test with 2FA override + with patch('homeassistant.components.google_assistant.helpers' + '.Config.should_2fa', return_value=False): + await trt.execute( + trait.COMMAND_LOCKUNLOCK, BASIC_DATA, {'lock': False}, {}) + assert len(calls) == 2 + async def test_fan_speed(hass): """Test FanSpeed trait speed control support for fan domain.""" @@ -1216,6 +1225,8 @@ async def test_openclose_cover_secure(hass, device_class): assert helpers.get_google_type(cover.DOMAIN, device_class) is not None assert trait.OpenCloseTrait.supported( cover.DOMAIN, cover.SUPPORT_SET_POSITION, device_class) + assert trait.OpenCloseTrait.might_2fa( + cover.DOMAIN, cover.SUPPORT_SET_POSITION, device_class) trt = trait.OpenCloseTrait(hass, State('cover.bla', cover.STATE_OPEN, { ATTR_DEVICE_CLASS: device_class, From a252065f99c63b84a0211be423ddb4b56a807f26 Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Wed, 29 May 2019 09:09:25 -0700 Subject: [PATCH 164/232] Fix calling notify.notify with mobile_app targets in play. Fixes #24064 (#24156) --- homeassistant/components/mobile_app/notify.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/mobile_app/notify.py b/homeassistant/components/mobile_app/notify.py index 721751e69a8..e10ebf13c4c 100644 --- a/homeassistant/components/mobile_app/notify.py +++ b/homeassistant/components/mobile_app/notify.py @@ -89,13 +89,12 @@ class MobileAppNotificationService(BaseNotificationService): targets = kwargs.get(ATTR_TARGET) if not targets: - targets = push_registrations(self.hass) + targets = push_registrations(self.hass).values() 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 From dfb992adb264d5672e9f55ed50314c1c61b58edb Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 29 May 2019 09:41:35 -0700 Subject: [PATCH 165/232] Updated frontend to 20190529.0 --- homeassistant/components/frontend/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 1150c70d0d8..72885690223 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/components/frontend", "requirements": [ - "home-assistant-frontend==20190523.0" + "home-assistant-frontend==20190529.0" ], "dependencies": [ "api", diff --git a/requirements_all.txt b/requirements_all.txt index f045a546798..8f79fbcedbf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -580,7 +580,7 @@ hole==0.3.0 holidays==0.9.10 # homeassistant.components.frontend -home-assistant-frontend==20190523.0 +home-assistant-frontend==20190529.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 39f0512cdeb..728a2f3dfa3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -151,7 +151,7 @@ hdate==0.8.7 holidays==0.9.10 # homeassistant.components.frontend -home-assistant-frontend==20190523.0 +home-assistant-frontend==20190529.0 # homeassistant.components.homekit_controller homekit[IP]==0.14.0 From fd3902f7e7feaa1397aaad728c3b1aa3d7684b94 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 29 May 2019 10:16:58 -0700 Subject: [PATCH 166/232] update translations --- .../ambiclimate/.translations/fr.json | 23 +++++++++++++++++++ .../ambiclimate/.translations/sl.json | 23 +++++++++++++++++++ .../components/deconz/.translations/fr.json | 3 ++- .../components/esphome/.translations/ca.json | 1 + .../components/esphome/.translations/en.json | 1 + .../components/esphome/.translations/fr.json | 1 + .../components/esphome/.translations/ru.json | 1 + .../components/esphome/.translations/sl.json | 1 + .../components/esphome/.translations/sv.json | 1 + .../esphome/.translations/zh-Hant.json | 1 + .../components/heos/.translations/fr.json | 3 ++- .../homekit_controller/.translations/fr.json | 5 ++++ .../homekit_controller/.translations/sl.json | 8 ++++++- .../components/hue/.translations/ca.json | 1 + .../components/hue/.translations/en.json | 1 + .../components/hue/.translations/fr.json | 1 + .../components/hue/.translations/ru.json | 1 + .../components/hue/.translations/sl.json | 1 + .../components/hue/.translations/sv.json | 1 + .../components/hue/.translations/zh-Hant.json | 1 + .../components/ipma/.translations/fr.json | 1 + .../components/iqvia/.translations/sl.json | 18 +++++++++++++++ .../logi_circle/.translations/fr.json | 16 ++++++++++++- .../onboarding/.translations/sl.json | 7 ++++++ .../components/ps4/.translations/fr.json | 1 + .../components/ps4/.translations/sl.json | 1 + 26 files changed, 119 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/ambiclimate/.translations/fr.json create mode 100644 homeassistant/components/ambiclimate/.translations/sl.json create mode 100644 homeassistant/components/iqvia/.translations/sl.json create mode 100644 homeassistant/components/onboarding/.translations/sl.json diff --git a/homeassistant/components/ambiclimate/.translations/fr.json b/homeassistant/components/ambiclimate/.translations/fr.json new file mode 100644 index 00000000000..6d09fd6ee05 --- /dev/null +++ b/homeassistant/components/ambiclimate/.translations/fr.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "access_token": "Erreur inconnue lors de la g\u00e9n\u00e9ration d'un jeton d'acc\u00e8s.", + "already_setup": "Le compte Ambiclimate est configur\u00e9.", + "no_config": "Vous devez configurer Ambiclimate avant de pouvoir vous authentifier aupr\u00e8s de celui-ci. [Veuillez lire les instructions] (https://www.home-assistant.io/components/ambiclimate/)." + }, + "create_entry": { + "default": "Authentifi\u00e9 avec succ\u00e8s avec Ambiclimate" + }, + "error": { + "follow_link": "Veuillez suivre le lien et vous authentifier avant d'appuyer sur Soumettre.", + "no_token": "Non authentifi\u00e9 avec Ambiclimate" + }, + "step": { + "auth": { + "description": "Suivez ce [lien] ( {authorization_url} ) et Autorisez l'acc\u00e8s \u00e0 votre compte Ambiclimate, puis revenez et appuyez sur Envoyer ci-dessous. \n (Assurez-vous que l'URL de rappel sp\u00e9cifi\u00e9 est {cb_url} )", + "title": "Authentifier Ambiclimate" + } + }, + "title": "Ambiclimate" + } +} \ No newline at end of file diff --git a/homeassistant/components/ambiclimate/.translations/sl.json b/homeassistant/components/ambiclimate/.translations/sl.json new file mode 100644 index 00000000000..cae2e940d56 --- /dev/null +++ b/homeassistant/components/ambiclimate/.translations/sl.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "access_token": "Neznana napaka pri ustvarjanju \u017eetona za dostop.", + "already_setup": "Ra\u010dun Ambiclimate je konfiguriran.", + "no_config": "Ambiclimat morate konfigurirati, preden lahko z njo preverjate pristnost. [Preberite navodila] (https://www.home-assistant.io/components/ambiclimate/)." + }, + "create_entry": { + "default": "Uspe\u0161no overjeno z funkcijo Ambiclimate" + }, + "error": { + "follow_link": "Preden pritisnete Po\u0161lji, sledite povezavi in preverite pristnost", + "no_token": "Ni overjeno z Ambiclimate" + }, + "step": { + "auth": { + "description": "Sledite temu povezavi ( {authorization_url} in Dovoli dostopu do svojega ra\u010duna Ambiclimate, nato se vrnite in pritisnite Po\u0161lji spodaj. \n (Poskrbite, da je dolo\u010den url za povratni klic {cb_url} )", + "title": "Overi Ambiclimate" + } + }, + "title": "Ambiclimate" + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/fr.json b/homeassistant/components/deconz/.translations/fr.json index 23036c19aec..3d658ca00b0 100644 --- a/homeassistant/components/deconz/.translations/fr.json +++ b/homeassistant/components/deconz/.translations/fr.json @@ -3,7 +3,8 @@ "abort": { "already_configured": "Ce pont est d\u00e9j\u00e0 configur\u00e9", "no_bridges": "Aucun pont deCONZ n'a \u00e9t\u00e9 d\u00e9couvert", - "one_instance_only": "Le composant prend uniquement en charge une instance deCONZ" + "one_instance_only": "Le composant prend uniquement en charge une instance deCONZ", + "updated_instance": "Instance deCONZ mise \u00e0 jour avec la nouvelle adresse d'h\u00f4te" }, "error": { "no_key": "Impossible d'obtenir une cl\u00e9 d'API" diff --git a/homeassistant/components/esphome/.translations/ca.json b/homeassistant/components/esphome/.translations/ca.json index f9c60979c8d..2e6f8dc62ad 100644 --- a/homeassistant/components/esphome/.translations/ca.json +++ b/homeassistant/components/esphome/.translations/ca.json @@ -8,6 +8,7 @@ "invalid_password": "Contrasenya inv\u00e0lida!", "resolve_error": "No s'ha pogut trobar l'adre\u00e7a de l'ESP. Si l'error persisteix, configura una adre\u00e7a IP est\u00e0tica: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" }, + "flow_title": "ESPHome: {name}", "step": { "authenticate": { "data": { diff --git a/homeassistant/components/esphome/.translations/en.json b/homeassistant/components/esphome/.translations/en.json index 3a73e54c345..f5236d1735d 100644 --- a/homeassistant/components/esphome/.translations/en.json +++ b/homeassistant/components/esphome/.translations/en.json @@ -8,6 +8,7 @@ "invalid_password": "Invalid password!", "resolve_error": "Can't resolve address of the ESP. If this error persists, please set a static IP address: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" }, + "flow_title": "ESPHome: {name}", "step": { "authenticate": { "data": { diff --git a/homeassistant/components/esphome/.translations/fr.json b/homeassistant/components/esphome/.translations/fr.json index b230a73c354..26fa4ec0bd4 100644 --- a/homeassistant/components/esphome/.translations/fr.json +++ b/homeassistant/components/esphome/.translations/fr.json @@ -8,6 +8,7 @@ "invalid_password": "Mot de passe invalide !", "resolve_error": "Impossible de r\u00e9soudre l'adresse de l'ESP. Si cette erreur persiste, veuillez d\u00e9finir une adresse IP statique: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" }, + "flow_title": "ESPHome: {name}", "step": { "authenticate": { "data": { diff --git a/homeassistant/components/esphome/.translations/ru.json b/homeassistant/components/esphome/.translations/ru.json index 9777a920a94..1405112c070 100644 --- a/homeassistant/components/esphome/.translations/ru.json +++ b/homeassistant/components/esphome/.translations/ru.json @@ -8,6 +8,7 @@ "invalid_password": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043f\u0430\u0440\u043e\u043b\u044c!", "resolve_error": "\u041d\u0435 \u0443\u0434\u0430\u0435\u0442\u0441\u044f \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c \u0430\u0434\u0440\u0435\u0441 ESP. \u0415\u0441\u043b\u0438 \u044d\u0442\u0430 \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0432\u0442\u043e\u0440\u044f\u0435\u0442\u0441\u044f, \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u0441\u0442\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0439 IP-\u0430\u0434\u0440\u0435\u0441: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" }, + "flow_title": "ESPHome: {name}", "step": { "authenticate": { "data": { diff --git a/homeassistant/components/esphome/.translations/sl.json b/homeassistant/components/esphome/.translations/sl.json index 93ca607aabe..5f4e9d3e4c4 100644 --- a/homeassistant/components/esphome/.translations/sl.json +++ b/homeassistant/components/esphome/.translations/sl.json @@ -8,6 +8,7 @@ "invalid_password": "Neveljavno geslo!", "resolve_error": "Ne moremo razre\u0161iti naslova ESP. \u010ce se napaka ponovi, prosimo nastavite stati\u010dni IP naslov: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" }, + "flow_title": "ESPHome: {name}", "step": { "authenticate": { "data": { diff --git a/homeassistant/components/esphome/.translations/sv.json b/homeassistant/components/esphome/.translations/sv.json index da977af601a..37788522e4f 100644 --- a/homeassistant/components/esphome/.translations/sv.json +++ b/homeassistant/components/esphome/.translations/sv.json @@ -8,6 +8,7 @@ "invalid_password": "Ogiltigt l\u00f6senord!", "resolve_error": "Det g\u00e5r inte att hitta IP-adressen f\u00f6r ESP med DNS-namnet. Om det h\u00e4r felet kvarst\u00e5r anger du en statisk IP-adress: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" }, + "flow_title": "ESPHome: {name}", "step": { "authenticate": { "data": { diff --git a/homeassistant/components/esphome/.translations/zh-Hant.json b/homeassistant/components/esphome/.translations/zh-Hant.json index 9a5821f0b8f..721f4362103 100644 --- a/homeassistant/components/esphome/.translations/zh-Hant.json +++ b/homeassistant/components/esphome/.translations/zh-Hant.json @@ -8,6 +8,7 @@ "invalid_password": "\u5bc6\u78bc\u7121\u6548\uff01", "resolve_error": "\u7121\u6cd5\u89e3\u6790 ESP \u4f4d\u5740\uff0c\u5047\u5982\u6b64\u932f\u8aa4\u6301\u7e8c\u767c\u751f\uff0c\u8acb\u53c3\u8003\u8aaa\u660e\u8a2d\u5b9a\u70ba\u975c\u614b\u56fa\u5b9a IP \uff1a https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" }, + "flow_title": "ESPHome\uff1a{name}", "step": { "authenticate": { "data": { diff --git a/homeassistant/components/heos/.translations/fr.json b/homeassistant/components/heos/.translations/fr.json index 274075af749..549cd00e8e0 100644 --- a/homeassistant/components/heos/.translations/fr.json +++ b/homeassistant/components/heos/.translations/fr.json @@ -9,7 +9,8 @@ "step": { "user": { "data": { - "access_token": "H\u00f4te" + "access_token": "H\u00f4te", + "host": "H\u00f4te" }, "description": "Veuillez saisir le nom d\u2019h\u00f4te ou l\u2019adresse IP d\u2019un p\u00e9riph\u00e9rique Heos (de pr\u00e9f\u00e9rence connect\u00e9 au r\u00e9seau filaire).", "title": "Se connecter \u00e0 Heos" diff --git a/homeassistant/components/homekit_controller/.translations/fr.json b/homeassistant/components/homekit_controller/.translations/fr.json index 5e1bea42bdc..955e11d12b0 100644 --- a/homeassistant/components/homekit_controller/.translations/fr.json +++ b/homeassistant/components/homekit_controller/.translations/fr.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "accessory_not_found_error": "Impossible d'ajouter le couplage car l'appareil est introuvable.", "already_configured": "L'accessoire est d\u00e9j\u00e0 configur\u00e9 avec ce contr\u00f4leur.", "already_paired": "Cet accessoire est d\u00e9j\u00e0 associ\u00e9 \u00e0 un autre appareil. R\u00e9initialisez l\u2019accessoire et r\u00e9essayez.", "ignored_model": "La prise en charge de HomeKit pour ce mod\u00e8le est bloqu\u00e9e car une int\u00e9gration native plus compl\u00e8te est disponible.", @@ -9,10 +10,14 @@ }, "error": { "authentication_error": "Code HomeKit incorrect. S'il vous pla\u00eet v\u00e9rifier et essayez \u00e0 nouveau.", + "busy_error": "L'appareil a refus\u00e9 d'ajouter le couplage car il est d\u00e9j\u00e0 coupl\u00e9 avec un autre contr\u00f4leur.", + "max_peers_error": "L'appareil a refus\u00e9 d'ajouter le couplage car il ne dispose pas de stockage de couplage libre.", + "max_tries_error": "Le p\u00e9riph\u00e9rique a refus\u00e9 d'ajouter le couplage car il a re\u00e7u plus de 100 tentatives d'authentification infructueuses.", "pairing_failed": "Une erreur non g\u00e9r\u00e9e s'est produite lors de la tentative d'appairage avec cet appareil. Il se peut qu'il s'agisse d'une panne temporaire ou que votre appareil ne soit pas pris en charge actuellement.", "unable_to_pair": "Impossible d'appairer, veuillez r\u00e9essayer.", "unknown_error": "L'appareil a signal\u00e9 une erreur inconnue. L'appairage a \u00e9chou\u00e9." }, + "flow_title": "Accessoire HomeKit: {name}", "step": { "pair": { "data": { diff --git a/homeassistant/components/homekit_controller/.translations/sl.json b/homeassistant/components/homekit_controller/.translations/sl.json index afee189216d..0404dd7beb5 100644 --- a/homeassistant/components/homekit_controller/.translations/sl.json +++ b/homeassistant/components/homekit_controller/.translations/sl.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "accessory_not_found_error": "Seznanjanja ni mogo\u010de dodati, ker naprave ni ve\u010d mogo\u010de najti.", "already_configured": "Dodatna oprema je \u017ee konfigurirana s tem krmilnikom.", "already_paired": "Ta dodatna oprema je \u017ee povezana z drugo napravo. Ponastavite dodatno opremo in poskusite znova.", "ignored_model": "Podpora za HomeKit za ta model je blokirana, saj je na voljo ve\u010d funkcij popolne nativne integracije.", @@ -9,15 +10,20 @@ }, "error": { "authentication_error": "Nepravilna koda HomeKit. Preverite in poskusite znova.", + "busy_error": "Naprava je zavrnila seznanjanje, saj se \u017ee povezuje z drugim krmilnikom.", + "max_peers_error": "Naprava je zavrnila seznanjanje, saj nima prostega pomnilnika za seznanjanje.", + "max_tries_error": "Napravaje zavrnila seznanjanje, saj je prejela ve\u010d kot 100 neuspe\u0161nih poskusov overjanja.", + "pairing_failed": "Pri poskusu seznanjanja s to napravo je pri\u0161lo do napake. To je lahko za\u010dasna napaka ali pa naprava trenutno ni podprta.", "unable_to_pair": "Ni mogo\u010de seznaniti. Poskusite znova.", "unknown_error": "Naprava je sporo\u010dila neznano napako. Seznanjanje ni uspelo." }, + "flow_title": "HomeKit Oprema: {name}", "step": { "pair": { "data": { "pairing_code": "Koda za seznanjanje" }, - "description": "Vnesi HomeKit kodo, \u010de \u017eeli\u0161 uporabiti to dodatno opremo", + "description": "\u010ce \u017eeli\u0161 uporabiti to dodatno opremo, vnesi HomeKit kodo.", "title": "Seznanite s HomeKit Opremo" }, "user": { diff --git a/homeassistant/components/hue/.translations/ca.json b/homeassistant/components/hue/.translations/ca.json index a37d4ef1518..078c4e75377 100644 --- a/homeassistant/components/hue/.translations/ca.json +++ b/homeassistant/components/hue/.translations/ca.json @@ -3,6 +3,7 @@ "abort": { "all_configured": "Tots els enlla\u00e7os Philips Hue ja estan configurats", "already_configured": "L'enlla\u00e7 ja est\u00e0 configurat", + "already_in_progress": "El flux de dades de configuraci\u00f3 per l'enlla\u00e7 ja est\u00e0 en curs.", "cannot_connect": "No s'ha pogut connectar amb l'enlla\u00e7", "discover_timeout": "No s'han pogut descobrir enlla\u00e7os Hue", "no_bridges": "No s'han trobat enlla\u00e7os Philips Hue", diff --git a/homeassistant/components/hue/.translations/en.json b/homeassistant/components/hue/.translations/en.json index cea8d8be10a..744efb1b15e 100644 --- a/homeassistant/components/hue/.translations/en.json +++ b/homeassistant/components/hue/.translations/en.json @@ -3,6 +3,7 @@ "abort": { "all_configured": "All Philips Hue bridges are already configured", "already_configured": "Bridge is already configured", + "already_in_progress": "Config flow for bridge is already in progress.", "cannot_connect": "Unable to connect to the bridge", "discover_timeout": "Unable to discover Hue bridges", "no_bridges": "No Philips Hue bridges discovered", diff --git a/homeassistant/components/hue/.translations/fr.json b/homeassistant/components/hue/.translations/fr.json index 5414bf01ea7..ddb647c18ed 100644 --- a/homeassistant/components/hue/.translations/fr.json +++ b/homeassistant/components/hue/.translations/fr.json @@ -3,6 +3,7 @@ "abort": { "all_configured": "Tous les ponts Philips Hue sont d\u00e9j\u00e0 configur\u00e9s", "already_configured": "Ce pont est d\u00e9j\u00e0 configur\u00e9", + "already_in_progress": "Le flux de configuration pour le pont est d\u00e9j\u00e0 en cours.", "cannot_connect": "Connexion au pont impossible", "discover_timeout": "D\u00e9tection de ponts Philips Hue impossible", "no_bridges": "Aucun pont Philips Hue n'a \u00e9t\u00e9 d\u00e9couvert", diff --git a/homeassistant/components/hue/.translations/ru.json b/homeassistant/components/hue/.translations/ru.json index ce71fb670be..713e86f49b7 100644 --- a/homeassistant/components/hue/.translations/ru.json +++ b/homeassistant/components/hue/.translations/ru.json @@ -3,6 +3,7 @@ "abort": { "all_configured": "\u0412\u0441\u0435 Philips Hue \u0448\u043b\u044e\u0437\u044b \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u044b", "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430", + "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0448\u043b\u044e\u0437\u0430 \u0443\u0436\u0435 \u043d\u0430\u0447\u0430\u0442\u0430.", "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0448\u043b\u044e\u0437\u0443", "discover_timeout": "\u0428\u043b\u044e\u0437 Philips Hue \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d", "no_bridges": "\u0428\u043b\u044e\u0437\u044b Philips Hue \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b", diff --git a/homeassistant/components/hue/.translations/sl.json b/homeassistant/components/hue/.translations/sl.json index 7ad7a2e6ade..fc3142ba820 100644 --- a/homeassistant/components/hue/.translations/sl.json +++ b/homeassistant/components/hue/.translations/sl.json @@ -3,6 +3,7 @@ "abort": { "all_configured": "Vsi mostovi Philips Hue so \u017ee konfigurirani", "already_configured": "Most je \u017ee konfiguriran", + "already_in_progress": "Konfiguracijski tok za most je \u017ee v teku.", "cannot_connect": "Ni mogo\u010de vzpostaviti povezave z mostom", "discover_timeout": "Ni bilo mogo\u010de odkriti Hue mostov", "no_bridges": "Ni odkritih mostov Philips Hue", diff --git a/homeassistant/components/hue/.translations/sv.json b/homeassistant/components/hue/.translations/sv.json index a7ffc7bacb2..b0b8ea3cbfa 100644 --- a/homeassistant/components/hue/.translations/sv.json +++ b/homeassistant/components/hue/.translations/sv.json @@ -3,6 +3,7 @@ "abort": { "all_configured": "Alla Philips Hue-bryggor \u00e4r redan konfigurerade", "already_configured": "Bryggan \u00e4r redan konfigurerad", + "already_in_progress": "Konfigurations fl\u00f6det f\u00f6r bryggan p\u00e5g\u00e5r redan.", "cannot_connect": "Det gick inte att ansluta till bryggan", "discover_timeout": "Det gick inte att uppt\u00e4cka n\u00e5gra Hue-bryggor", "no_bridges": "Inga Philips Hue-bryggor uppt\u00e4cktes", diff --git a/homeassistant/components/hue/.translations/zh-Hant.json b/homeassistant/components/hue/.translations/zh-Hant.json index eae4c09da49..a585cfd38c3 100644 --- a/homeassistant/components/hue/.translations/zh-Hant.json +++ b/homeassistant/components/hue/.translations/zh-Hant.json @@ -3,6 +3,7 @@ "abort": { "all_configured": "\u6240\u6709 Philips Hue Bridge \u7686\u5df2\u8a2d\u5b9a\u5b8c\u6210", "already_configured": "Bridge \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_in_progress": "Bridge \u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d\u3002", "cannot_connect": "\u7121\u6cd5\u9023\u7dda\u81f3 Bridge", "discover_timeout": "\u7121\u6cd5\u641c\u5c0b\u5230 Hue Bridge", "no_bridges": "\u672a\u641c\u5c0b\u5230 Philips Hue Bridge", diff --git a/homeassistant/components/ipma/.translations/fr.json b/homeassistant/components/ipma/.translations/fr.json index 1ca5353ec7e..64d03c6ae71 100644 --- a/homeassistant/components/ipma/.translations/fr.json +++ b/homeassistant/components/ipma/.translations/fr.json @@ -10,6 +10,7 @@ "longitude": "Longitude", "name": "Nom" }, + "description": "Instituto Portugu\u00eas do Mar e Atmosfera", "title": "Emplacement" } }, diff --git a/homeassistant/components/iqvia/.translations/sl.json b/homeassistant/components/iqvia/.translations/sl.json new file mode 100644 index 00000000000..fa04c00c7a2 --- /dev/null +++ b/homeassistant/components/iqvia/.translations/sl.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "identifier_exists": "Po\u0161tna \u0161tevilka je \u017ee registrirana", + "invalid_zip_code": "Po\u0161tna \u0161tevilka ni veljavna" + }, + "step": { + "user": { + "data": { + "zip_code": "Po\u0161tna \u0161tevilka" + }, + "description": "Izpolnite svojo ameri\u0161ko ali kanadsko po\u0161tno \u0161tevilko.", + "title": "IQVIA" + } + }, + "title": "IQVIA" + } +} \ No newline at end of file diff --git a/homeassistant/components/logi_circle/.translations/fr.json b/homeassistant/components/logi_circle/.translations/fr.json index 85e8edc6bb1..7f8a2f2a098 100644 --- a/homeassistant/components/logi_circle/.translations/fr.json +++ b/homeassistant/components/logi_circle/.translations/fr.json @@ -13,6 +13,20 @@ "auth_error": "L'autorisation de l'API a \u00e9chou\u00e9.", "auth_timeout": "L'autorisation a expir\u00e9 lors de la demande du jeton d'acc\u00e8s.", "follow_link": "Veuillez suivre le lien et vous authentifier avant d'appuyer sur Soumettre." - } + }, + "step": { + "auth": { + "description": "Suivez le lien ci-dessous et acceptez acc\u00e8s \u00e0 votre compte Logi Circle, puis revenez et appuyez sur Envoyer ci-dessous. \n\n [Lien] ( {authorization_url} )", + "title": "Authentifier avec Logi Circle" + }, + "user": { + "data": { + "flow_impl": "Fournisseur" + }, + "description": "Choisissez via quel fournisseur d'authentification vous souhaitez vous authentifier avec Logi Circle.", + "title": "Fournisseur d'authentification" + } + }, + "title": "Logi Circle" } } \ No newline at end of file diff --git a/homeassistant/components/onboarding/.translations/sl.json b/homeassistant/components/onboarding/.translations/sl.json new file mode 100644 index 00000000000..c340a26a5c8 --- /dev/null +++ b/homeassistant/components/onboarding/.translations/sl.json @@ -0,0 +1,7 @@ +{ + "area": { + "bedroom": "Spalnica", + "kitchen": "Kuhinja", + "living_room": "Dnevna soba" + } +} \ No newline at end of file diff --git a/homeassistant/components/ps4/.translations/fr.json b/homeassistant/components/ps4/.translations/fr.json index cfd65c910d9..03baf0c032e 100644 --- a/homeassistant/components/ps4/.translations/fr.json +++ b/homeassistant/components/ps4/.translations/fr.json @@ -8,6 +8,7 @@ "port_997_bind_error": "Impossible de se connecter au port 997." }, "error": { + "credential_timeout": "Le service d'informations d'identification a expir\u00e9. Appuyez sur soumettre pour red\u00e9marrer.", "login_failed": "\u00c9chec de l'association \u00e0 la PlayStation 4. V\u00e9rifiez que le code PIN est correct.", "no_ipaddress": "Entrez l'adresse IP de la PlayStation 4 que vous souhaitez configurer.", "not_ready": "PlayStation 4 n'est pas allum\u00e9e ou connect\u00e9e au r\u00e9seau." diff --git a/homeassistant/components/ps4/.translations/sl.json b/homeassistant/components/ps4/.translations/sl.json index 429a409fb7e..f51bc45e0e8 100644 --- a/homeassistant/components/ps4/.translations/sl.json +++ b/homeassistant/components/ps4/.translations/sl.json @@ -8,6 +8,7 @@ "port_997_bind_error": "Ne morem se povezati z vrati 997. Dodatne informacije najdete v [dokumentaciji] (https://www.home-assistant.io/components/ps4/)." }, "error": { + "credential_timeout": "Storitev poverilnic je potekla. Pritisnite Po\u0161lji za ponovni zagon.", "login_failed": "Neuspelo seznanjanje s PlayStation 4. Preverite, ali je koda PIN pravilna.", "no_ipaddress": "Vnesite IP naslov PlayStation-a 4, ki ga \u017eelite konfigurirati.", "not_ready": "PlayStation 4 ni vklopljen ali povezan z omre\u017ejem." From bebfc3d16ec248b65bd7b5cce7d99b7f937efb02 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Wed, 29 May 2019 20:12:44 +0200 Subject: [PATCH 167/232] Remove unused Sonos turn on/off methods (#24174) --- homeassistant/components/sonos/media_player.py | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 2a96308c587..f165f345586 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -19,7 +19,7 @@ from homeassistant.components.media_player.const import ( SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, SUPPORT_SELECT_SOURCE, SUPPORT_SHUFFLE_SET, SUPPORT_STOP, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET) from homeassistant.const import ( - ENTITY_MATCH_ALL, STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING) + ENTITY_MATCH_ALL, STATE_IDLE, STATE_PAUSED, STATE_PLAYING) from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.util.dt import utcnow @@ -307,8 +307,6 @@ class SonosEntity(MediaPlayerDevice): return STATE_PAUSED if self._status in ('PLAYING', 'TRANSITIONING'): return STATE_PLAYING - if self._status == 'OFF': - return STATE_OFF return STATE_IDLE @property @@ -403,7 +401,7 @@ class SonosEntity(MediaPlayerDevice): self._player_volume = None self._player_muted = None - self._status = 'OFF' + self._status = 'IDLE' self._coordinator = None self._media_duration = None self._media_position = None @@ -802,16 +800,6 @@ class SonosEntity(MediaPlayerDevice): return sources - @soco_error() - def turn_on(self): - """Turn the media player on.""" - self.media_play() - - @soco_error() - def turn_off(self): - """Turn off media player.""" - self.media_stop() - @soco_error(UPNP_ERRORS_TO_IGNORE) @soco_coordinator def media_play(self): From 4b256f346650e88ad888a29ca4e0c5ef276dda4f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 29 May 2019 11:13:29 -0700 Subject: [PATCH 168/232] Reinstate passing loop to DSMR (#24127) * Reinstate passing loop * Also pass loop into other part --- homeassistant/components/dsmr/sensor.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index e19d910ad83..15b2b7fd0de 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -183,11 +183,12 @@ async def async_setup_platform(hass, config, async_add_entities, if CONF_HOST in config: reader_factory = partial( create_tcp_dsmr_reader, config[CONF_HOST], config[CONF_PORT], - config[CONF_DSMR_VERSION], update_entities_telegram) + config[CONF_DSMR_VERSION], update_entities_telegram, + loop=hass.loop) else: reader_factory = partial( create_dsmr_reader, config[CONF_PORT], config[CONF_DSMR_VERSION], - update_entities_telegram) + update_entities_telegram, loop=hass.loop) async def connect_and_reconnect(): """Connect to DSMR and keep reconnecting until Home Assistant stops.""" From 3fef9a93cfb1cc776da6af1a037317867463adf6 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 29 May 2019 20:19:50 +0200 Subject: [PATCH 169/232] =?UTF-8?q?Tr=C3=A5dfri=20component=20to=20use=20n?= =?UTF-8?q?ew=20zeroconf=20discovery=20(#24041)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Move tradfri component to use new zeroconf discovery * Will this work? * Remove prints * Correct order in generated zeroconf * Update test_init.py * Update test_init.py * Update test_init.py * Update test_init.py --- homeassistant/components/discovery/__init__.py | 2 -- homeassistant/components/tradfri/config_flow.py | 4 ++-- homeassistant/components/tradfri/manifest.json | 1 + homeassistant/generated/zeroconf.py | 3 +++ script/hassfest/zeroconf.py | 3 ++- tests/components/tradfri/test_config_flow.py | 4 ++-- tests/components/zeroconf/test_init.py | 5 +++-- 7 files changed, 13 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/discovery/__init__.py b/homeassistant/components/discovery/__init__.py index 130898018d5..466d9d40d07 100644 --- a/homeassistant/components/discovery/__init__.py +++ b/homeassistant/components/discovery/__init__.py @@ -34,7 +34,6 @@ SERVICE_HASSIO = 'hassio' SERVICE_HOMEKIT = 'homekit' SERVICE_HEOS = 'heos' SERVICE_IGD = 'igd' -SERVICE_IKEA_TRADFRI = 'ikea_tradfri' SERVICE_KONNECTED = 'konnected' SERVICE_MOBILE_APP = 'hass_mobile_app' SERVICE_NETGEAR = 'netgear_router' @@ -54,7 +53,6 @@ CONFIG_ENTRY_HANDLERS = { 'google_cast': 'cast', SERVICE_HEOS: 'heos', SERVICE_TELLDUSLIVE: 'tellduslive', - SERVICE_IKEA_TRADFRI: 'tradfri', 'sonos': 'sonos', SERVICE_IGD: 'upnp', SERVICE_HOMEKIT: 'homekit_controller', diff --git a/homeassistant/components/tradfri/config_flow.py b/homeassistant/components/tradfri/config_flow.py index bbbe104f296..76f6a8f5764 100644 --- a/homeassistant/components/tradfri/config_flow.py +++ b/homeassistant/components/tradfri/config_flow.py @@ -76,8 +76,8 @@ class FlowHandler(config_entries.ConfigFlow): errors=errors, ) - async def async_step_discovery(self, user_input): - """Handle discovery.""" + async def async_step_zeroconf(self, user_input): + """Handle zeroconf discovery.""" for entry in self._async_current_entries(): if entry.data[CONF_HOST] == user_input['host']: return self.async_abort( diff --git a/homeassistant/components/tradfri/manifest.json b/homeassistant/components/tradfri/manifest.json index c9a4fca3dc9..aba3805a4aa 100644 --- a/homeassistant/components/tradfri/manifest.json +++ b/homeassistant/components/tradfri/manifest.json @@ -7,6 +7,7 @@ "pytradfri[async]==6.0.1" ], "dependencies": [], + "zeroconf": ["_coap._udp.local."], "codeowners": [ "@ggravlingen" ] diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index f009132228c..434a463dc17 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -8,6 +8,9 @@ SERVICE_TYPES = { "_axis-video._tcp.local.": [ "axis" ], + "_coap._udp.local.": [ + "tradfri" + ], "_esphomelib._tcp.local.": [ "esphome" ] diff --git a/script/hassfest/zeroconf.py b/script/hassfest/zeroconf.py index 9cec4a7211b..390429c3da7 100644 --- a/script/hassfest/zeroconf.py +++ b/script/hassfest/zeroconf.py @@ -63,7 +63,8 @@ def validate(integrations: Dict[str, Integration], config: Config): config.cache['zeroconf'] = content = generate_and_validate(integrations) with open(str(zeroconf_path), 'r') as fp: - if fp.read().strip() != content: + current = fp.read().strip() + if current != content: config.add_error( "zeroconf", "File zeroconf.py is not up to date. " diff --git a/tests/components/tradfri/test_config_flow.py b/tests/components/tradfri/test_config_flow.py index 6756a01bbc7..8fcc72dd4a5 100644 --- a/tests/components/tradfri/test_config_flow.py +++ b/tests/components/tradfri/test_config_flow.py @@ -99,7 +99,7 @@ async def test_discovery_connection(hass, mock_auth, mock_entry_setup): }) flow = await hass.config_entries.flow.async_init( - 'tradfri', context={'source': 'discovery'}, data={ + 'tradfri', context={'source': 'zeroconf'}, data={ 'host': '123.123.123.123' }) @@ -249,7 +249,7 @@ async def test_discovery_duplicate_aborted(hass): ).add_to_hass(hass) flow = await hass.config_entries.flow.async_init( - 'tradfri', context={'source': 'discovery'}, data={ + 'tradfri', context={'source': 'zeroconf'}, data={ 'host': 'some-host' }) diff --git a/tests/components/zeroconf/test_init.py b/tests/components/zeroconf/test_init.py index 0596d5e0ed5..106cf3377f4 100644 --- a/tests/components/zeroconf/test_init.py +++ b/tests/components/zeroconf/test_init.py @@ -3,6 +3,7 @@ from unittest.mock import patch from aiozeroconf import ServiceInfo, ServiceStateChange +from homeassistant.generated import zeroconf as zc_gen from homeassistant.setup import async_setup_component from homeassistant.components import zeroconf @@ -36,5 +37,5 @@ async def test_setup(hass): hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) await hass.async_block_till_done() - assert len(MockServiceBrowser.mock_calls) == 2 - assert len(mock_config_flow.mock_calls) == 2 + assert len(MockServiceBrowser.mock_calls) == len(zc_gen.SERVICE_TYPES) + assert len(mock_config_flow.mock_calls) == len(zc_gen.SERVICE_TYPES) From 42ee8eef5051771a82bc8c613664f8f8c91342ca Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 29 May 2019 20:20:04 +0200 Subject: [PATCH 170/232] Move Homekit controller component to user zeroconf discovery (#24042) --- .../components/discovery/__init__.py | 2 -- .../homekit_controller/config_flow.py | 4 +-- .../homekit_controller/manifest.json | 1 + homeassistant/generated/zeroconf.py | 3 ++ tests/components/homekit_controller/common.py | 2 +- .../homekit_controller/test_config_flow.py | 32 +++++++++---------- 6 files changed, 23 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/discovery/__init__.py b/homeassistant/components/discovery/__init__.py index 466d9d40d07..54f7e1bf3de 100644 --- a/homeassistant/components/discovery/__init__.py +++ b/homeassistant/components/discovery/__init__.py @@ -31,7 +31,6 @@ SERVICE_ENIGMA2 = 'enigma2' SERVICE_FREEBOX = 'freebox' SERVICE_HASS_IOS_APP = 'hass_ios' SERVICE_HASSIO = 'hassio' -SERVICE_HOMEKIT = 'homekit' SERVICE_HEOS = 'heos' SERVICE_IGD = 'igd' SERVICE_KONNECTED = 'konnected' @@ -55,7 +54,6 @@ CONFIG_ENTRY_HANDLERS = { SERVICE_TELLDUSLIVE: 'tellduslive', 'sonos': 'sonos', SERVICE_IGD: 'upnp', - SERVICE_HOMEKIT: 'homekit_controller', } SERVICE_HANDLERS = { diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index deefb596310..48d427b8321 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -109,7 +109,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow): }) ) - async def async_step_discovery(self, discovery_info): + async def async_step_zeroconf(self, discovery_info): """Handle a discovered HomeKit accessory. This flow is triggered by the discovery component. @@ -132,7 +132,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow): # pylint: disable=unsupported-assignment-operation self.context['title_placeholders'] = { - 'name': discovery_info['name'], + 'name': discovery_info['name'].replace('._hap._tcp.local.', ''), } # The configuration number increases every time the characteristic map diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 8b0dfd199bb..62dbf3740a3 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -7,6 +7,7 @@ "homekit[IP]==0.14.0" ], "dependencies": [], + "zeroconf": ["_hap._tcp.local."], "codeowners": [ "@Jc2k" ] diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index 434a463dc17..acd912fe28a 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -13,5 +13,8 @@ SERVICE_TYPES = { ], "_esphomelib._tcp.local.": [ "esphome" + ], + "_hap._tcp.local.": [ + "homekit_controller" ] } diff --git a/tests/components/homekit_controller/common.py b/tests/components/homekit_controller/common.py index 87482f8e92c..34b6474c6e9 100644 --- a/tests/components/homekit_controller/common.py +++ b/tests/components/homekit_controller/common.py @@ -277,7 +277,7 @@ async def device_config_changed(hass, accessories): flow = config_flow.HomekitControllerFlowHandler() flow.hass = hass flow.context = {} - result = await flow.async_step_discovery(discovery_info) + result = await flow.async_step_zeroconf(discovery_info) assert result['type'] == 'abort' assert result['reason'] == 'already_configured' diff --git a/tests/components/homekit_controller/test_config_flow.py b/tests/components/homekit_controller/test_config_flow.py index 9c869809544..07e2dd4afb1 100644 --- a/tests/components/homekit_controller/test_config_flow.py +++ b/tests/components/homekit_controller/test_config_flow.py @@ -64,7 +64,7 @@ async def test_discovery_works(hass): flow = _setup_flow_handler(hass) # Device is discovered - result = await flow.async_step_discovery(discovery_info) + result = await flow.async_step_zeroconf(discovery_info) assert result['type'] == 'form' assert result['step_id'] == 'pair' assert flow.context == {'title_placeholders': {'name': 'TestDevice'}} @@ -119,7 +119,7 @@ async def test_discovery_works_upper_case(hass): flow = _setup_flow_handler(hass) # Device is discovered - result = await flow.async_step_discovery(discovery_info) + result = await flow.async_step_zeroconf(discovery_info) assert result['type'] == 'form' assert result['step_id'] == 'pair' assert flow.context == {'title_placeholders': {'name': 'TestDevice'}} @@ -172,7 +172,7 @@ async def test_discovery_works_missing_csharp(hass): flow = _setup_flow_handler(hass) # Device is discovered - result = await flow.async_step_discovery(discovery_info) + result = await flow.async_step_zeroconf(discovery_info) assert result['type'] == 'form' assert result['step_id'] == 'pair' assert flow.context == {'title_placeholders': {'name': 'TestDevice'}} @@ -226,7 +226,7 @@ async def test_pair_already_paired_1(hass): flow = _setup_flow_handler(hass) - result = await flow.async_step_discovery(discovery_info) + result = await flow.async_step_zeroconf(discovery_info) assert result['type'] == 'abort' assert result['reason'] == 'already_paired' assert flow.context == {'title_placeholders': {'name': 'TestDevice'}} @@ -248,7 +248,7 @@ async def test_discovery_ignored_model(hass): flow = _setup_flow_handler(hass) - result = await flow.async_step_discovery(discovery_info) + result = await flow.async_step_zeroconf(discovery_info) assert result['type'] == 'abort' assert result['reason'] == 'ignored_model' assert flow.context == {'title_placeholders': {'name': 'TestDevice'}} @@ -277,7 +277,7 @@ async def test_discovery_invalid_config_entry(hass): flow = _setup_flow_handler(hass) - result = await flow.async_step_discovery(discovery_info) + result = await flow.async_step_zeroconf(discovery_info) assert result['type'] == 'form' assert result['step_id'] == 'pair' assert flow.context == {'title_placeholders': {'name': 'TestDevice'}} @@ -311,7 +311,7 @@ async def test_discovery_already_configured(hass): flow = _setup_flow_handler(hass) - result = await flow.async_step_discovery(discovery_info) + result = await flow.async_step_zeroconf(discovery_info) assert result['type'] == 'abort' assert result['reason'] == 'already_configured' assert flow.context == {'title_placeholders': {'name': 'TestDevice'}} @@ -341,7 +341,7 @@ async def test_discovery_already_configured_config_change(hass): flow = _setup_flow_handler(hass) - result = await flow.async_step_discovery(discovery_info) + result = await flow.async_step_zeroconf(discovery_info) assert result['type'] == 'abort' assert result['reason'] == 'already_configured' assert flow.context == {'title_placeholders': {'name': 'TestDevice'}} @@ -366,7 +366,7 @@ async def test_pair_unable_to_pair(hass): flow = _setup_flow_handler(hass) # Device is discovered - result = await flow.async_step_discovery(discovery_info) + result = await flow.async_step_zeroconf(discovery_info) assert result['type'] == 'form' assert result['step_id'] == 'pair' assert flow.context == {'title_placeholders': {'name': 'TestDevice'}} @@ -403,7 +403,7 @@ async def test_pair_abort_errors_on_start(hass, exception, expected): flow = _setup_flow_handler(hass) # Device is discovered - result = await flow.async_step_discovery(discovery_info) + result = await flow.async_step_zeroconf(discovery_info) assert result['type'] == 'form' assert result['step_id'] == 'pair' assert flow.context == {'title_placeholders': {'name': 'TestDevice'}} @@ -436,7 +436,7 @@ async def test_pair_form_errors_on_start(hass, exception, expected): flow = _setup_flow_handler(hass) # Device is discovered - result = await flow.async_step_discovery(discovery_info) + result = await flow.async_step_zeroconf(discovery_info) assert result['type'] == 'form' assert result['step_id'] == 'pair' assert flow.context == {'title_placeholders': {'name': 'TestDevice'}} @@ -469,7 +469,7 @@ async def test_pair_abort_errors_on_finish(hass, exception, expected): flow = _setup_flow_handler(hass) # Device is discovered - result = await flow.async_step_discovery(discovery_info) + result = await flow.async_step_zeroconf(discovery_info) assert result['type'] == 'form' assert result['step_id'] == 'pair' assert flow.context == {'title_placeholders': {'name': 'TestDevice'}} @@ -508,7 +508,7 @@ async def test_pair_form_errors_on_finish(hass, exception, expected): flow = _setup_flow_handler(hass) # Device is discovered - result = await flow.async_step_discovery(discovery_info) + result = await flow.async_step_zeroconf(discovery_info) assert result['type'] == 'form' assert result['step_id'] == 'pair' assert flow.context == {'title_placeholders': {'name': 'TestDevice'}} @@ -738,7 +738,7 @@ async def test_parse_new_homekit_json(hass): pairing_cls.return_value = pairing with mock.patch('builtins.open', mock_open): with mock.patch('os.path', mock_path): - result = await flow.async_step_discovery(discovery_info) + result = await flow.async_step_zeroconf(discovery_info) assert result['type'] == 'create_entry' assert result['title'] == 'TestDevice' @@ -796,7 +796,7 @@ async def test_parse_old_homekit_json(hass): with mock.patch('builtins.open', mock_open): with mock.patch('os.path', mock_path): with mock.patch('os.listdir', mock_listdir): - result = await flow.async_step_discovery(discovery_info) + result = await flow.async_step_zeroconf(discovery_info) assert result['type'] == 'create_entry' assert result['title'] == 'TestDevice' @@ -865,7 +865,7 @@ async def test_parse_overlapping_homekit_json(hass): with mock.patch('builtins.open', side_effect=side_effects): with mock.patch('os.path', mock_path): with mock.patch('os.listdir', mock_listdir): - result = await flow.async_step_discovery(discovery_info) + result = await flow.async_step_zeroconf(discovery_info) await hass.async_block_till_done() From 84baaa324c529b1e5b7daeda2fc33994a8db95b0 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 29 May 2019 23:20:06 +0200 Subject: [PATCH 171/232] Revert Zeroconf back to previously used library (#24139) * Revert back to previously used library * Fix test * Remove unused import * Fix import * Update __init__.py * Update __init__.py * Fix test after rebase --- homeassistant/components/zeroconf/__init__.py | 48 +++++++++---------- .../components/zeroconf/manifest.json | 2 +- homeassistant/generated/zeroconf.py | 2 +- requirements_all.txt | 6 +-- requirements_test_all.txt | 6 +-- script/gen_requirements_all.py | 2 +- script/hassfest/zeroconf.py | 2 +- tests/components/default_config/test_init.py | 6 +-- tests/components/zeroconf/test_init.py | 11 ++--- 9 files changed, 42 insertions(+), 43 deletions(-) diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index a0185a3bbe2..fe757b8ae85 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -1,14 +1,16 @@ """Support for exposing Home Assistant via Zeroconf.""" +# PyLint bug confuses absolute/relative imports +# https://github.com/PyCQA/pylint/issues/1931 +# pylint: disable=no-name-in-module import logging import ipaddress import voluptuous as vol -from aiozeroconf import ( - ServiceBrowser, ServiceInfo, ServiceStateChange, Zeroconf) +from zeroconf import ServiceBrowser, ServiceInfo, ServiceStateChange, Zeroconf from homeassistant.const import (EVENT_HOMEASSISTANT_STOP, __version__) -from homeassistant.generated import zeroconf as zeroconf_manifest +from homeassistant.generated.zeroconf import ZEROCONF _LOGGER = logging.getLogger(__name__) @@ -28,7 +30,7 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -async def async_setup(hass, config): +def setup(hass, config): """Set up Zeroconf and make Home Assistant discoverable.""" zeroconf_name = '{}.{}'.format(hass.config.location_name, ZEROCONF_TYPE) @@ -42,35 +44,33 @@ async def async_setup(hass, config): info = ServiceInfo(ZEROCONF_TYPE, zeroconf_name, port=hass.http.server_port, properties=params) - zeroconf = Zeroconf(hass.loop) + zeroconf = Zeroconf() - await zeroconf.register_service(info) + zeroconf.register_service(info) - async def new_service(service_type, name): - """Signal new service discovered.""" - service_info = await zeroconf.get_service_info(service_type, name) - info = info_from_service(service_info) - _LOGGER.debug("Discovered new device %s %s", name, info) - - for domain in zeroconf_manifest.SERVICE_TYPES[service_type]: - await hass.config_entries.flow.async_init( - domain, context={'source': DOMAIN}, data=info - ) - - def service_update(_, service_type, name, state_change): + def service_update(zeroconf, service_type, name, state_change): """Service state changed.""" if state_change is ServiceStateChange.Added: - hass.async_create_task(new_service(service_type, name)) + service_info = zeroconf.get_service_info(service_type, name) + info = info_from_service(service_info) + _LOGGER.debug("Discovered new device %s %s", name, info) - for service in zeroconf_manifest.SERVICE_TYPES: + for domain in ZEROCONF[service_type]: + hass.add_job( + hass.config_entries.flow.async_init( + domain, context={'source': DOMAIN}, data=info + ) + ) + + for service in ZEROCONF: ServiceBrowser(zeroconf, service, handlers=[service_update]) - async def stop_zeroconf(_): + def stop_zeroconf(_): """Stop Zeroconf.""" - await zeroconf.unregister_service(info) - await zeroconf.close() + zeroconf.unregister_service(info) + zeroconf.close() - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_zeroconf) + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_zeroconf) return True diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index 07e620381e4..becd5d51c5a 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -3,7 +3,7 @@ "name": "Zeroconf", "documentation": "https://www.home-assistant.io/components/zeroconf", "requirements": [ - "aiozeroconf==0.1.8" + "zeroconf==0.22.0" ], "dependencies": [ "api" diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index acd912fe28a..05b0a0247b9 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -4,7 +4,7 @@ To update, run python3 -m hassfest """ -SERVICE_TYPES = { +ZEROCONF = { "_axis-video._tcp.local.": [ "axis" ], diff --git a/requirements_all.txt b/requirements_all.txt index 8f79fbcedbf..4f5ada38c44 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -162,9 +162,6 @@ aioswitcher==2019.3.21 # homeassistant.components.unifi aiounifi==4 -# homeassistant.components.zeroconf -aiozeroconf==0.1.8 - # homeassistant.components.aladdin_connect aladdin_connect==0.3 @@ -1877,6 +1874,9 @@ youtube_dl==2019.05.11 # homeassistant.components.zengge zengge==0.2 +# homeassistant.components.zeroconf +zeroconf==0.22.0 + # homeassistant.components.zha zha-quirks==0.0.13 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 728a2f3dfa3..b6b2616964c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -60,9 +60,6 @@ aioswitcher==2019.3.21 # homeassistant.components.unifi aiounifi==4 -# homeassistant.components.zeroconf -aiozeroconf==0.1.8 - # homeassistant.components.ambiclimate ambiclimate==0.1.2 @@ -354,5 +351,8 @@ vultr==0.1.2 # homeassistant.components.wake_on_lan wakeonlan==1.1.6 +# homeassistant.components.zeroconf +zeroconf==0.22.0 + # homeassistant.components.zha zigpy-homeassistant==0.3.3 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index dad20279d5b..f85758e464f 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -51,7 +51,6 @@ TEST_REQUIREMENTS = ( 'aiohue', 'aiounifi', 'aioswitcher', - 'aiozeroconf', 'apns2', 'av', 'axis', @@ -150,6 +149,7 @@ TEST_REQUIREMENTS = ( 'vultr', 'YesssSMS', 'ruamel.yaml', + 'zeroconf', 'zigpy-homeassistant', 'bellows-homeassistant', ) diff --git a/script/hassfest/zeroconf.py b/script/hassfest/zeroconf.py index 390429c3da7..1ed9575c95f 100644 --- a/script/hassfest/zeroconf.py +++ b/script/hassfest/zeroconf.py @@ -12,7 +12,7 @@ To update, run python3 -m hassfest \"\"\" -SERVICE_TYPES = {} +ZEROCONF = {} """.strip() diff --git a/tests/components/default_config/test_init.py b/tests/components/default_config/test_init.py index 8e2766a857b..5aacf06aa66 100644 --- a/tests/components/default_config/test_init.py +++ b/tests/components/default_config/test_init.py @@ -9,9 +9,9 @@ from tests.common import MockDependency, mock_coro @pytest.fixture(autouse=True) -def aiozeroconf_mock(): - """Mock aiozeroconf.""" - with MockDependency('aiozeroconf') as mocked_zeroconf: +def zeroconf_mock(): + """Mock zeroconf.""" + with MockDependency('zeroconf') as mocked_zeroconf: mocked_zeroconf.Zeroconf.return_value.register_service \ .return_value = mock_coro(True) yield diff --git a/tests/components/zeroconf/test_init.py b/tests/components/zeroconf/test_init.py index 106cf3377f4..e7d7756fe7c 100644 --- a/tests/components/zeroconf/test_init.py +++ b/tests/components/zeroconf/test_init.py @@ -1,7 +1,7 @@ """Test Zeroconf component setup process.""" from unittest.mock import patch -from aiozeroconf import ServiceInfo, ServiceStateChange +from zeroconf import ServiceInfo, ServiceStateChange from homeassistant.generated import zeroconf as zc_gen from homeassistant.setup import async_setup_component @@ -11,11 +11,11 @@ from homeassistant.components import zeroconf def service_update_mock(zeroconf, service, handlers): """Call service update handler.""" handlers[0]( - None, service, '{}.{}'.format('name', service), + zeroconf, service, '{}.{}'.format('name', service), ServiceStateChange.Added) -async def get_service_info_mock(service_type, name): +def get_service_info_mock(service_type, name): """Return service info for get_service_info.""" return ServiceInfo( service_type, name, address=b'\n\x00\x00\x14', port=80, weight=0, @@ -35,7 +35,6 @@ async def test_setup(hass): assert await async_setup_component( hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) - await hass.async_block_till_done() - assert len(MockServiceBrowser.mock_calls) == len(zc_gen.SERVICE_TYPES) - assert len(mock_config_flow.mock_calls) == len(zc_gen.SERVICE_TYPES) + assert len(MockServiceBrowser.mock_calls) == len(zc_gen.ZEROCONF) + assert len(mock_config_flow.mock_calls) == len(zc_gen.ZEROCONF) * 2 From e3307213b179252bf8772edc47a83f1cb1ab6881 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 29 May 2019 14:30:00 -0700 Subject: [PATCH 172/232] Deprecate Python 3.5.3 (#24177) --- homeassistant/bootstrap.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index d63caf9e76f..96417c54b12 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -94,6 +94,13 @@ async def async_from_config_dict(config: Dict[str, Any], stop = time() _LOGGER.info("Home Assistant initialized in %.2fs", stop-start) + if sys.version_info[:3] < (3, 6, 0): + hass.components.persistent_notification.async_create( + "Python 3.5 support is deprecated and will " + "be removed in the first release after August 1. Please " + "upgrade Python.", "Python version", "python_version" + ) + # TEMP: warn users for invalid slugs # Remove after 0.94 or 1.0 if cv.INVALID_SLUGS_FOUND or cv.INVALID_ENTITY_IDS_FOUND: From e2e001d04246d50cf0b872c032abbdb8b7fffd56 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 29 May 2019 23:34:44 +0200 Subject: [PATCH 173/232] Keep integrations in discovery (#24179) * Keep integrations that have been migrated to new discovery methods to avoid breaking changes * Additional migrated services --- homeassistant/components/discovery/__init__.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/discovery/__init__.py b/homeassistant/components/discovery/__init__.py index 54f7e1bf3de..ee6a8590515 100644 --- a/homeassistant/components/discovery/__init__.py +++ b/homeassistant/components/discovery/__init__.py @@ -96,8 +96,18 @@ OPTIONAL_SERVICE_HANDLERS = { SERVICE_DLNA_DMR: ('media_player', 'dlna_dmr'), } -DEFAULT_ENABLED = list(CONFIG_ENTRY_HANDLERS) + list(SERVICE_HANDLERS) -DEFAULT_DISABLED = list(OPTIONAL_SERVICE_HANDLERS) +MIGRATED_SERVICE_HANDLERS = { + 'axis': None, + 'esphome': None, + 'ikea_tradfri': None, + 'homekit': None, + 'philips_hue': None +} + +DEFAULT_ENABLED = list(CONFIG_ENTRY_HANDLERS) + list(SERVICE_HANDLERS) + \ + list(MIGRATED_SERVICE_HANDLERS) +DEFAULT_DISABLED = list(OPTIONAL_SERVICE_HANDLERS) + \ + list(MIGRATED_SERVICE_HANDLERS) CONF_IGNORE = 'ignore' CONF_ENABLE = 'enable' @@ -144,6 +154,9 @@ async def async_setup(hass, config): async def new_service_found(service, info): """Handle a new service if one is found.""" + if service in MIGRATED_SERVICE_HANDLERS: + return + if service in ignored_platforms: logger.info("Ignoring service: %s %s", service, info) return From f8572c1d71167cd85d431c4bd1ae576b60a9051b Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Thu, 30 May 2019 00:05:12 +0200 Subject: [PATCH 174/232] Avoid slow updates with unavailable Sonos devices (#24180) --- .../components/sonos/media_player.py | 57 ++++++++++--------- 1 file changed, 29 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index f165f345586..e8004ec8428 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -118,6 +118,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): _discovered_player, interface_addr=config.get(CONF_INTERFACE_ADDR)) + for entity in hass.data[DATA_SONOS].entities: + entity.check_unseen() + hass.helpers.event.call_later(DISCOVERY_INTERVAL, _discovery) hass.async_add_executor_job(_discovery) @@ -328,15 +331,36 @@ class SonosEntity(MediaPlayerDevice): """Record that this player was seen right now.""" self._seen = time.monotonic() + if self._available: + return + + self._available = True + self._set_basic_information() + self._subscribe_to_player_events() + self.schedule_update_ha_state() + + def check_unseen(self): + """Make this player unavailable if it was not seen recently.""" + if not self._available: + return + + if self._seen < time.monotonic() - 2*DISCOVERY_INTERVAL: + self._available = False + + def _unsub(subscriptions): + for subscription in subscriptions: + subscription.unsubscribe() + self.hass.add_job(_unsub, self._subscriptions) + + self._subscriptions = [] + + self.schedule_update_ha_state() + @property def available(self) -> bool: """Return True if entity is available.""" return self._available - def _check_available(self): - """Check that we saw the player recently.""" - return self._seen > time.monotonic() - 2*DISCOVERY_INTERVAL - def _set_basic_information(self): """Set initial entity information.""" speaker_info = self.soco.get_speaker_info(True) @@ -388,30 +412,7 @@ class SonosEntity(MediaPlayerDevice): def update(self): """Retrieve latest state.""" - available = self._check_available() - if self._available != available: - self._available = available - if available: - self._set_basic_information() - self._subscribe_to_player_events() - else: - for subscription in self._subscriptions: - subscription.unsubscribe() - self._subscriptions = [] - - self._player_volume = None - self._player_muted = None - self._status = 'IDLE' - self._coordinator = None - self._media_duration = None - self._media_position = None - self._media_position_updated_at = None - self._media_image_url = None - self._media_artist = None - self._media_album_name = None - self._media_title = None - self._source_name = None - elif available and not self._receives_events: + if self._available and not self._receives_events: try: self.update_groups() self.update_volume() From d9852bc75dd066bc0ede57bf956385a56d76215b Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 30 May 2019 00:30:09 +0200 Subject: [PATCH 175/232] Support Hass.io wheels / docker env (#24175) * Support Hass.io wheels / docker env * address comments * fix lint --- homeassistant/requirements.py | 5 ++- homeassistant/util/package.py | 11 +++++- tests/test_requirements.py | 63 +++++++++++++++++++++++++++++++++-- tests/util/test_package.py | 17 ++++++++++ 4 files changed, 92 insertions(+), 4 deletions(-) diff --git a/homeassistant/requirements.py b/homeassistant/requirements.py index 3a1081e4b87..ca34a4bbae4 100644 --- a/homeassistant/requirements.py +++ b/homeassistant/requirements.py @@ -47,6 +47,9 @@ def pip_kwargs(config_dir: Optional[str]) -> Dict[str, Any]: kwargs = { 'constraints': os.path.join(os.path.dirname(__file__), CONSTRAINT_FILE) } - if not (config_dir is None or pkg_util.is_virtual_env()): + if 'WHEELS_LINKS' in os.environ: + kwargs['find_links'] = os.environ['WHEELS_LINKS'] + if not (config_dir is None or pkg_util.is_virtual_env()) and \ + not pkg_util.is_docker_env(): kwargs['target'] = os.path.join(config_dir, 'deps') return kwargs diff --git a/homeassistant/util/package.py b/homeassistant/util/package.py index 3a34ab0a365..272a097b24c 100644 --- a/homeassistant/util/package.py +++ b/homeassistant/util/package.py @@ -6,6 +6,7 @@ from subprocess import PIPE, Popen import sys from typing import Optional from urllib.parse import urlparse +from pathlib import Path import pkg_resources from importlib_metadata import version, PackageNotFoundError @@ -21,6 +22,11 @@ def is_virtual_env() -> bool: hasattr(sys, 'real_prefix')) +def is_docker_env() -> bool: + """Return True if we run in a docker env.""" + return Path("/.dockerenv").exists() + + def is_installed(package: str) -> bool: """Check if a package is installed and will be loaded when we import it. @@ -42,7 +48,8 @@ def is_installed(package: str) -> bool: def install_package(package: str, upgrade: bool = True, target: Optional[str] = None, - constraints: Optional[str] = None) -> bool: + constraints: Optional[str] = None, + find_links: Optional[str] = None) -> bool: """Install a package on PyPi. Accepts pip compatible package strings. Return boolean if install successful. @@ -55,6 +62,8 @@ def install_package(package: str, upgrade: bool = True, args.append('--upgrade') if constraints is not None: args += ['--constraint', constraints] + if find_links is not None: + args += ['--find-links', find_links] if target: assert not is_virtual_env() # This only works if not running in venv diff --git a/tests/test_requirements.py b/tests/test_requirements.py index c061e37ca0a..35264c2e1b4 100644 --- a/tests/test_requirements.py +++ b/tests/test_requirements.py @@ -27,9 +27,10 @@ class TestRequirements: @patch('os.path.dirname') @patch('homeassistant.util.package.is_virtual_env', return_value=True) + @patch('homeassistant.util.package.is_docker_env', return_value=False) @patch('homeassistant.util.package.install_package', return_value=True) def test_requirement_installed_in_venv( - self, mock_install, mock_venv, mock_dirname): + self, mock_install, mock_venv, mock_denv, mock_dirname): """Test requirement installed in virtual environment.""" mock_venv.return_value = True mock_dirname.return_value = 'ha_package_path' @@ -45,9 +46,10 @@ class TestRequirements: @patch('os.path.dirname') @patch('homeassistant.util.package.is_virtual_env', return_value=False) + @patch('homeassistant.util.package.is_docker_env', return_value=False) @patch('homeassistant.util.package.install_package', return_value=True) def test_requirement_installed_in_deps( - self, mock_install, mock_venv, mock_dirname): + self, mock_install, mock_venv, mock_denv, mock_dirname): """Test requirement installed in deps directory.""" mock_dirname.return_value = 'ha_package_path' self.hass.config.skip_pip = False @@ -77,3 +79,60 @@ async def test_install_existing_package(hass): hass, 'test_component', ['hello==1.0.0']) assert len(mock_inst.mock_calls) == 0 + + +async def test_install_with_wheels_index(hass): + """Test an install attempt with wheels index URL.""" + hass.config.skip_pip = False + mock_integration( + hass, MockModule('comp', requirements=['hello==1.0.0'])) + + with patch( + 'homeassistant.util.package.is_installed', return_value=False + ), \ + patch( + 'homeassistant.util.package.is_docker_env', return_value=True + ), \ + patch( + 'homeassistant.util.package.install_package' + ) as mock_inst, \ + patch.dict( + os.environ, {'WHEELS_LINKS': "https://wheels.hass.io/test"} + ), \ + patch( + 'os.path.dirname' + ) as mock_dir: + mock_dir.return_value = 'ha_package_path' + assert await setup.async_setup_component(hass, 'comp', {}) + assert 'comp' in hass.config.components + print(mock_inst.call_args) + assert mock_inst.call_args == call( + 'hello==1.0.0', find_links="https://wheels.hass.io/test", + constraints=os.path.join('ha_package_path', CONSTRAINT_FILE)) + + +async def test_install_on_docker(hass): + """Test an install attempt on an docker system env.""" + hass.config.skip_pip = False + mock_integration( + hass, MockModule('comp', requirements=['hello==1.0.0'])) + + with patch( + 'homeassistant.util.package.is_installed', return_value=False + ), \ + patch( + 'homeassistant.util.package.is_docker_env', return_value=True + ), \ + patch( + 'homeassistant.util.package.install_package' + ) as mock_inst, \ + patch( + 'os.path.dirname' + ) as mock_dir: + mock_dir.return_value = 'ha_package_path' + assert await setup.async_setup_component(hass, 'comp', {}) + assert 'comp' in hass.config.components + print(mock_inst.call_args) + assert mock_inst.call_args == call( + 'hello==1.0.0', + constraints=os.path.join('ha_package_path', CONSTRAINT_FILE)) diff --git a/tests/util/test_package.py b/tests/util/test_package.py index 41af56265d2..3751c056907 100644 --- a/tests/util/test_package.py +++ b/tests/util/test_package.py @@ -167,6 +167,23 @@ def test_install_constraint(mock_sys, mock_popen, mock_env_copy, mock_venv): assert mock_popen.return_value.communicate.call_count == 1 +def test_install_find_links(mock_sys, mock_popen, mock_env_copy, mock_venv): + """Test install with find-links on not installed package.""" + env = mock_env_copy() + link = 'https://wheels-repository' + assert package.install_package( + TEST_NEW_REQ, False, find_links=link) + assert mock_popen.call_count == 1 + assert ( + mock_popen.call_args == + call([ + mock_sys.executable, '-m', 'pip', 'install', '--quiet', + TEST_NEW_REQ, '--find-links', link + ], stdin=PIPE, stdout=PIPE, stderr=PIPE, env=env) + ) + assert mock_popen.return_value.communicate.call_count == 1 + + @asyncio.coroutine def test_async_get_user_site(mock_env_copy): """Test async get user site directory.""" From 6667138b738f16ea767644f112d80d76d226b187 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 29 May 2019 15:32:36 -0700 Subject: [PATCH 176/232] Remove discovery from initial config (#24183) --- homeassistant/config.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/homeassistant/config.py b/homeassistant/config.py index cffaffd8985..9465025cfd2 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -59,9 +59,6 @@ default_config: # http: # base_url: example.duckdns.org:8123 -# Discover some devices automatically -discovery: - # Sensors sensor: # Weather prediction From 9303a56d8fd7f1385568129ce74d940a8e0c04b0 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Wed, 29 May 2019 23:50:00 +0100 Subject: [PATCH 177/232] Fix duplicated discovered homekit devices (#24178) --- .../homekit_controller/.translations/en.json | 1 + .../homekit_controller/config_flow.py | 7 + .../homekit_controller/strings.json | 3 +- .../homekit_controller/test_config_flow.py | 132 +++++++++++++++--- 4 files changed, 122 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/homekit_controller/.translations/en.json b/homeassistant/components/homekit_controller/.translations/en.json index 059f0f7afe7..31731a52203 100644 --- a/homeassistant/components/homekit_controller/.translations/en.json +++ b/homeassistant/components/homekit_controller/.translations/en.json @@ -3,6 +3,7 @@ "abort": { "accessory_not_found_error": "Cannot add pairing as device can no longer be found.", "already_configured": "Accessory is already configured with this controller.", + "already_in_progress": "Config flow for device is already in progress.", "already_paired": "This accessory is already paired to another device. Please reset the accessory and try again.", "ignored_model": "HomeKit support for this model is blocked as a more feature complete native integration is available.", "invalid_config_entry": "This device is showing as ready to pair but there is already a conflicting config entry for it in Home Assistant that must first be removed.", diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index 48d427b8321..c7100f3159e 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -131,10 +131,17 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow): paired = not status_flags & 0x01 # pylint: disable=unsupported-assignment-operation + self.context['hkid'] = hkid self.context['title_placeholders'] = { 'name': discovery_info['name'].replace('._hap._tcp.local.', ''), } + # If multiple HomekitControllerFlowHandler end up getting created + # for the same accessory dont let duplicates hang around + active_flows = self._async_in_progress() + if any(hkid == flow['context']['hkid'] for flow in active_flows): + return self.async_abort(reason='already_in_progress') + # The configuration number increases every time the characteristic map # needs updating. Some devices use a slightly off-spec name so handle # both cases. diff --git a/homeassistant/components/homekit_controller/strings.json b/homeassistant/components/homekit_controller/strings.json index eceaa624b0f..b51dcb1f6d8 100644 --- a/homeassistant/components/homekit_controller/strings.json +++ b/homeassistant/components/homekit_controller/strings.json @@ -33,7 +33,8 @@ "ignored_model": "HomeKit support for this model is blocked as a more feature complete native integration is available.", "already_configured": "Accessory is already configured with this controller.", "invalid_config_entry": "This device is showing as ready to pair but there is already a conflicting config entry for it in Home Assistant that must first be removed.", - "accessory_not_found_error": "Cannot add pairing as device can no longer be found." + "accessory_not_found_error": "Cannot add pairing as device can no longer be found.", + "already_in_progress": "Config flow for device is already in progress." } } } diff --git a/tests/components/homekit_controller/test_config_flow.py b/tests/components/homekit_controller/test_config_flow.py index 07e2dd4afb1..b5f923dd55e 100644 --- a/tests/components/homekit_controller/test_config_flow.py +++ b/tests/components/homekit_controller/test_config_flow.py @@ -47,6 +47,15 @@ def _setup_flow_handler(hass): return flow +async def _setup_flow_zeroconf(hass, discovery_info): + result = await hass.config_entries.flow.async_init( + 'homekit_controller', + context={'source': 'zeroconf'}, + data=discovery_info, + ) + return result + + async def test_discovery_works(hass): """Test a device being discovered.""" discovery_info = { @@ -67,7 +76,10 @@ async def test_discovery_works(hass): result = await flow.async_step_zeroconf(discovery_info) assert result['type'] == 'form' assert result['step_id'] == 'pair' - assert flow.context == {'title_placeholders': {'name': 'TestDevice'}} + assert flow.context == { + 'hkid': '00:00:00:00:00:00', + 'title_placeholders': {'name': 'TestDevice'} + } # User initiates pairing - device enters pairing mode and displays code result = await flow.async_step_pair({}) @@ -122,7 +134,10 @@ async def test_discovery_works_upper_case(hass): result = await flow.async_step_zeroconf(discovery_info) assert result['type'] == 'form' assert result['step_id'] == 'pair' - assert flow.context == {'title_placeholders': {'name': 'TestDevice'}} + assert flow.context == { + 'hkid': '00:00:00:00:00:00', + 'title_placeholders': {'name': 'TestDevice'} + } # User initiates pairing - device enters pairing mode and displays code result = await flow.async_step_pair({}) @@ -175,7 +190,10 @@ async def test_discovery_works_missing_csharp(hass): result = await flow.async_step_zeroconf(discovery_info) assert result['type'] == 'form' assert result['step_id'] == 'pair' - assert flow.context == {'title_placeholders': {'name': 'TestDevice'}} + assert flow.context == { + 'hkid': '00:00:00:00:00:00', + 'title_placeholders': {'name': 'TestDevice'} + } # User initiates pairing - device enters pairing mode and displays code result = await flow.async_step_pair({}) @@ -210,6 +228,29 @@ async def test_discovery_works_missing_csharp(hass): assert result['data'] == pairing.pairing_data +async def test_abort_duplicate_flow(hass): + """Already paired.""" + discovery_info = { + 'name': 'TestDevice', + 'host': '127.0.0.1', + 'port': 8080, + 'properties': { + 'md': 'TestDevice', + 'id': '00:00:00:00:00:00', + 'c#': 1, + 'sf': 1, + } + } + + result = await _setup_flow_zeroconf(hass, discovery_info) + assert result['type'] == 'form' + assert result['step_id'] == 'pair' + + result = await _setup_flow_zeroconf(hass, discovery_info) + assert result['type'] == 'abort' + assert result['reason'] == 'already_in_progress' + + async def test_pair_already_paired_1(hass): """Already paired.""" discovery_info = { @@ -229,7 +270,10 @@ async def test_pair_already_paired_1(hass): result = await flow.async_step_zeroconf(discovery_info) assert result['type'] == 'abort' assert result['reason'] == 'already_paired' - assert flow.context == {'title_placeholders': {'name': 'TestDevice'}} + assert flow.context == { + 'hkid': '00:00:00:00:00:00', + 'title_placeholders': {'name': 'TestDevice'} + } async def test_discovery_ignored_model(hass): @@ -251,7 +295,10 @@ async def test_discovery_ignored_model(hass): result = await flow.async_step_zeroconf(discovery_info) assert result['type'] == 'abort' assert result['reason'] == 'ignored_model' - assert flow.context == {'title_placeholders': {'name': 'TestDevice'}} + assert flow.context == { + 'hkid': '00:00:00:00:00:00', + 'title_placeholders': {'name': 'TestDevice'} + } async def test_discovery_invalid_config_entry(hass): @@ -280,7 +327,10 @@ async def test_discovery_invalid_config_entry(hass): result = await flow.async_step_zeroconf(discovery_info) assert result['type'] == 'form' assert result['step_id'] == 'pair' - assert flow.context == {'title_placeholders': {'name': 'TestDevice'}} + assert flow.context == { + 'hkid': '00:00:00:00:00:00', + 'title_placeholders': {'name': 'TestDevice'} + } # Discovery of a HKID that is in a pairable state but for which there is # already a config entry - in that case the stale config entry is @@ -314,7 +364,10 @@ async def test_discovery_already_configured(hass): result = await flow.async_step_zeroconf(discovery_info) assert result['type'] == 'abort' assert result['reason'] == 'already_configured' - assert flow.context == {'title_placeholders': {'name': 'TestDevice'}} + assert flow.context == { + 'hkid': '00:00:00:00:00:00', + 'title_placeholders': {'name': 'TestDevice'} + } assert conn.async_config_num_changed.call_count == 0 @@ -344,7 +397,10 @@ async def test_discovery_already_configured_config_change(hass): result = await flow.async_step_zeroconf(discovery_info) assert result['type'] == 'abort' assert result['reason'] == 'already_configured' - assert flow.context == {'title_placeholders': {'name': 'TestDevice'}} + assert flow.context == { + 'hkid': '00:00:00:00:00:00', + 'title_placeholders': {'name': 'TestDevice'} + } assert conn.async_refresh_entity_map.call_args == mock.call(2) @@ -369,7 +425,10 @@ async def test_pair_unable_to_pair(hass): result = await flow.async_step_zeroconf(discovery_info) assert result['type'] == 'form' assert result['step_id'] == 'pair' - assert flow.context == {'title_placeholders': {'name': 'TestDevice'}} + assert flow.context == { + 'hkid': '00:00:00:00:00:00', + 'title_placeholders': {'name': 'TestDevice'} + } # User initiates pairing - device enters pairing mode and displays code result = await flow.async_step_pair({}) @@ -406,7 +465,10 @@ async def test_pair_abort_errors_on_start(hass, exception, expected): result = await flow.async_step_zeroconf(discovery_info) assert result['type'] == 'form' assert result['step_id'] == 'pair' - assert flow.context == {'title_placeholders': {'name': 'TestDevice'}} + assert flow.context == { + 'hkid': '00:00:00:00:00:00', + 'title_placeholders': {'name': 'TestDevice'} + } # User initiates pairing - device refuses to enter pairing mode with mock.patch.object(flow.controller, 'start_pairing') as start_pairing: @@ -415,7 +477,10 @@ async def test_pair_abort_errors_on_start(hass, exception, expected): assert result['type'] == 'abort' assert result['reason'] == expected - assert flow.context == {'title_placeholders': {'name': 'TestDevice'}} + assert flow.context == { + 'hkid': '00:00:00:00:00:00', + 'title_placeholders': {'name': 'TestDevice'} + } @pytest.mark.parametrize("exception,expected", PAIRING_START_FORM_ERRORS) @@ -439,7 +504,10 @@ async def test_pair_form_errors_on_start(hass, exception, expected): result = await flow.async_step_zeroconf(discovery_info) assert result['type'] == 'form' assert result['step_id'] == 'pair' - assert flow.context == {'title_placeholders': {'name': 'TestDevice'}} + assert flow.context == { + 'hkid': '00:00:00:00:00:00', + 'title_placeholders': {'name': 'TestDevice'} + } # User initiates pairing - device refuses to enter pairing mode with mock.patch.object(flow.controller, 'start_pairing') as start_pairing: @@ -448,7 +516,10 @@ async def test_pair_form_errors_on_start(hass, exception, expected): assert result['type'] == 'form' assert result['errors']['pairing_code'] == expected - assert flow.context == {'title_placeholders': {'name': 'TestDevice'}} + assert flow.context == { + 'hkid': '00:00:00:00:00:00', + 'title_placeholders': {'name': 'TestDevice'} + } @pytest.mark.parametrize("exception,expected", PAIRING_FINISH_ABORT_ERRORS) @@ -472,7 +543,10 @@ async def test_pair_abort_errors_on_finish(hass, exception, expected): result = await flow.async_step_zeroconf(discovery_info) assert result['type'] == 'form' assert result['step_id'] == 'pair' - assert flow.context == {'title_placeholders': {'name': 'TestDevice'}} + assert flow.context == { + 'hkid': '00:00:00:00:00:00', + 'title_placeholders': {'name': 'TestDevice'} + } # User initiates pairing - device enters pairing mode and displays code result = await flow.async_step_pair({}) @@ -487,7 +561,10 @@ async def test_pair_abort_errors_on_finish(hass, exception, expected): }) assert result['type'] == 'abort' assert result['reason'] == expected - assert flow.context == {'title_placeholders': {'name': 'TestDevice'}} + assert flow.context == { + 'hkid': '00:00:00:00:00:00', + 'title_placeholders': {'name': 'TestDevice'} + } @pytest.mark.parametrize("exception,expected", PAIRING_FINISH_FORM_ERRORS) @@ -511,7 +588,10 @@ async def test_pair_form_errors_on_finish(hass, exception, expected): result = await flow.async_step_zeroconf(discovery_info) assert result['type'] == 'form' assert result['step_id'] == 'pair' - assert flow.context == {'title_placeholders': {'name': 'TestDevice'}} + assert flow.context == { + 'hkid': '00:00:00:00:00:00', + 'title_placeholders': {'name': 'TestDevice'} + } # User initiates pairing - device enters pairing mode and displays code result = await flow.async_step_pair({}) @@ -526,7 +606,10 @@ async def test_pair_form_errors_on_finish(hass, exception, expected): }) assert result['type'] == 'form' assert result['errors']['pairing_code'] == expected - assert flow.context == {'title_placeholders': {'name': 'TestDevice'}} + assert flow.context == { + 'hkid': '00:00:00:00:00:00', + 'title_placeholders': {'name': 'TestDevice'} + } async def test_import_works(hass): @@ -743,7 +826,10 @@ async def test_parse_new_homekit_json(hass): assert result['type'] == 'create_entry' assert result['title'] == 'TestDevice' assert result['data']['AccessoryPairingID'] == '00:00:00:00:00:00' - assert flow.context == {'title_placeholders': {'name': 'TestDevice'}} + assert flow.context == { + 'hkid': '00:00:00:00:00:00', + 'title_placeholders': {'name': 'TestDevice'} + } async def test_parse_old_homekit_json(hass): @@ -801,7 +887,10 @@ async def test_parse_old_homekit_json(hass): assert result['type'] == 'create_entry' assert result['title'] == 'TestDevice' assert result['data']['AccessoryPairingID'] == '00:00:00:00:00:00' - assert flow.context == {'title_placeholders': {'name': 'TestDevice'}} + assert flow.context == { + 'hkid': '00:00:00:00:00:00', + 'title_placeholders': {'name': 'TestDevice'} + } async def test_parse_overlapping_homekit_json(hass): @@ -872,4 +961,7 @@ async def test_parse_overlapping_homekit_json(hass): assert result['type'] == 'create_entry' assert result['title'] == 'TestDevice' assert result['data']['AccessoryPairingID'] == '00:00:00:00:00:00' - assert flow.context == {'title_placeholders': {'name': 'TestDevice'}} + assert flow.context == { + 'hkid': '00:00:00:00:00:00', + 'title_placeholders': {'name': 'TestDevice'} + } From 50db622689193eec57a7f3e21cd26f11c83acfbd Mon Sep 17 00:00:00 2001 From: Andre Lengwenus Date: Thu, 30 May 2019 00:59:38 +0200 Subject: [PATCH 178/232] Add service calls for LCN component (#24105) --- homeassistant/components/lcn/__init__.py | 21 ++ homeassistant/components/lcn/const.py | 24 +- homeassistant/components/lcn/helpers.py | 40 +++ homeassistant/components/lcn/services.py | 326 +++++++++++++++++++++ homeassistant/components/lcn/services.yaml | 201 +++++++++++++ 5 files changed, 611 insertions(+), 1 deletion(-) create mode 100755 homeassistant/components/lcn/services.py create mode 100755 homeassistant/components/lcn/services.yaml diff --git a/homeassistant/components/lcn/__init__.py b/homeassistant/components/lcn/__init__.py index 77ba00e451d..4a421274a18 100644 --- a/homeassistant/components/lcn/__init__.py +++ b/homeassistant/components/lcn/__init__.py @@ -21,6 +21,9 @@ from .const import ( LOGICOP_PORTS, MOTOR_PORTS, OUTPUT_PORTS, RELAY_PORTS, S0_INPUTS, SETPOINTS, THRESHOLDS, VAR_UNITS, VARIABLES) from .helpers import has_unique_connection_names, is_address +from .services import ( + DynText, Led, LockKeys, LockRegulator, OutputAbs, OutputRel, OutputToggle, + Pck, Relays, SendKeys, VarAbs, VarRel, VarReset) _LOGGER = logging.getLogger(__name__) @@ -155,6 +158,24 @@ async def async_setup(hass, config): hass.async_create_task( async_load_platform(hass, component, DOMAIN, config[DOMAIN][conf_key], config)) + + # register service calls + for service_name, service in (('output_abs', OutputAbs), + ('output_rel', OutputRel), + ('output_toggle', OutputToggle), + ('relays', Relays), + ('var_abs', VarAbs), + ('var_reset', VarReset), + ('var_rel', VarRel), + ('lock_regulator', LockRegulator), + ('led', Led), + ('send_keys', SendKeys), + ('lock_keys', LockKeys), + ('dyn_text', DynText), + ('pck', Pck)): + hass.services.async_register(DOMAIN, service_name, + service(hass), service.schema) + return True diff --git a/homeassistant/components/lcn/const.py b/homeassistant/components/lcn/const.py index 45dc04a491e..9307fb4d706 100644 --- a/homeassistant/components/lcn/const.py +++ b/homeassistant/components/lcn/const.py @@ -15,9 +15,20 @@ CONF_DIM_MODE = 'dim_mode' CONF_DIMMABLE = 'dimmable' CONF_TRANSITION = 'transition' CONF_MOTOR = 'motor' +CONF_LOCKABLE = 'lockable' +CONF_VARIABLE = 'variable' +CONF_VALUE = 'value' +CONF_RELVARREF = 'value_reference' CONF_SOURCE = 'source' CONF_SETPOINT = 'setpoint' -CONF_LOCKABLE = 'lockable' +CONF_LED = 'led' +CONF_KEYS = 'keys' +CONF_TIME = 'time' +CONF_TIME_UNIT = 'time_unit' +CONF_TABLE = 'table' +CONF_ROW = 'row' +CONF_TEXT = 'text' +CONF_PCK = 'pck' CONF_CLIMATES = 'climates' CONF_MAX_TEMP = 'max_temp' CONF_MIN_TEMP = 'min_temp' @@ -36,6 +47,8 @@ MOTOR_PORTS = ['MOTOR1', 'MOTOR2', 'MOTOR3', 'MOTOR4'] LED_PORTS = ['LED1', 'LED2', 'LED3', 'LED4', 'LED5', 'LED6', 'LED7', 'LED8', 'LED9', 'LED10', 'LED11', 'LED12'] +LED_STATUS = ['OFF', 'ON', 'BLINK', 'FLICKER'] + LOGICOP_PORTS = ['LOGICOP1', 'LOGICOP2', 'LOGICOP3', 'LOGICOP4'] BINSENSOR_PORTS = ['BINSENSOR1', 'BINSENSOR2', 'BINSENSOR3', 'BINSENSOR4', @@ -70,3 +83,12 @@ VAR_UNITS = ['', 'LCN', 'NATIVE', 'VOLT', 'V', 'AMPERE', 'AMP', 'A', 'DEGREE', '°'] + +RELVARREF = ['CURRENT', 'PROG'] + +SENDKEYCOMMANDS = ['HIT', 'MAKE', 'BREAK', 'DONTSEND'] + +TIME_UNITS = ['SECONDS', 'SECOND', 'SEC', 'S', + 'MINUTES', 'MINUTE', 'MIN', 'M', + 'HOURS', 'HOUR', 'H', + 'DAYS', 'DAY', 'D'] diff --git a/homeassistant/components/lcn/helpers.py b/homeassistant/components/lcn/helpers.py index 701b6e2436e..d663a6320b1 100644 --- a/homeassistant/components/lcn/helpers.py +++ b/homeassistant/components/lcn/helpers.py @@ -65,3 +65,43 @@ def is_address(value): conn_id = matcher.group('conn_id') return addr, conn_id raise vol.error.Invalid('Not a valid address string.') + + +def is_relays_states_string(states_string): + """Validate the given states string and return states list.""" + if len(states_string) == 8: + states = [] + for state_string in states_string: + if state_string == '1': + state = 'ON' + elif state_string == '0': + state = 'OFF' + elif state_string == 'T': + state = 'TOGGLE' + elif state_string == '-': + state = 'NOCHANGE' + else: + raise vol.error.Invalid('Not a valid relay state string.') + states.append(state) + return states + raise vol.error.Invalid('Wrong length of relay state string.') + + +def is_key_lock_states_string(states_string): + """Validate the given states string and returns states list.""" + if len(states_string) == 8: + states = [] + for state_string in states_string: + if state_string == '1': + state = 'ON' + elif state_string == '0': + state = 'OFF' + elif state_string == 'T': + state = 'TOGGLE' + elif state_string == '-': + state = 'NOCHANGE' + else: + raise vol.error.Invalid('Not a valid key lock state string.') + states.append(state) + return states + raise vol.error.Invalid('Wrong length of key lock state string.') diff --git a/homeassistant/components/lcn/services.py b/homeassistant/components/lcn/services.py new file mode 100755 index 00000000000..78a887a80c1 --- /dev/null +++ b/homeassistant/components/lcn/services.py @@ -0,0 +1,326 @@ +"""Service calls related dependencies for LCN component.""" +import pypck +import voluptuous as vol + +from homeassistant.const import ( + CONF_ADDRESS, CONF_BRIGHTNESS, CONF_STATE, CONF_UNIT_OF_MEASUREMENT) +import homeassistant.helpers.config_validation as cv + +from .const import ( + CONF_CONNECTIONS, CONF_KEYS, CONF_LED, CONF_OUTPUT, CONF_PCK, + CONF_RELVARREF, CONF_ROW, CONF_SETPOINT, CONF_TABLE, CONF_TEXT, CONF_TIME, + CONF_TIME_UNIT, CONF_TRANSITION, CONF_VALUE, CONF_VARIABLE, DATA_LCN, + LED_PORTS, LED_STATUS, OUTPUT_PORTS, RELVARREF, SENDKEYCOMMANDS, SETPOINTS, + THRESHOLDS, TIME_UNITS, VAR_UNITS, VARIABLES) +from .helpers import ( + get_connection, is_address, is_key_lock_states_string, + is_relays_states_string) + + +class LcnServiceCall(): + """Parent class for all LCN service calls.""" + + schema = vol.Schema({ + vol.Required(CONF_ADDRESS): is_address + }) + + def __init__(self, hass): + """Initialize service call.""" + self.connections = hass.data[DATA_LCN][CONF_CONNECTIONS] + + def get_address_connection(self, call): + """Get address connection object.""" + addr, connection_id = call.data[CONF_ADDRESS] + addr = pypck.lcn_addr.LcnAddr(*addr) + if connection_id is None: + connection = self.connections[0] + else: + connection = get_connection(self.connections, connection_id) + + return connection.get_address_conn(addr) + + +class OutputAbs(LcnServiceCall): + """Set absolute brightness of output port in percent.""" + + schema = LcnServiceCall.schema.extend({ + vol.Required(CONF_OUTPUT): vol.All(vol.Upper, vol.In(OUTPUT_PORTS)), + vol.Required(CONF_BRIGHTNESS): + vol.All(vol.Coerce(int), vol.Range(min=0, max=100)), + vol.Optional(CONF_TRANSITION, default=0): + vol.All(vol.Coerce(float), vol.Range(min=0., max=486.)) + }) + + def __call__(self, call): + """Execute service call.""" + output = pypck.lcn_defs.OutputPort[call.data[CONF_OUTPUT]] + brightness = call.data[CONF_BRIGHTNESS] + transition = pypck.lcn_defs.time_to_ramp_value( + call.data[CONF_TRANSITION] * 1000) + + address_connection = self.get_address_connection(call) + address_connection.dim_output(output.value, brightness, transition) + + +class OutputRel(LcnServiceCall): + """Set relative brightness of output port in percent.""" + + schema = LcnServiceCall.schema.extend({ + vol.Required(CONF_OUTPUT): vol.All(vol.Upper, vol.In(OUTPUT_PORTS)), + vol.Required(CONF_BRIGHTNESS): + vol.All(vol.Coerce(int), vol.Range(min=-100, max=100)) + }) + + def __call__(self, call): + """Execute service call.""" + output = pypck.lcn_defs.OutputPort[call.data[CONF_OUTPUT]] + brightness = call.data[CONF_BRIGHTNESS] + + address_connection = self.get_address_connection(call) + address_connection.rel_output(output.value, brightness) + + +class OutputToggle(LcnServiceCall): + """Toggle output port.""" + + schema = LcnServiceCall.schema.extend({ + vol.Required(CONF_OUTPUT): vol.All(vol.Upper, vol.In(OUTPUT_PORTS)), + vol.Optional(CONF_TRANSITION, default=0): + vol.All(vol.Coerce(float), vol.Range(min=0., max=486.)) + }) + + def __call__(self, call): + """Execute service call.""" + output = pypck.lcn_defs.OutputPort[call.data[CONF_OUTPUT]] + transition = pypck.lcn_defs.time_to_ramp_value( + call.data[CONF_TRANSITION] * 1000) + + address_connection = self.get_address_connection(call) + address_connection.toggle_output(output.value, transition) + + +class Relays(LcnServiceCall): + """Set the relays status.""" + + schema = LcnServiceCall.schema.extend({ + vol.Required(CONF_STATE): is_relays_states_string}) + + def __call__(self, call): + """Execute service call.""" + states = [pypck.lcn_defs.RelayStateModifier[state] + for state in call.data[CONF_STATE]] + + address_connection = self.get_address_connection(call) + address_connection.control_relays(states) + + +class Led(LcnServiceCall): + """Set the led state.""" + + schema = LcnServiceCall.schema.extend({ + vol.Required(CONF_LED): vol.All(vol.Upper, vol.In(LED_PORTS)), + vol.Required(CONF_STATE): vol.All(vol.Upper, vol.In(LED_STATUS))}) + + def __call__(self, call): + """Execute service call.""" + led = pypck.lcn_defs.LedPort[call.data[CONF_LED]] + led_state = pypck.lcn_defs.LedStatus[ + call.data[CONF_STATE]] + + address_connection = self.get_address_connection(call) + address_connection.control_led(led, led_state) + + +class VarAbs(LcnServiceCall): + """Set absolute value of a variable or setpoint. + + Variable has to be set as counter! + Reguator setpoints can also be set using R1VARSETPOINT, R2VARSETPOINT. + """ + + schema = LcnServiceCall.schema.extend({ + vol.Required(CONF_VARIABLE): vol.All(vol.Upper, + vol.In(VARIABLES + SETPOINTS)), + vol.Optional(CONF_VALUE, default=0): + vol.All(vol.Coerce(int), vol.Range(min=0)), + vol.Optional(CONF_UNIT_OF_MEASUREMENT, default='native'): + vol.All(vol.Upper, vol.In(VAR_UNITS)) + }) + + def __call__(self, call): + """Execute service call.""" + var = pypck.lcn_defs.Var[call.data[CONF_VARIABLE]] + value = call.data[CONF_VALUE] + unit = pypck.lcn_defs.VarUnit.parse( + call.data[CONF_UNIT_OF_MEASUREMENT]) + + address_connection = self.get_address_connection(call) + address_connection.var_abs(var, value, unit) + + +class VarReset(LcnServiceCall): + """Reset value of variable or setpoint.""" + + schema = LcnServiceCall.schema.extend({ + vol.Required(CONF_VARIABLE): vol.All(vol.Upper, + vol.In(VARIABLES + SETPOINTS)) + }) + + def __call__(self, call): + """Execute service call.""" + var = pypck.lcn_defs.Var[call.data[CONF_VARIABLE]] + + address_connection = self.get_address_connection(call) + address_connection.var_reset(var) + + +class VarRel(LcnServiceCall): + """Shift value of a variable, setpoint or threshold.""" + + schema = LcnServiceCall.schema.extend({ + vol.Required(CONF_VARIABLE): + vol.All(vol.Upper, vol.In(VARIABLES + SETPOINTS + THRESHOLDS)), + vol.Optional(CONF_VALUE, default=0): int, + vol.Optional(CONF_UNIT_OF_MEASUREMENT, default='native'): + vol.All(vol.Upper, vol.In(VAR_UNITS)), + vol.Optional(CONF_RELVARREF, default='current'): + vol.All(vol.Upper, vol.In(RELVARREF)) + }) + + def __call__(self, call): + """Execute service call.""" + var = pypck.lcn_defs.Var[call.data[CONF_VARIABLE]] + value = call.data[CONF_VALUE] + unit = pypck.lcn_defs.VarUnit.parse( + call.data[CONF_UNIT_OF_MEASUREMENT]) + value_ref = pypck.lcn_defs.RelVarRef[ + call.data[CONF_RELVARREF]] + + address_connection = self.get_address_connection(call) + address_connection.var_rel(var, value, unit, value_ref) + + +class LockRegulator(LcnServiceCall): + """Locks a regulator setpoint.""" + + schema = LcnServiceCall.schema.extend({ + vol.Required(CONF_SETPOINT): vol.All(vol.Upper, vol.In(SETPOINTS)), + vol.Optional(CONF_STATE, default=False): bool, + }) + + def __call__(self, call): + """Execute service call.""" + setpoint = pypck.lcn_defs.Var[call.data[CONF_SETPOINT]] + state = call.data[CONF_STATE] + + reg_id = pypck.lcn_defs.Var.to_set_point_id(setpoint) + address_connection = self.get_address_connection(call) + address_connection.lock_regulator(reg_id, state) + + +class SendKeys(LcnServiceCall): + """Sends keys (which executes bound commands).""" + + schema = LcnServiceCall.schema.extend({ + vol.Required(CONF_KEYS): cv.matches_regex(r'^([a-dA-D][1-8])+$'), + vol.Optional(CONF_STATE, default='hit'): + vol.All(vol.Upper, vol.In(SENDKEYCOMMANDS)), + vol.Optional(CONF_TIME, default=0): vol.All(int, vol.Range(min=0)), + vol.Optional(CONF_TIME_UNIT, default='s'): vol.All(vol.Upper, + vol.In(TIME_UNITS)) + }) + + def __call__(self, call): + """Execute service call.""" + address_connection = self.get_address_connection(call) + + keys = [[False] * 8 for i in range(4)] + + key_strings = zip(call.data[CONF_KEYS][::2], + call.data[CONF_KEYS][1::2]) + + for table, key in key_strings: + table_id = ord(table) - 65 + key_id = int(key) - 1 + keys[table_id][key_id] = True + + delay_time = call.data[CONF_TIME] + if delay_time != 0: + hit = pypck.lcn_defs.SendKeyCommand.HIT + if pypck.lcn_defs.SendKeyCommand[ + call.data[CONF_STATE]] != hit: + raise ValueError('Only hit command is allowed when sending' + ' deferred keys.') + delay_unit = pypck.lcn_defs.TimeUnit.parse( + call.data[CONF_TIME_UNIT]) + address_connection.send_keys_hit_deferred( + keys, delay_time, delay_unit) + else: + state = pypck.lcn_defs.SendKeyCommand[ + call.data[CONF_STATE]] + address_connection.send_keys(keys, state) + + +class LockKeys(LcnServiceCall): + """Lock keys.""" + + schema = LcnServiceCall.schema.extend({ + vol.Optional(CONF_TABLE, default='a'): cv.matches_regex(r'^[a-dA-D]$'), + vol.Required(CONF_STATE): is_key_lock_states_string, + vol.Optional(CONF_TIME, default=0): vol.All(int, vol.Range(min=0)), + vol.Optional(CONF_TIME_UNIT, default='s'): vol.All(vol.Upper, + vol.In(TIME_UNITS)) + }) + + def __call__(self, call): + """Execute service call.""" + address_connection = self.get_address_connection(call) + + states = [pypck.lcn_defs.KeyLockStateModifier[state] + for state in call.data[CONF_STATE]] + table_id = ord(call.data[CONF_TABLE]) - 65 + + delay_time = call.data[CONF_TIME] + if delay_time != 0: + if table_id != 0: + raise ValueError('Only table A is allowed when locking keys' + ' for a specific time.') + delay_unit = pypck.lcn_defs.TimeUnit.parse( + call.data[CONF_TIME_UNIT]) + address_connection.lock_keys_tab_a_temporary( + delay_time, delay_unit, states) + else: + address_connection.lock_keys(table_id, states) + + address_connection.request_status_locked_keys_timeout() + + +class DynText(LcnServiceCall): + """Send dynamic text to LCN-GTxD displays.""" + + schema = LcnServiceCall.schema.extend({ + vol.Required(CONF_ROW): vol.All(int, vol.Range(min=1, max=4)), + vol.Required(CONF_TEXT): vol.All(str, vol.Length(max=60)) + }) + + def __call__(self, call): + """Execute service call.""" + row_id = call.data[CONF_ROW] - 1 + text = call.data[CONF_TEXT] + + address_connection = self.get_address_connection(call) + address_connection.dyn_text(row_id, text) + + +class Pck(LcnServiceCall): + """Send arbitrary PCK command.""" + + schema = LcnServiceCall.schema.extend({ + vol.Required(CONF_PCK): str + }) + + def __call__(self, call): + """Execute service call.""" + pck = call.data[CONF_PCK] + address_connection = self.get_address_connection(call) + address_connection.pck(pck) diff --git a/homeassistant/components/lcn/services.yaml b/homeassistant/components/lcn/services.yaml new file mode 100755 index 00000000000..b8f4fbb20a7 --- /dev/null +++ b/homeassistant/components/lcn/services.yaml @@ -0,0 +1,201 @@ +# Describes the format for available LCN services + +output_abs: + description: Set absolute brightness of output port in percent. + fields: + address: + description: Module address + example: 'myhome.s0.m7' + output: + description: Output port + example: "output1" + brightness: + description: Absolute brightness in percent (0..100) + example: 50 + transition: + description: Transition time in seconds + example: 5 + +output_rel: + description: Set relative brightness of output port in percent. + fields: + address: + description: Module address + example: 'myhome.s0.m7' + output: + description: Output port + example: "output1" + brightness: + description: Relative brightness in percent (-100..100) + example: 50 + transition: + description: Transition time in seconds + example: 5 + +output_toggle: + description: Toggle output port. + fields: + address: + description: Module address + example: 'myhome.s0.m7' + output: + description: Output port + example: "output1" + transition: + description: Transition time in seconds + example: 5 + +relays: + description: Set the relays status. + fields: + address: + description: Module address + example: 'myhome.s0.m7' + state: + description: Relays states as string (1=on, 2=off, t=toggle, -=nochange) + example: "t---001-" + +led: + description: Set the led state. + fields: + address: + description: Module address + example: 'myhome.s0.m7' + led: + description: Led + example: "led6" + state: + description: Led state + example: 'blink' + values: + - on + - off + - blink + - flicker + +var_abs: + description: Set absolute value of a variable or setpoint. + fields: + address: + description: Module address + example: 'myhome.s0.m7' + variable: + description: Variable or setpoint name + example: 'var1' + value: + description: Value to set + example: '50' + unit_of_measurement: + description: Unit of value + example: 'celsius' + +var_reset: + description: Reset value of variable or setpoint. + fields: + address: + description: Module address + example: 'myhome.s0.m7' + variable: + description: Variable or setpoint name + example: 'var1' + +var_rel: + description: Shift value of a variable, setpoint or threshold. + fields: + address: + description: Module address + example: 'myhome.s0.m7' + variable: + description: Variable or setpoint name + example: 'var1' + value: + description: Shift value + example: '50' + unit_of_measurement: + description: Unit of value + example: 'celsius' + value_reference: + description: Reference value (current or programmed) for setpoint and threshold + example: 'current' + values: + - current + - prog + +lock_regulator: + description: Lock a regulator setpoint. + fields: + address: + description: Module address + example: 'myhome.s0.m7' + setpoint: + description: Setpoint name + example: 'r1varsetpoint' + state: + description: New setpoint state + example: true + +send_keys: + description: Send keys (which executes bound commands). + fields: + address: + description: Module address + example: 'myhome.s0.m7' + keys: + description: Keys to send + example: 'a1a5d8' + state: + description: 'Key state upon sending (optional, must be hit for deferred)' + example: 'hit' + values: + - hit + - make + - break + time: + description: Send delay (optional) + example: 10 + time_unit: + description: Time unit of send delay (optional) + example: 's' + +lock_keys: + description: Lock keys. + fields: + address: + description: Module address + example: 'myhome.s0.m7' + table: + description: 'Table with keys to lock (optional, must be A for interval).' + example: 'A5' + state: + description: Key lock states as string (1=on, 2=off, T=toggle, -=nochange) + example: '1---t0--' + time: + description: Lock interval (optional) + example: 10 + time_unit: + description: Time unit of lock interval (optional) + example: 's' + +dyn_text: + description: Send dynamic text to LCN-GTxD displays. + fields: + address: + description: Module address + example: 'myhome.s0.m7' + row: + description: Text row 1..4 (support of 4 independent text rows) + example: 1 + text: + description: Text to send (up to 60 characters encoded as UTF-8) + example: 'text up to 60 characters' + +pck: + description: Send arbitrary PCK command. + fields: + address: + description: Module address + example: 'myhome.s0.m7' + pck: + description: PCK command (without address header) + example: 'PIN4' + \ No newline at end of file From c08862679dd5191fe7f303a5bd71520dcf1bb757 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 29 May 2019 16:01:51 -0700 Subject: [PATCH 179/232] Bumped version to 0.94.0b0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 1dcea3e2daf..f00a3d5c9d7 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 94 -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 78ffb6f3e612c261381da64aff9215897fad115c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 30 May 2019 09:32:29 -0700 Subject: [PATCH 180/232] Updated frontend to 20190530.0 --- homeassistant/components/frontend/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 72885690223..cb6ce89198e 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/components/frontend", "requirements": [ - "home-assistant-frontend==20190529.0" + "home-assistant-frontend==20190530.0" ], "dependencies": [ "api", diff --git a/requirements_all.txt b/requirements_all.txt index 4f5ada38c44..cfa7122b1b7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -577,7 +577,7 @@ hole==0.3.0 holidays==0.9.10 # homeassistant.components.frontend -home-assistant-frontend==20190529.0 +home-assistant-frontend==20190530.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b6b2616964c..00707961ec6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -148,7 +148,7 @@ hdate==0.8.7 holidays==0.9.10 # homeassistant.components.frontend -home-assistant-frontend==20190529.0 +home-assistant-frontend==20190530.0 # homeassistant.components.homekit_controller homekit[IP]==0.14.0 From 2bfe7aa21979b50e334aa2f1ade802293b45326f Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 30 May 2019 17:35:47 +0200 Subject: [PATCH 181/232] Update azure-pipelines.yml for check version (#24194) --- azure-pipelines.yml | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 7a1e6e550d7..ce7def09821 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -8,7 +8,6 @@ trigger: tags: include: - '*' - variables: - name: versionBuilder value: '3.2' @@ -18,6 +17,7 @@ variables: - group: wheels - group: github + jobs: - job: 'Wheels' @@ -96,8 +96,30 @@ jobs: displayName: 'Run wheels build' -- job: 'Release' +- job: 'VersionValidate' condition: startsWith(variables['Build.SourceBranch'], 'refs/tags') + pool: + vmImage: 'ubuntu-latest' + steps: + - task: UsePythonVersion@0 + displayName: 'Use Python 3.7' + inputs: + versionSpec: '3.7' + - script: | + setup_version="$(python setup.py -V)" + branch_version="$(Build.SourceBranchName)" + + if [ "${setup_version}" != "${branch_version}" ]; then + echo "Version of tag ${branch_version} don't match with ${setup_version}!" + exit 1 + fi + displayName: 'Check version of branch/tag' + + +- job: 'Release' + condition: and(startsWith(variables['Build.SourceBranch'], 'refs/tags'), succeeded('VersionValidate') + dependsOn: + - 'VersionValidate' timeoutInMinutes: 120 pool: vmImage: 'ubuntu-16.04' From ca89d6184c9cdbdd15a7a674e3abcb5a435b5fa1 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 30 May 2019 18:46:08 +0200 Subject: [PATCH 182/232] Update azure-pipelines.yml for Azure Pipelines --- azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index ce7def09821..2f5792cfea6 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -117,7 +117,7 @@ jobs: - job: 'Release' - condition: and(startsWith(variables['Build.SourceBranch'], 'refs/tags'), succeeded('VersionValidate') + condition: and(startsWith(variables['Build.SourceBranch'], 'refs/tags'), succeeded('VersionValidate')) dependsOn: - 'VersionValidate' timeoutInMinutes: 120 From acc9fd0382106a693f93e7761257e75de8ec3a17 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 30 May 2019 04:37:01 -0700 Subject: [PATCH 183/232] Dynamic panels (#24184) * Don't require all panel urls to be registered * Allow removing panels, fire event when panels updated --- homeassistant/components/calendar/__init__.py | 2 +- homeassistant/components/config/__init__.py | 2 +- homeassistant/components/frontend/__init__.py | 96 +++++++++++-------- .../components/hassio/addon_panel.py | 2 +- homeassistant/components/history/__init__.py | 2 +- homeassistant/components/logbook/__init__.py | 2 +- homeassistant/components/lovelace/__init__.py | 2 +- homeassistant/components/mailbox/__init__.py | 2 +- homeassistant/components/map/__init__.py | 2 +- .../components/panel_custom/__init__.py | 2 +- .../components/panel_iframe/__init__.py | 2 +- .../components/shopping_list/__init__.py | 2 +- .../components/websocket_api/permissions.py | 2 + tests/components/frontend/test_init.py | 71 ++++++++++++-- 14 files changed, 133 insertions(+), 58 deletions(-) diff --git a/homeassistant/components/calendar/__init__.py b/homeassistant/components/calendar/__init__.py index 73a779816a3..5a1ce79c18c 100644 --- a/homeassistant/components/calendar/__init__.py +++ b/homeassistant/components/calendar/__init__.py @@ -36,7 +36,7 @@ async def async_setup(hass, config): hass.http.register_view(CalendarEventView(component)) # Doesn't work in prod builds of the frontend: home-assistant-polymer#1289 - # await hass.components.frontend.async_register_built_in_panel( + # hass.components.frontend.async_register_built_in_panel( # 'calendar', 'calendar', 'hass:calendar') await component.async_setup(config) diff --git a/homeassistant/components/config/__init__.py b/homeassistant/components/config/__init__.py index 8cd8856c1ec..0cb76cc8c3b 100644 --- a/homeassistant/components/config/__init__.py +++ b/homeassistant/components/config/__init__.py @@ -30,7 +30,7 @@ ON_DEMAND = ('zwave',) async def async_setup(hass, config): """Set up the config component.""" - await hass.components.frontend.async_register_built_in_panel( + hass.components.frontend.async_register_built_in_panel( 'config', 'config', 'hass:settings', require_admin=True) async def setup_panel(panel_name): diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 8d7f7213787..8a692d6f272 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -1,5 +1,4 @@ """Handle the frontend for Home Assistant.""" -import asyncio import json import logging import os @@ -26,6 +25,7 @@ CONF_EXTRA_HTML_URL = 'extra_html_url' CONF_EXTRA_HTML_URL_ES5 = 'extra_html_url_es5' CONF_FRONTEND_REPO = 'development_repo' CONF_JS_VERSION = 'javascript_version' +EVENT_PANELS_UPDATED = 'panels_updated' DEFAULT_THEME_COLOR = '#03A9F4' @@ -97,6 +97,28 @@ SCHEMA_GET_TRANSLATIONS = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ }) +def generate_negative_index_regex(): + """Generate regex for index.""" + skip = [ + # files + "service_worker.js", + "robots.txt", + "onboarding.html", + "manifest.json", + ] + for folder in ( + "static", + "frontend_latest", + "frontend_es5", + "local", + "auth", + "api", + ): + # Regex matching static, static/, static/index.html + skip.append("{}(/|/.+|)".format(folder)) + return r"(?!(" + "|".join(skip) + r")).*" + + class Panel: """Abstract class for panels.""" @@ -128,15 +150,6 @@ class Panel: self.config = config self.require_admin = require_admin - @callback - def async_register_index_routes(self, router, index_view): - """Register routes for panel to be served by index view.""" - router.add_route( - 'get', '/{}'.format(self.frontend_url_path), index_view.get) - router.add_route( - 'get', '/{}/{{extra:.+}}'.format(self.frontend_url_path), - index_view.get) - @callback def to_response(self): """Panel as dictionary.""" @@ -151,26 +164,36 @@ class Panel: @bind_hass -async def async_register_built_in_panel(hass, component_name, - sidebar_title=None, sidebar_icon=None, - frontend_url_path=None, config=None, - require_admin=False): +@callback +def async_register_built_in_panel(hass, component_name, + sidebar_title=None, sidebar_icon=None, + frontend_url_path=None, config=None, + require_admin=False): """Register a built-in panel.""" panel = Panel(component_name, sidebar_title, sidebar_icon, frontend_url_path, config, require_admin) - panels = hass.data.get(DATA_PANELS) - if panels is None: - panels = hass.data[DATA_PANELS] = {} + panels = hass.data.setdefault(DATA_PANELS, {}) if panel.frontend_url_path in panels: _LOGGER.warning("Overwriting component %s", panel.frontend_url_path) - if DATA_FINALIZE_PANEL in hass.data: - hass.data[DATA_FINALIZE_PANEL](panel) - panels[panel.frontend_url_path] = panel + hass.bus.async_fire(EVENT_PANELS_UPDATED) + + +@bind_hass +@callback +def async_remove_panel(hass, frontend_url_path): + """Remove a built-in panel.""" + panel = hass.data.get(DATA_PANELS, {}).pop(frontend_url_path, None) + + if panel is None: + _LOGGER.warning("Removing unknown panel %s", frontend_url_path) + + hass.bus.async_fire(EVENT_PANELS_UPDATED) + @bind_hass @callback @@ -233,28 +256,14 @@ async def async_setup(hass, config): if os.path.isdir(local): hass.http.register_static_path("/local", local, not is_dev) - index_view = IndexView(repo_path) - hass.http.register_view(index_view) + hass.http.register_view(IndexView(repo_path)) - @callback - def async_finalize_panel(panel): - """Finalize setup of a panel.""" - panel.async_register_index_routes(hass.http.app.router, index_view) + for panel in ('kiosk', 'states', 'profile'): + async_register_built_in_panel(hass, panel) - await asyncio.wait( - [async_register_built_in_panel(hass, panel) for panel in ( - 'kiosk', 'states', 'profile')]) - await asyncio.wait( - [async_register_built_in_panel(hass, panel, require_admin=True) - for panel in ('dev-event', 'dev-info', 'dev-service', 'dev-state', - 'dev-template', 'dev-mqtt')]) - - hass.data[DATA_FINALIZE_PANEL] = async_finalize_panel - - # Finalize registration of panels that registered before frontend was setup - # This includes the built-in panels from line above. - for panel in hass.data[DATA_PANELS].values(): - async_finalize_panel(panel) + for panel in ('dev-event', 'dev-info', 'dev-service', 'dev-state', + 'dev-template', 'dev-mqtt'): + async_register_built_in_panel(hass, panel, require_admin=True) if DATA_EXTRA_HTML_URL not in hass.data: hass.data[DATA_EXTRA_HTML_URL] = set() @@ -324,6 +333,9 @@ class IndexView(HomeAssistantView): url = '/' name = 'frontend:index' requires_auth = False + extra_urls = [ + "/{extra:%s}" % generate_negative_index_regex() + ] def __init__(self, repo_path): """Initialize the frontend view.""" @@ -349,6 +361,10 @@ class IndexView(HomeAssistantView): """Serve the index view.""" hass = request.app['hass'] + if (request.path != '/' and + request.url.parts[1] not in hass.data[DATA_PANELS]): + raise web.HTTPNotFound + if not hass.components.onboarding.async_is_onboarded(): return web.Response(status=302, headers={ 'location': '/onboarding.html' diff --git a/homeassistant/components/hassio/addon_panel.py b/homeassistant/components/hassio/addon_panel.py index 7291a87e954..e85c8f12247 100644 --- a/homeassistant/components/hassio/addon_panel.py +++ b/homeassistant/components/hassio/addon_panel.py @@ -61,7 +61,7 @@ class HassIOAddonPanel(HomeAssistantView): async def delete(self, request, addon): """Handle remove add-on panel requests.""" - # Currently not supported by backend / frontend + self.hass.components.frontend.async_remove_panel(addon) return web.Response() async def get_panels(self): diff --git a/homeassistant/components/history/__init__.py b/homeassistant/components/history/__init__.py index 7efe4f2beb2..d0dd098638f 100644 --- a/homeassistant/components/history/__init__.py +++ b/homeassistant/components/history/__init__.py @@ -252,7 +252,7 @@ async def async_setup(hass, config): use_include_order = conf.get(CONF_ORDER) hass.http.register_view(HistoryPeriodView(filters, use_include_order)) - await hass.components.frontend.async_register_built_in_panel( + hass.components.frontend.async_register_built_in_panel( 'history', 'history', 'hass:poll-box') return True diff --git a/homeassistant/components/logbook/__init__.py b/homeassistant/components/logbook/__init__.py index 70fe31e64d6..43fe9cb2d52 100644 --- a/homeassistant/components/logbook/__init__.py +++ b/homeassistant/components/logbook/__init__.py @@ -102,7 +102,7 @@ async def async_setup(hass, config): hass.http.register_view(LogbookView(config.get(DOMAIN, {}))) - await hass.components.frontend.async_register_built_in_panel( + hass.components.frontend.async_register_built_in_panel( 'logbook', 'logbook', 'hass:format-list-bulleted-type') hass.services.async_register( diff --git a/homeassistant/components/lovelace/__init__.py b/homeassistant/components/lovelace/__init__.py index f550f85bcef..b1b9cf1a524 100644 --- a/homeassistant/components/lovelace/__init__.py +++ b/homeassistant/components/lovelace/__init__.py @@ -53,7 +53,7 @@ async def async_setup(hass, config): # Pass in default to `get` because defaults not set if loaded as dep mode = config.get(DOMAIN, {}).get(CONF_MODE, MODE_STORAGE) - await hass.components.frontend.async_register_built_in_panel( + hass.components.frontend.async_register_built_in_panel( DOMAIN, config={ 'mode': mode }) diff --git a/homeassistant/components/mailbox/__init__.py b/homeassistant/components/mailbox/__init__.py index 939cc4a2aa2..3b5012ec160 100644 --- a/homeassistant/components/mailbox/__init__.py +++ b/homeassistant/components/mailbox/__init__.py @@ -30,7 +30,7 @@ SCAN_INTERVAL = timedelta(seconds=30) async def async_setup(hass, config): """Track states and offer events for mailboxes.""" mailboxes = [] - await hass.components.frontend.async_register_built_in_panel( + hass.components.frontend.async_register_built_in_panel( 'mailbox', 'mailbox', 'mdi:mailbox') hass.http.register_view(MailboxPlatformsView(mailboxes)) hass.http.register_view(MailboxMessageView(mailboxes)) diff --git a/homeassistant/components/map/__init__.py b/homeassistant/components/map/__init__.py index df8ac49a6d5..ab89ccf23ce 100644 --- a/homeassistant/components/map/__init__.py +++ b/homeassistant/components/map/__init__.py @@ -4,6 +4,6 @@ DOMAIN = 'map' async def async_setup(hass, config): """Register the built-in map panel.""" - await hass.components.frontend.async_register_built_in_panel( + hass.components.frontend.async_register_built_in_panel( 'map', 'map', 'hass:tooltip-account') return True diff --git a/homeassistant/components/panel_custom/__init__.py b/homeassistant/components/panel_custom/__init__.py index f6a4fcdb733..275d80facf4 100644 --- a/homeassistant/components/panel_custom/__init__.py +++ b/homeassistant/components/panel_custom/__init__.py @@ -112,7 +112,7 @@ async def async_register_panel( config['_panel_custom'] = custom_panel_config - await hass.components.frontend.async_register_built_in_panel( + hass.components.frontend.async_register_built_in_panel( component_name='custom', sidebar_title=sidebar_title, sidebar_icon=sidebar_icon, diff --git a/homeassistant/components/panel_iframe/__init__.py b/homeassistant/components/panel_iframe/__init__.py index f4038c82f71..fca33b1cf98 100644 --- a/homeassistant/components/panel_iframe/__init__.py +++ b/homeassistant/components/panel_iframe/__init__.py @@ -32,7 +32,7 @@ CONFIG_SCHEMA = vol.Schema({ async def async_setup(hass, config): """Set up the iFrame frontend panels.""" for url_path, info in config[DOMAIN].items(): - await hass.components.frontend.async_register_built_in_panel( + hass.components.frontend.async_register_built_in_panel( 'iframe', info.get(CONF_TITLE), info.get(CONF_ICON), url_path, {'url': info[CONF_URL]}, require_admin=info[CONF_REQUIRE_ADMIN]) diff --git a/homeassistant/components/shopping_list/__init__.py b/homeassistant/components/shopping_list/__init__.py index cfcbfdd4224..6318d8581c3 100644 --- a/homeassistant/components/shopping_list/__init__.py +++ b/homeassistant/components/shopping_list/__init__.py @@ -117,7 +117,7 @@ def async_setup(hass, config): 'What is on my shopping list' ]) - yield from hass.components.frontend.async_register_built_in_panel( + hass.components.frontend.async_register_built_in_panel( 'shopping-list', 'shopping_list', 'mdi:cart') hass.components.websocket_api.async_register_command( diff --git a/homeassistant/components/websocket_api/permissions.py b/homeassistant/components/websocket_api/permissions.py index 753c5688d18..887573f4abb 100644 --- a/homeassistant/components/websocket_api/permissions.py +++ b/homeassistant/components/websocket_api/permissions.py @@ -14,11 +14,13 @@ from homeassistant.components.lovelace import EVENT_LOVELACE_UPDATED from homeassistant.helpers.area_registry import EVENT_AREA_REGISTRY_UPDATED from homeassistant.helpers.device_registry import EVENT_DEVICE_REGISTRY_UPDATED from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED +from homeassistant.components.frontend import EVENT_PANELS_UPDATED # These are events that do not contain any sensitive data # Except for state_changed, which is handled accordingly. SUBSCRIBE_WHITELIST = { EVENT_COMPONENT_LOADED, + EVENT_PANELS_UPDATED, EVENT_PERSISTENT_NOTIFICATIONS_UPDATED, EVENT_SERVICE_REGISTERED, EVENT_SERVICE_REMOVED, diff --git a/tests/components/frontend/test_init.py b/tests/components/frontend/test_init.py index ee10b986697..09628b5d3fc 100644 --- a/tests/components/frontend/test_init.py +++ b/tests/components/frontend/test_init.py @@ -8,10 +8,11 @@ import pytest from homeassistant.setup import async_setup_component from homeassistant.components.frontend import ( DOMAIN, CONF_JS_VERSION, CONF_THEMES, CONF_EXTRA_HTML_URL, - CONF_EXTRA_HTML_URL_ES5) + CONF_EXTRA_HTML_URL_ES5, generate_negative_index_regex, + EVENT_PANELS_UPDATED) from homeassistant.components.websocket_api.const import TYPE_RESULT -from tests.common import mock_coro +from tests.common import mock_coro, async_capture_events CONFIG_THEMES = { @@ -232,12 +233,21 @@ def test_extra_urls(mock_http_client_with_urls, mock_onboarded): assert text.find('href="https://domain.com/my_extra_url.html"') >= 0 -async def test_get_panels(hass, hass_ws_client): +async def test_get_panels(hass, hass_ws_client, mock_http_client): """Test get_panels command.""" - await async_setup_component(hass, 'frontend', {}) - await hass.components.frontend.async_register_built_in_panel( + events = async_capture_events(hass, EVENT_PANELS_UPDATED) + + resp = await mock_http_client.get('/map') + assert resp.status == 404 + + hass.components.frontend.async_register_built_in_panel( 'map', 'Map', 'mdi:tooltip-account', require_admin=True) + resp = await mock_http_client.get('/map') + assert resp.status == 200 + + assert len(events) == 1 + client = await hass_ws_client(hass) await client.send_json({ 'id': 5, @@ -255,14 +265,21 @@ async def test_get_panels(hass, hass_ws_client): assert msg['result']['map']['title'] == 'Map' assert msg['result']['map']['require_admin'] is True + hass.components.frontend.async_remove_panel('map') + + resp = await mock_http_client.get('/map') + assert resp.status == 404 + + assert len(events) == 2 + async def test_get_panels_non_admin(hass, hass_ws_client, hass_admin_user): """Test get_panels command.""" hass_admin_user.groups = [] await async_setup_component(hass, 'frontend', {}) - await hass.components.frontend.async_register_built_in_panel( + hass.components.frontend.async_register_built_in_panel( 'map', 'Map', 'mdi:tooltip-account', require_admin=True) - await hass.components.frontend.async_register_built_in_panel( + hass.components.frontend.async_register_built_in_panel( 'history', 'History', 'mdi:history') client = await hass_ws_client(hass) @@ -331,3 +348,43 @@ async def test_auth_authorize(mock_http_client): resp = await mock_http_client.get(authorizejs.groups(0)[0]) assert resp.status == 200 assert 'public' in resp.headers.get('cache-control') + + +def test_index_regex(): + """Test the index regex.""" + pattern = re.compile('/' + generate_negative_index_regex()) + + for should_match in ( + '/', + '/lovelace', + '/lovelace/default_view', + '/map', + '/config', + ): + assert pattern.match(should_match), should_match + + for should_not_match in ( + '/service_worker.js', + '/manifest.json', + '/onboarding.html', + '/manifest.json', + 'static', + 'static/', + 'static/index.html', + 'frontend_latest', + 'frontend_latest/', + 'frontend_latest/index.html', + 'frontend_es5', + 'frontend_es5/', + 'frontend_es5/index.html', + 'local', + 'local/', + 'local/index.html', + 'auth', + 'auth/', + 'auth/index.html', + '/api', + '/api/', + '/api/logbook', + ): + assert not pattern.match(should_not_match), should_not_match From 325001933df202fd1717c39f94a8c9c9e5d835e6 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Thu, 30 May 2019 18:48:58 +0200 Subject: [PATCH 184/232] Fix ESPHome discovered when already exists (#24187) * Fix ESPHome discovered when already exists * Update .coveragerc --- .coveragerc | 1 + homeassistant/components/esphome/__init__.py | 169 +++--------------- .../components/esphome/config_flow.py | 21 ++- .../components/esphome/entry_data.py | 107 +++++++++++ tests/components/esphome/test_config_flow.py | 29 ++- 5 files changed, 183 insertions(+), 144 deletions(-) create mode 100644 homeassistant/components/esphome/entry_data.py diff --git a/.coveragerc b/.coveragerc index 030c48cd10c..967c560198c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -172,6 +172,7 @@ omit = homeassistant/components/esphome/camera.py homeassistant/components/esphome/climate.py homeassistant/components/esphome/cover.py + homeassistant/components/esphome/entry_data.py homeassistant/components/esphome/fan.py homeassistant/components/esphome/light.py homeassistant/components/esphome/sensor.py diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index d42bbb725dd..395c145e5df 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -2,12 +2,11 @@ import asyncio import logging import math -from typing import Any, Callable, Dict, List, Optional, Tuple +from typing import Any, Callable, Dict, List, Optional from aioesphomeapi import ( - COMPONENT_TYPE_TO_INFO, APIClient, APIConnectionError, DeviceInfo, - EntityInfo, EntityState, ServiceCall, UserService, UserServiceArgType) -import attr + APIClient, APIConnectionError, DeviceInfo, EntityInfo, EntityState, + ServiceCall, UserService, UserServiceArgType) import voluptuous as vol from homeassistant import const @@ -19,8 +18,7 @@ from homeassistant.exceptions import TemplateError from homeassistant.helpers import template import homeassistant.helpers.config_validation as cv import homeassistant.helpers.device_registry as dr -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, async_dispatcher_send) +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.json import JSONEncoder @@ -30,16 +28,14 @@ from homeassistant.helpers.typing import ConfigType, HomeAssistantType # Import config flow so that it's added to the registry from .config_flow import EsphomeFlowHandler # noqa +from .entry_data import ( + DATA_KEY, DISPATCHER_ON_DEVICE_UPDATE, DISPATCHER_ON_LIST, + DISPATCHER_ON_STATE, DISPATCHER_REMOVE_ENTITY, DISPATCHER_UPDATE_ENTITY, + RuntimeEntryData) DOMAIN = 'esphome' _LOGGER = logging.getLogger(__name__) -DISPATCHER_UPDATE_ENTITY = 'esphome_{entry_id}_update_{component_key}_{key}' -DISPATCHER_REMOVE_ENTITY = 'esphome_{entry_id}_remove_{component_key}_{key}' -DISPATCHER_ON_LIST = 'esphome_{entry_id}_on_list' -DISPATCHER_ON_DEVICE_UPDATE = 'esphome_{entry_id}_on_device_update' -DISPATCHER_ON_STATE = 'esphome_{entry_id}_on_state' - STORAGE_KEY = 'esphome.{}' STORAGE_VERSION = 1 @@ -59,95 +55,6 @@ HA_COMPONENTS = [ CONFIG_SCHEMA = vol.Schema({}, extra=vol.ALLOW_EXTRA) -@attr.s -class RuntimeEntryData: - """Store runtime data for esphome config entries.""" - - entry_id = attr.ib(type=str) - client = attr.ib(type='APIClient') - store = attr.ib(type=Store) - reconnect_task = attr.ib(type=Optional[asyncio.Task], default=None) - state = attr.ib(type=Dict[str, Dict[str, Any]], factory=dict) - info = attr.ib(type=Dict[str, Dict[str, Any]], factory=dict) - services = attr.ib(type=Dict[int, 'UserService'], factory=dict) - available = attr.ib(type=bool, default=False) - device_info = attr.ib(type='DeviceInfo', default=None) - cleanup_callbacks = attr.ib(type=List[Callable[[], None]], factory=list) - disconnect_callbacks = attr.ib(type=List[Callable[[], None]], factory=list) - - def async_update_entity(self, hass: HomeAssistantType, component_key: str, - key: int) -> None: - """Schedule the update of an entity.""" - signal = DISPATCHER_UPDATE_ENTITY.format( - entry_id=self.entry_id, component_key=component_key, key=key) - async_dispatcher_send(hass, signal) - - def async_remove_entity(self, hass: HomeAssistantType, component_key: str, - key: int) -> None: - """Schedule the removal of an entity.""" - signal = DISPATCHER_REMOVE_ENTITY.format( - entry_id=self.entry_id, component_key=component_key, key=key) - async_dispatcher_send(hass, signal) - - def async_update_static_infos(self, hass: HomeAssistantType, - infos: 'List[EntityInfo]') -> None: - """Distribute an update of static infos to all platforms.""" - signal = DISPATCHER_ON_LIST.format(entry_id=self.entry_id) - async_dispatcher_send(hass, signal, infos) - - def async_update_state(self, hass: HomeAssistantType, - state: 'EntityState') -> None: - """Distribute an update of state information to all platforms.""" - signal = DISPATCHER_ON_STATE.format(entry_id=self.entry_id) - async_dispatcher_send(hass, signal, state) - - def async_update_device_state(self, hass: HomeAssistantType) -> None: - """Distribute an update of a core device state like availability.""" - signal = DISPATCHER_ON_DEVICE_UPDATE.format(entry_id=self.entry_id) - async_dispatcher_send(hass, signal) - - async def async_load_from_store(self) -> Tuple[List['EntityInfo'], - List['UserService']]: - """Load the retained data from store and return de-serialized data.""" - restored = await self.store.async_load() - if restored is None: - return [], [] - - self.device_info = _attr_obj_from_dict(DeviceInfo, - **restored.pop('device_info')) - infos = [] - for comp_type, restored_infos in restored.items(): - if comp_type not in COMPONENT_TYPE_TO_INFO: - continue - for info in restored_infos: - cls = COMPONENT_TYPE_TO_INFO[comp_type] - infos.append(_attr_obj_from_dict(cls, **info)) - services = [] - for service in restored.get('services', []): - services.append(UserService.from_dict(service)) - return infos, services - - async def async_save_to_store(self) -> None: - """Generate dynamic data to store and save it to the filesystem.""" - store_data = { - 'device_info': attr.asdict(self.device_info), - 'services': [] - } - - for comp_type, infos in self.info.items(): - store_data[comp_type] = [attr.asdict(info) - for info in infos.values()] - for service in self.services.values(): - store_data['services'].append(service.to_dict()) - - await self.store.async_save(store_data) - - -def _attr_obj_from_dict(cls, **kwargs): - return cls(**{key: kwargs[key] for key in attr.fields_dict(cls) - if key in kwargs}) - - async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: """Stub to allow setting up this component. @@ -159,7 +66,7 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: """Set up the esphome component.""" - hass.data.setdefault(DOMAIN, {}) + hass.data.setdefault(DATA_KEY, {}) host = entry.data[CONF_HOST] port = entry.data[CONF_PORT] @@ -171,7 +78,7 @@ async def async_setup_entry(hass: HomeAssistantType, # Store client in per-config-entry hass.data store = Store(hass, STORAGE_VERSION, STORAGE_KEY.format(entry.entry_id), encoder=JSONEncoder) - entry_data = hass.data[DOMAIN][entry.entry_id] = RuntimeEntryData( + entry_data = hass.data[DATA_KEY][entry.entry_id] = RuntimeEntryData( client=cli, entry_id=entry.entry_id, store=store, @@ -186,12 +93,12 @@ async def async_setup_entry(hass: HomeAssistantType, ) @callback - def async_on_state(state: 'EntityState') -> None: + def async_on_state(state: EntityState) -> None: """Send dispatcher updates when a new state is received.""" entry_data.async_update_state(hass, state) @callback - def async_on_service_call(service: 'ServiceCall') -> None: + def async_on_service_call(service: ServiceCall) -> None: """Call service when user automation in ESPHome config is triggered.""" domain, service_name = service.service.split('.', 1) service_data = service.data @@ -253,26 +160,6 @@ async def async_setup_entry(hass: HomeAssistantType, try_connect = await _setup_auto_reconnect_logic(hass, cli, entry, host, on_login) - # This is a bit of a hack: We schedule complete_setup into the - # event loop and return immediately (return True) - # - # Usually, we should avoid that so that HA can track which components - # have been started successfully and which failed to be set up. - # That doesn't work here for two reasons: - # - We have our own re-connect logic - # - Before we do the first try_connect() call, we need to make sure - # all dispatcher event listeners have been connected, so - # async_forward_entry_setup needs to be awaited. However, if we - # would await async_forward_entry_setup() in async_setup_entry(), - # we would end up with a deadlock. - # - # Solution is: complete the setup outside of the async_setup_entry() - # function. HA will wait until the first connection attempt is made - # before starting up (as it should), but if the first connection attempt - # fails we will schedule all next re-connect attempts outside of the - # tracked tasks (hass.loop.create_task). This way HA won't stall startup - # forever until a connection is successful. - async def complete_setup() -> None: """Complete the config entry setup.""" tasks = [] @@ -285,17 +172,16 @@ async def async_setup_entry(hass: HomeAssistantType, entry_data.async_update_static_infos(hass, infos) await _setup_services(hass, entry_data, services) - # If first connect fails, the next re-connect will be scheduled - # outside of _pending_task, in order not to delay HA startup - # indefinitely - await try_connect(is_disconnect=False) + # Create connection attempt outside of HA's tracked task in order + # not to delay startup. + hass.loop.create_task(try_connect(is_disconnect=False)) hass.async_create_task(complete_setup()) return True async def _setup_auto_reconnect_logic(hass: HomeAssistantType, - cli: 'APIClient', + cli: APIClient, entry: ConfigEntry, host: str, on_login): """Set up the re-connect logic for the API client.""" async def try_connect(tries: int = 0, is_disconnect: bool = True) -> None: @@ -351,7 +237,7 @@ async def _setup_auto_reconnect_logic(hass: HomeAssistantType, async def _async_setup_device_registry(hass: HomeAssistantType, entry: ConfigEntry, - device_info: 'DeviceInfo'): + device_info: DeviceInfo): """Set up device registry feature for a particular config entry.""" sw_version = device_info.esphome_core_version if device_info.compilation_time: @@ -371,7 +257,7 @@ async def _async_setup_device_registry(hass: HomeAssistantType, async def _register_service(hass: HomeAssistantType, entry_data: RuntimeEntryData, - service: 'UserService'): + service: UserService): service_name = '{}_{}'.format(entry_data.device_info.name, service.name) schema = {} for arg in service.args: @@ -391,7 +277,7 @@ async def _register_service(hass: HomeAssistantType, async def _setup_services(hass: HomeAssistantType, entry_data: RuntimeEntryData, - services: List['UserService']): + services: List[UserService]): old_services = entry_data.services.copy() to_unregister = [] to_register = [] @@ -424,7 +310,7 @@ async def _setup_services(hass: HomeAssistantType, async def _cleanup_instance(hass: HomeAssistantType, entry: ConfigEntry) -> None: """Cleanup the esphome client if it exists.""" - data = hass.data[DOMAIN].pop(entry.entry_id) # type: RuntimeEntryData + data = hass.data[DATA_KEY].pop(entry.entry_id) # type: RuntimeEntryData if data.reconnect_task is not None: data.reconnect_task.cancel() for disconnect_cb in data.disconnect_callbacks: @@ -467,7 +353,7 @@ async def platform_async_setup_entry(hass: HomeAssistantType, entry_data.state[component_key] = {} @callback - def async_list_entities(infos: List['EntityInfo']): + def async_list_entities(infos: List[EntityInfo]): """Update entities of this platform when entities are listed.""" old_infos = entry_data.info[component_key] new_infos = {} @@ -498,7 +384,7 @@ async def platform_async_setup_entry(hass: HomeAssistantType, ) @callback - def async_entity_state(state: 'EntityState'): + def async_entity_state(state: EntityState): """Notify the appropriate entity of an updated state.""" if not isinstance(state, state_type): return @@ -519,6 +405,7 @@ def esphome_state_property(func): """ @property def _wrapper(self): + # pylint: disable=protected-access if self._state is None: return None val = func(self) @@ -603,22 +490,22 @@ class EsphomeEntity(Entity): @property def _entry_data(self) -> RuntimeEntryData: - return self.hass.data[DOMAIN][self._entry_id] + return self.hass.data[DATA_KEY][self._entry_id] @property - def _static_info(self) -> 'EntityInfo': + def _static_info(self) -> EntityInfo: return self._entry_data.info[self._component_key][self._key] @property - def _device_info(self) -> 'DeviceInfo': + def _device_info(self) -> DeviceInfo: return self._entry_data.device_info @property - def _client(self) -> 'APIClient': + def _client(self) -> APIClient: return self._entry_data.client @property - def _state(self) -> 'Optional[EntityState]': + def _state(self) -> Optional[EntityState]: try: return self._entry_data.state[self._component_key][self._key] except KeyError: diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py index f2344e40b2a..283d09e7919 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -7,6 +7,8 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.helpers import ConfigType +from .entry_data import DATA_KEY, RuntimeEntryData + @config_entries.HANDLERS.register('esphome') class EsphomeFlowHandler(config_entries.ConfigFlow): @@ -76,10 +78,25 @@ class EsphomeFlowHandler(config_entries.ConfigFlow): async def async_step_zeroconf(self, user_input: ConfigType): """Handle zeroconf discovery.""" - address = user_input['properties'].get( - 'address', user_input['hostname'][:-1]) + # Hostname is format: livingroom.local. + local_name = user_input['hostname'][:-1] + node_name = local_name[:-len('.local')] + address = user_input['properties'].get('address', local_name) + + # Check if already configured for entry in self._async_current_entries(): + already_configured = False if entry.data['host'] == address: + # Is this address already configured? + already_configured = True + elif entry.entry_id in self.hass.data.get(DATA_KEY, {}): + # Does a config entry with this name already exist? + data = self.hass.data[DATA_KEY][ + entry.entry_id] # type: RuntimeEntryData + # Node names are unique in the network + already_configured = data.device_info.name == node_name + + if already_configured: return self.async_abort( reason='already_configured' ) diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py new file mode 100644 index 00000000000..47cadc00653 --- /dev/null +++ b/homeassistant/components/esphome/entry_data.py @@ -0,0 +1,107 @@ +"""Runtime entry data for ESPHome stored in hass.data.""" +import asyncio +from typing import Any, Callable, Dict, List, Optional, Tuple + +from aioesphomeapi import ( + COMPONENT_TYPE_TO_INFO, DeviceInfo, EntityInfo, EntityState, UserService) +import attr + +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.storage import Store +from homeassistant.helpers.typing import HomeAssistantType + +DATA_KEY = 'esphome' +DISPATCHER_UPDATE_ENTITY = 'esphome_{entry_id}_update_{component_key}_{key}' +DISPATCHER_REMOVE_ENTITY = 'esphome_{entry_id}_remove_{component_key}_{key}' +DISPATCHER_ON_LIST = 'esphome_{entry_id}_on_list' +DISPATCHER_ON_DEVICE_UPDATE = 'esphome_{entry_id}_on_device_update' +DISPATCHER_ON_STATE = 'esphome_{entry_id}_on_state' + + +@attr.s +class RuntimeEntryData: + """Store runtime data for esphome config entries.""" + + entry_id = attr.ib(type=str) + client = attr.ib(type='APIClient') + store = attr.ib(type=Store) + reconnect_task = attr.ib(type=Optional[asyncio.Task], default=None) + state = attr.ib(type=Dict[str, Dict[str, Any]], factory=dict) + info = attr.ib(type=Dict[str, Dict[str, Any]], factory=dict) + services = attr.ib(type=Dict[int, 'UserService'], factory=dict) + available = attr.ib(type=bool, default=False) + device_info = attr.ib(type=DeviceInfo, default=None) + cleanup_callbacks = attr.ib(type=List[Callable[[], None]], factory=list) + disconnect_callbacks = attr.ib(type=List[Callable[[], None]], factory=list) + + def async_update_entity(self, hass: HomeAssistantType, component_key: str, + key: int) -> None: + """Schedule the update of an entity.""" + signal = DISPATCHER_UPDATE_ENTITY.format( + entry_id=self.entry_id, component_key=component_key, key=key) + async_dispatcher_send(hass, signal) + + def async_remove_entity(self, hass: HomeAssistantType, component_key: str, + key: int) -> None: + """Schedule the removal of an entity.""" + signal = DISPATCHER_REMOVE_ENTITY.format( + entry_id=self.entry_id, component_key=component_key, key=key) + async_dispatcher_send(hass, signal) + + def async_update_static_infos(self, hass: HomeAssistantType, + infos: List[EntityInfo]) -> None: + """Distribute an update of static infos to all platforms.""" + signal = DISPATCHER_ON_LIST.format(entry_id=self.entry_id) + async_dispatcher_send(hass, signal, infos) + + def async_update_state(self, hass: HomeAssistantType, + state: EntityState) -> None: + """Distribute an update of state information to all platforms.""" + signal = DISPATCHER_ON_STATE.format(entry_id=self.entry_id) + async_dispatcher_send(hass, signal, state) + + def async_update_device_state(self, hass: HomeAssistantType) -> None: + """Distribute an update of a core device state like availability.""" + signal = DISPATCHER_ON_DEVICE_UPDATE.format(entry_id=self.entry_id) + async_dispatcher_send(hass, signal) + + async def async_load_from_store(self) -> Tuple[List[EntityInfo], + List[UserService]]: + """Load the retained data from store and return de-serialized data.""" + restored = await self.store.async_load() + if restored is None: + return [], [] + + self.device_info = _attr_obj_from_dict(DeviceInfo, + **restored.pop('device_info')) + infos = [] + for comp_type, restored_infos in restored.items(): + if comp_type not in COMPONENT_TYPE_TO_INFO: + continue + for info in restored_infos: + cls = COMPONENT_TYPE_TO_INFO[comp_type] + infos.append(_attr_obj_from_dict(cls, **info)) + services = [] + for service in restored.get('services', []): + services.append(UserService.from_dict(service)) + return infos, services + + async def async_save_to_store(self) -> None: + """Generate dynamic data to store and save it to the filesystem.""" + store_data = { + 'device_info': attr.asdict(self.device_info), + 'services': [] + } + + for comp_type, infos in self.info.items(): + store_data[comp_type] = [attr.asdict(info) + for info in infos.values()] + for service in self.services.values(): + store_data['services'].append(service.to_dict()) + + await self.store.async_save(store_data) + + +def _attr_obj_from_dict(cls, **kwargs): + return cls(**{key: kwargs[key] for key in attr.fields_dict(cls) + if key in kwargs}) diff --git a/tests/components/esphome/test_config_flow.py b/tests/components/esphome/test_config_flow.py index 5aeb9d1c045..f991c36c4f0 100644 --- a/tests/components/esphome/test_config_flow.py +++ b/tests/components/esphome/test_config_flow.py @@ -4,7 +4,7 @@ from unittest.mock import MagicMock, patch import pytest -from homeassistant.components.esphome import config_flow +from homeassistant.components.esphome import config_flow, DATA_KEY from tests.common import mock_coro, MockConfigEntry MockDeviceInfo = namedtuple("DeviceInfo", ["uses_password", "name"]) @@ -254,3 +254,30 @@ async def test_discovery_already_configured_ip(hass, mock_client): result = await flow.async_step_zeroconf(user_input=service_info) assert result['type'] == 'abort' assert result['reason'] == 'already_configured' + + +async def test_discovery_already_configured_name(hass, mock_client): + """Test discovery aborts if already configured via name.""" + entry = MockConfigEntry( + domain='esphome', + data={'host': '192.168.43.183', 'port': 6053, 'password': ''} + ) + entry.add_to_hass(hass) + mock_entry_data = MagicMock() + mock_entry_data.device_info.name = 'test8266' + hass.data[DATA_KEY] = { + entry.entry_id: mock_entry_data, + } + + flow = _setup_flow_handler(hass) + service_info = { + 'host': '192.168.43.183', + 'port': 6053, + 'hostname': 'test8266.local.', + 'properties': { + "address": "test8266.local" + } + } + result = await flow.async_step_zeroconf(user_input=service_info) + assert result['type'] == 'abort' + assert result['reason'] == 'already_configured' From 4ca588deaeea460c21661273eaa5ee7ab606d303 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Thu, 30 May 2019 17:40:38 +0100 Subject: [PATCH 185/232] homekit_controller no longer logs with transient network errors causing crypto failures as it will auto recover (#24193) --- homeassistant/components/homekit_controller/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index 1fcbddbb400..f1ddf1faacf 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -95,7 +95,8 @@ class HomeKitEntity(Entity): """Obtain a HomeKit device's state.""" # pylint: disable=import-error from homekit.exceptions import ( - AccessoryDisconnectedError, AccessoryNotFoundError) + AccessoryDisconnectedError, AccessoryNotFoundError, + EncryptionError) try: new_values_dict = await self._accessory.get_characteristics( @@ -106,7 +107,7 @@ class HomeKitEntity(Entity): # visible on the network. self._available = False return - except AccessoryDisconnectedError: + except (AccessoryDisconnectedError, EncryptionError): # Temporary connection failure. Device is still available but our # connection was dropped. return From 84719d944aa3f6056f42647b9456a72f1a3486c2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 30 May 2019 09:49:21 -0700 Subject: [PATCH 186/232] Update hass-nabucasa (#24197) --- homeassistant/components/cloud/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/cloud/manifest.json b/homeassistant/components/cloud/manifest.json index 863e3e86da4..982b51133a5 100644 --- a/homeassistant/components/cloud/manifest.json +++ b/homeassistant/components/cloud/manifest.json @@ -3,7 +3,7 @@ "name": "Cloud", "documentation": "https://www.home-assistant.io/components/cloud", "requirements": [ - "hass-nabucasa==0.12" + "hass-nabucasa==0.13" ], "dependencies": [ "http", diff --git a/requirements_all.txt b/requirements_all.txt index cfa7122b1b7..2be17312b34 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -547,7 +547,7 @@ habitipy==0.2.0 hangups==0.4.9 # homeassistant.components.cloud -hass-nabucasa==0.12 +hass-nabucasa==0.13 # homeassistant.components.mqtt hbmqtt==0.9.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 00707961ec6..c9ed5bc00ba 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -136,7 +136,7 @@ ha-ffmpeg==2.0 hangups==0.4.9 # homeassistant.components.cloud -hass-nabucasa==0.12 +hass-nabucasa==0.13 # homeassistant.components.mqtt hbmqtt==0.9.4 From 1ad495070dd8c9fe4bb93458b66c1f5a1ea2f8a0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 30 May 2019 14:59:14 -0700 Subject: [PATCH 187/232] Bumped version to 0.94.0b1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index f00a3d5c9d7..a74d6bf7ece 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 94 -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 bfc8d2457c6114e4cffd0f9a0847299aa5d03098 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 31 May 2019 10:53:34 +0200 Subject: [PATCH 188/232] Update azure-pipelines.yml for Azure Pipelines --- azure-pipelines.yml | 56 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 48 insertions(+), 8 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 2f5792cfea6..fc511615b60 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -16,6 +16,7 @@ variables: - group: docker - group: wheels - group: github + - group: twine jobs: @@ -24,7 +25,7 @@ jobs: condition: eq(variables['Build.SourceBranchName'], 'dev') timeoutInMinutes: 360 pool: - vmImage: 'ubuntu-16.04' + vmImage: 'ubuntu-latest' strategy: maxParallel: 3 matrix: @@ -114,15 +115,53 @@ jobs: exit 1 fi displayName: 'Check version of branch/tag' + - script: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends \ + jq curl + + release="$(Build.SourceBranchName)" + created_by="$(curl -s https://api.github.com/repos/home-assistant/home-assistant/releases/tags/${release} | jq --raw-output '.author.login')" + + if [[ "${created_by}" =~ ^(balloob|pvizeli|fabaff|robbiet480)$ ]]; then + exit 0 + fi + + echo "${created_by} is not allowed to create an release!" + exit 1 + displayName: 'Check rights' -- job: 'Release' +- job: 'ReleasePython' + condition: and(startsWith(variables['Build.SourceBranch'], 'refs/tags'), succeeded('VersionValidate')) + dependsOn: + - 'VersionValidate' + pool: + vmImage: 'ubuntu-latest' + steps: + - task: UsePythonVersion@0 + displayName: 'Use Python 3.7' + inputs: + versionSpec: '3.7' + - script: pip install twine + displayName: 'Install twine' + - script: python setup.py sdist bdist_wheel + displayName: 'Build package' + - script: | + export TWINE_USERNAME="$(twineUser)" + export TWINE_PASSWORD="$(twinePassword)" + + twine upload dist/* --skip-existing + displayName: 'Upload pypi' + + +- job: 'ReleaseDocker' condition: and(startsWith(variables['Build.SourceBranch'], 'refs/tags'), succeeded('VersionValidate')) dependsOn: - 'VersionValidate' timeoutInMinutes: 120 pool: - vmImage: 'ubuntu-16.04' + vmImage: 'ubuntu-latest' strategy: maxParallel: 5 matrix: @@ -167,16 +206,17 @@ jobs: displayName: 'Build Release' -- job: 'ReleasePublish' - condition: and(startsWith(variables['Build.SourceBranch'], 'refs/tags'), succeeded('Release')) +- job: 'ReleaseHassio' + condition: and(startsWith(variables['Build.SourceBranch'], 'refs/tags'), succeeded('ReleaseDocker')) dependsOn: - - 'Release' + - 'ReleaseDocker' pool: - vmImage: 'ubuntu-16.04' + vmImage: 'ubuntu-latest' steps: - script: | + sudo apt-get update sudo apt-get install -y --no-install-recommends \ - git jq + git jq curl git config --global user.name "Pascal Vizeli" git config --global user.email "pvizeli@syshack.ch" From 9be1b72ed78f6e7d3a316c0e728d14d8a216ba83 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Fri, 31 May 2019 11:27:27 +0200 Subject: [PATCH 189/232] Fix ESPHome config flow with invalid config entry (#24213) --- homeassistant/components/esphome/config_flow.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py index 283d09e7919..ad18e681021 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -94,7 +94,8 @@ class EsphomeFlowHandler(config_entries.ConfigFlow): data = self.hass.data[DATA_KEY][ entry.entry_id] # type: RuntimeEntryData # Node names are unique in the network - already_configured = data.device_info.name == node_name + if data.device_info is not None: + already_configured = data.device_info.name == node_name if already_configured: return self.async_abort( From 4fa6f2e54f2406db1e68d3df70db6691b8e45d42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Herv=C3=A9?= Date: Fri, 31 May 2019 09:17:50 +0200 Subject: [PATCH 190/232] Bump oauthlib version (#24111) * Bump oauthlib version * Bump fitbit instead * Update requirements --- homeassistant/components/fitbit/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/fitbit/manifest.json b/homeassistant/components/fitbit/manifest.json index baf0d8aaed1..6a6316d80a3 100644 --- a/homeassistant/components/fitbit/manifest.json +++ b/homeassistant/components/fitbit/manifest.json @@ -3,7 +3,7 @@ "name": "Fitbit", "documentation": "https://www.home-assistant.io/components/fitbit", "requirements": [ - "fitbit==0.3.0" + "fitbit==0.3.1" ], "dependencies": [ "configurator", diff --git a/requirements_all.txt b/requirements_all.txt index 2be17312b34..1a12b793d87 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -453,7 +453,7 @@ fiblary3==0.1.7 fints==1.0.1 # homeassistant.components.fitbit -fitbit==0.3.0 +fitbit==0.3.1 # homeassistant.components.fixer fixerio==1.0.0a0 From 16edcd99389b24adf0f30b9b3dff11cc1cfd45fb Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 30 May 2019 14:08:05 -0700 Subject: [PATCH 191/232] Allow discovery flows to be discovered via zeroconf/ssdp (#24199) --- homeassistant/helpers/config_entry_flow.py | 3 +++ tests/helpers/test_config_entry_flow.py | 12 +++++++----- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/homeassistant/helpers/config_entry_flow.py b/homeassistant/helpers/config_entry_flow.py index 6d200a39c85..7c087a1ee64 100644 --- a/homeassistant/helpers/config_entry_flow.py +++ b/homeassistant/helpers/config_entry_flow.py @@ -81,6 +81,9 @@ class DiscoveryFlowHandler(config_entries.ConfigFlow): return await self.async_step_confirm() + async_step_zeroconf = async_step_discovery + async_step_ssdp = async_step_discovery + async def async_step_import(self, _): """Handle a flow initialized by import.""" if self._async_in_progress() or self._async_current_entries(): diff --git a/tests/helpers/test_config_entry_flow.py b/tests/helpers/test_config_entry_flow.py index 5f8a642333a..eda62e1614c 100644 --- a/tests/helpers/test_config_entry_flow.py +++ b/tests/helpers/test_config_entry_flow.py @@ -75,24 +75,26 @@ async def test_user_has_confirmation(hass, discovery_flow_conf): assert result['type'] == data_entry_flow.RESULT_TYPE_FORM -async def test_discovery_single_instance(hass, discovery_flow_conf): - """Test we ask for confirmation via discovery.""" +@pytest.mark.parametrize('source', ['discovery', 'ssdp', 'zeroconf']) +async def test_discovery_single_instance(hass, discovery_flow_conf, source): + """Test we not allow duplicates.""" flow = config_entries.HANDLERS['test']() flow.hass = hass MockConfigEntry(domain='test').add_to_hass(hass) - result = await flow.async_step_discovery({}) + result = await getattr(flow, "async_step_{}".format(source))({}) assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT assert result['reason'] == 'single_instance_allowed' -async def test_discovery_confirmation(hass, discovery_flow_conf): +@pytest.mark.parametrize('source', ['discovery', 'ssdp', 'zeroconf']) +async def test_discovery_confirmation(hass, discovery_flow_conf, source): """Test we ask for confirmation via discovery.""" flow = config_entries.HANDLERS['test']() flow.hass = hass - result = await flow.async_step_discovery({}) + result = await getattr(flow, "async_step_{}".format(source))({}) assert result['type'] == data_entry_flow.RESULT_TYPE_FORM assert result['step_id'] == 'confirm' From 052641e620e151f6b7af142012769ea18e09fb83 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 31 May 2019 11:26:05 -0700 Subject: [PATCH 192/232] Instantiate lock inside event loop (#24203) --- homeassistant/helpers/entity_platform.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 7908440e92b..30868c33f9d 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -45,7 +45,7 @@ class EntityPlatform: self._async_unsub_polling = None # Method to cancel the retry of setup self._async_cancel_retry_setup = None - self._process_updates = asyncio.Lock() + self._process_updates = None # Platform is None for the EntityComponent "catch-all" EntityPlatform # which powers entity_component.add_entities @@ -404,6 +404,8 @@ class EntityPlatform: This method must be run in the event loop. """ + if self._process_updates is None: + self._process_updates = asyncio.Lock() if self._process_updates.locked(): self.logger.warning( "Updating %s %s took longer than the scheduled update " From 6371eca14d4f187216dce1dd3acce90d34ef2594 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 30 May 2019 16:23:42 -0700 Subject: [PATCH 193/232] Improve error handling (#24204) --- homeassistant/components/ssdp/__init__.py | 6 ++--- tests/components/ssdp/test_init.py | 29 +++++++++++++++++++++++ 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ssdp/__init__.py b/homeassistant/components/ssdp/__init__.py index af24dd22a89..aecca614e73 100644 --- a/homeassistant/components/ssdp/__init__.py +++ b/homeassistant/components/ssdp/__init__.py @@ -135,15 +135,15 @@ class Scanner: if not xml: resp = await session.get(xml_location, timeout=5) xml = await resp.text() - except aiohttp.ClientError as err: + except (aiohttp.ClientError, asyncio.TimeoutError) as err: _LOGGER.debug("Error fetching %s: %s", xml_location, err) - return None + return {} try: tree = ElementTree.fromstring(xml) except ElementTree.ParseError as err: _LOGGER.debug("Error parsing %s: %s", xml_location, err) - return None + return {} return util.etree_to_dict(tree).get('root', {}).get('device', {}) diff --git a/tests/components/ssdp/test_init.py b/tests/components/ssdp/test_init.py index 7ded5f12329..4b1e27d2dc8 100644 --- a/tests/components/ssdp/test_init.py +++ b/tests/components/ssdp/test_init.py @@ -1,6 +1,10 @@ """Test the SSDP integration.""" +import asyncio from unittest.mock import patch, Mock +import aiohttp +import pytest + from homeassistant.generated import ssdp as gn_ssdp from homeassistant.components import ssdp @@ -76,3 +80,28 @@ async def test_scan_match_device_type(hass, aioclient_mock): assert len(mock_init.mock_calls) == 1 assert mock_init.mock_calls[0][1][0] == 'mock-domain' assert mock_init.mock_calls[0][2]['context'] == {'source': 'ssdp'} + + +@pytest.mark.parametrize('exc', [asyncio.TimeoutError, aiohttp.ClientError]) +async def test_scan_description_fetch_fail(hass, aioclient_mock, exc): + """Test failing to fetch description.""" + aioclient_mock.get('http://1.1.1.1', exc=exc) + scanner = ssdp.Scanner(hass) + + with patch('netdisco.ssdp.scan', return_value=[ + Mock(st="mock-st", location='http://1.1.1.1') + ]): + await scanner.async_scan(None) + + +async def test_scan_description_parse_fail(hass, aioclient_mock): + """Test invalid XML.""" + aioclient_mock.get('http://1.1.1.1', text=""" +INVALIDXML + """) + scanner = ssdp.Scanner(hass) + + with patch('netdisco.ssdp.scan', return_value=[ + Mock(st="mock-st", location='http://1.1.1.1') + ]): + await scanner.async_scan(None) From 46cc6e199b74572b2298a8a8c34d273b9947d657 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 31 May 2019 20:34:06 +0200 Subject: [PATCH 194/232] Axis - Handle Vapix error messages (#24215) --- homeassistant/components/axis/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/axis/manifest.json b/homeassistant/components/axis/manifest.json index 27c108b334c..dc64e90ba9a 100644 --- a/homeassistant/components/axis/manifest.json +++ b/homeassistant/components/axis/manifest.json @@ -3,7 +3,7 @@ "name": "Axis", "config_flow": true, "documentation": "https://www.home-assistant.io/components/axis", - "requirements": ["axis==23"], + "requirements": ["axis==24"], "dependencies": [], "zeroconf": ["_axis-video._tcp.local."], "codeowners": ["@kane610"] diff --git a/requirements_all.txt b/requirements_all.txt index 1a12b793d87..25509c7f56c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -212,7 +212,7 @@ av==6.1.2 # avion==0.10 # homeassistant.components.axis -axis==23 +axis==24 # homeassistant.components.azure_event_hub azure-eventhub==1.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c9ed5bc00ba..d03208c9a1d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -70,7 +70,7 @@ apns2==0.3.0 av==6.1.2 # homeassistant.components.axis -axis==23 +axis==24 # homeassistant.components.zha bellows-homeassistant==0.7.3 From 35f57842875cfa3fdc561887de1f3fa4b4cc3802 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 31 May 2019 14:30:58 +0200 Subject: [PATCH 195/232] Don't follow redirect on ingress itself (#24218) * Don't follow redirect on ingress itself * Fix comment --- homeassistant/components/hassio/ingress.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hassio/ingress.py b/homeassistant/components/hassio/ingress.py index 824dee86fad..250d50681dc 100644 --- a/homeassistant/components/hassio/ingress.py +++ b/homeassistant/components/hassio/ingress.py @@ -119,8 +119,12 @@ class HassIOIngress(HomeAssistantView): source_header = _init_header(request, token) async with self._websession.request( - request.method, url, headers=source_header, - params=request.query, data=data + request.method, + url, + headers=source_header, + params=request.query, + allow_redirects=False, + data=data ) as result: headers = _response_header(result) From 52e33c2aa2423c0dcc1df49cc721309c53d9c2d2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 31 May 2019 11:27:05 -0700 Subject: [PATCH 196/232] Use resource for index routing. (#24223) --- homeassistant/components/frontend/__init__.py | 105 +++++++++++------- tests/components/frontend/test_init.py | 43 +------ 2 files changed, 66 insertions(+), 82 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 8a692d6f272..a18ed6eb3d1 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -4,9 +4,10 @@ import logging import os import pathlib -from aiohttp import web +from aiohttp import web, web_urldispatcher, hdrs import voluptuous as vol import jinja2 +from yarl import URL import homeassistant.helpers.config_validation as cv from homeassistant.components.http.view import HomeAssistantView @@ -50,7 +51,6 @@ for size in (192, 384, 512, 1024): 'type': 'image/png' }) -DATA_FINALIZE_PANEL = 'frontend_finalize_panel' DATA_PANELS = 'frontend_panels' DATA_JS_VERSION = 'frontend_js_version' DATA_EXTRA_HTML_URL = 'frontend_extra_html_url' @@ -97,28 +97,6 @@ SCHEMA_GET_TRANSLATIONS = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ }) -def generate_negative_index_regex(): - """Generate regex for index.""" - skip = [ - # files - "service_worker.js", - "robots.txt", - "onboarding.html", - "manifest.json", - ] - for folder in ( - "static", - "frontend_latest", - "frontend_es5", - "local", - "auth", - "api", - ): - # Regex matching static, static/, static/index.html - skip.append("{}(/|/.+|)".format(folder)) - return r"(?!(" + "|".join(skip) + r")).*" - - class Panel: """Abstract class for panels.""" @@ -256,7 +234,7 @@ async def async_setup(hass, config): if os.path.isdir(local): hass.http.register_static_path("/local", local, not is_dev) - hass.http.register_view(IndexView(repo_path)) + hass.http.app.router.register_resource(IndexView(repo_path, hass)) for panel in ('kiosk', 'states', 'profile'): async_register_built_in_panel(hass, panel) @@ -327,21 +305,64 @@ def _async_setup_themes(hass, themes): hass.services.async_register(DOMAIN, SERVICE_RELOAD_THEMES, reload_themes) -class IndexView(HomeAssistantView): +class IndexView(web_urldispatcher.AbstractResource): """Serve the frontend.""" - url = '/' - name = 'frontend:index' - requires_auth = False - extra_urls = [ - "/{extra:%s}" % generate_negative_index_regex() - ] - - def __init__(self, repo_path): + def __init__(self, repo_path, hass): """Initialize the frontend view.""" + super().__init__(name="frontend:index") self.repo_path = repo_path + self.hass = hass self._template_cache = None + @property + def canonical(self) -> str: + """Return resource's canonical path.""" + return '/' + + @property + def _route(self): + """Return the index route.""" + return web_urldispatcher.ResourceRoute('GET', self.get, self) + + def url_for(self, **kwargs: str) -> URL: + """Construct url for resource with additional params.""" + return URL("/") + + async def resolve(self, request: web.Request): + """Resolve resource. + + Return (UrlMappingMatchInfo, allowed_methods) pair. + """ + if (request.path != '/' and + request.url.parts[1] not in self.hass.data[DATA_PANELS]): + return None, set() + + if request.method != hdrs.METH_GET: + return None, {'GET'} + + return web_urldispatcher.UrlMappingMatchInfo({}, self._route), {'GET'} + + def add_prefix(self, prefix: str) -> None: + """Add a prefix to processed URLs. + + Required for subapplications support. + """ + + def get_info(self): + """Return a dict with additional info useful for introspection.""" + return { + 'panels': list(self.hass.data[DATA_PANELS]) + } + + def freeze(self) -> None: + """Freeze the resource.""" + pass + + def raw_match(self, path: str) -> bool: + """Perform a raw match against path.""" + pass + def get_template(self): """Get template.""" tpl = self._template_cache @@ -357,14 +378,10 @@ class IndexView(HomeAssistantView): return tpl - async def get(self, request, extra=None): - """Serve the index view.""" + async def get(self, request: web.Request): + """Serve the index page for panel pages.""" hass = request.app['hass'] - if (request.path != '/' and - request.url.parts[1] not in hass.data[DATA_PANELS]): - raise web.HTTPNotFound - if not hass.components.onboarding.async_is_onboarded(): return web.Response(status=302, headers={ 'location': '/onboarding.html' @@ -383,6 +400,14 @@ class IndexView(HomeAssistantView): content_type='text/html' ) + def __len__(self) -> int: + """Return length of resource.""" + return 1 + + def __iter__(self): + """Iterate over routes.""" + return iter([self._route]) + class ManifestJSONView(HomeAssistantView): """View to return a manifest.json.""" diff --git a/tests/components/frontend/test_init.py b/tests/components/frontend/test_init.py index 09628b5d3fc..c362499db15 100644 --- a/tests/components/frontend/test_init.py +++ b/tests/components/frontend/test_init.py @@ -8,8 +8,7 @@ import pytest from homeassistant.setup import async_setup_component from homeassistant.components.frontend import ( DOMAIN, CONF_JS_VERSION, CONF_THEMES, CONF_EXTRA_HTML_URL, - CONF_EXTRA_HTML_URL_ES5, generate_negative_index_regex, - EVENT_PANELS_UPDATED) + CONF_EXTRA_HTML_URL_ES5, EVENT_PANELS_UPDATED) from homeassistant.components.websocket_api.const import TYPE_RESULT from tests.common import mock_coro, async_capture_events @@ -348,43 +347,3 @@ async def test_auth_authorize(mock_http_client): resp = await mock_http_client.get(authorizejs.groups(0)[0]) assert resp.status == 200 assert 'public' in resp.headers.get('cache-control') - - -def test_index_regex(): - """Test the index regex.""" - pattern = re.compile('/' + generate_negative_index_regex()) - - for should_match in ( - '/', - '/lovelace', - '/lovelace/default_view', - '/map', - '/config', - ): - assert pattern.match(should_match), should_match - - for should_not_match in ( - '/service_worker.js', - '/manifest.json', - '/onboarding.html', - '/manifest.json', - 'static', - 'static/', - 'static/index.html', - 'frontend_latest', - 'frontend_latest/', - 'frontend_latest/index.html', - 'frontend_es5', - 'frontend_es5/', - 'frontend_es5/index.html', - 'local', - 'local/', - 'local/index.html', - 'auth', - 'auth/', - 'auth/index.html', - '/api', - '/api/', - '/api/logbook', - ): - assert not pattern.match(should_not_match), should_not_match From 3eeccc1a653e3cec731c584e9bf013ae8a78f0c6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 31 May 2019 11:58:48 -0700 Subject: [PATCH 197/232] Add manifest support for homekit discovery (#24225) * Add manifest support for homekit discovery * Add a space after model check * Update comment --- homeassistant/components/lifx/manifest.json | 5 ++ homeassistant/components/zeroconf/__init__.py | 60 ++++++++++++--- homeassistant/generated/zeroconf.py | 4 + homeassistant/helpers/config_entry_flow.py | 1 + script/hassfest/manifest.py | 3 + script/hassfest/zeroconf.py | 74 ++++++++++++++++--- tests/components/zeroconf/test_init.py | 54 +++++++++++--- 7 files changed, 169 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/lifx/manifest.json b/homeassistant/components/lifx/manifest.json index ca9b578432b..fd74d9831fc 100644 --- a/homeassistant/components/lifx/manifest.json +++ b/homeassistant/components/lifx/manifest.json @@ -7,6 +7,11 @@ "aiolifx==0.6.7", "aiolifx_effects==0.2.2" ], + "homekit": { + "models": [ + "LIFX" + ] + }, "dependencies": [], "codeowners": [ "@amelchio" diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index fe757b8ae85..2f93020b4d5 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -10,7 +10,7 @@ import voluptuous as vol from zeroconf import ServiceBrowser, ServiceInfo, ServiceStateChange, Zeroconf from homeassistant.const import (EVENT_HOMEASSISTANT_STOP, __version__) -from homeassistant.generated.zeroconf import ZEROCONF +from homeassistant.generated.zeroconf import ZEROCONF, HOMEKIT _LOGGER = logging.getLogger(__name__) @@ -24,6 +24,7 @@ ATTR_NAME = 'name' ATTR_PROPERTIES = 'properties' ZEROCONF_TYPE = '_home-assistant._tcp.local.' +HOMEKIT_TYPE = '_hap._tcp.local.' CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({}), @@ -50,21 +51,30 @@ def setup(hass, config): def service_update(zeroconf, service_type, name, state_change): """Service state changed.""" - if state_change is ServiceStateChange.Added: - service_info = zeroconf.get_service_info(service_type, name) - info = info_from_service(service_info) - _LOGGER.debug("Discovered new device %s %s", name, info) + if state_change != ServiceStateChange.Added: + return - for domain in ZEROCONF[service_type]: - hass.add_job( - hass.config_entries.flow.async_init( - domain, context={'source': DOMAIN}, data=info - ) + service_info = zeroconf.get_service_info(service_type, name) + info = info_from_service(service_info) + _LOGGER.debug("Discovered new device %s %s", name, info) + + # If we can handle it as a HomeKit discovery, we do that here. + if service_type == HOMEKIT_TYPE and handle_homekit(hass, info): + return + + for domain in ZEROCONF[service_type]: + hass.add_job( + hass.config_entries.flow.async_init( + domain, context={'source': DOMAIN}, data=info ) + ) for service in ZEROCONF: ServiceBrowser(zeroconf, service, handlers=[service_update]) + if HOMEKIT_TYPE not in ZEROCONF: + ServiceBrowser(zeroconf, HOMEKIT_TYPE, handlers=[service_update]) + def stop_zeroconf(_): """Stop Zeroconf.""" zeroconf.unregister_service(info) @@ -75,6 +85,36 @@ def setup(hass, config): return True +def handle_homekit(hass, info) -> bool: + """Handle a HomeKit discovery. + + Return if discovery was forwarded. + """ + model = None + props = info.get('properties', {}) + + for key in props: + if key.lower() == 'md': + model = props[key] + break + + if model is None: + return False + + for test_model in HOMEKIT: + if not model.startswith(test_model): + continue + + hass.add_job( + hass.config_entries.flow.async_init( + HOMEKIT[test_model], context={'source': 'homekit'}, data=info + ) + ) + return True + + return False + + def info_from_service(service): """Return prepared info from mDNS entries.""" properties = {} diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index 05b0a0247b9..024bb89dc99 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -18,3 +18,7 @@ ZEROCONF = { "homekit_controller" ] } + +HOMEKIT = { + "LIFX ": "lifx" +} diff --git a/homeassistant/helpers/config_entry_flow.py b/homeassistant/helpers/config_entry_flow.py index 7c087a1ee64..c3e5195131b 100644 --- a/homeassistant/helpers/config_entry_flow.py +++ b/homeassistant/helpers/config_entry_flow.py @@ -83,6 +83,7 @@ class DiscoveryFlowHandler(config_entries.ConfigFlow): async_step_zeroconf = async_step_discovery async_step_ssdp = async_step_discovery + async_step_homekit = async_step_discovery async def async_step_import(self, _): """Handle a flow initialized by import.""" diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index 31181ed76bd..3e25ab31712 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -17,6 +17,9 @@ MANIFEST_SCHEMA = vol.Schema({ vol.Optional('manufacturer'): [str], vol.Optional('device_type'): [str], }), + vol.Optional('homekit'): vol.Schema({ + vol.Optional('models'): [str], + }), vol.Required('documentation'): str, vol.Required('requirements'): [str], vol.Required('dependencies'): [str], diff --git a/script/hassfest/zeroconf.py b/script/hassfest/zeroconf.py index 1ed9575c95f..25e8da99b55 100644 --- a/script/hassfest/zeroconf.py +++ b/script/hassfest/zeroconf.py @@ -1,5 +1,5 @@ """Generate zeroconf file.""" -from collections import OrderedDict +from collections import OrderedDict, defaultdict import json from typing import Dict @@ -13,12 +13,15 @@ To update, run python3 -m hassfest ZEROCONF = {} + +HOMEKIT = {} """.strip() def generate_and_validate(integrations: Dict[str, Integration]): """Validate and generate zeroconf data.""" - service_type_dict = {} + service_type_dict = defaultdict(list) + homekit_dict = {} for domain in sorted(integrations): integration = integrations[domain] @@ -26,17 +29,30 @@ def generate_and_validate(integrations: Dict[str, Integration]): if not integration.manifest: continue - service_types = integration.manifest.get('zeroconf') + service_types = integration.manifest.get('zeroconf', []) + homekit = integration.manifest.get('homekit', {}) + homekit_models = homekit.get('models', []) - if not service_types: + if not service_types and not homekit_models: continue try: with open(str(integration.path / "config_flow.py")) as fp: - if ' async_step_zeroconf(' not in fp.read(): + content = fp.read() + uses_discovery_flow = 'register_discovery_flow' in content + + if (service_types and not uses_discovery_flow and + ' async_step_zeroconf(' not in content): integration.add_error( 'zeroconf', 'Config flow has no async_step_zeroconf') continue + + if (homekit_models and not uses_discovery_flow and + ' async_step_homekit(' not in content): + integration.add_error( + 'zeroconf', 'Config flow has no async_step_homekit') + continue + except FileNotFoundError: integration.add_error( 'zeroconf', @@ -45,16 +61,50 @@ def generate_and_validate(integrations: Dict[str, Integration]): continue for service_type in service_types: - - if service_type not in service_type_dict: - service_type_dict[service_type] = [] - service_type_dict[service_type].append(domain) - data = OrderedDict((key, service_type_dict[key]) - for key in sorted(service_type_dict)) + for model in homekit_models: + # We add a space, as we want to test for it to be model + space. + model += " " - return BASE.format(json.dumps(data, indent=4)) + if model in homekit_dict: + integration.add_error( + 'zeroconf', + 'Integrations {} and {} have overlapping HomeKit ' + 'models'.format(domain, homekit_dict[model])) + break + + homekit_dict[model] = domain + + # HomeKit models are matched on starting string, make sure none overlap. + warned = set() + for key in homekit_dict: + if key in warned: + continue + + # n^2 yoooo + for key_2 in homekit_dict: + if key == key_2 or key_2 in warned: + continue + + if key.startswith(key_2) or key_2.startswith(key): + integration.add_error( + 'zeroconf', + 'Integrations {} and {} have overlapping HomeKit ' + 'models'.format(homekit_dict[key], homekit_dict[key_2])) + warned.add(key) + warned.add(key_2) + break + + zeroconf = OrderedDict((key, service_type_dict[key]) + for key in sorted(service_type_dict)) + homekit = OrderedDict((key, homekit_dict[key]) + for key in sorted(homekit_dict)) + + return BASE.format( + json.dumps(zeroconf, indent=4), + json.dumps(homekit, indent=4), + ) def validate(integrations: Dict[str, Integration], config: Config): diff --git a/tests/components/zeroconf/test_init.py b/tests/components/zeroconf/test_init.py index e7d7756fe7c..27c1dc75749 100644 --- a/tests/components/zeroconf/test_init.py +++ b/tests/components/zeroconf/test_init.py @@ -1,6 +1,7 @@ """Test Zeroconf component setup process.""" from unittest.mock import patch +import pytest from zeroconf import ServiceInfo, ServiceStateChange from homeassistant.generated import zeroconf as zc_gen @@ -8,6 +9,13 @@ from homeassistant.setup import async_setup_component from homeassistant.components import zeroconf +@pytest.fixture +def mock_zeroconf(): + """Mock zeroconf.""" + with patch('homeassistant.components.zeroconf.Zeroconf') as mock_zc: + yield mock_zc.return_value + + def service_update_mock(zeroconf, service, handlers): """Call service update handler.""" handlers[0]( @@ -23,18 +31,44 @@ def get_service_info_mock(service_type, name): properties={b'macaddress': b'ABCDEF012345'}) -async def test_setup(hass): +def get_homekit_info_mock(service_type, name): + """Return homekit info for get_service_info.""" + return ServiceInfo( + service_type, name, address=b'\n\x00\x00\x14', port=80, weight=0, + priority=0, server='name.local.', + properties={b'md': b'LIFX Bulb'}) + + +async def test_setup(hass, mock_zeroconf): """Test configured options for a device are loaded via config entry.""" - with patch.object(hass.config_entries, 'flow') as mock_config_flow, \ - patch.object(zeroconf, 'ServiceBrowser') as MockServiceBrowser, \ - patch.object(zeroconf.Zeroconf, 'get_service_info') as \ - mock_get_service_info: - - MockServiceBrowser.side_effect = service_update_mock - mock_get_service_info.side_effect = get_service_info_mock - + with patch.object( + hass.config_entries, 'flow' + ) as mock_config_flow, patch.object( + zeroconf, 'ServiceBrowser', side_effect=service_update_mock + ) as mock_service_browser: + mock_zeroconf.get_service_info.side_effect = get_service_info_mock assert await async_setup_component( hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) - assert len(MockServiceBrowser.mock_calls) == len(zc_gen.ZEROCONF) + assert len(mock_service_browser.mock_calls) == len(zc_gen.ZEROCONF) assert len(mock_config_flow.mock_calls) == len(zc_gen.ZEROCONF) * 2 + + +async def test_homekit(hass, mock_zeroconf): + """Test configured options for a device are loaded via config entry.""" + with patch.dict( + zc_gen.ZEROCONF, { + zeroconf.HOMEKIT_TYPE: ["homekit_controller"] + }, clear=True + ), patch.object( + hass.config_entries, 'flow' + ) as mock_config_flow, patch.object( + zeroconf, 'ServiceBrowser', side_effect=service_update_mock + ) as mock_service_browser: + mock_zeroconf.get_service_info.side_effect = get_homekit_info_mock + assert await async_setup_component( + hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) + + assert len(mock_service_browser.mock_calls) == 1 + assert len(mock_config_flow.mock_calls) == 2 + assert mock_config_flow.mock_calls[0][1][0] == 'lifx' From 5c8f209aa74af710309c80b6c53fbf37e208c259 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 31 May 2019 13:45:41 -0700 Subject: [PATCH 198/232] Bumped version to 0.94.0b2 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index a74d6bf7ece..8a9dfa40454 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 94 -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 958b894020a6f4225373b0d9f44f04023eec9c44 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 31 May 2019 23:10:09 +0200 Subject: [PATCH 199/232] Update azure-pipelines.yml for Azure Pipelines --- azure-pipelines.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index fc511615b60..ae53e94a9b4 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -143,8 +143,8 @@ jobs: displayName: 'Use Python 3.7' inputs: versionSpec: '3.7' - - script: pip install twine - displayName: 'Install twine' + - script: pip install twine wheel + displayName: 'Install tools' - script: python setup.py sdist bdist_wheel displayName: 'Build package' - script: | From 7d1a02feb167e5ea37685b1565ecf13cc8b1d467 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 31 May 2019 22:59:16 -0700 Subject: [PATCH 200/232] Log HomeKit model (#24229) --- homeassistant/components/homekit_controller/config_flow.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index c7100f3159e..2ce8c0db6b7 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -126,14 +126,16 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow): # It changes if a device is factory reset. hkid = properties['id'] model = properties['md'] - + name = discovery_info['name'].replace('._hap._tcp.local.', '') status_flags = int(properties['sf']) paired = not status_flags & 0x01 + _LOGGER.debug("Discovered device %s (%s - %s)", name, model, hkid) + # pylint: disable=unsupported-assignment-operation self.context['hkid'] = hkid self.context['title_placeholders'] = { - 'name': discovery_info['name'].replace('._hap._tcp.local.', ''), + 'name': name, } # If multiple HomekitControllerFlowHandler end up getting created From 0cdea28e2a1770b33d8eb79e6b226cdfcda40076 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 1 Jun 2019 00:51:55 +0200 Subject: [PATCH 201/232] Don't allow more than one config flow per discovered Axis device (#24230) --- homeassistant/components/axis/config_flow.py | 7 +++++++ homeassistant/components/axis/strings.json | 1 + 2 files changed, 8 insertions(+) diff --git a/homeassistant/components/axis/config_flow.py b/homeassistant/components/axis/config_flow.py index fc2051e4925..2aa5c4de16e 100644 --- a/homeassistant/components/axis/config_flow.py +++ b/homeassistant/components/axis/config_flow.py @@ -155,6 +155,13 @@ class AxisFlowHandler(config_entries.ConfigFlow): return self.async_abort(reason='link_local_address') serialnumber = discovery_info['properties']['macaddress'] + # pylint: disable=unsupported-assignment-operation + self.context['macaddress'] = serialnumber + + if any(serialnumber == flow['context']['macaddress'] + for flow in self._async_in_progress()): + return self.async_abort(reason='already_in_progress') + device_entries = configured_devices(self.hass) if serialnumber in device_entries: diff --git a/homeassistant/components/axis/strings.json b/homeassistant/components/axis/strings.json index 3c528dfbb16..ebefbecf311 100644 --- a/homeassistant/components/axis/strings.json +++ b/homeassistant/components/axis/strings.json @@ -14,6 +14,7 @@ }, "error": { "already_configured": "Device is already configured", + "already_in_progress": "Config flow for device is already in progress.", "device_unavailable": "Device is not available", "faulty_credentials": "Bad user credentials" }, From dc8d4ac8e4b59ebbfd3cbfa49371225d26138074 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 31 May 2019 22:59:35 -0700 Subject: [PATCH 202/232] Add GPSLogger device_info and unique_id (#24231) --- .../components/gpslogger/device_tracker.py | 14 ++++++++ tests/components/gpslogger/test_init.py | 33 +++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/homeassistant/components/gpslogger/device_tracker.py b/homeassistant/components/gpslogger/device_tracker.py index 81a4fb3e7f8..49d421cbc8c 100644 --- a/homeassistant/components/gpslogger/device_tracker.py +++ b/homeassistant/components/gpslogger/device_tracker.py @@ -45,6 +45,7 @@ class GPSLoggerEntity(DeviceTrackerEntity): self._battery = battery self._location = location self._unsub_dispatcher = None + self._unique_id = device @property def battery_level(self): @@ -81,6 +82,19 @@ class GPSLoggerEntity(DeviceTrackerEntity): """No polling needed.""" return False + @property + def unique_id(self): + """Return the unique ID.""" + return self._unique_id + + @property + def device_info(self): + """Return the device info.""" + return { + 'name': self._name, + 'identifiers': {(GPL_DOMAIN, self._unique_id)}, + } + @property def source_type(self): """Return the source type, eg gps or router, of the device.""" diff --git a/tests/components/gpslogger/test_init.py b/tests/components/gpslogger/test_init.py index 2cffa86f393..dbc283895fc 100644 --- a/tests/components/gpslogger/test_init.py +++ b/tests/components/gpslogger/test_init.py @@ -140,6 +140,12 @@ async def test_enter_and_exit(hass, gpslogger_client, webhook_id): data['device'])).state assert STATE_NOT_HOME == state_name + dev_reg = await hass.helpers.device_registry.async_get_registry() + assert len(dev_reg.devices) == 1 + + ent_reg = await hass.helpers.entity_registry.async_get_registry() + assert len(ent_reg.entities) == 1 + async def test_enter_with_attrs(hass, gpslogger_client, webhook_id): """Test when additional attributes are present.""" @@ -172,6 +178,33 @@ async def test_enter_with_attrs(hass, gpslogger_client, webhook_id): assert state.attributes['provider'] == 'gps' assert state.attributes['activity'] == 'running' + data = { + 'latitude': HOME_LATITUDE, + 'longitude': HOME_LONGITUDE, + 'device': '123', + 'accuracy': 123, + 'battery': 23, + 'speed': 23, + 'direction': 123, + 'altitude': 123, + 'provider': 'gps', + 'activity': 'idle' + } + + req = await gpslogger_client.post(url, data=data) + await hass.async_block_till_done() + assert req.status == HTTP_OK + state = hass.states.get('{}.{}'.format(DEVICE_TRACKER_DOMAIN, + data['device'])) + assert state.state == STATE_HOME + assert state.attributes['gps_accuracy'] == 123 + assert state.attributes['battery_level'] == 23 + assert state.attributes['speed'] == 23 + assert state.attributes['direction'] == 123 + assert state.attributes['altitude'] == 123 + assert state.attributes['provider'] == 'gps' + assert state.attributes['activity'] == 'idle' + @pytest.mark.xfail( reason='The device_tracker component does not support unloading yet.' From 362f23a950143586177c4c5c774d3db0f14b1f5f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 31 May 2019 22:59:44 -0700 Subject: [PATCH 203/232] GeoFency unique ID and device info (#24232) --- .../components/geofency/device_tracker.py | 14 ++++++++++++++ tests/components/geofency/test_init.py | 6 ++++++ 2 files changed, 20 insertions(+) diff --git a/homeassistant/components/geofency/device_tracker.py b/homeassistant/components/geofency/device_tracker.py index e340272c966..0c60d5ef2ce 100644 --- a/homeassistant/components/geofency/device_tracker.py +++ b/homeassistant/components/geofency/device_tracker.py @@ -43,6 +43,7 @@ class GeofencyEntity(DeviceTrackerEntity): self._location_name = location_name self._gps = gps self._unsub_dispatcher = None + self._unique_id = device @property def device_state_attributes(self): @@ -74,6 +75,19 @@ class GeofencyEntity(DeviceTrackerEntity): """No polling needed.""" return False + @property + def unique_id(self): + """Return the unique ID.""" + return self._unique_id + + @property + def device_info(self): + """Return the device info.""" + return { + 'name': self._name, + 'identifiers': {(GF_DOMAIN, self._unique_id)}, + } + @property def source_type(self): """Return the source type, eg gps or router, of the device.""" diff --git a/tests/components/geofency/test_init.py b/tests/components/geofency/test_init.py index 718eb259db5..18f119a7539 100644 --- a/tests/components/geofency/test_init.py +++ b/tests/components/geofency/test_init.py @@ -217,6 +217,12 @@ async def test_gps_enter_and_exit_home(hass, geofency_client, webhook_id): 'device_tracker', device_name)).attributes['longitude'] assert NOT_HOME_LONGITUDE == current_longitude + dev_reg = await hass.helpers.device_registry.async_get_registry() + assert len(dev_reg.devices) == 1 + + ent_reg = await hass.helpers.entity_registry.async_get_registry() + assert len(ent_reg.entities) == 1 + async def test_beacon_enter_and_exit_home(hass, geofency_client, webhook_id): """Test iBeacon based zone enter and exit - a.k.a stationary iBeacon.""" From 35ffac1e015c9ee5b160592f87c58e6c6b266a50 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Sat, 1 Jun 2019 02:00:10 -0400 Subject: [PATCH 204/232] add a deprecation warning for tplink device_tracker (#24236) * add a deprecation warning for tplink device_tracker * reword the warning a bit --- homeassistant/components/tplink/device_tracker.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/components/tplink/device_tracker.py b/homeassistant/components/tplink/device_tracker.py index 7b665006a44..b139aed4eea 100644 --- a/homeassistant/components/tplink/device_tracker.py +++ b/homeassistant/components/tplink/device_tracker.py @@ -41,6 +41,12 @@ def get_scanner(hass, config): should be gradually migrated in the pypi package """ + _LOGGER.warning("TP-Link device tracker is unmaintained and will be " + "removed in the future releases if no maintainer is " + "found. If you have interest in this integration, " + "feel free to create a pull request to move this code " + "to a new 'tplink_router' integration and refactoring " + "the device-specific parts to the tplink library") for cls in [ TplinkDeviceScanner, Tplink5DeviceScanner, Tplink4DeviceScanner, Tplink3DeviceScanner, Tplink2DeviceScanner, Tplink1DeviceScanner From fe2e5089abe726fd5fc37975e983b3448058b68f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 31 May 2019 23:01:45 -0700 Subject: [PATCH 205/232] Mobile app to use device tracker config entry (#24238) * Mobile app to use device tracker config entry * Lint * Re-use device_info * Lint --- .../components/mobile_app/__init__.py | 16 +- homeassistant/components/mobile_app/const.py | 2 + .../components/mobile_app/device_tracker.py | 137 ++++++++++++++++++ homeassistant/components/mobile_app/entity.py | 16 +- .../components/mobile_app/helpers.py | 15 +- .../components/mobile_app/manifest.json | 1 - .../components/mobile_app/webhook.py | 49 +------ tests/components/mobile_app/__init__.py | 75 +--------- tests/components/mobile_app/conftest.py | 60 ++++++++ .../mobile_app/test_device_tracker.py | 68 +++++++++ tests/components/mobile_app/test_entity.py | 3 - tests/components/mobile_app/test_http_api.py | 3 +- tests/components/mobile_app/test_webhook.py | 35 ++--- .../mobile_app/test_websocket_api.py | 3 +- 14 files changed, 318 insertions(+), 165 deletions(-) create mode 100644 homeassistant/components/mobile_app/device_tracker.py create mode 100644 tests/components/mobile_app/conftest.py create mode 100644 tests/components/mobile_app/test_device_tracker.py diff --git a/homeassistant/components/mobile_app/__init__.py b/homeassistant/components/mobile_app/__init__.py index abb7bcb7628..839aa8a6c3b 100644 --- a/homeassistant/components/mobile_app/__init__.py +++ b/homeassistant/components/mobile_app/__init__.py @@ -7,13 +7,15 @@ 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, - STORAGE_VERSION) + DATA_DEVICES, DATA_DEVICE_TRACKER, DATA_SENSOR, DATA_STORE, + DOMAIN, STORAGE_KEY, STORAGE_VERSION) from .http_api import RegistrationsView from .webhook import handle_webhook from .websocket_api import register_websocket_handlers +PLATFORMS = 'sensor', 'binary_sensor', 'device_tracker' + async def async_setup(hass: HomeAssistantType, config: ConfigType): """Set up the mobile app component.""" @@ -24,7 +26,6 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType): DATA_BINARY_SENSOR: {}, DATA_CONFIG_ENTRIES: {}, DATA_DELETED_IDS: [], - DATA_DEVICES: {}, DATA_SENSOR: {} } @@ -33,6 +34,7 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType): DATA_CONFIG_ENTRIES: {}, DATA_DELETED_IDS: app_config.get(DATA_DELETED_IDS, []), DATA_DEVICES: {}, + DATA_DEVICE_TRACKER: {}, DATA_SENSOR: app_config.get(DATA_SENSOR, {}), DATA_STORE: store, } @@ -83,10 +85,8 @@ async def async_setup_entry(hass, entry): webhook_register(hass, DOMAIN, registration_name, webhook_id, handle_webhook) - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, - DATA_BINARY_SENSOR)) - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, DATA_SENSOR)) + for domain in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, domain)) return True diff --git a/homeassistant/components/mobile_app/const.py b/homeassistant/components/mobile_app/const.py index 8b33406216e..8cb5aa12731 100644 --- a/homeassistant/components/mobile_app/const.py +++ b/homeassistant/components/mobile_app/const.py @@ -25,6 +25,7 @@ DATA_BINARY_SENSOR = 'binary_sensor' DATA_CONFIG_ENTRIES = 'config_entries' DATA_DELETED_IDS = 'deleted_ids' DATA_DEVICES = 'devices' +DATA_DEVICE_TRACKER = 'device_tracker' DATA_SENSOR = 'sensor' DATA_STORE = 'store' @@ -160,6 +161,7 @@ SENSOR_TYPES = [ATTR_SENSOR_TYPE_BINARY_SENSOR, ATTR_SENSOR_TYPE_SENSOR] COMBINED_CLASSES = sorted(set(BINARY_SENSOR_CLASSES + SENSOR_CLASSES)) SIGNAL_SENSOR_UPDATE = DOMAIN + '_sensor_update' +SIGNAL_LOCATION_UPDATE = DOMAIN + '_location_update_{}' REGISTER_SENSOR_SCHEMA = vol.Schema({ vol.Optional(ATTR_SENSOR_ATTRIBUTES, default={}): dict, diff --git a/homeassistant/components/mobile_app/device_tracker.py b/homeassistant/components/mobile_app/device_tracker.py new file mode 100644 index 00000000000..19aade50876 --- /dev/null +++ b/homeassistant/components/mobile_app/device_tracker.py @@ -0,0 +1,137 @@ +"""Device tracker platform that adds support for OwnTracks over MQTT.""" +import logging + +from homeassistant.core import callback +from homeassistant.components.device_tracker.const import ( + DOMAIN, SOURCE_TYPE_GPS) +from homeassistant.components.device_tracker.config_entry import ( + DeviceTrackerEntity +) +from .const import ( + DOMAIN as MA_DOMAIN, + + ATTR_ALTITUDE, + ATTR_BATTERY, + ATTR_COURSE, + ATTR_DEVICE_ID, + ATTR_DEVICE_NAME, + ATTR_GPS_ACCURACY, + ATTR_GPS, + ATTR_LOCATION_NAME, + ATTR_SPEED, + ATTR_VERTICAL_ACCURACY, + + SIGNAL_LOCATION_UPDATE, +) +from .helpers import device_info + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up OwnTracks based off an entry.""" + @callback + def _receive_data(data): + """Receive set location.""" + dev_id = entry.data[ATTR_DEVICE_ID] + device = hass.data[MA_DOMAIN][DOMAIN].get(dev_id) + + if device is not None: + device.update_data(data) + return + + device = hass.data[MA_DOMAIN][DOMAIN][dev_id] = MobileAppEntity( + entry, data + ) + async_add_entities([device]) + + hass.helpers.dispatcher.async_dispatcher_connect( + SIGNAL_LOCATION_UPDATE.format(entry.entry_id), _receive_data) + return True + + +class MobileAppEntity(DeviceTrackerEntity): + """Represent a tracked device.""" + + def __init__(self, entry, data): + """Set up OwnTracks entity.""" + self._entry = entry + self._data = data + + @property + def unique_id(self): + """Return the unique ID.""" + return self._entry.data[ATTR_DEVICE_ID] + + @property + def battery_level(self): + """Return the battery level of the device.""" + return self._data.get(ATTR_BATTERY) + + @property + def device_state_attributes(self): + """Return device specific attributes.""" + attrs = {} + for key in (ATTR_ALTITUDE, ATTR_COURSE, + ATTR_SPEED, ATTR_VERTICAL_ACCURACY): + value = self._data.get(key) + if value is not None: + attrs[key] = value + + return attrs + + @property + def location_accuracy(self): + """Return the gps accuracy of the device.""" + return self._data.get(ATTR_GPS_ACCURACY) + + @property + def latitude(self): + """Return latitude value of the device.""" + gps = self._data.get(ATTR_GPS) + + if gps is None: + return None + + return gps[0] + + @property + def longitude(self): + """Return longitude value of the device.""" + gps = self._data.get(ATTR_GPS) + + if gps is None: + return None + + return gps[1] + + @property + def location_name(self): + """Return a location name for the current location of the device.""" + return self._data.get(ATTR_LOCATION_NAME) + + @property + def name(self): + """Return the name of the device.""" + return self._entry.data[ATTR_DEVICE_NAME] + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def source_type(self): + """Return the source type, eg gps or router, of the device.""" + return SOURCE_TYPE_GPS + + @property + def device_info(self): + """Return the device info.""" + return device_info(self._entry.data) + + @callback + def update_data(self, data): + """Mark the device as seen.""" + self._data = data + self.async_write_ha_state() diff --git a/homeassistant/components/mobile_app/entity.py b/homeassistant/components/mobile_app/entity.py index eca9d2b024b..8c1747d6f2b 100644 --- a/homeassistant/components/mobile_app/entity.py +++ b/homeassistant/components/mobile_app/entity.py @@ -6,11 +6,11 @@ from homeassistant.helpers.device_registry import DeviceEntry from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity -from .const import (ATTR_DEVICE_ID, ATTR_DEVICE_NAME, ATTR_MANUFACTURER, - ATTR_MODEL, ATTR_OS_VERSION, ATTR_SENSOR_ATTRIBUTES, +from .const import (ATTR_SENSOR_ATTRIBUTES, ATTR_SENSOR_DEVICE_CLASS, ATTR_SENSOR_ICON, ATTR_SENSOR_NAME, ATTR_SENSOR_TYPE, ATTR_SENSOR_UNIQUE_ID, DOMAIN, SIGNAL_SENSOR_UPDATE) +from .helpers import device_info def sensor_id(webhook_id, unique_id): @@ -76,17 +76,7 @@ class MobileAppEntity(Entity): @property def device_info(self): """Return device registry information for this entity.""" - return { - 'identifiers': { - (ATTR_DEVICE_ID, self._registration[ATTR_DEVICE_ID]), - (CONF_WEBHOOK_ID, self._registration[CONF_WEBHOOK_ID]) - }, - 'manufacturer': self._registration[ATTR_MANUFACTURER], - 'model': self._registration[ATTR_MODEL], - 'device_name': self._registration[ATTR_DEVICE_NAME], - 'sw_version': self._registration[ATTR_OS_VERSION], - 'config_entries': self._device.config_entries - } + return device_info(self._registration) async def async_update(self): """Get the latest state of the sensor.""" diff --git a/homeassistant/components/mobile_app/helpers.py b/homeassistant/components/mobile_app/helpers.py index 6aec4307464..30c111fe0b4 100644 --- a/homeassistant/components/mobile_app/helpers.py +++ b/homeassistant/components/mobile_app/helpers.py @@ -9,7 +9,7 @@ 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, +from .const import (ATTR_APP_DATA, ATTR_APP_ID, ATTR_APP_NAME, ATTR_DEVICE_ID, ATTR_APP_VERSION, ATTR_DEVICE_NAME, ATTR_MANUFACTURER, ATTR_MODEL, ATTR_OS_VERSION, ATTR_SUPPORTS_ENCRYPTION, CONF_SECRET, CONF_USER_ID, DATA_BINARY_SENSOR, @@ -148,3 +148,16 @@ def webhook_response(data, *, registration: Dict, status: int = 200, return Response(text=data, status=status, content_type='application/json', headers=headers) + + +def device_info(registration: Dict) -> Dict: + """Return the device info for this registration.""" + return { + 'identifiers': { + (DOMAIN, registration[ATTR_DEVICE_ID]), + }, + 'manufacturer': registration[ATTR_MANUFACTURER], + 'model': registration[ATTR_MODEL], + 'device_name': registration[ATTR_DEVICE_NAME], + 'sw_version': registration[ATTR_OS_VERSION], + } diff --git a/homeassistant/components/mobile_app/manifest.json b/homeassistant/components/mobile_app/manifest.json index 969817b62c7..85c6231daa8 100644 --- a/homeassistant/components/mobile_app/manifest.json +++ b/homeassistant/components/mobile_app/manifest.json @@ -7,7 +7,6 @@ "PyNaCl==1.3.0" ], "dependencies": [ - "device_tracker", "http", "webhook" ], diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py index 4f867885d4f..40002b5cfec 100644 --- a/homeassistant/components/mobile_app/webhook.py +++ b/homeassistant/components/mobile_app/webhook.py @@ -6,10 +6,6 @@ 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, - SERVICE_SEE as DT_SEE) from homeassistant.components.frontend import MANIFEST_JSON from homeassistant.components.zone.const import DOMAIN as ZONE_DOMAIN @@ -24,15 +20,12 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.template import attach from homeassistant.helpers.typing import HomeAssistantType -from homeassistant.util import slugify - -from .const import (ATTR_ALTITUDE, ATTR_BATTERY, ATTR_COURSE, ATTR_DEVICE_ID, +from .const import (ATTR_DEVICE_ID, ATTR_DEVICE_NAME, ATTR_EVENT_DATA, ATTR_EVENT_TYPE, - ATTR_GPS, ATTR_GPS_ACCURACY, ATTR_LOCATION_NAME, ATTR_MANUFACTURER, ATTR_MODEL, ATTR_OS_VERSION, - ATTR_SENSOR_TYPE, ATTR_SENSOR_UNIQUE_ID, ATTR_SPEED, + ATTR_SENSOR_TYPE, ATTR_SENSOR_UNIQUE_ID, ATTR_SUPPORTS_ENCRYPTION, ATTR_TEMPLATE, - ATTR_TEMPLATE_VARIABLES, ATTR_VERTICAL_ACCURACY, + ATTR_TEMPLATE_VARIABLES, ATTR_WEBHOOK_DATA, ATTR_WEBHOOK_ENCRYPTED, ATTR_WEBHOOK_ENCRYPTED_DATA, ATTR_WEBHOOK_TYPE, CONF_CLOUDHOOK_URL, CONF_REMOTE_UI_URL, CONF_SECRET, @@ -45,7 +38,7 @@ from .const import (ATTR_ALTITUDE, ATTR_BATTERY, ATTR_COURSE, ATTR_DEVICE_ID, WEBHOOK_TYPE_REGISTER_SENSOR, WEBHOOK_TYPE_RENDER_TEMPLATE, WEBHOOK_TYPE_UPDATE_LOCATION, WEBHOOK_TYPE_UPDATE_REGISTRATION, - WEBHOOK_TYPE_UPDATE_SENSOR_STATES) + WEBHOOK_TYPE_UPDATE_SENSOR_STATES, SIGNAL_LOCATION_UPDATE) from .helpers import (_decrypt_payload, empty_okay_response, error_response, @@ -151,37 +144,9 @@ async def handle_webhook(hass: HomeAssistantType, webhook_id: str, headers=headers) if webhook_type == WEBHOOK_TYPE_UPDATE_LOCATION: - see_payload = { - ATTR_DEV_ID: slugify(registration[ATTR_DEVICE_NAME]), - 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, - blocking=True, context=context) - # noqa: E722 pylint: disable=broad-except - except (vol.Invalid, ServiceNotFound, Exception) as ex: - _LOGGER.error("Error when updating location during mobile_app " - "webhook (device name: %s): %s", - registration[ATTR_DEVICE_NAME], ex) + hass.helpers.dispatcher.async_dispatcher_send( + SIGNAL_LOCATION_UPDATE.format(config_entry.entry_id), data + ) return empty_okay_response(headers=headers) if webhook_type == WEBHOOK_TYPE_UPDATE_REGISTRATION: diff --git a/tests/components/mobile_app/__init__.py b/tests/components/mobile_app/__init__.py index 98c7a20b059..9b37214d079 100644 --- a/tests/components/mobile_app/__init__.py +++ b/tests/components/mobile_app/__init__.py @@ -1,74 +1 @@ -"""Tests for mobile_app component.""" -# pylint: disable=redefined-outer-name,unused-import -import pytest - -from tests.common import mock_device_registry - -from homeassistant.setup import async_setup_component - -from homeassistant.components.mobile_app.const import (DATA_BINARY_SENSOR, - DATA_DELETED_IDS, - DATA_SENSOR, - DOMAIN, - STORAGE_KEY, - STORAGE_VERSION) - -from .const import REGISTER, REGISTER_CLEARTEXT - - -@pytest.fixture -def registry(hass): - """Return a configured device registry.""" - return mock_device_registry(hass) - - -@pytest.fixture -async def create_registrations(authed_api_client): - """Return two new registrations.""" - enc_reg = await authed_api_client.post( - '/api/mobile_app/registrations', json=REGISTER - ) - - assert enc_reg.status == 201 - enc_reg_json = await enc_reg.json() - - clear_reg = await authed_api_client.post( - '/api/mobile_app/registrations', json=REGISTER_CLEARTEXT - ) - - assert clear_reg.status == 201 - clear_reg_json = await clear_reg.json() - - return (enc_reg_json, clear_reg_json) - - -@pytest.fixture -async def webhook_client(hass, aiohttp_client, hass_storage, hass_admin_user): - """mobile_app mock client.""" - hass_storage[STORAGE_KEY] = { - 'version': STORAGE_VERSION, - 'data': { - DATA_BINARY_SENSOR: {}, - DATA_DELETED_IDS: [], - DATA_SENSOR: {} - } - } - - await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) - await hass.async_block_till_done() - return await aiohttp_client(hass.http.app) - - -@pytest.fixture -async def authed_api_client(hass, hass_client): - """Provide an authenticated client for mobile_app to use.""" - await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) - await hass.async_block_till_done() - return await hass_client() - - -@pytest.fixture(autouse=True) -async def setup_ws(hass): - """Configure the websocket_api component.""" - assert await async_setup_component(hass, 'websocket_api', {}) - await hass.async_block_till_done() +"""Tests for the mobile app integration.""" diff --git a/tests/components/mobile_app/conftest.py b/tests/components/mobile_app/conftest.py new file mode 100644 index 00000000000..b20d164e6e6 --- /dev/null +++ b/tests/components/mobile_app/conftest.py @@ -0,0 +1,60 @@ +"""Tests for mobile_app component.""" +# pylint: disable=redefined-outer-name,unused-import +import pytest + +from tests.common import mock_device_registry + +from homeassistant.setup import async_setup_component + +from homeassistant.components.mobile_app.const import DOMAIN + +from .const import REGISTER, REGISTER_CLEARTEXT + + +@pytest.fixture +def registry(hass): + """Return a configured device registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +async def create_registrations(authed_api_client): + """Return two new registrations.""" + enc_reg = await authed_api_client.post( + '/api/mobile_app/registrations', json=REGISTER + ) + + assert enc_reg.status == 201 + enc_reg_json = await enc_reg.json() + + clear_reg = await authed_api_client.post( + '/api/mobile_app/registrations', json=REGISTER_CLEARTEXT + ) + + assert clear_reg.status == 201 + clear_reg_json = await clear_reg.json() + + return (enc_reg_json, clear_reg_json) + + +@pytest.fixture +async def webhook_client(hass, aiohttp_client): + """mobile_app mock client.""" + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + await hass.async_block_till_done() + return await aiohttp_client(hass.http.app) + + +@pytest.fixture +async def authed_api_client(hass, hass_client): + """Provide an authenticated client for mobile_app to use.""" + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + await hass.async_block_till_done() + return await hass_client() + + +@pytest.fixture(autouse=True) +async def setup_ws(hass): + """Configure the websocket_api component.""" + assert await async_setup_component(hass, 'websocket_api', {}) + await hass.async_block_till_done() diff --git a/tests/components/mobile_app/test_device_tracker.py b/tests/components/mobile_app/test_device_tracker.py new file mode 100644 index 00000000000..448bd9181c8 --- /dev/null +++ b/tests/components/mobile_app/test_device_tracker.py @@ -0,0 +1,68 @@ +"""Test mobile app device tracker.""" + + +async def test_sending_location(hass, create_registrations, webhook_client): + """Test sending a location via a webhook.""" + resp = await webhook_client.post( + '/api/webhook/{}'.format(create_registrations[1]['webhook_id']), + json={ + 'type': 'update_location', + 'data': { + 'gps': [10, 20], + 'gps_accuracy': 30, + 'battery': 40, + 'altitude': 50, + 'course': 60, + 'speed': 70, + 'vertical_accuracy': 80, + 'location_name': 'bar', + } + } + ) + + assert resp.status == 200 + await hass.async_block_till_done() + state = hass.states.get('device_tracker.test_1') + assert state is not None + assert state.name == 'Test 1' + assert state.state == 'bar' + assert state.attributes['source_type'] == 'gps' + assert state.attributes['latitude'] == 10 + assert state.attributes['longitude'] == 20 + assert state.attributes['gps_accuracy'] == 30 + assert state.attributes['battery_level'] == 40 + assert state.attributes['altitude'] == 50 + assert state.attributes['course'] == 60 + assert state.attributes['speed'] == 70 + assert state.attributes['vertical_accuracy'] == 80 + + resp = await webhook_client.post( + '/api/webhook/{}'.format(create_registrations[1]['webhook_id']), + json={ + 'type': 'update_location', + 'data': { + 'gps': [1, 2], + 'gps_accuracy': 3, + 'battery': 4, + 'altitude': 5, + 'course': 6, + 'speed': 7, + 'vertical_accuracy': 8, + } + } + ) + + assert resp.status == 200 + await hass.async_block_till_done() + state = hass.states.get('device_tracker.test_1') + assert state is not None + assert state.state == 'not_home' + assert state.attributes['source_type'] == 'gps' + assert state.attributes['latitude'] == 1 + assert state.attributes['longitude'] == 2 + assert state.attributes['gps_accuracy'] == 3 + assert state.attributes['battery_level'] == 4 + assert state.attributes['altitude'] == 5 + assert state.attributes['course'] == 6 + assert state.attributes['speed'] == 7 + assert state.attributes['vertical_accuracy'] == 8 diff --git a/tests/components/mobile_app/test_entity.py b/tests/components/mobile_app/test_entity.py index e98307468d1..750c346cbc3 100644 --- a/tests/components/mobile_app/test_entity.py +++ b/tests/components/mobile_app/test_entity.py @@ -2,9 +2,6 @@ # pylint: disable=redefined-outer-name,unused-import import logging -from . import (authed_api_client, create_registrations, # noqa: F401 - webhook_client) # noqa: F401 - _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/mobile_app/test_http_api.py b/tests/components/mobile_app/test_http_api.py index dc51b850a16..80f01315f70 100644 --- a/tests/components/mobile_app/test_http_api.py +++ b/tests/components/mobile_app/test_http_api.py @@ -7,10 +7,9 @@ from homeassistant.const import CONF_WEBHOOK_ID from homeassistant.setup import async_setup_component from .const import REGISTER, RENDER_TEMPLATE -from . import authed_api_client # noqa: F401 -async def test_registration(hass, hass_client): # noqa: F811 +async def test_registration(hass, hass_client): """Test that registrations happen.""" try: # pylint: disable=unused-import diff --git a/tests/components/mobile_app/test_webhook.py b/tests/components/mobile_app/test_webhook.py index 43eac28ec18..cd5b0a5bbed 100644 --- a/tests/components/mobile_app/test_webhook.py +++ b/tests/components/mobile_app/test_webhook.py @@ -11,17 +11,14 @@ from homeassistant.setup import async_setup_component from tests.common import async_mock_service -from . import (authed_api_client, create_registrations, # noqa: F401 - webhook_client) # noqa: F401 - from .const import (CALL_SERVICE, FIRE_EVENT, REGISTER_CLEARTEXT, RENDER_TEMPLATE, UPDATE) _LOGGER = logging.getLogger(__name__) -async def test_webhook_handle_render_template(create_registrations, # noqa: F401, F811, E501 - webhook_client): # noqa: F811 +async def test_webhook_handle_render_template(create_registrations, + webhook_client): """Test that we render templates properly.""" resp = await webhook_client.post( '/api/webhook/{}'.format(create_registrations[1]['webhook_id']), @@ -34,7 +31,7 @@ async def test_webhook_handle_render_template(create_registrations, # noqa: F40 assert json == {'one': 'Hello world'} -async def test_webhook_handle_call_services(hass, create_registrations, # noqa: F401, F811, E501 +async def test_webhook_handle_call_services(hass, create_registrations, webhook_client): # noqa: E501 F811 """Test that we call services properly.""" calls = async_mock_service(hass, 'test', 'mobile_app') @@ -49,8 +46,8 @@ async def test_webhook_handle_call_services(hass, create_registrations, # noqa: assert len(calls) == 1 -async def test_webhook_handle_fire_event(hass, create_registrations, # noqa: F401, F811, E501 - webhook_client): # noqa: F811 +async def test_webhook_handle_fire_event(hass, create_registrations, + webhook_client): """Test that we can fire events.""" events = [] @@ -76,7 +73,7 @@ async def test_webhook_handle_fire_event(hass, create_registrations, # noqa: F4 async def test_webhook_update_registration(webhook_client, hass_client): # noqa: E501 F811 """Test that a we can update an existing registration via webhook.""" - authed_api_client = await hass_client() # noqa: F811 + authed_api_client = await hass_client() register_resp = await authed_api_client.post( '/api/mobile_app/registrations', json=REGISTER_CLEARTEXT ) @@ -102,8 +99,8 @@ 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 +async def test_webhook_handle_get_zones(hass, create_registrations, + webhook_client): """Test that we can get zones properly.""" await async_setup_component(hass, ZONE_DOMAIN, { ZONE_DOMAIN: { @@ -126,8 +123,8 @@ 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 +async def test_webhook_handle_get_config(hass, create_registrations, + webhook_client): """Test that we can get config properly.""" resp = await webhook_client.post( '/api/webhook/{}'.format(create_registrations[1]['webhook_id']), @@ -160,8 +157,8 @@ async def test_webhook_handle_get_config(hass, create_registrations, # noqa: F4 assert expected_dict == json -async def test_webhook_returns_error_incorrect_json(webhook_client, # noqa: F401, F811, E501 - create_registrations, # noqa: F401, F811, E501 +async def test_webhook_returns_error_incorrect_json(webhook_client, + create_registrations, caplog): # noqa: E501 F811 """Test that an error is returned when JSON is invalid.""" resp = await webhook_client.post( @@ -175,8 +172,8 @@ async def test_webhook_returns_error_incorrect_json(webhook_client, # noqa: F40 assert 'invalid JSON' in caplog.text -async def test_webhook_handle_decryption(webhook_client, # noqa: F811 - create_registrations): # noqa: F401, F811, E501 +async def test_webhook_handle_decryption(webhook_client, + create_registrations): """Test that we can encrypt/decrypt properly.""" try: # pylint: disable=unused-import @@ -221,8 +218,8 @@ async def test_webhook_handle_decryption(webhook_client, # noqa: F811 assert json.loads(decrypted_data) == {'one': 'Hello world'} -async def test_webhook_requires_encryption(webhook_client, # noqa: F811 - create_registrations): # noqa: F401, F811, E501 +async def test_webhook_requires_encryption(webhook_client, + create_registrations): """Test that encrypted registrations only accept encrypted data.""" resp = await webhook_client.post( '/api/webhook/{}'.format(create_registrations[0]['webhook_id']), diff --git a/tests/components/mobile_app/test_websocket_api.py b/tests/components/mobile_app/test_websocket_api.py index ee656159d2e..20676731393 100644 --- a/tests/components/mobile_app/test_websocket_api.py +++ b/tests/components/mobile_app/test_websocket_api.py @@ -5,7 +5,6 @@ from homeassistant.components.websocket_api.const import TYPE_RESULT from homeassistant.const import CONF_WEBHOOK_ID from homeassistant.setup import async_setup_component -from . import authed_api_client, setup_ws, webhook_client # noqa: F401 from .const import (CALL_SERVICE, REGISTER) @@ -45,7 +44,7 @@ async def test_webocket_get_user_registrations(hass, aiohttp_client, async def test_webocket_delete_registration(hass, hass_client, - hass_ws_client, webhook_client): # noqa: E501 F811 + hass_ws_client, webhook_client): """Test delete_registration websocket command.""" authed_api_client = await hass_client() # noqa: F811 register_resp = await authed_api_client.post( From e5cbf01ce10f8d6ac65f8ca75064313d4de9eef6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 31 May 2019 23:05:57 -0700 Subject: [PATCH 206/232] Bumped version to 0.94.0b3 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 8a9dfa40454..dc279f9725f 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 94 -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 09c43e8854dc5684fdce7b07771f141115692278 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 1 Jun 2019 14:27:25 -0700 Subject: [PATCH 207/232] Updated frontend to 20190601.0 --- homeassistant/components/frontend/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index cb6ce89198e..bd93a0f481c 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/components/frontend", "requirements": [ - "home-assistant-frontend==20190530.0" + "home-assistant-frontend==20190601.0" ], "dependencies": [ "api", diff --git a/requirements_all.txt b/requirements_all.txt index 25509c7f56c..fcb60d5068b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -577,7 +577,7 @@ hole==0.3.0 holidays==0.9.10 # homeassistant.components.frontend -home-assistant-frontend==20190530.0 +home-assistant-frontend==20190601.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d03208c9a1d..ed0ed7c8cb6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -148,7 +148,7 @@ hdate==0.8.7 holidays==0.9.10 # homeassistant.components.frontend -home-assistant-frontend==20190530.0 +home-assistant-frontend==20190601.0 # homeassistant.components.homekit_controller homekit[IP]==0.14.0 From bf85e18d456277ebfe0a54eebc8363e67f8b970f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 1 Jun 2019 01:04:12 -0700 Subject: [PATCH 208/232] Do not use the cache dir for PIP installs (#24233) --- homeassistant/requirements.py | 7 +++++-- homeassistant/util/package.py | 5 ++++- tests/test_requirements.py | 21 ++++++++++++++------- 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/homeassistant/requirements.py b/homeassistant/requirements.py index ca34a4bbae4..1164eff4eb8 100644 --- a/homeassistant/requirements.py +++ b/homeassistant/requirements.py @@ -44,12 +44,15 @@ async def async_process_requirements(hass: HomeAssistant, name: str, def pip_kwargs(config_dir: Optional[str]) -> Dict[str, Any]: """Return keyword arguments for PIP install.""" + is_docker = pkg_util.is_docker_env() kwargs = { - 'constraints': os.path.join(os.path.dirname(__file__), CONSTRAINT_FILE) + 'constraints': os.path.join(os.path.dirname(__file__), + CONSTRAINT_FILE), + 'no_cache_dir': is_docker, } if 'WHEELS_LINKS' in os.environ: kwargs['find_links'] = os.environ['WHEELS_LINKS'] if not (config_dir is None or pkg_util.is_virtual_env()) and \ - not pkg_util.is_docker_env(): + not is_docker: kwargs['target'] = os.path.join(config_dir, 'deps') return kwargs diff --git a/homeassistant/util/package.py b/homeassistant/util/package.py index 272a097b24c..6f6d03d67b6 100644 --- a/homeassistant/util/package.py +++ b/homeassistant/util/package.py @@ -49,7 +49,8 @@ def is_installed(package: str) -> bool: def install_package(package: str, upgrade: bool = True, target: Optional[str] = None, constraints: Optional[str] = None, - find_links: Optional[str] = None) -> bool: + find_links: Optional[str] = None, + no_cache_dir: Optional[bool] = False) -> bool: """Install a package on PyPi. Accepts pip compatible package strings. Return boolean if install successful. @@ -58,6 +59,8 @@ def install_package(package: str, upgrade: bool = True, _LOGGER.info('Attempting install of %s', package) env = os.environ.copy() args = [sys.executable, '-m', 'pip', 'install', '--quiet', package] + if no_cache_dir: + args.append('--no-cache-dir') if upgrade: args.append('--upgrade') if constraints is not None: diff --git a/tests/test_requirements.py b/tests/test_requirements.py index 35264c2e1b4..bbf86278bd2 100644 --- a/tests/test_requirements.py +++ b/tests/test_requirements.py @@ -30,9 +30,8 @@ class TestRequirements: @patch('homeassistant.util.package.is_docker_env', return_value=False) @patch('homeassistant.util.package.install_package', return_value=True) def test_requirement_installed_in_venv( - self, mock_install, mock_venv, mock_denv, mock_dirname): + self, mock_install, mock_denv, mock_venv, mock_dirname): """Test requirement installed in virtual environment.""" - mock_venv.return_value = True mock_dirname.return_value = 'ha_package_path' self.hass.config.skip_pip = False mock_integration( @@ -42,14 +41,16 @@ class TestRequirements: assert 'comp' in self.hass.config.components assert mock_install.call_args == call( 'package==0.0.1', - constraints=os.path.join('ha_package_path', CONSTRAINT_FILE)) + constraints=os.path.join('ha_package_path', CONSTRAINT_FILE), + no_cache_dir=False, + ) @patch('os.path.dirname') @patch('homeassistant.util.package.is_virtual_env', return_value=False) @patch('homeassistant.util.package.is_docker_env', return_value=False) @patch('homeassistant.util.package.install_package', return_value=True) def test_requirement_installed_in_deps( - self, mock_install, mock_venv, mock_denv, mock_dirname): + self, mock_install, mock_denv, mock_venv, mock_dirname): """Test requirement installed in deps directory.""" mock_dirname.return_value = 'ha_package_path' self.hass.config.skip_pip = False @@ -60,7 +61,9 @@ class TestRequirements: assert 'comp' in self.hass.config.components assert mock_install.call_args == call( 'package==0.0.1', target=self.hass.config.path('deps'), - constraints=os.path.join('ha_package_path', CONSTRAINT_FILE)) + constraints=os.path.join('ha_package_path', CONSTRAINT_FILE), + no_cache_dir=False, + ) async def test_install_existing_package(hass): @@ -108,7 +111,9 @@ async def test_install_with_wheels_index(hass): print(mock_inst.call_args) assert mock_inst.call_args == call( 'hello==1.0.0', find_links="https://wheels.hass.io/test", - constraints=os.path.join('ha_package_path', CONSTRAINT_FILE)) + constraints=os.path.join('ha_package_path', CONSTRAINT_FILE), + no_cache_dir=True, + ) async def test_install_on_docker(hass): @@ -135,4 +140,6 @@ async def test_install_on_docker(hass): print(mock_inst.call_args) assert mock_inst.call_args == call( 'hello==1.0.0', - constraints=os.path.join('ha_package_path', CONSTRAINT_FILE)) + constraints=os.path.join('ha_package_path', CONSTRAINT_FILE), + no_cache_dir=True, + ) From 22f68d70a71f88a2ec1fde4b8d53a4692009708b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 1 Jun 2019 14:34:39 -0700 Subject: [PATCH 209/232] Bumped version to 0.94.0b4 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index dc279f9725f..e8ce6c04f7a 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 94 -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 a8c73ffb93b6fae58fe6410e8c511a6ae41a00fd Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 2 Jun 2019 13:52:52 -0700 Subject: [PATCH 210/232] Updated frontend to 20190602.0 --- homeassistant/components/frontend/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index bd93a0f481c..820f17a98bf 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/components/frontend", "requirements": [ - "home-assistant-frontend==20190601.0" + "home-assistant-frontend==20190602.0" ], "dependencies": [ "api", diff --git a/requirements_all.txt b/requirements_all.txt index fcb60d5068b..7adb9d4bed0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -577,7 +577,7 @@ hole==0.3.0 holidays==0.9.10 # homeassistant.components.frontend -home-assistant-frontend==20190601.0 +home-assistant-frontend==20190602.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ed0ed7c8cb6..6c5f41a054f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -148,7 +148,7 @@ hdate==0.8.7 holidays==0.9.10 # homeassistant.components.frontend -home-assistant-frontend==20190601.0 +home-assistant-frontend==20190602.0 # homeassistant.components.homekit_controller homekit[IP]==0.14.0 From d2d3f27f85106cd242eca62bc43bf3ee7623d62b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 2 Jun 2019 13:57:21 -0700 Subject: [PATCH 211/232] Add restore state to OwnTracks device tracker (#24256) * Add restore state to OwnTracks device tracker * Lint * Also store entity devices * Update test_device_tracker.py --- .../components/owntracks/device_tracker.py | 136 ++++++++++-------- .../owntracks/test_device_tracker.py | 44 ++++++ 2 files changed, 124 insertions(+), 56 deletions(-) diff --git a/homeassistant/components/owntracks/device_tracker.py b/homeassistant/components/owntracks/device_tracker.py index fb9fedf26fa..d74fea43c29 100644 --- a/homeassistant/components/owntracks/device_tracker.py +++ b/homeassistant/components/owntracks/device_tracker.py @@ -2,10 +2,19 @@ import logging from homeassistant.core import callback -from homeassistant.components.device_tracker.const import ENTITY_ID_FORMAT +from homeassistant.const import ( + ATTR_GPS_ACCURACY, + ATTR_LATITUDE, + ATTR_LONGITUDE, + ATTR_BATTERY_LEVEL, +) +from homeassistant.components.device_tracker.const import ( + ENTITY_ID_FORMAT, ATTR_SOURCE_TYPE) from homeassistant.components.device_tracker.config_entry import ( DeviceTrackerEntity ) +from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.helpers import device_registry from . import DOMAIN as OT_DOMAIN _LOGGER = logging.getLogger(__name__) @@ -14,53 +23,52 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass, entry, async_add_entities): """Set up OwnTracks based off an entry.""" @callback - def _receive_data(dev_id, host_name, gps, attributes, gps_accuracy=None, - battery=None, source_type=None, location_name=None): + def _receive_data(dev_id, **data): """Receive set location.""" - device = hass.data[OT_DOMAIN]['devices'].get(dev_id) + entity = hass.data[OT_DOMAIN]['devices'].get(dev_id) - if device is not None: - device.update_data( - host_name=host_name, - gps=gps, - attributes=attributes, - gps_accuracy=gps_accuracy, - battery=battery, - source_type=source_type, - location_name=location_name, - ) + if entity is not None: + entity.update_data(data) return - device = hass.data[OT_DOMAIN]['devices'][dev_id] = OwnTracksEntity( - dev_id=dev_id, - host_name=host_name, - gps=gps, - attributes=attributes, - gps_accuracy=gps_accuracy, - battery=battery, - source_type=source_type, - location_name=location_name, + entity = hass.data[OT_DOMAIN]['devices'][dev_id] = OwnTracksEntity( + dev_id, data ) - async_add_entities([device]) + async_add_entities([entity]) hass.data[OT_DOMAIN]['context'].async_see = _receive_data + + # Restore previously loaded devices + dev_reg = await device_registry.async_get_registry(hass) + dev_ids = { + identifier[1] + for device in dev_reg.devices.values() + for identifier in device.identifiers + if identifier[0] == OT_DOMAIN + } + + if not dev_ids: + return True + + entities = [] + for dev_id in dev_ids: + entity = hass.data[OT_DOMAIN]['devices'][dev_id] = OwnTracksEntity( + dev_id + ) + entities.append(entity) + + async_add_entities(entities) + return True -class OwnTracksEntity(DeviceTrackerEntity): +class OwnTracksEntity(DeviceTrackerEntity, RestoreEntity): """Represent a tracked device.""" - def __init__(self, dev_id, host_name, gps, attributes, gps_accuracy, - battery, source_type, location_name): + def __init__(self, dev_id, data=None): """Set up OwnTracks entity.""" self._dev_id = dev_id - self._host_name = host_name - self._gps = gps - self._gps_accuracy = gps_accuracy - self._location_name = location_name - self._attributes = attributes - self._battery = battery - self._source_type = source_type + self._data = data or {} self.entity_id = ENTITY_ID_FORMAT.format(dev_id) @property @@ -71,43 +79,45 @@ class OwnTracksEntity(DeviceTrackerEntity): @property def battery_level(self): """Return the battery level of the device.""" - return self._battery + return self._data.get('battery') @property def device_state_attributes(self): """Return device specific attributes.""" - return self._attributes + return self._data.get('attributes') @property def location_accuracy(self): """Return the gps accuracy of the device.""" - return self._gps_accuracy + return self._data.get('gps_accuracy') @property def latitude(self): """Return latitude value of the device.""" - if self._gps is not None: - return self._gps[0] + # Check with "get" instead of "in" because value can be None + if self._data.get('gps'): + return self._data['gps'][0] return None @property def longitude(self): """Return longitude value of the device.""" - if self._gps is not None: - return self._gps[1] + # Check with "get" instead of "in" because value can be None + if self._data.get('gps'): + return self._data['gps'][1] return None @property def location_name(self): """Return a location name for the current location of the device.""" - return self._location_name + return self._data.get('location_name') @property def name(self): """Return the name of the device.""" - return self._host_name + return self._data.get('host_name') @property def should_poll(self): @@ -117,26 +127,40 @@ class OwnTracksEntity(DeviceTrackerEntity): @property def source_type(self): """Return the source type, eg gps or router, of the device.""" - return self._source_type + return self._data.get('source_type') @property def device_info(self): """Return the device info.""" return { - 'name': self._host_name, + 'name': self.name, 'identifiers': {(OT_DOMAIN, self._dev_id)}, } - @callback - def update_data(self, host_name, gps, attributes, gps_accuracy, - battery, source_type, location_name): - """Mark the device as seen.""" - self._host_name = host_name - self._gps = gps - self._gps_accuracy = gps_accuracy - self._location_name = location_name - self._attributes = attributes - self._battery = battery - self._source_type = source_type + async def async_added_to_hass(self): + """Call when entity about to be added to Home Assistant.""" + await super().async_added_to_hass() + # Don't restore if we got set up with data. + if self._data: + return + + state = await self.async_get_last_state() + + if state is None: + return + + attr = state.attributes + self._data = { + 'host_name': state.name, + 'gps': (attr[ATTR_LATITUDE], attr[ATTR_LONGITUDE]), + 'gps_accuracy': attr[ATTR_GPS_ACCURACY], + 'battery': attr[ATTR_BATTERY_LEVEL], + 'source_type': attr[ATTR_SOURCE_TYPE], + } + + @callback + def update_data(self, data): + """Mark the device as seen.""" + self._data = data self.async_write_ha_state() diff --git a/tests/components/owntracks/test_device_tracker.py b/tests/components/owntracks/test_device_tracker.py index b81f434a2c1..7d8d48de586 100644 --- a/tests/components/owntracks/test_device_tracker.py +++ b/tests/components/owntracks/test_device_tracker.py @@ -1491,3 +1491,47 @@ async def test_region_mapping(hass, setup_comp): await send_message(hass, EVENT_TOPIC, message) assert_location_state(hass, 'inner') + + +async def test_restore_state(hass, hass_client): + """Test that we can restore state.""" + entry = MockConfigEntry(domain='owntracks', data={ + 'webhook_id': 'owntracks_test', + 'secret': 'abcd', + }) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + client = await hass_client() + resp = await client.post( + '/api/webhook/owntracks_test', + json=LOCATION_MESSAGE, + headers={ + 'X-Limit-u': 'Paulus', + 'X-Limit-d': 'Pixel', + } + ) + assert resp.status == 200 + await hass.async_block_till_done() + + state_1 = hass.states.get('device_tracker.paulus_pixel') + assert state_1 is not None + + await hass.config_entries.async_reload(entry.entry_id) + await hass.async_block_till_done() + + state_2 = hass.states.get('device_tracker.paulus_pixel') + assert state_2 is not None + + assert state_1 is not state_2 + + assert state_1.state == state_2.state + assert state_1.name == state_2.name + assert state_1.attributes['latitude'] == state_2.attributes['latitude'] + assert state_1.attributes['longitude'] == state_2.attributes['longitude'] + assert state_1.attributes['battery_level'] == \ + state_2.attributes['battery_level'] + assert state_1.attributes['source_type'] == \ + state_2.attributes['source_type'] From 5f3bcedbba005ceef64bc8c35aa4be86d8ec19c2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 3 Jun 2019 01:30:56 -0700 Subject: [PATCH 212/232] Mobile app device tracker to restore state (#24266) --- .../components/mobile_app/__init__.py | 3 +- homeassistant/components/mobile_app/const.py | 1 - .../components/mobile_app/device_tracker.py | 80 +++++++++++++------ .../mobile_app/test_device_tracker.py | 52 +++++++++++- 4 files changed, 106 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/mobile_app/__init__.py b/homeassistant/components/mobile_app/__init__.py index 839aa8a6c3b..1d34babe3ac 100644 --- a/homeassistant/components/mobile_app/__init__.py +++ b/homeassistant/components/mobile_app/__init__.py @@ -7,7 +7,7 @@ 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_DEVICE_TRACKER, DATA_SENSOR, DATA_STORE, + DATA_DEVICES, DATA_SENSOR, DATA_STORE, DOMAIN, STORAGE_KEY, STORAGE_VERSION) from .http_api import RegistrationsView @@ -34,7 +34,6 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType): DATA_CONFIG_ENTRIES: {}, DATA_DELETED_IDS: app_config.get(DATA_DELETED_IDS, []), DATA_DEVICES: {}, - DATA_DEVICE_TRACKER: {}, DATA_SENSOR: app_config.get(DATA_SENSOR, {}), DATA_STORE: store, } diff --git a/homeassistant/components/mobile_app/const.py b/homeassistant/components/mobile_app/const.py index 8cb5aa12731..922835c1d40 100644 --- a/homeassistant/components/mobile_app/const.py +++ b/homeassistant/components/mobile_app/const.py @@ -25,7 +25,6 @@ DATA_BINARY_SENSOR = 'binary_sensor' DATA_CONFIG_ENTRIES = 'config_entries' DATA_DELETED_IDS = 'deleted_ids' DATA_DEVICES = 'devices' -DATA_DEVICE_TRACKER = 'device_tracker' DATA_SENSOR = 'sensor' DATA_STORE = 'store' diff --git a/homeassistant/components/mobile_app/device_tracker.py b/homeassistant/components/mobile_app/device_tracker.py index 19aade50876..22435fadc16 100644 --- a/homeassistant/components/mobile_app/device_tracker.py +++ b/homeassistant/components/mobile_app/device_tracker.py @@ -2,14 +2,17 @@ import logging from homeassistant.core import callback -from homeassistant.components.device_tracker.const import ( - DOMAIN, SOURCE_TYPE_GPS) +from homeassistant.const import ( + ATTR_LATITUDE, + ATTR_LONGITUDE, + ATTR_BATTERY_LEVEL, +) +from homeassistant.components.device_tracker.const import SOURCE_TYPE_GPS from homeassistant.components.device_tracker.config_entry import ( DeviceTrackerEntity ) +from homeassistant.helpers.restore_state import RestoreEntity from .const import ( - DOMAIN as MA_DOMAIN, - ATTR_ALTITUDE, ATTR_BATTERY, ATTR_COURSE, @@ -26,37 +29,29 @@ from .const import ( from .helpers import device_info _LOGGER = logging.getLogger(__name__) +ATTR_KEYS = ( + ATTR_ALTITUDE, + ATTR_COURSE, + ATTR_SPEED, + ATTR_VERTICAL_ACCURACY +) async def async_setup_entry(hass, entry, async_add_entities): """Set up OwnTracks based off an entry.""" - @callback - def _receive_data(data): - """Receive set location.""" - dev_id = entry.data[ATTR_DEVICE_ID] - device = hass.data[MA_DOMAIN][DOMAIN].get(dev_id) - - if device is not None: - device.update_data(data) - return - - device = hass.data[MA_DOMAIN][DOMAIN][dev_id] = MobileAppEntity( - entry, data - ) - async_add_entities([device]) - - hass.helpers.dispatcher.async_dispatcher_connect( - SIGNAL_LOCATION_UPDATE.format(entry.entry_id), _receive_data) + entity = MobileAppEntity(entry) + async_add_entities([entity]) return True -class MobileAppEntity(DeviceTrackerEntity): +class MobileAppEntity(DeviceTrackerEntity, RestoreEntity): """Represent a tracked device.""" - def __init__(self, entry, data): + def __init__(self, entry, data=None): """Set up OwnTracks entity.""" self._entry = entry self._data = data + self._dispatch_unsub = None @property def unique_id(self): @@ -72,8 +67,7 @@ class MobileAppEntity(DeviceTrackerEntity): def device_state_attributes(self): """Return device specific attributes.""" attrs = {} - for key in (ATTR_ALTITUDE, ATTR_COURSE, - ATTR_SPEED, ATTR_VERTICAL_ACCURACY): + for key in ATTR_KEYS: value = self._data.get(key) if value is not None: attrs[key] = value @@ -130,6 +124,42 @@ class MobileAppEntity(DeviceTrackerEntity): """Return the device info.""" return device_info(self._entry.data) + async def async_added_to_hass(self): + """Call when entity about to be added to Home Assistant.""" + await super().async_added_to_hass() + self._dispatch_unsub = \ + self.hass.helpers.dispatcher.async_dispatcher_connect( + SIGNAL_LOCATION_UPDATE.format(self._entry.entry_id), + self.update_data + ) + + # Don't restore if we got set up with data. + if self._data is not None: + return + + state = await self.async_get_last_state() + + if state is None: + self._data = {} + return + + attr = state.attributes + data = { + ATTR_GPS: (attr[ATTR_LATITUDE], attr[ATTR_LONGITUDE]), + ATTR_GPS_ACCURACY: attr[ATTR_GPS_ACCURACY], + ATTR_BATTERY: attr[ATTR_BATTERY_LEVEL], + } + data.update({key: attr[key] for key in attr if key in ATTR_KEYS}) + self._data = data + + async def async_will_remove_from_hass(self): + """Call when entity is being removed from hass.""" + await super().async_will_remove_from_hass() + + if self._dispatch_unsub: + self._dispatch_unsub() + self._dispatch_unsub = None + @callback def update_data(self, data): """Mark the device as seen.""" diff --git a/tests/components/mobile_app/test_device_tracker.py b/tests/components/mobile_app/test_device_tracker.py index 448bd9181c8..53f9ad6f6dd 100644 --- a/tests/components/mobile_app/test_device_tracker.py +++ b/tests/components/mobile_app/test_device_tracker.py @@ -22,7 +22,7 @@ async def test_sending_location(hass, create_registrations, webhook_client): assert resp.status == 200 await hass.async_block_till_done() - state = hass.states.get('device_tracker.test_1') + state = hass.states.get('device_tracker.test_1_2') assert state is not None assert state.name == 'Test 1' assert state.state == 'bar' @@ -54,7 +54,7 @@ async def test_sending_location(hass, create_registrations, webhook_client): assert resp.status == 200 await hass.async_block_till_done() - state = hass.states.get('device_tracker.test_1') + state = hass.states.get('device_tracker.test_1_2') assert state is not None assert state.state == 'not_home' assert state.attributes['source_type'] == 'gps' @@ -66,3 +66,51 @@ async def test_sending_location(hass, create_registrations, webhook_client): assert state.attributes['course'] == 6 assert state.attributes['speed'] == 7 assert state.attributes['vertical_accuracy'] == 8 + + +async def test_restoring_location(hass, create_registrations, webhook_client): + """Test sending a location via a webhook.""" + resp = await webhook_client.post( + '/api/webhook/{}'.format(create_registrations[1]['webhook_id']), + json={ + 'type': 'update_location', + 'data': { + 'gps': [10, 20], + 'gps_accuracy': 30, + 'battery': 40, + 'altitude': 50, + 'course': 60, + 'speed': 70, + 'vertical_accuracy': 80, + 'location_name': 'bar', + } + } + ) + + assert resp.status == 200 + await hass.async_block_till_done() + state_1 = hass.states.get('device_tracker.test_1_2') + assert state_1 is not None + + config_entry = hass.config_entries.async_entries('mobile_app')[1] + + # mobile app doesn't support unloading, so we just reload device tracker + await hass.config_entries.async_forward_entry_unload(config_entry, + 'device_tracker') + await hass.config_entries.async_forward_entry_setup(config_entry, + 'device_tracker') + + state_2 = hass.states.get('device_tracker.test_1_2') + assert state_2 is not None + + assert state_1 is not state_2 + assert state_2.name == 'Test 1' + assert state_2.attributes['source_type'] == 'gps' + assert state_2.attributes['latitude'] == 10 + assert state_2.attributes['longitude'] == 20 + assert state_2.attributes['gps_accuracy'] == 30 + assert state_2.attributes['battery_level'] == 40 + assert state_2.attributes['altitude'] == 50 + assert state_2.attributes['course'] == 60 + assert state_2.attributes['speed'] == 70 + assert state_2.attributes['vertical_accuracy'] == 80 From 89d7c0af91554df70f08d507d73e775f03e58e01 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 3 Jun 2019 01:29:45 -0700 Subject: [PATCH 213/232] Add restore state to Geofency (#24268) * Add restore state to Geofency * Lint --- .../components/geofency/device_tracker.py | 40 +++++++++++++++++-- tests/components/geofency/test_init.py | 30 ++++++++------ 2 files changed, 55 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/geofency/device_tracker.py b/homeassistant/components/geofency/device_tracker.py index 0c60d5ef2ce..f9a7df638eb 100644 --- a/homeassistant/components/geofency/device_tracker.py +++ b/homeassistant/components/geofency/device_tracker.py @@ -1,12 +1,18 @@ """Support for the Geofency device tracker platform.""" import logging +from homeassistant.const import ( + ATTR_LATITUDE, + ATTR_LONGITUDE, +) from homeassistant.core import callback from homeassistant.components.device_tracker import SOURCE_TYPE_GPS from homeassistant.components.device_tracker.config_entry import ( DeviceTrackerEntity ) from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.helpers import device_registry from . import DOMAIN as GF_DOMAIN, TRACKER_UPDATE @@ -30,15 +36,28 @@ async def async_setup_entry(hass, config_entry, async_add_entities): hass.data[GF_DOMAIN]['unsub_device_tracker'][config_entry.entry_id] = \ async_dispatcher_connect(hass, TRACKER_UPDATE, _receive_data) + # Restore previously loaded devices + dev_reg = await device_registry.async_get_registry(hass) + dev_ids = { + identifier[1] + for device in dev_reg.devices.values() + for identifier in device.identifiers + if identifier[0] == GF_DOMAIN + } + + if dev_ids: + hass.data[GF_DOMAIN]['devices'].update(dev_ids) + async_add_entities(GeofencyEntity(dev_id) for dev_id in dev_ids) + return True -class GeofencyEntity(DeviceTrackerEntity): +class GeofencyEntity(DeviceTrackerEntity, RestoreEntity): """Represent a tracked device.""" - def __init__(self, device, gps, location_name, attributes): + def __init__(self, device, gps=None, location_name=None, attributes=None): """Set up Geofency entity.""" - self._attributes = attributes + self._attributes = attributes or {} self._name = device self._location_name = location_name self._gps = gps @@ -95,12 +114,27 @@ class GeofencyEntity(DeviceTrackerEntity): async def async_added_to_hass(self): """Register state update callback.""" + await super().async_added_to_hass() self._unsub_dispatcher = async_dispatcher_connect( self.hass, TRACKER_UPDATE, self._async_receive_data) + if self._attributes: + return + + state = await self.async_get_last_state() + + if state is None: + self._gps = (None, None) + return + + attr = state.attributes + self._gps = (attr.get(ATTR_LATITUDE), attr.get(ATTR_LONGITUDE)) + async def async_will_remove_from_hass(self): """Clean up after entity before removal.""" + await super().async_will_remove_from_hass() self._unsub_dispatcher() + self.hass.data[GF_DOMAIN]['devices'].remove(self._unique_id) @callback def _async_receive_data(self, device, gps, location_name, attributes): diff --git a/tests/components/geofency/test_init.py b/tests/components/geofency/test_init.py index 18f119a7539..884ef125eab 100644 --- a/tests/components/geofency/test_init.py +++ b/tests/components/geofency/test_init.py @@ -5,13 +5,12 @@ from unittest.mock import patch, Mock import pytest from homeassistant import data_entry_flow -from homeassistant.components import zone, geofency +from homeassistant.components import zone from homeassistant.components.geofency import ( - CONF_MOBILE_BEACONS, DOMAIN, TRACKER_UPDATE) + CONF_MOBILE_BEACONS, DOMAIN) from homeassistant.const import ( HTTP_OK, HTTP_UNPROCESSABLE_ENTITY, STATE_HOME, STATE_NOT_HOME) -from homeassistant.helpers.dispatcher import DATA_DISPATCHER from homeassistant.setup import async_setup_component from homeassistant.util import slugify @@ -291,9 +290,6 @@ async def test_beacon_enter_and_exit_car(hass, geofency_client, webhook_id): assert STATE_HOME == state_name -@pytest.mark.xfail( - reason='The device_tracker component does not support unloading yet.' -) async def test_load_unload_entry(hass, geofency_client, webhook_id): """Test that the appropriate dispatch signals are added and removed.""" url = '/api/webhook/{}'.format(webhook_id) @@ -303,13 +299,23 @@ async def test_load_unload_entry(hass, geofency_client, webhook_id): await hass.async_block_till_done() assert req.status == HTTP_OK device_name = slugify(GPS_ENTER_HOME['device']) - state_name = hass.states.get('{}.{}'.format( - 'device_tracker', device_name)).state - assert STATE_HOME == state_name - assert len(hass.data[DATA_DISPATCHER][TRACKER_UPDATE]) == 1 + state_1 = hass.states.get('{}.{}'.format('device_tracker', device_name)) + assert STATE_HOME == state_1.state + assert len(hass.data[DOMAIN]['devices']) == 1 entry = hass.config_entries.async_entries(DOMAIN)[0] - assert await geofency.async_unload_entry(hass, entry) + assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() - assert not hass.data[DATA_DISPATCHER][TRACKER_UPDATE] + assert len(hass.data[DOMAIN]['devices']) == 0 + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state_2 = hass.states.get('{}.{}'.format('device_tracker', device_name)) + assert state_2 is not None + assert state_1 is not state_2 + + assert STATE_HOME == state_2.state + assert state_2.attributes['latitude'] == HOME_LATITUDE + assert state_2.attributes['longitude'] == HOME_LONGITUDE From 704cdac874bde0ba333da07ca386330e3e79000f Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 3 Jun 2019 08:36:38 +0000 Subject: [PATCH 214/232] Bumped version to 0.94.0b5 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index e8ce6c04f7a..aabee94dc39 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 94 -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 9ed5b70d01a5562508880c7a89cc722b9460ac13 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Mon, 3 Jun 2019 18:26:01 +0200 Subject: [PATCH 215/232] deCONZ migrate to SSDP discovery (#24252) * Migrate deCONZ to use new SSDP discovery Add new discovery info manufacturer URL to be able to separate Hue and deCONZ bridges * Mark deCONZ as migrated in Discovery component * Fix tests * Fix Hue discovery ignore deCONZ bridge * Less snake more badger * Mushroom * Fix indentation * Config flow ignore manufacturer url that is not philips --- .../components/deconz/config_flow.py | 21 +++++++++---- homeassistant/components/deconz/manifest.json | 5 ++++ homeassistant/components/deconz/strings.json | 6 ++-- .../components/discovery/__init__.py | 3 +- homeassistant/components/hue/config_flow.py | 6 ++++ homeassistant/components/hue/strings.json | 3 +- homeassistant/components/ssdp/__init__.py | 2 ++ homeassistant/generated/ssdp.py | 1 + tests/components/deconz/test_config_flow.py | 30 +++++++++++++++---- tests/components/hue/test_config_flow.py | 21 +++++++++++-- 10 files changed, 78 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index 24eb3dd4d5d..cf172ad7991 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -9,12 +9,14 @@ from pydeconz.utils import ( async_discovery, async_get_api_key, async_get_bridgeid) from homeassistant import config_entries +from homeassistant.components.ssdp import ATTR_MANUFACTURERURL, ATTR_SERIAL 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_BRIDGEID, DEFAULT_PORT, DOMAIN +DECONZ_MANUFACTURERURL = 'http://www.dresden-elektronik.de' CONF_SERIAL = 'serial' @@ -149,12 +151,12 @@ class DeconzFlowHandler(config_entries.ConfigFlow): entry.data[CONF_HOST] = host self.hass.config_entries.async_update_entry(entry) - async def async_step_discovery(self, discovery_info): - """Prepare configuration for a discovered deCONZ bridge. + async def async_step_ssdp(self, discovery_info): + """Handle a discovered deCONZ bridge.""" + if discovery_info[ATTR_MANUFACTURERURL] != DECONZ_MANUFACTURERURL: + return self.async_abort(reason='not_deconz_bridge') - This flow is triggered by the discovery component. - """ - bridgeid = discovery_info[CONF_SERIAL] + bridgeid = discovery_info[ATTR_SERIAL] gateway_entries = configured_gateways(self.hass) if bridgeid in gateway_entries: @@ -162,10 +164,17 @@ class DeconzFlowHandler(config_entries.ConfigFlow): await self._update_entry(entry, discovery_info[CONF_HOST]) return self.async_abort(reason='updated_instance') + # pylint: disable=unsupported-assignment-operation + self.context[ATTR_SERIAL] = bridgeid + + if any(bridgeid == flow['context'][ATTR_SERIAL] + for flow in self._async_in_progress()): + return self.async_abort(reason='already_in_progress') + deconz_config = { CONF_HOST: discovery_info[CONF_HOST], CONF_PORT: discovery_info[CONF_PORT], - CONF_BRIDGEID: discovery_info[CONF_SERIAL] + CONF_BRIDGEID: bridgeid } return await self.async_step_import(deconz_config) diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json index 08a01cd1379..56ea52b7693 100644 --- a/homeassistant/components/deconz/manifest.json +++ b/homeassistant/components/deconz/manifest.json @@ -6,6 +6,11 @@ "requirements": [ "pydeconz==59" ], + "ssdp": { + "manufacturer": [ + "Royal Philips Electronics" + ] + }, "dependencies": [], "codeowners": [ "@kane610" diff --git a/homeassistant/components/deconz/strings.json b/homeassistant/components/deconz/strings.json index 16177dbd3cc..d1c70793063 100644 --- a/homeassistant/components/deconz/strings.json +++ b/homeassistant/components/deconz/strings.json @@ -34,9 +34,11 @@ }, "abort": { "already_configured": "Bridge is already configured", + "already_in_progress": "Config flow for bridge is already in progress.", "no_bridges": "No deCONZ bridges discovered", - "updated_instance": "Updated deCONZ instance with new host address", - "one_instance_only": "Component only supports one deCONZ instance" + "not_deconz_bridge": "Not a deCONZ bridge", + "one_instance_only": "Component only supports one deCONZ instance", + "updated_instance": "Updated deCONZ instance with new host address" } } } diff --git a/homeassistant/components/discovery/__init__.py b/homeassistant/components/discovery/__init__.py index ee6a8590515..0541b5d223a 100644 --- a/homeassistant/components/discovery/__init__.py +++ b/homeassistant/components/discovery/__init__.py @@ -25,7 +25,6 @@ DOMAIN = 'discovery' SCAN_INTERVAL = timedelta(seconds=300) SERVICE_APPLE_TV = 'apple_tv' SERVICE_DAIKIN = 'daikin' -SERVICE_DECONZ = 'deconz' SERVICE_DLNA_DMR = 'dlna_dmr' SERVICE_ENIGMA2 = 'enigma2' SERVICE_FREEBOX = 'freebox' @@ -48,7 +47,6 @@ SERVICE_XIAOMI_GW = 'xiaomi_gw' CONFIG_ENTRY_HANDLERS = { SERVICE_DAIKIN: 'daikin', - SERVICE_DECONZ: 'deconz', 'google_cast': 'cast', SERVICE_HEOS: 'heos', SERVICE_TELLDUSLIVE: 'tellduslive', @@ -98,6 +96,7 @@ OPTIONAL_SERVICE_HANDLERS = { MIGRATED_SERVICE_HANDLERS = { 'axis': None, + 'deconz': None, 'esphome': None, 'ikea_tradfri': None, 'homekit': None, diff --git a/homeassistant/components/hue/config_flow.py b/homeassistant/components/hue/config_flow.py index 4167027bf89..9c81d144d1c 100644 --- a/homeassistant/components/hue/config_flow.py +++ b/homeassistant/components/hue/config_flow.py @@ -8,6 +8,7 @@ import async_timeout import voluptuous as vol from homeassistant import config_entries +from homeassistant.components.ssdp import ATTR_MANUFACTURERURL from homeassistant.core import callback from homeassistant.helpers import aiohttp_client @@ -15,6 +16,8 @@ from .bridge import get_bridge from .const import DOMAIN, LOGGER from .errors import AuthenticationRequired, CannotConnect +HUE_MANUFACTURERURL = 'http://www.philips.com' + @callback def configured_hosts(hass): @@ -143,6 +146,9 @@ class HueFlowHandler(config_entries.ConfigFlow): This flow is triggered by the SSDP component. It will check if the host is already configured and delegate to the import step if not. """ + if discovery_info[ATTR_MANUFACTURERURL] != HUE_MANUFACTURERURL: + return self.async_abort(reason='not_hue_bridge') + # Filter out emulated Hue if "HASS Bridge" in discovery_info.get('name', ''): return self.async_abort(reason='already_configured') diff --git a/homeassistant/components/hue/strings.json b/homeassistant/components/hue/strings.json index 079ac1a2b8d..78b990d5f42 100644 --- a/homeassistant/components/hue/strings.json +++ b/homeassistant/components/hue/strings.json @@ -24,7 +24,8 @@ "unknown": "Unknown error occurred", "cannot_connect": "Unable to connect to the bridge", "already_configured": "Bridge is already configured", - "already_in_progress": "Config flow for bridge is already in progress." + "already_in_progress": "Config flow for bridge is already in progress.", + "not_hue_bridge": "Not a Hue bridge" } } } diff --git a/homeassistant/components/ssdp/__init__.py b/homeassistant/components/ssdp/__init__.py index aecca614e73..e250b9c16fb 100644 --- a/homeassistant/components/ssdp/__init__.py +++ b/homeassistant/components/ssdp/__init__.py @@ -23,6 +23,7 @@ ATTR_MODEL_NAME = 'model_name' ATTR_MODEL_NUMBER = 'model_number' ATTR_SERIAL = 'serial_number' ATTR_MANUFACTURER = 'manufacturer' +ATTR_MANUFACTURERURL = 'manufacturerURL' ATTR_UDN = 'udn' ATTR_UPNP_DEVICE_TYPE = 'upnp_device_type' @@ -164,6 +165,7 @@ def info_from_entry(entry, device_info): info[ATTR_MODEL_NUMBER] = device_info.get('modelNumber') info[ATTR_SERIAL] = device_info.get('serialNumber') info[ATTR_MANUFACTURER] = device_info.get('manufacturer') + info[ATTR_MANUFACTURERURL] = device_info.get('manufacturerURL') info[ATTR_UDN] = device_info.get('UDN') info[ATTR_UPNP_DEVICE_TYPE] = device_info.get('deviceType') diff --git a/homeassistant/generated/ssdp.py b/homeassistant/generated/ssdp.py index 897f68a6521..cc1d286bf5f 100644 --- a/homeassistant/generated/ssdp.py +++ b/homeassistant/generated/ssdp.py @@ -8,6 +8,7 @@ SSDP = { "device_type": {}, "manufacturer": { "Royal Philips Electronics": [ + "deconz", "hue" ] }, diff --git a/tests/components/deconz/test_config_flow.py b/tests/components/deconz/test_config_flow.py index 46b0084b01b..2b9f2c013b0 100644 --- a/tests/components/deconz/test_config_flow.py +++ b/tests/components/deconz/test_config_flow.py @@ -168,22 +168,38 @@ async def test_link_no_api_key(hass): assert result['errors'] == {'base': 'no_key'} -async def test_bridge_discovery(hass): - """Test a bridge being discovered.""" +async def test_bridge_ssdp_discovery(hass): + """Test a bridge being discovered over ssdp.""" result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, data={ config_flow.CONF_HOST: '1.2.3.4', config_flow.CONF_PORT: 80, - config_flow.CONF_SERIAL: 'id', + config_flow.ATTR_SERIAL: 'id', + config_flow.ATTR_MANUFACTURERURL: + config_flow.DECONZ_MANUFACTURERURL }, - context={'source': 'discovery'} + context={'source': 'ssdp'} ) assert result['type'] == 'form' assert result['step_id'] == 'link' +async def test_bridge_ssdp_discovery_not_deconz_bridge(hass): + """Test a non deconz bridge being discovered over ssdp.""" + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + data={ + config_flow.ATTR_MANUFACTURERURL: 'not deconz bridge' + }, + context={'source': 'ssdp'} + ) + + assert result['type'] == 'abort' + assert result['reason'] == 'not_deconz_bridge' + + async def test_bridge_discovery_update_existing_entry(hass): """Test if a discovered bridge has already been configured.""" entry = MockConfigEntry(domain=config_flow.DOMAIN, data={ @@ -195,9 +211,11 @@ async def test_bridge_discovery_update_existing_entry(hass): config_flow.DOMAIN, data={ config_flow.CONF_HOST: 'mock-deconz', - config_flow.CONF_SERIAL: 'id', + config_flow.ATTR_SERIAL: 'id', + config_flow.ATTR_MANUFACTURERURL: + config_flow.DECONZ_MANUFACTURERURL }, - context={'source': 'discovery'} + context={'source': 'ssdp'} ) assert result['type'] == 'abort' diff --git a/tests/components/hue/test_config_flow.py b/tests/components/hue/test_config_flow.py index 37cece0bbd8..b7736e62390 100644 --- a/tests/components/hue/test_config_flow.py +++ b/tests/components/hue/test_config_flow.py @@ -195,13 +195,26 @@ async def test_bridge_ssdp(hass): side_effect=errors.AuthenticationRequired): result = await flow.async_step_ssdp({ 'host': '0.0.0.0', - 'serial': '1234' + 'serial': '1234', + 'manufacturerURL': config_flow.HUE_MANUFACTURERURL }) assert result['type'] == 'form' assert result['step_id'] == 'link' +async def test_bridge_ssdp_discover_other_bridge(hass): + """Test that discovery ignores other bridges.""" + flow = config_flow.HueFlowHandler() + flow.hass = hass + + result = await flow.async_step_ssdp({ + 'manufacturerURL': 'http://www.notphilips.com' + }) + + assert result['type'] == 'abort' + + async def test_bridge_ssdp_emulated_hue(hass): """Test if discovery info is from an emulated hue instance.""" flow = config_flow.HueFlowHandler() @@ -211,7 +224,8 @@ async def test_bridge_ssdp_emulated_hue(hass): result = await flow.async_step_ssdp({ 'name': 'HASS Bridge', 'host': '0.0.0.0', - 'serial': '1234' + 'serial': '1234', + 'manufacturerURL': config_flow.HUE_MANUFACTURERURL }) assert result['type'] == 'abort' @@ -229,7 +243,8 @@ async def test_bridge_ssdp_already_configured(hass): result = await flow.async_step_ssdp({ 'host': '0.0.0.0', - 'serial': '1234' + 'serial': '1234', + 'manufacturerURL': config_flow.HUE_MANUFACTURERURL }) assert result['type'] == 'abort' From 5085ce8ab10cff4733961e47d7ac72f3b1710987 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Mon, 3 Jun 2019 21:40:16 +0200 Subject: [PATCH 216/232] Add temperature sensor support to google smarthome thermostat device (#24264) * Add temperature sensor support to google smarthome thermostat device * fix lint for trait_test * Reset temperature unit in tests * Address comment --- .../components/google_assistant/const.py | 2 + .../components/google_assistant/trait.py | 158 +++++++++++------- .../components/google_assistant/test_trait.py | 33 ++++ 3 files changed, 133 insertions(+), 60 deletions(-) diff --git a/homeassistant/components/google_assistant/const.py b/homeassistant/components/google_assistant/const.py index 92afe90a5ac..ebded79447e 100644 --- a/homeassistant/components/google_assistant/const.py +++ b/homeassistant/components/google_assistant/const.py @@ -12,6 +12,7 @@ from homeassistant.components import ( media_player, scene, script, + sensor, switch, vacuum, ) @@ -108,6 +109,7 @@ DEVICE_CLASS_TO_GOOGLE_TYPES = { (binary_sensor.DOMAIN, binary_sensor.DEVICE_CLASS_WINDOW): TYPE_SENSOR, (media_player.DOMAIN, media_player.DEVICE_CLASS_TV): TYPE_TV, (media_player.DOMAIN, media_player.DEVICE_CLASS_SPEAKER): TYPE_SPEAKER, + (sensor.DOMAIN, sensor.DEVICE_CLASS_TEMPERATURE): TYPE_SENSOR, } CHALLENGE_ACK_NEEDED = 'ackNeeded' diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index f9590a07b95..7776daf65c9 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -13,6 +13,7 @@ from homeassistant.components import ( lock, scene, script, + sensor, switch, vacuum, ) @@ -550,89 +551,126 @@ class TemperatureSettingTrait(_Trait): @staticmethod def supported(domain, features, device_class): """Test if state is supported.""" - if domain != climate.DOMAIN: - return False + if domain == climate.DOMAIN: + return features & climate.SUPPORT_OPERATION_MODE - return features & climate.SUPPORT_OPERATION_MODE + return (domain == sensor.DOMAIN + and device_class == sensor.DEVICE_CLASS_TEMPERATURE) def sync_attributes(self): """Return temperature point and modes attributes for a sync request.""" - modes = [] - supported = self.state.attributes.get(ATTR_SUPPORTED_FEATURES) + response = {} + attrs = self.state.attributes + domain = self.state.domain + response['thermostatTemperatureUnit'] = _google_temp_unit( + self.hass.config.units.temperature_unit) - if supported & climate.SUPPORT_ON_OFF != 0: - modes.append(STATE_OFF) - modes.append(STATE_ON) + if domain == sensor.DOMAIN: + device_class = attrs.get(ATTR_DEVICE_CLASS) + if device_class == sensor.DEVICE_CLASS_TEMPERATURE: + response["queryOnlyTemperatureSetting"] = True - if supported & climate.SUPPORT_OPERATION_MODE != 0: - for mode in self.state.attributes.get(climate.ATTR_OPERATION_LIST, - []): - google_mode = self.hass_to_google.get(mode) - if google_mode and google_mode not in modes: - modes.append(google_mode) + elif domain == climate.DOMAIN: + modes = [] + supported = attrs.get(ATTR_SUPPORTED_FEATURES) - return { - 'availableThermostatModes': ','.join(modes), - 'thermostatTemperatureUnit': _google_temp_unit( - self.hass.config.units.temperature_unit) - } + if supported & climate.SUPPORT_ON_OFF != 0: + modes.append(STATE_OFF) + modes.append(STATE_ON) + + if supported & climate.SUPPORT_OPERATION_MODE != 0: + for mode in attrs.get(climate.ATTR_OPERATION_LIST, []): + google_mode = self.hass_to_google.get(mode) + if google_mode and google_mode not in modes: + modes.append(google_mode) + response['availableThermostatModes'] = ','.join(modes) + + return response def query_attributes(self): """Return temperature point and modes query attributes.""" - attrs = self.state.attributes response = {} - - operation = attrs.get(climate.ATTR_OPERATION_MODE) - supported = self.state.attributes.get(ATTR_SUPPORTED_FEATURES) - - if (supported & climate.SUPPORT_ON_OFF - and self.state.state == STATE_OFF): - response['thermostatMode'] = 'off' - elif (supported & climate.SUPPORT_OPERATION_MODE and - operation in self.hass_to_google): - response['thermostatMode'] = self.hass_to_google[operation] - elif supported & climate.SUPPORT_ON_OFF: - response['thermostatMode'] = 'on' - + attrs = self.state.attributes + domain = self.state.domain unit = self.hass.config.units.temperature_unit + if domain == sensor.DOMAIN: + device_class = attrs.get(ATTR_DEVICE_CLASS) + if device_class == sensor.DEVICE_CLASS_TEMPERATURE: + current_temp = self.state.state + if current_temp is not None: + response['thermostatTemperatureAmbient'] = \ + round(temp_util.convert( + float(current_temp), + unit, + TEMP_CELSIUS + ), 1) - current_temp = attrs.get(climate.ATTR_CURRENT_TEMPERATURE) - if current_temp is not None: - response['thermostatTemperatureAmbient'] = \ - round(temp_util.convert(current_temp, unit, TEMP_CELSIUS), 1) + elif domain == climate.DOMAIN: + operation = attrs.get(climate.ATTR_OPERATION_MODE) + supported = attrs.get(ATTR_SUPPORTED_FEATURES) - current_humidity = attrs.get(climate.ATTR_CURRENT_HUMIDITY) - if current_humidity is not None: - response['thermostatHumidityAmbient'] = current_humidity + if (supported & climate.SUPPORT_ON_OFF + and self.state.state == STATE_OFF): + response['thermostatMode'] = 'off' + elif (supported & climate.SUPPORT_OPERATION_MODE + and operation in self.hass_to_google): + response['thermostatMode'] = self.hass_to_google[operation] + elif supported & climate.SUPPORT_ON_OFF: + response['thermostatMode'] = 'on' - if operation == climate.STATE_AUTO: - if (supported & climate.SUPPORT_TARGET_TEMPERATURE_HIGH and - supported & climate.SUPPORT_TARGET_TEMPERATURE_LOW): - response['thermostatTemperatureSetpointHigh'] = \ + current_temp = attrs.get(climate.ATTR_CURRENT_TEMPERATURE) + if current_temp is not None: + response['thermostatTemperatureAmbient'] = \ round(temp_util.convert( - attrs[climate.ATTR_TARGET_TEMP_HIGH], - unit, TEMP_CELSIUS), 1) - response['thermostatTemperatureSetpointLow'] = \ - round(temp_util.convert( - attrs[climate.ATTR_TARGET_TEMP_LOW], - unit, TEMP_CELSIUS), 1) + current_temp, + unit, + TEMP_CELSIUS + ), 1) + + current_humidity = attrs.get(climate.ATTR_CURRENT_HUMIDITY) + if current_humidity is not None: + response['thermostatHumidityAmbient'] = current_humidity + + if operation == climate.STATE_AUTO: + if (supported & climate.SUPPORT_TARGET_TEMPERATURE_HIGH and + supported & climate.SUPPORT_TARGET_TEMPERATURE_LOW): + response['thermostatTemperatureSetpointHigh'] = \ + round(temp_util.convert( + attrs[climate.ATTR_TARGET_TEMP_HIGH], + unit, TEMP_CELSIUS), 1) + response['thermostatTemperatureSetpointLow'] = \ + round(temp_util.convert( + attrs[climate.ATTR_TARGET_TEMP_LOW], + unit, TEMP_CELSIUS), 1) + else: + target_temp = attrs.get(ATTR_TEMPERATURE) + if target_temp is not None: + target_temp = round( + temp_util.convert( + target_temp, + unit, + TEMP_CELSIUS + ), 1) + response['thermostatTemperatureSetpointHigh'] = \ + target_temp + response['thermostatTemperatureSetpointLow'] = \ + target_temp else: target_temp = attrs.get(ATTR_TEMPERATURE) if target_temp is not None: - target_temp = round( + response['thermostatTemperatureSetpoint'] = round( temp_util.convert(target_temp, unit, TEMP_CELSIUS), 1) - response['thermostatTemperatureSetpointHigh'] = target_temp - response['thermostatTemperatureSetpointLow'] = target_temp - else: - target_temp = attrs.get(ATTR_TEMPERATURE) - if target_temp is not None: - response['thermostatTemperatureSetpoint'] = round( - temp_util.convert(target_temp, unit, TEMP_CELSIUS), 1) return response async def execute(self, command, data, params, challenge): """Execute a temperature point or mode command.""" + domain = self.state.domain + if domain == sensor.DOMAIN: + raise SmartHomeError( + ERR_NOT_SUPPORTED, + 'Execute is not supported by sensor') + # All sent in temperatures are always in Celsius unit = self.hass.config.units.temperature_unit min_temp = self.state.attributes[climate.ATTR_MIN_TEMP] @@ -687,8 +725,8 @@ class TemperatureSettingTrait(_Trait): ATTR_ENTITY_ID: self.state.entity_id, } - if(supported & climate.SUPPORT_TARGET_TEMPERATURE_HIGH and - supported & climate.SUPPORT_TARGET_TEMPERATURE_LOW): + if(supported & climate.SUPPORT_TARGET_TEMPERATURE_HIGH + and supported & climate.SUPPORT_TARGET_TEMPERATURE_LOW): svc_data[climate.ATTR_TARGET_TEMP_HIGH] = temp_high svc_data[climate.ATTR_TARGET_TEMP_LOW] = temp_low else: diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index 28cab008201..6b1b6a7c9f4 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -14,6 +14,7 @@ from homeassistant.components import ( media_player, scene, script, + sensor, switch, vacuum, group, @@ -1380,3 +1381,35 @@ async def test_volume_media_player_relative(hass): ATTR_ENTITY_ID: 'media_player.bla', media_player.ATTR_MEDIA_VOLUME_LEVEL: .5 } + + +async def test_temperature_setting_sensor(hass): + """Test TemperatureSetting trait support for temperature sensor.""" + assert helpers.get_google_type(sensor.DOMAIN, + sensor.DEVICE_CLASS_TEMPERATURE) is not None + assert not trait.TemperatureSettingTrait.supported( + sensor.DOMAIN, + 0, + sensor.DEVICE_CLASS_HUMIDITY + ) + assert trait.TemperatureSettingTrait.supported( + sensor.DOMAIN, + 0, + sensor.DEVICE_CLASS_TEMPERATURE + ) + + hass.config.units.temperature_unit = TEMP_FAHRENHEIT + + trt = trait.TemperatureSettingTrait(hass, State('sensor.test', "70", { + ATTR_DEVICE_CLASS: sensor.DEVICE_CLASS_TEMPERATURE, + }), BASIC_CONFIG) + + assert trt.sync_attributes() == { + 'queryOnlyTemperatureSetting': True, + 'thermostatTemperatureUnit': 'F', + } + + assert trt.query_attributes() == { + 'thermostatTemperatureAmbient': 21.1 + } + hass.config.units.temperature_unit = TEMP_CELSIUS From ee71d2ca60aacdb77f020ccd66fe7cd8db0c90f5 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Mon, 3 Jun 2019 16:30:30 +0200 Subject: [PATCH 217/232] Bump aioesphomeapi to 2.1.0 (#24278) * Bump aioesphomeapi to 2.1.0 * Update requirements txt --- homeassistant/components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 71d233fee2e..a986a864189 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/components/esphome", "requirements": [ - "aioesphomeapi==2.0.1" + "aioesphomeapi==2.1.0" ], "dependencies": [], "zeroconf": ["_esphomelib._tcp.local."], diff --git a/requirements_all.txt b/requirements_all.txt index 7adb9d4bed0..30449f87738 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -126,7 +126,7 @@ aiobotocore==0.10.2 aiodns==2.0.0 # homeassistant.components.esphome -aioesphomeapi==2.0.1 +aioesphomeapi==2.1.0 # homeassistant.components.freebox aiofreepybox==0.0.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6c5f41a054f..e2ec52ecf03 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -45,7 +45,7 @@ aioautomatic==0.6.5 aiobotocore==0.10.2 # homeassistant.components.esphome -aioesphomeapi==2.0.1 +aioesphomeapi==2.1.0 # homeassistant.components.emulated_hue # homeassistant.components.http From 014cc14b7e0c4f7d366be485f1420572caa4ebcd Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 3 Jun 2019 11:43:13 -0700 Subject: [PATCH 218/232] Fix cors on the index view (#24283) --- homeassistant/components/http/cors.py | 5 +++++ tests/components/http/test_cors.py | 12 ++++++++++++ 2 files changed, 17 insertions(+) diff --git a/homeassistant/components/http/cors.py b/homeassistant/components/http/cors.py index 1ef70b5e022..419b62be2c6 100644 --- a/homeassistant/components/http/cors.py +++ b/homeassistant/components/http/cors.py @@ -1,4 +1,5 @@ """Provide CORS support for the HTTP component.""" +from aiohttp.web_urldispatcher import Resource, ResourceRoute from aiohttp.hdrs import ACCEPT, CONTENT_TYPE, ORIGIN, AUTHORIZATION from homeassistant.const import ( @@ -8,6 +9,7 @@ from homeassistant.core import callback ALLOWED_CORS_HEADERS = [ ORIGIN, ACCEPT, HTTP_HEADER_X_REQUESTED_WITH, CONTENT_TYPE, HTTP_HEADER_HA_AUTH, AUTHORIZATION] +VALID_CORS_TYPES = (Resource, ResourceRoute) @callback @@ -31,6 +33,9 @@ def setup_cors(app, origins): else: path = route + if not isinstance(path, VALID_CORS_TYPES): + return + path = path.canonical if path in cors_added: diff --git a/tests/components/http/test_cors.py b/tests/components/http/test_cors.py index e17fb105efe..d9fa6c11309 100644 --- a/tests/components/http/test_cors.py +++ b/tests/components/http/test_cors.py @@ -140,3 +140,15 @@ async def test_cors_middleware_with_cors_allowed_view(hass): hass.http.app._on_startup.freeze() await hass.http.app.startup() + + +async def test_cors_works_with_frontend(hass, hass_client): + """Test CORS works with the frontend.""" + assert await async_setup_component(hass, 'frontend', { + 'http': { + 'cors_allowed_origins': ['http://home-assistant.io'] + } + }) + client = await hass_client() + resp = await client.get('/') + assert resp.status == 200 From 4a71593ffdd476d0de3d36142fd005507c5793de Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 3 Jun 2019 12:37:27 -0700 Subject: [PATCH 219/232] Remove deps folder in config when on Docker (#24284) * Remove deps folder in config * Fix tests * Fix tests with docker check --- homeassistant/config.py | 16 +++++++++++++--- tests/test_config.py | 25 ++++++++++++++++++++++++- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/homeassistant/config.py b/homeassistant/config.py index 9465025cfd2..7e8bcec08a5 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -1,7 +1,7 @@ """Module to help with parsing and generating configuration files.""" from collections import OrderedDict # pylint: disable=no-name-in-module -from distutils.version import LooseVersion # pylint: disable=import-error +from distutils.version import StrictVersion # pylint: disable=import-error import logging import os import re @@ -31,6 +31,7 @@ from homeassistant.loader import ( Integration, async_get_integration, IntegrationNotFound ) from homeassistant.util.yaml import load_yaml, SECRET_YAML +from homeassistant.util.package import is_docker_env import homeassistant.helpers.config_validation as cv from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM from homeassistant.helpers.entity_values import EntityValues @@ -333,13 +334,15 @@ def process_ha_config_upgrade(hass: HomeAssistant) -> None: _LOGGER.info("Upgrading configuration directory from %s to %s", conf_version, __version__) - if LooseVersion(conf_version) < LooseVersion('0.50'): + version_obj = StrictVersion(conf_version) + + if version_obj < StrictVersion('0.50'): # 0.50 introduced persistent deps dir. lib_path = hass.config.path('deps') if os.path.isdir(lib_path): shutil.rmtree(lib_path) - if LooseVersion(conf_version) < LooseVersion('0.92'): + if version_obj < StrictVersion('0.92'): # 0.92 moved google/tts.py to google_translate/tts.py config_path = find_config_file(hass.config.config_dir) assert config_path is not None @@ -357,6 +360,13 @@ def process_ha_config_upgrade(hass: HomeAssistant) -> None: _LOGGER.exception("Migrating to google_translate tts failed") pass + if version_obj < StrictVersion('0.94.0b6') and is_docker_env(): + # In 0.94 we no longer install packages inside the deps folder when + # running inside a Docker container. + lib_path = hass.config.path('deps') + if os.path.isdir(lib_path): + shutil.rmtree(lib_path) + with open(version_path, 'wt') as outp: outp.write(__version__) diff --git a/tests/test_config.py b/tests/test_config.py index 5579679937b..29058f185ad 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -256,7 +256,8 @@ async def test_entity_customization(hass): @mock.patch('homeassistant.config.shutil') @mock.patch('homeassistant.config.os') -def test_remove_lib_on_upgrade(mock_os, mock_shutil, hass): +@mock.patch('homeassistant.config.is_docker_env', return_value=False) +def test_remove_lib_on_upgrade(mock_docker, mock_os, mock_shutil, hass): """Test removal of library on upgrade from before 0.50.""" ha_version = '0.49.0' mock_os.path.isdir = mock.Mock(return_value=True) @@ -275,6 +276,28 @@ def test_remove_lib_on_upgrade(mock_os, mock_shutil, hass): assert mock_shutil.rmtree.call_args == mock.call(hass_path) +@mock.patch('homeassistant.config.shutil') +@mock.patch('homeassistant.config.os') +@mock.patch('homeassistant.config.is_docker_env', return_value=True) +def test_remove_lib_on_upgrade_94(mock_docker, mock_os, mock_shutil, hass): + """Test removal of library on upgrade from before 0.94 and in Docker.""" + ha_version = '0.94.0b5' + mock_os.path.isdir = mock.Mock(return_value=True) + mock_open = mock.mock_open() + with mock.patch('homeassistant.config.open', mock_open, create=True): + opened_file = mock_open.return_value + # pylint: disable=no-member + opened_file.readline.return_value = ha_version + hass.config.path = mock.Mock() + config_util.process_ha_config_upgrade(hass) + hass_path = hass.config.path.return_value + + assert mock_os.path.isdir.call_count == 1 + assert mock_os.path.isdir.call_args == mock.call(hass_path) + assert mock_shutil.rmtree.call_count == 1 + assert mock_shutil.rmtree.call_args == mock.call(hass_path) + + def test_process_config_upgrade(hass): """Test update of version on upgrade.""" ha_version = '0.92.0' From 281fe93a265881502ceccfd212d57a805186dfb1 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 3 Jun 2019 12:41:45 -0700 Subject: [PATCH 220/232] Bumped version to 0.94.0b6 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index aabee94dc39..eea2602ce49 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 94 -PATCH_VERSION = '0b5' +PATCH_VERSION = '0b6' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From bad920fa87db55089eb27c23206a2d0252c872b1 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 4 Jun 2019 12:42:45 +0200 Subject: [PATCH 221/232] Bumped version to 0.94.0b7 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index eea2602ce49..56802281e81 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 94 -PATCH_VERSION = '0b6' +PATCH_VERSION = '0b7' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From b67d32824c22873d9b5e7e886a9ef3f644c9f427 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 4 Jun 2019 08:50:25 -0700 Subject: [PATCH 222/232] Updated frontend to 20190604.0 --- homeassistant/components/frontend/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 820f17a98bf..0d517aa6560 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/components/frontend", "requirements": [ - "home-assistant-frontend==20190602.0" + "home-assistant-frontend==20190604.0" ], "dependencies": [ "api", diff --git a/requirements_all.txt b/requirements_all.txt index 30449f87738..4f92fed078c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -577,7 +577,7 @@ hole==0.3.0 holidays==0.9.10 # homeassistant.components.frontend -home-assistant-frontend==20190602.0 +home-assistant-frontend==20190604.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e2ec52ecf03..b43ef070d6b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -148,7 +148,7 @@ hdate==0.8.7 holidays==0.9.10 # homeassistant.components.frontend -home-assistant-frontend==20190602.0 +home-assistant-frontend==20190604.0 # homeassistant.components.homekit_controller homekit[IP]==0.14.0 From bb0867f1a89ef545409b5c9ec6bdfc9efd6623a8 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 4 Jun 2019 10:18:26 -0700 Subject: [PATCH 223/232] Guard against bad states in Mobile App/OwnTracks (#24292) --- homeassistant/components/mobile_app/device_tracker.py | 6 +++--- homeassistant/components/owntracks/device_tracker.py | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/mobile_app/device_tracker.py b/homeassistant/components/mobile_app/device_tracker.py index 22435fadc16..7fb76f3af41 100644 --- a/homeassistant/components/mobile_app/device_tracker.py +++ b/homeassistant/components/mobile_app/device_tracker.py @@ -145,9 +145,9 @@ class MobileAppEntity(DeviceTrackerEntity, RestoreEntity): attr = state.attributes data = { - ATTR_GPS: (attr[ATTR_LATITUDE], attr[ATTR_LONGITUDE]), - ATTR_GPS_ACCURACY: attr[ATTR_GPS_ACCURACY], - ATTR_BATTERY: attr[ATTR_BATTERY_LEVEL], + ATTR_GPS: (attr.get(ATTR_LATITUDE), attr.get(ATTR_LONGITUDE)), + ATTR_GPS_ACCURACY: attr.get(ATTR_GPS_ACCURACY), + ATTR_BATTERY: attr.get(ATTR_BATTERY_LEVEL), } data.update({key: attr[key] for key in attr if key in ATTR_KEYS}) self._data = data diff --git a/homeassistant/components/owntracks/device_tracker.py b/homeassistant/components/owntracks/device_tracker.py index d74fea43c29..ed2749262bd 100644 --- a/homeassistant/components/owntracks/device_tracker.py +++ b/homeassistant/components/owntracks/device_tracker.py @@ -153,10 +153,10 @@ class OwnTracksEntity(DeviceTrackerEntity, RestoreEntity): attr = state.attributes self._data = { 'host_name': state.name, - 'gps': (attr[ATTR_LATITUDE], attr[ATTR_LONGITUDE]), - 'gps_accuracy': attr[ATTR_GPS_ACCURACY], - 'battery': attr[ATTR_BATTERY_LEVEL], - 'source_type': attr[ATTR_SOURCE_TYPE], + 'gps': (attr.get(ATTR_LATITUDE), attr.get(ATTR_LONGITUDE)), + 'gps_accuracy': attr.get(ATTR_GPS_ACCURACY), + 'battery': attr.get(ATTR_BATTERY_LEVEL), + 'source_type': attr.get(ATTR_SOURCE_TYPE), } @callback From d17f27b65cf4f09f589a82461900b1cd8b4a95e2 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 4 Jun 2019 20:04:20 +0200 Subject: [PATCH 224/232] Create progress file for pip installs (#24297) * Create progress file for pip installs * fix dedlock * unflacky test * Address comments * Lint * Types --- homeassistant/requirements.py | 20 ++++++++++++++++---- tests/test_requirements.py | 22 +++++++++++++++++++++- 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/homeassistant/requirements.py b/homeassistant/requirements.py index 1164eff4eb8..2ab4fe28bdc 100644 --- a/homeassistant/requirements.py +++ b/homeassistant/requirements.py @@ -1,6 +1,6 @@ """Module to handle installing requirements.""" import asyncio -from functools import partial +from pathlib import Path import logging import os from typing import Any, Dict, List, Optional @@ -11,6 +11,7 @@ from homeassistant.core import HomeAssistant DATA_PIP_LOCK = 'pip_lock' DATA_PKG_CACHE = 'pkg_cache' CONSTRAINT_FILE = 'package_constraints.txt' +PROGRESS_FILE = '.pip_progress' _LOGGER = logging.getLogger(__name__) @@ -24,15 +25,16 @@ async def async_process_requirements(hass: HomeAssistant, name: str, if pip_lock is None: pip_lock = hass.data[DATA_PIP_LOCK] = asyncio.Lock() - pip_install = partial(pkg_util.install_package, - **pip_kwargs(hass.config.config_dir)) + kwargs = pip_kwargs(hass.config.config_dir) async with pip_lock: for req in requirements: if pkg_util.is_installed(req): continue - ret = await hass.async_add_executor_job(pip_install, req) + ret = await hass.async_add_executor_job( + _install, hass, req, kwargs + ) if not ret: _LOGGER.error("Not initializing %s because could not install " @@ -42,6 +44,16 @@ async def async_process_requirements(hass: HomeAssistant, name: str, return True +def _install(hass: HomeAssistant, req: str, kwargs: Dict) -> bool: + """Install requirement.""" + progress_path = Path(hass.config.path(PROGRESS_FILE)) + progress_path.touch() + try: + return pkg_util.install_package(req, **kwargs) + finally: + progress_path.unlink() + + def pip_kwargs(config_dir: Optional[str]) -> Dict[str, Any]: """Return keyword arguments for PIP install.""" is_docker = pkg_util.is_docker_env() diff --git a/tests/test_requirements.py b/tests/test_requirements.py index bbf86278bd2..fc9dee20ed2 100644 --- a/tests/test_requirements.py +++ b/tests/test_requirements.py @@ -1,10 +1,11 @@ """Test requirements module.""" import os +from pathlib import Path from unittest.mock import patch, call from homeassistant import setup from homeassistant.requirements import ( - CONSTRAINT_FILE, async_process_requirements) + CONSTRAINT_FILE, async_process_requirements, PROGRESS_FILE, _install) from tests.common import ( get_test_home_assistant, MockModule, mock_coro, mock_integration) @@ -143,3 +144,22 @@ async def test_install_on_docker(hass): constraints=os.path.join('ha_package_path', CONSTRAINT_FILE), no_cache_dir=True, ) + + +async def test_progress_lock(hass): + """Test an install attempt on an existing package.""" + progress_path = Path(hass.config.path(PROGRESS_FILE)) + kwargs = {'hello': 'world'} + + def assert_env(req, **passed_kwargs): + """Assert the env.""" + assert progress_path.exists() + assert req == 'hello' + assert passed_kwargs == kwargs + return True + + with patch('homeassistant.util.package.install_package', + side_effect=assert_env): + _install(hass, 'hello', kwargs) + + assert not progress_path.exists() From 185af1b42a72fdba6ce7419c46278b6ccb8b1ff1 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 4 Jun 2019 11:04:02 -0700 Subject: [PATCH 225/232] Run SSDP discovery in parallel (#24299) --- homeassistant/components/ssdp/__init__.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ssdp/__init__.py b/homeassistant/components/ssdp/__init__.py index e250b9c16fb..79c9cd94871 100644 --- a/homeassistant/components/ssdp/__init__.py +++ b/homeassistant/components/ssdp/__init__.py @@ -86,13 +86,16 @@ class Scanner: if not to_load: return - for entry, info, domains in to_load: + tasks = [] + for entry, info, domains in to_load: for domain in domains: _LOGGER.debug("Discovered %s at %s", domain, entry.location) - await self.hass.config_entries.flow.async_init( + tasks.append(self.hass.config_entries.flow.async_init( domain, context={'source': DOMAIN}, data=info - ) + )) + + await asyncio.wait(tasks) async def _process_entry(self, entry): """Process a single entry.""" From 389da16947a1bb24dee0e0137f3e013886ca948f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 4 Jun 2019 11:05:11 -0700 Subject: [PATCH 226/232] Upgrade Zeroconf to 0.23 (#24300) --- homeassistant/components/zeroconf/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index becd5d51c5a..1461a54d147 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -3,7 +3,7 @@ "name": "Zeroconf", "documentation": "https://www.home-assistant.io/components/zeroconf", "requirements": [ - "zeroconf==0.22.0" + "zeroconf==0.23.0" ], "dependencies": [ "api" diff --git a/requirements_all.txt b/requirements_all.txt index 4f92fed078c..debfb43b41e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1875,7 +1875,7 @@ youtube_dl==2019.05.11 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.22.0 +zeroconf==0.23.0 # homeassistant.components.zha zha-quirks==0.0.13 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b43ef070d6b..cd76ca3f748 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -352,7 +352,7 @@ vultr==0.1.2 wakeonlan==1.1.6 # homeassistant.components.zeroconf -zeroconf==0.22.0 +zeroconf==0.23.0 # homeassistant.components.zha zigpy-homeassistant==0.3.3 From 1096fe3d8707f7df5f077790dff393804c362c2d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 4 Jun 2019 11:06:25 -0700 Subject: [PATCH 227/232] Bumped version to 0.94.0b8 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 56802281e81..eae31b11ce4 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 94 -PATCH_VERSION = '0b7' +PATCH_VERSION = '0b8' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 3b60081e2ae3e4d01a2f5fa8ea2ef790aa1bd25f Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 4 Jun 2019 23:14:51 +0200 Subject: [PATCH 228/232] address is deprecated in favor of addresses (#24302) --- homeassistant/components/zeroconf/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index 2f93020b4d5..bdb1d52159c 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -127,7 +127,7 @@ def info_from_service(service): except UnicodeDecodeError: _LOGGER.warning("Unicode decode error on %s: %s", key, value) - address = service.address or service.address6 + address = service.addresses[0] info = { ATTR_HOST: str(ipaddress.ip_address(address)), From eca424656a4c7a58312ab2e17b1d5a9b9a8288d5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 4 Jun 2019 14:06:49 -0700 Subject: [PATCH 229/232] Fix OwnTracks race condition (#24303) * Fix OwnTracks race condition * Lint --- .../components/owntracks/__init__.py | 12 +++++++++- .../components/owntracks/device_tracker.py | 2 +- tests/components/owntracks/test_init.py | 23 ++++++++++++++++++- 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/owntracks/__init__.py b/homeassistant/components/owntracks/__init__.py index a4df4303fa8..1cc7a050aec 100644 --- a/homeassistant/components/owntracks/__init__.py +++ b/homeassistant/components/owntracks/__init__.py @@ -192,6 +192,7 @@ class OwnTracksContext: self.region_mapping = region_mapping self.events_only = events_only self.mqtt_topic = mqtt_topic + self._pending_msg = [] @callback def async_valid_accuracy(self, message): @@ -222,10 +223,19 @@ class OwnTracksContext: return True + @callback + def set_async_see(self, func): + """Set a new async_see function.""" + self.async_see = func + for msg in self._pending_msg: + func(**msg) + self._pending_msg.clear() + + # pylint: disable=method-hidden @callback def async_see(self, **data): """Send a see message to the device tracker.""" - raise NotImplementedError + self._pending_msg.append(data) @callback def async_see_beacons(self, hass, dev_id, kwargs_param): diff --git a/homeassistant/components/owntracks/device_tracker.py b/homeassistant/components/owntracks/device_tracker.py index ed2749262bd..742b7c34435 100644 --- a/homeassistant/components/owntracks/device_tracker.py +++ b/homeassistant/components/owntracks/device_tracker.py @@ -36,7 +36,7 @@ async def async_setup_entry(hass, entry, async_add_entities): ) async_add_entities([entity]) - hass.data[OT_DOMAIN]['context'].async_see = _receive_data + hass.data[OT_DOMAIN]['context'].set_async_see(_receive_data) # Restore previously loaded devices dev_reg = await device_registry.async_get_registry(hass) diff --git a/tests/components/owntracks/test_init.py b/tests/components/owntracks/test_init.py index fafe9678e78..b662bbcd6bd 100644 --- a/tests/components/owntracks/test_init.py +++ b/tests/components/owntracks/test_init.py @@ -4,7 +4,7 @@ import asyncio import pytest from homeassistant.setup import async_setup_component - +from homeassistant.components import owntracks from tests.common import mock_component, MockConfigEntry MINIMAL_LOCATION_MESSAGE = { @@ -160,3 +160,24 @@ def test_returns_error_missing_device(mock_client): json = yield from resp.json() assert json == [] + + +def test_context_delivers_pending_msg(): + """Test that context is able to hold pending messages while being init.""" + context = owntracks.OwnTracksContext( + None, None, None, None, None, None, None, None + ) + context.async_see(hello='world') + context.async_see(world='hello') + received = [] + + context.set_async_see(lambda **data: received.append(data)) + + assert len(received) == 2 + assert received[0] == {'hello': 'world'} + assert received[1] == {'world': 'hello'} + + received.clear() + + context.set_async_see(lambda **data: received.append(data)) + assert len(received) == 0 From 13c38335930157e3f41c5c0cdf3ba2875ae099ad Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 4 Jun 2019 14:34:06 -0700 Subject: [PATCH 230/232] Bumped version to 0.94.0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index eae31b11ce4..58897e78d0c 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 94 -PATCH_VERSION = '0b8' +PATCH_VERSION = '0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From ce93a332a7553790e377cb8b6b9f40082f2d615a Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 5 Jun 2019 09:37:57 +0200 Subject: [PATCH 231/232] Update and rename azure-pipelines.yml to azure-pipelines-release.yml --- ...pelines.yml => azure-pipelines-release.yml | 93 +------------------ 1 file changed, 2 insertions(+), 91 deletions(-) rename azure-pipelines.yml => azure-pipelines-release.yml (57%) diff --git a/azure-pipelines.yml b/azure-pipelines-release.yml similarity index 57% rename from azure-pipelines.yml rename to azure-pipelines-release.yml index 35571a9105a..4f37966f9f5 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines-release.yml @@ -2,108 +2,20 @@ trigger: batch: true - branches: - include: - - dev - - master tags: include: - '*' +pr: none variables: - name: versionBuilder value: '3.2' - - name: versionWheels - value: '0.7' - group: docker - - group: wheels - group: github - group: twine jobs: -- job: 'Wheels' - condition: or(eq(variables['Build.SourceBranchName'], 'dev'), eq(variables['Build.SourceBranchName'], 'master')) - timeoutInMinutes: 360 - pool: - vmImage: 'ubuntu-latest' - strategy: - maxParallel: 3 - matrix: - amd64: - buildArch: 'amd64' - i386: - buildArch: 'i386' - armhf: - buildArch: 'armhf' - armv7: - buildArch: 'armv7' - aarch64: - buildArch: 'aarch64' - steps: - - script: | - sudo apt-get update - sudo apt-get install -y --no-install-recommends \ - qemu-user-static \ - binfmt-support \ - curl - - sudo mount binfmt_misc -t binfmt_misc /proc/sys/fs/binfmt_misc - sudo update-binfmts --enable qemu-arm - sudo update-binfmts --enable qemu-aarch64 - displayName: 'Initial cross build' - - script: | - mkdir -p .ssh - echo -e "-----BEGIN RSA PRIVATE KEY-----\n$(wheelsSSH)\n-----END RSA PRIVATE KEY-----" >> .ssh/id_rsa - ssh-keyscan -H $(wheelsHost) >> .ssh/known_hosts - chmod 600 .ssh/* - displayName: 'Install ssh key' - - script: sudo docker pull homeassistant/$(buildArch)-wheels:$(versionWheels) - displayName: 'Install wheels builder' - - script: | - cp requirements_all.txt requirements_wheels.txt - if [ "$(Build.SourceBranchName)" == "dev" ]; then - curl -s -o requirements_diff.txt https://raw.githubusercontent.com/home-assistant/home-assistant/master/requirements_all.txt - else - touch requirements_diff.txt - fi - - requirement_files="requirements_wheels.txt requirements_diff.txt" - for requirement_file in ${requirement_files}; do - sed -i "s|# pytradfri|pytradfri|g" ${requirement_file} - sed -i "s|# pybluez|pybluez|g" ${requirement_file} - sed -i "s|# bluepy|bluepy|g" ${requirement_file} - sed -i "s|# beacontools|beacontools|g" ${requirement_file} - sed -i "s|# RPi.GPIO|RPi.GPIO|g" ${requirement_file} - sed -i "s|# raspihats|raspihats|g" ${requirement_file} - sed -i "s|# rpi-rf|rpi-rf|g" ${requirement_file} - sed -i "s|# blinkt|blinkt|g" ${requirement_file} - sed -i "s|# fritzconnection|fritzconnection|g" ${requirement_file} - sed -i "s|# pyuserinput|pyuserinput|g" ${requirement_file} - sed -i "s|# evdev|evdev|g" ${requirement_file} - sed -i "s|# smbus-cffi|smbus-cffi|g" ${requirement_file} - sed -i "s|# i2csense|i2csense|g" ${requirement_file} - sed -i "s|# python-eq3bt|python-eq3bt|g" ${requirement_file} - sed -i "s|# pycups|pycups|g" ${requirement_file} - sed -i "s|# homekit|homekit|g" ${requirement_file} - sed -i "s|# decora_wifi|decora_wifi|g" ${requirement_file} - sed -i "s|# decora|decora|g" ${requirement_file} - sed -i "s|# PySwitchbot|PySwitchbot|g" ${requirement_file} - sed -i "s|# pySwitchmate|pySwitchmate|g" ${requirement_file} - sed -i "s|# face_recognition|face_recognition|g" ${requirement_file} - done - displayName: 'Prepare requirements files for Hass.io' - - script: | - sudo docker run --rm -v $(pwd):/data:ro -v $(pwd)/.ssh:/root/.ssh:rw \ - homeassistant/$(buildArch)-wheels:$(versionWheels) \ - --apk "build-base;cmake;git;linux-headers;bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;autoconf;automake;cups-dev;linux-headers;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev" \ - --index $(wheelsIndex) \ - --requirement requirements_wheels.txt \ - --requirement-diff requirements_diff.txt \ - --upload rsync \ - --remote wheels@$(wheelsHost):/opt/wheels - displayName: 'Run wheels build' - - job: 'VersionValidate' condition: startsWith(variables['Build.SourceBranch'], 'refs/tags') @@ -157,7 +69,7 @@ jobs: - script: | export TWINE_USERNAME="$(twineUser)" export TWINE_PASSWORD="$(twinePassword)" - + twine upload dist/* --skip-existing displayName: 'Upload pypi' @@ -221,7 +133,6 @@ jobs: vmImage: 'ubuntu-latest' steps: - script: | - sudo apt-get update sudo apt-get install -y --no-install-recommends \ git jq curl From 8d3c9bc2d056962be1c72fa14ae70cd8df2441f8 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 5 Jun 2019 17:13:40 +0200 Subject: [PATCH 232/232] Don't let zeroconf be smart with addresses (#24321) --- homeassistant/components/zeroconf/__init__.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index bdb1d52159c..289aba6ef56 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -3,12 +3,14 @@ # https://github.com/PyCQA/pylint/issues/1931 # pylint: disable=no-name-in-module import logging +import socket import ipaddress import voluptuous as vol from zeroconf import ServiceBrowser, ServiceInfo, ServiceStateChange, Zeroconf +from homeassistant import util from homeassistant.const import (EVENT_HOMEASSISTANT_STOP, __version__) from homeassistant.generated.zeroconf import ZEROCONF, HOMEKIT @@ -42,8 +44,16 @@ def setup(hass, config): 'requires_api_password': True, } - info = ServiceInfo(ZEROCONF_TYPE, zeroconf_name, - port=hass.http.server_port, properties=params) + host_ip = util.get_local_ip() + + try: + host_ip_pton = socket.inet_pton(socket.AF_INET, host_ip) + except socket.error: + host_ip_pton = socket.inet_pton(socket.AF_INET6, host_ip) + + info = ServiceInfo(ZEROCONF_TYPE, zeroconf_name, None, + addresses=[host_ip_pton], port=hass.http.server_port, + properties=params) zeroconf = Zeroconf()